How to use canvas, SVG, and multi-touch to create a tiled puzzle game
Starting from the basics and concluding with a multi-touch image-puzzle game that uses both canvas and SVG, this tutorial describes, in a graduated way, how to handle pointer events such as those generated from a mouse, pen, or one or more fingers (multi-touch).
Note Pointer events require Windows 8 or later.
Note The Internet Explorer pointer events implementation has changed slightly since this article was written for Internet Explorer 10. For details on how to update and future-proof your code, see Pointer Events updates.
Introduction
In Internet Explorer 10 and Windows Store apps using JavaScript, developers can use a type of input called a pointer. A pointer, in this context, can be any point of contact on the screen made by a mouse, pen, finger, or multiple fingers. This tutorial first describes how to get started with pointers, and then walks through the implementation of a multi-pointer, image-puzzle game that utilizes both canvas and SVG:
If you already know how to use mouse events, pointer events should look very familiar to you: MSPointerDown, MSPointerMove, MSPointerUp, MSPointerOver, MSPointerOut, and so on. Handling mouse and pointer events is straightforward, as shown in Example 1 next. Additionally, for multi-touch enabled devices, Example 1 works as-is with as many simultaneous points of contact (fingers) as the device is capable of handling. This is possible because the pointer events fire for each point of screen contact. So apps like the following elementary drawing example support multi-touch without any special coding:
Example 1 - elementary drawing app
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>Example 1</title>
<style>
html {
-ms-touch-action: double-tap-zoom;
}
</style>
</head>
<body>
<canvas id="drawSurface" width="500" height="500" style="border: 1px black dashed;"></canvas>
<script>
var _canvas = document.getElementById("drawSurface");
var _context = _canvas.getContext("2d");
_context.fillStyle = "rgba(255, 0, 0, 0.5)";
if (navigator.msPointerEnabled) {
_canvas.addEventListener('MSPointerMove', paintCanvas, false);
}
else {
_canvas.addEventListener('mousemove', paintCanvas, false);
}
function paintCanvas(evt) {
_context.fillRect(evt.clientX, evt.clientY, 5, 5);
}
</script>
</body>
</html>
Normally, the browser consumes touch events for its own purposes. For example, to scroll a webpage up, the user can touch the screen (not on a link) and pull down. Or to zoom in a page, an expanding two finger pinch action can be used. In Example 1, we don’t want these default behaviors to occur, otherwise creating a drawing with one (or more) fingers would result in panning (or possibly zooming) the page instead. To allow these events to flow to your JavaScript code, we use the following CSS:
html {
-ms-touch-action: double-tap-zoom;
}
This instructs the browser to "ignore" all touch actions except for a double-tap (which zooms in a page). In other words, all touch events are now available to your JavaScript code except for the ability to capture the double-tap event. The other possible values for -ms-touch-action
are auto
, none
, manipulation
, and inherit
as described in Guidelines for Building Touch-friendly Sites.
From a addEventListener
perspective, it’s important to note that Windows Internet Explorer pointer events and traditional mouse events are mutually exclusive, meaning when pointer events are available, they encompass mouse events as well. In other words, you can’t concurrently register the paintCanvas
event listener with both mousemove
and MSPointerMove
:
// DO NOT DO THE FOLLOWING:
_canvas.addEventListener('mousemove', paintCanvas, false);
_canvas.addEventListener('MSPointerMove', paintCanvas, false);
Instead, if MS pointer events, which also report mouse events, are available, we use them. Otherwise, we use traditional mouse events:
if (window.navigator.msPointerEnabled) {
_canvas.addEventListener('MSPointerMove', paintCanvas, false);
}
else {
_canvas.addEventListener('mousemove', paintCanvas, false);
}
Thus, the previous code fragment allows the drawing app to work with either touch-enabled or traditional (non-touch) devices.
With the basics of pointer events understood, we move on to a more realistic usage – the implementation of an image puzzle game:
Slicing an image
The first step in creating such a game is creating the image pieces or tiles. The canvas API’sdrawImage method allows you to easily slice up a source image to a canvas, by setting portions of the image to copy or display. The syntax is drawImage(image, i_x, i_y, i_width, i_eight, c_x, c_y, c_width, c_height)
The next two illustrations show how portions of an <img> element are selected and displayed on a canvas:
Source Image
Canvas
By breaking the source puzzle image up into a series of rows and columns (a table), the drawImage method can be applied to each table cell, resulting in the required individual image tiles:
This tile generating process is demonstrated in Example 2. To view the source code for example 2, right-click the Example 2 page and choose View source. The discussion of Example 2 is broken into the follow two sections:
X-UA-Compatible meta tag
Because Example 2 was developed on a local intranet using Internet Explorer, the <meta http-equiv="X-UA-Compatible" content="IE=10">
tag was used to ensure that Internet Explorer is placed in the correct browser and document mode. For more info about this, see Defining Document Compatibility. In general, this tag should be removed just before the page goes into production.
Image slicing
To convert the above 400 by 400 puzzle image into useful puzzle pieces (or tiles), we first create an in-memory image object of the image to be sliced (400x400.png) and invoke an anonymous function when the image has fully loaded:
var image = new Image();
image.src = "images/400x400.png";
image.onload = function () { ... }
Based on the size of the source image (image.width
and image.height
) and the desired number of rows and columns to break the image up into (NUM_COLS
and NUM_ROWS
), we calculate the required tile size within the anonymous function:
var dx = _canvas.width = image.width / NUM_COLS;
var dy = _canvas.height = image.height / NUM_ROWS;
We set the canvas width and height to be the same size as a tile because we’ll be creating all such tiles from this canvas. The individual tile images are created as follows:
for (var row = 0; row < NUM_ROWS; row++) {
for (var col = 0; col < NUM_COLS; col++) {
ctx.drawImage(image, dx * col, dy * row, dx, dy, 0, 0, dx, dy);
ctx.strokeRect(0, 0, dx, dy); // Place a border around each tile.
slicedImageTable.push( _canvas.toDataURL() );
}
}
To better understand this doubly nested for
loop, assume that we want a 5 row by 5 column puzzle and that the variables row
and col
are currently 2 and 3 respectively. That is, we are currently in table cell (2, 3) of the source image:
If the size of the source image is 400 by 400 pixels, then:
dx = canvas.width = 400 / 5 = 80
dy = canvas.height = 400 / 5 = 80
Which results in:
ctx.drawImage(image, 80*3, 80*2, 80, 80, 0, 0, 80, 80)
Or:
ctx.drawImage(image, 240, 160, 80, 80, 0, 0, 80, 80)
In other words, we take an 80 by 80 snapshot of the source image at position (240, 160):
And place the snapshot in the top left corner of an 80px by 80px canvas:
This canvas is then converted to a data URL image and stored in the sliced image array, as shown here:
slicedImageTable.push( _canvas.toDataURL() );
The rest of Example 2 ensures that the puzzle image was successfully sliced up by displaying the individual tiles in the same order in which they were acquired (sliced).
Converting image slices to SVG
Now that we can generate image slices (that is, tiles), we next investigate how to convert these data URL image tiles into SVG image objects. This process is demonstrated in example 3. To view the output of example 3, your browser's debugger console window must open. For more information, see How to use F12 Developer Tools to Debug your Webpages. The main difference between Examples 2 and 3 is in how SVG image elements are created and set, as shown here:
for (var row = 0; row < NUM_ROWS; row++) {
for (var col = 0; col < NUM_COLS; col++) {
ctx.drawImage(image, dx*col, dy*row, dx, dy, 0, 0, dx, dy);
ctx.strokeRect(0, 0, dx, dy); // Place a border around each tile.
var svgImage = document.createElementNS('http://www.w3.org/2000/svg', 'image');
svgImage.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", _canvas.toDataURL());
svgImage.setAttribute('width', dx);
svgImage.setAttribute('height', dy);
svgImage.setAttribute('x', dx * col);
svgImage.setAttribute('y', dy * row);
svgImage.correctX = dx * col;
svgImage.correctY = dy * row;
slicedImageTable.push(svgImage);
}
}
Because SVG is a form of XML, a namespace must be specified when creating an SVG element (at least from outside of the SVG object model):
var svgImage = document.createElementNS('http://www.w3.org/2000/svg', 'image')
SVG image elements use an href
attribute as opposed to the src
attribute (which is used with the non-SVG <img>
element). Also, be aware that for Internet Explorer, svgImage.setAttribute('href', _canvas.toDataURL())
can be used to set the href
attribute of the SVG image elements. Other browsers, however, might require XLink syntax, which is why the following is used instead:
svgImage.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", _canvas.toDataURL());
The default width
and height
of an SVG image is 0
, so we must explicitly set these values:
svgImage.setAttribute('width', dx);
svgImage.setAttribute('height', dy);
Lastly, with respect to the coordinate system associated with the SVG viewport (see SVG Coordinate Transformations), we create and set two custom properties, correctX
and correctY
, in order to record where each tile should be in a correct (that is, won) puzzle.
Displaying SVG images
Now that we have positioned SVG images stored in the tile array, our next task is to display them on the screen. To do so easily, we add an SVG element <svg>
to the webpage along with a few other augmentations as discussed in the next three sections and as shown in Example 4 (right-click to view source).
Liquid SVG
In addition to simply adding an SVG element, we also use a few CSS properties to make the SVG viewport entirely liquid (or fluid). The first item to consider is the SVG element itself:
<svg width="75%" height="75%" viewbox="0 0 400 400"></svg>
Here, the square SVG viewport is 75% of the smallest browser viewport dimension, and a 400 by 400 unit coordinate system is applied to it. In order for liquid SVG to work as desired, make sure the following CSS rules are applied:
The
html
andbody
elements require aheight
of100%
:html, body { margin: 0; padding: 0; height: 100% }
Now, as the browser’s viewport is reduced in size, so are the contents of the SVG viewport. Note that the inherent 400 by 400 unit SVG viewport coordinate system remains intact – only the size of the coordinate units change.
Like the
<img>
element, the SVG element is an inline element. So to center it within the browser’s viewport, we set itsdisplay
property toblock
and itsleft
andright
margins toauto
:svg { display: block; margin: 0 auto; }
Feature detection
Because we’re using addEventListener, canvas, and SVG, let's detect for these features before attempting to display a puzzle. Example 4 does this as shown here:
if (window.addEventListener) {
window.addEventListener('load', main, false);
} else {
document.getElementsByTagName('body')[0].innerHTML = "<h1>The addEventListener method is not supported - please upgrade your browser and try again.</h1>";
} // if-else
function main() {
var game = {};
game.canvas = document.createElement('canvas');
game.hasCanvas = !!game.canvas.getContext;
game.svg = document.getElementsByTagName('svg')[0];
game.hasSVG = game.svg.namespaceURI == "http://www.w3.org/2000/svg";
.
.
.
if (!game.hasCanvas) {
document.getElementsByTagName('body')[0].innerHTML = "<h1>Canvas is not supported - please upgrade your browser and try again.</h1>";
return;
}
if (!game.hasSVG) {
document.getElementsByTagName('body')[0].innerHTML = "<h1>SVG is not supported - please upgrade your browser and try again.</h1>";
return;
}
.
.
.
} // main
As you can see, we only enter the main
function if the addEventListener
method is supported (by the way, if addEventListener
isn’t supported, it’s extremely unlikely that canvas or SVG will be either).
After we’re in the main
function, we create a game
variable to contain all our game related "global" variables and state. As you can see, if the user’s browser doesn't support all the required features, a message is displayed identifying the issue.
When we’re certain that all the required features are supported, we continue with exactly the same approach that we used in Example 3, except that some of the variable names have changed, such as NUM_ROWS
to game.numRows
and slicedImageTable
to game.tiles
. Then, after we’ve completed creating all our SVG image elements (tiles) in the doubly nested for
loop, we display them, as discussed next.
Displaying tiles
With each tile’s position already calculated and set, we can display the SVG tile images simply by appending them to the SVG element:
function displaySvgImages() {
for (var i = 0; i < game.tiles.length; i++) {
game.svg.appendChild(game.tiles[i]);
}
}
To verify the liquidity (or fluidity) of the SVG tiles, change the size of the browser’s window. Note how the SVG viewport is always 75% of the smallest dimension (width or height) of the browser’s viewport.
Moving tiles
Now that we have the puzzle tiles displayed, our next challenge is to allow the user to move them with a pointer device. We’ll explore this problem by first examining how to move three SVG circles, as shown in Example 5 (right-click to view source).
We first create a global array to contain which circles are currently active. That is, which circles have been "clicked" as indicated by a mousedown
or MSPointerDown
event:
var _activeCircles = [];
To help explain the following code fragment, if a mousemove
event handler were attached directly to the SVG circles (as seems reasonable), it (unfortunately) becomes possible for the user to move the mouse rapidly enough to "lose" a circle. That is, the movement of the SVG circle cannot keep up with the user’s very rapid mouse movements. Assuming the mousemove
event handler is responsible for moving a circle across the screen, as soon as the mouse and circle are physically separated, the mousemove
event handler ceases to execute and consequently the circle’s movement stops. This is why we attach the mousemove
event handler to the window
object instead of the SVG circles – no matter how fast a user moves the mouse, it can never lose the omnipresent window
object:
if (navigator.msPointerEnabled) {
window.addEventListener('MSPointerMove', handlePointerEvents, false);
} else {
window.addEventListener('mousemove', handlePointerEvents, false);
}
As shown in the previous code fragment, we register a MSPointerMove
pointer event handler if available (which handles traditional mouse events as well) and if not, we register a mousemove
event handler. Recall that you cannot register for both event types (MSPointerMove
and mousemove
) on the same object (as describe in Example 1).
Next, we register pointer down and pointer up event handlers on each SVG circle element:
var svgCircles = document.getElementsByTagName('circle');
for (var i = 0; i < svgCircles.length; i++) {
if (navigator.msPointerEnabled) {
svgCircles[i].addEventListener('MSPointerDown', handlePointerEvents, false);
svgCircles[i].addEventListener('MSPointerUp', handlePointerEvents, false);
} else {
svgCircles[i].addEventListener('mousedown', handlePointerEvents, false);
svgCircles[i].addEventListener('mouseup', handlePointerEvents, false);
}
} // for
Pay close attention to the handlePointerEvents
function, as we'll discuss this next:
function handlePointerEvents(evt) {
var activeCircle;
var activeCircleIndex = evt.pointerId || 0;
switch (evt.type) {
case "mousedown":
case "MSPointerDown":
_svgElement.removeChild(evt.target);
_svgElement.appendChild(evt.target);
if (evt.pointerId) {
evt.target.msSetPointerCapture(evt.pointerId);
}
_activeCircles[activeCircleIndex] = evt.target;
break;
case "mousemove":
case "MSPointerMove":
activeCircle = _activeCircles[activeCircleIndex];
if (activeCircle) {
var svgPoint = _svgElement.createSVGPoint();
svgPoint.x = evt.clientX;
svgPoint.y = evt.clientY;
var ctm = activeCircle.getScreenCTM();
svgPoint = svgPoint.matrixTransform(ctm.inverse());
activeCircle.setAttribute('cx', svgPoint.x);
activeCircle.setAttribute('cy', svgPoint.y);
} // if
break;
case "mouseup":
case "MSPointerUp":
if (evt.pointerId) {
_activeCircles[activeCircleIndex].msReleasePointerCapture(evt.pointerId);
}
delete _activeCircles[activeCircleIndex];
break;
default:
alert("Error in handlePointerEvents on: " + evt.type);
} // switch
} // handlePointerEvents
To help describe handlePointerEvents
, we’ll walk through two scenarios - moving a single circle, then moving two circles simultaneously.
Single circle movement
There are three events to handle: down, move, and up.
Down event
When the user touches a single circle (with the mouse, pen, or finger), the down event fires, which invokes handlePointerEvents
. If pointer events are supported, evt.pointerId
will be non-null and activeCircleIndex
will equal evt.pointerId
; otherwise, activeCircleIndex
will be 0
(thanks to evt.pointerId || 0
). If evt.pointerId
is null
, only one circle can be active at a time, namely _activeCircles[0]
, which is the only possibility when a mouse is the only allowed pointer device.
Next, the switch
statement checks evt.type
and shunts control flow to the mousedown
/MSPointerDown
clause. To ensure that the active circle is always on top of all the others, we simply remove and then append it to the DOM (the last element appended is always the last/top element rendered).
Then, if evt.pointerId
is defined, we call msSetPointerCapture on evt.target
(that is, the active circle) so that the circle can continue to receive all registered events. This allows the circle to be physically pulled off of and back onto the browser’s viewport.
Lastly, we record the touched circle in the list of active circles:
_activeCircles[activeCircleIndex] = evt.target;
Move event
When the pointing device is moved (on the window
object), the mousemove
/MSPointerMove
event handler (handlePointerEvents
) executes, which shunts control flow to the mousemove
/MSPointerMove
clause of the switch
statement. In the event that a circle had not previously been touched, the _activeCircles
array will be empty and activeCircle = _activeCircles[activeCircleIndex]
will be null
. In this case, we jump straight to the break
statement and exit the switch
statement.
If, on the other hand, activeCircle
is not null
(that is, there’s an active circle that should be moved), we convert the coordinates of the pointing device (evt.clientX
, evt.clientY
), which are relative to the browser’s viewport, to the 400 by 400 SVG coordinate system. This is done through the coordinate transformation matrix (CTM). For more info, see SVG Coordinate Transformations.
Lastly, we move the circle’s center (cx
, cy
) to the transformed coordinate values:
activeCircle.setAttribute('cx', svgPoint.x);
activeCircle.setAttribute('cy', svgPoint.y);
Up event
If an up event fires on a circle, control flow shunts to the mouseup
/MSPointerUp
clause of the switch
statement. The touched/clicked circle, indicated by activeCircleIndex
, is removed from the active circle list and, if applicable, its capture events request is released via msRelasePointerCapture
.
Dual circle movement
As in the single circle case, there are three events to handle: down, move, and up.
Down events
In this case, two (nearly simultaneous) circle down event objects flow into the mousedown
/MSPointerDown
clause of the switch
. The _activeCircles
array now contains two circle objects (whose indices that access them are the associated evt.pointerId
values).
Move events
As the two (nearly simultaneous) window move event objects flow into the mousemove
/MSPointerMove
clause of the switch
, each circle is moved in turn, just as in the single circle case.
Up events
As each (nearly simultaneous) circle up event object flows into the mouseup
/MSPointerUp
clause, each is cleaned up in turn, just as in the single circle case.
Image puzzle
We now have all the essential pieces required to create a fully functional (but not necessarily captivating) touch-friendly image-puzzle game. We’ll break this rather lengthy discussion up into a number easily digestible components. We'll start with Example 6, the skeletal framework for the game (the completed game is presented in Example 7 and is discussed later).
Skeletal framework
As opposed to the game presented in Advanced SVG Animation, the skeletal framework for the puzzle is relatively straightforward, as shown in Example 6 (right-click to view source). The core markup of Example 6 is as follows:
<table>
<tr>
<td colspan="3"><h1>A Multi-Touch Enabled Image Puzzle Using Canvas & SVG</h1></td>
</tr>
<tr>
<td id="playButtonWrapper"><button id="playButton">Play</button></td>
<td id="messageBox">Always carefully study the image before clicking the play button!</td>
<td id="scoreBox"><strong>Score:</strong> 0</td>
</tr>
</table>
<svg width="75%" height="75%" viewBox="0 0 400 400">
<rect width="100%" height="100%" style="fill: black; stroke: black;" />
</svg>
The <rect>
element is used to color the entire SVG viewport black (regardless of its current size). This provides a "playing field" for the puzzle’s tile pieces. This effect can be seen in the following Example 6 screen shot:
The rest of the skeletal frame for Example 6 consists of the following JavaScript:
if (window.addEventListener) {
window.addEventListener('load', main, false);
} else {
document.getElementsByTagName('body')[0].innerHTML = "<h1>The addEventListener method is not supported - please upgrade your browser and try again.</h1>";
} // if-else
function main() {
var imageList = ["images/puzzle0.png", "images/puzzle1.png", "images/puzzle2.png", "images/puzzle3.png", "images/puzzle4.png", "images/puzzle5.png"]; // Contains the paths to the puzzle images (and are cycled through as necessary).
var game = new Game(2, imageList);
function Game(size_in, imageList_in) {
var gameData = {};
gameData.elements = {};
gameData.elements.canvas = document.createElement('canvas');
gameData.elements.svg = document.getElementsByTagName('svg')[0];
this.hasCanvas = !!gameData.elements.canvas.getContext;
this.hasSVG = gameData.elements.svg.namespaceURI == "http://www.w3.org/2000/svg";
this.init = function() { alert("The init() method fired.") };
} // Game
if (!game.hasCanvas) {
document.getElementsByTagName('body')[0].innerHTML = "<h1>Canvas is not supported - please upgrade your browser and try again.</h1>";
return;
}
if (!game.hasSVG) {
document.getElementsByTagName('body')[0].innerHTML = "<h1>SVG is not supported - please upgrade your browser and try again.</h1>";
return;
}
// Assert: The user's browser supports all required features.
game.init(); // Start the game.
} // main
As described in the earlier feature detection section, if addEventListener
is available, we fire the main
function. Then, we initialize an array, imageList
, which contains the paths to the images to be used in the game (that is, the images to be broken up into tiles, randomized, and solved). If the user makes it to the seventh level, the first image (puzzle0.png) is recycled - ad infinitum.
We next invoke the Game
constructor function. The first parameter, 2
, instructs the constructor to generate a game
object that has two columns and two rows. The last parameter is, of course, the list of puzzle images to cycle through (if necessary).
Within the constructor, we place all of its "global" variables into a handy gameData
variable. If you’re unfamiliar with the this
keyword, the this.hasCanvas = !!game.elements.canvas.getContext
statement creates and sets a property called hasCanvas
on the object that the constructor constructed (the game
variable). The double negation (!!
) simply forces the game.elements.canvas.getContext
expression to a Boolean value (true
if canvas is supported).
Similarly, this.init = function() { … }
defines a method, called init
, for all objects created by the constructor (there’s just one such object, game
). Invoking game.init()
, among other things, starts the game.
A multi-touch image-puzzle game
We are now in a position to combine all of the above info into a fully functioning multi-touch image-puzzle game, as revealed in Example 7. The source code associated with Example 7 should look familiar and is well commented, however the following two components might be clearer with additional explanation:
Tile randomization
In the createAndAppendTiles
function, we generate the SVG tile image objects (as in Example 4). But before appending them to the SVG element, we randomize them and make sure that the resulting randomized pattern doesn’t happen to exactly match the original (full) puzzle image (which is really only an issue on the first, 4-tile, level):
var randomizationOK = false;
while (!randomizationOK) {
coordinatePairs.sort(function() { return 0.5 - Math.random(); });
for (var i = 0; i < gameData.tiles.length; i++) {
if (gameData.tiles[i].correctX != coordinatePairs[i].x ||
gameData.tiles[i].correctY != coordinatePairs[i].y) {
randomizationOK = true;
break;
} // if
} // for
} // while
To easily randomize the tiles, we place their associated coordinate pairs in an array (coordinatePairs
) and use the JavaScript array sort method as follows:
coordinatePairs.sort(function() { return 0.5 - Math.random(); });
As described in sort Method (JavaScript) and given that Math.random()
returns a value between 0
and 1
, this anonymous sort function randomly sorts the array’s elements.
Extended pointer up clause
The down and move clauses within the switch
statement are nearly identical to prior examples. The up clause, however, is significantly extended:
case "mouseup":
case "MSPointerUp":
activeTile = gameData.activeTiles[activeTileIndex];
var currentX = activeTile.getAttribute('x');
var currentY = activeTile.getAttribute('y');
for (var i = 0; i < gameData.tiles.length; i++) {
var correctX = gameData.tiles[i].correctX;
var correctY = gameData.tiles[i].correctY;
if (currentX >= (correctX - gameData.snapDelta) && currentX <= (correctX + gameData.snapDelta) &&
currentY >= (correctY - gameData.snapDelta) && currentY <= (correctY + gameData.snapDelta)) {
activeTile.setAttribute('x', correctX);
activeTile.setAttribute('y', correctY);
break;
} // if
} // for
if (evt.pointerId) {
gameData.activeTiles[activeTileIndex].msReleasePointerCapture(evt.pointerId);
}
delete gameData.activeTiles[activeTileIndex];
if (gameData.inProgress) {
for (var i = 0; i < gameData.tiles.length; i++) {
currentX = Math.round(gameData.tiles[i].getAttribute('x'));
currentY = Math.round(gameData.tiles[i].getAttribute('y'));
correctX = Math.round(gameData.tiles[i].correctX);
correctY = Math.round(gameData.tiles[i].correctY);
if (currentX != correctX || currentY != correctY) {
return;
} // if
} // for
// Assert: The user has solved the puzzle.
gameData.inProgress = false;
gameData.score += gameData.size * gameData.size;
gameData.elements.scoreBox.innerHTML = "<strong>Score:</strong> " + gameData.score;
++(gameData.size);
var randomIndex = Math.floor(Math.random() * gameData.congrats.length);
document.getElementById('messageBox').innerHTML = gameData.congrats[randomIndex];
for (var i = 0; i < gameData.tiles.length; i++) {
gameData.elements.svg.removeChild(gameData.tiles[i])
}
createAndAppendTiles();
gameData.elements.playButton.innerHTML = "Play";
} // if
break;
The first component of the up clause to discuss is tile snapping. For many puzzle games (including this one), it's necessary for an in-flight puzzle piece (tile) to snap into place when moved appropriately close to its correct location. The code used for doing so, as copied from the prior example, is shown here:
activeTile = gameData.activeTiles[activeTileIndex];
var currentX = activeTile.getAttribute('x');
var currentY = activeTile.getAttribute('y');
for (var i = 0; i < gameData.tiles.length; i++) {
var correctX = gameData.tiles[i].correctX;
var correctY = gameData.tiles[i].correctY;
if (currentX >= (correctX - gameData.snapDelta) && currentX <= (correctX + gameData.snapDelta) &&
currentY >= (correctY - gameData.snapDelta) && currentY <= (correctY + gameData.snapDelta)) {
activeTile.setAttribute('x', correctX);
activeTile.setAttribute('y', correctY);
break; // We've positioned the active tile correctly, so exit the FOR loop now.
} // if
} // for
In this code fragment, we get the position of the active tile (when the up event fires) and iterate through all of the tiles to determine the correct positions for any tile. If the position of the active tile is "close enough" to one of these correct positions, we snap it into place and immediately break out of the for
loop (in that there’s no need to look at any of the other correct positions). The if
clause metaphorically draws a small collision detection box around the center of a correct position and if the position of the active tile falls within it, the clause becomes true
and snaps the tile into place via the following two setAttribute
method calls:
if (currentX >= (correctX - gameData.snapDelta) && currentX <= (correctX + gameData.snapDelta) &&
currentY >= (correctY - gameData.snapDelta) && currentY <= (correctY + gameData.snapDelta)) {
activeTile.setAttribute('x', correctX);
activeTile.setAttribute('y', correctY);
break;
} // if
Be aware that as gameData.snapDelta
is increased, so is the size of the collision detection box, making tile snapping less sensitive.
Next, if the game is currently in progress, we check to see if the placement of the active tile was the final and correct one by iterating through all of the tiles and checking a ’la brute force:
if (gameData.inProgress) {
for (var i = 0; i < gameData.tiles.length; i++) {
currentX = Math.round(gameData.tiles[i].getAttribute('x'));
currentY = Math.round(gameData.tiles[i].getAttribute('y'));
correctX = Math.round(gameData.tiles[i].correctX);
correctY = Math.round(gameData.tiles[i].correctY);
if (currentX != correctX || currentY != correctY) {
return;
} // if
} // for
If not all the tiles are in their correct positions, we immediately exit handlePointerEvents
and wait for the next pointer event to trigger handlePointerEvents
. Otherwise, the user has solved the puzzle and the following code is executed:
gameData.inProgress = false;
gameData.score += gameData.size * gameData.size;
gameData.elements.scoreBox.innerHTML = "<strong>Score:</strong> " + gameData.score;
++(gameData.size);
var randomIndex = Math.floor(Math.random() * gameData.congrats.length);
document.getElementById('messageBox').innerHTML = gameData.congrats[randomIndex];
for (var i = 0; i < gameData.tiles.length; i++) {
gameData.elements.svg.removeChild(gameData.tiles[i])
}
createAndAppendTiles();
gameData.elements.playButton.innerHTML = "Play";
Because this level of the game has been won, we set gameData.inProgress
to false
and display the new score. Given that the current size (the number of rows and columns) of the game is also used to indicate the current level of the game (that is, how many puzzles the user has solved so far) and because the difficulty of the game is proportional to the square of its row count (or column count, since they’re the same), the score is increased by the square of game’s current size:
gameData.score += gameData.size * gameData.size;
We then increment our game level proxy, gameData.size
, and display a random quip from an array of possible "you won" sentences:
++(gameData.size);
var randomIndex = Math.floor(Math.random() * gameData.congrats.length);
document.getElementById('messageBox').innerHTML = gameData.congrats[randomIndex];
Finally, we remove any pre-existing SVG image elements (tiles) that are attached to the SVG element to prepare for the next round of (different) SVG image elements that will be created and appended to the SVG element via createAndAppendTiles
:
for (var i = 0; i < gameData.tiles.length; i++) {
gameData.elements.svg.removeChild(gameData.tiles[i])
}
createAndAppendTiles();
gameData.elements.playButton.innerHTML = "Play";
Our goal here was to show you, through this tutorial, how to handle multi-touch events in a reasonably realistic scenario – the implementation of a touch friendly puzzle game. The guidance here should provide you with sufficient knowledge to handle touch events in a number of circumstances (possibly including those involving canvas and SVG).
Related topics
Guidelines for Building Touch-friendly Sites
How to simulate hover on touch-enabled devices