Having fun with HTML5 — Canvas, part 4

Fol­low­ing on from part 3 where we basi­cal­ly made a lit­tle app that lets you scrib­ble with HTML5’s can­vas ele­ment, let us push on and see what else we can achieve with the can­vas ele­ment.

The log­i­cal next step would be to made ani­ma­tions, and give you a way to inter­act with the ani­ma­tion. So how about a lit­tle game that lets you shoot mov­ing tar­gets like those clas­sic Point Blank games? Hands up who’s up for that?

image

Good good, that’s decid­ed then! :-)

To Start Off..

First, we’ll need a sim­ple HTML page, some­thing like my pre­vi­ous 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>

Noth­ing fan­cy here, the CSS for this page is also pret­ty basic too, the only thing worth not­ing is that to get the crosshair cur­sor inside the can­vas I includ­ed this in my CSS:

   1: #canvas

   2: {

   3:     cursor: crosshair;

   4: }

The onS­e­lect­Start line in the can­vas HTML is to ensure that the crosshair cur­sor is used when you click and hold inside the can­vas ele­ment.

You might have also noticed that I had spec­i­fied two can­vas ele­ments in the HTML, this is to sim­pli­fy the task of draw­ing mov­ing tar­gets onto the main can­vas. With the 2D con­text object’s draw­Im­age func­tion, you can draw an img, can­vas, or video ele­ment so I can draw the tar­get onto the tar­get can­vas dur­ing ini­tial­iza­tion and then reuse it in the main can­vas.

Drawing the Target board

Next, I need to draw the tar­get boards, and make them look like this, but with­out the score on the rings:

image

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

As you can imag­ine, we can cre­ate a tar­get board look­ing like this by draw­ing 6 cir­cles, with the inner­most 2 cir­cles hav­ing rough­ly half the radius of the oth­er 4 and the out­er­most 2 cir­cles hav­ing the inversed fill/stroke colour com­bi­na­tion. Expand the below sec­tion to see the ini­tial­ize­Tar­get­Can­vas func­tion which takes a can­vas ele­ment and draws a tar­get 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 cre­at­ed tar­get board onto the can­vas page is one thing, but how do I ani­mate them using the can­vas? Hav­ing looked at a few oth­er exam­ples it seems the com­mon way to do ani­ma­tion with the can­vas is to sim­ply clear the can­vas and redraw the con­tents on a reg­u­lar inter­val. This seems a lit­tle low lev­el and requires a bit of plumb­ing but I’m sure it won’t be long (if not already) before some good, sol­id frame­works emerge to make these tasks eas­i­er.

For now though, let me illus­trate how you might cre­ate this frame by frame ani­ma­tion your­self.

If you sep­a­rate the respon­si­bil­i­ties of the app, broad­ly speak­ing you end up with:

  • a View – i.e. the can­vas ele­ment, respon­si­ble for dis­play­ing the tar­gets
  • a Mod­el – the objects rep­re­sent­ing the tar­gets in the scene, respon­si­ble for keep­ing track of their cur­rent posi­tion, etc.

If you’ve dealt with any sort of MVC/MVP/MVVM pat­tern then this kind of sep­a­ra­tion of duty should be famil­iar to you already. On each frame (or if you pre­fer, every time the set­Inter­val del­e­gate func­tion gets invoked), you update the posi­tion of the tar­gets in the mod­el, then redraw the tar­gets onto the can­vas to reflect their updat­ed posi­tions:

image

This is all you need to do to cre­ate a basic ani­ma­tion. So in my case, I need a sim­ple Tar­get object to keep track of:

  • X, Y coor­di­nates of the top left hand cor­ner of the tar­get
  • the radius of the tar­get
  • the direc­tion and speed (per frame) it’s mov­ing at

using MooTools here’s the class I arrived at this Tar­get ‘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 func­tion, which is invoked at reg­u­lar inter­vals, first clears the can­vas (by fill­ing it with the back­ground colour) then goes through all the tar­gets in the tar­gets array and updates their loca­tion and then draw them onto the can­vas:

   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 ani­ma­tion loop, here’s how it looks with 10 mov­ing tar­gets on screen at the same time:

image

Adding Interactions

Now that the ani­ma­tions are in place, let’s add some play­er inter­ac­tions. The inter­ac­tions I’m after here is sim­ple, click in the can­vas and knock out any (can be one or more) tar­get that’s clicked on.

I’ve already gone over the process of cal­cu­lat­ing the coor­di­nates of the click in respect to the can­vas in a pre­vi­ous 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 can­vas’ coor­di­nate sys­tem, then I can sim­ply run a hit test against each mov­ing tar­get and com­pare the dis­tance between the click and the cen­tre of the tar­get and the target’s radius:

image

If the dis­tance is small­er or equal to the radius then the click hap­pened INSIDE the tar­get and there­fore it’s a hit, oth­er­wise it’s a miss. Sounds rea­son­able enough? Here’s the code that takes a posi­tion (a sim­ple object with X and Y posi­tion of the click inside the can­vas’ coor­di­nate sys­tem) and return the tar­gets 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 han­dler to the mouse­down event on the can­vas dur­ing ini­tial­iza­tion:

   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 tar­get it’ll be removed from the array of mov­ing tar­gets and there­fore won’t be drawn again when the can­vas is refreshed in the next frame.

Adding Notifications

Final­ly, for this first-pass imple­men­ta­tion of a mini-shoot­ing game, I’d like to add some noti­fi­ca­tions to tell you when you’ve hit some­thing, or when you’ve missed com­plete­ly!

This is slight­ly trick­i­er than the tar­gets as the mes­sages should not stay around for­ev­er until some user-trig­gered action, instead it should be shown for a giv­en amount of time (or frames). To facil­i­tate this require­ment, I cre­at­ed anoth­er ‘class’ called Mes­sage:

   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 Mes­sage objects can only been drawn a num­ber of times, after which it will remove itself from the array of mes­sages cur­rent­ly being dis­played.

To make it a bit more inter­est­ing, I defined a num­ber of mes­sages which will be dis­played depend­ing on how many tar­gets you’ve man­aged 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 mouse­down event han­dler (see above) I added these cou­ple of lines to push a new mes­sage to the mes­sages stack to be dis­played for 30 frames, and the mes­sage is deter­mined by how many tar­gets 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 exam­ple, if you man­aged to hit three tar­gets one shot:

image

Pret­ty neat, eh? :-)

Demo

Here is the full demo, over the next cou­ple of days, I will pol­ish it up and add some more fea­tures to make it feel more gamey and post anoth­er update. In the mean time though, feel free to play around with it and let me know of any sug­ges­tions you have on how to improve it!

References:

HTML5 can­vas cheat sheet

Dive into HTML5 – peeks, pokes and point­ers

Related posts:

Hav­ing fun with HTML5 — Can­vas, part 1

Hav­ing fun with HTML5 — Can­vas, part 2

Hav­ing fun with HTML5 — Can­vas, part 3

Hav­ing fun with HTML5 — Can­vas, part 5