Back

DHTML Path Animations

We've done animation before using a single <img> element and then rapidly cycling through a series of pictures stored in an array (remember the baseball?).  What we found was that if we cycled through the images fast enough we could fool our eyes into seeing smooth movement.  The limitation, of course, was that the animation was limited to the bounding rectangle of the <img> element.  In IE or Netscape 6 we could write a script that also changed the height and width of the element but the Netscape 4 DOM doesn't allow that.  We'd still be bound by the rectangle, but at least the rectangle could be dynamic.

With DHTML, we gain further control over the elements on our pages.  We can use DHTML to break out of the bounding rectangle by animating an entire layer.  If we wrap up an <img> element inside a <div> element, we can then manipulate the style properties of the <div>.

A path animation is exactly what you'd expect: an element on the page follows a path that you design.  In DHTML terms, we'll need to access the top and left properties of the layer and then rapidly change their values to achieve the appearance of motion.  It is convenient to store values for each of these properties in separate arrays and then cycle through the values in the array rapidly.

A Simple Example

Let's start off with a simple page containing a single layer.  We'll then write assign some style attributes to the layer and finally write a script that manipulates those style attributes.  Start with something like this:

<html>
<head>
<title>Path Animation Example</title>
</head>
<body bgcolor="#000000">
<div id="bulb">
<img border="0" src="images/lightbulb.gif" width="32" height="64"></div>
</body>
</html>

Now, let's add some style attributes to the <div> element:

<html>
<head>
<title>Path Animation Example</title>
</head>
<body bgcolor="#000000">
<div id="bulb" style="position: absolute; top: 0; left: 0; width: 32px; height: 64px;">
<img border="0" src="images/lightbulb.gif" width="32" height="64"></div>
</body>
</html>

Notice that when the page loads we set the bulb layer up in the top left corner of the screen.  Now, it's time to write our script.  Add a <script> element inside the head and mask the DOM variables:

<script language="JavaScript">
//Mask the DOM
if(document.layers){
pre = 'document.';
post = '';
}
if(document.getElementById){
pre = 'document.getElementById("';
post = '").style';
}
if(document.all){
pre = 'document.all.';
post = '.style';
}
</script>

Now, let's declare our two arrays to hold the coordinates of the top left corner and two counter variables to keep track of our location within the array:

//Arrays
var xPos = new Array(8,8,10,12,20,28,36,54,64,86,108,130,146,168,184,194,200,201);
var yPos = new Array(17,25,33,47,65,81,99,121,139,159,175,189,201,209,215,216,216,216);
//Counters
var currentX = 0;
var currentY = 0;

Finally, let's write the animation function that will actually do all the work:

function animate(){
//Return at end of arrays
if(currentX>xPos.length-1){
currentX = 0;
currentY=0;
return;
}
//Move the bulb
eval(pre + 'bulb' + post).left = parseInt(xPos[currentX]);
eval(pre + 'bulb' + post).top = parseInt(yPos[currentY]);
currentX++;
currentY++;
setTimeout('animate()',10);
}

Notice that we are calling the animate() function recursively - the function calls itself - so the first thing we must do is set up the end of the animation.  The if conditional does just that.  It checks whether our currentX variable is greater than the number of elements in the xPos array.  If this is so, the animation ends and currentX and currentY are then set back to 0.  We then get a handle to the bulb layer using our DOM making variables and the eval() function and set the top and left properties to the coordinates of the currentX-and currentY-elements in our arrays.  Next we increment currentX and currentY to set ourselves up for the next position and then finally call animate() all over again.

In English, what we are doing is mapping the top left corner of our layer to a set of pre-determined coordinates.  The layer is positioned at (8,17) to begin the animation.  The next time through, we move the layer to (8,25), then to (10,33) and so on until we finally reach (201,216).  At that point, currentX equals xPos.length-1 - the number of elements in the xPos Array.  Once we increment currentX, it will trigger the end conditional.  You can view the sample here.

Path animations are good for a number of reasons.  The first, and obvious, reason is that you can move any layer along as complex a path as you'd like.  This means that the elements on your pages are no longer fixed on the screen.  The good news is that all of the major browsers support path animations going back to Netscape and IE 4.  The script above will work in all current 4+ level browsers (*Note - not tested with Opera).

The second reason path animations are good is because they are relatively easy for slower computers to display.  Since the path of the layer is predetermined, the user's computer doesn't have to do any extra calculations to figure out where to display the next frame in the animation.  As we'll see in our next project, other types of animations force the user's computer to work much harder.

OK, I'm sure you've seen the downside of path animations.  You've got to figure out what the coordinates for the path are in order for them to work.  For a complex path this can be a major undertaking if you need to do it by hand.  Well, here's some good news.  I've developed a tool that will calculate array values based on a user's dragging of the mouse.  The tool - unimaginatively named Path Animation Generator - can be found here.  It has been tested with IE 4/5 and Netscape 4.  As of this writing, it doesn't work with Netscape 6 but it soon will.  Check back a little later for an update.

A More Complex Example

Let's see if we can make use of a path animation to do something a little more complex.  In this example, we'll try to get more than one layer to follow a given path using some naming and looping tricks.  The effect we are trying to achieve is a single layer leading a number of other layers along the path so that each trailing layer is one position behind in our position arrays.  Start off with this basic page:

