Chapter 5
Animation

Are you ready for your journey into the … fourth dimension!? (Cue spooky theramin music.)

Animation is all about time—stopping time, starting time, and bending time to produce the effect you want: movement. You don’t actually have to learn much new JavaScript in order to create web page animation—just two little functions. One and a half, really. What are they? You’ll have to read on to find out!

But first, let’s take a look at exactly what animation is, and how we can best go about producing it on a computer screen.

The Principles of Animation

If, like me, you’re a connoisseur of Saturday morning cartoons, you’ll probably be familiar with the basic principles of animation. As illustrated in Figure 5.1, what we see on the TV screen isn’t actually Astroboy jetting around, but a series of images played rapidly, with minute changes between each one, producing the illusion that Astroboy can move.

Standard TV displays between 25 and 30 pictures (or frames) per second, but we don’t perceive each picture individually—we see them as one continuous, moving image. Animation differs from live TV only in the way it is produced. The actual display techniques are identical—a series of rapidly displayed images that fools our brains into seeing continuous movement.

Creating the illusion of movement using a series of progressively changing images

Figure 5.1. Creating the illusion of movement using a series of progressively changing images

Adding animation to your interfaces is no different from animating something for TV, although it may seem somewhat less artistic. If you want a drop-down menu to actually drop down, you start it closed, end it open, and include a whole bunch of intermediate steps that make it look like it moved.

The faster you want to move it, the fewer steps you put in between the starting and finishing frames. As it is, a drop-down menu without animation is akin to teleportation—the menu moves directly from point A to point B, without any steps in between. If you add just one step, it’s not quite teleportation, but it’s still faster than you can blink. Add ten steps and the menu begins to look like it’s moving smoothly. Add 50 and it’s probably behaving a bit too much like a tortoise. It’s really up to you to find the optimal intervals that give you that nice, pleasing feeling of the menu opening.

Controlling Time with JavaScript

The most intuitive way of thinking about animations is as slices of time. We’re capturing and displaying discrete moments in time so that they look like one continuous experience. But, given the way we handle animation in JavaScript, it’s probably more accurate to think of ourselves as inserting the pauses—putting gaps between the slices, as is illustrated in Figure 5.2.

An animation running at 25 frames per second, in which the gap between each “slice” of time is 40 milliseconds

Figure 5.2. An animation running at 25 frames per second, in which the gap between each “slice” of time is 40 milliseconds

In normal program flow a browser will execute JavaScript as quickly as it can, pumping instructions to the CPU whenever they can be processed. This means that your entire, 5,000-line program could execute faster than you can take a breath. That’s great if you’re performing long calculations, but it’s useless if you’re creating animation. Watching Snow White and the Seven Dwarfs in 7.3 seconds might be efficient, but it doesn’t do much for the storyline.

To create smooth, believable animation we need to be able to control when each frame is displayed. In traditional animation, the frame rate (that is, the number of images displayed per second) is fixed. Decisions about how you slice the intervals, where you slice them, and what you slice, are all up to you, but they’re displayed at the same, measured pace all the time.

In JavaScript, you can create the same slices and control the same aspects, but you’re also given control of the pauses—how long it takes for one image to be displayed after another. You can vary the pauses however you like, slowing time down, speeding it up, or stopping it entirely (though most of the time you’ll want to keep it steady). We control time in JavaScript using setTimeout.

Instead of allowing your program to execute each statement as quickly as it can, setTimeout tells your program to wait awhile—to take a breather. The function takes two arguments: the statement you want it to run next, and the time that the program should wait before executing that statement (in milliseconds).

In order to stop the statement inside the setTimeout call from being evaluated immediately, we must make it into a string. So if we wanted to display an alert after one second, the setTimeout call would look like this:

setTimeout("alert('Was it worth the wait?')", 1000);

From the moment this statement is executed, our program will wait 1000 milliseconds (one second) before it executes the code inside the string, and produces the popup shown in Figure 5.3.

Using setTimeout to execute code after a specified number of milliseconds have elapsed

Figure 5.3. Using setTimeout to execute code after a specified number of milliseconds have elapsed

Technically speaking, what happens when setTimeout is called is that the browser adds the statement to a “to-do list” of sorts, while the rest of your program continues to run without pausing. Once it finishes what it’s currently doing (for instance, executing an event listener), the browser consults its to-do list and executes any tasks it finds scheduled there.

Usually, when you call a function from inside another function, the execution of the outer function halts until the inner function has finished executing. So a function like the one shown here actually executes as depicted in Figure 5.4:

function fabric()
{
  …
  alert("Stop that man!");
  …
}
Calling a function inside another function

Figure 5.4. Calling a function inside another function

But what happens if you have a setTimeout call in the middle of your code?

function fabric()
{
  …
  setTimeout("alert('Was it worth the wait?'),", 1000);
  …
}

In this case, the execution of the statement passed to setTimeout is deferred, while the rest of fabric executes immediately. The scheduled task waits until the specified time has elapsed, and the browser has finished what it’s doing, before executing. Figure 5.5 illustrates this concept.

Calling setTimeout schedules a task to be run after a delay

Figure 5.5. Calling setTimeout schedules a task to be run after a delay

Strictly speaking, the delay that you specify when calling setTimeout will be the minimum amount of time that must elapse before the scheduled task will occur. If the browser is still tied up doing other things when that time arrives, your task will be put off until all others have been completed, as Figure 5.6 indicates.[26]

setTimeout waits until JavaScript is free before running the newly specified task

Figure 5.6. setTimeout waits until JavaScript is free before running the newly specified task

Using Variables with setTimeout

Statements passed to setTimeout are run in global scope. We talked about scope in Chapter 2, so if you’d like to refresh your memory, flick back to that chapter. The effect that scope has here is that the code run by setTimeout will not have access to any variables that are local to the function from which setTimeout was called.

Take a look at this example:

function periscope()
{
  var message = "Prepare to surface!";

  setTimeout("alert(message);", 2000);
}

Here, the variable message is local to periscope, but the code passed to the setTimeout will be run with global scope, entirely separately from periscope. Therefore, it won’t have access to message when it runs, and we’ll receive the error shown in Figure 5.7.

The error that occurs when we try to use a locally scoped variable as part of setTimeout code

Figure 5.7. The error that occurs when we try to use a locally scoped variable as part of setTimeout code

There are three ways around this problem.

The first is to make message a global variable. This isn’t that great an idea, because it’s not good practice to have variables floating around, cluttering up the global namespace, and causing possible clashes with other scripts. But if we were to go down this path, we could just leave the var keyword off our variable declaration:

function periscope()
{
  message = "Prepare to surface!";

  setTimeout("alert(message)", 2000);
}

message would be a global variable that’s accessible from any function, including the code passed to setTimeout.

The second option is available when the variable we’re using is a string. In this case, we can encode the value of the variable directly into the setTimeout code simply by concatenating the variable into the string:

function periscope()
{
  message = "Prepare to surface!";

  setTimeout("alert('" + message + "')", 2000);
}

Since we want the string to be interpreted as a string—not a variable name—we have to include single quote marks inside the setTimeout code string, and in between those marks, insert the value of the variable. Though the above code is equivalent to the code below, we have the advantage of being able to use dynamically assigned text with the variable:

