HTML5 Canvas Particles Revisisted — With Physics!

Posted by in Code

While I wrote a few years ago about HTML5 canvas particle effects, I’ve gotten a lot of questions about the effect I have on my new personal site. No, not the crazy swirling thing on the left—we’ll go over that another time—but the fun little bouncy ball thing. Go ahead and check it out. I’ll wait.

Pretty cool, right? It’s written entirely without any framework or library like jQuery or D3.js or Three.js. While they’re pretty handy for compatibility, data visualization, and comprehensive 3D graphics rendering (respectively), I didn’t have much of a need for something so straightforward. Plus, it makes it easier to understand the mechanics of it all when I write about it.

Which is right now. (Duh doy.)

The Setup

HTML-wise, this isn’t terribly complex. In fact, all we need is a canvas to draw on. Like, literally.

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en-us">
    <head>
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title>Weeeee!</title>
    </head>
    <body>
        <canvas id="bubbles"></canvas>
    </body>
</html>

For styles, we can keep it simple, too. All we want is to make sure the canvas is where we want it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
html, body, #bubbles {
    border: 0;
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
}

body {
    background: linear-gradient(to bottom, rgba(91,91,91,1) 0%, rgba(58,58,58,1) 100%);
}

/* Only if you want the hover effect */
#bubbles {
    opacity: 0;
    transition: opacity 0.8s ease-in-out;
}
#bubbles:hover {
    opacity: 1;
}

The JavaScript

While not terribly complex, let’s break it down into bite-sized chunks.

1
2
3
4
5
6
7
8
var canvas = document.querySelector('#bubbles'),
    context = canvas.getContext('2d'),
    particles = [],
    last_draw = undefined,

    MAX_PARTICLES = 100,
    CANVAS_WIDTH = 0,
    CANVAS_HEIGHT = 0;

All this does is instantiate and set the necessary variables. canvas is the DOM node, context is the drawing context, particles is where we’ll track all the particles we create, and last_draw will be used for requestAnimationFrame() calculations.

The rest of the variables are constants for drawing. They should mostly speak for themselves.

1
2
3
4
5
6
7
8
function setDimensions(e) {
    CANVAS_WIDTH = canvas.clientWidth;
    CANVAS_HEIGHT = canvas.clientHeight;
    canvas.width = CANVAS_WIDTH;
    canvas.height = CANVAS_HEIGHT;
}
setDimensions();
window.addEventListener('resize', setDimensions);

This is how we make sure the canvas stays the right size. The CSS styles will make sure the canvas stays full, but you need to set the HTML attributes to make sure the drawing functions properly. It’ll run every resize event.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
function Particle() {
    var self = this,
        pos_x = 0,
        pos_y = 0,
        vel_x = 0,
        vel_y = 0,
        acc_x = 0,
        acc_y = 1.5,
        color = 0,
        radius = 0;

    this.reset = function(){
        // Centralize position
        self.setPosX(CANVAS_WIDTH / 2);
        self.setPosY(CANVAS_HEIGHT / 2);

        // Randomize velocities
        self.setVelX(Math.random() * 40 - 20);
        self.setVelY(Math.random() * -30);

        // Also randomize the radius
        self.setRadius(Math.random() * 15);

        // And finally the color
        self.setColor(Math.random() * 360);
    };

    this.setColor = function(c){
        if (typeof(c) === 'number') {
            color = c;
        }
    };
    this.getColor = function(){
        return color;
    };

    this.setRadius = function(r){
        if (typeof(r) === 'number') {
            radius = r;
        }
    };
    this.getRadius = function(){
        return radius;
    };

    this.setPosX = function(x){
        if (typeof(x) === 'number') {
            pos_x = x;
        }
    };
    this.setPosY = function(y){
        if (typeof(y) === 'number') {
            pos_y = y;
        }
    };

    this.getPosX = function(){
        return pos_x;
    };
    this.getPosY = function(){
        return pos_y;
    };

    this.setVelX = function(x){
        if (typeof(x) === 'number') {
            vel_x = x;
        }
    };
    this.setVelY = function(y){
        if (typeof(y) === 'number') {
            vel_y = y;
        }
    };

    this.getVelX = function(){
        return vel_x;
    };
    this.getVelY = function(){
        return vel_y;
    };

    this.setAccX = function(x){
        if (typeof(x) === 'number') {
            acc_x = x;
        }
    };
    this.setAccY = function(y){
        if (typeof(y) === 'number') {
            acc_y = y;
        }
    };

    this.getAccX = function(){
        return acc_x;
    };
    this.getAccY = function(){
        return acc_y;
    };

    this.step = function(tdiff){
        // Move position
        self.setPosX(self.getPosX() + (self.getVelX() * tdiff));
        self.setPosY(self.getPosY() + (self.getVelY() * tdiff));

        // Alter velocities
        self.setVelX(self.getVelX() + (self.getAccX() * tdiff));
        self.setVelY(self.getVelY() + (self.getAccY() * tdiff));

        // Detect boundaries
        if (self.getPosX() - self.getRadius() <= 0 || self.getPosX() + self.getRadius() >= CANVAS_WIDTH) {
            self.setVelX(self.getVelX() * -1.0);
        }
        if (self.getPosY() - self.getRadius() >= CANVAS_HEIGHT) {
            self.reset();
        }
    };

    this.reset();
}

