Monday, September 27, 2010

Moving images in JavaScript

So now that I have a drawing program and can save drawings, I am ready for the next step. I want to be able to place an icon on the grid and be able to move it around, treating the lines drawn as walls. This next app is a little simpler though and will just move icons around. I will leave it for a future app to combine the two.

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

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>
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 */ } = 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 as
var 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 }

Anyway, here's the demo.  Click on one of the icons.  Click a destination.  Lather, rinse, repeat.

No comments: