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
(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.
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.
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:
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)
A better iteration technique is shown in the following implementation. Contrast its characteristics with those of the old technique:
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)
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)
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)
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:
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)
Note that all the demo elements are absolutely positioned within a relatively positioned element.
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.
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 ); }
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)
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)
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.
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 ); }
xAnimateCss('div7', 'left', 600, 1500)
xAnimateCss('div7', 'left', 400, 1500)
xAnimateCss('div7', 'padding-left', 100, 1000)
xAnimateCss('div7', 'padding-left', 10, 1000)
xAnimateCss('div7', 'width', 200, 500)
xAnimateCss('div7', 'width', 400, 500)
xAnimateCss('div7', 'letter-spacing', 10, 800)
xAnimateCss('div7', 'letter-spacing', 1, 800)
xAnimateCss('div7', 'line-height', 28, 500)
xAnimateCss('div7', 'line-height', 14, 500)
Some of these leave rendering artifacts in Opera 9.10.
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.
The second article in this series.
The X Animation Index page.
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.
Forum support is available at the X Library Support Forums.