Here is the workhorse object. It’s the Particle object that contains all of the movement functions.

It all kicks off from the reset() function. It centers the particle within the canvas and then randomizes the initial velocity, radius, and color. Color is pretty cool because you are just randomizing against an hsl() value and picking a hue somewhere on the spectrum with some bubble gum-style saturation and lightness.

The physics of it all is super simple. If you remember your high school kinematics, then you’ll get this.

It’s all handled in the step() function. Acceleration, velocity, and position are broken into their component X and Y vectors. The acceleration will always stay the same, though you can tweak it to make the particles fall faster or slower. Velocity is impacted by the acceleration, resulting in the position coordinates.

newPosition = oldPosition + (oldVelocity * timeDelta);
newVelocity = oldVelocity + (acceleration * timeDelta);

You’ll also notice inside step() that if a particle’s edge also hits the edge of the canvas, it’ll bounce back. You can add a dampening effect by changing self.setVelX(self.getVelX() * -1.0); to some other value less than 1.0, which is more realistic but less fun.

And then if the particle clears the bottom of the canvas, it’ll reset to the center with a new radius, color, and velocity.

Now for particle instantiation, which is also pretty simple.

1
2
3
for (var i = 0; i < MAX_PARTICLES; i++) {
    particles.push(new Particle());
}

And finally, we just write another step function that is called by requestAnimationFrame(). If you haven’t used it before, it’s a nice way to tell the browser to throw to an animation callback when it’s appropriate to do so. Paul Irish still has the gold standard on explaining why you should be using it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function step(timestamp) {
    if (last_draw === undefined) {
        last_draw = timestamp;
    }

    // Clear the canvas
    context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

    // Draw the particles
    for (var i = 0; i < MAX_PARTICLES; i++) {
        context.beginPath();
        context.fillStyle = 'hsl(' + particles[i].getColor() + ', 80%, 70%)';
        context.arc(particles[i].getPosX(), particles[i].getPosY(), particles[i].getRadius(), Math.PI*2, false);
        context.fill();

        // Force particle movement calculations
        particles[i].step((timestamp - last_draw) / 50);
    }

    last_draw = timestamp;
    window.requestAnimationFrame(step);
}
window.requestAnimationFrame(step);

The gist of it is you clear the canvas and then iterate through the list of particle objects to draw them and step() them. In this case, I’ve taken the time delta and scaled it down so it animates at a reasonable rate.

If you’re not familiar with canvas drawing, these steps are rather uncomplicated. You start be defining a new path, its fill style (our random HSL color), drawing a circle via the arc() context method, ad then filling it. Easy!

Party Time!

And that’s it! You can check out a live demo if you don’t believe me.

If you have any questions, shout at me in the comments. I’ll gladly answer anything you’ve got. It’s pretty simple stuff, but even simple things are hard the first time you do it. Having someone light the way makes it a lot more digestible.

Bye, Internet friends!