JavaScript Canvas - Animation
Written by Ian Elliot   
Monday, 03 May 2021
Article Index
JavaScript Canvas - Animation
Speed
Full Program

The Problem of Speed

In both of the examples given above the amount of rotation is fixed at each update, which is fine as long as each update takes the same amount of time. If the update rate changes then you need to modify the amount of rotation to keep the speed constant. To do this you have to take into account the time since the last frame. Essentially you have to use a velocity variable and multiply by the time since the last frame to discover how far the object has moved. For example, to make the rotating square take account of frame rate, the animate function has to be modified to:

 var pt = 0;
 var inc = 0.1/16;
 function animate(t) {
   if (pt === 0) {
      pt = t;
      requestAnimationFrame(animate);
      return
   }
 ctx.clearRect(-100, -100, ctx.canvas.width,
ctx.canvas.height); var dt=t-pt; pt=t; ctx.rotate(inc*dt); ctx.fill(path); requestAnimationFrame(animate); }

You can see now that inc is the amount that the square rotates in one millisecond. Notice that pt has to be a global variable to retain its value between function calls, although there are other ways of achieving this.

When it comes to varying frame rates, due to the browser having to attend to other matters such as garbage collection or updating other graphical elements, there are two approaches. Firstly, you can simply show the same movement per frame. In this case when a frame pauses your animation pauses. Alternatively, you can use the time between frames to alter the amount of distance moved. In this case when a frame pauses, your animation seems to jump forward to the correct position. Neither effect is particularly good from the user’s point of view.

largecover360

Position, Speed & Acceleration

In many cases it is good to think of what you are animating as having a position, a speed and an acceleration. This is ideal for implementation as an object - usually called a sprite. There are many different ways of organizing this idea, but the basics are to create an object with a position, velocity and acceleration property. After this things vary, but the object usually has an update method which modifies its position based on its velocity and its velocity based on its acceleration.

The big problem is trading elegant design for efficiency. Let's start off with good design and see how much it costs in terms of performance. The simplest test of animation is bouncing a ball around the screen - it is the hello world of animation. In this case we are going to create a ball sprite and use this to animate any number of bouncing balls.

First we need some objects to hold position, velocity and acceleration.

function Pos(x, y) {
  this.x = x;
  this.y = y;
}
function Vel(x, y) {
  this.x = x;
  this.y = y;
}
function Acc(x, y) {
  this.x = x;
  this.y = y;
}

 

Now we can create the Ball constructor:

function Ball(pos, vel, acc, r) {
  this.pos = pos;
  this.vel = vel;
  this.acc = acc;

The r variable isn't being stored as a property, but the methods of the object can access it via closure.

The first method, and the most complicated is the update method:

this.update = function () {
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
this.vel.x += this.acc.x;
this.vel.y += this.acc.y;

As the ball is going to bounce when it hits the edges of the canvas we can include this in the update method:

 if (this.pos.x + r > ctx.canvas.width) {
     this.pos.x = ctx.canvas.width - r;
     this.vel.x = -this.vel.x;
 }
 if (this.pos.y + r > ctx.canvas.height) {
     this.pos.y = ctx.canvas.height - r;
     this.vel.y = -this.vel.y;
 }
 if (this.pos.x - r < 0) {
     this.pos.x = r;
     this.vel.x = -this.vel.x;
 }
 if (this.pos.y - r < 0) {
     this.pos.y = r;
     this.vel.y = -this.vel.y;
 }
};

We also need a render method which will draw the ball at its current location:

 this.render = function () {
                var path = new Path2D();
                path.arc(this.pos.x, this.pos.y, r, 0, 2 * Math.PI);
                ctx.fill(path);
              };
}

This uses a path to draw a filled circle. Later we will modify the program to use a bitmap.

Having defined the ball sprite we still need to animate it. There could be many sprites involved in the animation and so we need a general Animation object that will look after all of them. There usually only needs to be one animation object so it can be constructed as a singleton:

Animation = {};
Animation.spriteList = [];

The spriteList contains all of the sprites to be animated.

We need two methods to implement the animation - clearCanvas:

Animation.clearCanvas = function () {
            ctx.clearRect(0, 0, ctx.canvas.width,
                                   ctx.canvas.height);
                         };

and run:

Animation.run = function (t) {
     Animation.frameRate(t);
     Animation.clearCanvas(t);
     for (var i = 0;i<Animation.spriteList.length;i++){
                Animation.spriteList[i].update();
                Animation.spriteList[i].render();
     }
     requestAnimationFrame(Animation.run);
 };

Notice that every sprite has to have an update and a render method. As we want to see how fast all of this works, there is also a frameRate method which updates an input tag with id fps:

Animation.frameRate = function (t) {
    if (typeof t !== "undefined") {
         Animation.frameRate.temp = 0.8 *
           Animation.frameRate.temp + 0.2 *
                 (t - Animation.frameRate.tp);
                          Animation.frameRate.tp = t;
      }
      Animation.frameRate.count++;
      if (Animation.frameRate.count === 120) {
         fps.value=(1000/Animation.frameRate.temp)
                                        .toFixed(2);
         Animation.frameRate.temp = 0;
         Animation.frameRate.count = 0;
       }
  };
Animation.frameRate.count = 0;
Animation.frameRate.tp = 0;
Animation.frameRate.temp = 0;

The frame rate is calculated by taking the difference in the time each frame starts. This is used to form a running weighted average in temp. Every 120 frames the form is updated to show the new frameRate.

Now all we need is the main program to use the ball object:

var ctx = document.body.appendChild(createCanvas(600, 600)).getContext("2d");
var noBalls = 3;
var balls = [];
for (i = 0; i < noBalls; i++) {
  balls[i] = new Ball(new Pos(Math.floor(Math.random()
* 250), Math.floor(Math.random() * 250)), new Vel(Math.floor(Math.random() * 10) – 5, Math.floor(Math.random() * 10) – 5), new Acc(0, 0.1), 20); } Animation.spriteList = balls; Animation.run();

A for loop is used to create an array of ball instances with random starting points and velocities. All of the balls have an acceleration of 0,0.1 which makes them appear to fall and bounce.



Last Updated ( Monday, 03 May 2021 )