Creating the Game Display - Creating the Basic Game - HTML5 Games: Creating Fun with HTML5, CSS3, and WebGL (2012)

HTML5 Games: Creating Fun with HTML5, CSS3, and WebGL (2012)

part 2

Creating the Basic Game

Chapter 7

Creating the Game Display

in this chapter

• Preloading game files

• Creating a prettier background

• Using canvas to create the game display

• Using CSS sprites

Now that you have familiarized yourself with the canvas element, it’s time to put that knowledge to work. The game is sorely missing some visuals, and you can now begin putting things on the screen.

The graphics you create in this chapter aren’t anything complex. Rather, I focus on showing you how to get the basic jewel board up and running. Before getting to that, though, I show you how you can modify the loader to preload game assets. You use that functionality to freshen up the splash screen with a progress bar as well as a new background.

I also show you two different approaches to creating the graphics for the jewel board. One is based on the canvas element and uses some of the techniques you have just learned. The other uses old-fashioned DOM and CSS to create a similar result.

Preloading Game Files

Game images such as sprites and other game art are prime targets for preloading, especially when you need the images for canvas-related purposes. Because the canvas element lets you draw images on it only after they finish loading, having the images ready to use when you need them can simplify your canvas code significantly.

Jewel Warrior has seven different types of jewels. Each type has a different shape and color. I created a set of images for these jewel types; you can find them included in the project archive for this chapter. The size of a jewel sprite depends on the resolution and orientation of the device, so you need sprite images of varying sizes. I included four sets of images in the archive for this chapter. You can find the files in the images folders. Each file is named jewelsNN.png, where NN is 24, 32, 40, 64, or 80, which refers to the width and height of each jewel. Figure 7-1 shows the jewel sprites.

Figure 7-1: The jewel sprites

9781119975083-fg0701

Now add an empty images container object to the jewel namespace. The purpose of this object is to hold any image elements that have already been loaded and are ready to use. Listing 7.1 shows the change in loader.js.

Listing 7.1 Adding the Image Container

var jewel = {

...

images : {}

}

...

Now you can preload the jewel sprites and store them in jewel.images for later use.

Detecting the jewel size

How can you determine which jewel size to use? The JavaScript code does not know the dimensions of the game screen; only the CSS rules are resolution-aware. Passing that information directly from CSS to JavaScript is not really possible, but you can style an HTML element and then pick up its dimensions with JavaScript. Listing 7.2 shows such an element added to index.html.

Listing 7.2 Adding a Jewel Prototype Element to the HTML

<div id=”game”>

...

<div id=”jewel-proto” class=”jewel-size”></div>

</div>

The only function of this jewel-proto element is to act as a connection between CSS and JavaScript. The jewel-size class controls the jewel dimensions with CSS. Listing 7.3 shows the added rules to main.css.

Listing 7.3 Adding CSS for the Jewel Prototype

.jewel-size {

font-size : 40px;

}

#jewel-proto {

position : absolute;

width : 1em;

height : 1em;

left : -1000px;

}

The CSS places the jewel-proto element well outside the view and gives it a width and height of 1em. Now you can let the jewel-size class determine the actual size of the element. You use this class again later to control the size of the game board.

Reading the jewel size with JavaScript is now fairly trivial. The getBoundingClientRect() method returns both the position and dimensions of an element. Just use that in the loader.js script to add a new jewelSize setting, as shown in Listing 7.4.

Listing 7.4 Getting the Jewel Size

var jewel = {

...

}

window.addEventListener(“load”, function() {

// determine jewel size

var jewelProto = document.getElementById(“jewel-proto”),

rect = jewelProto.getBoundingClientRect();

jewel.settings.jewelSize = rect.width;

...

Modernizr.load([

...

]);

}, false);

Modifying the loader script

Now that you know which sprite image to load, you can preload the file in the loader script. I mentioned earlier in the book that the splash screen would also serve as a loading screen. At the moment, the user can click through to the main menu right away, regardless of whether the rest of the files have finished loading. If you can track the progress of the rest of the scripts and image files, you will know when you can allow the user to move forward to the menu. You can use another custom yepnope prefix to keep track of how many files have finished loading. Listing 7.5 shows the new loader prefix added to loader.js.

Listing 7.5 Tracking Loading Progress

var numPreload = 0,

