Graphics and animation are the most fundamental aspects of any video game, so in this article I start with a brief overview of the Canvas 2D API, followed by a discussion of the implementation of Snail Bait's central animation. In this article, you will learn how to:
- Draw images and graphics primitives into a canvas
- Create smooth, flicker-free animations
- Implement the game loop
- Monitor animation rate in frames per second
- Scroll the game's background
- Use parallax to simulate three dimensions
- Implement time-based motion
The end result of the code discussed in this article is shown in Figure 1:
Figure 1. Scrolling the background and monitoring frame rate
The background and platforms scroll horizontally. The platforms are in the foreground, so they move noticeably faster than the background, creating a mild parallax effect. When the game begins, the background scrolls from right to left. At the end of the level, the background and platforms reverse direction.
At this stage of development, the runner does not move. Also, the game has no collision detection yet, so the runner floats in mid-air when there are no platforms underneath her.
Eventually, icons above and to the left of the game's canvas will indicate the number of remaining lives (as shown in Figure 1 in this series' first article). For now, the game displays the current animation rate in frames per second at that location.
Before continuing, you might want to try the game as it stands in Figure 1; the code will be easier to understand if you do. (See Download to get this installment's implementation of Snail Bait.)
The Canvas 2D context provides an extensive graphics API that lets you implement everything from text editors to platform video games. At the time this article was written, that API contained more than 30 methods, but Snail Bait uses only a handful of them, shown in Table 1:
Table 1. Canvas 2D context methods used by Snail Bait
| Method | Description |
|---|---|
drawImage() |
Draws all, or part, of an image at a specific location in a canvas. Can also draw another canvas or a frame from a video element.
|
save() | Saves context attributes on a stack. |
restore() | Pops context attributes off the stack and applies them to the context. |
strokeRect() | Draws an unfilled rectangle. |
fillRect() | Fills a rectangle. |
translate() | Translates the coordinate system. This is a powerful method that is useful in many different scenarios. All scrolling in Snail Bait is implemented with this one method call. |
Everything in Snail Bait, with the exception of the platforms, is an image. The background, the runner, and all the good guys and bad guys are images that the game draws with the drawImage() method.
Ultimately Snail Bait will use a spritesheet — a single image containing all the game's graphics — but for now I use separate images for the background and the runner. I draw the runner with the function shown in Listing 1:
Listing 1. Drawing the runner
function drawRunner() {
context.drawImage(runnerImage, // image
STARTING_RUNNER_LEFT, // canvas left
calculatePlatformTop(runnerTrack) - RUNNER_HEIGHT); // canvas top
}
|
The drawRunner() function passes three arguments to drawImage(): an image and the left and top coordinates at which to draw the image in the canvas. The left coordinate is a constant, whereas the top coordinate is determined by the platform on which the runner resides.
I draw the background in a similar manner, as Listing 2 illustrates:
Listing 2. Drawing the background
function drawBackground() {
context.drawImage(background, 0, 0);
}
|
The drawBackground() function in Listing 2 draws the background image at (0,0) in the canvas. Later in this article, I modify that function to scroll the background.
Drawing the platforms, which are not images, requires more extensive use of the Canvas API, as shown in Listing 3:
Listing 3. Drawing platforms
var platformData = [
// Screen 1.......................................................
{
left: 10,
width: 230,
height: PLATFORM_HEIGHT,
fillStyle: 'rgb(150,190,255)',
opacity: 1.0,
track: 1,
pulsate: false,
},
...
],
...
function drawPlatforms() {
var data, top;
context.save(); // Save the current state of the context
context.translate(-platformOffset, 0); // Translate the coord system for all platforms
for (var i=0; i < platformData.length; ++i) {
data = platformData[i];
top = calculatePlatformTop(data.track);
context.lineWidth = PLATFORM_STROKE_WIDTH;
context.strokeStyle = PLATFORM_STROKE_STYLE;
context.fillStyle = data.fillStyle;
context.globalAlpha = data.opacity;
context.strokeRect(data.left, top, data.width, data.height);
context.fillRect (data.left, top, data.width, data.height);
}
context.restore(); // Restore context state saved above
}
|
The JavaScript in Listing 3 defines an array named platformData. Each object in the array represents metadata that describes an individual platform.
The drawPlatforms() function uses the Canvas context's strokeRect() and fillRect() methods to
draw platform rectangles. The characteristics of those rectangles — which are
stored in the objects in the platformData array —
are used to set the context's fill style, and the globalAlpha attribute, which sets the opacity of anything that you subsequently draw in the canvas.
The call to context.translate() translates the canvas's coordinate system — depicted in Figure 2 — by a specified number of pixels in the horizontal direction. That translation and the attribute settings are temporary because they're made between calls to context.save() and context.restore().
Figure 2. The default Canvas coordinate system
By default, the origin of the coordinate system is at the upper left corner of the canvas. You can move the origin of the coordinate system with context.translate().
I discuss scrolling the background with context.translate() in
Scrolling the background. But at this point, you know nearly everything you need to know about HTML5 Canvas to implement Snail Bait. For the rest of this series, I will focus on other aspects of HTML5 game development, starting with animation.
Fundamentally, implementing animations is simple: You repeatedly draw a sequence of images that make it appear as though objects are animating in some fashion. That means you must implement a loop that periodically draws an image.
Traditionally, animation loops were implemented in JavaScript with setTimeout() or, as illustrated in Listing 4, setInterval():
Listing 4. Implementing animations with
setInterval()
setInterval( function (e) { // Don't do this for time-critical animations
animate(); // A function that draws the current animation frame
}, 1000 / 60); // Approximately 60 frames/second (fps)
|
The code in Listing 4 will undoubtedly produce an animation by repeatedly invoking an animate() function that draws the next animation frame; however, you may not be satisfied with the results, because setInterval() and setTimeout() know nothing about animation. (Note: You must implement the animate() function; it is not part of the Canvas API.)
In Listing 4, I set the interval to 1000/60 milliseconds, which equates to roughly 60 frames per second. That number is my best estimate of an optimal frame rate, and it may not be a very good one; however, because setInterval() and setTimeout() don't know anything about animation, it's up to me to specify the frame rate. It would be better if the browser, which assuredly knows better than I when to draw the next animation frame, specified the frame rate instead.
There is an even more serious drawback to using setTimeout and setInterval(). Although you pass those methods time intervals specified in milliseconds, the methods are not millisecond-precise; in fact, according to the HTML specification, those methods — in an effort to conserve resources — can generously pad the interval you specify.
To avoid these drawbacks, you shouldn't use setTimeout() and setInterval() for time-critical animations; instead, you should use requestAnimationFrame().
In the Timing control for script-based animations specification (see Resources), the W3C defines a method on the window object named requestAnimationFrame(). Unlike setTimeout() or setInterval(), requestAnimationFrame() is specifically meant for implementing
animations. It therefore suffers from none of the drawbacks associated with setTimeout() and setInterval(). It's also
simple to use, as Listing 5 illustrates:
Listing 5. Implementing animations with
requestAnimationFrame()
function animate(time) { // Animation loop
draw(time); // A function that draws the current animation frame
requestAnimationFrame(animate); // Keep the animation going
};
requestAnimationFrame(animate); // Start the animation
|
You pass requestAnimationFrame() a reference to a callback function, and when the browser is ready to draw the next animation frame, it invokes that callback. To sustain the animation, the callback also invokes requestAnimationFrame().
As you can see from Listing 5, the browser passes a time parameter to your callback function. You may wonder exactly what that time parameter means. Is it the current time? The time at which the browser will draw the next animation frame?
Surprisingly, there is no set definition of that time. The only thing you can be sure of is that for any given browser, it always represents the same thing; therefore, you can use it to calculate the elapsed time between frames, as I illustrate in Calculating animation rate in fps.
A requestAnimationFrame() polyfill
In many ways, HTML5 is a programmer's utopia. Free from proprietary APIs, developers use HTML5 to implement applications that run cross-platform in the ubiquitous browser. The specifications progress rapidly, constantly incorporating new technology and refining existing functionality.
New technology, however, often makes its way into the specification through existing browser-specific functionality. Browser vendors often prefix such functionality so that it doesn't interfere with another browser's implementation; requestAnimationFrame(), for example, was originally implemented by Mozilla as mozRequestAnimationFrame(). Then it was implemented by WebKit, which named its function webkitRequestAnimationFrame(). Finally, the W3C standardized it as requestAnimationFrame().
Vendor-prefixed implementations and varying support for standard implementations make new functionality tricky to use, so the HTML5 community invented something known as a polyfill. Polyfills determine the browser's level of support for a particular feature and either give you direct access to it if the browser implements it, or a stopgap implementation that does its best to mimic the standard functionality.
Polyfills are simple to use but can be complicated to implement. Listing 6 shows the implementation of a polyfill for requestAnimationFrame():
Listing 6.
requestNextAnimationFrame() polyfill
// Reprinted from Core HTML5 Canvas
window.requestNextAnimationFrame =
(function () {
var originalWebkitRequestAnimationFrame = undefined,
wrapper = undefined,
callback = undefined,
geckoVersion = 0,
userAgent = navigator.userAgent,
index = 0,
self = this;
// Workaround for Chrome 10 bug where Chrome
// does not pass the time to the animation function
if (window.webkitRequestAnimationFrame) {
// Define the wrapper
wrapper = function (time) {
if (time === undefined) {
time = +new Date();
}
self.callback(time);
};
// Make the switch
originalWebkitRequestAnimationFrame = window.webkitRequestAnimationFrame;
window.webkitRequestAnimationFrame = function (callback, element) {
self.callback = callback;
// Browser calls the wrapper and wrapper calls the callback
originalWebkitRequestAnimationFrame(wrapper, element);
}
}
// Workaround for Gecko 2.0, which has a bug in
// mozRequestAnimationFrame() that restricts animations
// to 30-40 fps.
if (window.mozRequestAnimationFrame) {
// Check the Gecko version. Gecko is used by browsers
// other than Firefox. Gecko 2.0 corresponds to
// Firefox 4.0.
index = userAgent.indexOf('rv:');
if (userAgent.indexOf('Gecko') != -1) {
geckoVersion = userAgent.substr(index + 3, 3);
if (geckoVersion === '2.0') {
// Forces the return statement to fall through
// to the setTimeout() function.
window.mozRequestAnimationFrame = undefined;
}
}
}
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback, element) {
var start,
finish;
window.setTimeout( function () {
start = +new Date();
callback(start);
finish = +new Date();
self.timeout = 1000 / 60 - (finish - start);
}, self.timeout);
};
}
)
();
|
The polyfill implemented in Listing 6 attaches a function named requestNextAnimationFrame() to the window object. The inclusion of Next in the function name differentiates it from the underlying requestAnimationFrame() function.
The function that the polyfill assigns to requestNextAnimationFrame() is either requestAnimationFrame() if the browser supports it, or a vendor-prefixed implementation. If the browser does not support either of those, the function is an ad-hoc implementation that uses setTimeout() to mimic requestAnimationFrame() the best it can.
Nearly all of the polyfill's complexity involves working around two bugs and constitutes the code before the return statement. The first bug involves Chrome 10, which passes an undefined value for the time. The second bug involves Firefox 4.0, which restricts frame rates to 35-40 frames per second.
Although the requestNextAnimationFrame() polyfill's implementation is interesting, it's not necessary to understand it; instead, all you need to know is how to use it, as I illustrate in the next section.
Now that the graphics and animation prerequisites are out of the way, it's time to put Snail Bait in motion. To start, I include the JavaScript for the requestNextAnimationFrame() in the game's HTML, as shown in Listing 7:
Listing 7. The HTML
<html>
...
<body>
...
<script src='js/requestNextAnimationFrame.js'></script>
<script src='game.js'></script>
</body>
</html>
|
Listing 8 shows the game's animation loop, commonly referred to as the game loop:
Listing 8. The game loop
var fps;
function animate(now) {
fps = calculateFps(now);
draw();
requestNextAnimationFrame(animate);
}
function startGame() {
requestNextAnimationFrame(animate);
}
|
The startGame() function, which is invoked by the background image's onload event handler, starts the game by calling the requestNextAnimationFrame() polyfill. When it's time to draw the game's first animation frame, the browser invokes the animate() function.
The animate() function calculates the animation's frame rate, given the current time. (See requestAnimationFrame() for more about the time value.) After calculating the frame rate, animate() invokes a draw() function that draws the next animation frame. Subsequently, animate() calls requestNextAnimationFrame() to sustain the animation.
Calculating animation rate in fps
Listing 9 shows how Snail Bait calculates its frame rate, and how it updates the frame-rate readout shown in Figure 1:
Listing 9. Calculating fps and updating the
fps element
var lastAnimationFrameTime = 0,
lastFpsUpdateTime = 0,
fpsElement = document.getElementById('fps');
function calculateFps(now) {
var fps = 1000 / (now - lastAnimationFrameTime);
lastAnimationFrameTime = now;
if (now - lastFpsUpdateTime > 1000) {
lastFpsUpdateTime = now;
fpsElement.innerHTML = fps.toFixed(0) + ' fps';
}
return fps;
}
|
The frame rate is simply the amount of time since the last animation frame, so you could argue that it's frame per second instead of frames per second, which doesn't make it much of a rate at all. You could take a more rigorous approach and maintain an average frame rate over several frames, but I have not found that to be necessary; indeed, the elapsed time since the last animation frame is exactly what I will need in Time-based motion.
Listing 9 also illustrates an important animation technique: performing a task at a rate other than the animation rate. If I update the frames/second readout every animation frame, it will be unreadable because it will always be in flux; instead, I update that readout once per second.
With the game loop in place and frame rate in hand, I am now ready to scroll the background.
Snail Bait's background, shown in Figure 3, scrolls slowly in the horizontal direction:
Figure 3. The background image
The background scrolls seamlessly because the left and right edges of the background are identical, as Figure 4 illustrates:
Figure 4. Identical edges make smooth transitions (left: right edge; right: left edge)
Snail Bait endlessly scrolls the background by drawing it twice, as shown in Figure 5. Initially, as shown in the top screenshot in Figure 5, the background image on the left is entirely on screen, whereas the one on the right is entirely offscreen. As time progresses, the background scrolls, as illustrated by the middle and bottom screenshots in Figure 5:
Figure 5. Scrolling right to left: Translucent areas represent the offscreen parts of the images
Listing 10 shows the code that correlates to Figure 5. The drawBackground() function draws the image twice, always at the same locations. The apparent scrolling is the result of constantly translating the canvas's coordinate system to the left, which makes the background appear to scroll to the right.
(Here's how you can reconcile the apparent contradiction of translating left but scrolling right: Imagine the canvas as an empty picture frame on top of a long sheet of paper. The paper is the coordinate system, and translating it to the left is like sliding it to the left underneath the frame [canvas]. Therefore, the canvas appears to move to the right.)
Listing 10. Scrolling the background
var backgroundOffset; // This is set before calling drawBackground()
function drawBackground() {
context.translate(-backgroundOffset, 0);
// Initially onscreen:
context.drawImage(background, 0, 0);
// Initially offscreen:
context.drawImage(background, background.width, 0);
context.translate(backgroundOffset, 0);
}
|
The setBackground() function translates the canvas context
-backgroundOffset pixels in the horizontal direction. If
backgroundOffset is positive, the background scrolls to the
right; if it's negative, the background scrolls to the left.
After translating the background, drawBackground() draws the background image twice and then translates the context back to where it was before drawBackground() was called.
One seemingly trivial calculation remains: calculating backgroundOffset, which determines how far to translate the canvas's coordinate system for each animation frame. Although the calculation itself is indeed trivial, it has great significance, so I discuss it next.
Your animation's frame rate will vary, but you must not let that varying frame rate affect the rate at which your animation progresses. For example, Snail Bait scrolls the background at 42 pixels/second regardless of the animation's underlying frame rate. Animations must be time-based, meaning velocities are specified in pixels/second, and must not depend on the frame rate.
Using time-based motion to calculate the number of pixels to move an object for a given frame is simple: Divide velocity by the current frame rate. Velocity (pixels/second) divided by frame rate (frames/second) results in pixels/frame, meaning the number of pixels you need to move something for the current frame.
Listing 11 shows how Snail Bait uses time-based motion to calculate the background's offset:
Listing 11. Setting the background offset
var BACKGROUND_VELOCITY = 42, // pixels / second
bgVelocity = BACKGROUND_VELOCITY;
function setBackgroundOffset() {
var offset = backgroundOffset + bgVelocity/fps; // Time-based motion
if (offset > 0 && offset < background.width) {
backgroundOffset = offset;
}
else {
backgroundOffset = 0;
}
}
|
The setBackgroundOffset() function calculates the number of pixels to move the background for the current frame by dividing the background's velocity by the current frame rate. Then it adds that value to the current background offset.
To continuously scroll the background, setBackgroundOffset() resets the background offset to 0 when it becomes less than 0 or greater than the width of the background.
If you've ever sat in the passenger's seat of a moving car and watched your hand knife through telephone poles at high speed, you know that things close to you move faster than things that are farther away. That's known as parallax.
Snail Bait is a 2D platformer, but it uses a mild parallax effect to make it appear as though the platforms are closer to you than the background. The game implements that parallax by scrolling the platforms noticeably faster than the background.
Figure 6 illustrates how Snail Bait implements parallax. The top screenshot shows the background at a particular point in time, whereas the bottom screenshot shows the background a few animation frames later. From those two screen shots you can see that the platforms have moved much farther than the background in the same amount of time.
Figure 6. Parallax: The platforms (near) scroll faster than the background (far)
Listing 12 shows the functions that set platform velocities and offsets:
Listing 12. Setting platform velocities and offsets
var PLATFORM_VELOCITY_MULTIPLIER = 4.35;
function setPlatformVelocity() {
// Platforms move 4.35 times as fast as the background
platformVelocity = bgVelocity * PLATFORM_VELOCITY_MULTIPLIER;
}
function setPlatformOffset() {
platformOffset += platformVelocity/fps; // Time-based motion
}
|
Recall Listing 8, which lists Snail Bait's game loop. That loop consists of an animate() function that the browser invokes when it's time to draw the game's next animation frame. That animate() function, in turn, invokes a draw() function that draws the next animation frame. The code for the draw() function at this stage of development is shown in Listing 13:
Listing 13. The
draw() function
function setOffsets() {
setBackgroundOffset();
setPlatformOffset();
}
function draw() {
setPlatformVelocity();
setOffsets();
drawBackground();
drawRunner();
drawPlatforms();
}
|
The draw() function sets the platform velocity and the offsets for both the background and the platforms. Then it draws the background, runner, and platforms.
In the next article, I'll show you how to encapsulate the code for Snail Bait in a JavaScript object to avoid namespace collisions. I will also show you how to pause the game, including how to automatically pause it when the window loses focus, and how to restart the game with a countdown when the window regains focus. You'll also see how to control the game's runner with the keyboard. Along the way, you'll learn how to use CSS transitions and interject functionality into the game loop. See you next time.
| Description | Name | Size | Download method |
|---|---|---|---|
| Sample code | j-html5-game2.zip | 737KB | HTTP |
Information about download methods
Learn
-
Core HTML5 Canvas: (David Geary, Prentice Hall, 2012): David Geary's book offers extensive coverage of the Canvas API and game development. Also check out the companion website and blog.
-
Snail Bait: Play Snail Bait online in any HTML5-enabled browser (best in Chrome version 18+).
-
Mind-blowing apps with HTML5 Canvas: Watch David Geary's presentation from Strange Loop 2011.
-
HTML5 Game Development: Watch David Geary's presentation from the Norwegian Developer's Conference (NDC) 2011.
-
Platform games: Read about platform games at Wikipedia.
-
Side-scroller video games: Read about side-scroller video games at Wikipedia.
-
HTML5 fundamentals: Learn HTML5 basics with this developerWorks knowledge path.
-
Timing control for script-based animations: Check out the specification here.
Get products and technologies
-
Replica Island: You can download the source for this popular open source platform video game for Android.
Discuss
- Get involved in the developerWorks community. Connect with other developerWorks users while exploring the developer-driven blogs, forums, groups, and wikis.

The author of Core HTML5 Canvas, David Geary is also the co-founder of the HTML5 Denver User's Group and the author of eight Java books, including the best-selling books on Swing and JavaServer Faces. David is a frequent speaker at conferences, including JavaOne, Devoxx, Strange Loop, NDC, and OSCON, and he is a three-time JavaOne Rock Star. He wrote the JSF 2 fu and GWT fu article series for developerWorks. You can follow David on Twitter at @davidgeary.



