Table Of Contents

Previous topic

3. Events: mouse and keyboard

Next topic

5. Stay in world!

This Page

4. Animation

Your turn!

Complete the following code to make the circle move in the appropriate direction each time you press an arrow key:

clear_screen()

x = y = 200
dx = dy = 5
draw_circle(x, y, 10, 'red')

def move_circle(ev):
    global x, y
    if ev.keyCode == _: # which arrow?
        x -= dx
    if ev.keyCode == _: # which arrow?
        y -= dy
    if ev.keyCode == _: # which arrow?
        x += dx
    if ev.keyCode == _: # which arrow?
        y += dy
    if ev.keyCode == 81:  # q or Q
        doc.unbind("keydown")
    ev.preventDefault()

    draw_circle(x, y, 10, 'red')

doc.bind("keydown", move_circle)

One you have it working, you might want to see what happens if you press on two arrow keys at the same time.

Hint

The y coordinates increases from top to bottom. Also, there is an instruction missing before draw_circle.

4.1. Automatic animation

In the example above, to make the circle move, you needed to press an arrow key. However, it is possible to have the browser animate things by itself. If you look on the web, you will see that there are three Javascript functions to do that:

  1. setInterval
  2. setTimeout
  3. requestAnimationFrame

Furthermore, if you read any recent tutorial, you will see that the recommendation is to use requestAnimationFrame. While I agree it is a better function to use I will not use it for this tutorial. One of the reasons is that it is too good as it let the browser decides when is the most efficient time to draw things on the screen which, in turn, can hide some important concepts or make them more difficult to grasp. I will use instead setTimeout, or rather its Brython equivalent set_timeout. Here’s the code I ask you to use, presented in 4 blocks:

from browser.timer import set_timeout, clear_timeout

clear_screen()
dx = dy = 5

fps = 4          # frames per second
tbf = 1000/fps   # time between frames in ms
pause = True

We import not only set_timeout but also clear_timeout; we will see the relation between the two below.

Animations are done by showing a certain number of frames per second, usually abbreviated as fps. To have animations that look smooth to the eye, we should aim to have at least 24 if not 30 frames per second. The ideal situation is when we have one animation frame each time the monitor refreshes, which is usually 60 times per second. Here I chose to use only 4 fps so that we can perceive each frame individually.

The unit of time used by most functions is the millisecond abbreviated ms; there are 1000 ms in 1 second. The time between frames, in ms, is thus 1000 divided by the numbers of frames per second.

We then define a function to start the animation.

def start_animation():
    global x, y
    x = y = 10
    draw_circle(x, y, 10, 'red')
    update()

Note

id is the name of a Python function. This is why I use _id as a variable name so as to not confuse the two.

This function ends with a call to the function update:

def update():
    global x, y, _id
    x += dx
    y += dy
    clear_screen()
    draw_circle(x, y, 10, 'red')
    if pause:
        return
    _id = set_timeout(update, tbf)

The function update changes the values of x and y so that the circle is drawn elsewhere on the canvas and appears to move. The last line of this function is what makes animations possible: the function set_timeout instruct the browser to call the function given in its first argument, which is update here, at a time tbf (time between frames) in the future. It returns a unique number which the function clear_timeout can use to cancel the future request.

In a certain sense, having update call set_timeout which will call update is very much like recursion.

Finally, we define a callback function which will allow us to control what is being shown on the screen using our keyboard:

def animate(ev):
    global pause, _id
    if ev.keyCode == 80:  # p or P for Pause
        pause = True
        clear_timeout(_id)
    elif ev.keyCode == 81:  # q or Q  for Quit
        doc.unbind("keydown")
        pause = True
        clear_timeout(_id)
    elif ev.keyCode == 82 and pause: # r or R for Resume
        pause = False
        update()
    elif ev.keyCode == 83 and pause: # s or S for Start
        pause = False
        start_animation()
    ev.preventDefault()

doc.bind("keydown", animate)

Your turn!

Enter the code above in the browser, doing your best to understand what each line does before running the code. Then, run the code and confirm your understanding.

Important

Before you edit your code, if the animation is running, you must press q to stop it otherwise you will not be able to type code in the editor! This is a side effect of ev.preventDefault() that we included in the animate function which we bound to keydown events.

Experiment!

Here are some things you may want to try:

  1. Replace the line elif ev.keyCode == 83 and pause: by elif ev.keyCode == 83: and press on r a few times in a row; you should see multiple concurrent animations taking place.
  2. Change the value of fps and those of dx and dy.
  3. Add a couple more keyCodes and use them to change the size of the circle being drawn. You will need to introduce a variable like radius to do this, and use it instead of the number 10 as argument of draw_circle.