Categories
Articles

HTML5 Canvas Based Animation Asteriods Space Ship Movement

This is an example of an asteroids game space ship using the HTML5 canvas and JQuery.

Demo

Download Source

The goal here was to reproduce the same example i posted for Actionscript 3 here: Actionscript 3 Animation Following the Mouse to Show Velocity at an Angle

Foundation HTML5 Animation with JavaScript
Learn More

That example as well as this is not a full asteroids game. I am posting it for the basic principles of velocity, acceleration and force. In this case the velocity, direction with speed, is computed for x, y and rotation.

JQuery is used for keystroke capture. Our space ship has a thruster for forward motion and ability to turn left or right. The up key is used for thrusting forward and the left and right keys for the turning as you should expect. The ship enters the opposite side of the canvas if it leaves the canvas.

The ship is a basic triangle and also has a flame when thrusting.

[ad name=”Google Adsense”]
You can view the full html file at the end of the post. To explain what is going on inside excerpts of the code are broken out.

The Canvas and Basic Animation Framework

This snippet defines basic constants and variables to simplify code later.

	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;

The canvas element is created dynamically in JavaScript and appended to the body element. I like to name the canvas drawing context canvas as you can see on line 29.

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

These next few snippets represent the animation framework. I have used this in other HTML5 canvas examples.

Three framework functions are init, update and draw. The init function is the initialization of the animation framework and any components. The only components are the canvas and the ship.

For the init method you see some keys being identified for the document to process via JQuery. You could call JQuery that part of the framework.

The init method also initializes the ship object. Without the bounds set, the ship will leave the canvas and may never be seen again.

	// Initialize game
	function init()
	{
		// Set keys for play
		$(document).bind('keyup', KEY_NAME_THRUSTER, keyEvent); 
		$(document).bind('keydown', KEY_NAME_THRUSTER, keyEvent); 
		// Initialize the ship start position and boundaries.
		ship.x = canvasCenterX;
		ship.y = canvasCenterY;
		ship.bounds.left = 0;
		ship.bounds.right = CANVAS_WIDTH;
		ship.bounds.top = 0;
		ship.bounds.bottom = CANVAS_HEIGHT;
	}

Here is the key processing function. I am taking the JQuery key names and converting them to name I set in constants for more readable code.

The keyEvent function also would call animation object keyEvent methods and here we call the ship object;s keyEvent method.

	// Evaluate key input
	function keyEvent(evt)
	{
		// Animation name for the key pressed assumed to be the char name.
		var keyName = String.fromCharCode(evt.keyCode).toLowerCase();
		// Left arrow key
		if (evt.keyCode == 37)
		{
			keyName = KEY_NAME_LEFT;
		}
		// Right arrow key
		if (evt.keyCode == 39)
		{
			keyName = KEY_NAME_RIGHT;
		}
		// Up arrow key
		if (evt.keyCode == 38)
		{
			keyName = KEY_NAME_THRUSTER;	
		}
		// Call animation object keyEvent methods.
		ship.keyEvent(evt.type,keyName);
	}

The animation framework update method is called for updating model data. The draw method refreshes the view.

These two methods are called for each frame in the animation.

	// Update the model data.
	function update() 
	{
		ship.update();
	}
	// Draw the view.
	function draw() 
	{
		canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.strokeStyle = "blue";
		canvas.strokeRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.fillStyle = "black";
		canvas.fillRect(.5, .5, CANVAS_WIDTH-1, CANVAS_HEIGHT-1);
		ship.draw();
	}

This is the heart of the animation. Once JQuery loads the animation framework is initialized and then the timer starts to fire for each frame in the animation.

	// JQuery ready - start here
	$(function() 
	{
		// Initialization.
		init();
		// Timer for animation.
		setInterval(function() 
		{
			update();
			draw();
		}, 1000/FPS);
	});

[ad name=”Google Adsense”]
The Ship Object
The ship object has all the needed data and logic to run in the animation framework.

This first snippet of code show the public and private properties. The underscore is indicate a private property. They should be easy to follow with the comment included or their name.

	var ship = 
	{
		shipStrokeColor: "#8ED6FF",
		shipFillColor: "#0000ff",
		flameStrokeColor: "#ff0000",
		flameFillColor: "#ff0000",
		x: 0,  			// Canvas x.
		y: 0,			// Canvas y.
		leftThrust: -5, // Number of degrees for each left turn request.
		rightThrust: 5, // Number of degrees for each right turn request.
		bounds: {top:-1,bottom:-1,left:-1,right:-1}, // Boundaries. -1 = no boundary.
		_drawStartX: 10,// Drawing starting x.
		_drawStartY: 0,	// Drawing starting y.
		_radians:0,		// Rotation value required for the canvas rotate method.
		_vx:0,			// Velocity for x.
		_vy:0,			// Velocity for y.
		_vr:0,			// Velocity for rotation.
		thrust:.2,		// Incremental force added to acceleration.
		_thrust:0,		// Force added to acceleration.
		rotation:0,		// Degrees of rotation.
		_radians:0,		// Degrees of rotation converted to radians for canvas.rotate method.

This is the ship object’s update method required by the animation framework. Here we get into trigonometry and physics. Its fairly easy to follow when you think we need to compute a velocity for the x and y properties and a rotation for direction. Velocity is direction with speed. Then there is force applied to the x and y velocity when the ship’s thruster is used.

First the direction of the ship is computed on line 57. However we added a velocity for rotation. This is the _vr property and basically represents how much we turn with the left and right keys. The leftThrust and rightThrust properties provide an external interface to _vr. We do not have acceleration with rotation, so you can assume the ship automatically compensates for turning to avoid spinning like a top.

Our x and y velocities are represented by _vx and _vy. They are adjusted by force represented by the thrust property. Lines 61 and 62 compute the acceleration based on thrust for both x and y. Then we apply the acceleration to the x and y velocities on lines 64 and 65.

The last step is to set the drawing x and y properties for the ship. Lines 67 – 70 are for the x position and you can see there is a boundary check. This is repeated for the y property on lines 72 – 75.

		update: function() // Update model data
		{
			// Increase rotation by rotation velocity
			this.rotation += this._vr;
			// Calculate rotation in radians
			this._radians = this.rotation * Math.PI / 180;
			// Acceleration for x and y velocity increases
			var ax = Math.cos(this._radians) * this._thrust;
			var ay = Math.sin(this._radians) * this._thrust;
			// Velocity for x and y
			this._vx += ax;
			this._vy += ay;
			// New x position.
			this.x += this._vx;
			// Check for x boundaries
			if (this.x > this.bounds.right && this.bounds.right >= 0) this.x = this.bounds.left;
			if (this.x < this.bounds.left && this.bounds.left >= 0) this.x = this.bounds.right;
			// New y position.
			this.y += this._vy;
			// Check for y boundaries
			if (this.y > this.bounds.bottom && this.bounds.bottom >= 0) this.y = this.bounds.top;
			if (this.y < this.bounds.top && this.bounds.top >= 0) this.y = this.bounds.bottom;
		},

The draw method is the next animation framework requirement. Here we get into the HTML5 canvas method and techniques to draw the ship with and without a thruster flame.

The procedure is simplified by drawing the ship without regard to its position on the canvas and then translating that to the canvas position. The canvas save and restore methods on lines 80 and 112 respectively give us that ability.

In between the work is done by first specifying the translation point on line 82. Then canvas rotation is done. Then we draw the ship. Line 86 determines if we need to draw a flame for thrusters firing. Finally the ship, a basic triangle, is draw.

Note the beginPath methods on lines 88 and 98. These keep the drawing fresh and distinct. Without them you would see the colors and lines overlap and each time the ship is rotated, you would see the previous ship.

		draw: function() // Draw.
		{
			// Draw off canvas
			canvas.save();
			//Translate canvas next draw position
			canvas.translate(this.x,this.y);
			// Rotate
			canvas.rotate(this._radians);
			// Thrusters on draw flame
			if (this._thrust > 0)
			{
				canvas.beginPath();
				canvas.moveTo(this._drawStartX - 12.5, this._drawStartY - 5);	
				canvas.lineTo(this._drawStartX - 25 , this._drawStartY);	
				canvas.lineTo(this._drawStartX - 12.5 , this._drawStartY + 5);	
				canvas.fillStyle = this.flameStrokeColor;
				canvas.fill();
				canvas.strokeStyle = this.flameFillColor;		
				canvas.stroke();
			}
			// Begin drawing the ship
			canvas.beginPath();
			// Start.
			canvas.moveTo(this._drawStartX, this._drawStartY);	
			// 	Ship body	
			canvas.lineTo(this._drawStartX -20 , this._drawStartY + 10);		
			canvas.lineTo(this._drawStartX -15 , this._drawStartY);		
			canvas.lineTo(this._drawStartX -20 , this._drawStartY - 10);	
			// Close.			
			canvas.lineTo(this._drawStartX, this._drawStartY);	
			canvas.fillStyle = this.shipStrokeColor;
			canvas.fill();
			canvas.strokeStyle = this.shipFillColor;		
			canvas.stroke();
			// Put it on the canvas
			canvas.restore();
	  	},

The last integrating for the ship object into the animation framework is handling the keystrokes.

First take a look back at line 18 and you see we set up constants for the key names and key types. These are translated from the keys pressed in the main framework keyEvent function on line 159 shown earlier.

	// Keyname constants
	var KEY_NAME_THRUSTER = "up";
	var KEY_NAME_LEFT = "left";
	var KEY_NAME_RIGHT = "right";
	// Constants to match JQuery Hotkeys event value type.
	var KEY_TYPE_UP = "keyup";
	var KEY_TYPE_DOWN = "keydown";

Then the ship keyEvent method needs to evaluate the key name and key type to update data. For example when the left or right key is released, the rotation velocity is set to zero on lines 136 to 140 to zero. This is our automatic adjustment for acceleration when turning. Consider it an advanced feature.

For turning left or right, the rotation velocity is set to the leftThrust and rightThrust properties in lines 126 to 135. This only happens when the key type is down.

For forward motion is covered on lines 121 to 125. Simply the internal thrust property is set with the public interface thrust property. Here we are applying force.

To stop applying forward force, lines 117 to 121 set the internal thrust back to zero.

You can see the

		keyEvent: function(keyType, keyName)
		{
			// Thruster is off
			if (keyName == KEY_NAME_THRUSTER && keyType == KEY_TYPE_UP)
			{
				this._thrust = 0;
				this._vr = 0;	
			}
			// Thruster off
			else if(keyName == KEY_NAME_THRUSTER && keyType == KEY_TYPE_DOWN)
			{
				this._thrust = this.thrust;
			}
			// Turning left
			if (keyName == KEY_NAME_LEFT && keyType == KEY_TYPE_DOWN)
			{
				this._vr = this.leftThrust;
			}
			// Turning right
			if (keyName == KEY_NAME_RIGHT && keyType == KEY_TYPE_DOWN)
			{
				this._vr = this.rightThrust;
			}
			// Stop turning
			if ((keyName == KEY_NAME_RIGHT || keyName == KEY_NAME_LEFT ) && keyType == KEY_TYPE_UP)
			{
				this._vr = 0;
			}
		}

[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 Asteriods Space Ship Movement</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 Asteriods Space Ship Movement</a>
<p>Use up key to move. Left and right keys to turn.</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;
	// Keyname constants
	var KEY_NAME_THRUSTER = "up";
	var KEY_NAME_LEFT = "left";
	var KEY_NAME_RIGHT = "right";
	// Constants to match JQuery Hotkeys event value type.
	var KEY_TYPE_UP = "keyup";
	var KEY_TYPE_DOWN = "keydown";
	// 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');	
	// The ship
	var ship = 
	{
		shipStrokeColor: "#8ED6FF",
		shipFillColor: "#0000ff",
		flameStrokeColor: "#ff0000",
		flameFillColor: "#ff0000",
		x: 0,  			// Canvas x.
		y: 0,			// Canvas y.
		bounds: {top:-1,bottom:-1,left:-1,right:-1}, // Boundaries. -1 = no boundary.
		_drawStartX: 10,// Drawing starting x.
		_drawStartY: 0,	// Drawing starting y.
		_radians:0,		// Rotation value required for the canvas rotate method.
		_vx:0,			// Velocity for x.
		_vy:0,			// Velocity for y.
		_vr:0,			// Velocity for rotation.
		thrust:.2,		// Incremental force added to acceleration.
		_thrust:0,		// Force added to acceleration.
		rotation:0,		// Degrees of rotation.
		_radians:0,		// Degrees of rotation converted to radians for canvas.rotate method.
		update: function() // Update model data
		{
			// Increase rotation by rotation velocity
			this.rotation += this._vr;
			// Calculate rotation in radians
			this._radians = this.rotation * Math.PI / 180;
			// Acceleration for x and y velocity increases
			var ax = Math.cos(this._radians) * this._thrust;
			var ay = Math.sin(this._radians) * this._thrust;
			// Velocity for x and y
			this._vx += ax;
			this._vy += ay;
			// New x position.
			this.x += this._vx;
			// Check for x boundaries
			if (this.x > this.bounds.right && this.bounds.right >= 0) this.x = this.bounds.left;
			if (this.x < this.bounds.left && this.bounds.left >= 0) this.x = this.bounds.right;
			// New y position.
			this.y += this._vy;
			// Check for y boundaries
			if (this.y > this.bounds.bottom && this.bounds.bottom >= 0) this.y = this.bounds.top;
			if (this.y < this.bounds.top && this.bounds.top >= 0) this.y = this.bounds.bottom;
		},
		draw: function() // Draw.
		{
			// Draw off canvas
			canvas.save();
			//Translate canvas next draw position
			canvas.translate(this.x,this.y);
			// Rotate
			canvas.rotate(this._radians);
			// Thrusters on draw flame
			//if (this._thrust > 0)
			//{
				canvas.beginPath();
				canvas.moveTo(this._drawStartX - 12.5, this._drawStartY - 5);	
				canvas.lineTo(this._drawStartX - 25 , this._drawStartY);	
				canvas.lineTo(this._drawStartX - 12.5 , this._drawStartY + 5);	
				canvas.fillStyle = this.flameStrokeColor;
				canvas.fill();
				canvas.strokeStyle = this.flameFillColor;		
				canvas.stroke();
			//}
			// Begin drawing the ship
			canvas.beginPath();
			// Start.
			canvas.moveTo(this._drawStartX, this._drawStartY);	
			// 	Ship body	
			canvas.lineTo(this._drawStartX -20 , this._drawStartY + 10);		
			canvas.lineTo(this._drawStartX -15 , this._drawStartY);		
			canvas.lineTo(this._drawStartX -20 , this._drawStartY - 10);	
			// Close.			
			canvas.lineTo(this._drawStartX, this._drawStartY);	
			canvas.fillStyle = this.shipStrokeColor;
			canvas.fill();
			canvas.strokeStyle = this.shipFillColor;		
			canvas.stroke();
			// Put it on the canvas
			canvas.restore();
	  	},
		keyEvent: function(keyType, keyName)
		{
			// Thruster is off
			if (keyName == KEY_NAME_THRUSTER && keyType == KEY_TYPE_UP)
			{
				this._thrust = 0;
				this._vr = 0;	
			}
			// Thruster off
			else if(keyName == KEY_NAME_THRUSTER && keyType == KEY_TYPE_DOWN)
			{
				this._thrust = this.thrust;
			}
			// Turning left
			if (keyName == KEY_NAME_LEFT && keyType == KEY_TYPE_DOWN)
			{
				this._vr = -5;
			}
			// Turning right
			if (keyName == KEY_NAME_RIGHT && keyType == KEY_TYPE_DOWN)
			{
				this._vr = 5;
			}
			// Stop turning
			if ((keyName == KEY_NAME_RIGHT || keyName == KEY_NAME_LEFT ) && keyType == KEY_TYPE_UP)
			{
				this._vr = 0;
			}
		}
	};	
	// Initialize game
	function init()
	{
		// Set keys for play
		$(document).bind('keyup', KEY_NAME_THRUSTER, keyEvent); 
		$(document).bind('keydown', KEY_NAME_THRUSTER, keyEvent); 
		// Initialize the ship start position and boundaries.
		ship.x = canvasCenterX;
		ship.y = canvasCenterY;
		ship.bounds.left = 0;
		ship.bounds.right = CANVAS_WIDTH;
		ship.bounds.top = 0;
		ship.bounds.bottom = CANVAS_HEIGHT;
	}
	// Evaluate key input
	function keyEvent(evt)
	{
		// Animation name for the key pressed assumed to be the char name.
		var keyName = String.fromCharCode(evt.keyCode).toLowerCase();
		// Left arrow key
		if (evt.keyCode == 37)
		{
			keyName = KEY_NAME_LEFT;
		}
		// Right arrow key
		if (evt.keyCode == 39)
		{
			keyName = KEY_NAME_RIGHT;
		}
		// Up arrow key
		if (evt.keyCode == 38)
		{
			keyName = KEY_NAME_THRUSTER;	
		}
		// Call animation object keyEvent methods.
		ship.keyEvent(evt.type,keyName);
	}
	// Update the model data.
	function update() 
	{
		ship.update();
	}
	// Draw the view.
	function draw() 
	{
		canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.strokeStyle = "blue";
		canvas.strokeRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
		canvas.fillStyle = "black";
		canvas.fillRect(.5, .5, CANVAS_WIDTH-1, CANVAS_HEIGHT-1);
		ship.draw();
	}
	// JQuery ready - start here
	$(function() 
	{
		// Initialization.
		init();
		// Timer for animation.
		setInterval(function() 
		{
			update();
			draw();
		}, 1000/FPS);
	});
</script>
</body>
</html>