numLoaded = 0;

yepnope.addPrefix(“loader”, function(resource) {

// console.log(“Loading: “ + resource.url)

var isImage = /.+\.(jpg|png|gif)$/i.test(resource.url);

resource.noexec = isImage;

numPreload++;

resource.autoCallback = function(e) {

// console.log(“Finished loading: “ + resource.url)

numLoaded++;

if (isImage) {

var image = new Image();

image.src = resource.url;

jewel.images[resource.url] = image;

}

};

return resource;

});

// loading stage 1

...

The purpose of the prefix is to allow you to track two things: how many files are being loaded and how many of those files have finished loading.

When you add a prefix to yepnope, the associated function is called once for each file with that prefix. This call happens before the files start loading. The loader! prefix first uses a simple regular expression to determine if the resource is an image file and, if it is, stops yepnope from trying to execute it as a script. Then a counter is incremented so that, after the actual loading starts, numPreload equals the number of files that will be loaded.

Prefix functions also allow you to attach a function to the autoCallback property of the resource object. This function is called when the file finishes loading. Here, you use it to increment the numLoaded counter and store all images in the jewel.images container. Now add the new prefix to the files in the second loading stage as shown in Listing 7.6.

Listing 7.6 The Second Loading Stage

// loading stage 2

if (Modernizr.standalone) {

Modernizr.load([

{

test : Modernizr.webworkers,

yep : [

“loader!scripts/board.worker-interface.js”,

“preload!scripts/board.worker.js”

],

nope : “loader!scripts/board.js”

},{

load : [“loader!scripts/screen.main-menu.js”,

“loader!images/jewels”

+ jewel.settings.jewelSize + “.png”

]

}

]);

}

note.eps

The preload! prefix on the board.worker.js script is left untouched. It’s safe to let this file load in the background. The Worker() constructor doesn’t need the file to be loaded, so even in the unlikely event that the game starts before the worker script finishes preloading, things work out fine.

The splash screen needs to be able to track this progress to show a progress bar and to determine when it’s safe to let the user move on. Listing 7.7 shows a function, getLoadProgress(), that returns a value between 0 and 1, indicating how far the loading has progressed.

Listing 7.7 Getting the Current Loader Progress

...

function getLoadProgress() {

if (numPreload > 0) {

return numLoaded / numPreload;

} else {

return 0;

}

}

// loading stage 1

Modernizr.load([

...

]);

Of course, the splash screen doesn’t have access to getLoadProgress(). One solution is to modify the showScreen() in the game module so it accepts parameters and passes them on to the activated screen modules. This can also prove helpful in other scenarios, such as passing the player score to the high score screen after the game has ended. Listing 7.8 shows the showScreen() function in game.js modified to accept extra parameters.

Listing 7.8 Adding Arguments to Screen Modules

