Animation Techniques

Introduction

Purpose

Many people have asked me to explain the animation techniques and algorithms used by the X animation functions. This article is an attempt to answer those questions. The functions implemented in this article are not necessarily 'complete', in fact I have tried to keep them as minimal as possible because their purpose in this article is to illustrate animation techniques and algorithms. In the second article in this series I show full implementations of many of the functions discussed here.

I would very much appreciate your feedback. Visit the X Library support forums.

Thanks,
Mike Foster
1 Mar 2007

Revisions

(31Mar07, 14:00 UT) - provided link to second article in this series. Noted that the older iteration technique has some advantages not discussed here.

(5Mar07, 20:25 UT) - reorganized last two sections. added 'Abstracting' section and animate8. general content editing/improvements. gave some demo functions proper names.

Position-Based Animation

First we will look at two iteration techniques and then we will discuss animation algorithms. To keep things simple most of the functions will only animate in one dimension. Later in the article we will implement multi-dimensional animation.

Old

When I first wrote my animation functions (in 2000) I used an iteration technique that was in common use at the time. This technique has some distinct characteristics:

  1. It uses two separate functions, both at global scope. One presets variables and then calls the other which is the iteration function.
  2. Animation data is made specific to an element by creating custom properties on the element object itself.
  3. Iteration is accomplished using setTimeout. It is passed a string which calls the iteration function again and passes the ID string of the element being animated.
function animate0(sEleId, iTargetX)
{
  var ele = xGetElementById(sEleId);
  ele.anim = { // attach animation data to element object
    targetX: iTargetX,
    dx: 4, dt: 10, // Velocity is 4 pix per 10 ms.
    sign: ((xLeft(ele) <= iTargetX) ? 1 : -1) // is x increasing or decreasing?
  };
  _animate0(sEleId);
}
function _animate0(sEleId)
{
  var ele = xGetElementById(sEleId);
  var a = ele.anim; // animation data object
  var x = xLeft(ele) + (a.sign * a.dx); // new x position
  if ((a.sign == -1 || x > a.targetX) && (a.sign == 1 || x < a.targetX)) { // is x at or beyond the target?
    x = a.targetX;
  }
  else {
    setTimeout("_animate0('" + ele.id + "')", a.dt); // continue iterating
  }
  xLeft(ele, x); // move the element
}

animate0('div0', 587)

animate0('div0', 99)

animate0('div0', -25)

div0

New

A better iteration technique is shown in the following implementation. Contrast its characteristics with those of the old technique:

  1. It uses one function at global scope, which presets variables. It then has a nested function which performs the iterations.
  2. Animation data is made specific to an element by a single closure on the function with global scope. This creates an interesting (possibly troublesome) difference. The data is actually specific to each call of the global function - not just specific to the element object.
  3. Iteration is accomplished using setInterval, which is passed a reference to the iteration function. This is much more efficient than passing a string to setTimeout.

This results in a much more stream-lined implementation, as you see in the following. (However, the older iteration technique has some advantages over the closure-based technique. I have not had time to discuss those advantages here.)

function animate1(sEleId, iTargetX)
{
  var dx = 4, dt = 10; // Velocity is 4 pix per 10 ms.
  var ele = xGetElementById(sEleId);
  var sign = (xLeft(ele) <= iTargetX) ? 1 : -1; // is x increasing or decreasing?
  var tmr = setInterval( // add a timer event listener
    function() {
      var x = xLeft(ele) + (sign * dx); // new x position
      if ((sign == -1 || x > iTargetX) && (sign == 1 || x < iTargetX)) { // is x at or beyond the target?
        clearInterval(tmr); // stop iterations
        x = iTargetX;
      }
      xLeft(ele, x); // move the element
    }, dt // timer interval
  );
}

Both the old and new iteration techniques use the same position-based animation algorithm. Basically, we just increment (or decrement) the coordinate until the next change would put it beyond the target position. We can adjust the velocity of the animation by changing dx and dt, but the acceleration remains 0. So the movement begins abruptly, continues at a constant speed, and ends abruptly - it moves at the same speed either for a long or short distance - and so this is not always the smoothest animation technique for moving elements on a page. However, it works okay for things like opacity and colors - and for short animations.

animate1('div1', 587)

animate1('div1', 99)

animate1('div1', -25)

div1

Time-Based Animation

A better animation technique for moving elements on a page is a time-based technique that animates the element to the target position within a specified time. This also produces a more consistent animation across different browsers and OSs.

