Animating objects using HTML5 Canvas

Object animation using JavaScript is fairly simple once you get the hang of it. In the three previous posts I've explained how to render shapes and how to render a series of shapes. To recap some of the steps I've included the part where I create the objects.


Creating classes

I've created a generic shape class called CShape, with a set of basic properties that I find relevant for any of the shapes I'm going to render. This also includes properties relevant to animation. Each property is explained in the comments below.

// Applies to circles, boxes and lines
// X and Y coordinates
// expand (bool) growing (true) or shrinking (false)
// direction - animating going cw or ccw
// speed - animation speed
// borderThickness - border thickness
// borderColor - border color
// color - shape color
// clickZone - clickable zone with pointer cursor
// speed - animation speed
function CShape() {
    this.x = 0;
    this.y = 0;
    this.expand = true;
    this.speed = 0;
    this.borderThickness = 1;
    this.borderColor = "#FFFFFF";
    this.color = "#000000";
    this.delay = false;
    this.runAnimation = false;
}

Further on I've extended the CShape creating another class called CCircle. Here I've added the properties minRaduis and maxRadius as these only apply to circular shapes. The radius values will be used for animating references, growing or shrinking.

CCircle.prototype = new CShape();
CCircle.constructor = CCircle;
// Circle object - prototyped CShape
// Adds min and max radius. 
function CCircle() {
    this.minRadius = 0;
    this.maxRadius = 0;
}

A bit to the side, but indeed relevant to the mission we add a factor for resizing. You might know that resizing a canvas without rerendering the content might give you a poor result, so we'll do a calculation here and multiply all positions and sizes with it. I'll come back to this in a future blog post. Maybe even the next.

var ic = window.innerWidth / (canvas.width * 1.2) > 1 ? 1 : window.innerWidth / (canvas.width * 1.2);

Creating objects

Now I am ready to create some shapes. For this demo I'll create a series of small circles like you can see on my home page. I add them straight away to the array to save some steps, but you can create the objects based on the classes and then add them later if you wish.

var customShapes = [];

// Figures - Balls
customShapes.push({ x: ic * (canvas.width / 2 - 58),  minRadius: 0, maxRadius: ic * (3), y: ic * (canvas.height / 4 - 152), borderThickness: ic * (11), borderColor: ringBlue, color: ringBlue, runAnimation: !0, speed: 2.4 });
customShapes.push({ x: ic * (canvas.width / 2 - 117), minRadius: 0, maxRadius: ic * (3), y: ic * (canvas.height / 4 - 102), borderThickness: ic * (3),  borderColor: ringBlue, color: ringBlue, runAnimation: !0, speed: 3 });
customShapes.push({ x: ic * (canvas.width / 2 + 43),  minRadius: 0, maxRadius: ic * (3), y: ic * (canvas.height / 4 - 150), borderThickness: ic * (7),  borderColor: ringBlue, color: ringBlue, runAnimation: !0, speed: 1 });
customShapes.push({ x: ic * (canvas.width / 2 + 138), minRadius: 0, maxRadius: ic * (3), y: ic * (canvas.height / 4 - 120), borderThickness: ic * (15), borderColor: ringBlue, color: ringBlue, runAnimation: !0, speed: 1.4 });
customShapes.push({ x: ic * (canvas.width / 2 + 195), minRadius: 0, maxRadius: ic * (3), y: ic * (canvas.height / 4 + 53),  borderThickness: ic * (10), borderColor: ringBlue, color: ringBlue, runAnimation: !0, speed: 4 });
customShapes.push({ x: ic * (canvas.width / 2 + 99),  minRadius: 0, maxRadius: ic * (2), y: ic * (canvas.height / 4 + 152), borderThickness: ic * (6),  borderColor: ringBlue, color: ringBlue, runAnimation: !0, speed: 1.4 });
customShapes.push({ x: ic * (canvas.width / 2 - 75),  minRadius: 0, maxRadius: ic * (2), y: ic * (canvas.height / 4 + 232), borderThickness: ic * (26), borderColor: ringBlue, color: ringBlue, runAnimation: !0, speed: 2.9 });
customShapes.push({ x: ic * (canvas.width / 4 + 60),  minRadius: 0, maxRadius: ic * (2), y: ic * (canvas.height / 4 + 86),  borderThickness: ic * (12), borderColor: ringBlue, color: ringBlue, runAnimation: !0, speed: 5.4 });

Rendering shapes

Now it is time for some logic. We need a function that can render a circle based on these objects. The function below takes an object, an id and the canvas context as an argument. I choose to send in the context because I want to use the same function rendering on several canvas elements in one page. Other than that the function pretty much does what it says.

// Renders a circle based on a Circle object
function drawCircle(tmpShape, id, renderContext) {
    var tmpCircle = new CCircle();
    tmpCircle = tmpShape;
    var centerY = tmpCircle.y;
    var centerX = tmpCircle.x;
 
// These two lines are relevant to the rendering speed. 
// As long as the minRadius is less than the maxRadius it

// will grow according the the value of the speed property

    if (tmpCircle.minRadius <= tmpCircle.maxRadius) {
        tmpCircle.minRadius = tmpCircle.minRadius + tmpCircle.speed;
    }
    var radius = tmpCircle.minRadius;
    renderContext.fillStyle = tmpCircle.color;
    renderContext.lineWidth = tmpCircle.borderThickness + (tmpCircle.minRadius * 0.01);
    renderContext.strokeStyle = tmpCircle.borderColor;
    renderContext.save();
    renderContext.beginPath();
    renderContext.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
    renderContext.fill();
    renderContext.stroke();
    renderContext.restore();
    }

Animating the shapes

Oki, so now we can draw a circle. Now I'll add two functions. One for running the rendering based on the kind of shape, and one that runs through the array.

// Loops through the custom shape array calling renderShapes on every shape
function animationLoop() {
    var i = customShapes.length;
    while (i-- && i >= 0) {
        var customShape = customShapes[i];
  // Calling the rendering function
        renderShapes(customShape, i, context);
        if (customShapes[20].minRadius >= customShapes[20].maxRadius) {
 //checking if the shape should continue animating in the next iteration 
            keepAminationAlive = false;
        }
    }
    if (keepAminationAlive == true) {
  // if the shape should be reredered, the function is called again
        setTimeout(animationLoop, 50);
    }
}

Now, for each object in the previous function we need to know which kind of shape to render. This is simply done by checking if the object contains one of the unique shape properties applied to the shape object. Since we are only rendering circles in this demo, this is not a problem. However, if your animation is based on several shapes like on my site (www.engvoldsen.net) - you have to tend to this.

// Renders one figure based on the type of figure, testing on figure specific properties. 
function renderShapes(tmpShape, id, renderContext) {
    if (!isEmpty(tmpShape)) {
        // if circle
        if (tmpShape.hasOwnProperty("maxRadius")) {
            if (tmpShape.runAnimation == true) {
                drawCircle(tmpShape, id, renderContext);
            }
        }
            // if line
        else if (tmpShape.hasOwnProperty("length")) {
            if (tmpShape.runAnimation == true) {
                drawLine(tmpShape, renderContext);
            }
        }
            // if box
        else if (tmpShape.hasOwnProperty('width')) {
            drawBox(tmpShape, renderContext);
        } else
            return;
    }
}

Kicking of the animationLoop should give you a decent animation experience ;)

Comments

Popular posts from this blog

Designing and programming - Part 2

Filtering Dropdown choices in a Power Pages form using Dataverse Relations

Exploring the Power of Variables in Liquid and Power Pages