An Interest In:
Web News this Week
- March 31, 2024
- March 30, 2024
- March 29, 2024
- March 28, 2024
- March 27, 2024
- March 26, 2024
- March 25, 2024
Create an HTML5 Canvas Tile Swapping Puzzle
Create a dynamic tile swapping game in JavaScript. The result will work with any image, and 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 thecanvas
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 constant PUZZLE_HOVER_TINT
. The PUZZLE_HOVER_TINT
constant defines what the tint color should be in the hover image pieces.
Next is 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
hold the current difficulty. Later we will set this using a slider, but for now just keep it at four.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 prevent 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 of the edges to give us some nice even numbers to work with and assure 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 assures our white text will be legible on any image
We simply set fillStyle
to black and globalAlpha
to .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 to1
(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 to merely 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 its 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 on 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 be0
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 withpieceWidth
and pieceHeight
, to populate the parameters that declare the area of the image in which to draw from. 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 a x
and a y
property to hold the current mouse position - in this case, the position where it was clicked.
In 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 assure 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 in the way - faded or not.
Now we draw the image so its center is positioned at the mouse pointer. The first 5 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 do clear all graphics on the canvas. We essentially need to redraw the puzzle pieces because the object being dragged on top will effect 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 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 what 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 and any given mouse move. If not, currentDropPiece
will be null
and we can proceed into the logic. Since our mouse is in the middle of the dragged piece, all we really need to do is determine what other piece our mouse is over.
Next, 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 newxPos
andyPos
values, and 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. TheresetPuzzleAndCheckWin()
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 necessary values and waits for the user to play again.
Step 16: Adding a difficulty slider
Now, we will add the behavior to the difficulty slider. First, create a 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 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, a freelancer, and an open-source contributor.
Original Link: https://code.tutsplus.com/tutorials/create-an-html5-canvas-tile-swapping-puzzle--active-10747
TutsPlus - Code
Tuts+ is a site aimed at web developers and designers offering tutorials and articles on technologies, skills and techniques to improve how you design and build websites.More About this Source Visit TutsPlus - Code