(Un-)Stretch an image to all four sides

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""(Un-)Stretch an image to all four sides.

A basic 2D animation effect.

Requires Pygame_ and Numeric_.  That the latter is no longer actively
developed, but Pygame doesn't support the newer NumPy yet.  If you are using
Windows and Python 2.5, you might want to try out VPython_.  Its installer for
that constellation includes Numeric, but a separate Numeric Windows installer
seems not to be available for Python 2.5.

.. _Pygame:     http://www.pygame.org/
.. _Numeric:    http://numpy.scipy.org/
.. _VPython:    http://www.vpython.org/

Known issues
------------

- The image moves a little after the first half of an animation when stretching
  from the left or the top.

:Copyright: 2007 Jochen Kupperschmidt
:Date: 21-Sep-2007
:License: MIT
"""

from optparse import OptionParser
from sys import exit
from time import sleep

import Numeric as num
import pygame
from pygame import surfarray


class Stretcher(object):
    """(Un-)Stretch an image."""

    # The pause between each animation step.
    interval = 0.0025

    def __init__(self, screen, image):
        self.screen = screen
        self.image_array = num.array(surfarray.array2d(image))

        # ``xrange`` objects don't need to be recreated since iterating over
        # them starts at the beginning like lists, and unlike generators.
        self.width_range = xrange(image.get_rect().width)
        self.height_range = xrange(image.get_rect().height)

        self.clock = pygame.time.Clock()

    def get_new_image_array(self):
        return self.image_array.copy()

    def display(self, img_arr):
        self.clock.tick(100)
        check_events()
        surfarray.blit_array(self.screen, img_arr)
        pygame.display.update()
        sleep(self.interval)


    # horizontal animation

    def _from_left(self, range_):
        img_arr = self.get_new_image_array()
        for border in range_:
            img_arr[:border] = self.image_array[border]
            self.display(img_arr)

    def from_left(self):
        # Fade in and out.
        self._from_left(reversed(self.width_range))
        self._from_left(self.width_range)

    def _from_right(self, range_):
        img_arr = self.get_new_image_array()
        for border in range_:
            img_arr[border:] = self.image_array[border]
            self.display(img_arr)

    def from_right(self):
        # Fade in and out.
        self._from_right(self.width_range)
        self._from_right(reversed(self.width_range))


    # vertical animation

    def _from_top(self, outer, inner):
        img_arr = self.get_new_image_array()
        for border in outer:
            for col in inner:
                img_arr[col][:border] = self.image_array[col][border]
            self.display(img_arr)

    def from_top(self):
        # Fade in and out.
        self._from_top(reversed(self.height_range), self.width_range)
        self._from_top(self.height_range, self.width_range)

    def _from_bottom(self, outer, inner):
        img_arr = self.get_new_image_array()
        for border in outer:
            for col in inner:
                img_arr[col][border:] = self.image_array[col][border]
            self.display(img_arr)

    def from_bottom(self):
        # Fade in and out.
        self._from_bottom(self.height_range, self.width_range)
        self._from_bottom(reversed(self.height_range), self.width_range)


def check_events():
    """Process events and react on exit triggers."""
    for event in pygame.event.get():
        if (event.type == pygame.QUIT) or \
            ((event.type == pygame.KEYDOWN)
                and (event.key == pygame.K_ESCAPE)):
            exit()


if __name__ == '__main__':
    # Take image filename as sole argument.
    parser = OptionParser(usage='%prog <image filename>')
    args = parser.parse_args()[1]
    if len(args) != 1:
        parser.print_help()
        parser.exit()
    filename = args[0]

    pygame.init()
    image = pygame.image.load(filename)

    # Prepare display.
    screen = pygame.display.set_mode(image.get_rect().size)
    screen.fill((0, 0, 0))

    # Prepare stretcher and stretch forwards and backwards in all directions.
    stretcher = Stretcher(screen, image.convert())
    for method_name in ('from_left', 'from_right', 'from_top', 'from_bottom'):
        getattr(stretcher, method_name)()
        sleep(0.1)