The 2D Canvas API is a fantastic tool for implementing animations of all kinds. With a few tricks, you can write animations for the three big output formats: the web, GIFs, and video. Animation is flashy and fun, but it’s not magic. Let’s begin.
If you’re familiar with Canvas and just want animation protips, feel free to skip ahead.
Read Dive into Canvas for the first steps of interacting with a Canvas context. In a typical animation, I’ll use only a few methods, like
beginPath, and others. The MDN reference for
CanvasRenderingContext2D is a good handbook to have at your side when you start interacting with these APIs.
The Canvas API provides the means to draw several fundamental types:
bezierCurveTo, etc: lines
The Canvas API is a fundamentally raster way to interact with pixels: you can even interact with your drawings as pixels, using the
createImageData methods. If you want to handle high pixel densities in Canvas, you have to do it yourself by doubling the image size and halving its displayed size, like you would with an image tag.
Canvas vs SVG
Canvas is a very different system than SVG. SVG is an example of a Retained mode interface, and Canvas is Immediate mode. SVG remembers everything you draw: if you add a circle, then you can bind events to that circle, move it, or remove it. Canvas, on the other hand, takes your command to “draw a circle”, changes the pixels to show a circle, and immediately forgets about what is drawn. This can be an advantage: if you draw millions of circles into a Canvas, the millionth draws as quickly as the first.
Drawing in Canvas is typically fast, but if you’re drawing many things or in realtime, a little optimization can go a long way. These are a few assorted performance tricks that have a good return on investment.
Change style infrequently
Canvas keeps the shapes you draw separate from the style you use to draw it: the style is represented in properties like
context.font. Changing these properties and drawing with different styles is very expensive relative to drawing many things with the same style.
Batch draw operations
For an operation like
rect, the Canvas API has two methods:
rect method doesn’t immediately draw anything: it records that you want to draw a rectangle in a certain place, and then when you draw
stroke, it draws that thing.
If you’re drawing lots of lines, rectangles, or other shapes, it makes a big performance difference to call the
rect method a lot of times and
fill once you’re done, rather than calling
fillRect every time.
Whole Pixel Coordinates
Canvas can draw to sub-pixel coordinates, meaning that it will anti-alias a rectangle that goes halfway into the next pixel so that it looks like it’s a little more on the border than you think. But rarely is this what you want, and it will burn you on performance.
I use the
~~ trick to quickly round any values I use to draw on a Canvas: it’s a hacky shorthand for Math.floor
Okay, let’s start animating. Animations are images drawn differently as time moves. The parts of an animation are frames. Drawing animations in Canvas is like drawing anything else in Canvas, except you change what you draw over time. Let’s start with an example of a moving dot.
The key things to notice that makes this different than a normal Canvas drawing are:
- We use
canvas.width = canvas.width;to clear the canvas before drawing each frame. This looks nonsensical at first, but what it’s doing is resetting the canvas by assigning it a new width.
- We use
requestAnimationFrameto schedule when animations are drawn. requestAnimationFrame is more efficient than setInterval or setTimeout. It won’t run when the tab is hidden, and is specifically geared toward animations, so it won’t draw an image more than once per frame.
Check the time
An important takeaway from using requestAnimationFrame instead of
setTimeout is that you should use time as your guide for the speed of animations: the key is to transform what
+new Date() give you into a value that tells you what state the animation should be in. Using time to control animations ensures that the animation will proceed at the same speed regardless of how efficient or inefficient the computer displaying it is: it may run at 10 frames per second, but the circle does not move more slowly.
In this case, I used
Math.sin to turn an increasing number of milliseconds into an oscillating wave that moves the circle. If you wanted the circle to move from one side to the other and then pop back, you would use the modulus operator,
%, like this:
The blur trick
We were using
canvas.width = canvas.width; to clear the previously-drawn frame before drawing a new one. That’s one of many ways: another common method for wiping the drawing slate clean every frame is to draw a background color before drawing each frame. You can tweak this technique a little bit and draw an alpha-transparent background color before each frame, and bits of each frame will appear as ghostly motion blurs. Let’s demonstrate that, with a little more interesting movement.
The only lines that differ between these examples and the previous ones are:
So far I’ve shown examples with Canvas in the browser. That’s definitely the majority of what you’ll see around: browsers are the birthplace and natural environment for the canvas element. But in practice, less than half of the animations I make in Canvas happen in browsers. Meet
node-canvas is a node module that exposes an API that matches the Canvas API in browsers. Why would you want this? Simple: node can read from big files and write images to files.
node-canvas makes it possible to do things like turning a gigabyte-sized data file into thousands of image files you can then stitch into a video.
Let’s go back to that simple idea of drawing a circle, but do it in node with node-canvas:
leftpad module reformats a number like 0 into a filename like 00001. This is because a lot of filesystems and tools that you’ll run into sort filenames as strings, rather than numbers, so the file 100.png will come before the file 2.png, and that’s no good. We want these frames ordered.
First, convert the PNG frames that node-canvas generated from PNG to GIF format:
mogrify -format gif -path frames-gif/ frames/*.png
And then combine these GIF frames into one animated GIF, with 1/10 second between frames and looping.
gifsicle -d10 --loop frames-gif/*.gif > animated.gif
Gifsicle is a wonderful way to make GIFs because you can tweak much more than this and customize palette, optimization, and much more.
Here’s the finished GIF:
GIFs are fantastically portable: you can embed them easily, drag & drop them into chats and twitter, and put them almost anywhere an image fits. But the GIF format is old and inefficient: for higher-resolution, longer animations, with higher color fidelity, you’ll want to use a video instead of a GIF.
To create a video from a series of images, use ffmpeg. Like gifsicle, ffmpeg has many knobs to turn to get the best results, but I usually start with something like:
ffmpeg -i frames/%5d.png -c:v libx264 -r 30 -pix_fmt yuv420p circle.mp4
Note where the usage of leftpad from before fits in: the pattern
frames/%5d.png expands into 5-digit padded numbers, from 00000 to 99999 - and those are what we rendered.
Here’s that incredibly exciting video:
Welcome to animation
Hopefully these are some useful starting points to building animations on the web, as images, and as videos. The node-canvas examples are available as a GitHub repo with complete source code.