HTML5: Drag and Drop Multiple Objects on the Canvas Element


For those of you just tuning it, I have been working on a project to implement a tile based gamed called Bananagrams using HTML5, specifically using the canvas element and JavaScript. In my previous two posts (here and here) we discussed how to drag and drop a square tile on the canvas, how to make the tiles line up automatically so the user doesn’t have to use pinpoint accuracy, and how to make sure tiles don’t get stacked on top of one another. We have come pretty far in our development of the UI but if you try playing the game, you will find it is a bit tedious to have to drag and drop every single tile into place one at a time. In this post, we want to give the user the ability to select multiple tiles and drag and drop them all at once, as shown below.

Drawing a Dynamic Rectangle

Selecting multiple tiles can be done in a few different ways but the most intuitive way to me is to let the user draw a rectangle around the tiles they want to drag. We will assume that if the user doesn’t click directly on a tile, they are attempting to draw a rectangle and if they click on a tile, they are attempting to drag that tile or group of tiles to another location. Using this methodology we will need to add some more logic to our mouseDown, mouseMove, mouseUp, and draw functions. Lets deal with the mouseDown function first.

mouseDown Function
If we look at our current mouseDown function, the first part of the code below, as defined in part 2 we see that it loops through the tiles currently on the canvas and determines whether or not the user has clicked on one of them. We see on line 28 below that if we detect that a tile was clicked, we return and exit the function. On the other hand, if no tiles were selected, we exit the loop and continue on in the function. If we get to line 33 in the code below we know that the user has not clicked on a tile and we can assume they want to draw a rectangle.

function mouseDown(e) {
	// Get the current mouse coordinates
	getMouse(e);

	// Indicate that the user is not dragging any tiles
	isDragging = false;

	// Check to see if the user as clicked a tile
	for (var i = 0; i < tilesInPlay.length; i++) {
		var tile = tilesInPlay[i];

		// Calculate the left, right, top and bottom
		// bounds of the current tile
		var left = tile.x;
		var right = tile.x + TILE_TOTAL_WIDTH;
		var top = tile.y;
		var bottom = tile.y + TILE_TOTAL_HEIGHT;

		// Determine if the tile was clicked
		if (mouseX >= left && mouseX <= right && mouseY >= top && mouseY <= bottom) {
			// Indicate that the current tile is selected
			tilesInPlay[i].selected = true;
			isDragging = true;

			// Wire up the onmousemove event to handle the dragging
			document.onmousemove = mouseMove;
			needsRedraw();
			return;
		}
	}

	// No tiles were clicked, make sure all tiles are not selected
	clearSelectedTiles();
}

The first thing that we need to do is setup some global variables to help us when drawing the rectangle

var isSelecting = false;            // Indicates whether or not the user is drawing a selection rectangle
var selectionStartX;                // Stores the x coordinate of where the user started drawing the selection rectangle
var selectionStartY;                // Stores the y coordinate of where the user started drawing the selection rectangle
var selectionEndX;                  // Stores the x coordinate of the end of the current selection rectangle
var selectionEndY;                  // Stores the y coordinate of the end of the current selection rectangle

The first is a boolean variable that will let us know in the draw function whether or not the user is drawing a selection rectangle. The next four variables will store the starting and ending coordinates of the user drawn rectangle. Now lets update our mouseDown event to use the above variables.

function mouseDown(e) {
	// Code to determine if a tile has been clicked as defined in part 2
	...

	// ***** START NEW CODE ***** 
	// Indicate that the user is drawing a selection rectangle and
	// update the selection rectangle start and edit coordinates
	isSelecting = true;	
	selectionStartX = mouseX;
	selectionStartY = mouseY;
	selectionEndX = mouseX;
	selectionEndY = mouseY;
	
	// Wire up the onmousemove event so we can dynamically draw the rectangle
	document.onmousemove = mouseMove;	           
	needsRedraw();
	// ***** END NEW CODE *****
}

Add the above code to the end of the mouseDown event. If we get to this point, as mentioned before, the user didn’t click a tile and we are going to indicate that the user is drawing a selection rectangle and set the starting and ending coordinates of the rectangle to the current mouse coordinates.

mouseMove Function
In the mouseMove function we now need to update the ending coordinates of our selection rectangle if one is being created.

function mouseMove(e) {
	// Code to update the position of the selected tiles as defined in part two here
	...
	
	// Update the end coordinates of the selection rectangle
	if (isSelecting) {
		getMouse(e);

		selectionEndX = mouseX;
		selectionEndY = mouseY;	           
	
		needsRedraw();
	}
}

Add the above code to the end of the mouseMove event after the code we already defined to update the coordinates of the selected tiles.

draw Function
In the draw function, we now not only need to draw the tiles on the canvas but as well as a rectangle when the user is trying to select multiple tiles. Add the code on lines 20 – 23 to the draw function as shown below. I have included the entire draw function to see where to place the code.


// Draw the various objects on the canvas
function draw() {
	// Only draw the canvas if it is not valid
	if (redrawCanvas) {
		clear(ctx);

		// draw the unselected tiles first so they appear under the selected tiles
		for (var i = 0; i < tilesInPlay.length; i++) {
			if (!tilesInPlay[i].selected)
				drawTile(ctx, tilesInPlay[i]);
		}

		// now draw the selected tiles so they appear on top of the unselected tiles
		for (var i = 0; i < tilesInPlay.length; i++) {
			if (tilesInPlay[i].selected)
				drawTile(ctx, tilesInPlay[i]);
		}	
		
		// ***** START NEW CODE *****
		// If the user is drawing a selection rectangle, draw it
		if (isSelecting) {
			drawSelectionRectangle(ctx);
		}
		// ***** END NEW CODE *****
		
		// Indicate that the canvas no longer needs to be redrawn
		redrawCanvas = false;
	}
}

As you can see on lines 20 – 23 we are testing to see if the user is drawing a selection rectangle and if they are, we are calling the drawSelectionRectangle function to draw it. Below is the drawSelectionRectangle function.

// Draws the selection rectangle
function drawSelectionRectangle(context) {
	context.strokeStyle = TILE_STROKE;

	// Figure out the top left corner of the rectangle
	var x = Math.min(selectionStartX, selectionEndX);
	var y = Math.min(selectionStartY, selectionEndY);

	// Calculate the width and height of the rectangle
	var width = Math.abs(selectionEndX - selectionStartX);
	var height = Math.abs(selectionEndY - selectionStartY);

	// Draw the rectangle
	context.strokeRect(x, y, width, height);
}	 

To draw a rectangle on an HTML5 canvas element, you first specify the border color by setting the context.strokeStyle property. If you wanted a fill color you could set the context.fillStyle as well but for our case, we only want to draw the border so we won’t set this property. Then, we call the strokeRect function on the 2d context passing in the top left x and y coordinates of the rectangle and the desired width and height.

My initial code just set the variables x and y to selectionStartX and selectionStartY respectively and that works great if the user always draws their selection rectangle by dragging their mouse down and right. But, if they draw their rectangle starting at the bottom right corner of the canvas and drag up and left, your top left coordinates will be incorrect. To remedy this we take the minimum x and y coordinates between the stored start and end values. This will guarantee that our x and y are at the top left of the rectangle.

For the same reason mentioned above, we need to take the absolute value of the difference between the start and end coordinates to figure out the width and the height. We then call context.strokeRect passing in the calculated values.

mouseUp Function
The last part of drawing the rectangle is to get it to disappear when the user lets go of the mouse. Here is our updated mouseUp function to do this.

function mouseUp(e) {
	// Code to drop the tile in the closest slot as defined in part two
	...
		
	// Deselect all tiles
	clearSelectedTiles();
	
	// ***** START NEW CODE *****
	if (isSelecting) {			
		// Reset the selection rectangle
		isSelecting = false;
		selectionStartX = 0;
		selectionStartY = 0;
		selectionEndX = 0;
		selectionEndY = 0;				
	}
	// ***** END NEW CODE *****

	needsRedraw();
}

Add the code on lines 9 – 16 to the end of the mouseUp event. Here we set the isSelecting variable to false and reset the selection rectangle coordinates if the user was drawing a selection rectangle. On the next call to the draw function, isSelecting will be false and thus the rectangle will not be drawn.

With the added code, the user can now drag a rectangle on the canvas that will resize according to the current position of the mouse. The next step is to actually select the tiles found within the rectangle after the user draws it.

Selecting the Tiles in the Rectangle

If the user is drawing a selection rectangle, when they release their mouse, we need to implement some logic to select all the tiles within the drawn rectangle. Lets add a function called selectTilesInRectangle that will carry out this logic.

// Selects all the tiles is in the user dragged rectangle
function selectTilesInRectangle() {
	
	// Get the bounds of the drawn rectangle
	var selectionTop = Math.min(selectionStartY, selectionEndY);
	var selectionBottom = Math.max(selectionStartY, selectionEndY);
	var selectionLeft = Math.min(selectionStartX, selectionEndX);
	var selectionRight = Math.max(selectionStartX, selectionEndX);

	// Loop through all the tiles and select the tile if it lies within the 
	// bounds of the rectangle
	for (var i = 0; i < tilesInPlay.length; i++) {
		var tile = tilesInPlay[i];	            

		var tileTop = tile.y;
		var tileBottom = tile.y + TILE_TOTAL_HEIGHT;
		var tileLeft = tile.x;
		var tileRight = tile.x + TILE_TOTAL_WIDTH;

		tile.selected = (tileTop >= selectionTop && tileBottom <= selectionBottom && tileLeft >= selectionLeft && tileRight <= selectionRight);	            
	}			
}

In the first part of the function we use the same logic we used in the mouseMove function to get the bounds of the rectangle. Once we have those, we loop through each of the tiles on the canvas and if the tile is completely within the bounds of the rectangle, we set the selected property to true.

Now all we have to do is make a call to this selectTilesInRectangle in the mouseUp function if the user was drawing a selection rectangle.

function mouseUp(e) {
	// Code to drop the tile into the closest slot as defined in part 2
	...
	
	if (isSelecting) {
		// ***** START NEW CODE *****
		// Mark the tiles in the drawn rectangle as selected
		selectTilesInRectangle();
		// ***** END NEW CODE *****

		// Reset the selection rectangle
		isSelecting = false;
		selectionStartX = 0;
		selectionStartY = 0;
		selectionEndX = 0;
		selectionEndY = 0;				
	}
	
	needsRedraw();
}

Since we implemented our mouseMove and mouseUp functions to loop through all the tiles and perform actions on any tile that we selected, our code for selecting and dragging multiple tiles is done! The functionality as shown in the picture at the beginning of this post is now completed.

New Overlapping Problem

Unfortunately, if you select multiple tiles and drag your mouse off of the canvas, you will see a problem that we have created. Depending on which tile you click on to drag, they might begin to overlap as you mouse moves off the canvas as shown in the picture below.

In the example above, we clicked on the ‘B’ tile to drag the selected group. Dragging is fine until we reach the right side of the canvas. When the ‘K’ tile hits the right side of the canvas, it stops as that is how we have written the code. But, the ‘B’ and ‘O’ tiles do not stop as they have not hit the side of the canvas and thus they get pushed under the ‘K’. In order to remedy this problem, we need to figure out the extremes of the selected group, the top, right, left, and bottommost tiles, and if any of those extreme tiles hit their corresponding borders of the canvas, we need to stop the entire group from moving further.

getExtremeTiles Function
First we need to add a few global variables to store the extreme tiles of the selected group.

var topmostTile;                    // Stores the topmost tile in the selected group
var bottommostTile;                 // Stores the bottommost tile in the selected group
var leftmostTile;                   // Stores the leftmost tile in the selected group
var rightmostTile;                  // Stores the rightmost tile in the selected group

Then we need to add the getExtremeTiles function.

// Finds the top, bottom, left, and rightmost tiles of the selected group
function getExtremeTiles() {	            
	for (var i = 0; i < tilesInPlay.length; i++) {
		var tile = tilesInPlay[i];
 
		if (tile.selected) {	                
			if (topmostTile == null || tile.y < topmostTile.y)
				topmostTile = tile;

			if (bottommostTile == null || tile.y > bottommostTile.y)
				bottommostTile = tile;

			if (leftmostTile == null || tile.x < leftmostTile.x)
				leftmostTile = tile;

			if (rightmostTile == null || tile.x > rightmostTile.x)
				rightmostTile = tile;
		}	           
	}
}

This function loops through all the tiles and if the tile is selected, it checks to see if it is the top, bottom, left, or rightmost tile that has been seen. By the end of the loop, we will have found the extreme tiles of the selected group.

Now we need to add a call to this function at the end of the selectTilesInRectangle function.

// Selects all the tiles is in the user dragged rectangle
function selectTilesInRectangle() {
	// Same code as defined above	
	...

	// Get the top, bottom, left, and rightmost tiles
	getExtremeTiles();
}

Modified mouseMove Function
Now that we have references to the top, bottom, left, and rightmost tiles of the selected group, we can modify the mouseMove function to ensure that no overlapping occurs when one of the extreme tiles reaches the border of the canvas. If you followed along with part two of this project you may remember that when determining whether or not to move the tile in the mouseMove function we checked to see if the tile was within the bounds of the canvas and if it was we move it, otherwise we didn’t. Now, instead of just checking the current tile, we will use our extreme tiles so that if one of the extreme tiles hits the bounds of the canvas, all selected tiles will no longer be moved in that direction.

function mouseMove(e) {
	// If the user is dragging a tile
	if (isDragging) {
		getMouse(e);

		for (var i = 0; i < tilesInPlay.length; i++) {
			var tile = tilesInPlay[i];
			
			// Only if the tile is selected do we want to drag it
			if (tile.selected) {	                   

				// Only move tiles to the right or left if the mouse is between the left and 
				// right bounds of the canvas
				if (mouseX < CANVAS_RIGHT && mouseX > CANVAS_LEFT) {

					// Move the tile if the rightmost or leftmost tile of the group is not off the canvas 
					// or if the mouse was off the canvas on the left or right side before but has now
					// come back onto the canvas
					if ((rightmostTile.x + TILE_TOTAL_WIDTH <= WIDTH && leftmostTile.x >= 0) || offX) {	                          
						tile.x = tile.x + changeInX;	                            
					}
				}	                   

				// Only move tiles up or down if the mouse is between the top and bottom
				// bounds of the canvas
				if (mouseY < CANVAS_BOTTOM && mouseY > CANVAS_TOP) {

					// Move the tile if the topmost or bottommost tile of the group is not off the canvas and the
					// or if the mouse was off the canvas on the top or bottom side before but has now
					// come back onto the canvas
					if ((topmostTile.y >= 0 && bottommostTile.y + TILE_TOTAL_HEIGHT <= HEIGHT) || offY) {	                            
						tile.y = tile.y + changeInY;
					}
				}                      
			}
		}

		// Update the variables indicating whether or not the mouse in on the canvas
		offX = (mouseX > CANVAS_RIGHT || mouseX < CANVAS_LEFT)
		offY = (mouseY > CANVAS_BOTTOM || mouseY < CANVAS_TOP)

		needsRedraw();
	}

	// Update the end coordinates of the selection rectangle
	if (isSelecting) {
		getMouse(e);

		selectionEndX = mouseX;
		selectionEndY = mouseY;	           
	
		needsRedraw();
	}
}

I have shown the entire function here but the only lines we changed were lines 19 and 31 where we replaced the tile variable with the rightmostTile, lefmostTile, topmostTile, and bottommostTile variables.

With the above code there is an occasional bug that shows up when you select multiple tiles and attempt to drag them off of the canvas. Due to the fact that the canvas element isn’t redrawing every time the mouse moves one pixel, depending on how fast you move the mouse while dragging the tiles sometimes the tiles will get stuck on the border of the canvas.

Another problem with the above implementation is that if you are dragging a line of say four tiles and you click on the farthest left tile and drag your mouse off the right border of the canvas, the moment your mouse enters the canvas again, it starts to drag the tiles instead of waiting till you get to the tile you clicked on originally as shown in the figure below (note the first image the mouse is moving to the right and the second the mouse is moving to the left).

What we want is if the user drags their mouse off of the canvas, when their mouse re-enters the canvas, the tiles won’t move until it reaches the tile the user clicked on to drag the entire group. The figure below describes the desired behavior (note the mouse is moving to the right.

In the picture, the ‘T’ tile was selected when dragging the group and when the sequence hits the right border, we no longer want to move the group to the right. Further, when the mouse comes back onto the canvas, we don’t want the group to move until the mouse reaches the x coordinate it was at when the group hit the border. The blue dashed line shows the x coordinate of the mouse when the group hits the right border and the green line marks the area we no longer want to move the group. To accomplish this we need to store the coordinates of the mouse when the group hits any of the borders of the canvas. To do this, we will create four global variables, one for each border.

var offRightX;						// Stores the x cooridnate of the mouse when a tile hits the right border
var offLeftX;						// Stores the x cooridnate of the mouse when a tile hits the left border
var offTopY;						// Stores the y cooridnate of the mouse when a tile hits the top border
var offBottomY;						// Stores the y cooridnate of the mouse when a tile hits the bottom border

Then we will initialize the variables in our init function to be coordinates that lie off of the canvas so as to not interfere with the moving of the tiles when the canvas is first rendered.

function init() {				
	// Setup the global variables
	TILE_TOTAL_WIDTH = TILE_WIDTH + TILE_RADIUS;
	TILE_TOTAL_HEIGHT = TILE_HEIGHT + TILE_RADIUS;

	canvas = document.getElementById('canvas');
	HEIGHT = canvas.height;
	WIDTH = canvas.width;
	ctx = canvas.getContext('2d');

	// Set the values of the x and y mouse coordinates used when
	// a tile hits the border to values outside of the canvas
	offRightX = -1;
	offLeftX = WIDTH + 1;
	offTopY = HEIGHT + 1;
	offBottomY = -1;
	
	...
	// The rest of the init function code as defined in part 2
}

Now we need to update our mouseMove function to take into account the stored coordinates when the selected tiles hit the borders of the canvas.

 function mouseMove(e) {
	// If the user is dragging a tile
	if (isDragging) {
		getMouse(e);

		for (var i = 0; i < tilesInPlay.length; i++) {
			var tile = tilesInPlay[i];
			
			// Only if the tile is selected do we want to drag it
			if (tile.selected) {	                   

				// Only move tiles to the right or left if the mouse is between the left and 
				// right bounds of the canvas
				if (mouseX < CANVAS_RIGHT && mouseX > CANVAS_LEFT) {

					// Move the tile if the rightmost or leftmost tile of the group is not off the canvas 
					// or if the the right or leftmost tiles hit the one of the borders previously and the
					// mouse X coordinate has now passed the stored X coordinate when the tile hit the border
					if ((rightmostTile.x + TILE_TOTAL_WIDTH <= WIDTH && leftmostTile.x >= 0) || (mouseX <= offRightX) || (mouseX >= offLeftX)) {
						tile.x = tile.x + changeInX;	                               
					}
				}	                   

				// Only move tiles up or down if the mouse is between the top and bottom
				// bounds of the canvas
				if (mouseY < CANVAS_BOTTOM && mouseY > CANVAS_TOP) {

					// Move the tile if the topmost or bottommost tile of the group is not off the canvas and the
					// or if the the top or bottommost tiles hit the one of the borders previously and the
					// mouse Y coordinate has now passed the stored Y coordinate when the tile hit the border
					if ((topmostTile.y >= 0 && bottommostTile.y + TILE_TOTAL_HEIGHT <= HEIGHT) || (mouseY <= offBottomY) || (mouseY >= offTopY)) {
						tile.y = tile.y + changeInY;
					}
				}                      
			}
		}

		// If offRightX is less than zero, meaning that the rightmostTile has not
		// hit the right border of the canvas since our last mouseMove call, and the 
		// now the rightmostTile has hit the right border, set the offRightX variable
		// to the current X coordinate of the mouse.  This will be used on the next
		// call to mouseMove to ensure the tiles are not dragged off the canvas.
		// Otherwise set offRightX to -1 indicating that the tiles are not being
		// dragged off the canvas.
		if (offRightX < 0 && (rightmostTile.x + TILE_TOTAL_WIDTH) >= WIDTH)
			offRightX = mouseX;
		else if (mouseX <= offRightX)
			offRightX = -1;

		// Same as above but for left border
		if (offLeftX > WIDTH && (leftmostTile.x <= 0))
			offLeftX = mouseX;
		else if (mouseX >= offLeftX)
			offLeftX = WIDTH + 1;

		// Same as above but for bottom border
		if (offBottomY < 0 && (bottommostTile.y + TILE_TOTAL_HEIGHT) >= HEIGHT)
			offBottomY = mouseY;
		else if (mouseY <= offBottomY)
			offBottomY = -1;
		
		// Same as above but for top border
		if (offTopY > HEIGHT && (topmostTile.y <= 0))
			offTopY = mouseY;
		else if (mouseY >= offTopY)
			offTopY = HEIGHT + 1;	                                 

		needsRedraw();
	}

	// Update the end coordinates of the selection rectangle
	if (isSelecting) {
		getMouse(e);

		selectionEndX = mouseX;
		selectionEndY = mouseY;	           
	
		needsRedraw();
	}
}

The changes are found on lines 19 and 31 again. If the program gets to these lines of code then it means the mouse is on the canvas. The extra conditions we added just say that if the current X or Y mouse coordinate have moved past the X or Y coordinate we stored when the tiles hit the border of the canvas, then start moving the tiles. If the mouse hasn’t hit that coordinate yet, or it is in the area marked by the green line in the image above, then don’t move the tiles.

In lines 45 to 66 we update the coordinates we store if the tiles have hit one of the borders.

Unfortunately it requires quite a bit of code to remedy this problem but I don’t see any other way to do it as you have to store the coordinates for each border. If the described problems are not something that bothers you then you can remove the above added code from the mouseMove function and just stick with what we had before.

Modified mouseDown Function
Now that we are using the extreme tile references in our mouseMove function, if the user selects and drags a single tile, we need to set that tile to be the top, bottom, left, and rightmost tiles. To do this, add lines 8 – 11 in the code below to the mouseDown function

function mouseDown(e) {
	...
		// Determine if the tile was clicked
		if (mouseX >= left && mouseX <= right && mouseY >= top && mouseY <= bottom) {																									
			// ***** START NEW CODE
			// If the user selected a tile that was not selected before,
			// clear all selected tiles
			if (!tilesInPlay[i].selected) {
				clearSelectedTiles();
				topmostTile = bottommostTile = leftmostTile = rightmostTile = tilesInPlay[i];
			}
			// ***** END NEW CODE *****

			// Indicate that the current tile is selected
			tilesInPlay[i].selected = true;			
			isDragging = true;

			...
		}
	...
}

Lastly, we need to clear the extreme tile variables whenever we clear the selected tiles. Add the following code to the clearSelectedTiles function.

// Sets the tile.selected property to false for
// all tiles in play and clears all extreme tiles
function clearSelectedTiles() {
	for (var i = 0; i < tilesInPlay.length; i++) {
		tilesInPlay[i].selected = false;
	}

	// ***** START NEW CODE *****	
	// Clear the exterme tiles
	topmostTile = null;
	bottommostTile = null;
	leftmostTile = null;
	rightmostTile = null;
	// ***** END NEW CODE *****
}

To recap, we have now added the ability for the user to select multiple tiles by dragging a dynamically drawn rectangle around the desired tiles and we have also prevented the tiles from stacking on top of each other when dragging multiple tiles.

he entire source code for the project up to this point can be downloaded here.

HTML5: Drag and Drop – Lining Up Tiles and Preventing Stacking


In my previous post I introduced my latest project, developing the game Bananagrams using HTML5. If you haven’t read that post, I recommend at lease skimming it to bring you up to speed on the project. To recap, since last time we have created the playing area using the new HTML5 canvas element and have given the user the ability to drag and drop lettered tiles on the canvas. Actually, we have completed a large portion of the playing interface but in this post I want to go over some enhancements to make things easier for the user.

Right now when the user drags a tile on the canvas, it is dropped at the exact location the user dragged it to. For most drag and drop applications this is a good thing, but for a tile game it makes it somewhat difficult to get things lined up correctly and it allows for one tile to be placed on top of another as shown below.

We would like to create the application so that the user can drop a tile and have it “snap” into place adjacent to the tile the user intended to place it next to. This way, the formation below could be achieved without pinpoint accuracy by the user.

To do this we will need to solve two problems:

  1. Ensure tiles lineup nicely right next to one another
  2. Ensure tiles cannot be stack on top of one another

Lining Up Tiles

Both of these problems will be solved in the mouseUp function as this is when the tiles are dropped onto the canvas. Here we just need to perform a little bit of logic to ensure the tiles are lined up correctly. The easiest way to think about this is to imagine the canvas as being divided up into square slots with each slot equal to size of the tiles as shown below. Then, when the user drops a tile, we simply need to move that tile into one of the imaginary slots.

One thing we need to decide is when the user drops the tile, which slot will we move the tile to. If the tile is right in the middle of a given slot, the decision is easy, but if the tile is close to the border of two slots, which one do we pick? First we have to remember that the x and y coordinates of each tile are relative to the top left corner of the tile. Thus it makes sense that if the y coordinates of the tile is close to the bottom border of a slot, we should drop the tile in the next lower slot. Similarly, if the x coordinate of the tile is close to the right border of a slot, we should drop the tile in the slot to the right.

The picture below is zoomed in on one imaginary slot on the canvas and shows parts of its eight adjacent neighbors. When the x and y coordinates of the tile are within the orange shaded region, we will drop the tile in the center slot. The blue lines indicate the distance we need to check for when determining which slot to drop the tile in.

In order to deal with this situation, we need to add a global variable to indicate how many pixels we are allowing for the distance marked in blue above. I called this variable TILE_DROP_OFFSET and set it to 10, or one third of the height and width of each tile.

...
var TILE_TOTAL_HEIGHT;              // The total height of each tile
var TILE_DROP_OFFSET = 10;			// Used in determine which slot to drop the tile into 
var TILE_FILL = '#F8D9A3';          // The background color of each tile
...

Now we need to alter the mouseUp function:

function mouseUp(e) {                
	// Indicate that we are no longer dragging tiles and stop 
	// handling mouse movement
	isDragging = false;
	document.onmousemove = null;

	// **** START NEW CODE ****
	// Drop the tile in the closest slot	       
	for (var i = 0; i < tilesInPlay.length; i++) {
		var tile = tilesInPlay[i];
		
		// Only move the tile if it is currently selected
		if (tile.selected) {
			// Mod-ing the current x and y coordinates by the width
			// and height of the tile will give us the distance
			// the tile is from the left and top border's of the
			// slot the tile's x and y coordinates lie
			var offsetX = (tile.x % TILE_TOTAL_WIDTH);
			var offsetY = (tile.y % TILE_TOTAL_HEIGHT);

			// If the offsetX is within the defined distance
			// from the left border of the slot to the right, 
			// update offsetX to move the tile to the slot to the right
			if (offsetX >= (TILE_TOTAL_WIDTH - TILE_DROP_OFFSET))
				offsetX = offsetX - TILE_TOTAL_WIDTH; 
				
			// If the offsetY is within the defined distance from
			// the top border of the slot below, update offsetY
			// to move the tile to the slot below
			if (offsetY >= (TILE_TOTAL_WIDTH - TILE_DROP_OFFSET))
				offsetY = offsetY - TILE_TOTAL_HEIGHT;

			// Update the tile's x and y coordinates to drop it
			// into a slot.  Note that if either of the above
			// conditions were true, the offset will be a negative
			// number thus moving the tile to the right or down
			tile.x = tile.x - offsetX;
			tile.y = tile.y - offsetY;

			needsRedraw();
		}
	}
	// **** END NEW CODE ****
	
	// Deselect all tiles
	clearSelectedTiles();
	needsRedraw();
}

The new code above starts at line 7. Here we loop through each tile in play and if it is selected then we know it was being dragged and thus we need to go through the logic to drop it into the correct slot. First we mod the tile’s current x and y positions by the width and height of the tile. This will give us the distance the tile is from the left and top border of the slot the tile’s x and y coordinates lie. For instance, if the tile’s x coordinate is 85 then 85 % 30 (the tile width) is 25. This means that the tile is 25 pixels to the right of a slot’s left border.

In line 24, we do the test to see if we should drop the tile in the slot to the right or not. Here we compare the calculated offsetX to the width of the tile minus the drop offset we specified. In the example just stated, our offsetX is 25 and the value of (TILE_TOTAL_WIDTH – TILE_DROP_OFFSET) is (30 – 10) or 20. Thus we see that the current x coordinate is past the drop offset and we need to move the tile to the right slot. To do this, we actually subtract the tile width from the offset, 25 – 30, to get -5. If we want to move the tile to the right why a negative number? You will see on line 37 that to update the x coordinate, we subtract the offsetX from the tile.x value. Thus, a negative offset will actually increase the x coordinate and thus move the tile to the right.

The same logic explained above is used to move the tile up or down.

And that’s it. With those few lines of code we have ensured that whenever a tile is dropped, it is placed in the nearest slot making all the tiles line up perfectly. But, we still have one problem; one tile can be dropped on top of another. Next we will discuss the logic needed to ensure that two tiles can’t be stacked on top of one another.

Preventing Tile Stacking

In order to prevent stacking we need to first check to see if the tile has been placed into a slot that is currently occupied by another tile. This can be done by looping through each of the tiles and comparing the x and y coordinates. If we find that the user is attempting to stack two tiles then we can move the tile around in a clockwise spiral until it has been placed in an empty slot. The picture below depicts the pattern we will follow to find an empty slot.

Below is the sequence of moves that will take place until an empty slot is open:

  • Move up 1 slot
  • Move right 1 slot
  • Move down 2 slots
  • Move left 2 slots
  • Move up 3 slots
  • Move right 3 slots
  • Move down 4 slots
  • Move left 4 slots
  • Move up 5 slots
  • etc.

We see two patterns in the above sequence. First, as it is a clockwise movement, we first move up, then right, then down, then left, and then repeat. The next pattern we see is that after moving in two directions, the number of slots we have to move in the next direction increases by one. Noting these patterns we can write a method to move the tile into an empty slot.

// Ensures that the passed in tile is not stacked on top of another tile
function moveToEmptySlot(tile) {	           
	var count = 0;
	var slotsToMove = 1;

	// We multiple the tile width and height by these
	// values in order to get the tile to move the correct
	// direction
	// [ Up, Right, Left, Down ]
	var xMultipliers = [0, 1, 0, -1]
	var yMultipliers = [-1, 0, 1, 0]
	
	// Each iteration of this loop will move the tile the needed
	// number of slots before we need to change directions again
	while (!isInEmptySlot(tile)) {
		// The slotsToMove variable indicates how many
		// slots we need to move in the current direction
		// before we need to turn a corner
		for (var i = 0; i < slotsToMove; i++) {
			// Move the tile in the current direction
			tile.x += xMultipliers[count % 4] * TILE_TOTAL_WIDTH;
			tile.y += yMultipliers[count % 4] * TILE_TOTAL_HEIGHT;
			needsRedraw();

			// Check to see if the tile is in an empty slot now
			if (isInEmptySlot(tile)) {
				break;
			}
		}

		count = count + 1;

		// If count % 2 == 0 then we need to increase the
		// number of slots the tile should be moved in the 
		// next direction the next round                        
		if (count % 2 == 0)
			slotsToMove = slotsToMove + 1;
	}
}

function isInEmptySlot(tile) {

	// If the tile is off the canvas, then return that it is not in an empty slot
	if (tile.x < 0 || tile.x + TILE_TOTAL_WIDTH > WIDTH || tile.y < 0 || tile.y + TILE_TOTAL_HEIGHT > HEIGHT) {
		return false;
	}

	// Check to see if there is another tile on the canvas with the same coordinates                
	for (var i = 0; i < tilesInPlay.length; i++) {
		var otherTile = tilesInPlay[i];

		// If we are comparing two different tiles and they have the same x and y values 
		// then return false indicating that we are not in an an empty slot
		if (otherTile != tile && tile.x == otherTile.x && tile.y == otherTile.y) {
			return false;
		}
	}

	return true;
}

moveToEmptySlot Function
We first declare two variables count and slotsToMove to keep track of how many times we have changed direction and the current number of slots to move in a given direction, respectively.

On lines 10 and 11, we create two arrays, one named xMultipliers and one named yMultiplers. These will be used to move the tile in the direction we need it to go. If we want to move the tile up one slot then we need to decrease the tile’s y coordinate by the height of the tile, tile.y – TILE_TOTAL_WIDTH and keep the tile’s x coordinate the same. This movement can be carried out increasing the tile’s y coordinate by -1 * TILE_TOTAL_HEIGHT and increasing the tile’s x coordinate by 0 * TILE_TOTAL_WIDTH. The numbers we need to multiple the width and the height of tile by for each movement, up, right, down, and left are stored in the x and yMultipler arrays.

The code on lines 15 to 39 moves the tile one slot at a time and checks to see if the last move put the tile into an empty slot. If so, we break the loop and return, otherwise we move the tile another slot and check again. On lines 21 and 22 we use the logic as explained above to update either the x or the y coordinate of the tile to move it in the current direction. The direction to move is determined by calculating count % 4 and using the result to index into our x and yMultipler arrays.

Lastly, the second pattern we noticed is implemented in lines 36 and 37. Here every time we change directions twice, or go through the while loop twice, we increase the slotsToMove variable so we spiral outward.

isInEmptySlot Function
This function determines whether or not the passed in tile is currently in an empty slot. First we check to see if the tile is within the bounds of the canvas. If not, we return false. Next we loop through the tilesInPlay array and check to see if the x and y coordinates match any of the other tiles. If we get through the entire array without finding a match, we return true as the tile is in an empty slot.

The last thing we need to do is call the moveToEmptySlot function in our mouseUp function after we have dropped the tile into a slot. Add this function call to line 39 in the previous code block for the mouseUp function.

function mouseUp() {
	...	
	tile.x = tile.x - offsetX;
	tile.y = tile.y - offsetY;
	
	// **** START NEW CODE ****
	moveToEmptySlot(tile);  
	// **** END NEW CODE ****                 
	needsRedraw();
...
}

The entire source code for the project up to this point can be downloaded here.
UPDATE: I have completed the third portion of this project and a link to the complete source code for the entire project can be found here.

And that’s it! Now when the user drags and drops the tiles on the canvas, they snap into place forming perfect lines and we prevent the user from stacking tiles on top of one another. In the next post, we will go over how to allow the user to select and move multiple tiles at one time.

HTML5: Drag and Drop on the Canvas Element


With more and more browsers supporting HTML5 I wanted to see how we as developers could take advantage of the new functionalities provided. My wife and I love to play the game Banagrams and I thought making an online version of this game utilizing the HTML5 canvas element would be a great project to get my feet wet. Bananagrams is a Scrabble like game where each player has their own set of letter tiles, just like Scrabble, but the difference is that you build your own crossword puzzle. The first person to create a connected crossword puzzle with all the pieces wins the game. Over the next few weeks I will post different portions of this project as I have time to work on it.

HTML Setup

The first part of this project is to be able to draw the lettered tiles on the canvas and to allow the user to drag them around. Let start out by setting up our html document.

<html>
<head>	
	<title>HTML5 Bananagrams</title>

       <style type="text/css">    
        #canvas-container {
            border: black 1px solid; 
            width: 810px; 
            height:810px;
        }		
	</style>	
</head>
<body>
	<section>
	    <div id="canvas-container">
		    <canvas id="canvas" width="810" height="810">
			    Your browser does not support HTML5.
		    </canvas>
	    </div>
	</section>
</body>
</html>

I’ll warn you that for at least the beginning part of this project I will be completely focusing on functionality and not aesthetics. Thus, the only thing on the page right now is a canvas tag and a container div with a black border. I have included the CSS inline for simplicity but obviously you would want to include this in a separate file.

The canvas tag, new to HTML5, is the key element here as this defines the element on the page where we will be drawing the tiles and where the user will be able to play the game. If the browser does not support HTML5 and thus does not support the canvas element, it will be ignored and whatever is found within that element will be rendered on the page. On other hand, if the user’s browser does support HTML5, anything found within the canvas tags will be ignored. This is a great feature as we don’t have to detect whether or not the browser supports HTML5. If it does, the text “Your browser does not support HTML5.” will not be visible, and if it does not, it will.

Note that in the code below we are setting this up to be able to select and drag multiple tiles at once but for now, we will focus on just selecting and dragging a single tile.

Setup and Initialization

JavaScript will be heavily used in this program as it is through JavaScript that everything is rendered and how the mouse events are handled. To start, lets take a look at the global variables and the init() function that we will use in the first step.

<script type="text/javascript">
	var WIDTH; 					        // Width of the canvas
	var HEIGHT; 					    // Height of the canvas
	var CANVAS_RIGHT = 800;
	var CANVAS_LEFT = 9;
	var CANVAS_TOP = 9;
	var CANVAS_BOTTOM = 800;
	var INTERVAL = 20; 				    // How often to redraw the canavas (ms)

	var TILE_WIDTH = 28;                // SThe width of each tile
	var TILE_HEIGHT = 28;               // The height of each tile
	var TILE_RADIUS = 2;                // The radius of the rounded edges of the tiles
	var TILE_TOTAL_WIDTH;               // The total width of each tile
	var TILE_TOTAL_HEIGHT;              // The total height of each tile
	var TILE_FILL = '#F8D9A3';          // The background color of each tile
	var TILE_STROKE = '#000000';        // The border color of each tile
	var TILE_SELECTED_FILL = '#FF0000'; // The background color of selected tiles
	var TILE_TEXT_FILL = '#000000';     // The color of the text on the tile

	var canvas;                         // Reference to the canvas element
	var ctx;                            // Reference to the context used for drawing
	var isDragging = false;             // Indicating whether or not the user is dragging a tile
	var mouseX; 					    // Current mouse X coordinate
	var mouseY;                         // Current mouse Y coordinate
	var lastMouseX = 0;                 // The last seen mouse X coordinate
	var lastMouseY = 0;                 // the last seen mouse Y coordinate
	var changeInX;                      // The difference between the last and current mouse X coordinate
	var changeInY;                      // The difference between the last and current mouse Y coordinate

	var redrawCanvas = false;           // Indicates whether or not the canvas needs to be redrawn	
	
	var tilesInPlay = [];               // Stores all tiles currently on the canvas
	var tiles = [];                     // Stores the tiles not currently on the canvas 	

	var offX;                           // Indicates that the mouse has moved off the canvas 
										// on the x axis
	var offY                            // Indicates that the mouse has moved off the canvas
										// on the y axis

	// Object to represent each tile in the game
	function Tile() {
		this.x = 0;
		this.y = 0;
		this.letter = '';
		this.value = 0;
		this.selected = false;
	}

	function init() {		
		// Setup the global variables
		TILE_TOTAL_WIDTH = TILE_WIDTH + TILE_RADIUS;
		TILE_TOTAL_HEIGHT = TILE_HEIGHT + TILE_RADIUS;

		canvas = document.getElementById('canvas');
		HEIGHT = canvas.height;
		WIDTH = canvas.width;	      
		ctx = canvas.getContext('2d');

		// Set the global text properties for the text drawn on the letters
		ctx.font = '20px sans-serif';
		ctx.textBaseline = 'top';		

		// Set how often the draw method will be called
		setInterval(draw, INTERVAL);

		// Wire up the mouse event handlers
		canvas.onmousedown = mouseDown;	            
		document.onmouseup = mouseUp;	            

		// Setup the tile arrays
		initTiles();

		// Add 21 tiles at the bottom of the canvas
		var y = HEIGHT - (TILE_TOTAL_HEIGHT * 2);
		var x = 60;
		
		for (var i = 0; i < 21; i++) {
			addTile(x, y);
			x = x + TILE_TOTAL_WIDTH;
		}
	}

	function initTiles() {
		// All the possible letter tiles in the game
		var possibleLetters = ['J', 'J', 'K', 'K', 'Q', 'Q', 'X', 'X', 'Z', 'Z',
							   'B', 'B', 'B', 'C', 'C', 'C', 'F', 'F', 'F', 'H', 'H', 'H', 'M', 'M', 'M', 'P', 'P', 'P', 'V', 'V', 'V', 'W', 'W', 'W', 'Y', 'Y', 'Y',
							   'G', 'G', 'G', 'G',
							   'L', 'L', 'L', 'L', 'L',
							   'D', 'D', 'D', 'D', 'D', 'D', 'S', 'S', 'S', 'S', 'S', 'S', 'U', 'U', 'U', 'U', 'U',
							   'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N',
							   'T', 'T', 'T', 'T', 'T', 'T', 'T', 'T', 'T', 'R', 'R', 'R', 'R', 'R', 'R', 'R', 'R', 'R',
							   'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O',
							   'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I', 'I',
							   'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A',
							   'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'E'
							  ];

		// The value associated with each letter above.  This will
		// be used in a Bananagram variant where tiles are scored like
		// Scrabble
		var values = [8, 8, 5, 5, 10, 10, 8, 8, 10, 10,
					  3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4,
					  2, 2, 2, 2,
					  1, 1, 1, 1, 1,
					  2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
					  1, 1, 1, 1, 1, 1, 1, 1,
					  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
					  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
					  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
					  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
					  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
					];

		// Create a new tile object for each letter and value above
		for (var i = 0; i < possibleLetters.length; i++) {
			var tile = new Tile;
			tile.letter = possibleLetters[i];
			tile.value = values[i];
		   
			// Add the tile to the tiles array
			tiles.push(tile);
		}
	}

	// Adds a random tile to the canvas at the given coordinates
	function addTile(x, y) {
		// Get a random number the be used to index into 
		// the tiles array
		var index = Math.floor(Math.random() * tiles.length);

		// Remove the random tile from the array and
		// set its location 
		var tile = tiles.splice(index, 1)[0];
		tile.x = x;
		tile.y = y;

		// Add the tile to the tilesInPlay array and
		// indicate taht the canvas needs to be redrawn
		tilesInPlay.push(tile);
		needsRedraw();
	}
	
	// Indicate that the canvas needs to be redrawn 
	function needsRedraw() {
		redrawCanvas = true;
	}

</script>

Global Variables
You can read the descriptions for the global variables to get an idea what they will be used for but I will explain each of them in greater detail if needed as we come to them.

Tile
The first function we come to, Tile(), is really going to be used to represent instances of each letter tile that will be on the canvas. It stores the current x and y coordinates, the top left corner, of the tile, the letter and value associated with the tile, and a boolean variable indicating whether or not the tile is currently selected.

init() Function
The first thing to note in the init function is line 58, ctx = canvas.getContext(’2d’). This line creates a new two dimensional context for our canvas element. This context will be used whenever we draw anything on the canvas.

The next two lines, 60 and 61, set some global properties for when we draw any text on the canvas. The first line is simply a CSS property defining the size and style of the font, and the second line controls where the text is drawn relative to the starting point. I have set the textBaseline property to ‘top’ here indicating that if we write text at the coordinates (0, 0), the top left corner of the letter will be at coordinates (0, 0).

The way the canvas element works is some function is called every X milliseconds which then in turn draws the various objects we want to display on the canvas. On line 64 we define which function will be called, the draw function, and specify the interval or how often it will be called. The interval is a best case scenario and various factors will go into how often the function is really called included of which is how much logic you place in your draw function.

Next we wire up the mouse events for the canvas and the document. We only want to drag tiles around when the user clicks the canvas so we wire up the canvas.onmousedown event but we still want to know when the user lets go of the mouse even if the mouse is no longer on the canvas. For this reason, we have wired up the document.onmouseup event rather than just the canvas.onmouseup event.

On line 71 we call the initTiles function, discussed later, that sets up the tiles array.

And lastly, we place the initial tiles on the board. The game is started with each player having 21 tiles so here we draw 21 random tiles from the tiles array and place them on the board.

initTiles Function
This function is pretty self explanatory. Here we simply populate the tiles array with new Tile objects for each possible playing piece.

addTile Function
The addTile function simply selects a random tile from the unplayed tiles and adds it to the canvas at the given location. Notice the use of the splice array method which removes a given number of elements from an array and returns them in an array. Since we are removing only one element, we index into the first position of the returned value to get the removed element.

Drawing on the Canvas

Now that we have setup our canvas and tiles, we now need to implement the function to draw the tiles on the canvas. As mentioned before, the browser will call the draw() function every 20 ms and it is there that we will draw the tiles on the canvas. Here is the code that we will use.

// Draw the various objects on the canvas
function draw() {
	// Only draw the canvas if it is not valid
	if (redrawCanvas) {
		clear(ctx);

		// draw the unselected tiles first so they appear under the selected tiles
		for (var i = 0; i < tilesInPlay.length; i++) {
			if (!tilesInPlay[i].selected)
				drawTile(ctx, tilesInPlay[i]);
		}

		// now draw the selected tiles so they appear on top of the unselected tiles
		for (var i = 0; i < tilesInPlay.length; i++) {
			if (tilesInPlay[i].selected)
				drawTile(ctx, tilesInPlay[i]);
		}	

		// Indicate that the canvas no longer needs to be redrawn
		redrawCanvas = false;
	}
}

// Draw a single tile using the passed in context
function drawTile(context, tile) {	            
	// Draw the tile with rounded corners
	context.beginPath();
	context.moveTo(tile.x + TILE_RADIUS, tile.y);
	context.lineTo(tile.x + TILE_WIDTH - TILE_RADIUS, tile.y);
	context.quadraticCurveTo(tile.x + TILE_WIDTH, tile.y, tile.x + TILE_WIDTH, tile.y + TILE_RADIUS);
	context.lineTo(tile.x + TILE_WIDTH, tile.y + TILE_HEIGHT - TILE_RADIUS);
	context.quadraticCurveTo(tile.x + TILE_WIDTH, tile.y + TILE_HEIGHT, tile.x + TILE_WIDTH - TILE_RADIUS, tile.y + TILE_HEIGHT);
	context.lineTo(tile.x + TILE_RADIUS, tile.y + TILE_HEIGHT);
	context.quadraticCurveTo(tile.x, tile.y + TILE_HEIGHT, tile.x, tile.y + TILE_HEIGHT - TILE_RADIUS);
	context.lineTo(tile.x, tile.y + TILE_RADIUS);
	context.quadraticCurveTo(tile.x, tile.y, tile.x + TILE_RADIUS, tile.y);
	context.closePath();

	// Draw the border around the tile
	context.strokeStyle = TILE_STROKE;
	context.stroke();	            

	// Fill the tile background depending on whether or not
	// the tile is selected or not
	context.fillStyle = (tile.selected ? TILE_SELECTED_FILL : TILE_FILL);
	context.fill();	            	            

	// Draw the letter on the tile
	context.fillStyle = TILE_TEXT_FILL;                
	
	// Get the text metrics so we can measure the width of the letter
	// that will be drawn
	var textMetrics = context.measureText(tile.letter);

	// Draw the letter in the middle of the tile
	context.fillText(tile.letter, tile.x + ((TILE_TOTAL_WIDTH - textMetrics.width - 2) / 2), tile.y + 2);
}

// Clears the canvas
function clear(c) {
	c.clearRect(0, 0, WIDTH, HEIGHT);
}

draw Function
Since drawing on the canvas can be an intense process and it will be done many times a second, we only want to run the code when we absolutely have to. In order to do this, we have a boolean variable named redrawCanvas that will indicate whether or not the canvas needs to be redrawn. If this flag is true, we will draw the canvas, otherwise we will not.

When we need to redraw the canvas, we first clear the context to get rid of whatever is currently on the canvas and then loop through the tilesInPlay array calling the drawTile function for each. I have separated this process into two loops; the first to draw all the unselected tiles, and the second to draw all the selected tiles. The reason for this is if you are dragging one tile over top of another, you want the dragging tile, or the tile that is selected, to appear on top of the unselected tile.

drawTile Function
In this function we first draw the tile with rounded corners. The code for this was written by Juan Mendes and can be found here. The code essentially traverses around the outside of the tile drawing the straight lines and then quadratic curves for the corners.

Next on line 40 and 41 we draw the border or the stroke around the tile. To do this, you first set the context.strokeStyle property to be the color of the border you want and then call the context.stroke() function.

Then on lines 45 and 46 we draw the background color of the tiles in a similar fashion as the border. Here we change the background color of the tile if it is selected so the user knows which tiles they have selected.

Lastly we need to draw the letter on top of the tile. If you remember, we set a global setting to draw text at a size of 20px and with a font style of sans-serif. Since the sans-serif fonts are not fixed width, we can’t just draw the text in the same location for every letter as they won’t all be centered. To remedy this, we simply need to get the width of the letter and do a few calculations to find where it needs to be drawn to center it in the tile. This is very simply using the context.measureText function which returns the display width of the text as well as other things.

Mouse Events

Now that we have the code written to draw the tiles, we just now need to handle the mouse down, up, and move events in order to drag the tiles. Here is the code for the named events.

function mouseDown(e) {
	// Get the current mouse coordinates
	getMouse(e);
	
	// Indicate that the user is not dragging any tiles
	isDragging = false;

	// Check to see if the user as clicked a tile
	for (var i = 0; i < tilesInPlay.length; i++) {
		var tile = tilesInPlay[i];

		// Calculate the left, right, top and bottom
		// bounds of the current tile
		var left = tile.x;
		var right = tile.x + TILE_TOTAL_WIDTH;
		var top = tile.y;
		var bottom = tile.y + TILE_TOTAL_HEIGHT;

		// Determine if the tile was clicked
		if (mouseX >= left && mouseX <= right && mouseY >= top && mouseY <= bottom) {			 			
			// Indicate that the current tile is selected
			tilesInPlay[i].selected = true;
			isDragging = true;

			// Wire up the onmousemove event to handle the dragging
			document.onmousemove = mouseMove;
			needsRedraw();
			return;
		}
	}
	
	// No tiles were clicked, make sure all tiles are not selected
	clearSelectedTiles();
}

function mouseMove(e) {
	// If the user is dragging a tile
	if (isDragging) {
		getMouse(e);

		for (var i = 0; i < tilesInPlay.length; i++) {
			var tile = tilesInPlay[i];
			
			// Only if the tile is selected do we want to drag it
			if (tile.selected) {	                   

				// Only move tiles to the right or left if the mouse is between the left and 
				// right bounds of the canvas
				if (mouseX < CANVAS_RIGHT && mouseX > CANVAS_LEFT) {

					// Move the tile if it is not off the canvas 
					// or if the mouse was off the canvas on the left or right
					// side before but has now come back onto the canvas				   
					if ((tile.x + TILE_TOTAL_WIDTH <= WIDTH && tile.x >= 0) || offX) {	                       
						tile.x = tile.x + changeInX;	                            
					}
				}	                   

				// Only move tiles up or down if the mouse is between the top and bottom
				// bounds of the canvas
				if (mouseY < CANVAS_BOTTOM && mouseY > CANVAS_TOP) {

					// Move the tile if the it is not off the canvas and the
					// or if the mouse was off the canvas on the top or bottom
					// side before but has not come back onto the canvas					
					if ((tile.y >= 0 && tile.y + TILE_TOTAL_HEIGHT <= HEIGHT) || offY) {
						tile.y = tile.y + changeInY;
					}
				}                      
			}
		}

		// Update the variables indicating whether or not the mouse in on the canvas
		offX = (mouseX > CANVAS_RIGHT || mouseX < CANVAS_LEFT)
		offY = (mouseY > CANVAS_BOTTOM || mouseY < CANVAS_TOP)

		needsRedraw();
	}	
}

function mouseUp(e) {
	
	// Indicate that we are no longer dragging tiles and stop 
	// handling mouse movement
	isDragging = false;
	document.onmousemove = null;
	
	// Deselect all tiles
	clearSelectedTiles();	
	needsRedraw();
}

 // Sets the tile.selected property to false for
// all tiles in play
function clearSelectedTiles() {
	for (var i = 0; i < tilesInPlay.length; i++) {
		tilesInPlay[i].selected = false;
	}
}

// Sets mouseX and mouseY variables taking into account padding and borders
function getMouse(e) {
	var element = canvas;
	var offsetX = 0;
	var offsetY = 0;

	// Calculate offsets
	if (element.offsetParent) {
		do {
			offsetX += element.offsetLeft;
			offsetY += element.offsetTop;
		} while ((element = element.offsetParent));
	}	
	
	// Calculate the mouse location
	mouseX = e.pageX - offsetX;
	mouseY = e.pageY - offsetY;

	// Calculate the change in mouse position for the last
	// time getMouse was called
	changeInX = mouseX - lastMouseX;
	changeInY = mouseY - lastMouseY;

	// Store the current mouseX and mouseY positions
	lastMouseX = mouseX;
	lastMouseY = mouseY;	  
}

mouseDown Function
In the mouseDown function we need to determine whether or not the user has clicked on a tile to start dragging it. The first thing we do on line 3 is to get the current coordinates of the mouse cursor. This is accomplished in the getMouse function.

Then, we loop through the all of the tiles currently in play checking to see if the user clicked on any of them. To do this, first get the pixels marking the left, right, top, and bottom bounds of the given tile. Then we see if the current mouse position falls within the bounds of the tile. If it does, mark the tile as selected and set the isDragging boolean variable to true. Next we wire up the document.onmousemove to track the movement while the tile is being dragged.

If we get through the the entire collection of tiles then the user hasn’t selected any of the tiles. To make sure nothing is selected, call the clearSelectedTiles function to clear any selected tiles.

mouseMove Function
The mouseMove function will update the x and y coordinates of each of the selected tiles whenever the mouse moves. First, in line 38 we check to ensure that the user is dragging one of the tiles. If such is the case, then we get the current mouse coordinates which will calculate the amount we need to change the x and y coordinates to carry out the dragging.

Next we loop through the tiles and if the tile is selected, go through the logic to update the location of the tile to make it seem that it is being dragged. Line 49 ensures that we only move the tile to the left or the right if the x coordinate of the mouse cursor is within the left and right bounds of the canvas. This will help ensure that if the mouse is outside the bounds, the tile won’t be dragged off the canvas. Line 54 checks to see if the bounds of the current tile itself are within the bounds of the canvas. If they are, the x location of the tile is changed. There is another way in which we update the x coordinate of the tile. If the mouse is moved off the canvas to the right, the tile will be pegged to the right side of the canvas, with the (tile.x + TILE_TOTAL_WIDTH) value possibly a few pixels more than the WIDTH variable depending on how fast the cursor is moved. If such is the case, then even when the mouse is moved back onto the canvas, the x value of the tile will not be updated. To remedy this, we update a global variable named offX indicating whether or not the mouse cursor has been moved off the canvas. If it is moved off the canvas, the next time the mouse moves back onto the canvas, the tile’s x location will be updated as offX will be set to true.

The same logic is used for moving the tile up and down in the next lines of code.

At the end of this method we update the offX and offY variables to indicate whether or not the mouse is currently on the canvas or not and set the canvas up to be redrawn.

mouseUp Function
The mouseUp function simply sets the isDragging variable to false to indicate that the user is no longer dragging any tiles and we remove the document.onmousemove event handler.

Finalization

The last thing we need to do is to call the init() function when the page loads. This can be done in a few ways but one way is to add a function call on the body onload event. Make the following change to the body tag.

<body onload="init()">
     ....
</body>

The entire source code can be downloaded from here.
UPDATE: I have completed the third portion of this project and a link to the complete source code for the entire project can be found here.

And that is it. We have created an application that draws the tiles onto the canvas element and allows the user to drag and drop them around the canvas. In the next post, we will discuss how to ensure that tiles line up nicely when dragged and dropped and how to prevent overlapping of tiles.

Follow

Get every new post delivered to your Inbox.

Join 66 other followers