jewel.game = (function() {

...

// hide the active screen (if any) and show the

// screen with the specified id

function showScreen(screenId) {

var activeScreen = $(“#game .screen.active”)[0],

screen = $(“#” + screenId)[0];

if (activeScreen) {

dom.removeClass(activeScreen, “active”);

}

// extract screen parameters from arguments

var args = Array.prototype.slice.call(arguments, 1);

// run the screen module

jewel.screens[screenId].run.apply(

jewel.screens[screenId], args

);

// display the screen html

dom.addClass(screen, “active”);

}

...

})();

The showScreen() function should support any number of parameters that must be passed on to the relevant screen module. It does so by calling the run() method on the screen module via its apply() method and passing it the remaining arguments that were used to call showScreen().

In case you are unfamiliar with this procedure, let me explain what goes on in showScreen(). When a function is called, all arguments are accessible via an object called arguments, even if they are not named like screenId. The arguments object is what is called array-like; that is, it behaves somewhat like an array. Like an array, arguments is a list of elements and has a length property that indicates how many elements (that is, arguments) it has. The screenId argument corresponds to arguments[0], so any remaining elements should be passed on to the screen module.

Although arguments may look like an array, it doesn’t have array functions like slice(), which you could have used here to slice out the remaining elements of the arguments object. What you can do, though, is use the slice() function from Array.prototype on the arguments object. In JavaScript, functions are objects just like everything else. All functions have a call() method, which can be used to invoke the function as if it were called on another object. You simply pass this other object as the first parameter to call(). If the function takes any parameters, they should be passed after the object. The array functions don’t require the object they’re called on to be a true array; any array-like object works fine. If arguments had its own slice() method, the expression

Array.prototype.slice.call(arguments, 1)

would be the same as

arguments.slice(1)

Functions also have a method called apply(). This method is similar to call(), but instead of supplying the function parameters directly as arguments to call(), you supply an array of values as the second parameter. The showScreen() function uses this to call run() on the screen module using its own remaining arguments.

note.eps

At one point, the fifth edition of ECMAScript was actually supposed to make the arguments object a true array, but this idea was abandoned because it would potentially break existing web sites. One key difference between arguments and arrays is that elements inarguments are bound to the named arguments. For example, in showScreen(), changing the value of arguments[0] would also change the value of screenId and vice versa.

Now you can pass the getLoadProgress() function as a parameter to the splash screen module. Listing 7.9 shows the change to the first loading stage in loader.js.

Listing 7.9 Passing the Progress Tracker to the Splash Screen

// loading stage 1

Modernizr.load([

{

load : [

“scripts/sizzle.js”,

“scripts/dom.js”,

“scripts/game.js”

]

},{

test : Modernizr.standalone,

yep : “scripts/screen.splash.js”,

nope : “scripts/screen.install.js”,

complete : function() {

jewel.game.setup();

if (Modernizr.standalone) {

jewel.game.showScreen(“splash-screen”,

getLoadProgress);

} else {

jewel.game.showScreen(“install-screen”);

}

}

}

]);

The progress function is now being passed to the splash screen so it can use the function to track how the loading progresses. You can use the same pattern in other situations like, for example, to pass score values to the high score screen.

Adding a progress bar

Now that the splash screen can access the progress value, you can also add a visual cue on the splash screen that informs the user what is going on. A simple progress bar goes a long way. First, add a few div elements to the splash screen HTML in index.html. Listing 7.10 shows the new tags.

Listing 7.10 The Splash Screen HTML with Progress Bar

<div id=”game”>

...

<div class=”screen” id=”splash-screen”>

<h1 class=”logo”>Jewel <br/>Warrior</h1>

<div class=”progress”>

<div class=”indicator”></div>

</div>

<span class=”continue”>Click to continue</span>

</div>

...

</div>

HTML5 actually introduces a new progress tag that you could have used if not for the fact that there’s no widespread support for it yet. Had it been supported across the board, the HTML for it could have looked something like the following:

<progress class=”loader” value=”0” max=”100” />

Changing the value attribute would automatically change the appearance of the element as well. You can easily make your own progress bar, though. Create two nested elements like the ones in Listing 7.10 and let the outer element define the overall dimensions and border of the progress bar. You can then simply scale the CSS width of the inner element progressively from 0% to 100% to indicate the progress. Listing 7.11 shows the new CSS rules in main.css.

Listing 7.11 Styling the Progress Bar

#splash-screen .continue {

cursor : pointer;

font-size : 0.75em;

display : none;

}

...

/* Progress bar */

.progress {

margin : 0 auto;

width : 6em;

height : 0.5em;

border-radius : 0.5em;

overflow : hidden;

border : 1px solid rgb(200,200,100);

}

.progress .indicator {

background-color : rgb(200,200,100);

height : 100%;

width : 0%;

}

The CSS modifications in Listing 7.11 also hide the continue text. It shouldn’t be visible until the splash screen determines that it is safe to continue. The .progress styles just center the progress bar and give it some rounded corners and the same color as the game text. If you load the splash screen now, you’ll see the empty progress bar beneath the game logo. You can test the progress bar by manually setting the width of the .indicator element. You can do this in the browser’s JavaScript console by entering, for example:

Sizzle(“.progress .indicator”)[0].style.width = “25%”;

Let’s move on to the splash screen module and modify it so it checks the loading progress and updates the progress bar. Listing 7.12 shows the revised screen.splash.js.

Listing 7.12 Updating the Progress Bar

jewel.screens[“splash-screen”] = (function() {

var game = jewel.game,

dom = jewel.dom,

$ = dom.$,

firstRun = true;

function setup(getLoadProgress) {

var scr = $(”#splash-screen”)[0];

function checkProgress() {

var p = getLoadProgress() * 100;

$(“.indicator”,scr)[0].style.width = p + “%”;

if (p == 100) {

$(“.continue”,scr)[0].style.display = “block”;

dom.bind(scr, “click”, function() {

jewel.game.showScreen(“main-menu”);

});

} else {

setTimeout(checkProgress, 30);

}

}

checkProgress();

}

function run(getLoadProgress) {

if (firstRun) {

setup(getLoadProgress);

firstRun = false;

}

}

return {

run : run

};

})();

Because of the changes you implemented in the loader in Listings 7.8 and 7.9, the run() method now accepts the getLoadProgress() function as a parameter. It simply passes the parameter on to the setup() function, which is a bit more interesting. The setup() function starts a cycle that keeps calling checkProgress() until getLoadProgress() returns 1. The indicator HTML element is scaled proportionally to the progress value, and as soon as the progress value reaches 1, the continue text is displayed and the click event handler is attached, allowing the user to proceed.

Try loading the game now. You should see the progress bar on the splash screen filling up as the files load. If the files are on your local drive, the loading might happen so fast that you can barely see it. At the very least, there should a brief flash.

Improving the Background

Earlier in the book, I mentioned that you would improve the dull background a bit. At the moment, it’s just a bland, solid gray color. You can give it just a bit more style. Start by adding a new background div element to the game container in index.html, as shown in Listing 7.13.

Listing 7.13 Adding the Background Markup

<div id=”game”>

<div class=”background”></div>

...

</div>

Make sure you add it before any of the screen elements, so the background is always displayed underneath any other content. Listing 7.14 shows the new CSS rules added to main.css.

Listing 7.14 The Background Canvas Styles

#game .background {

position : absolute;

left : 0;

top : 0;

width : 100%;

height : 100%;

z-index : 0;

}

#game .background canvas {

width : 100%;

height : 100%;

}

You should create the background relatively early, so do it in the setup() function in the game module. Listing 7.15 shows the modified game.js with the new createBackground() function.

Listing 7.15 Creating the Background Canvas

jewel.game = (function() {

...

// create background pattern

function createBackground() {

if (!Modernizr.canvas) return;

var canvas = document.createElement(“canvas”),

ctx = canvas.getContext(“2d”),

background = $(“#game .background”)[0],

rect = background.getBoundingClientRect(),

gradient,

i;

canvas.width = rect.width;

canvas.height = rect.height;

ctx.scale(rect.width, rect.height);

gradient = ctx.createRadialGradient(

0.25, 0.15, 0.5,

0.25, 0.15, 1

);

gradient.addColorStop(0, “rgb(55,65,50)”);

gradient.addColorStop(1, “rgb(0,0,0)”);

ctx.fillStyle = gradient;

ctx.fillRect(0, 0, 1, 1);

ctx.strokeStyle = “rgba(255,255,255,0.02)”;

ctx.strokeStyle = “rgba(0,0,0,0.2)”;

ctx.lineWidth = 0.008;

ctx.beginPath();

for (i=0;i<2;i+=0.020) {

ctx.moveTo(i, 0);

ctx.lineTo(i - 1, 1);

}

ctx.stroke();

background.appendChild(canvas);

}

function setup() {

...

createBackground();

}

...

})();

Modernizr also lets you test for canvas capability, and this feature is used here to immediately bail out if the canvas element isn’t supported. If the canvas element is supported, Modernizr creates a new canvas element. The dimensions of the canvas element are automatically figured out by using the getBoundingClientRect() DOM method on the background. Because the CSS dimensions are set to 100%, this makes the canvas fill the entire game screen. When you scale the canvas coordinates using the ctx.scale() method on the context, the rest of the canvas code can use unit space without regard to the actual size of the canvas.

The background texture is relatively simple and is composed of two elements. First, there’s a radial gradient going from a gray tone to black. On top of this gradient, I added a diagonally striped pattern in a lighter color. A simple background like this is enough to make everything a bit more visually appealing. The fact that it doesn’t depend on a specific resolution or aspect ratio also makes it easy to use across different devices and screens. Figure 7-2 shows the splash screen with the new background.

Figure 7-2: The new background

9781119975083-fg0702

Building the Game Screen

Finally, we reach the actual game screen. Start by adding the game screen element to index.html, as shown in Listing 7.16.

Listing 7.16 The Game Screen HTML

<div id=”game”>

...

<div class=”screen” id=”game-screen”>

<div class=”game-board jewel-size”>

</div>

</div>

</div>

Just one element appears on the game screen for now, the game-board element, and that is just a container for the actual jewel board. Notice that the game-board element also uses the jewel-size class that you defined earlier. The jewel-size class specifies the size of a single jewel by setting the font-size value. You can then use em units to scale the board. Give the element the correct dimensions in main.css, as shown in Listing 7.17.

Listing 7.17 The Game Board CSS

#game-screen .game-board {

position : relative;

width : 8em;

height : 8em;

}

