My first step was to get an icon. I have no artistic skill, so I downloaded icons. To make sure I don't step on anyone's licensed toes I grabbed images from commons.wikimedia.org.
Next was to create the HTML. The HTML looks very similar to the drawing program, except that I have an icon layer rather than a drawing layer. Also, I don't need to show any control information, so I am did away with the control layer.
<div id='windowContainer'> <canvas id="gridLayer" class="gridLayer" height="500" width="800"></canvas> <canvas id="iconLayer" class="gridLayer" height="500" width="800"></canvas> </div>Next I created some helper classes. I have an Icon class which holds the image, the location of the icon, knows how to move the icon, and how to draw the icon. I have an IconLayer class which keeps track of all of the icons and is responsible for drawing/clearing the icon layer as a whole. I have a IconControl class which is responsible for tracking mouse clicks and calling the move methods on the appropriate icon at the appropriate time. I also reused the Grid class and Geometry classes from before.
function Icon(img, iconLayer, gridPt) { /* code */ } Icon.prototype.draw = function() { /* code */ } Icon.prototype.setGridPoint = function(point) { /* code */ } Icon.prototype.moveTo = function(point) { /* code */ } function IconLayer(canvas, grid) { /* code */ } IconLayer.prototype.draw = function() { /* code */ } IconLayer.prototype.addIcon = function(img, gridPoint) { /* code */ } IconLayer.prototype.findIcon = function(point){ /* code */ } function IconControls(iconLayer) { /* code */ } IconControls.prototype.click = function(x, y) { /* code */ }I want the movement of the icons to be visible to the user which means that I can't just draw them at their destination. So, to move an icon, I'll erase it, draw it slightly closer to the new locations, and then wait a little bit and repeat the process. However, if I have two icons moving, I don't want to be redrawing twice as often. My solution to this is to have each icon, when it is moving, to just update its own location, but not to redraw. The IconLayer will periodically redraw itself with the icons in their new location. To keep from redrawing repeatedly when no icons are moving, the IconLayer will only redraw as long as at least one Icon is moving.
I was concerned about threading issues, but it appears that while JavaScript is asynchronous, it is actually single threaded. This means I shouldn't have to worry about race conditions. Of course if a function goes into an infinite loop, it does mean nothing else will run. Anyway, here are the methods for moving an icon.
1 Icon.prototype.movePerUnit = 5; 2 Icon.prototype.delayPerMove = 50; 3 Icon.prototype.moveTo = function(point){ 4 this.moveQueue.push({gridPoint:point, realPoint:this.grid.getReal(point)}); 5 if (!this.moving) { 6 this.moving = true; 7 this.iconLayer.incrMovingIcon(); 8 this.moveImpl(); 9 } 10 } 11 Icon.prototype.moveImpl = function(){ 12 var move = this.nextMove(); 13 if (!move) { 14 this.moving = false; 15 this.iconLayer.decrMovingIcon(); 16 } else { 17 this.realPoint.x += move.dx; 18 this.realPoint.y += move.dy; 19 if (--move.steps < 1) { 20 this.realPoint = move.realPoint; 21 this.gridPoint = move.gridPoint; 22 } 23 setTimeout(this.moveFunc, this.delayPerMove); 24 } 25 } 26 Icon.prototype.nextMove = function(){ 27 var move = this.moveQueue.peek(); 28 while (move != null && this.gridPoint.eq(move.gridPoint)) { 29 this.moveQueue.pop(); 30 move = this.moveQueue.peek(); 31 } 32 if (move && !move.steps) { 33 move.steps = move.gridPoint.dis(this.gridPoint) * this.movePerUnit; 34 move.dx = (move.realPoint.x - this.realPoint.x) / move.steps; 35 move.dy = (move.realPoint.y - this.realPoint.y) / move.steps; 36 } 37 return move; 38 }The
moveTo
method (lines 3-10) pushes the new move onto the moveQueue
and then starts the move, if necessary. moveToImpl
method does the real action of making a move. Lines 14-15 handles ending a move. Lines 17-18 actually make the move. Lines 20 and 21 make sure that we end on the right spot and gets rid of rounding errors that might've happend along the way. Lines 23 makes sure that the moveImpl
function which will get called repeatedly. nextMove
calculates the next move. The loop on lines 28-31 finds the next move in the queue that isn't the current location. Lines 33-35 calculate how the move will be made, if that hasn't already been done for this move object.The function
this.moveFunc
that is referenced on line 23 is defined in the Icon constructor asvar self = this; this.moveFunc = function() {self.moveImpl();};This is done so that we will have access to the appropriate
this
value when moveImpl
is called by the setTimeout
function. IconLayer's methods incrMovingIcon
and decrMovingIcon
called on lines 7 and 15 tell the IconLayer to start or stop drawing, if needed. These methods look like:1 IconLayer.prototype.refreshTimeout = 25; 2 IconLayer.prototype.incrMovingIcon = function(){ 3 if (this.movingIconCt == 0) { 4 this.drawTimer = setInterval(this.drawFunc, this.refreshTimeout); 5 } 6 this.movingIconCt++; 7 } 8 IconLayer.prototype.decrMovingIcon = function(){ 9 this.movingIconCt--; 10 if (this.movingIconCt == 0) { 11 clearInterval(this.drawTimer); 12 this.draw(); 13 } 14 }
drawFunc
(on line 4) is similar to moveFunc
up above and is set in the constructor and refers to draw
. draw
just clears the IconLayer and then draws all of the Icons in their current location. These methods scream "race-condition" to me, but as I stated above, JavaScript is actually run single-threaded, so this isn't an issue.Well, that was the meat of the code. The only other interesting thing was the loading of the images. I originally just added the icons right away in the script, like:
1 var names = ["smile", "frown", "kiss", "cool"]; 2 for (var i = 0; i < names.length; i++) { 3 var img = document.getElementById(names[i] + "_icon"); 4 iconLayer.addIcon(img, {x: i*2, y:0}); 5 } 6 iconLayer.draw(); // ERROR! - doesn't work.and this caused errors because JavasScript would try drawing the image to the canvas before the browser had actually downloaded the whole image. Oops. I had to use the
image.onload
method to add the images after they were loaded. However, I really wanted to run after the last image was loaded. The code to do that is below. I add each icon as they are loaded, and keep a count. I don't call draw
until the last one is loaded. Note that I actually add a property to the image object so that I will have access to it inside the onload method. And since onload
is a property of the image, these parameters are accessible via the this
variable inside the onload function, which is still kind of odd to my C++/Java/C# brain, but I am getting used to it.1 var names = ["smile", "frown", "kiss", "cool"]; 2 var loaded = 0; 3 for (var i = 0; i < names.length; i++) { 4 var img = document.getElementById(names[i] + "_icon"); 5 img.my_x = 2*i; 6 img.onload = function() { 7 iconLayer.addIcon(this, {x: this.my_x, y:0}); 8 loaded++; 9 if (loaded == names.length) { 10 iconLayer.draw(); 11 } 12 } 13 }Demo
Anyway, here's the demo. Click on one of the icons. Click a destination. Lather, rinse, repeat.