hexatlas

Better Loading Animations

Loading GIFs are boring. In an age where <canvas> support is nearly ubiquitous, we can do far better than an ugly spinning GIF.

Take, for example, Wolfram Alpha's loading animation. It plays a version of Conway's Game of Life for a few seconds while it processes each request. It looks neat. It's fun to watch! You, too, can create a simple loading animation using only a few lines of JavaScript.

Today, we'll go through one I built recently:

This is a simple particle system with a fade effect. To use it, you create (or already have) a <canvas> element, and then pass it to the function that sets up the animation:

<canvas id="loader" width="250" height="250"></canvas>

<script>
show_loading_particles(document.getElementById("loader"));
</script>

That function is where all the fun bits go; the rest of the code goes in that function. First, we set up the <canvas>:

// get canvas dimensions and half dimensions
var canvas_w = canvas.width;
var canvas_h = canvas.height;
var canvas_w2 = Math.floor(canvas_w/2);
var canvas_h2 = Math.floor(canvas_h/2);

// get 2d context
var ctx = canvas.getContext("2d");

// center on 0,0 and scale to 250 units by 250 units
ctx.translate(canvas_w2, canvas_h2);
ctx.scale(canvas_w/250, canvas_h/250);

That part is consistent across most <canvas> projects.

Next, we need to set up some state that we can use to keep track of what's going on. These next parts depend on what you're trying to build. Today, we're making a particle system, so we'll make an array of dots, each a regular object with properties for its position (x, y), velocity (vx, vy), and acceleration (ax, ay). We'll give those properties sane starting values (found after some experimentation).

var dots = [];

// make 10 dots
for (var i=0; i<10; i++) {
  dots.push({
    // position: anywhere in the middle half
    x:canvas_w2 * (Math.random()-.5), y:canvas_h2 * (Math.random()-.5),
    // velocity: -15 to 15
    vx:30 * (Math.random()-.5), vy:30 * (Math.random()-.5),
    // acceleration: -7.5 to 7.5
    ax:15 * (Math.random()-.5), ay:15 * (Math.random()-.5)
  });
}

Finally, we'll set up a function to run on each frame. There are fancier ways to do this involving requestAnimationFrame(), but a regular 33ms interval works fine for a simple loading animation.

return setInterval(function() {
  ...
}, 33);

We have it return the interval ID. To stop the animation, call clearInterval() on the return value of show_loading_particles() and remove the <canvas> element.

Everything left goes in that function.

First, we fade the image slightly. To achieve this, we draw over the whole image with a nearly-transparent rectangle.

// fade current image
ctx.save();
ctx.globalAlpha = .1; // fade 10% each frame
ctx.fillStyle = "#000000";
ctx.fillRect(-canvas_w2, -canvas_h2, canvas_w, canvas_h);
ctx.restore();

Unfortunately, because of integer rounding, a faint trail is left behind the particles. Fortunately, the loading animation won't be up long, and so that's probably okay.

The last thing we have to draw is the next step for each particle:

// draw next step
ctx.save();
ctx.strokeStyle = "#ffffff";
for (var i=0; i<dots.length; i++) {
  var dot = dots[i];
  ...
}
ctx.restore();

For each dot, we draw a line from where it was to where it is now. If we just drew a dot at the new location, we'd get a dotted line any time a dot moves further than one dot-radius from its previous location.

So, we start by putting the beginning of the line at the current dot's position before we adjust it.

ctx.beginPath();
ctx.moveTo(dot.x, dot.y);

Next, we update the dot. There's a little math involved, but it's not bad.

First, we need the direction (t, or theta) from the origin to the dot. We'll use this in a moment to apply gravity to the dot.

var t = Math.atan2(dot.y, dot.x);

Next, we update the acceleration (ax, ay). Each frame, the acceleration will increase or decrease by a random number from -0.5 to 0.5 (Math.random()-.5) and point a little more toward the origin (-Math.cos(t) or -Math.sin(t)). Then, we dampen it by 0.9 to prevent it from getting too large.

dot.ax = (dot.ax + (Math.random()-.5) - Math.cos(t)) * .9;
dot.ay = (dot.ay + (Math.random()-.5) - Math.sin(t)) * .9;

Then, we update the velocity (vx, vy). Each frame, the velocity just gets the acceleration added to it. We also simulate drag by multiplying the velocity by 0.75, which helps to smooth out the motion of the particles.

dot.vx = (dot.vx + dot.ax) * .75;
dot.vy = (dot.vy + dot.ay) * .75;

Finally, we update the position (x, y) by adding the velocity.

dot.x += dot.vx;
dot.y += dot.vy;

Now that the dot has been updated, finish drawing the line to its new position:

ctx.lineTo(dot.x, dot.y);
ctx.stroke();

We repeat that for each dot, and then repeat that for each frame.

It seems like a lot of code when it's all spread out like this, but it's really pretty straightforward. I encourage you to play with the algorithm — especially the dampening/drag constants, the fade rate, and the rules for how the acceleration changes every frame. Maybe make it follow the mouse cursor or change colors.

Go ahead, download the source code.