MISCELLANEOUS TECHNICAL ARTICLES BY Dr A R COLLINS

JavaScript Animation

Object movement

JavaScript makes webpage content dynamic, allowing user interaction and animation. The principle of JavaScript animation is rather simple; webpage elements, most often images, have their 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 webpage 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:

red ball
function moveBall()
{
  var ballObj = document.getElementById("ball");

  ballObj.style.left = "70px";
  ballObj.style.top = "80px";
}

Press here to try the code.

Continuing movement

To get repetitive movement, the window.setTimeout method can be used. This method takes a reference to a function and a time delay (in milliseconds) as parameters. setTimeout waits for the delay time and calls the function. The drawing having been done, the setTimout can be called again and so on.

In this example the stepBall2() function is called every 100msec. The global variable s 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, s 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.

red ball
function moveDomObj(id, left, top)
{
  var domObj = document.getElementById(id);

  domObj.style.left = left+"px";
  domObj.style.top = top+"px";
}

var timer2 = null;
var s = 0;

function stepBall2()
{
  x = 50+4*s;
  y = 35+1.6*s*s;   // a parabolic path y=x*x
  moveDomObj("ball2", x, y);
  s++;
  if (s < 14)
    timer2 = setTimeout(stepBall2, 100);
  else
    s = 0;     // so we can do it again
}

function startBall2()
{
  timer2 = setTimeout(stepBall2, 100);
}

Press here to run the function 'startBall2()'.

window.requestAnimationFrame has arrived

The setTimeout utility is not ideal for generating smooth continuous animation, the operating system has so many tasks competing for attention timeout servicing can be delayed and the assumption that the frames are equally spaced breaks down and at worst timeouts may stack up so that the animation jitters. To address the requirements of animation, the window method requestAnimationFrame has been implemented by all the major browsers.

requestAnimationFrame (rAF) attempts to draw the frame at a rate of about 60 frames/sec, comparable with the typical screen refresh rate. In so doing, it avoids wasting CPU time switching among competing interrupts, allowing more efficient use of the hardware resources. The major difference to code using rAF rather than timeout, is rAF frame rate should not be assumed a priori. rAF will pass the callback the time it is called in milliseconds relative to an arbtrary start time, movement should be calculated on elapsed time since the last frame.

The cross browser support for window.requestAnimationFrame is good but since the utility is so new the function still has a browser prefix. An excellent polyfill written by Erik Möller should be added to any page using the rAF utility.

Circular Motion using requestAnimationFrame

Here is a simple example of continuous motion using the requestAnimationFrame method. The image of the red ball is moved in circluar orbit. The Start button makes an initial call to rAF and then, the circularPath callback moves the ball and then calls rAF again, providing the play flag is still true. The Stop button simply sets the 'play' flag to false, halting the animation.

The only subtlety in the code is the manipulation of the 'currTime' variable which saves the time at which the currently displayed frame was drawn. If resuming from a pause, currTime is updated with the Date.now() function so there is no discntinuity in the motion due to the time lost while paused.

Here is all the source code of the circular motion example.

var cm_tline = null;

function Orbit(objId)
{
  var elem = document.getElementById(objId),
      radius = 80,
      va = 3,             // angular velocity, 3 radians / sec
      cx = 120,           // coordinates of orbit center
      cy = 120,
      ang = 0,
      currTime = 0,       // time when current frame was drawn
      playing = false;

  function circularPath(time)
  {
    var x, y,
        dt = time - currTime;  // time since last frame

    ang += va*dt/1000;         // angle moved at constant angular velocity
    if (ang > 5)               // wraparound for angle
    {
      ang -= 2*Math.PI;
    }
    x = cx + radius * Math.cos(ang);  // calculate coords of ball
    y = cy + radius * Math.sin(ang);
    elem.style.left = x + "px";          // move the element
    elem.style.top = y + "px";

    currTime = time;           // save the time this frame is drawn
    if (playing)
    {
      requestAnimationFrame(circularPath);
    }
  };

  this.play = function()
  {
    currTime = Date.now();  // move timeLime as if currFrame just drawn
    playing = true;
    requestAnimationFrame(circularPath);
  };

  this.pause = function()
  {
    playing = false;  // stop after next frame
  }
}

Timeline JavaScript utility

For more complax animations where multiple objets are animated and may ineract, a timeline is still required to coordinate the animations.

The same JavaScript animation functionality may be applied to several webpage elements. They may have different algorithms to define their animation paths. This code reuse is ideally suited to Object Oriented programming.

The basic timeline functionality is provded by the timeline2v08.js object, which has play(), stop(), pause() and step() methods. Each Timeline can control a single Animation object or an array of Animation objects.

Each Animation object takes a user defined init function, to initialise the animation variables, a draw function, to render the object as it moves, and a path function that generates new position coordinates for each re-draw.

