Web DevCenter    
 Published on Web DevCenter (http://www.oreillynet.com/javascript/)
 See this if you're having trouble printing code examples


2419 6????{zmBINs

Cross-Browser Animation

by Dave Thau and Apple Developer Connection
05/24/2002

The fundamental idea behind animation has been around for a long time: when a group of images are presented quickly, the series of images seems to form a single moving picture. One of the first examples of this effect was the thaumatrope, a gizmo created by Paul Roget in 1828. A thaumatrope is a disk with a pole or string attached so the disk can be twirled. Each side of the disk contains an illustration, and twirling the disk merges the two illustrations, making it seem that there's only one image. If the disk has a bird on one side and a cage on the other, twirling the disk gives the illusion that there's just one image: a bird in a cage.

Animation on the web works in a similar way. In the Web's early days animators could only use proprietary systems, like Shockwave, or animated GIF files. JavaScript expanded the animator's repertoire to include rapid GIF swapping. Swapping GIFs with JavaScript makes for quicker downloads than proprietary systems and provides more flexibility than animated GIFs.

Dynamic HTML (DHTML) provides a new range of ways to animate a page. DHTML can animate both text and images and animations can move throughout the browser window, instead of being anchored in one spot. Unfortunately, DHTML can be tricky because of differences between browsers.

This article will cover the basics of cross-browser animation. You'll learn how to animate text and images. Plus you'll see how to move HTML elements around the screen. After you've finished reading this article, you should be able to add cross-browser compatible DHTML animations to your web pages.

Resizing Images

Many web pages make use of an image that resizes with a mouseover event. You can use this technique to make a button jump out or make a small image bigger and easier to see. Instead of using an animated gif, which can have a large file size, or using image swapping, which means preloading images, you can resize an image with DHTML

In IE4.0+ and NN 6.0+, resizing an image is quite simple. Change the height and width properties of the image:

document.my_image.height = "100px";
document.my_image.width = "200px";

Netscape 4.0 doesn't let you change the height and width properties using JavaScript, so instead, it's best to put the image in a DIV and then use JavaScript to rewrite the contents of the DIV:

document.the_div.document.write("<img src='one.jpg' width='400' height='400'>");
document.the_div.document.close();

Related Reading

Designing with JavaScript
Creating Dynamic Web Pages
By Nick Heinle, Bill Pena

The first line writes the contents to the DIV. The second closes the document, which forces the sometimes-reticent Netscape to write your change to the Web page.

Steady Growth

The lines above show you how to change an image from one size to another, but that's not really animation. If you want the image to slowly grow from one size to another, instead of changing in one big jump, you have to use a more animation-like technique.

You want to change the image a small amount, then wait a very short period, then change the image another small amount. To do this in JavaScript, write a function which changes the image a little bit, and then uses a command called setTimeout() to call the function again at some point in the future. When the function is called again at that future time, the image is changed again, and the setTimeout() sets the function to be called yet again at some point in the future. This timed loop animates the image. The quantity of time between calls to the function determines the speed of the animation.

The setTimeout() function works like this:

var my_timeout = setTimeout("someFunction();", some_duration_in_milliseconds);

The setTimeout() function's first parameter determines the name of the function you want called sometime in the future. The second parameter sets the duration you want to wait in milliseconds (there are 1000 milliseconds in a second). So, if you want to call a function 2 seconds in the future, the second parameter should be 2000.

Notice that I've set a variable called my_timeout in the above function. JavaScript will refer to the entire setTimeout() function through this variable. If you want to stop the timeout from happening, you can use the clearTimeout() function and refer to the my_timeout variable:

clearTimeout(the_timeout);

Here's an example of a function that resizes an image and then calls itself using setTimeout():

function growImage()
{
  document.my_image.width = parseInt(document.my_image.width) + 10;
  document.my_image.height = parseInt(document.my_image.height) + 10;

  if (parseInt(document.my_image.width) < 200) 
  {
    my_timeout = setTimeout("growImage();", 100);
  }
}

The first two lines in the function resize the image in IE4+ and NN6+. The if-then statement sets a timeout to call the growImage() function in a tenth of a second as long as the width of the image hasn't exceeded 199 pixels. Note that I used parseInt() when getting the image dimensions. When you read the width of an image, some browsers return something like "50px". This could make for a problematic equation when added to the number 10. The parseInt() function grabs the first integer it sees in the string so you can be sure the image's width and height properties will be set to numeric values.

Take a look at the growing button demo and its source code to see a cross-browser version of image resizing.

Growing Across Browsers

The growing button demo contains some cross-browser compatibility code that's worth studying. Notice that I've put the image inside a DIV:

<div id="justforNS4" style="position:absolute;top:50;left:5;">
<a href = "#" onClick="alert('thanks!');">
<img src="button.gif" name="the_button" border="0"></a>
</div>

I did this because Netscape 4.0 does not allow dynamic resizing of images. In Netscape 4.0, I need to change the image by rewriting the contents of this DIV. Other browsers are easier to work with. You can simply change the height and width properties of the images. Here's the function which makes the image grow on a mouseOver:

function makeBig()
{
  var the_image;

  // if this is NN6 or IE4+
  if (document.documentElement || document.all) 
  {
    the_image = document.the_button;
    the_image.height = the_image.height + 2;
    the_image.width = the_image.width + 2;
  } 
  else if (document.layers)  // NN4
  {

    the_image = document.justforNS4.document.the_button;
    var new_width = the_image.width + 2;
    var new_height = the_image.height + 2;
   
    var write_string string = "<img src='button.gif' border='0' " +
       "width = '" + width + "' height = '" + height + "'>";    

    document.justforNS4.document.writeln(write_string);
    document.justforNS4.document.close();
  }

  if (parseInt(the_image.height < 110)
  {
    setTimeout("makeBig();", 10);
  }
}

First the function checks to see what image-changing method it needs to use: changing the image size for W3C DOM compliant browsers and IE4+, or rewriting the DIV for Netscape 4. Browsers that comply with the W3C DOM (Netscape 6.0+, IE 5+) recognize the document.documentElement property, and all IE4+ browsers recognize the document.all property, so the line

if (document.documentElement || document.all)

will catch all browsers that let us resize the image. If the browser doesn't recognize document.documentElement or document.all, the script checks to see if the browser recognizes document.layers, which is a Netscape 4 property. If so, the code rewrites the contents of the DIV.

After the image size has been changed, the function checks to see if the image has become too big. If its height is less than 110 pixels, the function uses setTimeout() to call itself in 10 milliseconds.

Animating Text

Animating text is much like animating images. But instead of changing the height and width of an image, you'll change the fontsize of the text. In IE5+ or NN6+, you can do this:
var my_div = document.getElementById("myDiv");
my_div.style.fontSize = "36px";

Be careful when using IE5/Mac; this browser won't let you change font style information if the font is inside a positioned element. If you want to animate text on IE5/Mac, you have to make sure the text is not inside a positioned element.

Here's the code for changing the font in IE4:

var my_div = document.all.myDiv;
my_div.style.fontSize = "36px";

and in NN4:

var my_div = window.myDiv;
document.my_div.document.write("<font size = '36px'>This is my text</font>);
document.my_div.document.close();

The dancing letters demo shows this kind of thing in action (IE5+ and NN6+ only). Each letter lives in its own SPAN:

<span>h</span>

The animation goes through each SPAN and changes the fontSize in its style:

var timeout;
function dance()
{    

  var mySpans = document.getElementsByTagName("span");
  var size;
  var loop = 0;

  while (mySpans.item(loop))
  {
    size = Math.floor(Math.random() * 60) + 12;
    mySpans.item(loop).style.fontSize = size;
    loop++;
  }

  timeout = setTimeout("dance();",500);
}

The first three lines in the function declare some variables. The first value gets a list of all the SPANs on the page. Once the variables have been set, the while loop goes through the list of SPANs using the item() method. Each time through, the random() method generates a random number between 12 and 72, and the fontSize of the appropriate SPAN is changed to that number. If the getElementsByTagName() or the item() methods are unfamiliar to you, take a look at the articles on the new DOM.

JavaScript: The Definitive Guide

Related Reading

JavaScript: The Definitive Guide
By David Flanagan

Although this type of thing is relatively straightforward in IE5+ and NN6+, it's quite a bit trickier in NN4. In NN4, you have to put the word inside a DIV and rewrite the contents of the DIV as I did when resizing images. Here's a cross-browser version of the jumpy text resizing.

If you want to see the letters resize a bit more smoothly, look at the smoothly growing letters demo. You can view the source to see how it works.

Moving Pictures

So far you've seen how to animate pictures and text in place. In this section you'll learn how to move things around the screen.

A DIV can be moved by dynamically changing its left and top properties. For example, to move a DIV with an id of "myDiv" you could use the getStyleObject() function introduced in the article on hiding and showing layers to get the stylesheet of the DIV, and then change its left and top properties. One slight complication is that Netscape 4 expects these values to be numbers and will give you an error if you try to introduce strings to these properties. The other browsers expect the numbers to be strings, formatted as a number followed by "px" if the number is in pixels.

Here's an example that moves a DIV from one place to another. If you look at the source of the example, you'll see that clicking on a link triggers the jump() function, which gets the stylesheet of the DIV and moves it a bit to the left and a bit down. The critical lines for moving the DIV are:

var the_style = getStyleObject("myDiv");

var the_left = parseInt(the_style.left) + 100;
var the_top = parseInt(the_style.top) + 100;
if (document.layers)
{
  the_style.left = the_left;
  the_style.top = the_top;
}
else 
{
  the_style.left = the_left + "px";
  the_style.top = the_top + "px";
}

First, the script gets the stylesheet using the getStyleObject() function. Then it calculates the new values of the top and left coordinates of the DIV. Next, it assigns the new values to the left and top property of the DIV. If the browser viewing the page supports document.layers (Netscape 4), the script assigns new numbers to the left and top properties. If the browser is not Netscape 4, the script sticks a "px" at the end of the numbers and assigns that to the left and top properties.

Moving in a Simple Path

To make a DIV move in a smooth line rather than a jump, you should use the same setTimeout() trick I showed in the section on resizing images and letters. Take a look at this demo that smoothly moves a DIV across the screen. As you'll see if you look at the source of this example, the moveDiv() function only moves the DIV a small amount, and then uses setTimeout() to call itself in a short while. This is the essence of animation: many small differences over time simulating smooth movement.

There's a little trick in the moveDiv() function which stops the DIV when it hits a certain point. By placing the setTimeout() call in an if block, you can keep the DIV from moving if it has already gone far enough.

    if (new_left < 400)
    {
      the_timeout = setTimeout('moveDiv();',10);
    }

Or you could have the DIV change direction when it hits a certain point or start over in the same place it began.

Moving in a More Complicated Path

The solution above works when you want your DIV to follow a relatively simple path. In smooth_move.html, the DIV simply moves in a straight line. But if you want your DIV to follow a more complicated path, say a circle, or a more irregular path, you'll need additional code. If you're mathematically inclined, you can use a mathematical formula to determine where your DIV should go at each step. The formula for a line, y=mx+b, is relatively easy to code into your JavaScript. The formula for a circle (y^2 + x^2 = 1) is harder. And, if your path doesn't conform to a known geometric object, coming up with a formula in the first place can be tough.

If you don't feel like cracking open your math texts, you can simply create an array of points that describe the path and have your DIV move from point to point. The more points in the array, the longer or smoother your path will be. The square dance example shows one way to make a DIV move in a square. If you look at the source, you'll see that there's an array containing the points. After all the points have been visited, the code starts back at the beginning of the array. Each element in the array is a string with two numbers separated by a colon, for example, "160:100". These numbers are left and top positions. I used the split function to separate each array element into the two positions.

   var the_points = next_point.split(":");

   var left = the_points[0];
   var top = the_points[1];

The split() function takes a string and splits it into an array based on a delimiter you supply. You don't have to create the array beforehand as split() takes care of that for you. So, in the lines above, the string "160:100" gets split into two parts and loaded into the_points. The first element in the_points, the_points[0], will be 160, and the second element, the_points[1], will be 100.

Here's the function that moves the DIV:

function moveDiv(array_position)
{
  // get the style sheet
  //
  var the_style = getStyleObject('myDiv');
  if (the_style)
  {
    // go to the next point in the array
    //
    array_position = array_position + 1;
    
    // if we've gone past the end of the array, start
    // at position 0
    //
    if (array_position >= the_coords.length) {
      array_position = 0;
    }
    
    // turn the point "120:100" into an array of two
    // elements: the left and the top
    //
    var next_point = the_coords[array_position];
    var the_points = next_point.split(":");
    var left = the_points[0];
    var top = the_points[1];

    // now set the left and top
    // properties appropriately
    //
    if (!document.layers) 
    {
      left = left + "px";
      top = top + "px";
    }
    the_style.left = left;
    the_style.top = top;

    // and call moveDiv() again, with the current array position
    //
    the_timeout = setTimeout('moveDiv(' + array_position + ')',100);
  }
}

The call for the moveDiv() function contains a number that represents where the animation should go next in its path; that is, which element in the array it needs to process next. The link that triggers the animation, starts at the first element of the array, element 0:

<a href="#" onClick="the_timeout=setTimeout('moveDiv(0);',100); return false;">start moving!</a>

The function first gets the stylesheet information for the DIV. Then it adds one to array_position so that it knows to grab the next set of coordinates from the array. If the new array_position is past the end of the path array, array_position is reset to 0.

Once it has the array_position, it grabs the coordinates from the array, uses split() to determine the individual properties, and then sets the top and left properties of the DIV.

Finally, it uses setTimeout() to call the function again. Notice that the last line looks a little weird:

the_timeout = setTimeout('moveDiv(' + array_position + ')',100);

You might think that this would work:

the_timeout = setTimeout('moveDiv(array_position)',100);

Unfortunately, the array_position variable only exists inside the moveDiv() function. Once the function exits, the variable disappears. In 100 milliseconds, when setTimeout() tries to call moveDiv() again, JavaScript will try to look up the value of array_position. By that time that iteration of the moveDiv() function will have ended and JavaScript won't know what that variable is, causing an error.

To overcome this annoyance, avoid passing variables into a function call of a setTimeout() and instead pass the values of the variables.

Smoothing Out Those Complicated Paths

Using the array method shown above can lead to jerky movement. The number of points in your array will determine the jerkiness of the path; the more points, the smoother the movement. But putting lots of points into the array can be a hassle as it may take a long time to calculate each point. A quicker and more elegant solution is to use the points in the array as anchors, and then use setTimeout() to smoothly move between the anchor points. This won't always give exactly the path you want, but often it's good enough. This technique is demonstrated in the busy bee example.

If you take a look at the source of the example you'll see it involves a bit more code than the others. There are two main functions: getAnchors() and moveDiv().

The getAnchors() function looks up the beginning and ending points of each segment of the path and feeds these numbers to the moveDiv() function. The moveDiv() function moves the image using the setTimeout() trick discussed earlier. moveDiv() also checks that the image doesn't move past the ending anchor point. If the image is about to move past the end of the segment, moveDiv() moves the image to the ending anchor point and then calls getAnchors() to get the next pair of anchors.

The getAnchors() function is simple. It takes the current segment of the path as a parameter, and retrieves the appropriate beginning and ending points (the anchors). If the array_position is zero, it looks up the first anchor (the_coords[0]) and the second anchor (the_coords[1]):

var first_anchor = the_coords[array_position];
var second_anchor = the_coords[array_position+1];

Then the function adds one to the array_position variable and makes sure it hasn't reached the end of the array. If it is the end of the array, the function resets the position to 0, which means the animation will start over:

array_position++;
if (array_position == the_coords.length-1)
{
  array_position = 0;
}

After determining the next segment, the function calls moveDiv() to actually move the DIV:

moveDiv(array_position, first_anchor, second_anchor, 0, 0);

The first three variables sent to moveDiv() indicate, respectively, the current array element, the coordinates of the first anchor, and the coordinates of the second anchor. The final two parameters keep track of how far to move the DIV horizontally and vertically. Initially, you won't know what those values are, so you can just put zeros there. It's up to the moveDiv() function to figure out how much to move the DIV each time.

The moveDiv() function is more complicated than the getAnchors() function. First, it gets the stylesheet information using getStyleObject(). The next lines get the left and top coordinates of each of the anchors. Here's the code for the first anchor:

    var first_points = anchor_one.split(":");
    var first_left = parseInt(first_points[0]);
    var first_top = parseInt(first_points[1]);

Here's split() again, dividing the anchor, which looks like "100:120" into two numbers.

After that, moveDiv() calculates the horizontal and vertical step sizes, if they need to be calculated. They only need to be calculated at the beginning of each segment, when moveDiv() is called by getAnchors() with left and top step sizes set to zero. The getStepSize() function returns a number, which determines how much the DIV should move in that direction each time moveDiv() is called. getStepSize() takes three parameters: the coordinates of the two anchors and a number which indicates whether we want to know about the horizontal step size or the vertical step size (0 for horizontal and 1 for vertical):

    if ((horizontal_step_size == 0) && (vertical_step_size == 0))
    {
      horizontal_step_size = 
        getStepSize(anchor_one, anchor_two, 0);

      vertical_step_size = 
        getStepSize(anchor_one, anchor_two, 1);
    }

I'll describe getStepSize() in greater detail soon. Once the step size has been determined, the new left and top coordinates are calculated by adding the step size to the DIV's current location:

    var new_left = first_left + horizontal_step_size;
    var new_top = first_top + vertical_step_size;

Before the DIV is moved you have to make sure you're not overshooting the ending anchor of the segment. This is actually a bit trickier than you might initially guess. I wrote a separate function called atEndOfPath() to figure out whether or not the DIV is about to overshoot the ending anchor. The atEndOfPath() function needs to know the step size, the position of the end of the segment, and where the moveDiv() function wants to move the DIV. Based on these numbers, it returns true if the DIV is about to move past the end of the segment, or false if it's not. If it's about to move past the end of the segment, the DIV should instead move to the end anchor.

    if (atEndOfPath(left_step_size, second_left, new_left) 
       ||(atEndOfPath(top_step_size, second_top, new_top)))
    {
      new_left = second_left;
      new_top = second_top;
    }

Now, all you have to do before moving the DIV is to add "px" at the end of the coordinates, unless the page is being rendered by Netscape Navigator 4:

    if (!document.layers) 
    {
      new_left = new_left + "px";
      new_top = new_top + "px";
    }

And finally, you can move the DIV:

    the_style.left = new_left;
    the_style.top = new_top;

But even after the DIV has moved, there's more work to do. You have to figure out what the script should do next. There are two situations. If the script has reached the end of the segment, it should call getAnchor() to start working on the next segment. If the script hasn't reached the end of the segment, then it should call moveDiv() again in a short time to move the DIV along the segment a bit.

The first part of this is easy:

    if ((parseInt(new_left) == parseInt(second_left)) && 
            (parseInt(new_top) == parseInt(second_top)))
    {
      getAnchors(array_position);
    }

If the DIV is the end of the segment, it calls getAnchors() to get the next set of anchors. The second part is slightly trickier. You have to call moveDiv() again with the correct parameters. The call to moveDiv() has five parameters: how far into the path array we are, the position from which movement starts, the place where it ends, the horizontal step size, and the vertical step size. Something like this:

moveDiv(1, '120:100', '140:100', 10, 0);

The last three lines of the function create the string, and then use setTimeout() to call the function in a hundredth of a second:

   var new_anchor_one = new_left + ":" + new_top;

     var timeout_string = "moveDiv(" +
        array_position + ", '" + new_anchor_one + "', '" +
        anchor_two + "', " + left_step_size + "," + 
        top_step_size + ");";

      the_timeout = setTimeout(timeout_string, 10);

Here's the function which determines whether or not the DIV is about to go past the segment boundary:

function atEndOfPath(the_step_size, second_number, new_number)
{
    var the_end = false;

    if (((the_step_size > 0) && (new_number > second_number)) ||
        ((the_step_size < 0) && (new_number < second_number)))
   {
     the_end = true;
   }

   return the_end;
}

This function takes parameters representing the amount the DIV should move each time, the end of the segment, and the location the DIV is about to move to. There are two conditions that need to be considered. If the_step_size is positive, that means the DIV is moving right or down, so the DIV will have gone too far if the new_number parameter (the next step in the movement) is greater than the second_number. On the other hand, if the_step_size is negative, that means the DIV is moving to the left, or up, so the DIV has gone too far if new_number is less than second_number.

The last function figures out the step size and is pretty simple: it simply gets the relevant coordinate of the first and second point, and divides the distance between them by 10. This means that each segment will be broken up into 10 pieces, and moveDiv() will be called 10 times per segment.

And that is that. As you can see, it's quite a bit more complicated than any of the examples we saw earlier. However, it's a pretty good solution to the general problem of smoothly moving DIVs along complicated paths.

Time to Get Animated

Animation can take on many forms, but they generally follow a similar model. Create one or more images and play them in succession at a speed that makes it seem that one object is moving smoothly. The techniques you've learned here can apply to many effects, from scrolling menus to making superman fly across your screen. It takes a little practice, but once you get the hang of it, the possibilities are limited only by your imagination.

Dave Thau has been creating Internet applications since 1993, starting with bianca.com, the first web-based community on the Internet. Since then he's written The Book of JavaScript, acted as Director of Software Engineering and Senior Scientist at Wired Digital, and taught programming languages to hundreds of artists, engineers and children.


Return to the Web Development DevCenter.

Copyright © 2009 O'Reilly Media, Inc.