Having fun with HTML5 – Canvas, part 4

Following on from part 3 where we basically made a little app that lets you scribble with HTML5’s canvas element, let us push on and see what else we can achieve with the canvas element.

The logical next step would be to made animations, and give you a way to interact with the animation. So how about a little game that lets you shoot moving targets like those classic Point Blank games? Hands up who’s up for that?

image

Good good, that’s decided then! :-)

To Start Off..

First, we’ll need a simple HTML page, something like my previous demos would do:

   1: <div id="wrapper">

   2:     <h1>HTML5 Canvas Demo</h1>

   3:     <h3>Shoot 'em!</h3>

   4:     <aside id="message"></aside>

   5:

   6:     <div class="hide">

   7:         <canvas id="target" width="101" height="101" class="hide"/>

   8:     </div>

   9:

  10:     <div class="block">

  11:         <canvas id="canvas" class="block" width="800" height="700"

  12:                 onSelectStart="this.style.cursor='crosshair'; return false;"/>

  13:     </div>

  14: </div>

Nothing fancy here, the CSS for this page is also pretty basic too, the only thing worth noting is that to get the crosshair cursor inside the canvas I included this in my CSS:

   1: #canvas

   2: {

   3:     cursor: crosshair;

   4: }

The onSelectStart line in the canvas HTML is to ensure that the crosshair cursor is used when you click and hold inside the canvas element.

You might have also noticed that I had specified two canvas elements in the HTML, this is to simplify the task of drawing moving targets onto the main canvas. With the 2D context object’s drawImage function, you can draw an img, canvas, or video element so I can draw the target onto the target canvas during initialization and then reuse it in the main canvas.

Drawing the Target board

Next, I need to draw the target boards, and make them look like this, but without the score on the rings:

image

(Why not just use an image? Because where’s the fun in that! ;-) )

As you can imagine, we can create a target board looking like this by drawing 6 circles, with the innermost 2 circles having roughly half the radius of the other 4 and the outermost 2 circles having the inversed fill/stroke colour combination. Expand the below section to see the initializeTargetCanvas function which takes a canvas element and draws a target board in inside it:

   1: // Draws the target canvas

   2: function initializeTargetCanvas(element) {

   3:     var baseX = 1.5, baseY = 1.5;

   4:

   5:     // get the width and height of the element

   6:     var width = (element.width - baseX * 2) / 2,

   7:         height = (element.height - baseY * 2) / 2;

   8:

   9:     // work out the necessary metrics to draw the target

  10:     var radius = Math.min(width, height),

  11:         centreX = baseX + radius,

  12:         centreY = baseY + radius,

  13:         ringWidth = radius / 10;

  14:

  15:     // get the 2D context to start drawing the target!

  16:     var context = element.getContext("2d");

  17:     context.lineWidth = "2";

  18:

  19:     // define function to draw a ring

  20:     var drawRing = function (strokeStyle, fillStyle, ringRadius) {

  21:         context.strokeStyle = strokeStyle;

  22:         context.fillStyle = fillStyle;

  23:

  24:         // draw the circle

  25:         context.beginPath();

  26:         context.arc(centreX, centreY, ringRadius, 0, Math.PI * 2, true);

  27:         context.closePath();

  28:

  29:         context.stroke();

  30:         context.fill();

  31:     };

  32:

  33:     // draw the rings for each score

  34:     drawRing("#000", "#FFF", radius);

  35:     drawRing("#000", "#FFF", radius -= (ringWidth * 2));

  36:     drawRing("#FFF", "#000", radius -= (ringWidth * 2));

  37:     drawRing("#FFF", "#000", radius -= (ringWidth * 2));

  38:     drawRing("#FFF", "#000", radius -= (ringWidth * 2));

  39:     drawRing("#FFF", "#000", radius -= ringWidth);

  40: }

Animating the Target Boards

Being able to draw an already created target board onto the canvas page is one thing, but how do I animate them using the canvas? Having looked at a few other examples it seems the common way to do animation with the canvas is to simply clear the canvas and redraw the contents on a regular interval. This seems a little low level and requires a bit of plumbing but I’m sure it won’t be long (if not already) before some good, solid frameworks emerge to make these tasks easier.

For now though, let me illustrate how you might create this frame by frame animation yourself.

If you separate the responsibilities of the app, broadly speaking you end up with:

  • a View – i.e. the canvas element, responsible for displaying the targets
  • a Model – the objects representing the targets in the scene, responsible for keeping track of their current position, etc.

If you’ve dealt with any sort of MVC/MVP/MVVM pattern then this kind of separation of duty should be familiar to you already. On each frame (or if you prefer, every time the setInterval delegate function gets invoked), you update the position of the targets in the model, then redraw the targets onto the canvas to reflect their updated positions:

image

This is all you need to do to create a basic animation. So in my case, I need a simple Target object to keep track of:

  • X, Y coordinates of the top left hand corner of the target
  • the radius of the target
  • the direction and speed (per frame) it’s moving at

using MooTools here’s the class I arrived at this Target ‘class’:

   1: /* GLOBAL VARIABLES */

   2: var WIDTH,              // width of the canvas area

   3:     HEIGHT,             // height of the canvas area

   4:

   5:     targets = new Array(),  // the live targets

   6:     targetId = 0,        // the current target ID

   7:

   8:

   9: // define the Target 'class' to represent an on-screen target

  10: var Target = new Class({

  11:     initialize: function (x, y, radius, dx, dy) {

  12:         var _id, _x, _y, _radius, _dx, _dy, is;

  13:

  14:         _id = targetId++;

  15:

  16:         // the X and Y coordinate of the top left corner

  17:         _x = x;

  18:         _y = y;

  19:

  20:         // the radius of the target

  21:         _radius = radius;

  22:

  23:         // the rate of movement in the X and Y direction

  24:         if (dx) {

  25:             _dx = dx;

  26:         } else {

  27:             _dx = Math.ceil(Math.random() * 10);

  28:         }

  29:         if (dy) {

  30:             _dy = dy;

  31:         } else {

  32:             _dy = Math.ceil(Math.random() * 10);

  33:         }

  34:

  35:         // getters

  36:         this.getId = function () {

  37:             return _id;

  38:         }

  39:

  40:         this.getX = function () {

  41:             return _x;

  42:         };

  43:

  44:         this.getY = function () {

  45:             return _y;

  46:         };

  47:

  48:         this.getRadius = function () {

  49:             return _radius;

  50:         };

  51:

  52:         // move the target to its position for the next frame

  53:         this.move = function () {

  54:             _x += _dx;

  55:             _y += _dy;

  56:

  57:             // change direction in X if it 'hits' the border

  58:             if ((_x + _radius * 2) >= WIDTH || _x <= 0) {

  59:                 _dx *= -1;

  60:             }

  61:

  62:             // change direction in Y if it 'hits' the border

  63:             if ((_y + _radius * 2) >= HEIGHT || _y <= 0) {

  64:                 _dy *= -1;

  65:             }

  66:         };

  67:

  68:         // draws the target on the canvas

  69:         this.draw = function () {

  70:             context.drawImage(targetElement, _x, _y);

  71:         };

  72:

  73:         // hit the target!

  74:         this.hit = function () {

  75:             for (var i = 0; i < targets.length; i++) {

  76:                 var target = targets[i];

  77:

  78:                 if (target.getId() == _id) {

  79:                     targets.splice(i, 1);

  80:                     break;

  81:                 }

  82:             }

  83:         };

  84:     }

  85: });

The draw function, which is invoked at regular intervals, first clears the canvas (by filling it with the background colour) then goes through all the targets in the targets array and updates their location and then draw them onto the canvas:

   1: // clear the canvas page

   2: function clear() {

   3:     context.fillStyle = "#000";

   4:     context.fillRect(0, 0, WIDTH, HEIGHT);

   5: }

   6:

   7: // redraw the target boards on the canvas

   8: function draw() {

   9:     // clear the canvas page first

  10:     clear();

  11:

  12:     for (var i = 0; i < targets.length; i++) {

  13:         targets[i].move();

  14:         targets[i].draw();

  15:     }

  16: }

This will give you the basic animation loop, here’s how it looks with 10 moving targets on screen at the same time:

image

Adding Interactions

Now that the animations are in place, let’s add some player interactions. The interactions I’m after here is simple, click in the canvas and knock out any (can be one or more) target that’s clicked on.

I’ve already gone over the process of calculating the coordinates of the click in respect to the canvas in a previous blog post here, this is what that code looks like:

   1: // works out the X, Y position of the click INSIDE the canvas from the X, Y 

   2: // position on the page

   3: function getPosition(mouseEvent, element) {

   4:     var x, y;

   5:     if (mouseEvent.pageX != undefined && mouseEvent.pageY != undefined) {

   6:         x = mouseEvent.pageX;

   7:         y = mouseEvent.pageY;

   8:     } else {

   9:         x = mouseEvent.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;

  10:         y = mouseEvent.clientY + document.body.scrollTop + document.documentElement.scrollTop;

  11:     }

  12:

  13:     return { X: x - element.offsetLeft, Y: y - element.offsetTop };

  14: }

If I can work out where you’ve clicked in the canvas’ coordinate system, then I can simply run a hit test against each moving target and compare the distance between the click and the centre of the target and the target’s radius:

image

If the distance is smaller or equal to the radius then the click happened INSIDE the target and therefore it’s a hit, otherwise it’s a miss. Sounds reasonable enough? Here’s the code that takes a position (a simple object with X and Y position of the click inside the canvas’ coordinate system) and return the targets it has hit:

   1: // check if the player managed to hit any of the live targets

   2: function hitTest(position) {

   3:     var hitTargets = new Array();

   4:

   5:     // check if the position is within the bounds of any of the live targets

   6:     for (var i = 0; i < targets.length; i++) {

   7:         var target = targets[i];

   8:

   9:         var targetCentreX = target.getX() + target.getRadius(),

  10:             targetCentreY = target.getY() + target.getRadius();

  11:

  12:         // work out the distance between the position and the target's centre

  13:         var xdiff = position.X - targetCentreX,

  14:             ydiff = position.Y - targetCentreY,

  15:             dist = Math.sqrt(Math.pow(xdiff, 2) + Math.pow(ydiff, 2));

  16:

  17:         // if that distance is less than the radius of the target then the

  18:         // position is inside the target

  19:         if (dist <= target.getRadius()) {

  20:             hitTargets.push(target);

  21:         }

  22:     }

  23:

  24:     return hitTargets;

  25: }

To hook this up, I added an event handler to the mousedown event on the canvas during initialization:

   1: $("#canvas").mousedown(function (mouseEvent) {

   2:     // get the coordinates of the click inside the canvas

   3:     var position = getPosition(mouseEvent, this);

   4:

   5:     // find out which targets were hit

   6:     var hitTargets = hitTest(position);

   7:

   8:     // hit the targets

   9:     for (var i = 0; i < hitTargets.length; i++) {

  10:         hitTargets[i].hit();

  11:     }

  12: }

And now, when you ‘hit’ a target it’ll be removed from the array of moving targets and therefore won’t be drawn again when the canvas is refreshed in the next frame.

Adding Notifications

Finally, for this first-pass implementation of a mini-shooting game, I’d like to add some notifications to tell you when you’ve hit something, or when you’ve missed completely!

This is slightly trickier than the targets as the messages should not stay around forever until some user-triggered action, instead it should be shown for a given amount of time (or frames). To facilitate this requirement, I created another ‘class’ called Message:

   1: // define the Message 'class' to represent an on-screen message

   2: var Message = new Class({

   3:     initialize: function (x, y, message, duration) {

   4:         var _id, _x, _y, _message, _duration;

   5:

   6:         _id = messageId++;

   7:

   8:         // X, Y coordinates of where to display the message

   9:         _x = x;

  10:         _y = y;

  11:

  12:         // the message

  13:         _message = message;

  14:

  15:         // how many frames to display the message for

  16:         _duration = duration;

  17:

  18:         this.getId = function () {

  19:             return _id;

  20:         }

  21:

  22:         this.draw = function () {

  23:             if (_duration >= 0) {

  24:                 context.textBaseline = "middle";

  25:                 context.textAlign = "center";

  26:                 context.fillStyle = "#FFF";

  27:                 context.strokeStyle = "#000";

  28:                 context.font = "bold 40px arial";

  29:

  30:                 // draw the message at the specified X, Y coordinates

  31:                 context.fillText(_message, _x, _y);

  32:

  33:                 _duration--;

  34:             } else {

  35:                 // remove the message

  36:                 for (var i = 0; i < messages.length; i++) {

  37:                     var message = messages[i];

  38:

  39:                     if (message.getId() == _id) {

  40:                         messages.splice(i, 1);

  41:                         break;

  42:                     }

  43:                 }

  44:             }

  45:         }

  46:     }

  47: });

The Message objects can only been drawn a number of times, after which it will remove itself from the array of messages currently being displayed.

To make it a bit more interesting, I defined a number of messages which will be displayed depending on how many targets you’ve managed to hit at once:

   1: // define the messages to show

   2: var hitMessages = new Array();

   3: hitMessages[0] = "MISS";

   4: hitMessages[1] = "HIT!";

   5: hitMessages[2] = "DOUBLE HIT!!";

   6: hitMessages[3] = "HAT-TRICK!!!";

   7: hitMessages[4] = "UN~BELIEVABLE!!!!";

   8: hitMessages[5] = "OH MY GOSH!!";

On the mousedown event handler (see above) I added these couple of lines to push a new message to the messages stack to be displayed for 30 frames, and the message is determined by how many targets was hit:

   1: // use one of the defined messages if possible, otherwise use a default

   2: var hitMessage = hitMessages[hitTargets.length];

   3: if (hitMessage == undefined)

   4: {

   5:     hitMessage = "TOO GOOOOOOOOD..";

   6: }

   7:

   8: messages.push(new Message(position.X, position.Y, hitMessage, 30));

For example, if you managed to hit three targets one shot:

image

Pretty neat, eh? :-)

Demo

Here is the full demo, over the next couple of days, I will polish it up and add some more features to make it feel more gamey and post another update. In the mean time though, feel free to play around with it and let me know of any suggestions you have on how to improve it!

References:

HTML5 canvas cheat sheet

Dive into HTML5 – peeks, pokes and pointers

Related posts:

Having fun with HTML5 – Canvas, part 1

Having fun with HTML5 – Canvas, part 2

Having fun with HTML5 – Canvas, part 3

Having fun with HTML5 – Canvas, part 5