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()'.

Using window.requestAnimationFrame

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 library

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

Each Animator 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 Animator object is created. The init method returns a reference to the object to be animated. This is stored in the Animator object as the property obj. The pathFn and drawFn are called in the scope of the Animator 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 Animator 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 small 'Timeline' JavaScript library. Each Timeline object has play(), stop(), pause() and step() methods which control a single Animator object or an array of Animator objects.

The current version of Timeline is 2v14, and the source code is available at Timeline-3v00.js.

Animation example using Timeline

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 Animator.path function in this example is the bouncingPath function. The path function requires a knowledge of the previous state to calculate the next. For this type of pathFn Timeline provides the 'currState' and 'nextState' objects in the scope of the 'path' function. All the newly calculated values that 'path' creates can be saved in this.nextState object. After each animated object is drawn the 'currState' and 'nextState' objects are swapped so that the 'as drawn' properties are available to the path function in 'this.currState'. The currState object should be treated as read-only, read it, make calculations and write values to be used to 'nextState' then apply the changes to the object to be drawn. The properties of the state vectors are user defined, the properties only need to be understood by the path and draw functions so a wide range of animation behaviours is possible.

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

Here is the code for the bouncing ball example which relies on the currState, nextState objects.

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 Animator object
    var reflect = -1,     // bounce off wall else disappear
        coeff = 0.80,     // percentage bounce height
        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 Animator(getBall, moveElem, bouncingPath);
  bb_tline = new Timeline(anim, 3000, false);
}

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