I wanted to give a brief overview of the techniques involved in creating this. It’s slightly pointless but still quite fun and I think there are a few notable aspects of its implementation that are worth writing about.

It is a “Fairy” generator that creates small pulsating elements and moves them around the viewport in random directions, but not too eratically. You can load some fairies on this page by clicking this bookmarklet!

The original problem was that I had been mentally locked-in to a specific type of animation on the client-side and I would sit there thinking, “Okay, how do I animate this element from ‘a’ to ‘b’ while taking random detours and going in random directions?”… I was still in the conventional client-side animation mindset, where you take an element and call an animation function to mutate certain CSS properties over a specified duration. The prospect of doing it via manual calls to setInterval, with delta-x/y values never crossed my mind.

Eventually it became clear that this would be the only way to make the fairies fly as I wanted — i.e. not to simply go from ‘a’ to ‘b’, but to continually fly around in all directions.

I started with an environment singleton instance, named “fairyEnvironment”, containing the various bounds and rules attributed to the current document:

var fairyEnvironment = new function() {
 
    this.bounds = {
        left: 0,
        top: 0,
        right: 0,
        bottom: 0
    };
 
    this.speedLimit = {
        high: 6,
        low: 2
    };
 
    this.spawn = function(amount, postCreation, config) {
 
        var ret = [], i = -1;
 
        amount = ~~amount
 
        while (++i < amount) {
            postCreation &&
                postCreation.call(
                    ret[i] = new fairyEnvironment.Fairy(config)
                );
        }
 
        return ret;
 
    };
 
};

The generated fairies would have to stay within a specific area, defined by the bounds, “top”, “left”, “right” and “bottom”. I also specified the upper and lower speed limit and a function to spawn multiple Fairy instances at once. You can see that I’ve referenced fairyEnvironment.Fairy, which is defined as follows:

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
fairyEnvironment.Fairy = function Fairy(config) {
 
    // Config is optional, defaults apply
    config = config || {};
 
    this.bounds = fairyEnvironment.bounds;
    this.speedLimit = fairyEnvironment.speedLimit;
 
    this.timer;
 
    // If no width is specified then generate width between 10 and 30
    this.width = config.width || 0 | Math.random() * 20 + 10;
    this.height = config.height || this.width;
 
    this.opacity = 1;
    this.dOpacity = Math.random() * .1;
 
    // If no speed is specified then genarate a random width between
    // the upper and lower limits specified in FairyEnvironment
    this.speed = config.speed || Math.random() *
                 (this.speedLimit.high - this.speedLimit.low)
                 + this.speedLimit.low;
 
    this.dX = 0;
    this.dY = 0;
 
    // Setup an arbitrary direction (to get things rolling)
    this.shiftDirection();
 
    // If no color is specified then generate one randomly
    // Note that this will generate intensities between 155 and 255
    this.color = config.color || [
        0|Math.random()*100+155,
        0|Math.random()*100+155,
        0|Math.random()*100+155
    ];
 
    // Spawn the fairy somewhere random
    this.y = Math.random() * (this.bounds.bottom - this.height);
    this.x = Math.random() * (this.bounds.right - this.width);
 
    // Build the canvas
    // Note, this doesn't HAVE to be a canvas - it could
    // just be a regular DOM element
    this.dom = this.build();
 
    // set initial position
    if (this.dom) this.step(); 
 
};

The dX and dY properties are probably the most important. The ‘d’ stands for “delta” which means the difference between two things — here it means the change that the x and y properties will experience on each step of the animation. So, if dX is set to 5 then the fairy will move five pixels to the right on every step of the animation. The dX and dY values can be changed at any time, before or after each step, thus producing changes in speed and direction.

The speed property (line 20) defines the limits of dX and dY. So, if the speed is set as 10 then the delta values can only go up to ten (either positive or negative) — so, dX would be able to change from -10 to +10.

I’ve also used both the double-bitwise-not (~~) and bitwise-or (| – but with zero as left-hand operand – 0|) operators quite a lot — in essence, they both floor the right-hand-side operand (i.e. number -> integer). They’re better than Math.floor because they return a number (zero) even from Undefined, Null, NaN, and Infinity operands. 0| is used instead of ~~ in some places simply because | (bitwise-or) has a lower precedence than ~ (bitwise-not), meaning that it can be used in expressions like 0|a*b+c without having to wrap a*b+c in parenthesis (i.e. ~~(a*b+c)). You can read more about this “flooring” operation here.

