Monday, February 25, 2008

JavaScript Animation - Part 3 - The Code So Far

In the last JavaScript Animation post, we left off with code for the AnimationManager class. So far we can do is call addMovement, specifying a direction, the total number of pixels to move, and the number of pixels to move during each "frame". Then, when startAnimation is called, the manager will automatically daisy chain calls to the Window object's setInterval method. When the element specified in each movement instruction has reached it's destination, the next movement instruction is executed. Not bad for a start. But this doesn't yet allow us to do two rather useful things.


  • Move in two directions at once, i.e., diagonally or along a curve.

  • Move two elements at once.


But before we add new functionality, let's go over what we have so far.



A quick note before we begin. I prefer to use the object literal syntax for class declaration. In case you aren't familiar with it, all it means is that instead of adding member variables and functions to the hidden prototype object one at a time...

Foo.prototype.someMethod = function(){ ... };
Foo.prototype.some_int = 1;

I add them all at once...
Foo.prototype = {
some_int : 1,
someMethod : function(){ ... }
}

Not that different, but it is easier to read, is a bit more compact (fewer characters=smaller files), and much easier to type since you don't have to constantly be writing out prototype. Now there is a downside: since most of the JavaScript IDE plugins I've seen are not able to parse class structures when written this way, so they can't help in outlining the class for quick navigation. For example, the "Outline" view in Eclipse becomes useless, so you might want to stick with the more verbose style if that will slow you down too much.



The AnimationManager class begins with a few member variable declarations.

