This content originally appeared on Envato Tuts+ Tutorials and was authored by Brad Manderscheid
Create a dynamic tile swapping game in JavaScript. The result will work with any image and will have adjustable difficulty levels.
The Complete HTML5 Canvas Puzzle
Here is a demo of the puzzle we will be building:
A couple of notes:
-
Cross-browser compatibility: This puzzle was tested and works in all versions of Safari, Firefox, and Chrome that support the
canvas
element.
- Adjustable Difficulty: We will build a slider that allows you to change the difficulty before each game.
Getting Started
To get started, create a directory for the project. Place an image in the directory that you want to use as your puzzle. Any web-friendly image will do, and it can be any size your heart desires—just make sure it fits within the fold of your browser's window.
1. Creating the HTML Template
Open a new file using your favorite text editor and save it inside your project directory, next to your image. Next, fill out this basic HTML template.
<!DOCTYPE html> <html> <head> <title>HTML5 Puzzle</title> </head> <body> <canvas id="canvas"></canvas> <br /> <label for="difficulty">Difficulty</label> <input type="range" min="2" max="16" value="4" id="difficulty" /> <script> </script> </body> </html>
All we need to do here is create a standard HTML5 template containing one canvas
tag with the id of “canvas”
, along with the difficulty slider. We will work on making the difficulty slider work later.
Now start by placing your cursor inside the script
tag. From here on out, all JavaScript is within that tag. With the exception of the initial variables, I'll be organizing the sections by function, first showing you the code and then explaining the logic.
Ready? Let's get right to it!
2. Setting Up Our Variables
Let’s set up our variables and take a look at each one.
const PUZZLE_HOVER_TINT = '#009900'; const canvas = document.querySelector("#canvas"); const stage = canvas.getContext("2d"); const img = new Image(); let difficulty = 4; let pieces; let puzzleWidth; let puzzleHeight; let pieceWidth; let pieceHeight; let currentPiece; let currentDropPiece; let mouse;
First, we have the PUZZLE_HOVER_TINT
constant. The PUZZLE_HOVER_TINT
constant defines what the tint color should be in the hover image pieces.
Next we have a series of variables:
-
CANVAS
andSTAGE
will hold a reference to the canvas and to its drawing context respectively. We do this so we don’t have to write out the entire query each time we use them. And we’ll be using them a lot! -
difficulty
holds the current difficulty. Later we will set this using a slider, but for now just keep it at 4. -
img
will be a reference to the loaded image, which we will be copying pixels from throughout the application. -
puzzleWidth
,puzzleHeight
,pieceWidth
, andpieceHeight
will be used to store the dimensions of both the entire puzzle and each individual puzzle piece. We set these once to avoid calculating them over and over again each time we need them. -
currentPiece
holds a reference to the piece currently being dragged. -
currentDropPiece
holds a reference to the piece currently in position to be dropped on. (In the demo, this piece is highlighted green.) -
mouse
is a reference that will hold the mouse's currentx
andy
position. This gets updated when the puzzle is clicked to determine which piece is touched, and when a piece is being dragged to determine what piece it's hovering over.
Now, on to our functions.
3. Initialize the Image
img.addEventListener('load',onImage,false); img.src = "mke.jpg";
The first thing we want to do in our application is to load the image for the puzzle. The image object is first instantiated and set to our img
variable. Next, we listen for the load
event, which will then fire our onImage()
function when the image has finished loading. Last, we set the source of the image, which triggers the load.
4. The onImage()
Function
function onImage(e) { pieceWidth = Math.floor(img.width / difficulty); pieceHeight = Math.floor(img.height / difficulty); puzzleWidth = pieceWidth * difficulty; puzzleHeight = pieceHeight * difficulty; setCanvas(); initPuzzle(); }
Now that the image is successfully loaded, we can set the majority of the variables declared earlier. We do this here because we now have information about the image and can set our values appropriately.
The first thing we do is calculate the size of each puzzle piece. We do this by dividing the difficulty
value by the width and height of the loaded image. We also trim the fat off the edges to give us some nice even numbers to work with and ensure that each piece can appropriately swap ‘slots’ with others.
Next, we use our new puzzle piece values to determine the total size of the puzzle and set these values to puzzleWidth
and puzzleHeight
.
Lastly, we call off a few functions: setCanvas()
and initPuzzle()
.
5. The setCanvas()
Function
function setCanvas() { canvas.width = puzzleWidth; canvas.height = puzzleHeight; canvas.style.border = "1px solid black"; }
Now that our puzzle values are complete, we want to set up our canvas
element. First, we set our canvas
variable to reference our canvas
element, and stage
to reference its context
.
Now we set the width
and height
of our canvas
to match the size of our trimmed image, followed by applying some simple styles to create a black border around our canvas
to display the bounds of our puzzle.
6. The initPuzzle()
Function
function initPuzzle() { pieces = []; mouse = { x: 0, y: 0 }; currentPiece = null; currentDropPiece = null; stage.drawImage( img, 0, 0, puzzleWidth, puzzleHeight, 0, 0, puzzleWidth, puzzleHeight ); createTitle("Click to Start Puzzle"); buildPieces(); }
Here, we initialize the puzzle. We set this function up in such a way that we can call it again later when we want to replay the puzzle. Anything else that needed to be set prior to playing will not need to be set again.
First, we set pieces
as an empty array and create the mouse
object, which will hold our mouse position throughout the application. Next we set the currentPiece
and currentPieceDrop
to null
. (On the first play these values would already be null
, but we want to make sure they get reset when replaying the puzzle.)
Finally, it’s time to draw! First, we draw the entire image to display to the player what they will be creating. After that, we create some simple instructions by calling our createTitle()
function.
7. The createTitle()
Function
function createTitle(msg) { stage.fillStyle = "#000000"; stage.globalAlpha = 0.4; stage.fillRect(100, puzzleHeight - 40, puzzleWidth - 200, 40); stage.fillStyle = "#FFFFFF"; stage.globalAlpha = 1; stage.textAlign = "center"; stage.textBaseline = "middle"; stage.font = "20px Arial"; stage.fillText(msg, puzzleWidth / 2, puzzleHeight - 20); }
Here, we create a fairly simple message that instructs the user to click the puzzle to begin. Our message will be a semi-transparent rectangle that will serve as the background of our text. This allows the user to see the image behind it and also ensures our white text will be legible on any image.
We simply set fillStyle
to black and globalAlpha
to 0.4
, before filling in a short black rectangle at the bottom of the image.
Since globalAlpha
affects the entire canvas, we need to set it back to 1
(opaque) before drawing the text. To set up our title, we set the textAlign
to "center"
and the textBaseline
to "middle"
. We can also change the font through the font
property.
To draw the text, we use the fillText()
method. We pass in the msg
variable and place it at the horizontal center of the canvas
and the vertical center of the rectangle.
8. The buildPieces()
Function
function buildPieces() { let i; let piece; let xPos = 0; let yPos = 0; for (i = 0; i < difficulty * difficulty; i++) { piece = {}; piece.sx = xPos; piece.sy = yPos; pieces.push(piece); xPos += pieceWidth; if (xPos >= puzzleWidth) { xPos = 0; yPos += pieceHeight; } } document.onpointerdown = shufflePuzzle; }
Finally, it's time to build the puzzle!
We do this by building an object for each piece. These objects will not be responsible for rendering to the canvas, but rather will hold references on what to draw and where. That being said, let’s get to it.
First off, let’s declare a few variables that we’ll be reusing through the loop. We want to set up the loop to iterate through the number of puzzle pieces we need. We get this value by multiplying difficulty
by itself—so in this case, we get 16.
In the Loop
for (i = 0; i < difficult * difficulty; i++) { piece = {}; piece.sx = xPos; piece.sy = yPos; pieces.push(piece); xPos += pieceWidth; if (xPos >= puzzleWidth) { xPos = 0; yPos += pieceHeight; } }
Start by creating an empty piece
object. Next, add the sx
and sy
properties to the object. In the first iteration, these values are 0
and represent the point in our image where we will begin to draw from. Now, push it to the pieces
array. This object will also contain the properties xPos
and yPos
, which will tell us the current position in the puzzle where the piece should be drawn. We’ll be shuffling the objects before it's playable, so these values don’t need to be set quite yet.
The last thing we do in each loop is increase the local variable xPos
by pieceWidth
. Before continuing with the loop, we determine if we need to step down to the next row of pieces by checking whether xPos
is beyond the width of the puzzle. If so, we reset xPos
back to 0 and increase yPos
by pieceHeight
.
Now we have our puzzle pieces all stored away nicely in our pieces
array. At this point, the code finally stops executing and waits for the user to interact. We set a click listener to the document
to fire the shufflePuzzle()
function when triggered, which will begin the game.
9. The shufflePuzzle()
Function
function shufflePuzzle() { pieces = shuffleArray(pieces); stage.clearRect(0, 0, puzzleWidth, puzzleHeight); let xPos = 0; let yPos = 0; for (const piece of pieces) { piece.xPos = xPos; piece.yPos = yPos; stage.drawImage( img, piece.sx, piece.sy, pieceWidth, pieceHeight, xPos, yPos, pieceWidth, pieceHeight ); stage.strokeRect(xPos, yPos, pieceWidth, pieceHeight); xPos += pieceWidth; if (xPos >= puzzleWidth) { xPos = 0; yPos += pieceHeight; } } document.onpointerdown = onPuzzleClick; }
function shuffleArray(o){ for(var j, x, i = o.length; i; j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x); return o; }
First things first: shuffle the pieces[]
array. I’m using a nice utility function here that will shuffle the indices of the array passed into it. The explanation of this function is beyond the topic of this tutorial, so we’ll move on, knowing that we have successfully shuffled our pieces. (For a basic introduction to shuffling, take a look at this tutorial.)
Let’s first clear all graphics drawn to the canvas
to make way for drawing our pieces. Next, set up the array similar to how we did when first creating our piece objects.
In the Loop
for (i = 0; i < pieces.length; i++) { piece = pieces[i]; piece.xPos = xPos; piece.yPos = yPos; stage.drawImage( img, piece.sx, piece.sy, pieceWidth, pieceHeight, xPos, yPos, pieceWidth, pieceHeight ); stage.strokeRect(xPos, yPos, pieceWidth, pieceHeight); xPos += _pieceWidth; if (xPos >= puzzleWidth) { xPos = 0; yPos += pieceHeight; } }
First of all, use the i
variable to set up our reference to the current piece object in the loop. Now we populate the xPos
and yPos
properties I mentioned earlier, which will be 0
in our first iteration.
Now, at long last, we draw our pieces.
The first parameter of drawImage()
assigns the source of the image we want to draw from. Then use the piece objects' sx
and sy
properties, along with pieceWidth
and pieceHeight
, to populate the parameters that declare the area of the image from which to draw. The last four parameters set the area of the canvas
where we want to draw. We use the xPos
and yPos
values that we are both building in the loop and assigning to the object.
Immediately after this, we draw a quick stroke around the piece to give it a border, which will separate it nicely from the other pieces.
Now we wait for the user to grab a piece by setting another click
listener. This time, it will fire an onPuzzleClick()
function.
10. The onPuzzleClick()
Function
function onPuzzleClick(e) { if (e.layerX || e.layerX === 0) { mouse.x = e.layerX - canvas.offsetLeft; mouse.y = e.layerY - canvas.offsetTop; } else if (e.offsetX || e.offsetX === 0) { mouse.x = e.offsetX - canvas.offsetLeft; mouse.y = e.offsetY - canvas.offsetTop; } currentPiece = checkPieceClicked(); if (currentPiece !== null) { stage.clearRect( currentPiece.xPos, currentPiece.yPos, pieceWidth, pieceHeight ); stage.save(); stage.globalAlpha = 0.9; stage.drawImage( img, currentPiece.sx, currentPiece.sy, pieceWidth, pieceHeight, mouse.x - pieceWidth / 2, mouse.y - pieceHeight / 2, pieceWidth, pieceHeight ); stage.restore(); document.onpointermove = updatePuzzle; document.onpointerup = pieceDropped; } }
We know that the puzzle was clicked; now we need to determine what piece was clicked on. This simple conditional will get us our mouse position on all modern desktop browsers that support canvas
, using either e.layerX
and e.layerY
or e.offsetX
and e.offsetY
. Use these values to update our mouse
object by assigning it an x
and a y
property to hold the current mouse position—in this case, the position where it was clicked.
On line 112, we then immediately set currentPiece
to the returned value from our checkPieceClicked()
function. We separate this code because we want to use it later when dragging the puzzle piece. I’ll explain this function in the next step.
If the value returned was null
, we simply do nothing, as this implies that the user didn’t actually click on a puzzle piece. However, if we do retrieve a puzzle piece, we want to attach it to the mouse and fade it out a bit to reveal the pieces underneath. So how do we do this?
First, we clear the canvas
area where the piece sat before we clicked it. We use clearRect()
once again, but in this case we pass in only the area obtained from the currentPiece
object. Before we redraw it, we want to save()
the context of the canvas before proceeding. This will ensure that anything we draw after saving will not simply draw over anything in its way. We do this because we’ll be slightly fading the dragged piece and want to see the pieces under it. If we didn’t call save()
, we’d just draw over any graphics that are in the way—faded or not.
Now, we draw the image so its center is positioned at the mouse pointer. The first five parameters of drawImage
will always be the same throughout the application. When clicking, the next two parameters will be updated to center itself to the pointer of the mouse. The last two parameters, the width
and height
to draw, will also never change.
Lastly, we call the restore()
method. This essentially means we are done using the new alpha value and want to restore all properties back to where they were. To wrap up this function, we add two more listeners: one for when we move the mouse (dragging the puzzle piece), and one for when we let go (drop the puzzle piece).
11. The checkPieceClicked()
Function
function checkPieceClicked() { for (const piece of pieces) { if ( mouse.x < piece.xPos || mouse.x > piece.xPos + pieceWidth || mouse.y < piece.yPos || mouse.y > piece.yPos + pieceHeight ) { //PIECE NOT HIT } else { return piece; } } return null; }
Now we need to backtrack a bit. We were able to determine what piece was clicked, but how did we do it? It’s pretty simple, actually. What we need to do is loop through all of the puzzle pieces and determine if the click was within the bounds of any of our objects. If we find one, we return the matched object and end the function. If we find nothing, we return null
.
12. The updatePuzzle()
Function
function updatePuzzle(e) { currentDropPiece = null; if (e.layerX || e.layerX === 0) { mouse.x = e.layerX - canvas.offsetLeft; mouse.y = e.layerY - canvas.offsetTop; } else if (e.offsetX || e.offsetX === 0) { mouse.x = e.offsetX - canvas.offsetLeft; mouse.y = e.offsetY - canvas.offsetTop; } stage.clearRect(0, 0, puzzleWidth, puzzleHeight); for (const piece of pieces) { if (piece === currentPiece) { continue; } stage.drawImage( img, piece.sx, piece.sy, pieceWidth, pieceHeight, piece.xPos, piece.yPos, pieceWidth, pieceHeight ); stage.strokeRect(piece.xPos, piece.yPos, pieceWidth, pieceHeight); if (currentDropPiece === null) { if ( mouse.x < piece.xPos || mouse.x > piece.xPos + pieceWidth || mouse.y < piece.yPos || mouse.y > piece.yPos + pieceHeight ) { //NOT OVER } else { currentDropPiece = piece; stage.save(); stage.globalAlpha = 0.4; stage.fillStyle = PUZZLE_HOVER_TINT; stage.fillRect( currentDropPiece.xPos, currentDropPiece.yPos, pieceWidth, pieceHeight ); stage.restore(); } } } stage.save(); stage.globalAlpha = 0.6; stage.drawImage( img, currentPiece.sx, currentPiece.sy, pieceWidth, pieceHeight, mouse.x - pieceWidth / 2, mouse.y - pieceHeight / 2, pieceWidth, pieceHeight ); stage.restore(); stage.strokeRect( mouse.x - pieceWidth / 2, mouse.y - pieceHeight / 2, pieceWidth, pieceHeight ); }
Now back to the dragging. We call this function when the user moves the mouse. This is the biggest function of the application as it’s doing several things. Let’s begin. I'll break it down as we go.
currentDropPiece = null; if (e.layerX || e.layerX === 0) { mouse.x = e.layerX - canvas.offsetLeft; mouse.y = e.layerY - canvas.offsetTop; } else if (e.offsetX || e.offsetX === 0) { mouse.x = e.offsetX - canvas.offsetLeft; mouse.y = e.offsetY - canvas.offsetTop; }
Start by setting currentDropPiece
to null
. We need to reset this back to null
on update because of the chance that our piece was dragged back to its home. We don’t want the previous currentDropPiece
value hanging around. Next, we set the mouse
object the same way we did on click.
stage.clearRect(0, 0, puzzleWidth, puzzleHeight);
Here, we need to clear all the graphics on the canvas. We essentially need to redraw the puzzle pieces because the object being dragged on top will affect their appearance. If we didn't do this, we’d see some very strange results following the path of our dragged puzzle piece.
for (const piece of pieces) {
Begin by setting up our usual pieces loop.
In the Loop
if(piece === currentPiece){ continue; }
Create our piece
reference as usual. Next, check if the piece we are currently referencing is the same as piece we are dragging. If so, continue the loop. This will keep the dragged piece's home slot empty.
stage.drawImage( img, piece.sx, piece.sy, pieceWidth, pieceHeight, piece.xPos, piece.yPos, pieceWidth, pieceHeight ); stage.strokeRect(piece.xPos, piece.yPos, pieceWidth, pieceHeight);
Moving on, redraw the puzzle piece using its properties exactly the same way we did when we first drew them. You’ll need to draw the border as well.
if (currentDropPiece === null) { if ( mouse.x < piece.xPos || mouse.x > piece.xPos + pieceWidth || mouse.y < piece.yPos || mouse.y > piece.yPos + pieceHeight ) { //NOT OVER } else { currentDropPiece = piece; stage.save(); stage.globalAlpha = 0.4; stage.fillStyle = PUZZLE_HOVER_TINT; stage.fillRect( currentDropPiece.xPos, currentDropPiece.yPos, pieceWidth, pieceHeight ); stage.restore(); } }
Since we have a reference to each object in the loop, we can also use this opportunity to check if the dragged piece is on top of it. We do this because we want to give the user feedback on which piece it can be dropped on. Let’s dig into that code now.
First, we want to see if this loop has already produced a drop target. If so, we don’t need to bother since only one drop target can be possible for any given mouse move. If not, currentDropPiece
will be null
, and we can proceed to the logic. Since our mouse is in the middle of the dragged piece, all we really need to do is determine which other piece our mouse is over.
Next, we use our handy checkPieceClicked()
function to determine whether the mouse is hovering over the current piece object in the loop. If so, we set the currentDropPiece
variable and draw a tinted box over the puzzle piece, indicating that it is now the drop target.
Remember to save()
and restore()
. Otherwise you’d get the tinted box and not the image underneath.
Out of the Loop
stage.save(); stage.globalAlpha = 0.6; stage.drawImage( img, currentPiece.sx, currentPiece.sy, pieceWidth, pieceHeight, mouse.x - pieceWidth / 2, mouse.y - pieceHeight / 2, pieceWidth, pieceHeight ); stage.restore(); stage.strokeRect( mouse.x - pieceWidth / 2, mouse.y - pieceHeight / 2, pieceWidth, pieceHeight );
Last but not least, we need to redraw the dragged piece. The code is the same as when we first clicked it, but the mouse has moved, so its position will be updated.
13. The pieceDropped()
Function
function pieceDropped(e){ document.onpointermove = null; document.onpointerup = null; if(currentDropPiece !== null){ let tmp = {xPos:currentPiece.xPos,yPos:currentPiece.yPos}; currentPiece.xPos = currentDropPiece.xPos; currentPiece.yPos = currentDropPiece.yPos; currentDropPiece.xPos = tmp.xPos; currentDropPiece.yPos = tmp.yPos; } resetPuzzleAndCheckWin(); }
OK, the worst is behind us. We are now successfully dragging a puzzle piece and even getting visual feedback on where it will be dropped. Now, all that is left is to drop the piece. Let’s first remove the listeners right away since nothing is being dragged.
Next, check that _currentDropPiece
is not null
. If it is, this means that we dragged it back to the piece's home area and not over another slot. If it’s not null
, we continue with the function.
What we do now is simply swap the xPos
and yPos
of each piece. We make a quick temp object as a buffer to hold one of the object's values in the swapping process. At this point, the two pieces both have new xPos
and yPos
values, and they will snap into their new homes on the next draw. That’s what we’ll do now, simultaneously checking whether the game has been won.
14. The resetPuzzleAndCheckWin()
Function
function resetPuzzleAndCheckWin() { stage.clearRect(0, 0, puzzleWidth, puzzleHeight); let gameWin = true; for (piece in pieces) { stage.drawImage( img, piece.sx, piece.sy, pieceWidth, pieceHeight, piece.xPos, piece.yPos, pieceWidth, pieceHeight ); stage.strokeRect(piece.xPos, piece.yPos, pieceWidth, pieceHeight); if (piece.xPos != piece.sx || piece.yPos != piece.sy) { gameWin = false; } } if (gameWin) { setTimeout(gameOver, 500); } }
Once again, clear the canvas
and set up a gameWin
variable, setting it to true
by default. Now proceed with our all-too-familiar pieces loop.
The code here should look familiar, so we won’t go over it. It simply draws the pieces back into their original or new slots. Within this loop, we want to see if each piece is being drawn in its winning position. This is simple: we check to see if our sx
and sy
properties match up with xPos
and yPos
. If not, we know we couldn't possibly win the puzzle and set gameWin
to false
. If we made it through the loop with everyone in their winning places, we set up a quick timeout
to call our gameOver()
method. (We set a timeout so the screen doesn’t change so drastically upon dropping the puzzle piece.)
15. The gameOver()
Function
function gameOver() { document.onpointerdown = null; document.onpointermove = null; document.onpointerup = null; initPuzzle(); }
This is our last function for the main game mechanics! Here, we just remove all listeners and call initPuzzle()
, which resets all the necessary values and waits for the user to play again.
16. Add a Difficulty Slider
Now, we will add the behavior to the difficulty slider. First, create an updateDifficulty()
function.
function updateDifficulty(e) { difficulty = e.target.value; pieceWidth = Math.floor(img.width / difficulty); pieceHeight = Math.floor(img.height / difficulty); puzzleWidth = pieceWidth * difficulty; puzzleHeight = pieceHeight * difficulty; gameOver(); }
Let's go through this step by step.
difficulty = e.target.value;
This part updates the difficulty so that it is set to the value in the slider.
pieceWidth = Math.floor(img.width / difficulty); pieceHeight = Math.floor(img.height / difficulty); puzzleWidth = pieceWidth * difficulty; puzzleHeight = pieceHeight * difficulty;
This snippet updates the pieces to be correctly sized based on the difficulty.
gameOver();
Finally, this restarts the game with the new difficulty.
Conclusion
As you can see, you can do a lot of new creative things in HTML5 by using selected bitmap areas of loaded images and drawing. You can easily extend this application by adding scoring and perhaps even a timer to give it more gameplay.
This post has been updated with contributions from Jacob Jackson. Jacob is a web developer, technical writer, freelancer, and open-source contributor.
This content originally appeared on Envato Tuts+ Tutorials and was authored by Brad Manderscheid
Brad Manderscheid | Sciencx (2014-01-20T02:13:04+00:00) Create an HTML5 Canvas Tile-Swapping Puzzle. Retrieved from https://www.scien.cx/2014/01/20/create-an-html5-canvas-tile-swapping-puzzle/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.