Let's look at the following code, which could be called an animation engine because its only purpose is to calculate values for the variable f, which is all we need for animation algorithms. The engine code will have the same text color in all the following functions.

function animate2(uTotalTime)
{
  var freq = (1 / uTotalTime); // frequency
  var startTime = new Date().getTime();
  var tmr = setInterval(
    function() {
      var elapsedTime = new Date().getTime() - startTime;
      if (elapsedTime < uTotalTime) {
        var f = elapsedTime * freq;
      }
      else {
        clearInterval(tmr);
      }
    }, 10
  );
}

We save the start time then use it to get the elapsed time during iterations. We determine the end of iterations based on the elapsed time and total time - instead of based on position as we did in animate1.

The variable f becomes elapsedTime / uTotalTime which is the ratio of elapsed time to total time, that is, f represents a fraction of the whole. f starts at 0 and increases until it reaches 1.0 (it is a floating point number). We want to correlate time and displacement (we want the element to reach the target when uTotalTime milliseconds have elapsed) and since f represents a fraction of the whole the whole can refer to the total time or it can also refer to the total displacement (the total distance the element is to move). So f * totalDisplacement == displacementAtElapsedTime. Now we add displacementAtElapsedTime to the start position and we have the new position, as implemented in the following.

function animate3(sEleId, iTargetX, uTotalTime)
{
  var ele = xGetElementById(sEleId);
  var startX = xLeft(ele);
  var disp = iTargetX - startX; // total displacement
  var freq = (1 / uTotalTime); // frequency
  var startTime = new Date().getTime();
  var tmr = setInterval(
    function() {
      var elapsedTime = new Date().getTime() - startTime;
      if (elapsedTime < uTotalTime) {
        var f = elapsedTime * freq;
        xLeft(ele, Math.round(f * disp + startX));
      }
      else {
        clearInterval(tmr);
        xLeft(ele, iTargetX);
      }
    }, 10
  );
}

So now we can animate based on time - but animate3 still animates at constant velocity - no acceleration.

animate3('div3', 600, 1000)

animate3('div3', 100, 1000)

animate3('div3', 0, 1000)

div3

Animation with Acceleration

The variable f is the key to using this technique to animate anything and with any type of acceleration. Let's look at f as if it were a value returned from a function named ff which accepts elapsedTime and totalTime as arguments.

In 'Graph 1' we have a plot of the ff function used in animate3. As elapsedTime changes from 0 to totalTime, f changes from 0.0 to 1.0. If we multiply f by the total displacement we get the displacement at a specific elapsedTime, which must then be added to the start position to get the new position.

Note that f changes linearly (along a straight line) - which gives us a constant velocity (no acceleration). So if the plot of ff were not a straight line, but a curve, then it would give us a changing velocity, that is, acceleration.


We can easily make ff plot a sine curve since "Math.sin" is a native Javascript function. In 'Graph 2' we have a plot of one-fourth of a sine curve. Let this curve define our velocity instead of the straight line in Graph 1. We want the total time to correlate with this curve as the horizontal axis changes from 0 to pi/2. Why use "pi/2"? Well, we are using radian units. There are 2*pi radians in a cycle (circle) and we only want to use the first quarter of the cycle. Thus: 2*pi/4 = pi/2. This determines how we calculate the frequency. During iterations we multiply the elapsed time by the frequency, as we did in animate3, but now we take the sine of that value. The result is an f value which follows the first quarter-cycle of a sine curve.

Note how the curve has a very steep slope initially, but the closer the elapsed time gets to the total time the more the slope decreases. This describes our velocity - it will start fast then end slow. Other curves will produce different effects, for example a cosine curve will start slow then end fast.


'Graph 2' illustrates the ff function used in animate4, below. Notice that there is not much difference between animate3 and animate4. Since the period (total time) doesn't change, we calculate the frequency before we start iterations.

function animate4(sEleId, iTargetX, uTotalTime)
{
  var ele = xGetElementById(sEleId);
  var startX = xLeft(ele);
  var disp = iTargetX - startX; // total displacement
  var freq = Math.PI / (2 * uTotalTime); // frequency
  var startTime = new Date().getTime();
  var tmr = setInterval(
    function() {
      var elapsedTime = new Date().getTime() - startTime;
      if (elapsedTime < uTotalTime) {
        var f = Math.abs(Math.sin(elapsedTime * freq));
        xLeft(ele, Math.round(f * disp + startX));
      }
      else {
        clearInterval(tmr);
        xLeft(ele, iTargetX);
      }
    }, 10
  );
}

Now we have time-based animation with sinusoidal acceleration. Cool! :-)

animate4('div4', 600, 1000)

animate4('div4', 100, 1000)

animate4('div4', 0, 1000)

div4

Multiple Dimensions

The functions thus far have only implemented one-dimensional animation. Now let's look at adding support for two or more dimensions. Look at animate2 again. We can consider it our animation engine because its only purpose is to generate f. In animate4 if we ignore all code that relates to the engine then what remains can be generalized as follows:

  1. Save the starting (initial) value.
  2. Save the total displacement (the initial value subtracted from the target value).
  3. During iterations, multiply f by the displacement and add the result to the initial value.

To animate an element's X and Y coordinates, first save the element's initial X and Y coordinates and then calculate the displacement for each. During iterations multiply f by both the X and Y displacements and add the results to the inital values. This is two dimensional animation. These ideas have been implemented in xAnimateXY:

function xAnimateXY(sEleId, iTargetX, iTargetY, uTotalTime)
{
  var ele = xGetElementById(sEleId);
  var startX = xLeft(ele); // x start position
  var startY = xTop(ele); // y start position
  var dispX = iTargetX - startX; // x displacement
  var dispY = iTargetY - startY; // y displacement
  var freq = Math.PI / (2 * uTotalTime); // frequency
  var startTime = new Date().getTime();
  var tmr = setInterval(
    function() {
      var elapsedTime = new Date().getTime() - startTime;
      if (elapsedTime < uTotalTime) {
        var f = Math.abs(Math.sin(elapsedTime * freq));
        xLeft(ele, Math.round(f * dispX + startX));
        xTop(ele, Math.round(f * dispY + startY));
      }
      else {
        clearInterval(tmr);
        xLeft(ele, iTargetX);
        xTop(ele, iTargetY);
      }
    }, 10
  );
}

xAnimateXY('div5', 600, -300, 1000)

xAnimateXY('div5', 100, 100, 1000)

xAnimateXY('div5', 0, 0, 1000)

div5

Note that all the demo elements are absolutely positioned within a relatively positioned element.

Animating Any CSS Property

In Javascript we can access properties of the element.style object with array syntax, using a property's name string as the subscript. This allows us to write an animation function which will animate any CSS property. However we have to handle different property values differently. Properties with color values have to be handled differently than other values because a color value is really three separate values (red, green and blue). Properties with integer values and real values have to be handled differently. Values with different units cannot be combined in expressions, for example you can't add '1em' and '1px'. For these reasons (and in the interest of keeping the demos simple) xAnimateRgb will only work with properties that have color values and xAnimateCss will only work with properties that have integer pixel values.

CSS Properties with Color Values

To animate color instead of position, first save the initial R, G and B values and then calculate the displacement for each. During iterations multiply f by each of the R, G and B displacements and add the results to the initial values. This is three dimensional animation. These ideas have been implemented in xAnimateRgb. A few support functions were needed to deal with color values and to convert CSS property names to style object property names. View the source of this page to see those functions.

function xAnimateRgb(sEleId, sCssProp, targetColor, uTotalTime)
{
  var ele = xGetElementById(sEleId);
  var startC = xParseColor(xGetComputedStyle(ele, sCssProp)); // start colors
  var targetC = xParseColor(targetColor); // target colors
  var disp = { // color displacements
    r: targetC.r - startC.r,
    g: targetC.g - startC.g,
    b: targetC.b - startC.b
  };
  var freq = Math.PI / (2 * uTotalTime); // frequency
  var startTime = new Date().getTime();
  var tmr = setInterval(
    function() {
      var elapsedTime = new Date().getTime() - startTime;
      if (elapsedTime < uTotalTime) {
        var f = Math.abs(Math.sin(elapsedTime * freq));
        ele.style[xCamelize(sCssProp)] = xRgbToHex(
          Math.round(f * disp.r + startC.r),
          Math.round(f * disp.g + startC.g),
          Math.round(f * disp.b + startC.b));
      }
      else {
        clearInterval(tmr); // stop iterations
        ele.style[xCamelize(sCssProp)] = targetC.s;
      }
    }, 10 // timer interval
  );
}
"color"

xAnimateRgb('div6', 'color', '#ffffff', 2000)

xAnimateRgb('div6', 'color', '#000000', 2000)

xAnimateRgb('div6', 'color', '#ff0000', 2000)

xAnimateRgb('div6', 'color', '#00ff00', 2000)

xAnimateRgb('div6', 'color', '#0000ff', 2000)

"background-color"

xAnimateRgb('div6', 'background-color', '#ffffff', 2000)

xAnimateRgb('div6', 'background-color', '#000000', 2000)

xAnimateRgb('div6', 'background-color', '#ff0000', 2000)

xAnimateRgb('div6', 'background-color', '#00ff00', 2000)

xAnimateRgb('div6', 'background-color', '#0000ff', 2000)

"border-color"

xAnimateRgb('div6', 'border-left-color', '#ffffff', 2000)

xAnimateRgb('div6', 'border-left-color', '#000000', 2000)

xAnimateRgb('div6', 'border-left-color', '#ff0000', 2000)

xAnimateRgb('div6', 'border-left-color', '#00ff00', 2000)

xAnimateRgb('div6', 'border-left-color', '#0000ff', 2000)

In Firefox 2 getComputedStyle returned an empty string for "border-color" even though the element had "border-color" set in its CSS.

div6

Animating CSS Properties with Color Values!

unde omnis iste natus error sit voluptatem accusantium doloremque laudantium

Lorem ipsum dolor sit amet, consectetuer adipiscing elit.

CSS Properties with Integer Pixel Values

The following function animates any CSS property with integer pixel values. Its implementation is simple compared to xAnimateRgb.

function xAnimateCss(sEleId, sCssProp, iTarget, uTotalTime)
{
  units = 'px'; // only supports integer pixel values for now
  var ele = xGetElementById(sEleId);
  var startValue = parseInt(xGetComputedStyle(ele, sCssProp)) || 0;
  var disp = iTarget - startValue; // total displacement
  var freq = Math.PI / (2 * uTotalTime); // frequency
  var startTime = new Date().getTime();
  var tmr = setInterval(
    function() {
      var elapsedTime = new Date().getTime() - startTime;
      if (elapsedTime < uTotalTime) {
        var f = Math.abs(Math.sin(elapsedTime * freq));
        ele.style[xCamelize(sCssProp)] = Math.round(f * disp + startValue) + units;
      }
      else {
        clearInterval(tmr);
        ele.style[xCamelize(sCssProp)] = iTarget + units;
      }
    }, 10
  );
}
"left"

xAnimateCss('div7', 'left', 600, 1500)

xAnimateCss('div7', 'left', 400, 1500)

"padding-left"

xAnimateCss('div7', 'padding-left', 100, 1000)

xAnimateCss('div7', 'padding-left', 10, 1000)

"width"

xAnimateCss('div7', 'width', 200, 500)

xAnimateCss('div7', 'width', 400, 500)

"letter-spacing"

xAnimateCss('div7', 'letter-spacing', 10, 800)

xAnimateCss('div7', 'letter-spacing', 1, 800)

"line-height"

xAnimateCss('div7', 'line-height', 28, 500)

xAnimateCss('div7', 'line-height', 14, 500)

div7

Animating CSS Properties with Integer Pixel Values!

unde omnis iste natus error sit voluptatem accusantium doloremque laudantium

Lorem ipsum dolor sit amet, consectetuer adipiscing elit.

Some of these leave rendering artifacts in Opera 9.10.

Abstracting The Engine

You've probably noticed the same animation engine code in all the time-based functions. In the listings for those functions you can see how the non-engine code is interspersed in the engine code. This helps to see how the engine could be abstracted. For example animate8 is a simple implementation of this idea.

function animate8(uTotalTime, fnOnStart, fnOnRun, fnOnEnd)
{
  if (fnOnStart) { fnOnStart(); }
  var freq = Math.PI / (2 * uTotalTime); // frequency
  var startTime = new Date().getTime();
  var tmr = setInterval(
    function() {
      var elapsedTime = new Date().getTime() - startTime;
      if (elapsedTime < uTotalTime) {
        var f = Math.abs(Math.sin(elapsedTime * freq));
        if (fnOnRun) { fnOnRun(f); }
      }
      else {
        clearInterval(tmr);
        if (fnOnEnd) { fnOnEnd(); }
      }
    }, 10
  );
}

The second article in this series takes this abstraction idea much farther.

Related

The second article in this series.

The X Animation Index page.

Latest Demos

Pick A Card, Demo 2 - an improved version of this demo.

xFenster rev 21 - added support for a "control menu".

xCalendar - a new X UI object.

Tech Support

Forum support is available at the X Library Support Forums.

License

By your use of X and/or CBE and/or any Javascript from this site you consent to the GNU LGPL - please read it. If you have any questions about the license, read the FAQ and/or come to the forums.