setTimeout("alert('Prepare to surface!')", 2000);

Warning: Troublesome Quotes

Of course, if the variable’s string value happens to contain one or more single quotes, this approach will fall to pieces unless you go to the trouble of escaping each of the single quotes with a backslash. And while that is certainly possible, the code involved isn’t much fun.

The final option, and the one that’s used more regularly in complex scripts, is to use a closure. A closure is a tricky JavaScript concept that allows any function that’s defined inside another function to access the outer function’s local variables, regardless of when it’s run. Put another way, functions have access to the context in which they’re defined, even if that context is a function that’s no longer executing. So, even though the outer function may have finished executing minutes ago, functions that were declared while it was executing will still be able to access its local variables.

How does this help us given the fact that setTimeout takes a string as its first argument? Well, it can also take a function:

function periscope()
{
  var message = "Prepare to surface!";

  var theFunction = function()
  {
    alert(message);
  };

  setTimeout(theFunction, 2000);
}

Or, more briefly:

function periscope()
{
  var message = "Prepare to surface!";

  setTimeout(function(){alert(message);}, 2000);
}

When you pass a function to setTimeout, setTimeout doesn’t execute it then and there; it executes the function after the specified delay. Since that function was declared within periscope, the function creates a closure, giving it access to periscope’s local variables (in this case, message).

Even though periscope will have finished executing when the inner function is run in two seconds’ time, message will still be available, and the call to alert will bring up the right message.

The concept of closures is difficult to get your head around, so for the moment, you can be satisfied if you’ve just got a general feel for them.

Stopping the Timer

When setTimeout is executed, it creates a timer that “counts down” to the moment when the specified code should be executed. The setTimeout function returns the ID of this timer so that you can access it later in your script. So far, we’ve been calling setTimeout without bothering about its return value, but if you ever want to intervene in a timer’s countdown, you have to pay attention to this value.

To stop a timer before its countdown has finished, we need to capture the timer’s ID inside a variable and pass it to a function called clearTimeout. clearTimeout will immediately cancel the countdown of the associated timer, and the scheduled task will never occur:

var timer = setTimeout("alert('This will never appear')", 3000);
clearTimeout(timer);

The alert in the code above will never be displayed, because we stop the setTimeout call immediately using clearTimeout.

Let’s consider something that’s a little more useful. Suppose we create a page that displays two buttons:

clear_timeout.html (excerpt)
<button id="start">Start</button>
<button id="stop">Stop</button>

We can add some behavior to those buttons with a short program:

clear_timeout.js
var ClearTimer =
{
  init: function()
  {
    var start = document.getElementById("start");
    Core.addEventListener(start, "click", ClearTimer.clickStart);

    var stop = document.getElementById("stop");
    Core.addEventListener(stop, "click", ClearTimer.clickStop);
  },
  clickStart: function()
  {
    ClearTimer.timer = setTimeout("alert('Launched')", 2000);
  },
  clickStop: function()
  {
    if (ClearTimer.timer)
    {
      clearTimeout(ClearTimer.timer);
    }

    alert("Aborted");
  }
};

Core.start(ClearTimer);

This program begins by running ClearTimer.init via Core.start, which initializes the page by adding a click event listener to each of the buttons. When the Start button is clicked, ClearTimer.clickStart will be called, and when the Stop button is clicked, ClearTimer.clickStop will be called.

ClearTimer.clickStart uses setTimeout to schedule an alert, but we store the ID to that scheduled alert inside the variable ClearTimer.timer. Whenever ClearTimer.clickStop is pressed we pass ClearTimer.timer to clearTimeout, and the timer will be stopped.

So, once you click Start you will have two seconds to click Stop; otherwise, the alert dialog will appear, displaying the message “Launched.” This simple example provides a good illustration of what’s involved in controlling timers via user interaction; we’ll look at how it can be used in a more complex interface later in this chapter.

Creating a Repeating Timer

There’s another timing function that’s very similar to setTimeout, but which allows you to schedule a repeating piece of code. This function is called setInterval.

setInterval takes exactly the same arguments as setTimeout, and when it’s executed, it waits in exactly the same way. But the fun doesn’t stop once the scheduled code has finished executing—setInterval schedules the code again, and again, and again, and again, until you tell it to stop. If you tell it to wait 1,000 milliseconds, setInterval will schedule the code to run once every 1,000 milliseconds.

Each interval is scheduled as a separate task, so if a particular occurrence of the scheduled code takes longer to run than the interval time, the next occurrence will execute immediately after it finishes, as the browser scrambles to catch up.

At the start of this chapter I mentioned that there were really one and a half functions that could be used to control time in JavaScript. That’s because setInterval isn’t used very often.

As setInterval relentlessly schedules tasks at the specified interval no matter how long those tasks actually take, long-running tasks can generate an undesirable backlog that causes fast-running tasks to run instantly, with no pause between them. In an animation, this can be disastrous, as the pause between each frame is crucial to generating that illusion of motion.

When it comes to animation, it’s usually best to rely on chained setTimeout calls, including at the end of your scheduled code a setTimeout call to that same piece of code. This approach ensures that you only have one task scheduled at any one time, and that the crucial pauses between frames are maintained throughout the animation. You’ll see examples of this technique in each of the animation programs we write in this chapter.

Stopping setInterval

Just like setTimeout, setInterval returns a timer ID. To stop setInterval from executing any more scheduled code, we pass this ID to clearInterval:

var timer = setInterval("alert('Do this over and over!')", 3000);
clearInterval(timer);

Done! No more timer.

Revisiting Rich Tooltips

It’s probably easiest to get a feel for working with setTimeout if we start simply—using it to put in a short delay on an action. I’ll demonstrate this by modifying the Rich Tooltips script from Chapter 4, so that the tooltips behave more like regular tooltips and appear after the user has hovered the mouse over the element for a moment. We won’t need to change much of the program—just the two event listeners that handle the mouseover/focus and mouseout/blur events.

Let’s think about how the delay should work. We want the tooltip to pop up about 500 milliseconds after the mouseover or focus event occurs. In our original script, the event listener showTipListener immediately made a call to Tooltips.showTip. But in the delayed script, we want to move that call into a setTimeout:

tooltips_delay.js (excerpt)
showTipListener: function(event)
{
  var link = this;
  this._timer = setTimeout(function()
      {
        Tooltips.showTip(link);
      }, 500);
  Core.preventDefault(event);
},

In order to pass showTip a reference to the link in question, we need to store that reference in a local variable called link. We can then make use of a closure by passing setTimeout a newly created function. That new function will have access to showTipListener’s local variables, and when it’s run, it will be able to pass the link to showTip.

The other point to note about this setTimeout call is that we assign the ID it returns to a custom property of the anchor element, called _timer. We store this value so that if a user mouses off the anchor before the tooltip has appeared, we can stop the showTip call from going ahead. To cancel the method call, we update hideTipListener with a clearTimeout call:

tooltip_delay.js (excerpt)
hideTipListener: function(event)
{
  clearTimeout(this._timer);
  Tooltips.hideTip(this);
}

The ID of the setTimeout call is passed to clearTimeout every time that hideTipListener is executed, and this will prevent premature “tooltipification.”

And that’s it: our Rich Tooltips script has now been modified into a moderately less annoying Rich Delayed Tooltips script.

Old-school Animation in a New-school Style

To demonstrate how to create a multi-stepped animation sequence in JavaScript we’re going to do something novel, though you probably wouldn’t implement it on a real web page. We’re going to re-create a film reel in HTML.

First of all, we need a strip of film—or a graphic that looks almost like a real piece of film. As you can see in Figure 5.8, our reel contains a series of progressively changing images that are joined together in one long strip.

The “reel” we’ll use to create animation

Figure 5.8. The “reel” we’ll use to create animation

If you had a real reel, you’d pass it through a projector, and the frames would be projected individually onto the screen, one after the other. For this virtual film reel, our projector will be an empty div:

robot_animation.html (excerpt)
<div id="robot"></div>

The div will be styled to the exact dimensions of a frame (150x150px), so that we can show exactly one frame inside it:

robot_animation.css (excerpt)
#robot {
  width: 150px;
  height: 150px;

If we use the strip of robots as a background image, we can change the background-position CSS property to move the image around and display different parts of the strip:

robot_animation.css (excerpt)
#robot {
  width: 150px;
  height: 150px;
  background-image:
    url(../images/robot_strip.gif);
  background-repeat: no-repeat;
  background-position: 0 0;
}

Getting the idea now? You can think of the div as a little window inside which we’re moving the strip around, as Figure 5.9 illustrates.

Changing the position of the background image to specify which frame is in view film strip (in HTML) changing position of background image to specify which frame is in view

Figure 5.9. Changing the position of the background image to specify which frame is in view

So, we know how to make any one frame appear inside the div window, but the page is still static—there’s no animation. That’s what that ol’ JavaScript magic is for!

In order to create a fluid animation, we’re going to need to change the background-position at a regular interval, flicking through the frames so that they look like one fluid, moving image. With all the talk of setTimeout in this chapter, you could probably take a guess that the function is the key to it all. And you’d be wrong. Sorry, right:

robot_animation.js
var Robot =
{
  init: function()
  {
    Robot.div = document.getElementById("robot");(1)
    Robot.frameHeight = 150;
    Robot.frames = 10;
    Robot.offsetY = 0;(2)

    Robot.animate();(3)
  },
  animate: function()(4)
  {
    Robot.offsetY -= Robot.frameHeight;(5)

    if (Robot.offsetY <= -Robot.frameHeight * Robot.frames)(6)
    {
      Robot.offsetY = 0;
    }

    Robot.div.style.backgroundPosition =
        "0 " + Robot.offsetY + "px";(7)

    setTimeout(Robot.animate, 75);(8)
  }
};

Core.start(Robot);

The Robot object contains two methods. Robot.init is included mainly to declare some variables and kick-start the animation. I include the variables here, rather than in Robot.animate, because Robot.animate will be called a lot, and it’s more efficient to declare the variables just once than to have them declared for each frame of our animation.

Here are the highlights of this script:

(1)

The initialized variables include a reference to the div element we’re modifying, as well as two constant properties— frameHeight and frames—which tell the program that each frame is 150 pixels high, and that there are ten frames in total. These values will remain fixed throughout an animation, but by declaring them up front, you’ll find it easier to modify the frame dimensions and the number of frames later if you need to. If you create your own animation, you should modify these variables according to your needs.

(2)

The other variable, Robot.offsetY, is the only variable we’ll be updating dynamically. It’s set to 0 initially, but it’s used to record the current vertical position of the animation image, just so we know which frame we’re up to.

(3)

After the init method does its thing, we step into our first iteration of Robot.animate.

(4)

If you look to the end of the function, you’ll see that the function calls itself with a setTimeout delay. This lets you know that we’re setting up a timed, repeating task.

(5)

The first statement in the function reduces Robot.offsetY by the height of one frame. What we’re calculating here is the vertical background-position required to view the next frame. However, our animation graphic isn’t infinitely long; if we just kept reducing the value of Robot.offsetY, we’d eventually run out of frames and end up with an empty box. To avoid this, we need to return to the beginning of the strip at the right moment.

(6)

Next up is an if statement that checks whether the new Robot.offsetY value is outside the range of our animation strip. If it is, we set it back to 0.

(7)

With our new offset value in hand, we’re ready to update the position of the image. To do so, we modify the div’s style.backgroundPosition property, using Robot.offsetY for the vertical coordinate and 0 for the horizontal coordinate (because we don’t need to move the image horizontally at all).

Tip: Units Required

Remember to include units at the end of all non-zero CSS length values, otherwise your changes will be ignored. We’re using pixels in this case, so we add px to the end of the value.

(8)

Changing the style of the div commits the new frame to the browser for the user to see. Once that’s done, we have to schedule the next change using setTimeout. The wait until the next frame change is 75 milliseconds, which will produce a frame rate of approximately 13 frames per second. You might want to tweak this rate depending on how fast or smooth you want your animation to be. The most pleasing results are often reached by trial and error.

Once the program has made a few passes through Robot.animate, you have yourself an animation. Since there’s nothing in our code to stop the repeated calls to that method, the animation will keep going forever. If you wanted to, you could provide start and stop buttons as we did previously in this chapter to allow users to stop and restart the animation. Alternatively, you could limit the number of cycles that the animation goes through by including a counter variable that’s incremented each time Robot.offsetY returns to 0.

Path-based Motion

The JavaScript techniques we used to animate our digital film reel can be applied whenever you want to make a series of progressive changes to the display over time. All you have to do is replace background-position with the attribute you want to modify: color, width, height, position, opacity—anything you can think of.

In our next example, we’re going to look at moving an HTML element along a linear path. So, instead of changing the background-position of the target element, we change its actual position.

In cases where you want to move an element, you usually know where it is now (that is, its starting location), and where you want it to be (its end location), as Figure 5.10 shows.

Visualizing a path from the object’s starting location to the point where you want it to end up

Figure 5.10. Visualizing a path from the object’s starting location to the point where you want it to end up

Once you’ve defined the two end points of the path, it’s the job of the animation program to figure out all the steps that must occur to let the animated element move smoothly from point A to point B, as shown in Figure 5.11.

Calculating the steps required to move an element from point A to point B linear path (animation) steps required to move from point A to point B elements (HTML) steps required to move an element from point A to point B

Figure 5.11. Calculating the steps required to move an element from point A to point B

Given that the movement is going to be automated by an animation function, all we really have to identify at the moment is:

  • the element we want to move

  • where we want to move it to

  • how long we want it to take to reach its destination

We’ll use the soccer ball and grassy background as the basis for a working example. The HTML for this document is fairly simple:

path-based_motion.html (excerpt)
<div id="grass">
  <div id="soccerBall"></div>
</div>

There are a number of ways we could position the soccer ball, but for this example I’ve chosen to use absolute positioning:

path-based_motion.css (excerpt)
#soccerBall {
  background-image: url(soccer_ball.png);
  background-repeat: no-repeat;
  height: 125px;
  left: 0;
  margin-top: 25px;
  position: absolute;
  top: 75px;
  width: 125px;
}

Note: Positioning and Animation

If you’re going to animate an element’s movement, the element will need to be positioned relatively or absolutely; otherwise, changing its left and top properties won’t have any effect.

The JavaScript we used to animate our previous film reel example makes a fairly good template for any animation, so we’ll use its structure to help us define the movement we need in this new animation. Inside init, we declare some of the variables we’ll need, then start the actual animation:

path-based_motion.js (excerpt)
var SoccerBall =
{
  init: function()
  {
    SoccerBall.frameRate = 25;(1)
    SoccerBall.duration = 2;
    SoccerBall.div = document.getElementById("soccerBall");(2)
    SoccerBall.targetX = 600;(3)
    SoccerBall.originX = parseInt(
        Core.getComputedStyle(SoccerBall.div, "left"), 10);(4)
    SoccerBall.increment =
        (SoccerBall.targetX - SoccerBall.originX) /
        (SoccerBall.duration * SoccerBall.frameRate);(5)
    SoccerBall.x = SoccerBall.originX;(6)

    SoccerBall.animate();(7)
  },
  …
};

(1)

The first two variables control the speed of the animation. SoccerBall.frameRate specifies the number of frames per second at which we want the animation to move. This property is used when we set the delay time for the setTimeout call, and determines the “smoothness” of the soccer ball’s movement. SoccerBall.duration determines how long the animation should take to complete (in seconds) and affects the speed with which the ball appears to move.

(2)

SoccerBall.div is self-explanatory.

(3)

SoccerBall.targetX specifies the location to which we’re moving the soccer ball. Once it reaches this point, the animation should stop.

(4)

In order to support an arbitrary starting position for the soccer ball, SoccerBall.originX is actually calculated from the browser’s computed style for the div.

The computed style of an element is its style information after all CSS rules and inline styles have been applied to it. So, if an element’s left position has been specified inside an external style sheet, obtaining the element’s computed style will let you access that property value.

Unfortunately, there are differences between the way that Internet Explorer implements computed style and the way that other browsers implement it. Internet Explorer exposes a currentStyle property on every element. This property has exactly the same properties as style, so you could ascertain an element’s computed left position using element.currentStyle.left. Other browsers require you to retrieve a computed style object using the method document.defaultView.getComputedStyle. This method takes two arguments—the first is the element you require the styles for; the second must always be null—then returns a style object that has the same structure as Internet Explorer’s currentStyle property.

To get around these browser differences, we’ll create a new Core library function that will allow us to get a particular computed style property from an element:

core.js (excerpt)
Core.getComputedStyle = function(element, styleProperty)
{
  var computedStyle = null;

  if (typeof element.currentStyle != "undefined")
  {
    computedStyle = element.currentStyle;
  }
  else
  {
    computedStyle =
        document.defaultView.getComputedStyle(element, null);
  }

  return computedStyle[styleProperty];
};

Core.getComputedStyle does a little object detection to check which way we should retrieve the computed style, then passes back the value for the appropriate property. Using this method, we can get the correct starting point for SoccerBall.originX without requiring a hard-coded value in our script.

Let’s return to our init method above:

(5)

With SoccerBall.originX and SoccerBall.targetX in hand, we can calculate the increment by which we’ll need to move the soccer ball in each frame so that it ends up at the target location within the duration specified for the animation. This is a simple matter of finding the distance to be traveled (SoccerBall.targetX – SoccerBall.originX) and dividing it by the total number of frames in the animation (SoccerBall.duration * SoccerBall.frameRate). Calculating this figure during initialization—rather than inside the actual animating function—reduces the number of calculations that we’ll have to complete for each step of the animation.

(6)

The last variable we declare is SoccerBall.x. It acts similarly to Robot.offsetY, keeping track of the horizontal position of the element. It would be possible to keep track of the soccer ball’s position using its actual style.left value, however, that value has to be an integer, whereas SoccerBall.x can be a floating point number. This approach produces more accurate calculations and smoother animation.

(7)

After SoccerBall.init has finished declaring all the object properties, we start the animation by calling SoccerBall.animate.

Here’s the code for this method:

path-based_motion.js (excerpt)
animate: function()
{
  SoccerBall.x += SoccerBall.increment;

  if ((SoccerBall.targetX > SoccerBall.originX &&
      SoccerBall.x > SoccerBall.targetX) ||
      (SoccerBall.targetX <= SoccerBall.originX &&
      SoccerBall.x <= SoccerBall.targetX))
  {
    SoccerBall.x = SoccerBall.targetX;
  }
  else
  {
    setTimeout(SoccerBall.animate, 1000 / SoccerBall.frameRate)
  }

  SoccerBall.div.style.left = Math.round(SoccerBall.x) + "px";
}

The similarities between this animate method and Robot.animate, which we saw in the previous example, are striking. The process for both is basically:

  1. Calculate the new position.

  2. Check whether the new position exceeds the limit.

  3. Apply the new position to the element.

  4. Repeat the process with a delay.

In this case, we’re calculating a new position by adding to the current position one “slice” of the total distance to be traveled:

path-based_motion.js (excerpt)
SoccerBall.x += SoccerBall.increment;

Then, we check whether that new position goes beyond the end point of the animation:

path-based_motion.js (excerpt)
if ((SoccerBall.targetX > SoccerBall.originX &&
    SoccerBall.x >= SoccerBall.targetX) ||
    (SoccerBall.targetX < SoccerBall.originX &&
    SoccerBall.x <= SoccerBall.targetX))

That condition looks a little daunting, but we can break it down into smaller chunks. There are actually two possible states represented here, separated by an OR operator.

The first of these states (SoccerBall.targetX > SoccerBall.originX && SoccerBall.x >= SoccerBall.targetX) checks whether SoccerBall.targetX is greater than SoccerBall.originX. If it is, we know that the soccer ball is moving to the right. If that’s the case, the soccer ball will be beyond the end point if SoccerBall.x is greater than SoccerBall.targetX.

In the second state (SoccerBall.targetX < SoccerBall.originX && SoccerBall.x <= SoccerBall.targetX), if SoccerBall.targetX is less than SoccerBall.originX, the soccer ball will be moving to the left, and it will be beyond the end point if SoccerBall.x is less than SoccerBall.targetX. By including these two separate states inside the condition, we allow the soccer ball to move in any direction without having to modify the code.

If the newly calculated position for the soccer ball exceeds the end point, we automatically set the new position to be the end point:

path-based_motion.js (excerpt)
if ((SoccerBall.targetX > SoccerBall.originX &&
    SoccerBall.x >= SoccerBall.targetX) ||
    (SoccerBall.targetX < SoccerBall.originX &&
    SoccerBall.x <= SoccerBall.targetX))
{
  SoccerBall.x = SoccerBall.targetX;
}

Otherwise, the soccer ball needs to keep moving, so we schedule another animation frame:

path-based_motion.js (excerpt)
else
{
  setTimeout(SoccerBall.animate, 1000 / SoccerBall.frameRate)
}

Notice that the delay for the setTimeout is specified as 1000 milliseconds (one second) divided by the specified frame rate. This calculation transforms the frame rate into the millisecond format that’s required for the delay.

The last thing we need to do for each frame is apply the newly calculated position to the style.left property of the soccer ball. This step causes the browser to display the update:

path-based_motion.js (excerpt)
SoccerBall.div.style.left = Math.round(SoccerBall.x) + "px";

That statement converts SoccerBall.x to an integer using Math.round, which rounds any number up or down to the nearest integer. We need to do this because SoccerBall.x might be a floating point number, and CSS pixel values can’t be decimals.

The last two statements (the setTimeout and the style change) would normally be written in the reverse of the order shown here, but the setTimeout is placed inside the else statement for code efficiency. If we were to place it after the style assignment, setTimeout would require an additional conditional check. By doing it this way, we can avoid adversely affecting the performance of our script.

Once we assemble both the init and animate functions inside the one object, which we initialize using Core.start, we have our finished program. We’re all set to roll that soccer ball over a lush, green field:

path-based_motion.js
var SoccerBall =
{
  init: function()
  {
    SoccerBall.frameRate = 25;
    SoccerBall.duration = 2;
    SoccerBall.div = document.getElementById("soccerBall");
    SoccerBall.targetX = 600;
    SoccerBall.originX = parseInt(
        Core.getComputedStyle(SoccerBall.div, "left"), 10);
    SoccerBall.increment =
        (SoccerBall.targetX - SoccerBall.originX) /
        (SoccerBall.duration * SoccerBall.frameRate);
    SoccerBall.x = SoccerBall.originX;

    SoccerBall.animate();
  },

  animate: function()
  {
    SoccerBall.x += SoccerBall.increment;

    if ((SoccerBall.targetX > SoccerBall.originX &&
        SoccerBall.x >= SoccerBall.targetX) ||
        (SoccerBall.targetX < SoccerBall.originX &&
        SoccerBall.x <= SoccerBall.targetX))
    {
      SoccerBall.x = SoccerBall.targetX;
    }
    else
    {
      setTimeout(SoccerBall.animate, 1000 / SoccerBall.frameRate)
    }

    SoccerBall.div.style.left = Math.round(SoccerBall.x) + "px";
  }
};

Core.start(SoccerBall);

Animating in Two Dimensions

The example above only animated the soccer ball horizontally, but it’s quite easy to modify our program to deal with vertical movement as well:

path-based_motion2.js
var SoccerBall =
{
  init: function()
  {
    SoccerBall.frameRate = 25;
    SoccerBall.duration = 2;
    SoccerBall.div = document.getElementById("soccerBall");
    SoccerBall.targetX = 600;
    SoccerBall.targetY = 150;
    SoccerBall.originX = parseInt(
        Core.getComputedStyle(SoccerBall.div, "left"), 10);
    SoccerBall.originY = parseInt(
        Core.getComputedStyle(SoccerBall.div, "top"), 10);
    SoccerBall.incrementX =
        (SoccerBall.targetX - SoccerBall.originX) /
        (SoccerBall.duration * SoccerBall.frameRate);
    SoccerBall.incrementY =
        (SoccerBall.targetY - SoccerBall.originY) /
        (SoccerBall.duration * SoccerBall.frameRate);
    SoccerBall.x = SoccerBall.originX;
    SoccerBall.y = SoccerBall.originY;

    SoccerBall.animate();
  },
  animate: function()
  {
    SoccerBall.x += SoccerBall.incrementX;
    SoccerBall.y += SoccerBall.incrementY;

    if ((SoccerBall.targetX > SoccerBall.originX &&
        SoccerBall.x >= SoccerBall.targetX) ||
        (SoccerBall.targetX < SoccerBall.originX &&
        SoccerBall.x <= SoccerBall.targetX))
    {
      SoccerBall.x = SoccerBall.targetX;
      SoccerBall.y = SoccerBall.targetY;
    }
    else
    {
      setTimeout(SoccerBall.animate, 1000 / SoccerBall.frameRate)
    }

    SoccerBall.div.style.left = Math.round(SoccerBall.x) + "px";
    SoccerBall.div.style.top = Math.round(SoccerBall.y) + "px";
  }
};

Core.start(SoccerBall);

For every instance in which we performed a calculation with the x coordinate, we add an equivalent statement for the y coordinate. So, SoccerBall.init ends up with a vertical end-point in SoccerBall.targetY, a vertical origin in SoccerBall.originY, and a vertical position tracker in SoccerBall.y. These variables are used by SoccerBall.animate to increment the vertical position and write it to the style.top property.

The only other change we need to make is a small tweak to the starting position of the ball:

path-based_motion2.css (excerpt)
#soccerBall {
  …
  top: 0;
  …
}

Once that’s done, we’ve got a ball that moves in both dimensions, as Figure 5.12 illustrates.

Adding vertical movement to the animation

Figure 5.12. Adding vertical movement to the animation

Creating Realistic Movement

The animation that we’ve created so far treats the movement of the soccer ball homogeneously—each frame moves the ball by the same amount, and the ball stops without any deceleration. But this isn’t how objects move in the real world: they speed up, they slow down, they bounce. It’s possible for us to mimic this type of behavior by using different algorithms to calculate the movement of our soccer ball.

If we wanted to get our soccer ball to slow to a halt, there are just a few minor tweaks we’d have to make to our program:

path-based_motion3.js
var SoccerBall =
{
  init: function()
  {
    SoccerBall.frameRate = 25;
    SoccerBall.deceleration = 10;
    SoccerBall.div = document.getElementById("soccerBall");
    SoccerBall.targetX = 600;
    SoccerBall.targetY = 150;
    SoccerBall.originX = parseInt(
        Core.getComputedStyle(SoccerBall.div, "left"), 10);
    SoccerBall.originY = parseInt(
        Core.getComputedStyle(SoccerBall.div, "top"), 10);
    SoccerBall.x = SoccerBall.originX;
    SoccerBall.y = SoccerBall.originY;

    SoccerBall.animate();
  },

  animate: function()
  {
    SoccerBall.x += (SoccerBall.targetX - SoccerBall.x) /
        SoccerBall.deceleration;
    SoccerBall.y += (SoccerBall.targetY - SoccerBall.y) /
        SoccerBall.deceleration;

    if ((SoccerBall.targetX > SoccerBall.originX &&
        Math.round(SoccerBall.x) >= SoccerBall.targetX) ||
        (SoccerBall.targetX < SoccerBall.originX &&
        Math.round(SoccerBall.x) <= SoccerBall.targetX))
    {
      SoccerBall.x = SoccerBall.targetX;
      SoccerBall.y = SoccerBall.targetY;
    }
    else
    {
      setTimeout(SoccerBall.animate, 1000 / SoccerBall.frameRate)
    }

    SoccerBall.div.style.left = Math.round(SoccerBall.x) + "px";
    SoccerBall.div.style.top = Math.round(SoccerBall.y) + "px";
  }
};

Core.start(SoccerBall);

In this code, we’ve replaced SoccerBall.duration with SoccerBall.deceleration—a deceleration factor that’s applied to our new position calculation inside animate. This time, instead of dividing the total distance into neat little increments, the position calculator takes the distance remaining to the end-point, and divides it by the deceleration factor. In this way, the steps start out being big, but as the ball moves closer to its goal, the steps become smaller and smaller. The ball travels a smaller distance between frames, creating the sense that it’s slowing down, or decelerating.