I’ve also used Math.random() a lot, e.g.

// If no color is specified then generate one randomly
// Note that this will generate intensities between 155 and 255
this.color = config.color || [
    0|Math.random()*100+155, // Red
    0|Math.random()*100+155, // Green
    0|Math.random()*100+155  // Blue
];

Generating a random number between two bounds is quite simple:

Math.random() * (upperBound - lowerBound) + lowerBound

Math.random() generates a number from zero to just below one (never one). Note that the lower bound is inclusive and the upper bound is exclusive.

This generated color is used when creating and painting the canvas:

/* fairyEnvironment.Fairy.prototype = { ... */
build: function() {
 
    // Create <canvas>
 
    var c = document.createElement('canvas'),
        _ = c.getContext && c.getContext('2d'),
        color = this.color;
 
    if (!_) {
        // Just for IE :D
        var d = document.createElement('div');
        d.style.fontSize = '12px';
        d.style.fontFamily = 'monospace';
        d.style.position = 'absolute';
        d.style.color = '#FFF';
        d.style.background = '#000';
        d.innerHTML = 'IE SUCKS';
        this.width = 48;
        return d;
    }
 
    c.width = this.width;
    c.height = this.height;
 
    // Create radial gradient to make it Fairy-like
    var grad = _.createRadialGradient(
        this.width/2,
        this.height/2,
        0,
        this.width/2,
        this.height/2,
        this.width/2
    );
 
    grad.addColorStop(0, 'rgba(' + color.join() + ',1)');
    grad.addColorStop(1, 'rgba(' + color.join() + ',0)');
 
    _.fillStyle = grad;
    _.fillRect(0, 0, this.width, this.height);
 
    c.style.position = 'absolute';
 
    return c;
 
}/*,...};*/

Jacob Seidelin’s canvas cheatsheet was infinitely useful during this endeavour. I don’t yet use canvas enough to know its API well enough — one day perhaps…

As you can see in the Fairy constructor, the return value of build() is assigned to instance.dom (line 43: this.dom = this.build();). Whoever instantiates the fairy is expected to add the canvas to the document. A simple example:

var myFairy = new fairyEnvironment.Fairy();
document.body.appendChild(myFairy.dom); // append to document
myFairy.start();

Or, with our useful spawn method:

fairyEnvironment.spawn(25, function(){
    document.body.appendChild(this.dom);
    this.start();
});

Calling start() begins the animation with a simple call to setInterval:

start: function() {
    var hoc = this;
    this.timer = setInterval(function(){
        hoc.step();
    }, 30);
}

The step() method is run every 30 milliseconds, which equates to about 33 frames per second. It could probably be made a little more infrequent though — although the jury is still out on the perfect FPS.

The step() method involves a number of simple operations. This is where abstraction really starts to shine:

step: function() {
 
    if (!this.detectEdges()) {
        this.shiftDirection();
    }
 
    this.limitSpeed();
 
    this.dom.style.left = ~~(this.x += this.dX) + 'px';
    this.dom.style.top = ~~(this.y += this.dY) + 'px';
 
    this.pulse();
 
}

With the right level of abstraction and proper naming one can do away with the clutter caused by the overuse of comments. The only possibly questionable area in the above method is this:

this.dom.style.left = ~~(this.x += this.dX) + 'px';
this.dom.style.top = ~~(this.y += this.dY) + 'px';

We’ve already established what ~~ does. One important thing that happens here, which would be easy to miss, is the expression this.x += this.dX which adds the dX (delta-x) value to the current x value and then returns the result of that addition. Retaining state using this.x is much better than having to re-query this.dom.style.left on every step.

As logic would dictate, the detectEdges() method determines whether the fairy is too close to any of the bounds specified in fairyEnvironment.bounds, and if it is then it takes action by calling turnAway().

detectEdges():

detectEdges: function() {
 
    // If the fairy is too close to an edge (padding is fixed at 60px)
    // then slowly turn away... 
 
    var x = this.x,
        y = this.y,
        w = this.width,
        h = this.height,
        bounds = this.bounds,
        padding = 60;
 
    if (x < padding + bounds.left) return this.turnAway(this.LEFT_EDGE);
    if (y < padding + bounds.top) return this.turnAway(this.TOP_EDGE);
    if (x + w > bounds.right - padding) return this.turnAway(this.RIGHT_EDGE);
    if (y + h > bounds.bottom - padding) return this.turnAway(this.BOTTOM_EDGE);
 
}

turnAway():

turnAway: function(fromEdge) {
 
    if (fromEdge === this.TOP_EDGE || fromEdge === this.BOTTOM_EDGE) {
        // SLowly start inverting Y
        this.dY += fromEdge === this.TOP_EDGE ? 1 : -1;
        this.compensate('X');
    } else {
        // Slowly start inverting X
        this.dX += fromEdge === this.LEFT_EDGE ? 1 : -1;
        this.compensate('Y');
    }
 
    return true;
 
}

The compensate() method takes a single axis as its argument and makes sure that the full speed of the fairy is being expressed on both dX and dY. For example, if the speed is 10 and dX has been set to 3, then compensate will change dY to either -7 or +7 dependent on its current direction. abs(dX) plus abs(dY) must always equal speed.

The shiftDirection() method is called by step(), but only when the fairy is not near any edges. This method will randomly shift the direction of the fairy:

shiftDirection: function() {
 
    // Only change direction 30% of the time
    if (Math.random() > .7) {
        this.dX += Math.random() * 2 - 1; // between 1 & -1
        this.compensate('Y');
    }
 
}

I might make this configurable in the future but right now it will only change the direction 30% of the time, and only one pixel at a time.

The pulsating opacity of the fairies is achieved with a very rudimentary pulse() method:

pulse: function() {
 
    if (this.opacity >= 1 || this.opacity <= .6) {
        this.dOpacity = -this.dOpacity;
    }
 
    this.dom.style.opacity = this.opacity += this.dOpacity;
 
}

The opacity pulses between 0.6 and 1. dOpacity is set in the Fairy constructor to a number between zero and 0.1:

16
this.dOpacity = Math.random() * .1;

That’s it!

Like I said: quite pointless yet quite fun. You can check out the Github repo over here:

“Fairy” on Github

The bookmarklet

The bookmarklet loads the Fairy script from Github and then spawns 25 fairies. It also listens for the window’s scroll and resize events so that it can change the bounds of fairyEnvironment according to where you are in the document. You can see this in action by clicking this and then scrolling up or down — the fairies will follow you.

bookmarklet-demo.fairy:

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
(function(){
 
    if (window.fairyEnvironment && fairyEnvironment.Fairy) {
        return init();
    }
 
    var s = document.documentElement.appendChild(document.createElement('script')),
        t = setInterval(function(){
            if (window.fairyEnvironment && fairyEnvironment.Fairy) {
                clearInterval(t);
                s.parentNode.removeChild(s);
                init();
            }
        }, 50);
 
    s.src = 'http://github.com/jamespadolsey/Fairy/raw/master/fairy.js';
 
    function init() {
 
        var d = document.body.appendChild(document.createElement('div')),
            docEl = document.documentElement,
            body = document.body,
            winHeight,
            resize = function() {
 
                winHeight = window.innerHeight ||
                            docEl.clientHeight ||
                            body.clientHeight;
 
                fairyEnvironment.bounds.right =
                    Math.max(docEl.clientWidth, docEl.offsetWidth);
 
                scroll();
 
            },
            scroll = function() {
 
                fairyEnvironment.bounds.top =
                    Math.max(
                        ~~window.pageYOffset,
                        body.scrollTop,
                        docEl.scrollTop
                    );
                fairyEnvironment.bounds.left =
                    Math.max(
                        ~~window.pageXOffset,
                        body.scrollLeft,
                        docEl.scrollLeft
                    );
                fairyEnvironment.bounds.bottom =
                    fairyEnvironment.bounds.top + winHeight;
 
            };
 
        resize();
 
        if (window.addEventListener) {
            window.addEventListener('resize', resize, false);
            window.addEventListener('scroll', scroll, false);
        } else {
            window.attachEvent('onresize', resize);
            window.attachEvent('onscroll', scroll);
        }
 
        fairyEnvironment.spawn(25, function(){
            if (this.dom) {
                d.appendChild(this.dom);
                this.start();
            }
        });
 
    }
 
}());

Thanks for reading! Please share your thoughts with me on Twitter. Have a great day!