The init method is called once when the Animation object is created. The init method returns a reference to the object to be animated. This is stored in the Animation object as the property obj. The pathFn and drawFn are called in the scope of the Animation object so they have access to the obj, currState and nextState.

The draw function, is passed this.obj and this.nextState as parameters along with a copy of all the extra parameters passed to Animation on creation. This additional method of accessing the object to be drawn and the position in which to draw it, are provided for those cases where a generic draw function is used.

Rev 2.08 adds the property 'stepTime' with a default value of 50 msec. This is the user definable size of the time step take when single stepping along the timeline using the 'step' method.

Here is the full source code of the Timeline utility.

Timeline = function(animation, duration, looping)
{
  var PAUSED = 1, STOPPED = 2, PLAYING = 3, STEPPING = 4;

  var savThis = this;
  var i;
  this.anims = animation;     // anims is an array of animation objects
  if (this.anims instanceof Array)
  {
    for (i=0; i<this.anims.length; i++)
    {
      if (this.anims[i])        // make sure it hasn't been removed
      {
        this.anims[i].timeline = this;
      }
    }
  }
  else
  {
    this.anims.timeline = this;
  }

  this.dur = -1;     // if 0 or negative value entered: go forever
  if (duration > 0)
  {
    this.dur = duration;
  }

  this.stepTime = 50;   // msec time interval between single steps

  var loop = (looping == true)? true: false;    // convert to boolean
  var mode = STOPPED;
  var prevMode = STOPPED;
  var startTime = 0;    // system time when animation started
  var currTime = 0;     // msecs along timeline when current frame drawn
  var timer = null;

  function stepper()
  {
    var time = Date.now();   // generate local time stamp, browsers pass different time types
    var i;
    if (prevMode == STOPPED)
    {
      startTime = time;                // forces localTime = 0
    }
    var localTime = time - startTime;    // millsecs along timeline
    if ((localTime > savThis.dur) && (savThis.dur > 0))
    {
      if (loop)
      {
        startTime = time;   // we will re-start
        localTime = 0;      // pass this to pathFn to re-initialize
      }
      else                  // end of the animation
      {
        savThis.stop();
        return;
      }
    }
    // now draw each animated object for the new frame
    if (savThis.anims instanceof Array)
    {
      for (i=0; i<this.anims.length; i++)
      {
        if (savThis.anims[i])
        {
          savThis.anims[i].nextFrame(localTime);
        }
      }
    }
    else
    {
      savThis.anims.nextFrame(localTime);
    }
    // drawing done
    currTime = localTime;      // timestamp of what is currently on screen
    if (mode == STEPPING)
    {
      prevMode = PAUSED;
      mode = PAUSED;
    }
    if (mode == PLAYING)
    {
      prevMode = PLAYING;
      timer = window.requestAnimationFrame(stepper);
    }
  }

  this.play = function()
  {
    if (mode == PLAYING)
    {
      return;
    }
    if (mode == PAUSED)
    {
      startTime = Date.now() - currTime;  // move timeLime as if currFrame just drawn
    }
    prevMode = mode;
    mode = PLAYING;
    timer = window.requestAnimationFrame(stepper);
  };

  this.step = function()
  {
    // eqivalent to play for one frame and pause
    if (mode == PLAYING)
    {
      return;
    }
    if (mode == PAUSED)
    {
      startTime = Date.now() - currTime;  // move timeLime as if currFrame just drawn
    }
    prevMode = mode;
    mode = STEPPING;
    setTimeout(function(){timer = window.requestAnimationFrame(stepper);}, this.stepTime);
  };

  this.pause = function()
  {
    if (timer)
    {
      window.cancelAnimationFrame(timer);
    }
    prevMode = mode;
    mode = PAUSED;
  };

  this.stop = function()
  {
    if (timer)
    {
      window.cancelAnimationFrame(timer);
    }
    prevMode = mode;
    mode = STOPPED;
    // reset the currTime so play and step know to start again
    currTime = 0;
  };
  // draw the initial frame of the animation (pass time =0)
  timer = window.requestAnimationFrame(stepper);
}


Animation = function(init, draw, path) // additional arguments  may be passed
{
  var args = Array.prototype.slice.call(arguments); // grab array of arguments

  this.timeline = null;           // Initialised by the parent Timeline constructor
  this.drawFn = draw;             // drawFn draws this.object in this.nextSate
  this.pathFn = path;             // pathFn takes current time, calculates nextState vector
                                  // representing x, y, z, translations and rotations etc
  this.currState = {};            // current (as drawn) state vector
  this.nextState = {};            // pathFn return next state vector here
  this.parms = args.slice(3);     // skip init, draw and path parameters and save the
                                  // rest to pass to initFn, drawFn and pathFn
  this.obj = null;                // object to be animated (returned from initFn)
  var rtnVal;
  if (typeof init == "function")
  {
    rtnVal = init.call(this, this.parms);     // call initialization code
  }
  if (rtnVal != undefined)
  {
    this.obj = rtnVal;
  }
}

