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?
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: #canvas2: {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:
(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 canvas2: function initializeTargetCanvas(element) {3: var baseX = 1.5, baseY = 1.5;4:5: // get the width and height of the element6: var width = (element.width - baseX * 2) / 2,7: height = (element.height - baseY * 2) / 2;8:9: // work out the necessary metrics to draw the target10: 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 ring20: var drawRing = function (strokeStyle, fillStyle, ringRadius) {21: context.strokeStyle = strokeStyle;22: context.fillStyle = fillStyle;23:24: // draw the circle25: 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 score34: 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:
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 area3: HEIGHT, // height of the canvas area4: …5: targets = new Array(), // the live targets6: targetId = 0, // the current target ID7: …8:9: // define the Target 'class' to represent an on-screen target10: 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 corner17: _x = x;18: _y = y;19:20: // the radius of the target21: _radius = radius;22:23: // the rate of movement in the X and Y direction24: 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: // getters36: 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 frame53: this.move = function () {54: _x += _dx;55: _y += _dy;56:57: // change direction in X if it 'hits' the border58: if ((_x + _radius * 2) >= WIDTH || _x <= 0) {59: _dx *= -1;60: }61:62: // change direction in Y if it 'hits' the border63: if ((_y + _radius * 2) >= HEIGHT || _y <= 0) {64: _dy *= -1;65: }66: };67:68: // draws the target on the canvas69: 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 page2: function clear() {3: context.fillStyle = "#000";4: context.fillRect(0, 0, WIDTH, HEIGHT);5: }6:7: // redraw the target boards on the canvas8: function draw() {9: // clear the canvas page first10: 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:
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, Y2: // position on the page3: 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:
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 targets2: function hitTest(position) {3: var hitTargets = new Array();4:5: // check if the position is within the bounds of any of the live targets6: 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 centre13: 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 the18: // position is inside the target19: 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 canvas3: var position = getPosition(mouseEvent, this);4:5: // find out which targets were hit6: var hitTargets = hitTest(position);7:8: // hit the targets9: 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 message2: 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 message9: _x = x;10: _y = y;11:12: // the message13: _message = message;14:15: // how many frames to display the message for16: _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 coordinates31: context.fillText(_message, _x, _y);32:33: _duration--;34: } else {35: // remove the message36: 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 show2: 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 default2: 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:
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:
Dive into HTML5 — peeks, pokes and pointers
Related posts:
Having fun with HTML5 — Canvas, part 1
Having fun with HTML5 — Canvas, part 2