function AnimationManager(){
this.initialize();
}
AnimationManager.prototype = {

movements : undefined,
intervals : undefined,
current_movement_index : undefined,


The first two are arrays that will hold the movements we ask the manager to perform and the interval ids returned by the various calls to the Window object's setInterval method They are not initialized when declared, because though the AnimationManager class will most often be instantiated as a Singleton, you may want more than one at some point and we don't want them all sharing the same array. (I will talk more about this later). So instead they are initialized
in a method called, you guessed it, initialize.



initialize : function(){

this.movements = new Array();
this.intervals = new Array();
},


To those of you familiar with the Prototype JavaScript library, the initialize method is essentially a replacement for the classes constructor method. So when you call

Class.create({ // class definition using object literal syntax })

the initialize() method will be called as soon as the class is instantiated.
But since I am not using Prototype here, we will accomplish more or less the same thing by calling initialize()
in the contructor.



So on to the method that tells AnimationManager what to do. Since there are no private methods in JavaScript, we can't hide the rest of the class, but this is really the only method aside from startAnimation and stopAnimation that another class or script would ever have to access. (At least until we start adding functionality later.) The addMovement method takes four arguments,


  • element - the element we want to move around,

  • direction - the direction (up, down, left, right) we want element to move,

  • pixel_count - how many pixels to move it,

  • increment - how many pixels to move it from frame to frame of the animation.

The first three are self-explanatory, but increment might be a little confusing if you still haven't grasped the whole "frame" thing.More on that in a bit. For now, all we need to know is that these four values are put together into an anonymous object and stored (in order) along with any other instructions the manager may receive. But what about the other two variables being stored in the object - start_x and start_y? What are they for? We'll get to that in a moment.


addMovement : function( element, direction, pixel_count, increment ){

if( element ){

this.movements.push( {

element : element,
start_x : 0,
start_y : 0,
direction : direction,
pixel_count : pixel_count,
increment : increment

});

}else{
// element doesn't exist
return false;
}
},



Next we have nextMovement, which takes no arguments, but knows a couple of things about its containing class.

nextMovement : function(){

if( this.current_movement_index < this.movements.length ){

var movement = this.movements[this.current_movement_index];
movement.start_x= this.findPosX( movement.element );
movement.start_y = this.findPosY( movement.element );

...


The member variable current_movement_index is simply that, an index of the movement currently being executed. The list that it is associated with is movements, the array we stored all our movments in as they were added by an outside script or class. The first line of nextMovement asks if there are still movements left to be executed. If so, we go on to calculate and store the values for start_x and start_y. These are simply the starting x and y positions (in pixels) for the element we are about to start moving. These are used to check how much progress has been made with each successive call of executeMovement. Again, more about them later. Next we come to one of the more confusing parts of the whole project. It's actually not that complicated once you get the hang of it, but it can present a bit of a conceptual hurdle.



When you call a JavaScript function, the variable scope of that function is determined not by its declaration, but by its caller. So even if we create the class Foo and declare and define the method bar() as a member function, we cannot guarantee that, when it is called, a this pointer inside the scope of the method will actually point to an instance of the class Foo. In practice, this really only applies to callbacks.
Here is an example.

function Foo(){};
Foo.prototype = {
class_name : "Foo",
bar : function(){
alert( "bar belongs to " + this.class_name );
}
}

var foo = new Foo();
some_button.onclick = foo.bar;

When some_button is clicked, you will get the following alert message,
bar belongs to undefined

Why? Because when some_button is clicked, and it calls Foo's bar method, bar is executed inside some_button's scope, and some_button does not have a member variable named class_name. Basically some_button has stolen and co-opted bar(), and made it it's own.



But what, you may ask, does this have to do with the AnimationManager? Well let's have a look. First we will write the method in the same way as we did the Foo.bar() example.


...

this.intervals[this.current_movement_index] = window.setInterval( function(){

if( !this.executeMovement( this, new Array() ) ){

window.clearInterval( this.intervals[this.current_movement_index] );
this.current_movement_index++;
this.nextMovement( this, new Array() );
}

}, 30 );

return true;
}

},


This looks like a fairly straightforward example of a recursive function call. Each time setInterval is called, we check to see if executeMovement is through with its work, and if it is, we clear the interval and queue up the next movment. But run it and you won't get past the second line. The problem is this. Inside the anonymous function that will be run over and over by setInterval, the this pointer refers to the anonymous function object we created, which has no idea about what executeMovement or intervals might mean. It would simply tell the interpreter that they are undefined.



Now you might suggest pointing setInterval to a member function instead, but this would still result in the same error, only it would happen further up the call stack. In the same way some_button stole the scope of bar() away from Foo, so too would Window.setInterval steal the member function's scope away from AnimationManager.



So how do we deal with this? We need to maintain scope or we'll have to put the entire functionality of AnimationManager inside setInterval. The solution turns out to be an otherwise obscure native function called apply(). apply is a member of the native JavaScript function class, so any function object can use it. It takes two arguments.



  • thisArg - the object you want this to resolve to,

  • arguments - an array of arguments passed to the function you are calling.

So going back to the Foo.bar example, if we use apply(), it would look like this.
function Foo(){};
Foo.prototype = {
class_name : "Foo",
bar : function(){
alert( "bar belongs to " + this.class_name );
}
}

var foo = new Foo();
some_button.onclick = function(){
Foo.prototype.bar.apply( foo, new Array() );
}

Now if we click on some_button, the alert will say,
bar belongs to Foo


So applying (pardon the pun) the same principle to AnimationManager, we can write it like this.

    ... 

var ani_mgr = this;
this.intervals[this.current_movement_index] = window.setInterval( function(){

if( !AnimationManager.prototype.executeMovement.apply( ani_mgr, new Array() ) ){

window.clearInterval( ani_mgr.intervals[ani_mgr.current_movement_index] );
ani_mgr.current_movement_index++;
AnimationManager.prototype.nextMovement.apply( ani_mgr, new Array() );
}

}, 30 );
}

},

Now when executeMovement is called, the this pointer refers to the instance of AnimationManager we assigned to the local variable ani_mgr. We solve the same problem inside the anonymous function by substituting ani_mgr for this.



Notice that we set the time between executions of setInterval to 30ms, as per the formula mentioned in the
first post in the series.



Next we have executeMovement. It acts mainly as a router. It determines the direction of the movement being executed, and makes a call to the appropriate member function. If the element is to be moved to the right, it calls - you guessed it - moveRight. This is done using a simple switch-case enclosure, but to make things a bit more readable and yet avoid using cpu intensive string comparisons, I created a series of static member variables that can be used kind of like an Enumeration. The variable declarations are at the end of the class declaration, since static members are not generally declared as a part of the prototype object, and so do not have to be associated with a particular instance of the class.


executeMovement : function(){

var movement = this.movements[this.current_movement_index];
var element = movement.element;

if( element ){

switch( movement.direction ){

case AnimationManager.UP:
return this.moveUp( movement );
break;

case AnimationManager.RIGHT:
return this.moveRight( movement );
break;

case AnimationManager.DOWN:
return this.moveDown( movement );
break;

case AnimationManager.LEFT:
return this.moveLeft( movement );
break;

default: return false;
}
}else{

console.info( "no element for executeMovement" );
}


},


Next up we have the machinery of the AnimationManager.


  • moveUp - moves the element up

  • moveRight - moves the element right

  • moveDown - moves the element down

  • moveLeft - moves the element left





moveUp : function( movement ){

var destination = movement.start_y - movement.pixel_count;
var current_y = this.findPosY( movement.element );

if( movement.element && destination < current_y ){
movement.element.style.top = ( current_y - movement.increment ) + "px";
return true;
}else
return false;


},

moveRight : function( movement ){

var destination = movement.start_x + movement.pixel_count;
var current_x = this.findPosX( movement.element );

if( movement.element && current_x < destination ){
movement.element.style.left = ( current_x + movement.increment ) + "px";
return true;
}else
return false;
},


moveDown : function( movement ){

var destination = movement.start_y + movement.pixel_count;
var current_y = this.findPosY( movement.element );

if( movement.element && destination > current_y ){
movement.element.style.top = ( current_y + movement.increment ) + "px";
return true;
}else
return false;
},

moveLeft : function( movement ){

var destination = movement.start_x - movement.pixel_count;
var current_x = this.findPosX( movement.element );

if( movement.element && destination < current_x ){
movement.element.style.left = ( current_x - movement.increment ) + "px";
return true;
}else
return false;

},

I ordered them this way because they match the clockwise directional order you see in CSS classes: top, right, bottom, left. Better to be consistent than confused.



All four direction methods function in the same way: check the current position of the element against the place it's supposed to end up. If it hasn't reached it's destination yet move it increment pixels. The destination is determined based on the direction the element is supposed to move. If its moving to the right, executeMovement calls moveRight. Moving to the right means moving in a positive direction along the x-axis, so we can simply add the the pixel_count provided by addMovement to the element's start_x. So,

var destination = movement.start_x + movement.pixel_count;


To get the current_x position of the element, we use findPosX, which is based on the work of Peter-Paul Koch over at quirksmode.org. It is a cross-browser method for getting the pixel position of any element on the page. It is a little slow for work like this because it has to deal with the quirks inherent to the various browsers, but it works well, so I'm not about to mess with it at the moment. We'll go over the code for it later.
var current_x = this.findPosX( movement.element );

Now that moveRight still has work to do, it goes ahead and does it.


if( movement.element && current_x < destination ){
movement.element.style.left = ( current_x + movement.increment ) + "px";
return true;
}else
return false;
},

