Categories
Articles

HTML5 Canvas Based Animation Rotate Arrow To Mouse Position

This is an example of using the HTML5 canvas and using JavaScript Math.atan2 to rotate a canvas drawn arrow to point at the mouse position.

Demo

Download Source

Basically I wanted to reproduce the logic for the same example I did in Actionscript 3 which you can read here: Actionscript 3 Animation Following the Mouse to Show Velocity at an Angle

Foundation HTML5 Animation with JavaScript
Learn More

This example required using the save and restore canvas methods to draw an arrow, translate its position to center of the canvas and then rotate. Also I needed the beginPath canvas method to solve a gotcha when using save and restore. Without beginPath, the saved drawing keeps the previous drawing and you end up drawing another arrow on the saved arrow. Makes sense, but the syntax of canvas methods are not intuitive in the first place and those who post about the save and restore methods miss this key point.

I am using the Google CDN for JQuery. JQuery is just standard procedure and its use here is to detect we are ready to go.

This example adds the canvas node to the body dynamically in JavaScript and is just a technique. You could include the canvas tag statically. Also you see that event listeners are added to the canvas for mouse up and mouse move events.
[ad name=”Google Adsense”]
You can view the full html file at the end of the post. I will excerpt parts to explain what is going on inside the code.

The Canvas and Mouse Data Singleton
Line 19 creates a singleton to represent mouse data. This was we can inject it into any other classes or objects that need access to mouse information. Our example is simple and only one object needs access to the mouse data.

The remaining lines in this snippet are dynamically appending the canvas element to the document.

	var CANVAS_WIDTH = 480;		// Canvas width
	var CANVAS_HEIGHT = 480;	// Canvas height
	var FPS = 30;				// Frames per second
	// Canvas center points.
	var canvasCenterX = CANVAS_WIDTH / 2;
	var canvasCenterY = CANVAS_HEIGHT / 2;
	// Object to hold mouse information.
	var mouse =
	{
		x:0,
		y:0
	};
	// State for playing or not playing the animations.
	var isPlaying = false;
	// Create html syntax for canvas element.
	var canvasElement = $("<canvas width='" + CANVAS_WIDTH +
	  "' height='" + CANVAS_HEIGHT + "'></canvas");
	// Reference to the canvas 2d context.
	var canvas = canvasElement.get(0).getContext("2d");
	// Dynamically append a canvas element to the body tag.
	canvasElement.appendTo('body');

Mouse Event Handling
Mouse up and mouse move event listeners are added to the canvas on lines 34 and 36.

For the mouse up the function called on line 38 toggles the playing state for the interaction.

Line 43 computes the mouse position and updates our mouse object. There are many posts discussing how to capture the mouse position over the canvas. I modified Bibhas Bhattacharya’s code in his article Converting Mouse Event Coordinates to Canvas Coordinate.

	// Add the mousemove event listener to the canvas element.
	canvasElement.get(0).addEventListener("mousemove", mouseMove, false);
	// Add the mouseup event listener to the canvas element.
	canvasElement.get(0).addEventListener("mouseup", mouseUp, false);
	// Handler for mouseMove events. Computes mouseX and mouseY properties.
	function mouseUp(e)
	{
		isPlaying  = !isPlaying;
	}
	// Handler for mouseMove events. Computes mouseX and mouseY properties.
	function mouseMove(e)
	{
		if (e.pageX)
		{
			mouse.x = e.pageX;
		}
			else if (e.clientX)
		{
			mouse.x = e.clientX + document.body.scrollLeft
		  		+ document.documentElement.scrollLeft;
		}
		mouse.x = mouse.x - canvasElement.get(0).offsetLeft;
		if (e.pageY)
		{
			mouse.y = e.pageY;
		}
		else if (e.clientY)
		{
			mouse.y = e.clientY + document.body.scrollTop
		  		+ document.documentElement.scrollTop;
		}
		mouse.y = mouse.y - canvasElement.get(0).offsetTop;
	}

[ad name=”Google Adsense”]

The Arrow Singleton
Starting on line 68 is the arrow object. It is a singleton for our example. The main purpose is to draw an arrow in any direction, or from a geometry perspective, for any angle.

The main animation timer calls the arrow update method on line 79 to compute the needed angle expressed in radians. The distance from the center of the arrow and the position of the mouse are fed to the Math.atan2 method and we get the needed radian value.

The draw method on line 88 is also called with the main animation timer. The arrow is drawn pointing right. The drawing starts on the top left which is the shaft to the arrow. Drawing proceeds around to return to the same point. You can follow the notes in the code for the path taken.

The arrow measurements are such that drawing assumes a 0,0 registration point within the arrow. So you can see the offsets at each point to assure that registration point.

The arrow is drawn separately at position 0,0 for the canvas using these offsets. This position is translated to the center of the canvas and then rotated as shown on lines 93 and 95. To make this happen with 0,0 coordinates, the canvas save and canvas restore methods on lines 91 and 116 are used.

One important step is to include the canvas beginPath method on line 97. Leaving that out still draws the arrow but draws it over the last arrow. The saved state will assume drawing has not ended and each time the draw method is called it assumes the drawing is additive.

Notice on line 77 the arrow has a mouse object.

	// The arrow drawing we are rotating.
	var arrow =
	{
		strokeColor: "#8ED6FF",
		fillColor: "#0000ff",
		x: -50,  	// Starting x
		y: -25,		// Starting y
		_radians:0,	// Rotation value required for the canvas rotate method.
		centerX: 0,	// Center x point on canvas to draw
		centerY: 0,	// Center y point on canvas to draw.
		mouse: {},	// Mouse object
		_dx:0,
		_dy:0,
		update: function()
		{
			// Distance from mouse x and center of canvas.
			this._dx = mouse.x - this.centerX;
			// Distance from mouse y and center of canvas.
			this._dy = mouse.y - this.centerY;
			// Radians for the canvas rotate method.
			this._radians = Math.atan2(this._dy,this._dx);
		},
		draw: function() // Draw.
		{
			// Draw off canvas
			canvas.save();
			//Translate canvas to center
			canvas.translate(this.centerX, this.centerY);
			// Rotate
			canvas.rotate(this._radians);
			// Create new drawing
			canvas.beginPath();
			// Start point top left of arrow shaft.
			canvas.moveTo(this.x, this.y);
			// Top left of arrow shaft plus top left of arrow head.
			canvas.lineTo(this.x + 50 , this.y);
			canvas.lineTo(this.x + 50, this.y - 25);
			// Arrow point.
			canvas.lineTo(this.x + 100, this.y + 25);
			canvas.lineTo(this.x + 50, this.y + 75);
			// Bottom left of arrow head and bottom left of arrow shaft.
			canvas.lineTo(this.x + 50, this.y + 50);
			canvas.lineTo(this.x, this.y + 50);
			// Close the bottom of arrow shaft.
			canvas.lineTo(this.x, this.y);
			canvas.fillStyle = this.strokeColor;
			canvas.fill();
			canvas.strokeStyle = this.fillColor;
			canvas.stroke();
			// Put it on the canvas
			canvas.restore();
	  	}
	};

The update and draw function are the main animation functions. The timer calls them on lines 148 and 149.

Update simply calls the arrow update method when the state isPlaying is true. The update methods are the place to compute new data.

The draw function calls the arrow draw method. The draw methods are for drawing the view with the animation model data.

Lines 141 to 143 initializes the arrow object with its center point on the canvas and the mouse object. These provided necessary data to compute the angle from its center to the mouse position.

	// Update the model data.
	function update()
	{
		if (isPlaying)
		{
			arrow.update();
		}
	}
	// Draw the views
	function draw()
	{
		canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.strokeStyle = "red";
		canvas.strokeRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.fillStyle = "#ccc";
		canvas.fillRect(.5, .5, CANVAS_WIDTH-1, CANVAS_HEIGHT-1);
		arrow.draw();
	}
	// JQuery ready - start here
	$(function()
	{
		// Initialization. Since arrow is a singleton, need to set initalization values.
		arrow.centerX = canvasCenterX;
		arrow.centerY = canvasCenterY;
		arrow.mouse = mouse;
		// Timer for animation.
		setInterval(function()
		{
			update();
			draw();
		}, 1000/FPS);
	});

[ad name=”Google Adsense”]
The entire HTML document for your convenience to view and copy.

<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<script language="javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js" type="text/javascript"></script>
<title>HTML5 Canvas Based Animation Rotate Arrow To Mouse Position</title>
</head>
<body>
<div><a href="https://www.lonhosford.com/lonblog/2011/10/22/html5-canvas-bobbing-circle-animation-demonstrating-sine-wave-movement/">Article Posted By Lon Hosford - HTML5 Canvas Based Animation Rotate Arrow To Mouse Position</a>
<p>Click canvas to start and stop. Move mouse to rotate arrow.</div>
<script type='text/javascript'>
	var CANVAS_WIDTH = 480;		// Canvas width
	var CANVAS_HEIGHT = 480;	// Canvas height
	var FPS = 30;				// Frames per second
	// Canvas center points.
	var canvasCenterX = CANVAS_WIDTH / 2;
	var canvasCenterY = CANVAS_HEIGHT / 2;
	// Object to hold mouse information.
	var mouse = 
	{
		x:0,
		y:0
	};
	// State for playing or not playing the animations.
	var isPlaying = false;
	// Create html syntax for canvas element.
	var canvasElement = $("<canvas width='" + CANVAS_WIDTH + 
	  "' height='" + CANVAS_HEIGHT + "'></canvas");
	// Reference to the canvas 2d context.
	var canvas = canvasElement.get(0).getContext("2d");
	// Dynamically append a canvas element to the body tag.
	canvasElement.appendTo('body');	
	// Add the mousemove event listener to the canvas element.
	canvasElement.get(0).addEventListener("mousemove", mouseMove, false);
	// Add the mouseup event listener to the canvas element.
	canvasElement.get(0).addEventListener("mouseup", mouseUp, false);
	// Handler for mouseMove events. Computes mouseX and mouseY properties.
	function mouseUp(e)
	{
		isPlaying  = !isPlaying;
	}
	// Handler for mouseMove events. Computes mouseX and mouseY properties.
	function mouseMove(e)
	{
		if (e.pageX)   
		{
			mouse.x = e.pageX;
		} 
			else if (e.clientX)   
		{
			mouse.x = e.clientX + document.body.scrollLeft
		  		+ document.documentElement.scrollLeft;
		}
		mouse.x = mouse.x - canvasElement.get(0).offsetLeft;
		if (e.pageY)   
		{
			mouse.y = e.pageY;
		} 
		else if (e.clientY)   
		{
			mouse.y = e.clientY + document.body.scrollTop
		  		+ document.documentElement.scrollTop;
		}
		mouse.y = mouse.y - canvasElement.get(0).offsetTop;
	}
	// The arrow drawing we are rotating.
	var arrow = 
	{
		strokeColor: "#8ED6FF",
		fillColor: "#0000ff",
		x: -50,  	// Starting x
		y: -25,		// Starting y
		_radians:0,	// Rotation value required for the canvas rotate method.
		centerX: 0,	// Center x point on canvas to draw
		centerY: 0,	// Center y point on canvas to draw.
		mouse: {},	// Mouse object
		_dx:0,
		_dy:0,
		update: function()
		{
			// Distance from mouse x and center of canvas.
			this._dx = mouse.x - this.centerX; 
			// Distance from mouse y and center of canvas.
			this._dy = mouse.y - this.centerY; 
			// Radians for the canvas rotate method.
			this._radians = Math.atan2(this._dy,this._dx);
		},
		draw: function() // Draw.
		{
			// Draw off canvas
			canvas.save();
			//Translate canvas to center
			canvas.translate(this.centerX, this.centerY);
			// Rotate
			canvas.rotate(this._radians);
			// Create new drawing
			canvas.beginPath();
			// Start point top left of arrow shaft.
			canvas.moveTo(this.x, this.y);	
			// Top left of arrow shaft plus top left of arrow head.			
			canvas.lineTo(this.x + 50 , this.y);		
			canvas.lineTo(this.x + 50, this.y - 25);			
			// Arrow point.
			canvas.lineTo(this.x + 100, this.y + 25);		
			canvas.lineTo(this.x + 50, this.y + 75);	
			// Bottom left of arrow head and bottom left of arrow shaft.
			canvas.lineTo(this.x + 50, this.y + 50);	
			canvas.lineTo(this.x, this.y + 50);
			// Close the bottom of arrow shaft.			
			canvas.lineTo(this.x, this.y);	
			canvas.fillStyle = this.strokeColor;
			canvas.fill();
			canvas.strokeStyle = this.fillColor;		
			canvas.stroke();
			// Put it on the canvas
			canvas.restore();
	  	}
	};	
	// Update the model data.
	function update() 
	{
		if (isPlaying)
		{
			arrow.update();
		}
	}
	// Draw the views
	function draw() 
	{
		canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.strokeStyle = "red";
		canvas.strokeRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.fillStyle = "#ccc";
		canvas.fillRect(.5, .5, CANVAS_WIDTH-1, CANVAS_HEIGHT-1);
		arrow.draw();
	}
	// JQuery ready - start here
	$(function() 
	{
		// Initialization. Since arrow is a singleton, need to set initalization values.
		arrow.centerX = canvasCenterX;
		arrow.centerY = canvasCenterY;
		arrow.mouse = mouse;
		// Timer for animation.
		setInterval(function() 
		{
			update();
			draw();
		}, 1000/FPS);
	});
</script>
</body>
</html>