Prerequisites:
To animate the canvas, we have to render the canvas repeatedly, gradually (or maybe not so gradually) changing the drawing over time. For smooth motion, we need to do this 20 to 30 “frames” per second.
You might expect that this would involve a loop, such as a for or while statement—but oh, no, that’s not the way JavaScript works. Maybe with recursive functions, then? Wrong again! We use a timer with a callback function.
Actually, the combination of timer and callback function is a lot like recursion, but it’s indirect. A recursive function calls itself. Instead, our timer will call the callback function, and the callback function will call to make a new timer, which will call the callback function again, and so on.
05-01. Animate a small circle moving across the canvas from left to right.
Here’s a first try.
After getting references to the canvas and 2D context in the usual way,
var canvas = document.getElementById('canvas1');
var ctx = canvas.getContext('2d');
we’ll encapsulate most of the solution in a function start_animation:
function start_animation () {
// Canvas properties
var canvas_width = 800;
var canvas_height = 600;
// Circle properties
var radius = 10;
var x = 0 + radius; // initial position, far left
var xmax = canvas_width - radius; // end position, far right
var y = canvas_height / 2;
// Timing
var delta_t = 1000 / 20; // 1/20 sec in millisec
var delta_x = 10;
function draw_circle () {
ctx.beginPath();
ctx.arc(x, y, radius, 2 * Math.PI, false);
ctx.closePath();
ctx.fillStyle = "blue";
ctx.strokeStyle = "black";
ctx.fill();
ctx.stroke();
}
function animate () {
draw_circle();
x = x + delta_x;
if (x < xmax)
setTimeout(animate, delta_t);
}
animate();
}
The start_animation function begins with some variables storing the dimensions of the canvas, then some variables storing the properties of the circle. Of the circle properties, x is the only one that varies; the rest are constants. Then we have some timing parameters in the variables delta_t (time between frames, in milliseconds) and delta_x (how far the circle will move, in the x direction, on each frame update).
Then we have a couple of inner functions. Unsurprisingly, draw_circle draws the circle, in its current position; it puts the circle in the path by calling ctx.arc with a full sweep of 2π radians (360 degrees).
The animate function does the interesting work. The first thing it does is call draw_circle. Then it updates the position of the circle (the variable x). Finally, it checks whether x < xmax; if this is true, we want the animation to keep going, and this is achieved by calling setTimeout; otherwise, the animation is finished.
But what does setTimeout do? It is a built-in JavaScript function, and if we call it like this,
setTimeout(CALLBACK, DELAY);
then it sets up a timer which will wait DELAY milliseconds, and then (when the timer “times out”) call the function CALLBACK, with no arguments.1 But, we do not have to wait for the timer and can go on immediately to do other things.
In our code, we have
setTimeout(animate, delta_t);
so CALLBACK is animate, and DELAY is delta_t. Therefore, this will set up a timer which will, after 1/20 second, call the animate function.
Think carefully about what’s going on here: the animate function draws the circle, updates the x coordinate of the circle, and then (assuming we’re not done yet) sets up a delayed call to itself through setTimeout. If we abstract from the delay mechanism, then: animate calls setTimeout, and setTimeout calls animate. The two functions are mutually recursive.
The last thing in the start_animation function is a call to animate.
To summarize the start_animation function:
draw_circle and animate, andanimate.Now, the final statement in the script (we are now past the definition of start_animation) is a call to start_animation, which is what gets things going:
start_animation();
Think it will work? Let’s see:
Hmm — almost right, but instead of seeing many circles across the canvas, we want to see just one circle that moves along.
Whatever is drawn on the canvas, remains on the canvas until it is erased or we draw over it with something else.
Clearly, before we draw the circle in its new position, we need to erase it from its old position. There are (at least) two ways we could do this:
We could just erase the little circle from where it was. But, if there’s a background, or any other objects in the scene that were obscured by the circle, we’d have to re-render those objects as well — but just the parts of them behind the circle. In this example, though, there is no background, and there are no other objects.
We could erase the whole canvas and then redraw the background and all the objects in the scene. Yep, everything. Again, though, in this example, there is no background, and there are no other objects.
So in this example, because there are no other objects and no background, it really doesn’t matter which way we do it.
If the scene were more complicated, though, we might need to think more carefully. The first method seems cleanest, since there is less to render; but we’d have to be very careful to redraw just the right parts of everything, and there might be some flickering or other “artifacts” of the process. The second method seems to require more work, on the part of the computer, but might require less work on the part of the programmer.
Okay, let’s try it the first way. We’re going to erase the circle before drawing it. Of course, if we just erase and then redraw without moving it, that would be pretty silly, so we need to re-arrange some of the code in animate as well as put in a new call to erase_circle (which we’ll define shortly):
function animate () {
erase_circle();
x = x + delta_x;
if (x < xmax)
{
draw_circle();
setTimeout(animate, delta_t);
}
}
The definition of erase_circle could look like that of draw_circle, except that the stroke and fill colors would be white (which is our canvas’s background color):2
function erase_circle () {
ctx.beginPath();
ctx.arc(x, y, radius, 2 * Math.PI, false);
ctx.closePath();
ctx.fillStyle = "white";
ctx.strokeStyle = "white";
ctx.fill();
ctx.stroke();
}
But, when we are tempted to write two such similar functions, we should resist that idea a little and consider whether it isn’t better to do a little abstraction, or generalization, instead. What do these two functions have in common? Almost everything! How are they different? In their names, and the fill and stroke colors.
We’d gain 9 lines of code that has to be maintained, but only 3 lines are different from the original 9 lines.
So, how about a function to generate our two functions?
function circle_fs (fill, stroke) {
return function () {
ctx.beginPath();
ctx.arc(x, y, radius, 2 * Math.PI, false);
ctx.closePath();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.fill();
ctx.stroke();
}
}
var draw_circle = circle_fs("blue", "black");
var erase_circle = circle_fs("white", "white");
This way we gain only 4 lines of code.
We are still leaving a trace of where the circle was, but it is now a much fainter trail. It turns out that we need to erase a slightly larger circle than what was drawn. We can do this by adding a delta_r parameter to the circle_fs function, and add it to the radius:
function circle_fs (fill, stroke, delta_r) {
return function () {
ctx.beginPath();
ctx.arc(x, y, radius + delta_r, 2 * Math.PI, false);
ctx.closePath();
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.fill();
ctx.stroke();
}
}
var draw_circle = circle_fs("blue", "black", 0);
var erase_circle = circle_fs("white", "white", 1);
Finally there is no trace of the circle’s past positions. The motion is jerky, at least in my browsers, but let’s not worry about that for now.
Now let’s back up: the second way, remember, is to erase the whole canvas, or in other words to draw the background again and everything else in the scene. This should turn out to be a simpler solution. Just to make it a little more interesting, let’s make the background yellow.
Our point of departure is Solution (a). We only need to define one more function, draw_background, and call it in animate before draw_circle:
function draw_background () {
ctx.fillStyle = "yellow";
ctx.fillRect(0, 0, canvas_width, canvas_height);
}
function animate () {
draw_background();
draw_circle();
x = x + delta_x;
if (x < xmax)
setTimeout(animate, delta_t);
}
It works well, it was easy to implement.
Maybe the motion is even a little more jerky—or maybe it just seems so because the yellow background is more vivid—but again, we’ll defer worrying about that sort of thing.
We can use setTimeout for animation. This function takes two parameters: a callback function and a delay.
The callback function also needs to call setTimeout again, unless the animation is finished. Thus, setTimeout and its callback function are mutually recursive.
The callback is a functional parameter. After the delay, the timer created by setTimeout will call the callback function. We are seeing an application of functions as first-class values, namely, using a function (the callback) as a parameter to another function (setTimeout).
We also saw that when we have two very similar functions, it is profitable to generalize their structure. We can define a function which generates the similar functions by returning them as values. Again, this is a use of functions as first-class values.
There is another function, setInterval, which is similar to setTimeout. The difference is that though setTimeout calls its callback function only once (so if you want repeated calls, the callback must call setTimeout again to set up the next one), setInterval will call the callback indefinitely. It works like this:
setInterval(CALLBACK, DELAY);
Let’s use setInterval to solve a problem. I’d like to animate an oval which rotates forever. Unfortunately, the context ellipse method (which would draw a rotated oval, according to the specification) isn’t defined in current browsers, at least not in Firefox and Chromium.
So I’ll settle for this instead:
Animate an orbiting square.
The square will orbit counterclockwise around the center of our canvas.
Trigonometry tells us that if a point is a radius r from the origin (0, 0) and the direction from (0, 0) to the point is the angle θ, then our point’s Cartesian coordinates are
We get the sine and cosine functions from the Math object, like in Java: Math.sin(theta), Math.cos(theta).
If (x, y) is the center of our square, and side is the width (also the height) of the square, the the top left corner coordinates of the square are
But since we are orbiting around the canvas center (XC, YC) instead of around (0, 0), we must offset accordingly:
To move counterclockwise, we will increment θ negatively, and to keep it in the range −2π ≤ θ ≤ 0 we will add back 2π to it whenever it becomes too low.
This time we’ll push even more information into the the protective custody of the closure. Here’s our code:
function start_animation () {
// The canvas and its properties
var canvas = document.getElementById('canvas1');
var ctx = canvas.getContext('2d');
var canvas_width = 800;
var canvas_height = 600;
var XC = canvas_width / 2; // canvas center x
var YC = canvas_height / 2; // canvas center y
// The square's properties
var theta = 0; // direction from canvas center, in radians
var side = 10;
var orbit_radius = 200; // distance from canvas center
// Timing
var delay = 1000 / 20; // 1/20 sec in millisec
var delta_theta = -Math.PI / 180 / 4; // (-1/4 degree)
function draw_square () {
var x = orbit_radius * Math.cos(theta);
var y = orbit_radius * Math.sin(theta);
var xleft = XC + x - side / 2;
var ytop = YC + y - side / 2;
ctx.beginPath();
ctx.rect(xleft, ytop, side, side);
ctx.closePath();
ctx.fillStyle = "blue";
ctx.strokeStyle = "white";
ctx.fill();
ctx.stroke();
}
function draw_background () {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas_width, canvas_height);
}
// animate renders and then updates the rotation
function animate () {
// render everything
draw_background();
draw_square();
// update the square's rotation
theta = theta + delta_theta;
if (theta < -2 * Math.PI)
theta = theta + 2 * Math.PI;
}
setInterval(animate, delay);
}
start_animation();
And here you can “play” the solution:
Both setTimeout and setInterval return a value, which can be used to “turn off” the timer. The value returned by setTimeout is called a timeout id, and the value returned by setInterval is called an interval id.
So, if we set them up like this,
var tid = setTimeout(CB1, DT1);
var iid = setInterval(CB2, DT2);
we can later cancel the scheduled events, like this:
clearTimeout(tid);
clearInterval(iid);Since setInterval seems more convenient than setTimeout, why do we have both? There are a couple of things you can (easily) do with setTimeout, which you can’t do (at least not nearly so easily) with setInterval. One, of course, is to schedule a one-time callback. You want to wait for 20 seconds, and something happens, but it only happens once. The other involves the fact that setInterval sets a regular interval: if you set it for 20 seconds, then it’s always going to be 20 seconds between the events But if you’re always needing to make a call to setTimeout to schedule the event, you can calculate the DELAY as needed, and it can be different each time.
The timers do not guarantee to call your callback function exactly when DELAY milliseconds have elapsed. They can’t do that. Your computer might be too busy with a lot of processes running, and so your callback might occur later than planned. The timer can only make a reasonable effort. It can, however, guarantee that the callback does not happen before the scheduled delay.
If you study JavaScript examples in books or on the web, you will sometimes see a form of setInterval or setTimeout with extra arguments, like this, for example:
setInterval(CALLBACK, DELAY, ARG1, ARG2, ARG3);
What happens is that after DELAY milliseconds, the timer calls the CALLBACK function, but with arguments, like this: CALLBACK(ARG1, ARG2, ARG3) instead of just CALLBACK().
This is an archaic form of setting up callbacks that derives from the C programming language (an ancestor of Java), which did not have closures. It is totally unnecessary in a language, like JavaScript, with first-class functions. We can avoid it like this:
setInterval(function () { CALLBACK(ARG1, ARG2, ARG3); }, DELAY);The latest trick—a better alternative for setInterval or setTimer for animation—is requestAnimationFrame. Read about it here: