Canvas: Animation

on in Canvas
Last modified on

This is a Canvas programming introductory series:

Table of Contents

Drawing one picture can seem like a rather daunting task. The main difficulty with animation is the need to update the display of the Canvas content quickly enough so that it looks like it is naturally moving or changing.

Animation is an obvious and necessary ingredient for certain types of applications, such as real-time games and physics emulators. But for a much wider range of Canvas pages, simpler animation types are better. For example, animation can be used to highlight user actions, such as highlighting a shape when the mouse cursor is over it, or making it pulsate or flicker.

Animation can also be used to draw attention to changes in content, such as gradually introducing a new look or making graphs and charts “grow” to a desired position. These uses of animation are a powerful way to add gloss to web applications.

Simple Animation

Making an animation from an element on an HTML5 Canvas is easy enough. To do this, a timer is set that constantly calls the element, usually 30 or 40 times per second. On each call, the code completely updates the contents of the entire Canvas. If the code is written correctly, constantly changing frames will merge into a smooth, realistic animation.

JavaScript provides two ways to manage this repetitive Canvas content update:

setTimeout() Function

This function instructs the browser to wait a few milliseconds and then execute a piece of code, in this case the code to update the contents of the Canvas. When the code finishes executing, the setTimeout() function is executed again, calling the Canvas update code again, and so on until the animation needs to end.

setInterval() Function

This function instructs the browser to execute a piece of code at a regular interval, such as every 20ms. The effect of this function is basically the same as that of the setTimeout() function, but the setInterval() function only needs to be called once. To stop the browser from calling the code repeatedly, the clearInterval() function is executed.

requestAnimationFrame() Function

The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation before the next repaint. The method takes a callback as an argument to be invoked before the repaint. This is the smoothest of all three and supported by all modern browsers.

This article is part of the JavaScript Canvas series where I post experiments, JavaScript generative art, visualizations, Canvas animations, code snippets and how-to’s.

JavaScript Canvas Logo
let canvas;
let context;

window.onload = () => {
    canvas = document.getElementById('my-canvas');
    context = canvas.getContext('2d');

    window.requestAnimationFrame(drawFrame);
}

Let’s say we want to create an animation of a square falling down from the top of the page. To do this, we need to track the position of the square using two global variables. After that, we just need to change the position each time the drawFrame() function is executed, and then redraw the square in a new position:

let squarePosition_x = 10;
let squarePosition_y = 0;

function drawFrame() {
    context.clearRect(0, 0, canvas.width, canvas.height);

    context.beginPath();

    context.rect(squarePosition_x, squarePosition_y, 10, 10);
    context.lineStyle = '#109bfc';
    context.lineWidth = 1;
    context.stroke();

    squarePosition_y += 1;

    window.requestAnimationFrame(drawFrame);
}

The result of this code will be a square that falls from the top edge of the Canvas and disappears after passing through the bottom edge.

More sophisticated animation will require more complex calculations. For example, the previous example can be a bit more complicated by making the square fall faster to emulate gravity, or bouncing off the “floor”, which would require more complex maths than a uniform downward motion. But the basic approach — setting a timer, calling the draw function, and updating the contents of the entire Canvas — remains exactly the same.

Animation of Multiple Objects

Now that we’ve mastered the basics of animation and drawing interactive graphics on Canvas, it’s time to take the next step and combine these skills. We will do this using the example of the animation program for falling “balls”, as shown in the figure below:

This animation program allows you to add an unlimited number of balls to the Canvas. The program provides an option to choose the size of the ball (the default radius is 15 pixels) and connect the balls with lines, as shown on the right in the figure. Each ball added to the Canvas moves independently of the others, falling with acceleration until it hits the bottom edge of the Canvas, then bounces off it and starts moving in the other direction.

Add some basic markup for the example:

<canvas id="my-canvas" width="600" height="400"></canvas>
<p>
    <button onclick="addBall()">Add Ball</button>
    <button onclick="clearBalls()">Clear Balls</button>
</p>
<div>
    Ball size <input id="ballSize" type="number" min="0" max="50" value="15">
    <input id="connectedBalls" type="checkbox">Show wireframe<br>
</div>

We’ll use a custom object to manage all of these balls. In this case, we need to keep track of an array of Ball objects, and in addition to the position (represented by the x and y properties) for each ball, we also need to track the speed (represented by the dx and dy properties):

function Ball(x, y, dx, dy, radius) {
    this.x = x;
    this.y = y;
    this.dx = dx;
    this.dy = dy;
    this.radius = radius;
    this.strokeColor = 'black';
    this.fillColor = 'red';
}

let balls = [];

When the “Add Ball” button is clicked, this simple code creates a new Ball object and stores it in the balls array:

function addBall() {
    let radius = parseFloat(document.getElementById('ballSize').value);

    let ball = new Ball(50, 50, 1, 1, radius);

    balls.push(ball);
}

In addition to clearing the Canvas, the “Clear Canvas” button also clears the balls array:

function clearBalls() {
    balls = [];
}

But neither the addBall() function nor the clearBalls() function not only don’t actually draw anything, they don’t even call a function to draw. Instead, the page code is designed to call the drawFrame() function:

window.onload = () => {
    canvas = document.getElementById('drawingCanvas');
    context = canvas.getContext('2d');

    window.requestAnimationFrame(drawFrame);
}

The drawFrame() function is the key part of this example. It not only draws the balls on the Canvas, but also calculates their current position and speed. The drawFrame() function performs several calculations to more realistically emulate the movement of balls, such as speeding up balls when they fall and slowing down when they bounce off obstacles. The complete function code looks like this:

function drawFrame() {
    context.clearRect(0, 0, canvas.width, canvas.height);

    context.beginPath();

    for (let i = 0; i < balls.length; i++) {
        let ball = balls[i];
        ball.x += ball.dx;
        ball.y += ball.dy;

        // Add a "gravity" effect that speeds up the fall of the ball
        if ((ball.y) < canvas.height) {
            ball.dy += 0.22;
        }

        // Add a "friction" effect that slows down the ball
        ball.dx = ball.dx * 0.998;

        // If the ball hit the edge of the Canvas, bounce it off
        if ((ball.x + ball.radius > canvas.width) || (ball.x - ball.radius < 0)) {
            ball.dx = -ball.dx;
        }

        // If the ball fell down, bounce it, but slightly reduce the speed
        if ((ball.y + ball.radius > canvas.height) || (ball.y - ball.radius < 0)) {
            ball.dy = -ball.dy * 0.96; 
        }

        // Check if wireframes should be used
        if (!document.getElementById('connectedBalls').checked) {
            context.beginPath();
            context.fillStyle = ball.fillColor;
        } else {
            context.fillStyle = 'white';
        }

        // Draw the ball
        context.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
        context.lineWidth = 1;
        context.fill();
        context.stroke(); 
    }

    window.requestAnimationFrame(drawFrame);
}

The code performs the following operations:

  1. Clears the Canvas.
  2. Loops through the balls in the array.
  3. Adjusts the position and speed of each ball.
  4. Draws each ball on the Canvas.
  5. Calls the drawFrame() method via requestAnimationFrame().

The tricky part here is step 3, which adjusts the position and speed of the balls. The level of complexity of this code can be whatever the developer wants this code, depending on the effects that he is trying to implement. Smooth, natural movement is especially difficult to model, requiring more complex mathematical calculations.

Finally, now that we can keep track of each ball, it’s easy to add interactivity. In fact, you can use almost the same code that was used to detect circle clicks in the circle drawing program from the previous article. But only in this case, we want to do something different with the clicked ball, like speed it up or change its direction:

window.onload = () => {
    ...

    canvas.onmousedown = canvasClick; 

    ...
}

function canvasClick(e) {
    let clickX = e.pageX - canvas.offsetLeft;
    let clickY = e.pageY - canvas.offsetTop;

    for (let i in balls) {
        let ball = balls[i];

        if ((clickX > (ball.x - ball.radius)) && (clickX < (ball.x + ball.radius))) {
            if ((clickY > (ball.y - ball.radius)) && (clickY < (ball.y + ball.radius))) {
                ball.dx -= 2;
                ball.dy -= 3;

                return;
            }
        }
    }
}

A rather impressive version of this example can be explored on the JavaScript Canvas Bouncing Balls demo. Here, hovering over the balls scatters them in different directions (how exactly depends on how the cursor is hovering over them), and if you move the cursor away, the balls’ position is then restored.

See the Pen JavaScript Canvas Bouncing Balls by Ciprian (@ciprian) on CodePen.

Related posts