Creating animations with the Canvas API

Creating animations with the Canvas API

And throttling requestAnimationFrame()

·

6 min read

According to the Oxford Dictionary, an animation is defined as the following:

The technique of photographing successive drawings or positions of puppets or models to create an illusion of movement when the film is shown as a sequence.

Translating that into how we are going to draw animations with the canvas means we will use the Canvas API to draw a successive series of frames. Each frame will need to include all the shapes, lines, images and rectangles of our animation. This must then be followed by completely clearing the canvas and drawing the next frame with the same set of objects potentially in slightly different positions.

In the example below, we have a Particle class which will draw a small circle at a start position when Particle.draw() is called. When we are dealing with animations, we will also need an update() method which can be used to modify the position of the Particle between frames.

If we look at index.js, we implement a basic animate() function which will be responsible for drawing each of our new frames. We can call Particle.update() from each frame which will update the position of the Particle in each frame and redraw based on its new position.

If you click PLAY, currently nothing will happen as we have not yet called the animate() function. We will do that by using a technique for automatically calling our animate() function with each new frame. To start with we can use a naive approach using the window.setInterval() method.

Uncomment the window.setInterval() method. We should be able to see the particle moving across the screen.

The first thing that we notice is that the particle, although moving across the screen as intended is also showing every single one of its previous positions. This is because the animate() function is only calling particle.update() which is simply drawing the particle at a new position.

If we uncomment the drawSpace() function call in the animate() function we should see that the particle behaves more like a proper animation now. What we have effectively done is make sure that each frame we are first redrawing the background space and then drawing our particle at the new position. This brings us an important learning of creating animations with the canvas:

Each frame will need to redraw all shapes whether they are stationary or moving.

Calling the drawSpace() function within the animate() function will make sure that we clear the canvas and start with a stationary black background. This could just as easily be an image or a collection of stationary objects. Note that if we had used clearRect() to clear the canvas, we could also solve the problem of the particle's previous positions still being shown. However, clearing the canvas means that the canvas is now transparent. In some situations this might actually be beneficial as we could instead place an <img> element directly behind the canvas and render this once and use this as our background. There could be some performance or memory benefits to doing this, especially if the image is a static high-definition image. For the moment, we won't consider the need to consider this kind of optimization as there are definitely pros and cons to this.

Try adjusting the delay of setInterval() and/or the velocity of the particle to see if you can create a more smooth animation.

Understanding requestAnimationFrame()

In our current implementation, we have used window.setInterval() to call our animate function at specific time intervals. For simple animations like a single particle moving across the screen, this naive approach won't be an issue. However, as an animation becomes more complicated an animation might suffer from problems with shear, flicker and frameskip. These are caused as a result of the canvas being passed to the display paint scan at out-of-step intervals causing pixels to be drawn in the wrong place, missing or even missing an entire frame.

In most circumstances, assuming that the canvas draw commands don't cause any performance issues, we can mitigate these display issues by using requestAnimationFrame().

The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation before the next repaint. The method takes a callback as an argument to be invoked before the repaint.

So by using requestAnimationFrame() we can be more confident about our canvas animation frames being drawn in sync with the display.

If we look at the updated animate() function in the lab below, the first thing we do is call our requestAnimationFrame(animate). We must pass the callback function which we want to be called in sync with the display. In our example, we want it to once again call the animate() function. This is a single-shot function which means that if we want our animation to keep running we will need to call it each time we run the animate function.

If you press PLAY on the code lab below you can see that the animation is now running, but running much faster than it was previously. This is because requestAnimationFrame()will be called each time the display does a paint which on most monitors will be at least 60fps or approximately every 16.666ms which is much faster than the original window.setInterval() solution that ran the animate() function every 100ms.

In order to prevent our animation from running faster than we intended, we need to make sure that each time after the animation function runs enough time has passed before we proceed to run the other functions in our animation. We can do this using a function that checks how much time has passed between frames. We have constructed a function that will check the frame rate called checkFrameRate() in utils.js . This function uses fps (frames per second) to check whether enough time has passed since the previous permitted call.

The original example was using setInterval() with a delay of 100ms, which is only 6 frames per second which is extremely slow. If we want to achieve the same behaviour as the last example then we can uncomment the ``if (!checkFrame()... code and set{fps: 6}`. we should see that the speed is approximately the same as before. Different kinds of animations or games might require different frame rates, but with most modern devices and screens, a good rule of thumb might be to aim for over 60 frames per second.

Note that we have exported checkFrame instead of checkFrameRate so that we can benefit from JavaScript closures which enables the prev value to be closed within the scope and there is no need to manage this variable outside the scope of this function. (See this article on Closures for more on this)

Try adjusting the frames per second to see how they impact the animation.