Image Manipulation with HTML5 Canvas: A Sliding Puzzle

Bruce Alderman
Bruce Alderman
Share

HTML5 includes many features to integrate multimedia natively into web pages. Among these features is the canvas element, a blank slate that can be filled with line drawings, image files, or animations. In this tutorial, I’m going to demonstrate HTML5 canvas’s image manipulation capabilities by creating a sliding puzzle game. To embed a canvas in the web page, use the <canvas> tag.

  <canvas width="480px" height="480px"></canvas>
The width and height attributes set the canvas size in pixels. If these attributes are not specified, they default to 300px for width and 150px for height. Drawing on the canvas is done via a context, which is initialized through the JavaScript function getContext(). The two-dimensional context specified by the W3C is called, appropriately, “2d”. So, to initialize the context for a canvas with an ID of “canvas” we simply call:
  document.getElementById("canvas").getContext("2d");
The next step is to display the image. JavaScript has only one function for this, drawImage(), but there are three ways to call this function. In its most basic form, this function takes three arguments: the image object and the x and y offset from the top left corner of the canvas.
  drawImage(image, x, y);
It’s also possible to add two more arguments, width and height, to resize the image.
  drawImage(image, x, y, width, height);
The most complex form of drawImage() takes nine arguments. The first is the image object. The next four are, in order, the source x, y, width, and height. The remaining four are, in order, the destination x, y, width, and height. This function extracts a portion of the image to draw on the canvas, resizing it if necessary. This allows us to treat the image as a sprite sheet.
  drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
A few caveats are in order with all forms of drawImage(). If the image is null, or the horizontal or vertical dimension is zero, or the source height or width is zero, drawImage()
will throw an exception. If the image can’t be decoded by the browser, or has not finished loading when the function is called, drawImage() will not display anything. That’s all there is to image manipulation with HTML5 canvas. Now let’s see it in practice.
  <div id="slider">
    <form>
      <label>Easy</label>
      <input type="range" id="scale" value="4" min="3" max="5" step="1">
      <label>Hard</label>
    </form>
    <br>
  </div>
  <div id="main" class="main">
    <canvas id="puzzle" width="480px" height="480px"></canvas>
  </div>
This HTML block includes another HTML5 feature, the range input, that lets the user select a number with a slider. We’ll see a little later how the range input interacts with the puzzle. Be forewarned, though: Although most browsers support range input, two of the more popular ones—Internet Explorer and Firefox—still do not at the time of this writing. Now, as I mentioned above, to draw on the canvas we need a context.
  var context = document.getElementById("puzzle").getContext("2d");
We will also need an image. You can use the one referenced below or any other square image that fits (or can be resized to fit) the canvas.
  var img = new Image();
  img.src = 'http://www.brucealderman.info/Images/dimetrodon.jpg';
  img.addEventListener('load', drawTiles, false);
The event listener is there to guarantee the image is finished loading before the browser attempts to draw it. The canvas won’t display the image if it’s not ready to be drawn. We’ll get the board size from the puzzle canvas, and the tile count from the range input. This slider has a range from 3 to 5, with the numeric value indicating the number of rows and columns.
  var boardSize = document.getElementById('puzzle').width;
  var tileCount = document.getElementById('scale').value;
With these two numbers, we can calculate the tile size.
  var tileSize = boardSize / tileCount;
Now we can create the board.
  var boardParts = new Object;
  setBoard();
The setBoard() function is where we’ll define and initialize the virtual board. The natural way to represent the board is with a two-dimensional array. In JavaScript creating such an array is not an elegant process. We first declare a flat array, then declare each of the array’s elements as an array. These elements can then be accessed as though they are a multi-dimensional array. For the sliding puzzle game, each element will be an object with x and y coordinates that define its location within the puzzle grid. Each object will therefore have two sets of coordinates. The first will be its position within the array. This represents its location on the board, so I’ll refer to this as the board square. Each board square has an object with x and y properties that represent its location in the puzzle image. I’ll refer to this location as the puzzle tile. When the coordinates of the board square match those of its puzzle tile, the tile is in the right place for solving the puzzle. For the purpose of this tutorial we will initialize each puzzle tile to the board square opposite its correct position in the puzzle. The tile for the upper right corner, for example, will be in the board sqaure of the lower left corner.
  function setBoard() {
    boardParts = new Array(tileCount);
    for (var i = 0; i < tileCount; ++i) {
      boardParts[i] = new Array(tileCount);
      for (var j = 0; j < tileCount; ++j) {
        boardParts[i][j] = new Object;
        boardParts[i][j].x = (tileCount - 1) - i;
        boardParts[i][j].y = (tileCount - 1) - j;
      }
    }
    emptyLoc.x = boardParts[tileCount - 1][tileCount - 1].x;
    emptyLoc.y = boardParts[tileCount - 1][tileCount - 1].y;
    solved = false;
  }
Those last three statements in setBoard()
introduce variables we haven’t yet defined. We’ll need to track the location of the empty tile, and to record where the user clicks.
  var clickLoc = new Object;
  clickLoc.x = 0;
  clickLoc.y = 0;

  var emptyLoc = new Object;
  emptyLoc.x = 0;
  emptyLoc.y = 0;
The final variable is a boolean indicating whether the puzzle has been solved.
  var solved = false;
We’ll set this to true once all the puzzle tiles match their respective board squares. Now we just need the functions related to solving the puzzle. First we’ll set the functions triggered by user input events. If the range input is changed, we need to recalculate the number and size of tiles before redrawing the board.
  document.getElementById('scale').onchange = function() {
    tileCount = this.value;
    tileSize = boardSize / tileCount;
    setBoard();
    drawTiles();
  };
