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:
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.
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!
This sure makes reading webmail a lot more interesting! 🙂
Thanks for your detailed explanation. You’re right — it is pointless, yet interesting nonetheless.
I bow before your JavaScript greatness.
Great!! I like the code style you write so much.It always give me a new idea when I review your site.
wow…can fly anywhere
its funny and cool
thanks
Thanks a lot for going through your code too. You used a lot of JS tricks I was not aware of and some provide huge performance boosts. The first thing I checked was CPU usage after I turned on the fairies, excellent job!
I learned a couple more JavaScript tricks after reading through this post! I enjoy making the occasional pointless script since you get to experiment more and break new ground.
Also, I was pleasantly surprised to find the fairies followed the scroll/resize events. Bravo on the efficient performance too!
Great! Thank you very much!
You used a lot of JS tricks I was not aware of and some provide huge performance boost
Really well explained and interesting. I learned a-lot from this, so thank you very much 🙂
Although I haven’t used it yet, there’s a new fork of CI called CI Reactor, which is a community-driven version of the framework!!!!
Wow thanks a lot for sharing this with us! It’s a amazing animation. While writing these lines, the fairies are coming down here but with a latency… Amazing! Makes it very real! Thanks, well done!
I learned a couple more JavaScript tricks after reading through this post! I enjoy making the occasional pointless script since you get to experiment more and break new ground.