I love playing games. And I love coding too. So, one day, I got thinking, why not use those coding skills to make a game? But it sounds hard. How would one even get started?
With baby steps. 👣
In this article, we learn to draw and animate objects using HTML5 Canvas and JavaScript before we optimize for performance.
“Animation is not the art of drawings that move but the art of movements that are drawn.” — Norman McLaren
Banner photo by Justin Lim on Unsplash
Introduction
Apple introduced canvas in 2004 to power applications and the Safari browser. A few years later it was standardized by the WHATWG. It comes with finer-grained control over rendering but with the cost of having to manage every detail manually. In other words, it can handle many objects, but we need to code everything in detail.
The canvas has a 2D drawing context used for drawing shapes, text, images, and other objects. First, we choose the color and brush, and then we paint. We can change the brush and color before every new drawing, or we can continue with what we have.
Canvas uses immediate rendering: When we draw, it immediately renders on the screen. But, it is a fire-and-forget system. After we paint something, the canvas forgets about the object and only knows it as pixels. So there is no object that we can move. Instead, we have to draw it again.
Doing animations on Canvas is like making a stop-motion movie. In every frame need to move the objects a little bit to animate them.
Canvas element
The HTML <canvas>
element provides a blank container on which we can draw graphics. We can draw shapes and lines on it via the Canvas API, which allows for drawing graphics via JavaScript.
A canvas is a rectangular area on an HTML page that by default has no border or content. The default size of the canvas is 300 pixels × 150 pixels (width × height). However, custom sizes can be defined using the HTML height
and width
property:
<canvas id="canvas" width="600" height="300"></canvas>
Specify the id
attribute to be able to refer to it from a script. To add a border, use the style
attribute or use CSS with the class
attribute:
<canvas id="canvas" width="600" height="300" style="border: 2px solid"></canvas>
<button onclick="animate()">Play</button>
Now that we added the border we see the size of our empty canvas on the screen.
We also have a button with an onclick
event to run our animate()
function when we click it.
We can place our JavaScript code in <script>
elements that we place into the document <body>
after the <canvas>
element:
<script type="text/javascript" src="canvas.js"></script>
We get a reference to the HTML <canvas>
element in the DOM (Document Object Model) with the getElementById()
method:
const canvas = document.getElementById('canvas');
Now we have the canvas element available but cannot draw directly on it. Instead, the canvas has rendering contexts that we can use.
Canvas context
The canvas has a 2D drawing context used for drawing shapes, text, images, and other objects. First, we choose the color and brush, and then we paint. We can change the brush and color before every new drawing, or we can continue with what we have.
The HTMLCanvasElement.getContext()
method returns a drawing context, where we render the graphics. By supplying '2d'
as the argument we get the canvas 2D rendering context:
const ctx = canvas.getContext('2d');
There are other available contexts, like
webgl
for a three-dimensional rendering context, that is outside the scope of this article.
The CanvasRenderingContext2D
has a variety of methods for drawing lines and shapes on the canvas. To set the color of the line we use strokeStyle
and to set the thickness we use lineWidth
:
ctx.strokeStyle = 'black';
ctx.lineWidth = 5;
Now, we are ready to draw our first line on the canvas. But, before we do that we need to understand how we tell the canvas where to draw. The HTML canvas is a two-dimensional grid. The upper-left corner of the canvas has the coordinates (0, 0).
X →
Y [(0,0), (1,0), (2,0), (3,0), (4,0), (5,0)]
↓ [(0,1), (1,1), (2,1), (3,1), (4,1), (5,1)]
[(0,2), (1,2), (2,2), (3,2), (4,2), (5,2)]
So, when we say we want to moveTo(4, 1)
on the canvas it means we start at the upper-left corner (0,0) and move four columns to the right and one row down.
Drawing 🔵
Once we have a canvas context, we can draw on it using the canvas context API. The method lineTo()
adds a straight line to the current sub-path by connecting its last point to the specified (x, y) coordinates. Like other methods that modify the current path, this method does not directly render anything. To draw the path onto a canvas, you can use the fill()
or stroke()
methods.
ctx.beginPath(); // Start a new path
ctx.moveTo(100, 50); // Move the pen to x=100, y=50.
ctx.lineTo(300, 150); // Draw a line to x=300, y=150.
ctx.stroke(); // Render the path
We can use fillRect()
to draw a filled rectangle. Setting the fillStyle
determines the color used when filling drawn shapes:
ctx.fillStyle = 'blue';
ctx.fillRect(100, 100, 30, 30); // (x, y, width, height);
This draws a filled blue rectangle:
Animation 🎥
Now, let's see if we can get our block to move on the canvas. We start by setting the size
of the square to 30. Then, we can move the x
value to the right with steps of size
and draw the object over and over again. We move the block to the right until it reaches the canvas end:
const size = 30;
ctx.fillStyle = 'blue';
for (let x = 0; x < canvas.width; x += size) {
ctx.fillRect(x, 50, size, size);
}
OK, we were able to draw the square as we wanted. But we have two issues:
- We are not cleaning up after ourselves.
- It's too fast to see the animation.
We need to clear away the old block. What we can do is erase the pixels in a rectangular area with clearRect()
. By using the width and height of the canvas, we can clean it between paints.
for (let x = 0; x < canvas.width; x += size) {
ctx.clearRect(0, 0, canvas.width, canvas.height); // Clean up
ctx.fillRect(x, 50, size, size);
}
Great! We fixed the first problem. Now let's try to slow down the painting so we can see the animation.
You might be familiar with setInterval(function, delay)
. It starts repeatedly executing the specified function
every delay
milliseconds. I set the interval to 200 ms, which means the code runs five times a second.
let x = 0;
const id = setInterval(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(x, 50, size, size);
x += size;
if (x >= canvas.width) {
clearInterval(id);
}
}, 200);
To stop a timer created by setInterval()
, we need to call clearInterval()
and give it the identifier for the interval to cancel. The id to use is the one that is returned by setInterval()
, and this is why we need to store it.
We can now see that if we press the button, we get a square that moves from left to right. But, if we press the play button several times, we can see that there is a problem animating multiple squares at the same time.
Every square gets its interval that clears the board and paints the square.
It's all over the place! Let's see how we can fix this.
Multiple objects
To be able to run the animations for several blocks, we need to rethink the logic. As of now, each block gets its animation method with setInterval()
. Instead, we should manage the moving objects before sending them to be drawn, all at once.
We can add a variable started
to only start setInterval()
on the first button click. Every time we press the play button, we add a new value 0 to a squares
array. This is enough for this simple animation but for something more complex we could create a Square
object with the coordinates and eventual other properties like color.
let squares = [];
let started = false;
function play() {
// Add 0 as x value for object to start from the left.
squares.push(0);
if (!started) {
started = true;
setInterval(() => {
tick();
}, 200)
}
}
function tick() {
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Paint objects
squares.forEach(x => ctx.fillRect(x, 50, size, size));
squares = squares.map(x => x += size) // move x to right
.filter(x => x < canvas.width); // remove when at end
}
The tick()
function clears the screen and paints all the objects in the array every 200ms. And by having only one interval, we avoid the flicker we had before. And now we get better animations:
What we did here is the first step of making a game loop. This loop is the heart of every game. It's a controlled infinite loop that keeps your game running ; it's the place where all your little pieces get updated and drawn on the screen.
🚶Optimize animations 🏃
Another option for animating is to use requestAnimationFrame()
. It tells the browser that you wish to perform an animation and requests the browser to call a function to update an animation before the next repaint. In other words, we tell the browser: "Next time you paint on the screen, also run this function because I want to paint something too."
The way to animate with requestAnimationFrame()
is to create a function that paints a frame and then schedules itself to invoke again. With this, we get an asynchronous loop that executes when we draw on the canvas. We invoke the animate method over and over again until we decide to stop. So, now we instead call the animate()
function:
function play() {
// Add 0 as x value for object to start from the left.
squares.push(0);
if (!started) {
animate();
}
}
function animate() {
tick();
requestAnimationFrame(animate);
}
If we try this out we notice that we can see the animation, which was not the case with setInterval()
, even though it's super fast. The number of callbacks is usually 60 times per second.
The requestAnimationFrame()
method returns an id
that we use for canceling the scheduled animation frame. To cancel a scheduled animation frame, you can use the cancelAnimationFrame(id)
method.
To slow down the animation we need a timer to check the elapsed
time since the last time we called the tick()
function. To help us, the callback function is passed an argument, a DOMHighResTimeStamp
, indicating the point in time when requestAnimationFrame()
starts to execute callback functions.
let start = 0;
function animate(timestamp) {
const elapsed = timestamp - start;
if (elapsed > 200) {
start = timestamp;
tick();
}
requestAnimationFrame(animate);
}
With this, we have the same functionality as we had earlier with setInterval()
.
So, in conclusion, why should we use requestAnimationFrame()
instead of setInterval()
?
- It enables browser optimizations.
- It handles the frame rate.
- Animations only run when visible.
Conclusion
In this article, we created an HTML5 Canvas and used its 2D rendering context and JavaScript to draw on the canvas. We got introduced to some of the methods available in the canvas context and used them to render different shapes.
Finally, we were able to animate multiple objects on the canvas. We learned how to use setInterval()
to create an animation loop that manages and draws the objects on the screen.
We also learned how to optimize animations with requestAnimationFrame()
.
With this intro to canvas animations, we have taken our first steps into game development. We are ready to start on a real game next:
Top comments (0)