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.

One Response to “HTML5: Drag and Drop – Lining Up Tiles and Preventing Stacking”

  1. HTML5: Drag and Drop on the Canvas Element « Nick Olsen's Programming Tips Says:

    […] The entire source code can be downloaded from here. UPDATE: I have completed the second portion of this project and a link to the complete source code for the first and section parts can be found here. […]


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: