More on Classes, and Animation
Today, we are going to continue to look at Python classes, and we will look at writing class functions, and animation in particular.
Slide 2
What is animation?
Let's start by talking about what animation is as it refers to computer graphics. Put most simply, it is moving objects around the screen in a continuous loop.
Computer animation has been tremendously successful since the late 1990s. In fact, Stanford Professor Pat Hanrahan was instrumental in writing software for the animation studio Pixar, and he won computing's higest award, the Turing Award for his pioneering work in computer graphics and animation.
For our Bouncing Ball example, we have created a Ball object, and we will run a loop that moves all the balls on the screen around, bouncing them off the "walls" of the screen.
Slide 3
Animation in Python using Tkinter
Let's take a look at some of the lower level workings of our Ball example's animation. We are relying on the Tkinter library for the on-screen effects, and we have used Tkinter before for other graphics.
Here is the main()
function:
def main():
num_balls = 1
random_loc = True
collide = False
if len(sys.argv) > 1:
num_balls = int(sys.argv[1])
if '--center' in sys.argv:
random_loc = False
if '--collide' in sys.argv:
collide = True
(canvas, tk) = make_canvas(CANVAS_WIDTH, CANVAS_HEIGHT, "Bouncing Ball")
ball_list = [Ball(canvas, random_loc=random_loc) for b in
range(num_balls)]
animate(canvas, ball_list, collide)
tk.mainloop()
Some things to note:
- We are allowing some parameters: the number of balls, whether we want the balls all starting from the center (the default is random), and whether we want them to collide against other balls.
- There is a
make_canvas()
function that does some Tkinter setup (seeball.py
for that function). - We create all of the balls in main in a list (using a list comprehension).
- We call the
animate
function - We call Tkinter's
mainloop()
function.- Because animation is a looping process, there has to be a loop of some sort.
- We could use a
while True:
loop (perhaps inside of animate), but if there were any other graphical elements on the screen, they would never work because we were stuck inside thewhile True:
loop. - Python can use what we call multithreading, which means that two or more parts of your program can run simultaneously (or at least, seemingly simultaneously). You will learn all about multithreading when you get to CS111.
- By using
tk.mainloop()
, we let Tkinter handle the multithreading and looping.
Slide 4
The animate() function
Here is the animate()
function:
def animate(canvas, ball_list, collide):
if len(ball_list) <= 100:
time.sleep(0.01 - 0.0001 * len(ball_list))
for ball in ball_list:
ball.update()
if collide:
for ball in ball_list:
for other in ball_list:
if ball == other:
continue
ball.handle_collision(other)
canvas.after(1, lambda: animate(canvas, ball_list, collide))
Notes:
- The animation is pretty fast for a small number of balls, so we add a bit of code to slow things down in that case.
- The
animate()
function simply updates all the balls in the list, and calls the collision routine for all the balls with each other if required. - The last line of the function tells Tkinter to run the animation again after 1 millisecond. Notice the lambda function! We want that function to be called with all of its parameters. It turns out we didn't strictly need the lambda – we could have written the line like this:
canvas.after(1, animate, canvas, ball_list, collide)
The
after
function expects a function as the second parameter, and then it passes the rest of the parameters into that function when it calls it. I prefer the lambda function because it is clear that theanimate
function is getting called by theafter
function.
Slide 5
More on the Ball class
Let's go through the Ball class one method at a time.
class Ball:
"""
The Ball class defines a "ball" that can bounce around the screen
"""
def __init__(self, canvas, x=-1, y=-1, random_loc=False, fill=None):
self.canvas = canvas
self.canvas_object = None
if random_loc:
self.x = random.randint(0, canvas.winfo_width() - 1)
self.y = random.randint(0, canvas.winfo_height() - 1)
else:
if x == -1:
self.x = canvas.winfo_width() / 2
else:
self.x = x
if y == -1:
self.y = canvas.winfo_height() / 2
else:
self.y = y
self.width = 30
self.height = 30
self.direction = random.random() * 360 / (2 * math.pi)
if fill:
self.fill = fill
else:
self.fill = f'#{random.randint(0, 16777215):06x}'
self.outline = self.fill
self.draw()
The init()
function does the setup for the class.
- The
self.canvas_object
keeps track of the circle on the canvas so we can remove and replace it when we change the position of the ball. - This line determines the direction of the ball, in radians (e.g., between 0 and 360º):
self.direction = random.random() * 360 / (2 * math.pi)
- This line randomly sets the color. A color needs to be formatted as a string,
#RRBBGG
, whereRRBBGG
are in base 16, or hexadecimal. The fancyf
-string accomplishes this by formatting the random number with six digits in hexadecimal.self.fill = f'#{random.randint(0, 16777215):06x}'
- The
init
method calls thedraw()
method to actually draw the ball on the canvas:
def draw(self):
self.canvas_object = self.canvas.create_oval(self.x, self.y, self.x +
self.width, self.y + self.height,
outline=self.outline,
fill=self.fill)
Slide 6
The update() method
The real heart of the class is the update()
method, which moves the ball by some amount in the ball's direction (in our case, we multiply both the x and y directions by 10, and add that to the current x and y coordinates of the ball). We need a bit of trigonometry for this, but only sin()
and cos()
(and they work on radians, which is why we calculated our direction in radians).
def update(self):
x_dir = math.cos(self.direction)
y_dir = math.sin(self.direction)
self.x += x_dir * 10
self.y += y_dir * 10
if self.x < 0:
self.x = 0
x_dir *= -1
if self.x > self.canvas.winfo_width() - self.width:
self.x = self.canvas.winfo_width() - self.width
x_dir *= -1
if self.y < 0:
self.y = 0
y_dir *= -1
if self.y > self.canvas.winfo_height() - self.height:
self.y = self.canvas.winfo_height() - self.height
y_dir *= -1
self.direction = math.atan2(y_dir, x_dir)
self.canvas.delete(self.canvas_object)
self.draw()
- Once we have the x- and y- directions, we simply multiply them by
- All of the
if
logic in this method is simply to bounce the ball off the walls. If either the x or y position is outside the wall, the direction is reversed. Clever! - We do need one further trigonometric function,
atan2
("two-argument arctangent"). As long as x and y are positive, this correctly converts the x and y directions back into a single direction, in radians.
Slide 7
Collisions
How do we handle collisions for the balls? We simply check to see if the balls are within one diameter of each other, using the pythagorean distance.
def collision(self, other_ball):
return(math.sqrt((other_ball.x - self.x)**2 +
(other_ball.y - self.y)**2)) < self.width
def handle_collision(self, other_ball):
if self.collision(other_ball):
tmp = other_ball.direction
other_ball.direction = self.direction # elastic collision?
other_ball.update()
self.direction = tmp
self.update()
- We perform a very simple collision between the balls – if there is any collision (overlap), the balls simply trade directions. This is the particle model of collisions, and not necessarily how real balls (such as soccer balls) would behave. We could model it more realistically if we wanted to.
- Undersanding the use of
self
in both functions above is important:self
is the ball whose function is called, andother
is the ball that is passed in as a parameter.
Slide 8
Final Thoughts
Animation is fun! It's a little tricky to get right, and every programming language will have some library that handles animation differently than others. But, it is worth learning the library if you want to do interesting animations.
We have now seen a decent-sized class (Ball
) that demonstrates the big ideas in Python classes.