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.

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.

	<title>HTML5 Bananagrams</title>

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

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

		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

		// 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

	// 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
	// Indicate that the canvas needs to be redrawn 
	function needsRedraw() {
		redrawCanvas = true;


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.

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) {

		// 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.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);

	// Draw the border around the tile
	context.strokeStyle = TILE_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);

	// 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
	// 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;
	// No tiles were clicked, make sure all tiles are not selected

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

		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)


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

 // 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.


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()">

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.


Nếu bạn thấy bài viết hữu ích, hãy nhấn +1 và các liên kết chia sẻ để website ngày càng phát triển hơn. Xin cám ơn bạn!

Nếu là khách, bạn phải đăng ký tài khoản và kích hoạt tài khoản để bình luận được hiển thị ở đây.
Thông tin kích hoạt gửi đến mail của bạn.

Tin mới hơn

Tin cũ hơn

Lên trên đầu