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

Canvas animation is simple - or is it? Just keep drawing the pictures. In this extract from JavaScript Bitmap Graphics With Canvas we look at how it works.

Now available as a paperback or ebook from Amazon.

JavaScript Bitmap Graphics
With Canvas

largecover360

 

Contents

  1. JavaScript Graphics
  2. Getting Started With Canvas
  3. Drawing Paths
      Extract: Basic Paths 
      Extract: Bezier Curves 
  4. Stroke and Fill
      Extract: Stroke Properties 
      Extract: Fill and Holes 
  5. Transformations
      Extract: Transformations
      Extract: Custom Coordinates **NEW!
  6. Text
      Extract: Text, Typography & SVG 
      Extract: Unicode 
  7. Clipping, Compositing and Effects
      Extract: Clipping & Basic Compositing 
  8. Generating Bitmaps
      Extract:  Introduction To Bitmaps
      Extract :  Animation 
  9. WebWorkers & OffscreenCanvas
      Extract: OffscreenCanvas
  10. Bit Manipulation In JavaScript
  11. Typed Arrays
  12. Files, blobs, URLs & Fetch
      Extract: Blobs & Files
      Extract: Read/Writing Local Files 
  13. Image Processing
      Extract: ImageData
      Extract: The Filter API
  14. 3D WebGL
  15. 2D WebGL
    Extract: WebGL Convolutions 

<ASIN:1871962625>

<ASIN:B07XJQDS4Z>

<ASIN:1871962579>

<ASIN:1871962560>

<ASIN:1871962501>

<ASIN:1871962528>

In book but not included in this extract:

  • The Image Object
  • Drawing an Image
  • ImageBitmap

Animation

One of the main uses of Canvas is to create simple animations. The principle of animation is well known. You show an image for a short time, change the image, show the new image and so on. How fast should you show new images? The answer is that one new image every 1/25th of a second generally creates the impression of movement, but in most cases programs try to show a new image every time the display frame is updated by the hardware. This the fastest rate possible and usually produces smooth animation.

JavaScript has a method that will call a function as soon as possible after each frame refresh. The requestAnimationFrame method will call a function that you supply so that it has the most time possible to update the graphics before the next paint update to generate the frame. The function is:

var id = requestAnimationFrame(animate);

The id returned can be used to cancel an animation using:

cancelAnimationFrame(id);

The function, called animate in this example, is passed a single parameter which is the time, usually in milliseconds, at the start of the frame as measured from the load of the web page.

The requestAnimationFrame method is simple enough to understand, but it does have some subtle points. The first is that the animate function has to end with a call to requestAnimationFrame if it wants to be called for each frame. That is, requestAnimationFrame runs the function just once at the start of the next frame. The amount of time that the function has to run is as much of the time between frames as the JavaScript engine can give it. After a frame finishes, the engine deals with all events and outstanding tasks and then calls the requestAnimationFrame function, i.e. animate in our example.

The function can run for as long as it likes, but if it runs for longer than the time to the next frame it misses the update and any changes are visible in the next frame after it finishes. If there are multiple requestAnimationFrame functions pending then they all get the same time stamp, which is just an indicator of the frame that they are supposed to be preparing for. As you might expect, if any of the functions take longer than a frame time then the next frame is skipped.

What all of this means is that it is advisable to keep your requestAnimationFrame functions down to one per application and keep its processing time short.

How short is short?

The simple answer is less than 16 ms.

If you try the following program:

time=[];
count=20;
function animate(t){
    count--;
    time.push(t);
    if(count==0){
        for(i=0;i<19;i++){
            console.log(time[i+1]-time[i]);
        }
            return;
    }
    requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

you will be able to see a sample of interframe times for the particular browser you are targeting.

For Chrome, at the time of writing, the results were mostly 16.66 milliseconds, which is close to the time between frames for 60 fps. However, occasionally you will see values of 50 ms and even 500 ms. These are caused by the browser having to do something else other than service your request. There is nothing you can do about these glitches and in the main they go undetected by users.

Draw or BitBlt?

There are two ways you can achieve a changing image between frames. You can draw your image using paths and so on or you can BitBlt a bitmap to the canvas. BitBlt, pronounced bit-blit, is short for bit-block operation and is often shortened to blit. This is essentially a low-level copy of a group of bits stored in memory to another area of memory and, as we have just discovered, we can do this with drawImage.

To compare the two methods let's animate a simple rotating square first using drawing methods and then by blitting.

To draw a rotating square all we need is to adjust the co‑ordinate system so that we are rotating about the center of the square, see Chapter 5.

var inc = 0.1;
var ctx = document.body.appendChild(
createCanvas(600, 600)).getContext("2d"); var path = new Path2D(); path.rect(-25, -25, 50, 50); ctx.translate(100, 100); requestAnimationFrame(animate);

The animate function simply clears the canvas, rotates the co‑ordinate system and draws the path:

function animate(t) {
  ctx.clearRect(-100, -100, ctx.canvas.width,
ctx.canvas.height); ctx.rotate(inc); ctx.fill(path); requestAnimationFrame(animate); }

If you try the program you will see a smoothly rotating square. There are some minor problems with this approach - mainly having to remember what the current co‑ordinate system is. Generally this can be avoided by using save and restore.


The same animation achieved by blitting needs a single sprite animation image. A sprite animation image is a single bitmap with a shape, the sprite, drawn in different positions for the animation. In this case 16 sub-images are sufficient:

rotcube

How this bitmap is made isn't really relevant. You could use a bitmap editor or you could write a program to generate it. The basic idea is load the bitmap and use requestAnimationFrame to show each cell of the animation in turn:

async function getImage(url) {
 function animate(t) {
     var cellx = 76;
     var celly = 76;
     var cellno = 16;
     ctx.clearRect(100, 100, cellx, celly);
     ctx.drawImage(img, cellx * i, 0, cellx, celly,
                              100, 100, cellx, celly);
     i++;
     i = i % cellno;
     requestAnimationFrame(animate);
 }
 var img = new Image();
 img.src = url;
 await imgLoaded(img);
 var i = 0;
 requestAnimationFrame(animate);
}

In this case the getImage function is now acting more like an asynchronous main program - which is what often happens. The animate function is defined within it so that it can access the local variables of getImage. The getImage function collects the sprite images and then calls requestAnimationFrame to start the animation. The variable i sets which cell of the bitmap is displayed.

If you try the program you will find that you get a smooth animation, although not quite as smooth as the first version. To get an even smoother animation would require more cells.

The key difference between the two approaches is that the drawing method takes as long to complete as it takes to render the paths involved. The BitBlt method always takes the same time, no matter how complex the sprite. The drawing method does have the advantage of being dynamically changeable, whereas in most cases blitting is restricted to the sprites that you have already created.



Last Updated ( Monday, 03 May 2021 )