Animation.prototype.nextFrame = function(t)
{
  this.pathFn(t, this.parms);
  if (this.nextState == null)  // might be null if this is a static frame
  {
    return;
  }
  this.drawFn(this.obj, this.nextState, this.parms); // pass the new state
  // swap state pointers
  var tmp = this.currState;
  this.currState = this.nextState; // save current state vector, pathFn will use it
  this.nextState = tmp;
};

A more complex example

In this animation, a path object simulates freefall under gravity with reflection off the boundary walls. The ball is given randomised initial tragectory after each time its stops.

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 'as drawn' position data returned in the Animation.currState property.

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 init, draw function and path parameters, so parms[0] holds the fourth parameter and parms[1] the fifth and so on.

The properties of the state vectors passed around are user defined, the properties only need to be understood by the path and draw functions. Since any 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.

function initAnimation4(elemId)
{
  var elem = document.getElementById(elemId),
      // save width, height and boundary locations for bouncing off walls
      elementWidth = elem.offsetWidth,
      elementHeight = elem.offsetHeight,
      leftWall = 0,                               // containing box walls
      rightWall = elem.parentNode.offsetWidth,
      topWall = 0,
      bottomWall = elem.parentNode.offsetHeight;

  function getBall()
  {
    // create the currState and nextState objects
    this.currState = {t: 0, x: 0, y: 0, vx: 0, vy: 0};
    this.nextState = {t: 0, x: 0, y: 0, vx: 0, vy: 0};
    // return the object to be animated
    return elem;
  }

  function bouncingPath(time)    // time passed is the time since start of animation
  {
    // 'this' refers to the Animation object
    var reflect = -1,     // bounce off wall else disappear
        coeff = 0.9,      // energy loss at bounce
        friction = 0.003, // rolling friction loss/msec
        px_mm = 3.5,      // 90dpi = 90px/25.4mm = 3.5
        gravity = 0.0098 * px_mm, // gravity =9.8m/s/s =0.0098mm/ms/ms =0.0098*px/mm
        xVel = 0,
        yVel = 0,
        x, y,
        vel, startAngle, timeInt, s, u,
        anim;

    if (time == 0)   // generate random start condition for each reset
    {
      // restart at 0.4 m/sec initial velocity and at a random angle
      // velocity 1m/s = 1 mm/ms = 1mm/ms * px/mm = speed * px_mm
      vel = 0.4 * px_mm;    // px /ms
      startAngle = 10 * Math.floor(Math.random() * 12) + 2;
      this.nextState.t = time;
      this.nextState.x = 120;
      this.nextState.y = 50;
      this.nextState.vx = vel * Math.cos(startAngle * Math.PI / 180);
      this.nextState.vy = vel * Math.sin(startAngle * Math.PI / 180);
      return;     // this is the state to get drawn at start
    }
    // calculate the new position and velocity
    timeInt = time - this.currState.t;   // time since last draw
    // v = u + at
    yVel = this.currState.vy + gravity * timeInt;    // accelerating due to gravity
    xVel = this.currState.vx;                    // constant
    x = this.currState.x + xVel*timeInt;
    y = this.currState.y + yVel*timeInt;
     // now check for hitting the walls
    if (x > rightWall - elementWidth)
    {
      x = rightWall - elementWidth;
      xVel *= reflect*coeff;    // lossy reflection next step
    }
    if (x < leftWall)
    {
      x = leftWall;
      xVel *= reflect*coeff;    // lossy reflection next step
    }
    if (y > bottomWall - elementHeight)
    {
      y = bottomWall - elementHeight;
      // calc velocity at the floor   (v^2 = u^2 + 2*g*s)
      s = bottomWall - elementHeight - this.currState.y;     // pre bounce
      u = this.currState.vy;
      yVel = Math.sqrt(u*u + 2*gravity*s);
      yVel *= reflect*coeff;  // lossy reflection next step
      xVel *= 0.97;
    }
    if (y < topWall)
    {
      y = topWall;
      yVel *= reflect*coeff;    // lossy reflection next step
    }
    this.nextState.t = time;
    this.nextState.x = x;
    this.nextState.y = y;
    this.nextState.vx = xVel;
    this.nextState.vy = yVel;
  }

  function moveElem(obj, pos)
  {
    obj.style.left = pos.x + "px";
    obj.style.top = pos.y + "px";
  }

  anim = new Animation(getBall, moveElem, bouncingPath);

  bb_tline = new Timeline(anim, 3000, false);
}

The same Timeline object with an array of Animation objects can be used in canvas animation. Typically a canvas Stack is used in animation and one Animation object manages one canvas layer. The Timeline updating each layer every tick.