When the moving is done, the method returns true. Back in nextMovement, it tests the return value of the move to determine whether that move should be repeated,
or whether the next move should be executed. So returning true means do it again, while returning false means the element has reached it's destination and
current_movement_index should be incremented by one before calling nextMovement again.




Next we come to the on/off switch for AnimationManager. After adding movements, we have to call startAnimation to get things actually moving. It simply sets
current_movement_index to 0 (just in case we've already run through some movements), and gets things started by calling nextMovement. If at some point we want
to stop on a dime, we can call stopAnimation, which will make sure any and all setInterval calls are cleared using clearInterval.



startAnimation : function(){


this.current_movement_index = 0;


this.nextMovement();
},


stopAnimation : function(){

for( var i=0; i<this.intervals.length; i++ ){

window.clearInterval( this.intervals[i] );

}

},




Finnally, we have the Peter-Paul Koch-based methods for finding the x and y position of an element. Actually what it finds is the left and top positions of an element, but they are effectively the same thing. I won't go into how he does it, because you should have a look at his site anyway. Its an invaluable resource for web developers. Check it out.

findPosX : function( element ){

var curleft = 0;

if(element.offsetParent){
while(1){
curleft += element.offsetLeft;
if(!element.offsetParent)
break;
element = element.offsetParent;
}
}else if(element.x){
curleft += element.x;
}
return curleft;
},


findPosY : function( element ){

var curTop = 0;
if(element.offsetParent)
while(1){
curTop += element.offsetTop;
if(!element.offsetParent)
break;
element = element.offsetParent;
}
else if(element.y)
curTop += element.y;
return curTop;
}



Finally we have the static values for UP, DOWN, LEFT and RIGHT. Nothing mysterious here, but this technique comes in handy
for making your code more human readable. Any time you can easily replace numbers with words you should, because a wise man once said, we read our code far more often than we write it.

AnimationManager.UP = 0;
AnimationManager.RIGHT = 1;
AnimationManager.DOWN = 2;
AnimationManager.LEFT = 3;




So that's what we have so far. But what's missing? Some were mentioned at the start.


  • Move in multiple directions simultaneously

  • Move more than one element at a time



But there are a few more I can think of.

  • Pause and Play - Currently, stopping the animation and starting it again will reset the whole thing. We need to be able to pause everything, then start again from were it was.

  • Dynamically change movement instructions - If we're going to make this interactive, it can't just have a bunch of hard coded instructions that can't be changed once it gets moving.
    We need to be able to modify the instructions based on user interactions.

  • Test for collisions - Flash allows us to test whether two things on the stage are in contact with one another. We need to be able to do this with just JavaScript, HTML and CSS.


So next time we will make some changes and some additions to AnimationManager, as well as create some new wrapper and helper classes that will make the new functionality
effective, efficient and scalable.

No comments: