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
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.
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.
For an operation like
rect, the Canvas API has two methods:
doesn’t immediately draw anything: it records that you want to draw a rectangle
in a certain place, and then when you draw
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
fill once you’re done, rather than calling
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:
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.
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.
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
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:
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:
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:
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.