Gameplay in HTML5: Sprite Sheets

Objective

Using a separate graphics file for every item we draw is convenient, but often inefficient. On many platforms, moving many separate files/bitmaps around is slower and/or requires more overhead than one or a few larger files. On the Web, in particular, each file request entails considerable overhead.
Since the Canvas API drawImage() method allows us to specify a source rectangle, we can combine many pictures together. (CSS allows the same thing.) Graphics files that contain several pictures to be displayed independently are called sprite sheets.
We will first see how we can use the Canvas API to create a sprite sheet of our playing cards, and then how we can modify our game code to use the sprite sheet.

Creating a sprite sheet using Canvas

Overview

We could use a graphics tool, such as GIMP, Photoshop, or ImageMagick to create a sprite sheet. Instead we will temporarily modify our game app to fill a canvas with the card images and then run a routine to convert that canvas to an image that can be downloaded.

HTML

We will temporarily add a new screen div to our HTML:
      <div id="cardSheetScreen" class="screen">
        <div id="cardSheetContent">
          <canvas id="cardSheetCanvas"></canvas>
          <img id="cardSheetImage" />
        </div>
        <nav>
          <button type="button" id="captureButton" name="captureImage">
            Capture image
          </button>
          <button type="button" name="mainMenu">Exit</button>
        </nav>
      </div>
          

CSS

The main CSS we need is to position both the canvas and img in the same place. (Only one will be shown at a time.)
#cardSheetCanvas,
#cardSheetImage
{
    position: absolute;
    left: 0;
    top: 0;
}
          

JavaScript

Setup

Our screen starts with
         function run( )
         {
             setupCardImages( );
             setTimeout( drawCards, 10 );
             setEventHandlers( );
         }
            
setupCardImages() is the same as in the game's view module. The time-out should give the images a chance to sync with their src URLs.

Drawing the cards

Then we set up our canvas and draw all of the cards in a 13x4 array:
         function drawCards( )
         {
             var cardWidth, cardHeight,
                 ctx,
                 s, numSuits = app.playingCards.numSuits,
                 r, numRanks = app.playingCards.numRanks,
                 x, y;
                 
             if ( canvas )
                 return;
             
             cardWidth = cardImages[0][0].width;
             cardHeight = cardImages[0][0].height;

             canvas = $("#cardSheetCanvas")[0];
             ctx = canvas.getContext( "2d" );
             canvas.width = numRanks * cardWidth;
             canvas.height = numSuits * cardHeight;

             ctx.clearRect( 0, 0, canvas.width, canvas.height );
             for ( s = 0; s < numSuits; ++s )
             {
                 y = s * cardHeight;
                 for ( r = 0; r < numRanks; ++r )
                 {
                     x = r * cardWidth;
                     ctx.drawImage( cardImages[ s ][ r ], x, y );
                 }
             }
         }
            

Capturing the canvas to an image

Our "Capture image" button handler uses the Canvas toDataURL() method. This function returns a data: URL string representing a snapshot of the canvas. (By default it uses the PNG format, which is good for us.) This is assigned as the src of our img element. Then we hide the canvas and show the image (and change our button).
         function captureImage( )
         {
             var dataUrl = canvas.toDataURL();
             ($("#cardSheetImage")[0]).src = dataUrl;

             $("#cardSheetCanvas").hide( );
             $("#cardSheetImage").show( );
             $("#captureButton").attr( "name", "returnCanvas" ).
                 text( "Return to canvas" );
         }
            
If we now right-click on the graphic, we get the usual "Save image as..." option, allowing us to download the sprite sheet.

Returning to the canvas

We toggled the button so its handler now allows us to return to the canvas:
         function returnCanvas( )
         {
             $("#cardSheetCanvas").show( );
             $("#cardSheetImage").hide( );
             $("#captureButton").attr( "name", "captureImage" ).
                 text( "Capture image" );
         }
            

Other uses

Besides creating sprite sheets, this technique could be used to allow users to capture snapshots of their game. Note that we would have to restrict our drawing to a single canvas to make this work. We would also need to pause the game time, etc., to allow for the interruption. Alternately, we could display the image in a separate tab or window, perhaps.

Limitations

For security reasons, toDataURL() (and getImageData()) are only allowed if the canvas has not had any images from another domain drawn on it. Unfortunately, images loaded as file:///... are considered "tainted", so you need to run the app from an HTTP server, such as Apache.

Example

Example 1

From the JavaScript console, run

            app.showScreen( "cardSheetScreen" );
          

Using the cards sprite sheet

Overview

Now that we have a sprite sheet containing all of our cards in a single file, we modify our code to use it.

Loader

Our Loader.js module now loads a single cards file:
         function listCardUrls( )
         {
             return [ getCardsUrl() ];
         }
         
    //-------------------------------------------------------------------------

         function getCardsUrl( )
         {
             return "images/cards-75.png";
         }
          

View

Our functions to load the card image and determine the card dimensions change to
         function setupCardImages( )
         {
             cardsImage = $("<img />")[0];
             cardsImage.src = app.loader.getCardsUrl( );
         }
        
    //-------------------------------------------------------------------------

         function setCardSize( )
         {
             cardSize.width = cardsImage.width / app.playingCards.numRanks;
             cardSize.height = cardsImage.height / app.playingCards.numSuits;
         }
          
We also draw cards a little differently:
         function drawCard( card, pos, moving, rotation )
         {
             var cardWidth = cardSize.width,
                 cardHeight = cardSize.height,
                 srcX = card.rank * cardWidth,
                 srcY = card.suit * cardHeight;
             ctx.save( );
             if ( moving )
             {
                 ctx.shadowColor = "black";
                 ctx.shadowOffsetX = 0.05 * cardWidth;
                 ctx.shadowOffsetY = 0.05 * cardHeight;
                 ctx.shadowBlur = 5;
             }
             if ( rotation )
             {
                 ctx.translate( pos.x  +  cardWidth / 2,
                                pos.y  +  cardHeight / 2 );
                 ctx.rotate( rotation );
                 ctx.translate( - (pos.x  +  cardWidth / 2),
                                - (pos.y  +  cardHeight / 2) );
             }
             ctx.drawImage( cardsImage,
                            srcX, srcY, cardWidth, cardHeight,
                            pos.x, pos.y, cardWidth, cardHeight );
             ctx.restore( );
         }
          

Example

Example 2