Animation in Python

Making graphical objects change position is simple, but making them seem to move is more difficult. Animation is something of an optical illusion; images are drawn in succession so quickly that the human eye cannot detect that they are distinct images. Small changes in position in a sequence of these images are seen as motion rather than as a set of still pictures. A typical animation draws a new image (frame) between 24 and 30 times per second to make the illusion work.

There are two kinds of animation that can be done using Pygame. The first involves objects that consist of primitives that are drawn by the library. A circle can represent a ball, for instance, of a set of rectangles and curves could be a car. The second kind of animation uses images, where each image is one frame in the sequence. These images are displayed entirely in rapid succession to create the animation. In the first case, the animation is being created as the program executes, whereas in the second, the animation is complete before the program runs, and the program really just puts it on the screen.

1. Object Animation

Animating an object involves updating its position, speed, and orientation at small time intervals, so all of these aspects of the object must be kept in variables. If there are many objects being animated, then all of these variables must exist for each object, and are updated at the end of each time interval. If the anima­tion is displaying 30 frames per second then a new frame is drawn every 0.03 seconds. In Pygame, we control the frame rate using clock.tick(FPS), which does not return until a duration of 1/FPS seconds has passed. We control the rate by changing the value of FPS.

Example: A Ball in a Box

Imagine a ball bouncing in a square box. A box has three dimensions, of course, but for this example, it is restricted to two, so it looks like a circle within a square. The ball is moving, and when it strikes one of the sides of the square, it bounces, thus changing direction. There is one moving object: the ball. Graphi­cally, it is simply a circle, with position x,y and speed dx in the x direction and dy in the y direction. It will have size 30 pixels. The box is the window the circle is drawn in.

During each frame, the ball moves dx pixels in the x direction and dy pixels in the y direction, so within the draw() function the position is updated as

x = x + dx

y = y + dy

This new position is where to draw the circle. However, if the ball is outside of the box after it is moved, then a bounce has to be performed. That is, if the new position of x is, for instance, less than 0, then it would have struck the left side of the square and then changed to the x direction (bounced). In this case, and also if x>width, the bounce is implemented by

dx = -dx

Similarly, if the y coordinate of the ball becomes less than 0 or greater than the height, then it bounces vertically:

dy = -dy

This would all be true if the ball were very tiny, a single point, but it has a size of 30 pixels, and the coordinates of the circle are the coordinates of its center. This means that the method described above will bounce the circle only after the center coordinate passes the boundary, meaning that half of the circle is already on the other side. It’s easy to fix: the ball is 30 pixels in size, so it should bounce when it gets within 15 pixels of any boundary. For example, the x bounce should occur when x<=15 or x>=width-15. The entire solution is as follows:

def draw ():

global dx, dy, x, y

screen.fill( (200, 200, 200)  )  # Erase the prior frame

x = x + dx                       # Change ball position

y = y + dy

if x<=15 or x>=width-15:         # Bounce in X direction?

dx = -dx

if y<=15 or y>=height-15:        # Bounce in Y direction?

dy = -dy

pygame.draw.circle(screen, fill, (x, y),   15)

                                 # Draw the ball

FPS = 30 while True:

clock.tick(FPS)

mouseX, mouseY = pygame.mouse.get pos()

for event in pygame.event.get():

if event.type == pygame.QUIT:

quit()

draw()

pygame.display.update()

Eight frames from this animation showing the ball bouncing in a corner of the box are shown in Figure 9.2. An entire second’s worth of frames (30) are given on the accompanying disk.

If there are many objects, then all of the positions and speeds, and perhaps even shape, size, and color would have to be kept and updated during each frame. There are two usual ways to do this. In the first case, the parameters are kept in arrays (lists). There would be an array of x coordinates, an array of y coordinates, of speeds, and so on. Each frame could involve an update to all elements of the arrays. Updating the position can be done using the following code:

for i in range(0,Nobjects):

x[i] = x[i] + dx[i]

y[i] = y[i] + dy[i]

The other usual method for handling multiple objects is to create an object class that contains all of the parameters needed to display the object. There is still an array, but it is an array of object instances, and if it is cleverly programmed the class can be updated by calling an update() method:

for i in range(0,Nobjects):

ball[i].update()

Example: Many Balls in a Box

This example uses the same premise as the previous one, but will draw many balls in the window, all of them bouncing. Both methods for keeping track of objects, arrays, and classes, are illustrated. The many arrays solution has lists for x and y, for dx and dy, for color and for size. All parameters are initialized at random when the program begins.

The solution that uses classes defines a class ball within which the position, speed, color, and size are defined. The constructor initializes the values and the update method changes the ball’s position and performs any needed bounces. The two solutions are as follows:

These two solutions illustrate how classes work very neatly. The class con­tains individual properties of a ball and many are created; the arrays contain many instances of each property. So x[i] and ball[i].x represent the same thing. In this case, the two programs are about the same size, but the class-based implementation encapsulates the details of the ball and what can be done with it. The class-based draw() function only says “draw each ball,” but in the ar­ray implementation, the draw() function looks at all of the details of all balls to draw them. One of the implications is that it would be possible to divide the labor between two persons, one who wrote the class and another who wrote the rest of the code.

2. Frame Animation

The hard work in frame animation is done before the computer program is written. An animator has created drawings of an object in various stages of move­ment. All the program does is display frames one after the other, often looping them to create the desired effect. A common example of this is the animation of gait, walking or running. An artist draws multiple stages of a single step, being careful to ensure that timing is correct: how long does it take for a normal person to stake a pair of steps (left, right)? This time should agree with the frames the artist creates. If it takes one second to make the step, then it should be drawn as 30 frames.

Other kinds of animation are performed, too. A fire can be animated as a very few frames, as can smoke and water. The program that draws the animation reads all of the image files into a collection. When the animation is played, the program displays one image after another within the draw function. This can be complicated by the fact that there may be multiple animations playing at the same time, possibly of different lengths and frame sizes.

Example: Read Frames and Play Them Back as an Animation.

In this example, there are 10 drawn animation frames of a cartoon character walking. These frames are intended to represent a single gait cycle, and so should be repeated. The program does the following: when the “up arrow” key is pressed and held down, the character drawn in the window “walks;” otherwise, a still image is displayed.

First, the images should be read in and stored in a list so that they can be played repeatedly. Then the draw function should be written so that when called it displays the next frame, which is one of the images in the list, and increments the frame count. A list named frames is initialized with all of the images in the sequence.

def draw ():

global f

screen.blit (frames[f], (0,  0))

f = f + 1

if (f > 10):

f = 1

It cycles through the frames and repeats when all have been displayed.

The initialization can be a simple matter of reading ten images into variables and creating a list. This code does it in a loop, using a number in the name and incrementing it:

frames = []

for i in range (1,     10):

s = “images/a00″+str(i) +”.bmp”

x = pygame.image.load (s)

frames = frames + [x,]

x = pygame.image.load (“images/a010.bmp”)

frames = frames + [x,]

x = pygame.image.load (“images/a011.bmp”)

frames = frames + [x,]

The variable frames is a list holding all of the images, and frames[i] is the ith image in the sequence.

The building of the file name is interesting. It is common to use numbered names for animation frames (for example, frame01 or frame02). In this case, the sequence is a***.bmp, where the *** represents a three-digit number. If the vari­able i is an integer, then str(i) is a string containing that integer, but the leading zeros are not present. Thus, for values of i between 0 and 9 (one digit) the string will be “a00”+str(i)+”.bmp”; for values of i between 10 and 99 (two digits), the string is “a0”+str(i)++”.bmp”; finally, for numbers between 100 and 999, the string will be “a”+str(i)+”.bmp” (three digits). The leading zeros are manually inserted into the string.

The animation frames for the gait sequence are on the disk along with this code.

Example: Simulation of the Space Shuttle Control Console (A Class That Will Draw an Animation at a Specific Location)

Animations can sometimes be used to decorate a scene in interesting ways. A control panel showing video screens and data displays could use animations to fill the screens, giving the illusion of real things being monitored. A class that can play a frame-by-frame animation at any location on the screen could be instanti­ated many times, once for each display.

The class would have to read the frames it was to play and store them, play back the frames in a loop when requested, and place them within the window at any location. None of these tasks is especially hard. Code for reading frames from a file was written for the previous example, as was code for displaying the frames. Each class instance would need a frame count so that the loop could start over at the right place, and each class instance could have an animation with a different number of frames. Finally, placing at the right location is a mat­ter of passing the correct parameters to the image() function. The class would be instantiated given the position as x and y coordinates of the upper left corner.

Sometimes, especially when multiple animations are playing, it will be nec­essary to slow down some animations so that they look right. The code calls draw() a fixed number of times each second, but that may not always be the cor­rect speed for an animation. A count can be introduced so that the fame advances to the next only when a count exceeds a fixed delay value. If the count is 2, for example, then 2 calls to draw() are required before a new frame is chosen, mean­ing that the frame rate has been decreased by 50%.

This example implements a simulation of a space shuttle control console. This is a visual simulation, not one that allows interaction at any level, and we insert animations into a still photo of a real shuttle console and make it look more active. Figure 4a shows the static image that is used. There are many video screens visible, and the program being developed will replace the still image on some of those screens with moving, animated images.

Three of the screens are selected for animation. The image was displayed using Paint and the coordinates of the upper left corner of each of these screens was determined, as were the sizes. Figure 9.4b shows the location of these regions on the image.

The code for the class starts like this:

class Anim:

def init (self, x, y):

self.frames = []

self.xpos = x

self.ypos = y

self.n = 0

self.f = 0

self.active = False

self.delay = 1

self.count = 100000

def draw (self):

if self.active:

screen.blit (self.frames[self.f],

(self.xpos, self.ypos))

self.count = self.count + 1

if self.count >= self.delay:

self.f = self.f + 1

self.count = 0

if (self.f >= self.n): self.f = 0

The part of the class that reads the frames as images is taken from the previ­ous example:

def getframes (self, s1, s2):

self.frames = []

for i in range (0,100):

if i<10:

s = si + “0”+str(i)  + s2

print (“Reading “, s)

elif i<100:

s = s1 + str(i) + s2

try:

x = pygame.image.load (s) except:

self.n = i

print (“Saw “, self.n, ” frames.”) break

self.frames = self.frames + [x,]

There is a flag named active that determines whether the animations are cur­rently running. The methods start() and stop() turn the animation on and off by toggling this variable.

def start(self):

self.active = True

def stop (self):

self.active = False

Finally, for this class, the delay can be set using a call to the setdelay() meth­od, which simply changes the value of a class local variable delay.

def setdelay (self, d):

self.delay = d

The draw() method of the program simply draws the animations by calling their respective draw() methods:

def draw ():

a.draw()

b.draw()

c.draw()

The main program opens the window and loads and draws the background image:

background = pygame.image.load (“images/800px-STSCPanel.jpg”)

screen.blit (background, (0,0))

The first animation, at x=239 and y=284, shows some television static, seven frames of which were created for this purpose using another program. A class instance is created to draw at (239,284) and getFrames() is called to load the im­ages (the file names are “g100.gif” through “g106.gif”):

a = Anim(239, 284)

a.getframes (“images/g1”, “.gif”)

The second animation is at x=319 and y=258 and will display some exterior shots of the space shuttle. The process is the same as before, but the file names are “g200.jpg” through “g204.jpg.” In addition, a delay of 100 is set, because these images are to be displayed for multiple seconds each to simulate a display scan­ning a set of cameras:

b = Anim (319, 258)

b.getframes (“images/g2”, “.jpg”)

b.setdelay(100)

Finally, the third animation, at x=319 and y=322, consists of a computer dis­play showing Python code (this class, in fact). It was created by another program and consists of nine frames named “g300.gif” through “g308.gif” This anima­tion is delayed a little as well so that it appears as if the text is scrolling properly:

c = Anim (319, 322)

c.getframes (“images/g3”, “.gif”)

c.setdelay(10)

The last step in the program is to start all of the animations playing:

a.start()

b.start()

c.start()

The example is complete on the disk and needs to be executed with the im­ages directory, which contains the animation frames.

Source: Parker James R. (2021), Python: An Introduction to Programming, Mercury Learning and Information; Second edition.

Leave a Reply

Your email address will not be published. Required fields are marked *