Introduction
This article explains how to create a maze game with a digital timer using HTML5 (the canvas
-element and JavaScript, without the use of Flash).
Important note: This maze doesn't work if your browser doesn't support JavaScript or HTML5 (the canvas
-element to be specific). And if you download the maze, but if it is not working properly, then try the online demo at https://programfox.github.io/HTML5-Maze/[^].
Another important note: When I tested the game, it worked fine offline and online in Firefox, Opera and Safari. However, in Chrome and Internet Explorer, I wasn't able to move the blue rectangle if the file was not hosted. This is for security reasons: every file on the file system has a different origin, and you can load files from a different origin to your canvas
(then it is a tainted canvas[^]), but if you try to use getImageData()
on a tainted canvas, you get a security error in Chrome and Internet Explorer. For these browsers, you should go to the online demo to try out the maze, or host the downloaded files on the localhost
server.
After downloading the maze, don't forget to unzip all files (the HTML file and the image), otherwise the maze doesn't appear.
Generating the Maze
The first step to create a maze, is to get an image of the maze. To create a maze image, I use this online Maze Maker: http://www.hereandabove.com/maze/mazeorig.form.html[^]
This is the maze which the Maze Maker generated:
Positioning Elements and Drawing the Maze on the Canvas-Element
Positioning and Drawing
The next step is to position the rectangle which you can move using the arrow keys or WASD, and to position the end point (a circle on my maze). In my maze, the position of the rectangle is 425, 3px
and the size is 15px Z 15px
and the center point of the circle is 524px, 122px
and the radius is 7 px
. First, you need to create the canvas
-element:
<canvas width="616" height="556" id="mazecanvas">
Can't load the maze game, because your browser doesn't support HTML5.</canvas>
<noscript>JavaScript is not enabled. To play the game, you should enable it.</noscript>
The browser will show "Can't load the maze game ...
" if the browser doesn't support HTML5. The size of the maze is 556 x 556, but I did set the width to 616 because I added also a (digital) timer. The next step is to draw the maze to the canvas
-element using JavaScript. I use the drawImage
method to draw the maze image, I use the beginPath
, closePath
, rect
and fill
method to draw the rectangle and I use the beginPath
, closePath
, arc
and fill
method to draw the circle:
var canvas = document.getElementById("mazecanvas");
var context = canvas.getContext("2d");
var currRectX = 425;
var currRectY = 3;
var mazeWidth = 556;
var mazeHeight = 556;
var intervalVar;
function drawMazeAndRectangle(rectX, rectY) {
makeWhite(0, 0, canvas.width, canvas.height);
var mazeImg = new Image();
mazeImg.onload = function () {
context.drawImage(mazeImg, 0, 0);
drawRectangle(rectX, rectY, "#0000FF", false, true);
context.beginPath();
context.arc(542, 122, 7, 0, 2 * Math.PI, false);
context.closePath();
context.fillStyle = '#00FF00';
context.fill();
};
mazeImg.src = "577080/maze.gif";
}
function drawRectangle(x, y, style) {
makeWhite(currRectX, currRectY, 15, 15);
currRectX = x;
currRectY = y;
context.beginPath();
context.rect(x, y, 15, 15);
context.closePath();
context.fillStyle = style;
context.fill();
}
To draw the maze on your canvas
, add this code at the bottom of your script:
drawMazeAndRectangle(425, 3);
currRectX
and currRectY
represent the position of the rectangle, and intervalVar
is the variable for the timer, which we will create later.
After beginning a path (using the beginPath()
method), you can use the rect()
method to create a rectangle. But if you only use the rect()
method, no rectangle is drawn. To draw the created rectangle on your canvas
-element, you should use the fill()
or stroke()
method to actually draw the rectangle on the canvas
. The fill()
method fills a path, and the stroke()
method draws a path.
Name | Type | Description |
x | Number | The x-coordinate, in pixels, of the top-left corner of the rectangle |
y | Number | The y-coordinate, in pixels, of the top-left corner of the rectangle |
w | Number | The width of the rectangle, in pixels |
h | Number | The height of the rectangle, in pixels |
The stroke() and fill() methods have no parameters. |
The arc()
method draws an arc on the canvas
. After drawing the arc, you should call the stroke()
or fill()
method. You can use this method to draw a circle or a part of a circle.
Name | Type | Description |
x | Number | The x-coordinate, in pixels, of the center point of the arc |
y | Number | The y-coordinate, in pixels, of the center point of the arc |
radius | Number | The radius of the arc, in pixels |
startAngle | Number | The starting angle, in radians. 0 is at the 3 o'clock position of the arc's circle (see image) |
endAngle | Number | The ending angle, in radians |
antiClockwise | Boolean | true if the arc should be drawn in counterclockwise direction (from start to end)
false if the arc should be drawn in clockwise direction (from start to end) |
Image:
In the previous code snippet, you saw that I used a makeWhite()
function. I created this function myself. It makes a rectangle on the canvas
white. So, why don't I use the clear
method? Because the clear()
method makes all pixels in a given rectangle transparent, which we don't need in the maze. This is the code of the makeWhite()
function:
function makeWhite(x, y, w, h) {
context.beginPath();
context.rect(x, y, w, h);
context.closePath();
context.fillStyle = "white";
context.fill();
}
The next thing to implement is to make it possible to move the blue rectangle using the arrow keys or WASD. The first step is to calculate the new X and Y-coordinate of the blue rectangle. The second step is to move the rectangle, if it can move, and to show a "Congratulations!
" message if the blue rectangle reached the end point (the green circle):
function moveRect(e) {
var newX;
var newY;
var canMove;
e = e || window.event;
switch (e.keyCode) {
case 38:
case 87:
newX = currRectX;
newY = currRectY - 3;
break;
case 37:
case 65:
newX = currRectX - 3;
newY = currRectY;
break;
case 40:
case 83:
newX = currRectX;
newY = currRectY + 3;
break;
case 39:
case 68:
newX = currRectX + 3;
newY = currRectY;
break;
default: return;
}
movingAllowed = canMoveTo(newX, newY);
if (movingAllowed === 1) {
drawRectangle(newX, newY, "#0000FF");
currRectX = newX;
currRectY = newY;
}
else if (movingAllowed === 2) {
clearInterval(intervalVar);
makeWhite(0, 0, canvas.width, canvas.height);
context.font = "40px Arial";
context.fillStyle = "blue";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText("Congratulations!", canvas.width / 2, canvas.height / 2);
window.removeEventListener("keydown", moveRect, true);
}
}
If the rectangle reached the end point, I use the clearInterval()
method to stop the timer (which we'll set later in this article). Thereupon, I draw "Congratulations!
" on the canvas
, and then, I remove the keydown
event listener, because the blue rectangle shouldn't move anymore.
Before you're able to move the blue rectangle, you should add a keydown
event listener:
drawMazeAndRectangle(425, 3);
window.addEventListener("keydown", moveRect, true);
Checking Whether the Blue Rectangle Can Move: The canMoveTo() Function
In the previous code snippet, you saw a canMoveTo()
function. I created this method to check whether the blue rectangle can move. The logic behind it:
- Check whether the blue rectangle would move inside the bounds of the
canvas
- If that's the case, get the image data of the rectangle with
x = destinationX, y = destinationY, width = 15, height = 15
. I take 15
as width
and height
because that's the size of the blue rectangle. - Use a
for
loop to look at all pixels: if any of them has the color black, then the blue rectangle can't move. If any of them has the color lime, then you reached the end point.
This is the code for the canMoveTo()
function:
function canMoveTo(destX, destY) {
var imgData = context.getImageData(destX, destY, 15, 15);
var data = imgData.data;
var canMove = 1;
if (destX >= 0 && destX <= mazeWidth - 15 && destY >= 0 &&
destY <= mazeHeight - 15) {
for (var i = 0; i < 4 * 15 * 15; i += 4) {
if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0) {
canMove = 0;
break;
}
else if (data[i] === 0 && data[i + 1] === 255 &&
data[i + 2] === 0) {
canMove = 2;
break;
}
}
}
else {
canMove = 0;
}
return canMove;
}
The getImageData() Method
Using the getImageData()
method, you can get the image data of a specific region of your canvas
. This method returns a ImageData
object, and the data
property is an array containing the RGBA pixel data. So:
data[0]
contains the R (red) value of the first pixel in the ImageData
data[1]
contains the G (green) value of the first pixel in the ImageData
data[2]
contains the B (blue) value of the first pixel in the ImageData
data[3]
contains the A (alpha) value of the first pixel in the ImageData
data[4]
contains the R (red) value of the second pixel in the ImageData
data[5]
contains the G (green) value of the second pixel in the ImageData
data[6]
contains the B (blue) value of the second pixel in the ImageData
data[7]
contains the A (alpha) value of the second pixel in the ImageData
- and so on
Parameters of the getImageData() Method
Name | Type | Description |
sx | Number | The x-coordinate, in pixels, of the top-left corner of the rectangle to get the image data from |
sy | Number | The y-coordinate, in pixels, of the top-left corner of the rectangle to get the image data from |
sw | Number | The width, in pixels, of the rectangle to get the image data from |
sh | Number | The height, in pixels, of the rectangle to get the image data from |
Implementing the Timer
The next step is to implement the timer. To do this, we use the setInterval()
method to decrease the time with one second. If the time's up, we white out the whole canvas
and add a red "Time's up!
" message.
function createTimer(seconds) {
intervalVar = setInterval(function () {
makeWhite(mazeWidth, 0, canvas.width - mazeWidth, canvas.height);
if (seconds === 0) {
clearInterval(intervalVar);
window.removeEventListener("keydown", moveRect, true);
makeWhite(0, 0, canvas.width, canvas.height);
context.font = "40px Arial";
context.fillStyle = "red";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText("Time's up!", canvas.width / 2, canvas.height / 2);
return;
}
context.font = "20px Arial";
if (seconds <= 10 && seconds > 5) {
context.fillStyle = "orangered";
}
else if (seconds <= 5) {
context.fillStyle = "red";
}
else {
context.fillStyle = "green";
}
context.textAlign = "center";
context.textBaseline = "middle";
var minutes = Math.floor(seconds / 60);
var secondsToShow = (seconds - minutes * 60).toString();
if (secondsToShow.length === 1) {
secondsToShow = "0" + secondsToShow;
}
context.fillText(minutes.toString() + ":" + secondsToShow,
mazeWidth + 30, canvas.height / 2);
seconds--;
}, 1000);
}
In the createTimer()
function, I have created an anonymous function which will be called every second. The setInterval()
method returns a unique interval ID which you can pass to the clearInterval()
method if you no longer want the anonymous function to be called every second. At the first line in the anonymous function, I cleared a specific range of the canvas
: I cleared only the area of the timer. In this maze, the canvas
is split into two parts: in one part, the maze appears, and in the other part, the timer appears.
If seconds
is equal to zero, I call the clearInterval()
method, because I want to stop execution of the anonymous function every second. Then I remove the keydown
event listener, because it is not longer necessary to move the blue rectangle (because the time's up). Thereupon, I clear the whole canvas to draw "Time's up
" on it.
If seconds
is not yet equal to zero, I draw the remaining time on the canvas
. If the remaining time is more than 10 seconds, the color of the remaining time is green. If the remaining time is littler than or equal to 10, but greater than 5, then the color is orange-red, and if the remaining time is littler than or equal to 5 seconds, then the color is red.
First, we calculate the remaining minutes. To do this, we calculate the floor value of seconds / 60
, and to get the remaining seconds, we calculate seconds - minutes * 60
. If the number of seconds is 5
for example, then we make sure that it will be shown as 05
, by preceding the 5
by a zero if necessary.
To really create the timer, add this line at the bottom at the end of the script:
drawMazeAndRectangle(425, 3);
window.addEventListener("keydown", moveRect, true);
createTimer(120);
History
- 30th October, 2013: Added information about security error in Chrome and Internet Explorer
- 6th September, 2013: First version