JavaScript Animation
Object movement
JavaScript makes web page content dynamic, allowing user interaction and animation. The principle of JavaScript animation is rather simple; web page elements, most often images, have their the style or position within the page altered by a script. Repeating this process at sufficiently short intervals has the effect of simulating smooth motion of the element about the page.
Each of the elements comprising a web page is defined by an object (a data storage structure) forming part of the page's Document Object Model (DOM). The elements rendered on the screen are all rectangles defined by their width and height, their position is set by the coordinates of the element's upper left corner. To manipulate these values a reference to the element's DOM object must be generated. The simplest way to do this is to make sure the element has an 'id' attribute and then use the document.getElementById method.
The image of the red ball on the right had id="ball" and the JavaScript to reference its DOM object and change its left and top style values is as follows:
<script type="text/javascript">
function moveBall()
{
var ballObj = document.getElementById("ball");
ballObj.style.left = "7em";
ballObj.style.top = "8em";
}
</script>
Press here to try the code.
Continuing movement
To get repetitive movement, the window.setInterval method can be used. This method takes a reference to a function and a time interval as parameters. setInterval waits for the time interval and calls the function, then waits for the time interval and calls the function again and so on, until the timer is canceled.
In this example the stepBall2() function is called every 100msec. The global variable n2 stores the number of steps the ball has taken along its animation path. The function stepBall2() calculates the new values for the left and top coordinates of the ball for the current step number, and then calls moveBall2() to actually make the image of the ball move. The step number, n2 is incremented ready for the next call to stepBall() by the setInterval timer. Finally a check is made to stop and reset the animation after 13 steps.
<script type="text/javascript">
function moveDomObj(id, l, t)
{
var domObj = document.getElementById(id);
domObj.style.left = l+"px";
domObj.style.top = t+"px";
}
var timer2 = null;
var n2 = 0;
function stepBall2()
{
x = 50+4*n2;
y = 30+1.6*n2*n2; // a parabolic path y=x*x
moveDomObj("ball2", x, y);
n2++;
if (n2>13)
{
clearInterval(timer2);
n2 = 0;
}
}
function startBall2()
{
timer2 = setInterval(stepBall2, 100);
}
</script>
Press here to try the code.
Object oriented animation
The same JavaScript animation functionality may be applied to several web page elements. They may have different algorithms to define their animation paths. This code reuse is ideally suited to Object Oriented programming.
The basic animation functions are provided in the timeline-25.js library. This provides a Timeline object which has play(), stop(), pause() and step() methods. Each Timeline controls an array of Animation objects. Each Animation object has a draw function that is called to render the object in each new position as it moves, and a path function that generates new position information for each frame along the Timeline.
The example below moves the image of a ball along a circular path. The motion can be controlled with the pause and play methods.
The initialisation code creates an Animation object, passing a reference to the function moveElem() as the Animation.draw function, and a reference to the circularPath() function as the Animation.path function.
The Animation.pathFn functions take the Timeline.frameCounter as a parameter and return an object with the x, y coordinates to be passed to the Animation.drawFn for the next move. The Animation constructor can take user defined arguments after these draw and path parameters, it makes these available to the draw and path functions when each is called. In this example a reference to the DOM 'img' element (a picture of a ball) that is to be animated, is passed as an additional parameter. If the generic moveElem function is passed as the draw function, it needs to know which element to move, so passing a reference to the element's DOM node as an additional parameter to the Animation constructor, ensures it is accessible when moveElem is called as the draw function.
The Stop and Start buttons make the calls cmStop() and cmStart() which 'pause' and 'play' the timeline.
Here is all the source code required.
var cm_tline;
function cmStart()
{
cm_tline.play();
}
function cmStop()
{
cm_tline.pause();
}
function moveElem(pos, parms)
{
// pos is the object returned by the 'path' function
// parms is the array of parameters passed to new Animation()
// the draw and path function pointers are skipped so
// parms[0] is actually the 3rd parameter passed
var elem = parms[0];
elem.style.left = pos.x + "px";
elem.style.top = pos.y + "px";
}
function cicularPath(index)
{
var radius = 80;
var cx = 120;
var cy = 120;
var aStep = 3; // 3 degrees per step
var theta = index * aStep; // +ve angles are cw
var newX = cx + radius * Math.cos(theta * Math.PI / 180);
var newY = cy + radius * Math.sin(theta * Math.PI / 180);
// return an object defining state that can be understood by drawFn
return {x: newX, y: newY};
}
function initAnimation3()
{
var elem = document.getElementById("ball3");
var animArray = new Array();
animArray[0] = new Animation(moveElem, cicularPath, elem);
cm_tline = new Timeline(animArray, 0); // 0 number of frames means don't stop
}
Timeline JavaScript utility
As mentioned above the timeline JavaScript library takes care of the animation engine for the previous example. Here is the full source code of the library.
var modes = { PAUSED : 1, STOPPED : 2, PLAYING : 3 }
function Timeline(animationsArray, numFrames, loop)
{
this.anims = animationsArray; // anims is an array of animation objects
for (var i=0; i<this.anims.length; i++)
{
if (this.anims[i]) // make sure it hasn't been removed
this.anims[i].timeline = this;
}
if (numFrames > 0)
this.lastFrame = numFrames - 1;
else
this.lastFrame = -1; // if 0 or negative value entered: go forever
this.loop = (loop == true)? true: false; // convert to boolean
this.timer = null;
this.tickLen = 50; // default value 50msec
this.mode = modes.STOPPED;
// now move to frame 0
this.frameCounter = 0;
this.stepper(0);
}
Timeline.prototype.play = function(startFrame) // optional startFrame
{
if (this.mode == modes.PLAYING)
return;
var savThis = this;
if ((startFrame >= 0) && (startFrame < this.lastFrame))
this.frameCounter = startFrame;
if (this.mode == modes.STOPPED) // re-starting after stop
{
this.frameCounter = 0; // stop() resets framecounter
this.stepper(0); // so draw it
}
else
this.stepper(1);
this.mode = modes.PLAYING;
this.timer = setInterval(function(){savThis.stepper(1)}, savThis.tickLen);
}
Timeline.prototype.step = function()
{
if (this.mode == modes.PLAYING)
return;
if (this.mode == modes.STOPPED) // re-starting after stop
{
this.frameCounter = 0; // stop() resets framecounter
this.stepper(0); // so draw it
}
else
this.stepper(1);
this.mode = modes.PAUSED;
}
Timeline.prototype.pause = function()
{
if (this.timer)
clearInterval(this.timer);
this.mode = modes.PAUSED;
}
Timeline.prototype.stop = function()
{
if (this.timer)
clearInterval(this.timer);
this.mode = modes.STOPPED;
}
Timeline.prototype.stepper = function(inc) // inc = +1, 0
{
if (inc != 0)
{
if ((this.frameCounter < this.lastFrame) || (this.lastFrame <= 0))
this.frameCounter++;
else if (this.loop)
this.frameCounter = 0;
else
{
this.stop();
return;
}
}
// now draw each animated object for the new frame
for (var i=0; i<this.anims.length; i++)
{
if (this.anims[i])
this.anims[i].nextFrame(this.frameCounter);
}
}
Timeline.prototype.setTickInterval = function(tickInt) // in msec
{
if (tickInt > 0)
this.tickLen = tickInt;
}
function Animation(draw, path) // additional arguments are passed to drawFn and pathFn
{
var args = Array.prototype.slice.call(arguments); // grab array of arguments
this.timeline = null; // Initialised by the parent Timeline constructor
this.drawFn = draw; // This is the method draws the image it is passed the
// state object returned by the pathFn and all other
// parameters passed.
this.pathFn = path; // pathFn takes frameCounter and returns a state object
// representing x, y, z, translations and rotations
this.state = null; // store pointer to state vector object, path() should
// reuse this object avoid creating new object each step
this.parms = args.slice(2); // skip draw and path parameters and save the
// rest to pass to drawFn and pathFn
}
Animation.prototype.nextFrame = function(frameCounter)
{
var nextState = this.pathFn.call(this, frameCounter, this.parms);
if (nextState == null) // might be null if this is a static frame
return;
this.drawFn(nextState, this.parms); // pass the new state
this.state = nextState; // save ref to current state vector, pathFn should use it
}
A more complex example
The advantages of the timeline library may not be so obvious in the circular motion example. Here is a more complex example. In this case a path object simulates freefall under gravity with reflection (bouncing) off the boundary walls. The ball is given randomised initial conditions each time its reset.
The Animation.path function in this example is the bouncingPath function. The path function requires a knowledge of the previous state to calculate the next. The Animation object caters for this type of path function by saving the position data returned in the Animation.state property.
All the Animation object properties are accessible to the path functions using the this object. So at the next call to bouncingPath this previous state is available as this.state.
Another useful Animation property is a reference to the parent timeline object. By using this, the path function can read the timeline.tickLen property so that accurate time dependent motion can be calculated.
Parameters passed to the Animation constructor are also available in the Animation the parms property is an array of all the parameters passed except for the mandatory draw function and path parameters, so parms[0] holds the third parameter and parms[1] the fourth and so on.
When the path function returns its object it can contain any user defined properties, these properties only need to be understood by the draw function which gets a copy to tell it the new position to draw the image. Since any user defined object structure can be used with this library, a wide range of animation behaviours is possible.
Here is the code for the bouncing ball example.
var bb_tline;
function resetBall()
{
bb_tline.stop();
bb_tline.play();
}
function initAnimation4()
{
var elem = document.getElementById("ballBlue");
var animArray = new Array();
animArray[0] = new Animation(moveElem, bouncingPath, elem);
bb_tline = new Timeline(animArray, 150, false);
bb_tline.setTickInterval(25);
}
// constructor for position and velocity state of a DOM object.
function DomElementState(x, y, vx, vy)
{
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
}
function bouncingPath(index, parms)
{
// 'this' refers to the Animation object
// parms array holds the parameters passed to new Animation()
// but drawFn and pathFn parameters have been skipped so
// eg. var elem = parms[0];
// previously returned state is available as this.state
// parent timeline is available as this.timeline
var elem = parms[0];
var elementWidth = elem.offsetWidth;
var elementHeight = elem.offsetHeight;
var left = 0;
var right = elem.parentNode.offsetWidth;
var top = 0;
var bottom = elem.parentNode.offsetHeight;
// calc realistic constants
var reflect = -1; // bounce off wall else disappear
var coeff = 0.8;
var friction = 0.95;
var tickInt = this.timeline.tickLen;
var px_mm = 3; // 75dpi = 75px/25.4mm = 3
// gravity = 9.8m/s/s = 0.0098mm/ms/ms = 0.0098* ms/tick*ms/tick * px/mm = 0.0098 * tickInt^2 * px_mm
var gravity = 0.0098 * tickInt * tickInt * px_mm;
// velocity 1m/s = 1 mm/ms = 1mm/ms * px/mm * ms/tick = speed * px_mm * tinkInt
var vel = 0.6 * px_mm * tickInt; // 0.6 m/sec
var x;
var y;
var xVel;
var yVel;
if (index == 0)
{
var startAngle = 10 * Math.floor(Math.random() * 12);
xVel = vel * Math.cos(startAngle * Math.PI / 180);
yVel = vel * Math.sin(startAngle * Math.PI / 180);
x = 120;
y = 50;
// first call of a path function should make state object
// rather than create a new one to return every tick
this.state = new DomElementState(x, y, xVel, yVel);
// the returned reference is stored in this.nextStep
return this.state
}
// this.state is holding the previous results so grab them before we overwrite
xVel = this.state.vx;
yVel = this.state.vy + 0.5 * gravity; // accelerating
x = this.state.x + xVel;
y = this.state.y + yVel;
if (x > right - elementWidth)
{
x = right - elementWidth;
xVel *= reflect*coeff; // lossy reflection next step
}
if (x < left)
{
x = left;
xVel *= reflect*coeff; // lossy reflection next step
}
if (y > bottom - elementHeight)
{
y = bottom - elementHeight;
if (Math.abs(yVel) < gravity)
{
// Now if velocity is less than g, let the g term be the loss mechanism
gravity *= coeff;
yVel *= reflect;
xVel *= friction; // introduce rolling friction for x motion
}
else
yVel *= reflect*coeff; // lossy reflection next step
}
if (y < top)
{
y = top;
yVel *= reflect*coeff; // lossy reflection next step
}
// now just pass the new state vector back, we could create a new object , but
// this.state object has done its job, so overwrite its values and pass it back.
this.state.x = x;
this.state.y = y;
this.state.vx = xVel;
this.state.vy = yVel;
return this.state
}

