HTML5 2D game development: Graphics and animation
Drawing into the canvas and putting things in motion
David Geary 02 October 2012
Author and speaker
Clarity Training, Inc.
In this series, HTML5 maven David Geary shows you how to implement an HTML5 2D video
game one step at a time. This installment covers Canvas graphics and HTML5 animation. You'll
see how to draw the game's graphics and how to set them in motion. You will also learn the
best way to animate with HTML5, how to scroll the background, and how to implement parallax
to simulate three dimensions.
View more content in this series
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:
© Copyright IBM Corporation 2012 Trademarks
HTML5 2D game development: Graphics and animation Page 1 of 18
developerWorks® [Link]/developerWorks/
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.
Immediate-mode graphics
Canvas is an immediate-mode graphics system, meaning it immediately draws what you
specify and then immediately forgets. Other graphics systems, such as Scalable Vector
Graphics (SVG), implement retained-mode graphics, which means they maintain a list of
objects to draw. Without the overhead of maintaining a display list, Canvas is faster than
SVG; however, if you want to maintain a list of objects that users can manipulate, you must
implement that functionality on your own in Canvas.
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.)
HTML5 Canvas overview
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:
HTML5 2D game development: Graphics and animation Page 2 of 18
[Link]/developerWorks/ developerWorks®
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.
Path-based graphics
Like Apple's Cocoa and Adobe's Illustrator, the Canvas API is path-based, meaning you draw
graphics primitives in a canvas by creating a path and then subsequently stroking or filling
that path. The strokeRect() and fillRect() methods are convenience methods that
stroke or fill a rectangle, respectively.
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() {
[Link](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() {
[Link](background, 0, 0);
}
The versatile drawImage() method
You can draw an entire image, or any rectangular area within an image, anywhere inside
a canvas with the Canvas 2D context's drawImage() method, optionally scaling the
image along the way. Besides images, you can also draw the contents of another canvas
or the current frame of a video element with drawImage(). It's only one method, but
HTML5 2D game development: Graphics and animation Page 3 of 18
developerWorks® [Link]/developerWorks/
drawImage() facilitates straightforward implementations of interesting and otherwise
difficult-to-implement applications such as video-editing software.
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;
[Link](); // Save the current state of the context
[Link](-platformOffset, 0); // Translate the coord system for all platforms
for (var i=0; i < [Link]; ++i) {
data = platformData[i];
top = calculatePlatformTop([Link]);
[Link] = PLATFORM_STROKE_WIDTH;
[Link] = PLATFORM_STROKE_STYLE;
[Link] = [Link];
[Link] = [Link];
[Link]([Link], top, [Link], [Link]);
[Link] ([Link], top, [Link], [Link]);
}
[Link](); // 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 [Link]() translates the canvas's coordinate system — depicted in
Figure 2 — by a specified number of pixels in the horizontal direction. That translation and the
HTML5 2D game development: Graphics and animation Page 4 of 18
[Link]/developerWorks/ developerWorks®
attribute settings are temporary because they're made between calls to [Link]() and
[Link]().
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 [Link]().
I discuss scrolling the background with [Link]() 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.
HTML5 animations
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)
Best practice
Never use setTimeout() or setInterval() for time-critical animations.
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,
HTML5 2D game development: Graphics and animation Page 5 of 18
developerWorks® [Link]/developerWorks/
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().
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
HTML5 2D game development: Graphics and animation Page 6 of 18
[Link]/developerWorks/ developerWorks®
specifications progress rapidly, constantly incorporating new technology and refining existing
functionality.
Polyfills: Programming for the future
In the past, most cross-platform software was implemented for the lowest common
denominator. Polyfills turn that notion on its head by giving you access to advanced features
if they are available and falling back to a less-capable implementation when necessary.
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
[Link] =
(function () {
var originalWebkitRequestAnimationFrame = undefined,
wrapper = undefined,
callback = undefined,
geckoVersion = 0,
userAgent = [Link],
index = 0,
self = this;
// Workaround for Chrome 10 bug where Chrome
// does not pass the time to the animation function
if ([Link]) {
// Define the wrapper
wrapper = function (time) {
if (time === undefined) {
time = +new Date();
}
[Link](time);
};
// Make the switch
originalWebkitRequestAnimationFrame = [Link];
[Link] = function (callback, element) {
[Link] = callback;
HTML5 2D game development: Graphics and animation Page 7 of 18
developerWorks® [Link]/developerWorks/
// 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 ([Link]) {
// Check the Gecko version. Gecko is used by browsers
// other than Firefox. Gecko 2.0 corresponds to
// Firefox 4.0.
index = [Link]('rv:');
if ([Link]('Gecko') != -1) {
geckoVersion = [Link](index + 3, 3);
if (geckoVersion === '2.0') {
// Forces the return statement to fall through
// to the setTimeout() function.
[Link] = undefined;
}
}
}
return [Link] ||
[Link] ||
[Link] ||
[Link] ||
[Link] ||
function (callback, element) {
var start,
finish;
[Link]( function () {
start = +new Date();
callback(start);
finish = +new Date();
[Link] = 1000 / 60 - (finish - start);
}, [Link]);
};
}
)
();
Polyfill: The definition
The word polyfill is a portmanteau of polymorphism and backfill. Like polymorphism, polyfills
select appropriate code at run time, and they backfill missing functionality.
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
HTML5 2D game development: Graphics and animation Page 8 of 18
[Link]/developerWorks/ developerWorks®
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.
The game loop
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/[Link]'></script>
<script src='[Link]'></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,
HTML5 2D game development: Graphics and animation Page 9 of 18
developerWorks® [Link]/developerWorks/
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 = [Link]('fps');
function calculateFps(now) {
var fps = 1000 / (now - lastAnimationFrameTime);
lastAnimationFrameTime = now;
if (now - lastFpsUpdateTime > 1000) {
lastFpsUpdateTime = now;
[Link] = [Link](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.
Scrolling the background
Snail Bait's background, shown in Figure 3, scrolls slowly in the horizontal direction:
Figure 3. The background image
HTML5 2D game development: Graphics and animation Page 10 of 18
[Link]/developerWorks/ developerWorks®
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
HTML5 2D game development: Graphics and animation Page 11 of 18
developerWorks® [Link]/developerWorks/
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() {
[Link](-backgroundOffset, 0);
// Initially onscreen:
[Link](background, 0, 0);
// Initially offscreen:
[Link](background, [Link], 0);
[Link](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.
Time-based motion
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.
Best practice
Animation speed must be independent of frame rate.
HTML5 2D game development: Graphics and animation Page 12 of 18
[Link]/developerWorks/ developerWorks®
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 < [Link]) {
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.
Parallax
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.
HTML5 2D game development: Graphics and animation Page 13 of 18
developerWorks® [Link]/developerWorks/
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:
HTML5 2D game development: Graphics and animation Page 14 of 18
[Link]/developerWorks/ developerWorks®
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.
Next time
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.
HTML5 2D game development: Graphics and animation Page 15 of 18
developerWorks® [Link]/developerWorks/
Downloads
Description Name Size
Sample code [Link] 737KB
HTML5 2D game development: Graphics and animation Page 16 of 18
[Link]/developerWorks/ developerWorks®
Resources
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.
HTML5 2D game development: Graphics and animation Page 17 of 18
developerWorks® [Link]/developerWorks/
About the author
David Geary
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.
© Copyright IBM Corporation 2012
([Link]/legal/[Link])
Trademarks
([Link]/developerworks/ibm/trademarks/)
HTML5 2D game development: Graphics and animation Page 18 of 18