Animation in the <canvas>
element is sufficiently different from traditional CSS, SVG and JavaScript animation that it deserves its own explanation:
One Red Brick
In traditional HTML, a page is built up from tags and content, creating DOM elements and nodes. The most important aspect to understand about canvas is that, within the <canvas>
itself, everything is abstract code: there are no elements to grasp on to and manipulate. This can be disconcerting for those with a background in pure HTML, CSS or SVG, but it’s also somewhat freeing: rather than building things up piece-by-piece, you have the ability to create and destroy at will in the <canvas>
space.
We do still have to make a <canvas>
element, so let’s start with that:
<canvas id="expanse" width="800" height="600"></canvas>
Adding some CSS:
#expanse {
background: #000;
width: 100%; height: auto;
}
And at the bottom of the page, a script:
var expanse = document.getElementById("expanse");
var context = expanse.getContext("2d");
context.fillStyle="red";
context.fillRect(0, 0, 50, 50);
This combination places a red square in he top left corner of a responsive canvas. The problem now is to make it move across to the other side. As with almost all animation, it’s a matter of making small incremental visual changes. We could create a version of this by following the first fillRect
with another, just moved 10 pixels to the right:
context.fillRect(0, 0, 50, 50);
context.fillRect(10, 0, 50, 50);
This almost works, but you will see, the second rectangle builds on the first; what we need is to clear the previous drawing before adding a new one:
context.fillRect(0, 0, 50, 50);
context.clearRect(0, 0, expanse.width, expanse.height);
context.fillRect(10, 0, 50, 50);
This works, but too well. I’ve previously described the Canvas API as “the world’s fastest Etch-A-Sketch”, and that’s what we see here: the clearRect
wipes the expanse of the canvas too quickly to see the first red square. Therefore, we don’t see any animation.
Animating the Brick
To animate the rectangle, we need to do three things:
- Clear the canvas
- Draw the rectangle in an incremented new position
- Loop back to (1) quick enough to fool the human eye into seeing animation (not too fast, but not after too long a delay).
To accomplish this, we’ll create five new variables and a function:
var rectWidth = 50,
rectHeight = 50,
posX = 0,
posy = 0,
xInc = 2;
function drawRect() {
context.clearRect(0, 0, expanse.width, expanse.height);
context.fillRect(posX += xInc, 0, rectWidth, rectHeight);
}
We’ve made the “brick” dimension dynamic by casting both dimensions as variables; the rectangle’s position on the x and y of the canvas is also dynamic. Each time the function is called, it increments the horizontal position of the rectangle by the amount specified by xInc
.
To update the position of the brick, we need to call the drawRect()
function at regular intervals. We’ll do that using requestAnimationFrame
to gain the most efficient performance. We’ll use it once outside the function to initiate the first movement, and again inside the function to do so repeatedly:
function drawRect() {
context.clearRect(0, 0, expanse.width, expanse.height);
posX += xInc;
context.fillRect(posX, 0, rectWidth, rectHeight);
window.requestAnimationFrame(drawRect);
}
window.requestAnimationFrame(drawRect);
If you try this code, you’ll see that the red square goes completely off the edge of the canvas. That’s very inefficient, as the function will continue to move the brick long after it has disappeared from the canvas area. To avoid this, we’ll make the movement conditional on the fact that the right side of the rectangle (i.e. the position of the rectangle + its width) is still visible on the canvas:
function drawRect() {
context.clearRect(0, 0, expanse.width, expanse.height);
posX += xInc;
context.fillRect(posX, 0, rectWidth, rectHeight);
if ((posX + rectWidth) < expanse.width) {
window.requestAnimationFrame(drawRect);
}
}
In the example at the top of this article (and the associated CodePen) the function is initiated with a `<button>` element, rather than running automatically.
Speeding Up & Slowing Down The Animation
HTML5 canvas is a fairly low-level API: unlike CSS, there’s no immediate control over timing or easing, which is one of the primary reasons that many developers move to using Greensock and similar libraries. requestAnimationFrame
takes care of the smoothness of the animation (it automatically updates at 60 FPS, so long as that’s achievable), meaning that we can speed up or slow down the animation by changing the value of xInc
:
xInc = 1;
A lower value will slow down the animation (each loop in the function will move the rectangle less), while increasing the value will speed up the animation. Alternatively, rather than using precise pixel values, we could make xInc
relative to the width of the canvas
element:
xInc = expanse.width/100;
Obviously, there’s much more that can be done with canvas animation, but this introduces the very basics; there will be much more to come.
Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/VebRMp