Tom MacWright

tom@macwright.com

Canvas Animation

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.

Drawing Fundamentals

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 fillRect, moveTo, 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:

  • rect: rectangles
  • moveTo, lineTo, bezierCurveTo, etc: lines
  • fillText: text
  • drawImage: images

The Canvas API is a fundamentally raster way to interact with pixels: you can even interact with your drawings as pixels, using the getImageData and 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.

Performance Protips

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.fillStyle, context.lineWidth, and 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 and fillRect. The 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 fill or 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

Animation

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 requestAnimationFrame to 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 setInterval or setTimeout is that you should use time as your guide for the speed of animations: the key is to transform what Date.now() or +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:

ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
ctx.fillRect(0, 0, 600, 100);

Going offline

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.

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:

var Canvas = require('canvas'),
  fs = require('fs'),
  leftpad = require('leftpad');

// instead of finding an element, Canvas is a constructor.
var canvas = new Canvas(600, 100);
// get a context to draw in
var ctx = canvas.getContext('2d');
// count frames
var frame = 0;

for (var i = 0; i < Math.PI * 2; i += 0.05) {
  ctx.fillStyle = '#fff';
  ctx.fillRect(0, 0, 600, 100);
  ctx.fillStyle = '#000';
  ctx.beginPath();
  ctx.arc((Math.sin(i) + 1) * 250 + 50, 50, 20, 0, 2 * Math.PI);
  ctx.fill();
  // save a frame as a PNG file to disk
  fs.writeFileSync(
    'frames/' + leftpad(frame++, 5) + '.png',
    canvas.toBuffer());
}

The 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.

GIFs

Going the node-canvas route, you’ll end up with a directory filled with PNG images. How do you get from here to GIFs? You’ll need two tools: Imagemagick and gifsicle.

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:

moving ball gif

Videos

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.

See Also