<html>
<head>
<title>complexPath</title>
<style>
.pink{
position: absolute;
top: 10;
height: 32;
width: 32;
}
</style>
</head>
<body>
<div id="pink4" height="32" width="32" class="pink" style="left: 10;">
<img src="images/pink5.gif" width="32" height="32">
</div>
<div id="pink3" height="32" width="32" class="pink" style="left: 10;">
<img src="images/pink4.gif" width="32" height="32">
</div>
<div id="pink2" height="32" width="32" class="pink" style="left: 10;">
<img src="images/pink3.gif" width="32" height="32">
</div>
<div id="pink1" height="32" width="32" class="pink" style="left: 10;">
<img src="images/pink2.gif" width="32" height="32">
</div>
<div id="pink0" height="32" width="32" class="pink" style="left: 10;">
<img src="images/pink1.gif" width="32" height="32">
</div>
</body>
</html>

Notice that each of the layers' ID attribute has the same root name followed by an identifying number.  It's convenient to start the numbering at 0 rather than 1 because we will be accessing the coordinates of the layers in an array (remember, arrays are zero-based!).  Now, let's add a <script> element inside the head and mask the DOM:

//Mask the DOM
if(document.layers){
pre = 'document.';
post = '';
}
if(document.getElementById){
pre = 'document.getElementById("';
post = '").style';
}
if(document.all){
pre = 'document.all.';
post = '.style';
}

We'll now need to create the arrays for the paths of our layers as well as a counter variable to keep track of the position in the array.  To be honest, I just pulled out the Path Animation Generator and created a simple path.  Here's what I ended up with:

//Arrays
var xPos = new Array(16,18,26,32,38,40,46,56,64,70,72,78,84,86,94,100,108,114,122,130,132,138,144,146,154,162,164, 172,180,181,183,185,185,186,188,189,189,190,190,190,191,192,192,192,192,192,192,192,192,192,192, 192,192,192,192,192,192,192,192,192,192,192,192,192,192,192,193,195,197,203,211,213,221,229,235, 243,245,247,255,263,271,279,287,293,301,311,319,325,331,337,343,351,359,367,375,383,391,399,405, 415,421,427,429,435,443,451,453,459,467,475,483,489,497,507,515,521,527,533,539,545,547,553,561, 569,577,585,593,599,601,607,615,617,619,625,627,633,635,637,643,645,651,653,655,657,663,669,677, 685,693,694,694,694,693,693,693,693,693,693,687,686,684,682,676,674,672,672,672,671,670,670,670, 672,673,675,676,682,688,690,692,700,702,708,714,716,724,734,746,760,770,778,786,794,808,816,824, 834,848,860,874,884,890,898,908,914,922,928,936,938,940,942,948,950,952,958,966,968,974,982,990, 998,1000,1002,1010,1016,1018,1020,1022);
var yPos = new Array(16,16,15,15,16,16,15,15,16,16,16,16,16,16,17,17,17,17,17,17,17,17,18,18,19,20,20,26,28,30,31,32, 38,40,46,52,58,64,70,78,80,86,92,100,108,116,122,128,134,144,146,152,154,162,172,178,184,190,196, 204,206,214,220,226,228,230,232,238,239,245,246,248,249,250,251,252,252,253,253,254,254,255,255, 256,257,257,258,258,258,258,258,259,259,259,258,259,259,259,259,259,259,259,259,260,261,261,261, 261,261,262,261,261,261,260,260,260,259,260,260,260,260,260,261,261,261,261,261,261,261,262,262, 264,265,265,266,267,268,268,268,268,269,270,270,269,269,268,268,266,265,259,253,251,245,237,229, 221,215,207,197,189,181,173,163,153,147,145,137,131,129,127,125,125,123,121,119,113,107,105,103, 97,95,93,92,91,91,90,89,88,88,87,87,87,86,85,85,84,84,83,82,82,82,82,82,81,81,81,81,81,81,80,80,80,80, 80,80,80,80,80,80,80,80,79,79,77,77,76,76);
//Frame Number
var counter = 0;

Please note that the coordinates have been broken up into lines that fit on a normal screen.  Be very careful when breaking lines.  JavaScript is supposed to be insensitive to white space but certain editors include linefeeds when you press the return key.  To be safe, always put each line of code on a single line.  Now, let's write the animation code:

function animate(layerName,numLayers){
//Return at end of arrays
if(counter>(xPos.length-1)-numLayers){
return; 
}
//Move it
for(i=0;i<numLayers;i++){
eval(pre + layerName + i + post).left = parseInt(xPos[counter+(numLayers-i)]);
eval(pre + layerName + i + post).top = parseInt(yPos[counter+(numLayers-i)]);
}
counter++;
l = layerName;
n = numLayers;
setTimeout('animate(l,n)',40);
}

First, notice that our animation() function takes two arguments - the root name of the layer and the number of layers.  Then, notice that our end condition is a little bit different as well.  We want to end the animation on the coordinate that represents the number of layers less than the length of the arrays.  In other words, if there are 35 elements in our arrays and 5 layers, we want to stop at the 30th element.  as you'll see, we'll be adding the layer number to our counter variable to keep the layers staggered and we don't want to run past the end of the array.

The next section of the function does the actual work.  First we set up a loop that accesses all of the layers by index.  We then assign the position in the array based on the number of layers minus the layer's index.  This has the effect of putting layer0 at the actual counter's value, layer1 at the previous value, layer2 at the previous, previous value, and so on.  After we position the layers, we increment the counter.  The next two lines are really just placeholder variables that allow us to avoid building a string for the setTimeout() function.  We assign our layerName to l, and our numLayers to n and call animate(l,n) inside the setTimeout() function.  The final step is to attach the animate() function to the <body> element's onLoad event:

<body onLoad="animate('pink',5);">

You can view the code here.

Path animations are fun and relatively simple to write.  They allow developers to move objects around on the screen along complex paths, are cross-browser compatible, and take little or no CPU overhead to run.  The only downside is the fact that the paths must be known ahead of time.  With the Path Animation Generator, you take the drudgery out of the work!