We need to track mouse movement to know which tiles the user clicks.
  document.getElementById('puzzle').onmousemove = function(e) {
    clickLoc.x = Math.floor((e.pageX - this.offsetLeft) / tileSize);
    clickLoc.y = Math.floor((e.pageY - this.offsetTop) / tileSize);
  };

  document.getElementById('puzzle').onclick = function() {
    if (distance(clickLoc.x, clickLoc.y, emptyLoc.x, emptyLoc.y) == 1) {
      slideTile(emptyLoc, clickLoc);
      drawTiles();
    }
    if (solved) {
      alert("You solved it!");
    }
  };
In some browsers, the solved alert may be triggered before the board finishes redrawing. To prevent this, give the alert a short delay.
  if (solved) {
    setTimeout(function() {alert("You solved it!");}, 500);
  }
When a tile is clicked, we need to know whether it is next to the open square. This is true if and only if the total distance from the clicked tile to the open square is 1, in other words, if the difference of the x-coordinates of the clicked tile and the empty tile plus the difference of the y-coordinates of the clicked tile and the empty tile is 1. It’s easier to implement than to describe.
  function distance(x1, y1, x2, y2) {
    return Math.abs(x1 - x2) + Math.abs(y1 - y2);
  }
The distance() function calculates this distance by taking the absolute value of difference between the x-coordinates and the absolute value of the difference between the y-coordinates, and adding them. If this value is 1, the clicked tile can be moved into the open square. If this value is anything other than 1, the tile should not be moved. To move the tile, we simply copy the tile coordinates for that board square into the empty square. Then copy the tile coordinates for the removed tile into the clicked tile.
  function slideTile(toLoc, fromLoc) {
    if (!solved) {
      boardParts[toLoc.x][toLoc.y].x = boardParts[fromLoc.x][fromLoc.y].x;
      boardParts[toLoc.x][toLoc.y].y = boardParts[fromLoc.x][fromLoc.y].y;
      boardParts[fromLoc.x][fromLoc.y].x = tileCount - 1;
      boardParts[fromLoc.x][fromLoc.y].y = tileCount - 1;
      toLoc.x = fromLoc.x;
      toLoc.y = fromLoc.y;
      checkSolved();
    }
  }
Once the tile is moved, we need to check whether the puzzle is solved. We’ll scan the tiles to see if they are all in the correct board squares.
  function checkSolved() {
    var flag = true;
    for (var i = 0; i < tileCount; ++i) {
      for (var j = 0; j < tileCount; ++j) {
        if (boardParts[i][j].x != i || boardParts[i][j].y != j) {
          flag = false;
        }
      }
    }
    solved = flag;
  }
If any tiles are out of place, the function returns false. Otherwise it defaults to true. Finally, redraw the board with the clicked tile in its new position.
  function drawTiles() {
    context.clearRect ( 0 , 0 , boardSize , boardSize );
    for (var i = 0; i < tileCount; ++i) {
      for (var j = 0; j < tileCount; ++j) {
        var x = boardParts[i][j].x;
        var y = boardParts[i][j].y;
        if(i != emptyLoc.x || j != emptyLoc.y || solved == true) {
          context.drawImage(img, x * tileSize, y * tileSize, tileSize, tileSize,
              i * tileSize, j * tileSize, tileSize, tileSize);
        }
      }
    }
  }
When drawing the puzzle tiles, this function prevents filling the board square that matches the coordinates of emptyLoc until the solved flag has been set. Incidentally, because the board re-initializes whenever the range slider is moved, the user can try another difficulty level after solving the puzzle without refreshing the page. That’s all there is to it! The canvas element, along with a little JavaScript and a little math, brings powerful native image manipulation to HTML5. You’ll find a live demo of the sliding puzzle at http://html5.brucealderman.info/sliding.html.

Frequently Asked Questions (FAQs) about Image Manipulation with HTML5 Canvas and Sliding Puzzle

How can I create a sliding puzzle game using HTML5 Canvas?

Creating a sliding puzzle game using HTML5 Canvas involves several steps. First, you need to create a canvas element in your HTML file. Then, in your JavaScript file, you need to reference this canvas and its 2D context, which will allow you to draw on it. You can then load an image onto the canvas and divide it into a grid of tiles. These tiles can be shuffled to create the initial puzzle state. The game logic, which includes moving tiles and checking for a win condition, can then be implemented.

How can I manipulate pixels with the Canvas API?

The Canvas API provides a method called getImageData() that allows you to retrieve pixel data from a specified area of the canvas. This method returns an ImageData object, which contains an array of pixel values. Each pixel is represented by four values (red, green, blue, and alpha), so you can manipulate these values to change the color of individual pixels. To apply these changes, you can use the putImageData() method.

What is the toDataURL() method in HTMLCanvasElement?

The toDataURL() method in HTMLCanvasElement is a powerful tool that allows you to create a data URL representing the image displayed in the canvas. This data URL can be used as the source for an image element, saved to a database, or sent to a server. The method takes an optional argument that specifies the image format. If no argument is provided, the default format is PNG.

How can I contribute to the sliding puzzle game projects on GitHub?

GitHub is a platform where developers share their projects and collaborate with others. If you want to contribute to a sliding puzzle game project, you can start by forking the repository, which creates a copy of the project in your own GitHub account. You can then clone this repository to your local machine, make changes, and push these changes back to your forked repository. Finally, you can open a pull request to propose your changes to the original repository.

How can I use canvas for image manipulation?

Canvas provides a flexible and powerful way to manipulate images. You can draw images onto the canvas, apply transformations, and manipulate individual pixels. For example, you can create a grayscale effect by iterating over the pixel data and setting the red, green, and blue values to the average of the original values. You can also create a sepia effect by applying a specific formula to the red, green, and blue values. After manipulating the image, you can use the toDataURL() method to export the result.