Now you can create a new screen module for the game screen. Listing 7.18 shows the initial code in screen.game.js.

Listing 7.18 The Game Screen Module

jewel.screens[“game-screen”] = (function() {

var board = jewel.board,

display = jewel.display;

function run() {

board.initialize(function() {

display.initialize(function() {

// start the game

});

});

}

return {

run : run

};

})();

The run() method of the game screen module uses the asynchronous initialize() function on the board module from the previous chapters. The callback function tries to initialize the display module, but will, of course, fail because it doesn’t exist yet. Remember to add the new file to theloader.js script. Place it in the second loading stage after screen.main-menu.js:.

Drawing the board with canvas

Now you can move on quickly to the display module. I show you two different takes on how to create the first round of graphics for this game. The first approach is based on the canvas element. Listing 7.19 shows the initial canvas display module, display.canvas.js.

Listing 7.19 The Canvas Display Module

jewel.display = (function() {

var dom = jewel.dom,

$ = dom.$,

canvas, ctx,

cols, rows,

jewelSize,

firstRun = true;

function setup() {

var boardElement = $(”#game-screen .game-board”)[0];

cols = jewel.settings.cols;

rows = jewel.settings.rows;

jewelSize = jewel.settings.jewelSize;

canvas = document.createElement(”canvas”);

ctx = canvas.getContext(”2d”);

dom.addClass(canvas, ”board”);

canvas.width = cols * jewelSize;

canvas.height = rows * jewelSize;

boardElement.appendChild(canvas);

}

function initialize(callback) {

if (firstRun) {

setup();

firstRun = false;

}

callback();

}

return {

initialize : initialize

}

})();

The first time the module is initialized, the setup() function is called. This function creates a canvas element, sets the dimensions according to the jewel size, and adds it to the game board container. Add the file to the second stage of the loader in loader.js, as shown in Listing 7.20.

Listing 7.20 Loading the Canvas Display Module

// loading stage 2

if (Modernizr.standalone) {

Modernizr.load([

{

test : Modernizr.webworkers,

yep : [

“loader!scripts/board.worker-interface.js”,

“preload!scripts/board.worker.js”

],

nope : “loader!scripts/board.js”

},{

load : [

“loader!scripts/display.canvas.js”,

“loader!scripts/screen.main-menu.js”,

“loader!scripts/screen.game.js”,

“loader!images/jewels”

+ jewel.settings.jewelSize + “.png”

]

}

]);

}

Creating the board background

Before drawing the jewels, add a semitransparent, checkered pattern in the background of the board. As long as the foreground content — in this case, the jewels — doesn’t need to interact with the background, you can use separate canvas elements for the two layers. Always needing to make sure that the background remains nice when modifying foreground content can affect both the performance and the complexity of the code. In Listing 7.21, you see how to create a second canvas element and add it to the document before the foreground content.

Listing 7.21 Adding a Background Pattern

jewel.display = (function() {

...

function createBackground() {

var background = document.createElement(“canvas”),

bgctx = background.getContext(“2d”);

dom.addClass(background, “background”);

background.width = cols * jewelSize;

background.height = rows * jewelSize;

bgctx.fillStyle = “rgba(225,235,255,0.15)”;

for (var x=0;x<cols;x++) {

for (var y=0;y<cols;y++) {

if ((x+y) % 2) {

bgctx.fillRect(

x * jewelSize, y * jewelSize,

jewelSize, jewelSize

);

}

}

}

return background;

}

function setup() {

...

boardElement.appendChild(createBackground());

boardElement.appendChild(canvas);

}

...

})();

The pattern itself is not that interesting. The nested loops iterate over the entire board and fill semitransparent squares in every other cell. Now add the CSS rules shown in Listing 7.22 to main.css.

Listing 7.22 Adding Game Board CSS Rules

#game-screen .game-board .board-bg,

#game-screen .game-board .board {

position : absolute;

width : 100%;

height : 100%;

}

#game-screen .game-board .board {

z-index : 10;

}

#game-screen .game-board .board-bg {

z-index : 0;

}

Both the background canvas and board canvas are positioned absolute to place them on top of each other. Figure 7-3 shows the game screen with the checkered board background.

Now, the jewel size is set only in main.css. That’s no good on mobile devices where different sizes might be needed. Listing 7.23 shows the modified rules for small screens in mobile.css.

Listing 7.23 Adjusting Jewel Size for Small Screens

/* smartphones landscape */

@media (orientation: landscape) {

...

.jewel-size {

font-size : 32px;

}

}

/* small screens landscape */

@media (orientation: landscape) and (max-device-width : 480px) {

...

.jewel-size {

font-size : 24px;

}

}

Figure 7-3: The game board background

9781119975083-fg0703

Similarly, large screen devices like the iPad need different jewel sizes for portrait and landscape modes. Listing 7.24 shows the changes in mobile.css.

Listing 7.24 Adjusting Jewel Size for iPad and Tablets

/* use a bigger base size for tablets */

@media (min-device-width: 768px) {

...

#game-screen .game-board {

margin : 0.75em;

}

.jewel-size {

font-size : 80px;

}

}

...

/* tablets landscape */

@media (orientation: landscape) and (min-device-width : 768px) {

...

#game-screen .game-board {

margin : 0.25em;

}

.jewel-size {

font-size : 64px;

}

}

Filling the board with jewels

Time to draw some jewels! The display module needs a function that simply draws the entire board based on the board data. Listing 7.25 shows the new functions added to the display.canvas.js module.

Listing 7.25 The Display Redraw Function

jewel.display = (function() {

var jewels,

...

function drawJewel(type, x, y) {

var image = jewel.images[“images/jewels” +

jewelSize + “.png”];

ctx.drawImage(image,

type * jewelSize, 0, jewelSize, jewelSize,

x * jewelSize, y * jewelSize,

jewelSize, jewelSize

);

}

function redraw(newJewels, callback) {

var x, y;

jewels = newJewels;

ctx.clearRect(0,0,canvas.width,canvas.height);

for (x = 0; x < cols; x++) {

for (y = 0; y < rows; y++) {

drawJewel(jewels[x][y], x, y);

}

}

callback();

}

return {

initialize : initialize,

redraw : redraw

};

})();

The redraw() function first clears the entire canvas before iterating over all board positions to paint a jewel image in each cell. A drawJewel() helper function takes care of the actual drawing. Note that the redraw() function gets the jewel data from the caller. The display module has no connection to the board module and has no concept of the game state other than what it is told to draw.

The drawJewel() function uses the jewelSize setting to get the correct sprite sheet from the jewel.images container. Because the jewel sprites were preloaded, an image object is ready to be used in the ctx.drawImage() call.

Triggering the initial redraw

Now you just need to call the initialize() function, and when it finishes, call the first redraw(). Listing 7.26 shows the additions to the screen.game.js module.

Listing 7.26 Triggering the Initial Redraw

jewel.screens[“game-screen”] = (function() {

...

function run() {

...

board.initialize(function() {

display.initialize(function() {

display.redraw(board.getBoard(), function() {

// do nothing for now

});

});

});

}

...

})();

Now you can start the game, and you should see something similar to Figure 7-4. Load the game and click the Play button on the main menu.

Figure 7-4: The game screen with jewels

9781119975083-fg0704

Your work on the Canvas display is done for now. You haven’t implemented any user interaction with the game yet, so you have no way of making the jewels move.

Drawing the board with CSS and images

Before this chapter concludes, you’ll learn how to use regular DOM scripting and CSS to achieve roughly the same result as you would with canvas. You can use this to create a fallback in case canvas is not supported. Because Jewel Warrior is an HTML5 game, it doesn’t really target browsers that don’t have canvas capability, but knowing how to adjust for older browsers when necessary and possible is worthwhile.

Alternatively, you can use one of the available canvas polyfills such as FlashCanvas (http://flashcanvas.net/) or explorercanvas (http://code.google.com/p/explorercanvas/). The current game display doesn’t use much of the canvas drawing API, though, and as you see in the following section, replicating it with DOM is relatively easy.

Creating the DOM display module

Start by creating a new display module in display.dom.js. It should expose the same methods as the canvas module, so the two can be interchanged transparently. Listing 7.27 shows the initial module.

Listing 7.27 The DOM Display Module

jewel.display = (function() {

var dom = jewel.dom,

$ = dom.$,

cols, rows,

jewelSize,

firstRun = true,

jewelSprites;

function initialize(callback) {

// initialize the board

}

function redraw(jewels, callback) {

// redraw board

}

return {

initialize : initialize,

redraw : redraw

};

})();

Using CSS sprites

For the DOM display, you use a technique known as CSS sprites. This technique is useful if you have several small images that you can group together in one larger sprite image. The idea is to use that larger image as the CSS background in a container element. If the dimensions of the element equal those of a single sprite, only one image is ever visible. You can then use the CSS background position to switch between the various sprites in the sprite image. Figure 7-5 shows the basic idea.

Figure 7-5: CSS sprites

9781119975083-fg0705

You can easily manage and modify CSS sprites via code, and they decrease loading time significantly due to the fewer number of HTTP requests. The jewel images that you used in the canvas-based display are already grouped together in single images, so they are ready to be used as CSS sprites without further modification.

Creating the jewel sprites

The first time the display is initialized, you need to set up a few things and create some elements. Listing 7.28 shows the new setup() routine in display.dom.js:

Listing 7.28 Initializing the Jewel Board

jewel.display = (function() {

...

function setup() {

var boardElement = $(“#game-screen .game-board”)[0],

container = document.createElement(“div”),

sprite,

x, y;

cols = jewel.settings.cols;

rows = jewel.settings.rows;

jewelSize = jewel.settings.jewelSize;

jewelSprites = [];

for (x=0;x<cols;x++) {

jewelSprites[x] = [];

for (y=0;y<cols;y++) {

sprite = document.createElement(“div”);

dom.addClass(sprite, “jewel”);

sprite.style.left = x + “em”;

sprite.style.top = y + “em”;

sprite.style.backgroundImage =

“url(images/jewels” + jewelSize + “.png)”;

sprite.style.backgroundSize =

(jewel.settings.numJewelTypes * 100) + “%”;

jewelSprites[x][y] = sprite;

container.appendChild(sprite);

}

}

dom.addClass(container, “dom-container”);

boardElement.appendChild(container);

}

function initialize(callback) {

if (firstRun) {

setup();

firstRun = false;

}

callback();

}

...

})();

The setup() function creates a container div element to hold the jewel sprites. The nested loops create a sprite div element for each position on the board. The jewel size is used to assign the appropriate jewel background image before the jewel div is added to the container. The sprite divelements are also stored in a two-dimensional array so you can easily reference them later.

Because the size of the board could change later as a result of the user switching device orientation, the selected jewel background might end up too small or too large. One way to fix this situation would be to check the background images whenever an orientation change is detected. Another, simpler fix is to make sure the background scales with the board. By setting the backgroundSize property to numJewelTypes * 100%, you can make sure that a single jewel always fits the size of its div element. Listing 7.29 shows the new CSS rules added to main.css.

Listing 7.29 The DOM Jewel Board CSS

#game-screen .game-board .dom-container {

position : absolute;

width : 100%;

height : 100%;

}

#game-screen .game-board .dom-container .jewel {

position : absolute;

width : 1em;

height : 1em;

overflow : hidden;

}

The only thing missing to draw the jewels is the redraw() function. Add the code in Listing 7.30 to display.dom.js.

Listing 7.30 Drawing the Jewels

jewel.display = (function() {

...

function drawJewel(type, x, y) {

var sprite = jewelSprites[x][y];

sprite.style.backgroundPosition = type + “em 0em”;

sprite.style.display = “block”;

}

function redraw(jewels, callback) {

var x, y;

for (x = 0; x < cols; x++) {

for (y = 0; y < rows; y++) {

drawJewel(jewels[x][y], x, y, 0, 0)

}

}

callback();

}

...

})();

Unlike the redraw() function in the canvas-based display module, this function doesn’t so much draw the jewels as it changes them. The div elements with the background image are already there; you just need to adjust their background position. Because the font-size of the board is set specifically to match the size of a jewel, the number of em units you need to offset the background is simply equal to the jewel type.

Creating the board background

Now, take a look at how to do the background. One way to create the checkered pattern is with pure CSS. With the addition of gradients in CSS3, you can now create very cool and interesting patterns with just a bit of CSS. Listing 7.31 shows the CSS necessary to create a checkered pattern for the game board.

Listing 7.31 Creating a CSS Pattern with Gradients

#game-screen .game-board .board-bg {

background-image:

-webkit-linear-gradient(

45deg, rgba(255,255,255,0.15) 25%,

transparent 25%, transparent 75%,

rgba(255,255,255,0.15) 75%, rgba(255,255,255,0.15)

),

-webkit-linear-gradient(

45deg, rgba(255,255,255,0.15) 25%,

transparent 25%, transparent 75%,

rgba(255,255,255,0.15) 75%, rgba(255,255,255,0.15)

);

background-size : 2em 2em;

background-position:0 0, 1em 1em;

}

This listing shows just the WebKit-specific version; gradients are still at a stage where the standard hasn’t completely settled in yet, and you need to use vendor-specific properties. In addition to the hassle of creating multiple rules for different browsers, this approach unfortunately doesn’t always work well on, for example, Android or iDevices.

tip.eps

You can find many other nice patterns created entirely with CSS at the CSS3 Patterns site created by Lea Verou: http://leaverou.me/css3patterns/.

Alternatively, you can create a div element for each cell that should be colored white. This method is well supported and reliable. Listing 7.32 shows the background creation added to the display.dom.js module.

Listing 7.32 Creating the Checkered Background Pattern

jewel.display = (function() {

...

function setup() {

...

boardElement.appendChild(createBackground());

}

function createBackground() {

var x, y, cell,

background = document.createElement(“div”);

for (x=0;x<cols;x++) {

for (y=0;y<cols;y++) {

if ((x+y) % 2) {

cell = document.createElement(“div”);

cell.style.left = x + “em”;

cell.style.top = y + “em”;

background.appendChild(cell);

}

}

}

dom.addClass(background, “board-bg”);

return background;

}

...

})();

This approach is similar to how the canvas display did the background. A small div element is added at every other cell position. Now add the CSS rule in Listing 7.33 to main.css to give these div elements a semitransparent white color.

Listing 7.33 Adding DOM Background CSS Rules

#game-screen .game-board .board-bg div {

position : absolute;

width : 1em;

height : 1em;

background-color : rgba(225,235,255,0.15);

}

And the DOM version of the display module is done — for now, at least. Modify the second loading stage to switch to the DOM display if the browser doesn’t support canvas. Listing 7.34 shows the modified loader in loader.js.

Listing 7.34 Loading the DOM Display Module

// loading stage 2

if (Modernizr.standalone) {

Modernizr.load([

{

test : Modernizr.canvas,

yep : “loader!scripts/display.canvas.js”,

nope : “loader!scripts/display.dom.js”

},{

test : Modernizr.webworkers,

yep : [

“loader!scripts/board.worker-interface.js”,

“preload!scripts/board.worker.js”

],

nope : “loader!scripts/board.js”

},{

load : [

“loader!scripts/screen.main-menu.js”,

“loader!scripts/screen.game.js”,

“loader!images/jewels”

+ jewel.settings.jewelSize + “.png”

]

}

]);

}

You can simulate missing canvas support to trigger the DOM display by setting Modernizr.canvas to false before the loading starts. By doing so, you override whatever value Modernizr decided on in its feature detection and consequently toggle the DOM fallback display.

note.eps

If you truly want to support older browsers such as earlier editions of Internet Explorer, you need to make more changes to the code in general — for example, the standards-based event binding code in dom.js.

Summary

This chapter got you started with the game display, and the basic display routines for rendering the game board are now in place. You used the canvas element to finally put some jewels on the screen and also saw how to use regular DOM scripting and CSS to create similar results.

In the beginning of the chapter, you heavily modified the loader script so that it now supports preloading of both scripts and image files. This modification allowed you to put a progress bar on the splash screen to track the loading progress.

In the next chapter, you make more couplings between the display and board logic by adding user interactions. You then use those interactions to expand the capabilities of the display modules.