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, matching the 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 a time value (in msec) representing the time relative to an arbitrary start time, this can be used to calculate the elapsed time since the last frame and the amount of movement set accordingly.

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 circular 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 'playing' flag is still true. The Pause button simply sets the 'playing' 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 discontinuity 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,
      savTime = 0,       // time when last frame was drawn
      playing = false;

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

    ang += va*dt/1000;                // angle moved at constant angular velocity
    if (ang > 2*Math.PI){              // 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";

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

  this.play = function(){
    playing = true;
    savTime = Date.now();             // reset to avoid a jump in angle
    requestAnimationFrame(circularPath);
  };

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

Timeline JavaScript utility

For more complex animations where multiple objects are animated and may interact, a timeline is still required to coordinate the animations.

Each Animation object takes a user defined init function, to initialize the animation variables and return the object to be animated, a draw function, to render the object in each frame along the timeline, and a path function that generates new position coordinates for each frame.

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.

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

Here is the full source code of the Timeline utility.

Timeline = function(animationObj, duration, looping)
{
  var savThis = this;
  var PAUSED = 1, STOPPED = 2, PLAYING = 3, STEPPING = 4,
      loop = (looping == true)? true: false,    // convert to boolean
      mode = STOPPED,
      prevMode = STOPPED,
      startTime = 0,    // system time when animation started
      currTime = 0,     // msecs along timeline when current frame drawn
      timer = null;

  this.stepTime = 50;   // msec time interval between single steps
  this.anims = animationObj;   // anims can be an array of animation objects or just one
  if (this.anims instanceof Array)
  {
    this.anims.forEach(function(an){an.timeline = savThis;});  // save a reference to timeline
  }
  else
  {
    this.anims.timeline = this;
  }

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

  function stepper()
  {
    var time = Date.now(),   // generate local time stamp, browsers pass different time types
        localTime;

    if (prevMode == STOPPED)
    {
      startTime = time;     // forces localTime = 0
    }
    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)
    {
      savThis.anims.forEach(function(an){an.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 timeline as if currFrame just drawn
    }
    prevMode = mode;
    mode = PLAYING;
    timer = window.requestAnimationFrame(stepper);
  };

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

  this.redraw = function()
  {
    // equivalent to play for one frame and pause
    if (mode === PLAYING)
    {
      return;
    }
    startTime = Date.now() - currTime;  // move timeline as if currFrame just drawn
    stepper();
  };

  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
  timer = window.requestAnimationFrame(stepper);
};

Animation = function(initObj, draw, path, options) 
{
  var savThis = this,
      i;

  this.timeline = null;           // Initialized by the parent Timeline constructor
  this.obj = null;                // object to be animated
  this.drawFn = draw;             // drawFn draws this.obj in this.nextSate
  this.pathFn = path;             // pathFn takes current time, calculates nextState vector
  this.currState = {time:0};      // current (as drawn) state vector
  this.nextState = {time:0};      // pathFn return next state vector here

  if (typeof initObj === "function")
  {
    this.obj = initObj.call(this, options);  // call object creation code
  }
  else if (typeof initObj === "object")
  {
    this.obj = initObj;           // object exists already use it
  }
  for (i in this.nextState)   // if initFn creates new porperties make currState match
  {
    this.currState[i] = this.nextState[i];
  }

  this.nextFrame = function(t)
  {
    var tmp;

    savThis.pathFn(t, options);
    savThis.drawFn(savThis.obj, savThis.nextState, options); // pass the new state
    // swap state pointers
    tmp = savThis.currState;
    savThis.currState = savThis.nextState; // save current state vector, pathFn will use it
    savThis.nextState = tmp;
    savThis.currState.time = t;     // save the frame draw time for use in next call to pathFn
  };
};

A more complex example

In this animation, a path object simulates freefall under gravity with reflection off the boundary walls. The ball is given randomized initial trajectory 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.

Optional parameters can be passed to the Animation constructor as properties of the 'options' argument. The options object is passed to the init, draw, and path functions.

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, a wide range of animation behaviours is possible.

Here is the code for the bouncing ball example.

var bb_tline;

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,
      anim;

  function getBall()
  {
    // create the state vector properties required in nextState (currState will be a clone)
    // it already has 'time' property
    this.nextState.x = 120;
    this.nextState.y = 75;
    this.nextState.vx = 0;
    this.nextState.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.80,     // percentage bounce height (superball)
        friction = 0.98,  // rolling friction loss coeff
        px_mm = 2.0,      // 90dpi = 90px/25.4mm = 3.5 (slow it to 2)
        gravity = 0.0098 * px_mm, // gravity =9.8m/s/s =0.0098mm/ms/ms =0.0098*px/ms/ms
        xVel = 0,
        yVel = 0,
        x, y,
        vel, startAngle, timeInt, s, u;

    if (time == 0)   // generate random start condition for each reset
    {
      // restart at 0.8 m/sec initial velocity and at a random angle
      // speed m/s = speed mm/ms = speed mm/ms * px/mm = speed * px_mm
      vel = 1 * px_mm;    // px /ms
      startAngle = -(90 * Math.random() + 45);  // shoot upward
      this.nextState.x = 120;
      this.nextState.y = 75;
      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.time;   // 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 + 0.5*gravity * timeInt * 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 < topWall)
    {
      y = topWall;
      yVel *= 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 *= friction;
    }
    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.