One pitfall that you need to watch out for when you’re using an equation like this is that, if left to its own devices, SoccerBall.x will never reach the end point. The animation function will continually divide the remaining distance into smaller and smaller steps, producing an infinite loop. To counteract this (and stop the animation from going forever), we round SoccerBall.x to the nearest integer, using Math.round, before checking whether it has reached the end point. As soon as SoccerBall.x is within 0.5 pixels of the end point—which is close enough for the ball to reach its goal when its position is expressed in pixels—the rounding will cause the animation to end.

Using this deceleration algorithm, the movement of the ball looks more like that depicted in Figure 5.13.

Modeling realistic deceleration by changing the algorithm that moves the soccer ball

Figure 5.13. Modeling realistic deceleration by changing the algorithm that moves the soccer ball

If you want the ball to move more slowly—gliding more smoothly to its final position—increase the deceleration factor. If you want the ball to reach its destination more quickly—coming to a more sudden stop—decrease the deceleration factor. Setting the factor to 1 causes the ball to jump directly to its destination.

Faster!

If you wanted to accelerate the soccer ball—make it start off slow and get faster—then you’d have to reverse the algorithm we used for deceleration:

path-based_motion4.js (excerpt)
var SoccerBall =
{
  init: function()
  {

    SoccerBall.frameRate = 25;
    SoccerBall.acceleration = 2;
    SoccerBall.threshold = 0.5;
    SoccerBall.div = document.getElementById("soccerBall");
    SoccerBall.targetX = 600;
    SoccerBall.targetY = 150;
    SoccerBall.originX = parseInt(
        Core.getComputedStyle(SoccerBall.div, "left"));
    SoccerBall.originY = parseInt(
        Core.getComputedStyle(SoccerBall.div, "top"));

    if (SoccerBall.targetX < SoccerBall.originX)
    {
      SoccerBall.x = SoccerBall.originX - SoccerBall.threshold;
    }
    else
    {
      SoccerBall.x = SoccerBall.originX + SoccerBall.threshold;
    }

    SoccerBall.distanceY = SoccerBall.targetY - SoccerBall.originY;

    SoccerBall.animate();
  },

  animate: function()
  {
    SoccerBall.x += (SoccerBall.x - SoccerBall.originX) /
        SoccerBall.acceleration;
    var movementRatio = (SoccerBall.x - SoccerBall.originX) /
        (SoccerBall.targetX - SoccerBall.originX);
    var y = SoccerBall.originY + SoccerBall.distanceY *
        movementRatio;

    if ((SoccerBall.targetX > SoccerBall.originX &&
        SoccerBall.x >= SoccerBall.targetX) ||
        (SoccerBall.targetX < SoccerBall.originX &&
        SoccerBall.x <= SoccerBall.targetX))
    {
      SoccerBall.x = SoccerBall.targetX;
      y = SoccerBall.targetY;
    }
    else
    {
      setTimeout(SoccerBall.animate, 1000 / SoccerBall.frameRate)
    }

    SoccerBall.div.style.left = Math.round(SoccerBall.x) + "px";
    SoccerBall.div.style.top = Math.round(y) + "px";
  }
};

Core.start(SoccerBall);

Here, the SoccerBall.deceleration variable has been replaced by SoccerBall.acceleration, and the value has been lowered to give us a quicker start. The algorithm that calculates the increments now uses the distance to the start point to determine them, so as the ball gets further from the start point the increments get bigger, making the ball move faster.

As this algorithm uses SoccerBall.x – SoccerBall.originX as the basis for its calculations, we need SoccerBall.x to initially be offset slightly from SoccerBall.originX, otherwise the ball would never move. SoccerBall.x must be offset in the direction of the destination point, so during initialization we check the direction in which the destination lies from the origin, and add or subtract a small value—SoccerBall.threshold—as appropriate. For a more accurate model, you could reduce SoccerBall.threshold below 0.5, but you won’t see much difference.

The other difference between this algorithm and the decelerating one is the way in which the vertical positions are calculated. Since we use a fixed value for acceleration, if it was calculated separately, the acceleration in either dimension would be the same—the y position would increase in speed at the same rate as the x position. If your y destination coordinate is closer than your x destination coordinate, the soccer ball would reach its final y position faster than it reached its final x position, producing some strange (non-linear) movement.

In order to prevent this eventuality, we calculate the y position of the soccer ball based on the value of its x position. After the new x position has been calculated, the ratio between the distance traveled and the total distance is calculated and placed in the variable movementRatio. This figure is multiplied by the total vertical distance between the origin and the target, and gives us an accurate y position for the soccer ball, producing movement in a straight line.

As the y position is calculated in terms of the x position, we no longer need SoccerBall.y, so this property has been removed from the object. Instead, we just calculate a normal y variable inside the animate function. As a quick reference for the vertical distance, SoccerBall.distanceY is calculated upon initialization.

Moving Ahead

You can give countless different types of movements to an object. Deceleration and acceleration are just two of the more simple options; there’s also bouncing, circular movements, elastic movements—the list goes on. If you’d like to learn the mathematics behind other types of object movement, I highly recommend visiting Robert Penner’s site. He created the movement calculations for the Flash environment, so I’m pretty sure he knows his stuff.

Revisiting the Accordion Control

Well, you learned to make an accordion control in Chapter 4, and I’m sure you were clicking around it, collapsing and expanding the different sections, but thinking, “This isn’t quite there; it needs some more … snap!” Well, it’s now time to make it snap, move, and jiggle. Because we’re going to add some animation to that accordion. After all, what’s an accordion without moving parts, and a little monkey in a fez?

This upgrade will take a little more effort than the modifications we made to our Rich Tooltips script, but the effect is well worth it—we’ll wind up with an animated accordion that gives the user a much better sense of what’s going on, and looks very, very slick.

Making the Accordion Look Like it’s Animated

Before we dive into the updated code, let’s take a look at how we’re going to animate the accordion. To produce the effect of the content items collapsing and expanding, we have to modify the height of an element that contains all of that content. In this case, the list items in the menu are the most likely candidates.

Normally, if no height is specified for a block-level element, it will expand to show all of the content that it contains. But if you do specify a height for a block-level element, it will always appear at that specified height, and any content that doesn’t fit inside the container will spill out, or overflow, as shown in Figure 5.14.

Content overflowing the bounds of a block-level element for which height is specified content overflow

Figure 5.14. Content overflowing the bounds of a block-level element for which height is specified

Now, we don’t want the content to overflow when we’re animating our accordion; we want the parent element to hide any content that would ordinarily overflow. The way to do this is to adjust the CSS on the parent container, and specify the property overflow: hidden. When this is done, and a height (or a width) is specified, any overflowing content will be hidden from view, as illustrated in Figure 5.15.

Specifying overflow: hidden on the block-level element causing content that doesn’t fit inside it to be hidden

Figure 5.15. Specifying overflow: hidden on the block-level element causing content that doesn’t fit inside it to be hidden

Once we’ve made sure that the overflowing content is being hidden correctly, we can start to animate the container. To do this, we’ll gradually change its height to make it look like it’s expanding or collapsing.

Changing the Code

Now that you’ve got the general idea of how we’re going to animate our accordion, our first stop is the initialization method:

accordion_animated.js (excerpt)
init: function()
{
  Accordion.frameRate = 25;
  Accordion.duration = 0.5;

  var accordions = Core.getElementsByClass("accordion");

  for (var i = 0; i < accordions.length; i++)
  {
    var folds = accordions[i].childNodes;
    for (var j = 0; j < folds.length; j++)
    {
      if (folds[j].nodeType == 1)
      {
        var accordionContent = document.createElement("div");(1)
        accordionContent.className = "accordionContent";

        for (var k = 0; k < folds[j].childNodes.length; k++)(2)
        {
          if (folds[j].childNodes[k].nodeName.toLowerCase() !=
              "h2")(3)
          {
            accordionContent.appendChild(folds[j].childNodes[k]);(4)
            k--;(5)
          }
        }

        folds[j].appendChild(accordionContent);(6)
        folds[j]._accordionContent = accordionContent;(7)

        Accordion.collapse(folds[j]);
        var foldLinks = folds[j].getElementsByTagName("a");
        var foldTitleLink = foldLinks[0];
        Core.addEventListener(foldTitleLink, "click",
            Accordion.clickListener);

        for (var k = 1; k < foldLinks.length; k++)
        {
          Core.addEventListener(foldLinks[k], "focus",
              Accordion.focusListener);
        }
      }
    }

    if (location.hash.length > 1)
    {
      var activeFold =
          document.getElementById(location.hash.substring(1));
      if (activeFold && activeFold.parentNode == accordions[i])
      {
        Accordion.expand(activeFold);
      }
    }
  }
}

In this code, we’ve added two familiar animation constants, as well as a whole new block of code that modifies the HTML of the menu. Why?

One point you should note about using the overflow: hidden CSS property is that it can produce some weird visual effects in Internet Explorer 6. Given the way we’ve set up the accordion headings, if we applied overflow: hidden to those list items, they’d look completely warped in IE. To circumvent this pitfall, we separate the heading (the part that the user clicks on) from the rest of the content in a given fold of the accordion by putting that content inside its own div element. It’s also a lot easier to deal with the animation if we can collapse the container to zero height, rather than having to worry about leaving enough height for the heading.

But that’s no reason to go in and modify your HTML by hand! We can easily make these changes with JavaScript, as the code above shows:

(1)

Inside init, we create a new div and assign it a class of accordionContent.

(2)

Next, we have to move the current contents of the list item into this new container. We do so using a for loop that iterates through each of the list item’s childNodes.

(3)

We don’t want to move the h2 into that new container, because that’s the clickable part of the accordion, so we do a check for that, skip it, and include everything else.

(4)

You can move an element from one parent to another simply by appending the element to the new parent. The DOM will automatically complete the process of removing and adding the element in the position you’re identified.

(5)

One trick with that for loop is that it decrements the counter every time a child element is moved. The reason for this is that the counter automatically increments every time the loop executes, but if we remove an element from childNodes, its next sibling moves down to take its index, so if we actually incremented the index, we’d start to skip elements. Decrementing cancels out this effect.

(6)

Once all the existing content has been moved into the new container, we’re able to append that container into the list item, completing our modification of the DOM. Now our list item contains only two elements—the title and the content—and no one’s the wiser!

(7)

Finally, as a shortcut for later, we store a reference to the accordionContent element as a property of the list item: _accordionContent.

Another advantage of adding the accordionContent div is that it simplifies the CSS. For example, the CSS code that hides the contents of collapsed folds can be distilled down to this:

accordion_animated.css (excerpt)
.accordionContent {
  overflow: hidden;
  …
}

li.collapsed .accordionContent {
  position: absolute;
  left: -9999px;
}

/* Fixes Safari bug that prevents expanded content from displaying.
   See http://betech.virginia.edu/bugs/safari-stickyposition.html */
li.collapsed .accordionContent p {
  position: relative;
}

Thanks to this code, overflow: hidden will always be specified on elements with the class accordionContent, enabling us to animate them properly.

In the original script, our expand function changed some classes to make the selected accordion item pop open, but in animating the accordion, we need to use expand to do a little setup first:

accordion_animated.js (excerpt)
expand: function(fold)
{
  var content = fold._accordionContent;(1)

  Accordion.collapseAll(fold.parentNode);
  if (!Core.hasClass(fold, "expanded"))(2)
  {
    content.style.height = "0";(3)
    content._height = 0;(4)
    Core.removeClass(fold, "collapsed");
    Core.addClass(fold, "expanded");
    content._increment = content.scrollHeight /
        (Accordion.frameRate * Accordion.duration);(5)
    Accordion.expandAnimate(content);(6)
  }
},

(1)

The content variable is just a shortcut to the fold._accordionContent property we created earlier—it saves plenty of typing. The collapseAll method is then called to reset the accordion menu items, and we can focus our attention on the currently selected content.

(2)

Although previously it didn’t matter if expand was called on an already-expanded fold (for example, in response to the keyboard focus moving from link to link within an expanded fold), we’ve changed this method so that it kicks off an animation. As such, we need this if statement to avoid animating already-expanded folds.

(3)

Before we change its classes to make it visible, we set content’s style.height to 0. This step ensures that when the class change switches the content from collapsed to expanded, it will remain invisible, because it will have no height.

(4)

To keep track of the actual calculated height of the content, we create the variable _height as a property of content. As in the previous animation examples, this property allows us to keep an accurate calculation of the accordion’s movement. Once the height has been reset, we can remove the “collapsed” class and add the “expanded” class.

(5)

We’re almost ready to perform the animation, but before we do that, we need to check the height to which we’re going to expand the accordion item. As we’ll be increasing the height of the item from zero, we have to know when all the content is displayed, so we can work out when to stop. The scrollHeight property lets us do this—it calculates the height of the content irrespective of whether we cut it off with an explicit height and overflow: hidden.

We can save a bit of repetitive calculation within the animating function by calculating in advance the movement increment that we’ll apply in each step of the animation. This is determined by dividing the total height of the content (content.scrollHeight) by the total number of frames (Accordion.frameRate * Accordion.duration). The result of this calculation is assigned to the _increment property of content.

(6)

After all that setup, we can call expandAnimate, our new method that’s designed to animate the expansion of an accordion item.

Let’s take a look at that animation method:

accordion_animated.js (excerpt)
expandAnimate: function(content)
{
  var newHeight = content._height + content._increment;(1)

  if (newHeight > content.scrollHeight)(2)
  {
    newHeight = content.scrollHeight;(3)
  }
  else
  {
    content._timer = setTimeout(function()(4)
        {
          Accordion.expandAnimate(content);
        }, 1000 / Accordion.frameRate);
  }

  content._height = newHeight;(5)
  content.style.height = Math.round(newHeight) + "px";
  content.scrollTop = 0;(6)
},

(1)

expandAnimate starts off by calculating the new height of the content: content._height + content._increment.

(2)

This newHeight variable is used inside a conditional test to detect whether the animation should finish.

(3)

If newHeight is larger than content.scrollHeight, we change newHeight to equal the height of the content, and we don’t schedule any more animation cycles.

(4)

But if newHeight is less than the content’s height, we schedule another setTimeout call to the same function. This step produces the iterative animation we’re looking for. The setTimeout ID is assigned to the _timer property of the content element, so that if another accordion item is clicked mid-animation, we can stop the expansion animation from continuing.

(5)

After we’ve figured out what the new height of the content should be, we use that new height to update the element’s _height property, then change its appearance using a rounded value for style.height. One frame of the animation is now complete.

(6)

In certain browsers, if keyboard focus moves to a hyperlink inside a collapsed fold of the accordion, the browser will make a misguided attempt to scroll that collapsed content in order to make the link visible. If left this way, the content, once expanded, will not display properly. Thankfully, the fix is easy—after each frame of the animation, we reset the content’s scrollTop property to zero, which resets its vertical scrolling position.

With those two methods, you can see how animation works—the event listener is fired only once, and sets up the initial values for the animation. Then the iterative function is called via setTimeout to produce the visual changes needed to get to the final state.

Collapsing an item works in much the same fashion:

accordion_animated.js (excerpt)
collapse: function(fold)
{
  var content = fold._accordionContent;
  content._height = parseInt(content.style.height, 10);
  content._increment = content._height /
      (Accordion.frameRate * Accordion.duration);

  if (Core.hasClass(fold, "expanded"))
  {
    clearTimeout(fold._accordionContent._timer);
    Accordion.collapseAnimate(fold._accordionContent);
  }
  else
  {
    Core.addClass(fold, "collapsed");
  }
},

For the collapse method, we have to move the class changes into the animation method because we need the item to remain expanded until the animation has finished (otherwise it will disappear immediately, before the animation has had a chance to take place). To kick off the animation, we check whether the current item has the class expanded, and if it does, we send the browser off to execute collapseAnimate, but not before we cancel any expansion animation that’s currently taking place. If we didn’t cancel the expansion first, the collapsing animation might coincide with an expansion animation that was already in progress, in which case we’d end up with a stuck accordion (never a pretty sight—or sound).

If an element doesn’t have the expanded class on it when a collapse is initiated, we simply add the collapsed class and forego the animation. This takes care of the circumstance in which the page first loads and we need to hide all the menu items.

collapseAnimate is a lot like expandAnimate, but in reverse:

accordion_animated.js (excerpt)
collapseAnimate: function(content)
{
  var newHeight = content._height - content._increment;(1)

  if (newHeight < 0)
  {
    newHeight = 0;
    Core.removeClass(content.parentNode, "expanded");(2)
    Core.addClass(content.parentNode, "collapsed");
  }
  else
  {
    content._timer = setTimeout(function()
        {
          Accordion.collapseAnimate(content);
        }, 1000 / Accordion.frameRate);
  }

  content._height = newHeight;
  content.style.height = Math.round(newHeight) + "px";
}

(1)

We’re aiming to get the height of the content element to 0, so instead of adding the increment, we subtract it.

(2)

The if-else statement that’s used when we reach our target height is slightly different than the one we saw above, because we have to change the classes on the current item. Otherwise, it’s identical to expandAnimate.

With animation occurring in both directions, we now have a fully functioning animated accordion. Of course, you’ll get the best view of its effect if you check out the demo in the example files, but Figure 5.16 shows what happens when you click to open a new accordion item.

The progression of our animated accordion as one item collapses and another expands

Figure 5.16. The progression of our animated accordion as one item collapses and another expands

Exploring Libraries

Quite a few of the main JavaScript libraries don’t tackle animation; instead, they focus on core tasks like DOM manipulation and styling. However, around these have sprung up a number of little libraries devoted to animation tasks, and they do the job quite well.

The animation libraries are a lot more generalized than the scripts we’ve written here, so it’s possible to use them to apply a range of effects to almost any element, depending on how adventurous you’re feeling.

script.aculo.us

script.aculo.us is probably the most well-known effects library available. It’s actually an add-on to Prototype, which it uses for its DOM access and architecture capabilities, so if you want to run script.aculo.us, you’ll also need to include Prototype.

script.aculo.us comes with a host of effects that have become popular in the so-called Web 2.0 age: fading, highlighting, shrinking, and many other transitions. It has also more recently included larger pieces of functionality such as drag-and-drop and slider widgets. Here, we’ll focus on the effects.

All of the effects in script.aculo.us are available through the Effect object, which will be available in your programs if you include the scriptaculous.js file on your page.

Let’s imagine that we have a paragraph of text that we want to highlight:

scriptaculous_highlight.html (excerpt)
<p id="introduction">
  Industrial Light &amp; Magic (ILM) is a motion picture visual
  effects company, founded in May 1975 by George Lucas and owned
  by Lucasfilm Ltd. Lucas created the company when he discovered
  that the special effects department at Twentieth Century Fox was
  shut down after he was given the green light for his production
  of Star Wars.
</p>

We can do so by passing the ID string to Effect.Highlight:

new Effect.Highlight("introduction");

As soon as you make this call, the effect will be applied to the element, as shown in Figure 5.17.

Creating a yellow fade effect with script.aculo.us

Figure 5.17. Creating a yellow fade effect with script.aculo.us

Effect.Highlight also allows you to pass it a DOM node reference, so you could apply the effect to the paragraph like this:

new Effect.Highlight(document.getElementsByTagName("p")[0]);

Most of the effects in script.aculo.us have optional parameters that allow you to customize various aspects of the effect. These parameters are specified as properties inside an object literal, which itself is passed as an argument to the effect.

For instance, by default, Effect.Highlight fades the background from yellow to white, but it’s possible to specify the start color and the end color, so we could make a particularly lurid fade from red to blue like this:

new Effect.Highlight("introduction",
    {startcolor: "#FF0000", endcolor: "#0000FF"});

These optional parameters will differ from effect to effect, so you’ll have to read through the script.aculo.us documentation if you wish to customize a particular effect.

Most of the effects have a duration parameter, which lets you specify how quickly you want the effect to occur. This parameter is specified in seconds, so if you wanted a two-second, lurid red-blue fade, you’d include all these parameters:

scriptaculous_highlight.js (excerpt)
new Effect.Highlight("introduction",
    {startcolor: "#FF0000", endcolor: "#0000FF",
    duration: 2});

script.aculo.us also has a nice little event model that allows you to trigger functions while effects are happening, or after they have finished. These events are again specified as parameters in the object literal, and take a function name as a value. That function will be called when the event is fired:

function effectFinished()
{
  alert("The introduction has been effected");
}

new Effect.Highlight("introduction",
    {afterFinish: effectFinished});

That code pops up an alert dialog once the fade has finished, to tell us it’s done.

script.aculo.us is certainly a powerful and flexible effects library. Its popularity has largely been fueled by its ease of integration and execution. In case these benefits weren’t already apparent, I’ll leave you with this nugget: using script.aculo.us, we could have animated our soccer ball with just one line of code:

new Effect.MoveBy("soccerBall", 150, 600);

But that wouldn’t have been half as much fun, would it?

Summary

HTML was designed to be a static medium, but using JavaScript, we can bring it to life.

There’s no doubt that animation can add a lot of polish to an interface—it doesn’t have to be mere eye candy! Animation provides real benefits in guiding users around and providing them with visual cues as to functionality and state. I’m sure quite a few useful ideas have sprung into your mind while you’ve been reading. This chapter has given you a taste of what you can do with time-based processing, but there’s so much more to explore once you’ve learned the basics.

Next up, we’ll delve deeper into a subject that’s central to the development of web applications: forms.



[26] If you specify a delay of zero milliseconds when you call setTimeout, the browser will execute the task as soon as it’s finished what it’s currently doing.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset