Title: Roto-Zoom Effect

Author: Gareth Noyce (g at korruptor.demon.co.uk)
Submission date: March 8, 2002

Description: Rotates, zooms and tiles a bitmap.

Download: roto_zoom-complete.zip

pygame version required: Any (with Surfarray)
SDL version required: Any
Python version required: Any (with Numeric)

Comments: More effect craziness from Gareth Noyce. This code performs an effect you've probably seen somewhere before - it tiles a bitmap across the window, then rotates it, and zooms in and out. The code works pixel-by-pixel to generate each successive frame; it would be interesting to contrast the speed of this effect with one based on pygame's own transform module.

Messages: 0

"""roto_zoom.py -- Korruptor Jan 2001, test code for Seal Basher (v1.0)

This is a direct port of Allegro source written by "Amarillion" (amarillion@yahoo.com)!
I did not 'design', 'write' or figure this out! :-) But I sure did learn alot by playing
with it...

I also borrowed Pete Shinners code for scaling and blitting a surfarray (as featured in
his fast flame effect) as I've only calculated an 1/8th of the screen estate for speed.

The following script (and function) is the old 'roto_zoom' demo effect as seen pretty much
everywhere in the mid-late 90s. Why do this when pygame has scale and rotate functions?
Well, it's useful to know some of the tricks of cos and sin (IMHO) and this is me trying
to learn as much about basic maths and coordinate systems as I can... I figure someone
else might benefit from this as well... (Although for most of the ninja coders on #pygame
this is probably blindingly obvious, at least I'm catching up tho!)

The function is fairly simple, copying pixels from a source bitmap to a larger destination 
bitmap, where the destination pixel's source is calculated via cos (for X) and sin (for y) --
these calculated values are used as incrementors for the position to be copied from a source
surfarray, and with this information (and some modulo arithmetic to keep the position in bounds) 
we can create a tiling, rotated and scaled bitmap that will fit a much larger area.

The main function handles incrementing the angle and scale factor, and repeatedly 
calls the "roto_zoom" function with these new values to provide the animation...

And that as they say, is about that. 

As you can see from this example, choice of source bitmap is fairly important. Monty doesn't
really scale too well...

import pygame, pygame.transform, pygame.image
from operator import *
from pygame.surfarray import *
from pygame.locals import *
from Numeric import *

# ------------------------------------------------------------------------------------
# Glob decs

# Screen resolution...
RES 	= array((640,480))

# ------------------------------------------------------------------------------------
def main():
    "Inisalises display, precalculates the cosine values, and controls the update loop"
    # Initialise pygame, and grab an 8bit display.
    screen_surface = pygame.display.set_mode(RES, 0, 8)

    # Load a source image, and convert it to a surfarray...
    image = pygame.image.load("monty.gif")
    image_buff = pygame.surfarray.array2d(image)

    # Numeric array (working) for the display. Do all our fun stuff here...
    rot_buffer  = zeros(RES/8)
    # Pygame Surface object which will take the surfarray data and be translated into a screen blit... 
    rot_surface = pygame.Surface((RES[0]/8, RES[1]/8), 0, 8)
    # apply the same palette to plasma_surface

    # Setup our parameters for calling the roto_zoom function...
    angle = 0.1
    scale = 2
    surface_x = RES[0]/8
    surface_y = RES[1]/8
    image_x = image.get_width()
    image_y = image.get_height()

    # Angle incrementor
    inc = 0.1

    # Zoom up or down...
    dir = 0

    # Fruity loops...
    while 1:
        # Have we received an event to close the window?
        for e in pygame.event.get():
            if e.type in (QUIT,KEYDOWN,MOUSEBUTTONDOWN):

	# Call the roto_zoom function, blit a scaled surfarray to the screen and display it..            
	# We mod the angle to keep it in bounds...
	rot_buffer = roto_zoom(rot_buffer, image_buff, mod(angle,360), scale, surface_x, surface_y, image_x, image_y) 
	blit_scaled_surface(screen_surface, rot_buffer, rot_surface)

        # Increment the angle for the next iteration
    	angle += inc

        # Check the scale is in bounds, and increment or decrement accordingly...
	    scale -= 0.1
	    if(scale <= 0):
		dir = 0
	    scale += 0.1
            if(scale >= 4):
            	dir = 1
# ------------------------------------------------------------------------------------
def roto_zoom(dest, src, angle, scale, x_size, y_size, x_mask, y_mask):
    """ Function to rotate and scale a supplied surfarray, filling the screen with the results..."""
    # Our initial positions in the source surfarray
    src_x = 0.0
    src_y = 0.0

    # Our initial positions in the destination surfarray
    dest_x = 0
    dest_y = 0

    # The "delta" values for incrementing the position of src_x & src_y
    dx = 0.0
    dy = 0.0

    # Src_x & src_y will be initialised to start_x and start_y at the beginning of each newline
    start_x = 0.0
    start_y = 0.0

    # Calculate the increments we need for the source bitmap everytime we move right a pixel in the destination
    dx = cos(angle) * scale
    dy = sin(angle) * scale

    # For each row in the destination
    for dest_y in range(0,y_size):
        # Set the position in the source to the start of this line
	src_x = start_x
	src_y = start_y
	# For each pixel in the row...
	for dest_x in range(0,x_size):
		# copy a pixel from the source to the desitination
		# We mod the src_x/y vars to keep them in bounds of the source image...
		dest[dest_x][dest_y] = src[mod(int(src_x),x_mask)][mod(int(src_y),y_mask)]

		# Increment the source positions by our calculated increments...
		src_x += dx
		src_y += dy

	# Change the starting positions for the next line (this is a dot product I think...)
    	start_x -= dy
   	start_y += dx

    # Return the surfarray for blitting...
    return dest

# ------------------------------------------------------------------------------------
def blit_scaled_surface(screen, flame, miniflame):
    "double the size of the data, and blit to screen -- Nicked from Shread's Fast Flame"
    blit_array(miniflame, flame)
    s2 = pygame.transform.scale(miniflame, screen.get_size())
    screen.blit(s2, (0,0))

# Shit captain! In the void of space we're all alone...
# Don't worry ensign. I have a plan: Fire our trippy laser!
if __name__ == '__main__': main()

# End of sauce. Pass the chips...

Main - Repository - Submit - News