HTML5 2D game development: Graphics and animation

Drawing into the canvas and putting things in motion

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.

Share:

David Geary, Author and speaker, Clarity Training, Inc.

David GearyThe 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.



02 October 2012

Also available in Chinese Russian Japanese Vietnamese Portuguese

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
Scrolling Snail Bait's 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:

Table 1. Canvas 2D context methods used by Snail Bait
MethodDescription
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() {
   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 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 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;

   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
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.


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, 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 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

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);
         };
      }
   )
();

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 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/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.


Scrolling the background

Snail Bait's background, shown in Figure 3, scrolls slowly in the horizontal direction:

Figure 3. The background image
The snail bait 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)
Identical edges for background images

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
Scrolling Snail Bait's background right to left

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.


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.

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.


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.

Figure 6. Parallax: The platforms (near) scroll faster than the background (far)
Parallax

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.


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.


Download

DescriptionNameSize
Sample codej-html5-game2.zip737KB

Resources

Learn

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.

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology, Open source, Web development
ArticleID=838450
ArticleTitle=HTML5 2D game development: Graphics and animation
publish-date=10022012