GJ
Coding

Recreating the old iCloud background using Javascript and a HTML Canvas

In this post, I go through the steps I took to recreate the background that used to be on the iCloud web site - blurry bubbles!

Old iCloud Dashboard

The above image shows what the iCloud web site used to look like. The background was animated and randomly generated. I really liked this effect and so re-created it myself using Javascript and a HTML Canvas element.

I'll only post relevant code snippets to illustrate any concepts. For the full code I recommend that you view the project on Github.

Creating a Javascript module

To keep the library/package/module/whatever-you-want-to-call-it self contained and to keep from polluting the global namespace, we can define everything in an Immediately-Invoked Function Expression. This simply means we can start with the following:

var GJBubbles = (function () {
	// Module implementation goes here

	return {}; // Publicly accessible methods and properties go here
})();

Anything we put inside the function will be "private" and anything inside the object that's returned will be "public". We'll put the inner workings inside the private scope and then the public API inside the returned object.

The "bubble" object

Are these really bubbles? Perhaps I should have called them orbs. Anyway, the bubble object looks like this:

function Bubble(x, y, dx, dy, radius, opacity, blur)
{
	// Assign the function parameters to local variables
	this.x = x; // etc...
	// Draw the circle

	this.draw = function (canvas, context) {
		// Check the bounds of the bubble
		this.checkBounds(canvas);

		// Move the bubble
		this.x += this.dx;
		this.y += this.dy;

		// Draw the bubble using a radial gradient
		var radgrad = context.createRadialGradient(...);

		radgrad.addColorStop(0, 'rgba(255,255,255,' + this.opacity + ')');
		radgrad.addColorStop(1, 'rgba(255,255,255,0)');

		context.fillStyle = radgrad;
		context.fillRect(this.x, this.y, this.size * 2, this.size * 2);
	};

	// Checks to see if the bubble is out of bounds of the passed in canvas and reverses the direction
	this.checkBounds = function (canvas) {
		...
	}
}

There's a lot more going on here in the actual code (here), but for simplicity I've stripped a few things out such as parameter assignments, validation and animating the bubble in. The main things to focus on are the draw() and the checkBounds() functions. draw() calls checkBounds() to see if the bubble has reached the edge of the canvas that it's on and if it has, makes it "bounce" off in a different direction by inverting the values in the movement deltas. It then moves the bubble and draws it in its new location.

Note that the amount the bubble is blurred is proportionate to the bubble's size (smaller bubbles are more blurry).

Drawing all the bubbles

Next, we need a function to loop over an array of bubbles and draw them onto a canvas. Notice how we recursively pass the same function to window.requestAnimationFrame so that the bubbles get redrawn each frame.

function draw(bubbles, canvas)
{
	var context = canvas.getContext('2d');
	context.clearRect(0,0, canvas.width, canvas.height);
	for (var i = 0; i < bubbles.length; i++) {
		bubbles[i].draw(canvas, context);
	}
	var raf = window.requestAnimationFrame(function() {
		draw(bubbles, canvas);
	});
}

Creating the public API

I wanted this to be able to be included on a web page and then easily added to a containing element, so in the return statement I have an init() function that takes an ID of a container and a list of options. The options are values like how many bubbles you'd like and the minimum and maximum values you want each bubble property to be (for example, the size, speed and opacity).

return {
	options: {
		// How many bubbles, max and min speed, size etc.
	},
	init: function(container_id, options) {
		// Get the options, overwriting defaults

		// Get the container, create a canvas and append it as a child
		var container = document.getElementById(container_id);
		var canvas = document.createElement("canvas"); 
		container.appendChild(canvas);

		// Set the canvas size to be the same as the container

		var bubbles = [];

		// Create the bubbles using setInterval to stagger their creation
		var self = this;
		var interval = setInterval(function() {
			var radius = randomBetween(min_radius, max_radius);

			// The next two options make bubbles that are smaller appear blurrier and more transparent. Bigger ones are more in focus and opaque.
			var blur = 0.8 - (radius - (canvas.width * (self.options.min_radius / 100))) / ((canvas.width * (self.options.max_radius / 100)) - (canvas.width * (self.options.min_radius / 100)));
			var opacity = blur * self.options.max_opacity;

			if (opacity < self.options.min_opacity) {
				opacity = self.options.min_opacity;
			}

			bubbles.push(new Bubble(
				randomBetween(0, canvas.width),
				randomBetween(0, canvas.height),
				randomBetween(-self.options.max_speed, self.options.max_speed),
				randomBetween(-self.options.max_speed, self.options.max_speed),
				radius,
				opacity,
				blur
			));
			
			if (bubbles.length == self.options.bubble_count) {
				clearInterval(interval);
			}
		}, 50);
			
		draw(bubbles, canvas);
	}
};

Putting it all together

To use the module, add the following to a web page:

<div id="my-div"></div>

<script src="src/GJBubbles.js"></script>
<script>
	GJBubbles.init('my-div', {bubble_count: 10});
</script>

View a demo

As mentioned before, to keep this post from getting too code-heavy, I've stripped bits out, so be sure to check out the repository on GitHub.