html5 canvas and the flying dots

Aug. 22, 2010
comments

The header of this page features a couple flying dots in the Grey strip. They are drawn using a html5 feature called "canvas". Canvas is pretty cool, it makes a lot of things possible for which you had to use flash previously. This post is about how this works including lots of code and math.

In case you have a browser that does not display the canvas (how about getting one that works?) I have included this reference picture of how it looks.

You can download the code for this effect, see it running on my website (about 20cm up and 15cm right from here), see a standalone demo or embed it on your webpage with this code:

<iframe
  scrolling   = "no"
  style       = "width: 200px; height: 200px;"
  src         = "http://codeflow.org/misc/gravity.html">
</iframe>

Physics

The physics underlying the code is an n-body gravity simulation. There are 15 dots on the screen, and each has a velocity and pulls on each other dot, thus changing their velocities.

A suitably fast integration method has to be used (javascript isn't very fast) and the Euler method fits that bill. It works by stepping trough the simulation and at each step calculating the incremental change (to accelerations, velocity and position). The method is not very precise, but it's convincing enough.

Flying dot universe

A data model of the flying dot universe needs to contain all particles as well as their position, velocity and cumulative accelerations. A vector has two attributes, the x and y coordinate.

Universe = many particles

Particle

  • position (Vector)
  • velocity (Vector)
  • acceleration (Vector)

Vector

  • x (number)
  • y (number)

Short illustrated primer on javascript objects

Objects are wonderful, they let you express complex things more simply, or make simple things more complex.

To make objects in javascript works like this

var MyObject = function(){
    var a_private_variable = 1;
    
    this.a_public_attribute = 2;
    
    this.some_method = function(){
    };
}

var a_new_instance = new MyObject();

Meet the vector

In order to work with vectors, it is handy to have a little helper. Common operations you do with vectors are

  • addition
  • substraction
  • length after Pythagoras
  • division by a scalar

Expressed in code it looks like this

var Vector = function(x, y){
    this.x = x;
    this.y = y;

    this.sub = function(other){
        return new Vector(
            this.x - other.x,
            this.y - other.y
        );
    }
    this.isub = function(other){
        this.x -= other.x;
        this.y -= other.y;
    }
    this.iadd = function(other){
        this.x += other.x;
        this.y += other.y;
    }
    this.length = function(){
        return Math.sqrt(this.x*this.x + this.y*this.y);
    }
    this.idiv = function(scalar){
        this.x /= scalar;
        this.y /= scalar;
    }
    this.zero = function(){
        this.x = 0;
        this.y = 0;
    }
    this.validate = function(){
        if(isNaN(this.x+this.y)){
            this.x = 0;
            this.y = 0;
        }
    }
}

As a side note, the methods idiv, iadd and isub are in-place methods, whereas the sub method makes a new copy. This is handy to tune performance a bit. Two other convenience functions found in the Vector are "zero" which sets the vector in-place to zero, and "validate" which checks if the vector has become invalid and resets it to zero. For usage examples see the snippet below:

var vec1 = new Vector(1, 2);
var vec2 = new Vector(3, 4);
var vec3 = vec1.sub(vec2);
vec1.idiv(10)
var length = vec3.length();

The particle

The particle has to manage its acceleration, velocity and position. It has two methods: "step" which runs one step of this particles simulation and "draw" which draws the particle, here's the full code:

var Particle = function(canvas){
    var initial_speed = 1;
    var speed_limit = 4;
    var bounce_damping = 0.5;

    this.acceleration = new Vector(0, 0);
    this.velocity = new Vector(
        Math.random() * initial_speed - initial_speed * 0.5,
        Math.random() * initial_speed - initial_speed * 0.5
    )
    this.position = new Vector(
        Math.random() * canvas.width,
        Math.random() * canvas.height

    )

    this.step = function(){
        this.acceleration.validate();
        this.velocity.iadd(this.acceleration);

        speed = this.velocity.length();
        if(speed > speed_limit){
            this.velocity.idiv(speed/speed_limit);
        }
        this.position.iadd(this.velocity);
        this.acceleration.zero();

        // border bounce
        if(this.position.x < 0){
            this.position.x = 0;
            this.velocity.x *= -bounce_damping;
        }
        else if(this.position.x > canvas.width){
            this.position.x = canvas.width;
            this.velocity.x *= -bounce_damping;
        }

        if(this.position.y < 0){
            this.position.y = 0;
            this.velocity.y *= -bounce_damping;
        }
        else if(this.position.y > canvas.height){
            this.position.y = canvas.height;
            this.velocity.y *= -bounce_damping;
        }

    }
    this.draw = function(context){
        context.beginPath();
        context.arc(
            this.position.x, this.position.y,
            2.5, 0, Math.PI*2, false
        );
        context.fill();
    }
}

Particle step

It is possible that the acceleration has approached infinity, for this we do:

this.acceleration.validate();

Then the simulation modifies the existing velocity with the acceleration:

this.velocity.iadd(this.acceleration);

We need to have a traffic cop keeping watch on the speed of the particles (or some may get hurt!), so they are not to move more then 4 pixels per step.

speed = this.velocity.length();
if(speed > speed_limit){
    this.velocity.idiv(speed/speed_limit);
}

Note the line that uses idiv on the velocity. This is a trick to scale a vector to a desired length. Ordinarily you'd normalize the vector with vec.idiv(length) and then multiply it with the desired length vec.imul(desired). Resolving that formula you can spare yourself one vector operation by dividing the length by the desired length in order to scale a vector to it.

Next we step the position of the particle by simply:

this.position.iadd(this.velocity);

Since the accelerations that have acted on the particle are now processed, we can reset them for the next run

this.acceleration.zero();

Since we do not want the particle to fly out of the borders of the canvas element, and the simulation is accumulating energy in the system (remember the inaccurate integration method) we bounce them off the borders like this:

if(this.position.x < 0){
    this.position.x = 0;
    this.velocity.x *= -bounce_damping;
}

Drawing the Particle

this.draw = function(context){
    context.beginPath();
    context.arc(
        this.position.x, this.position.y,
        2.5, 0, Math.PI*2, false
    );
    context.fill();
}

Context is the drawing context derived from canvas (see more on that in the section about the System). The method "beginPath" instructs canvas to begin a new shape. The method "arc" draws a circle at the x and y positions of the Particle, it has a radius of 2.5 pixels and the circle is filled (2PI is one full circle in radians) in clockwise fashion (the false value).

System of a flying dot

The last class required to setup the flying dots is the System. It has the job of managing the n-body simulation. This is where the magic happens.

var System = function(amount, milliseconds){
    var factor = 9;
    var min_proximity = 4;

    var canvas = document.getElementById('particles');
    var context = canvas.getContext('2d');
        
    var particles = [];
    for(var i=0; i<amount; i++){
        particles.push(new Particle(canvas));
    }

    setInterval(function(){
        // fading
        context.globalCompositeOperation = 'source-in';
        context.fillStyle = 'rgba(128,128,128,0.85)';
        context.fillRect(0, 0, canvas.width, canvas.height);

        // dot drawing style
        context.globalCompositeOperation = 'lighter';
        context.fillStyle = 'rgba(128,128,128,0.5)';

        // nbody code acceleration accumulation
        for(var i=0, il=amount; i<il; i++){
            var a = particles[i];
            for(var j=i+1; j<amount; j++){
                var b = particles[j];
                var vec = a.position.sub(b.position);
                var length = vec.length();
                vec.idiv(Math.pow(length, 3)/factor);

                // safeguard for execessive integration error
                if(length > min_proximity){
                    b.acceleration.iadd(vec);
                    a.acceleration.isub(vec);
                }
            }

            a.step();
            a.draw(context);
        }
    }, milliseconds);
}

The canvas is setup by getting the canvas element from the document and requesting a 2d drawing context from it:

var canvas = document.getElementById('particles');
var context = canvas.getContext('2d');

Then we create all the particles:

var particles = [];
for(var i=0; i<amount; i++){
    particles.push(new Particle(canvas));
}

Stepping the simulation

The setInterval method accepts a function as its first parameter and the delay in milliseconds in which to repeatedly call this function. The first thing that needs to happen at each step is to fade the whole picture a bit, this makes the pretty trails possible. The "source-in" blending mode and a Grey color with 15% transparency acomplish this:

context.globalCompositeOperation = 'source-in';
context.fillStyle = 'rgba(128,128,128,0.85)';
context.fillRect(0, 0, canvas.width, canvas.height);

We need to setup the drawing mode for the particles such that they are drawn additively with a Grey color and semi transparently:

context.globalCompositeOperation = 'lighter';
context.fillStyle = 'rgba(128,128,128,0.5)';

Since all particles interact with each other, accumulating the accelerations for N particles would incur N*N operations. With 15 particles that would be 225 operations. We can cut down on that a bit by doing two loops that ensures that we have calculated the accelerations between each particle and add/subtract it from each pair of particles.

for(var i=0, il=amount-1; i<il; i++){
    var a = particles[i];
    for(var j=i+1; j<amount; j++){
        var b = particles[j];
    }
}

The accumulation of accelerations follows Newton's law of universal gravitation which states that

„[Text(Every point mass attracts every single other point mass by a force pointing along the line intersecting both points. The force is directly proportional to the product of the two masses and inversely proportional to the square of the distance between the point masses.)]“

So we first need the distance and direction between these two particles:

var vec = a.position.sub(b.position);
var length = vec.length();

Then we want to scale the vector to a length corresponding to the inverse square of the distance, using the trick of resolving that formula we can do this again in one vector operation:

vec.idiv(Math.pow(length, 3)/factor);

Lastly the result is added to one acceleration Vector and subtracted from the other. But this is only done if the particles are more then 4 pixels apart because at very close distances, the integration method would yield undesirably large accelerations.

// safeguard for execessive integration error
if(length > min_proximity){
    b.acceleration.iadd(vec);
    a.acceleration.isub(vec);
}

Then the particle is stepped and drawn

a.step();
a.draw(context);

Can I go home now?

Almost, just one more. we need to instantiate that system, I usually do that in some main function:

var main = function(){
    var system = new System(15, 40);
};

which I call in the onload handler of the body of the document:

<body onload="main()">

That's it, I hope you enjoyed this howto, see you for the next one.