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 (see ball.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 the while 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 the animate function is getting called by the after 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, where RRBBGG are in base 16, or hexadecimal. The fancy f-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 the draw() 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, and other 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.