Wednesday, April 29, 2015

HTML5 Canvas Drawing and Animation 101 (no frameworks!)

The other day I had some downtime and decided to mess around with drawing and animating with the canvas tag. As far as I can tell, unless your building a game or some 3D interactive thing on the web, you probably should opt for svg vector drawing and/or animation since they are way simpler.

For drawing with the canvas element, I recommend one of these great frameworks to ease development: EaselJS and Three.js.

For this experiment, I thought I'd just write vanilla JavaScript since I was just messing around and didn't want to spend a lot of time reading documentation. After a few hours, I end up with this neat little network of dots animation:


Below, I will talk through how I achieved this animation and show the code used.

First off, we need some JavaScript to add the canvas tag to the page. You can always just write the <canvas /> tag in the body of the page and give it an id, so whichever you prefer.
var canvas = document.createElement('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild(canvas);

Now for the heart of our application, the looper function. This is the function that will repeat over and over again and draw each frame of our animation, just like the frames of a movie. For this, it is recommended that we use the requestAnimationFrame() method in JavaScript instead of setInterval() since it is geared towards animating elements on the screen and will perform better/smoother. Plus, if the tab or window is inactive, the animation stops which is better for the computer's CPU.
var startTime = new Date().getTime(),
    lastTime = 0;

//start looping
requestAnimationFrame( looper );

function looper() {
    requestAnimationFrame( looper );

    //calculate current frame time and delta time
    //delta time - elapsed time since the last call to the looper method
    var now = new Date().getTime() - startTime,
        delta = (now - lastTime)/1000;

    //get the 2d context object
    var ctx = canvas.getContext('2d');

    //clear all graphics from the canvas
    ctx.clearRect(0 , 0 , canvas.width , canvas.height);

    //update game physics (animations)
    update( delta );

    //render everything back to the canvas
    draw( ctx );
    
    //save the current time for next loop
    lastTime = now;
}

//these 2 methods will be finished below
function update(delta) { }

function draw(ctx) { }


In the code above, I ended up not using delta time in my experiment, but if you are building a serious app or game be sure to take a look at some blog post about it: Fix Your Timestep. There is also quite a bit more code you'll need to make sure your game runs at a constant speed no matter what computer or browser it's running on.

Now to talk about what we are drawing/animating. In the picture above we have an array of orange circles (nodes) and a bunch of green lines (links) that are chasing each other around the screen. When one node catches it's target the link is broken and the node stops moving. In the code I represent each orange circle as a node agent. An agent is basically just an object that keeps track of the current values for one of the circles on the screen. This way, our draw function ends up being rather simple. Just loop over all node agents in the array and draw them on the screen.
//create network of nodes
var nodesArray = createNodes(200, canvas);

//This method will create an array of node agents to 
//be drawn on the screen
function createNodes(size, canvas) {
    var nodes = [];

    for(var i = 0; i < size; i++ ){
        //Here we create an agent at a random x, y position
        //with a random radius that is between 5px and 25px
        nodes.push(
            createNodeAgent(
                Math.random() * canvas.width,
                Math.random() * canvas.height,
                5 + (Math.random() * 20)
            )
        );
    }
    return nodes;
}
//This is a factory function that return a new node agent object
function createNodeAgent(x, y, r) {
    return {
        x : x,
        y : y,
        radius : r,
        //Here we give it the orange color and set the alpha
        //based on this agent's size (bigger means more opaque)
        color : "rgba(161,96,9," + r/30 + ")",
        link : -1
    };
}

With our array of nodes we can finish writing the draw method which will fill our canvas with a background color and draw each node agent on the screen.
function draw(ctx) {
    //fill screen with black background
    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

    //draw all nodes
    nodesArray.forEach(function (agent) {
        drawNode(ctx, agent);
    });
}
function drawNode(ctx, agent) {
    ctx.beginPath();
    ctx.arc(agent.x, agent.y, agent.radius, 0, 2 * Math.PI, false);
    ctx.fillStyle = agent.color;
    ctx.fill();
}

At this point, you should be able to run the app and see all the circles on the screen. Every time you refresh the screen they are placed at random spots on the screen which is pretty cool, but having motion will be way cooler so lets finish the update method.

One thing we need to do first though is figure out how to get the nodes to chase a target node. For this we will use one of the properties we added above to each node called "link". Link is going to be the index of another node in our master array. We could just pick one at random from the master array, but after a few test, it looked much nicer when nodes that are closest to each other are linked together. You end up with more of a networking effect this way.
//connect all nodes to the target node that it is closest to.
linkNodes(nodesArray);

function linkNodes(nodes) {
    nodes.forEach(function (agent, idx) {
        //check how far all other agents are from this agent
        var dists = nodes.map(function (agentToCheck, i) {
            return Math.abs(agent.x - agentToCheck.x) + Math.abs(agent.y - agentToCheck.y);
        });

        //find the next closest agent thats not already linked
        var closest = 0,
            dist = 99999;

        dists.forEach(function (distance, i) {
            if(distance < dist && nodes[i] !== agent && nodes[i].link === -1) {
                dist = distance;
                closest = i;
            }
        });
        agent.link = closest;
    });
}

I'll admit, this method isn't pretty and there is probably a way better way to do this but, whatever, it works so I'm rolling with it.

With this method, we have iterated through each node and gave it a target node to chase. Now we can finish the update method. The update method will slowly step each node towards it's target until it gets within a certain distance. When is distance is reached, the node will clear the link and stop moving.
function update(delta) {

    //nodes move towards the agent its connected to
    nodesArray.forEach(function (agent) {
        //if this agent still has a target, update position
        if(agent.link !== -1) {
            //find target node in master array
            var targetAgent = nodesArray[agent.link];
            
            //step this agent towards the target agent using
            //a simple easing function
            agent.x = chase(agent.x, targetAgent.x, 100);
            agent.y = chase(agent.y, targetAgent.y, 100);
            
            //check to see if we are next the our target node
            var distX = Math.abs(agent.x-targetAgent.x),
                distY = Math.abs(agent.y-targetAgent.y),
                min = (agent.radius + targetAgent.radius);

            //if we are then clear link and stop moving
            if(distX < min && distY < min){
                agent.link = -1;
            }
        }
    });
}
function chase(current, target, constant) {
    var change = (target - current) / constant;
    return current + change;
)

This is getting cooler! So when you run your app, you should see all the dot slowly chasing there targets around the screen. You can add all kinds of cool math here and see what happens, also if you haven't noticed, what we have here is the start of a basic Particle System. From here we can start looking into velocity and force and all kinds of neat things. But, in the spirit of not re-inventing the wheel, we should probably defer to a fully tested framework for more advanced topics.

Before I finish, you might be wondering where all the lines in the image at the top are. Lets revise our update method and add a function to draw the lines between each linked agent.
function draw(ctx) {
    //fill screen with black background
    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

    //first pass, draw all links
    nodesArray.forEach(function (agent) {
        if(agent.link !== -1) {
            var linkedAgent = nodes[agent.link];
            drawLink(ctx, agent, linkedAgent);
        }
    });

    //second pass, draw all nodes
    nodesArray.forEach(function (agent) {
        drawNode(ctx, agent);
    });
}

function drawLink(ctx, agent, linkedAgent) {
   ctx.beginPath();
   ctx.moveTo(agent.x, agent.y);
   ctx.lineTo(linkedAgent.x, linkedAgent.y);

   //line style
   ctx.lineWidth = 5;
   ctx.lineCap = 'round';
   ctx.strokeStyle = 'rgba(161,174,20,0.5)';

   //draw
   ctx.stroke();

   //cleanup
   ctx.lineWidth = 0;
}

Happy Coding!