Monday, February 18, 2008

Javascript Animation - Part 2 - Multiple Vectors

Yes, I know. "Multiple Vectors" looks very complicated and quite pretentious. But it is the most succinct way to describe what I am going to tackle in this post. If you have a better way to describe it in a couple of words, please let me know. That said, it's really not a complex concept, but it takes a bit of work to get one's mind around it, and I figure I should get your juices going early in the process.

So here's the problem.

In the last entry on the subject, I described the nature of the project - to do as much animation as possible without using anything except HTML, CSS and JavaScript. In other words, no Flash. (And certainly no SilverLight. Someday I will be including SVG in the mix, but not yet.) The first problem was getting an element to move around fast enough to appear as smooth, realistic movement, but slow enough for the eye and the brain to percieve it as motion and not an instantaneous change of location. We can do this by using JavaScript's native setInterval method to make incremental changes over time, instead of all at once. A pause of 40ms or so between executions of our moveElement function does the job. Each execution moves the element one pixel to the right, and 100 iterations later we use the JS native method clearInterval to bring things to a stop.

To check whether to continue or to stop, we simply test whether we have moved the element the desired number of pixels, and then call clearInterval...

if( current_x > dest_x )
window.clearInterval( interval_id );

Simple enough. But what if we want to now move it up 50 pixels? Your first instinct might be to simply daisy chain some setInterval calls like so...


function moveRight(){
if( current_x > dest_x ){
window.clearInterval( interval_id );
interval_id = window.setInterval( moveUp, 30 );
}else
// move the element to the right...
}


function moveUp(){
if( current_y < dest_y )
window.clearInterval( interval_id );
else
// move the element up
}

This will certainly work, but this will very quickly become very difficult to maintain. A better way to handle this would be to create a class capable of moving an element around on the page with a minimal amount of effort on the developer's part. What we're looking for here is a JS animation framework.


The AnimationManager Class


We begin with a very basic manager class to handle requests to move an element around on the page. So what do we need this class to do? First we need to give it the id attribute of an element on the page. Simple enough. Then we need to give it instructions on what we want to do with that element. Not as simple.

For the purposes of this post, we will need the class to take a series of instructions about how to move an element an arbitrary number of pixels along the X or Y axis. So here's what we would like to be able to type something like the following to move a target element in a 100x100 square, 5 pixels at a time.


var target = $( "target-div" ); // prototype element selector shortcut
var animation_mgr = new AnimationManager();
animation_mgr.addMovement( target, AnimationManager.RIGHT, 100, 5 );
animation_mgr.addMovement( target, AnimationManager.DOWN, 100, 5 );
animation_mgr.addMovement( target, AnimationManager.LEFT, 100, 5 );
animation_mgr.addMovement( target, AnimationManager.UP, 100, 5 );
animation_mgr.startAnimation();

Obviously it would be even simpler to call moveInASquareShape or something like that, and perhaps we might do that one day, but not right now. What we are looking for at the moment is some way to chain together a series of movements in an intuitive way. Here's what I came up with. Have a look, and I'll explain it all in the next post. So stay tuned!



function AnimationManager(){}
AnimationManager.prototype = {

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

initialize : function(){


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

},


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;
}
},

nextMovement : function(){

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

var movement = this.movements[this.current_movement_index];
var start_x = this.findPosX( movement.element );
var start_y = this.findPosY( movement.element );
movement.start_x = start_x;
movement.start_y = start_y;
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 );

return true;
}

},


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" );
}


},



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;

},




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] );

}

},


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;
}





})

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




We can test this by placing a test element on the page, adding a few movements, and adding a couple of buttons to trigger the startAnimation() and stopAnimation() methods of the AnimationManager.

NOTE: Be sure you have the AnimationManager.js file in the proper place.



<html>
<head>
<script language='JavaScript' type='text/javascript' src='AnimationManager.js'></script>
<script language='JavaScript'>
var ani_mgr = new AnimationManager();
function onWindowLoad(){

var target = document.getElementById( "target-div" );
ani_mgr.addMovement( target, AnimationManager.RIGHT, 100, 5 );
ani_mgr.addMovement( target, AnimationManager.DOWN, 100, 5 );
ani_mgr.addMovement( target, AnimationManager.LEFT, 100, 5 );
ani_mgr.addMovement( target, AnimationManager.UP, 100, 5 );

}

</script>
<style>
#target-div{

background-color: blue;
color: white;
height: 100px;
width: 100px;
position: absolute;
top: 50px;
left: 50px;
margin: auto auto;

}
</style>
</head>
<body>
<input type="button" onclick="ani_mgr.startAnimation()" value="Start" style="display:inline"/>
<input type="button" onclick="ani_mgr.stopAnimation()" value="Stop" style="display:inline" />
<div id="target-div">Target Div</div>
</body>
</html>

No comments: