Spring Loaders with Rebound and Canvas

A tutorial on how to create a loading spinner made with inscribed polygons and animated with Rebound spring motion.

Today we’re going to create a loading spinner that’s animated with Rebound with a spring motion. We’ll be using Canvas to cover the whole page and to display the spinner made of polygonal shapes that inscribe in itself. Then we’ll make the motion interesting and playful with Rebound.

The code presented in this tutorial is written in ES6 and compiled to ES5 using Babel, you’ll find the full gulp setup in the repository.

Creating the Spinner

Firstly, we need to create the logic of the spinner; let’s start by thinking of it as a simple triangle:

Inscribed triangle construction with Canvas
Inscribed triangle construction with Canvas

The idea is to draw a basePolygon and then draw a child so that it is inscribed in the original triangle. The Polygon.js method to compute the base points is the following:

/**
 * Get the points of any regular polygon based on
 * the number of sides and radius.
 */
_getRegularPolygonPoints() {

	let points = [];

	let i = 0;

	while (i < this._sides) {
		// Note that sin and cos are inverted in order to draw
		// polygon pointing down.
		let x = -this._radius * Math.sin(i * 2 * Math.PI / this._sides);
		let y = this._radius * Math.cos(i * 2 * Math.PI / this._sides);

		points.push({x, y});

		i++;
	}

	return points;
}

To draw the child we simply need to calculate where the vertex will be on each side at a given progress. We’ll achieve this by using linear interpolation between the points of the basePolygon.

Linear interpolation for a given progress
Linear interpolation for a given progress

The Polygon.js methods to compute the inscribed points looks as follows:

/**
 * Get the inscribed polygon points by calling `getInterpolatedPoint`
 * for the points (start, end) of each side.
 */
_getInscribedPoints(points, progress) {

	let inscribedPoints = [];

	points.forEach((item, i) => {

		let start = item;
		let end = points[i + 1];

		if (!end) {
		end = points[0];
		}

		let point = this._getInterpolatedPoint(start, end, progress);

		inscribedPoints.push(point);
	});

	return inscribedPoints;
}

/**
 * Get interpolated point using linear interpolation
 * on x and y axis.
 */
_getInterpolatedPoint(start, end, progress) {

	let Ax = start.x;
	let Ay = start.y;

	let Bx = end.x;
	let By = end.y;

	// Linear interpolation formula:
	// point = start + (end - start) * progress;
	let Cx = Ax + (Bx - Ax) * progress;
	let Cy = Ay + (By - Ay) * progress;

	return {
		x: Cx,
		y: Cy
	};
}

Once done, we can simply use the methods provided by Polygon.js to repeat the process for each child until a certain depth is reached.

/**
 * Update children points array.
 */
_getUpdatedChildren(progress) {

	let children = [];

	for (let i = 0; i < this._depth; i++) {

		// Get basePolygon points on first lap
		// then get previous child points.
		let points = children[i - 1] || this.points;

		let inscribedPoints = this._getInscribedPoints(points, progress);

		children.push(inscribedPoints);
	}

	return children;
}

/**
 * Render children, first update children array,
 * then loop and draw each child.
 */
renderChildren(context, progress) {

	let children = this._getUpdatedChildren(progress);

	// child = array of points at a certain progress over the parent sides.
	children.forEach((points, i) => {

		// Draw child.
		context.beginPath();
		points.forEach((point) => context.lineTo(point.x, point.y));
		context.closePath();

		// Set colors.
		let strokeColor = this._colors.stroke;
		let childColor = this._colors.child;

		if (strokeColor) {
		context.strokeStyle = strokeColor;
		context.stroke();
		}

		if (childColor) {
			let rgb = rebound.util.hexToRGB(childColor);

			let alphaUnit = 1 / children.length;
			let alpha = alphaUnit + (alphaUnit * i);

			let rgba = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;

			context.fillStyle = rgba;

			context.fill();
		}
	});
}

At this point we have the Spinner system and we can start thinking about the animation. Note that we can plug any animation engine into it, essentially from now on we just need to think about how to make the progress run from 0 to 1. In this case we’ll use Rebound, let’s see what it is and how to use it.

Animating with Rebound

Rebound is a simple to use library from Facebook. The library takes inputs for a spring (tension and friction), then once the spring is accelerating, the library computes a value for that spring which we’ll apply to the Spinner progress to create a great springy animation.

Creating the motion with Rebound

Animating with Rebound is an easy 5-step process:
1. Create a SpringSystem (demo.js:45):

// Create a SpringSystem.
	let springSystem = new rebound.SpringSystem();

2. Add a spring to the system (demo.js:48)

// Add a spring to the system.
	demo.spring = springSystem.createSpring(settings.tension, settings.friction);

3. Add a SpringListener to the Spring (Spinner.js:92)

_addSpringListener() {

	let ctx = this;

	// Add a listener to the spring. Every time the physics
	// solver updates the Spring's value onSpringUpdate will
	// be called.
	this._spring.addListener({
		// ...
	});
}

4. Set spring in motion from start value by giving it an end value (Spinner.js:160)

this._spring.setEndValue((this._spring.getCurrentValue() === 1) ? 0 : 1);

5. Use the value from the spring as it is updated in the onSpringUpdate callback to set progress (Spinner.js:100)

onSpringUpdate(spring) {

	let val = spring.getCurrentValue();

	// Input range in the `from` parameters.
	let fromLow = 0,
		fromHigh = 1,
		// Property animation range in the `to` parameters.
		toLow = ctx._springRangeLow,
		toHigh = ctx._springRangeHigh;

	val = rebound.MathUtil.mapValueInRange(val, fromLow, fromHigh, toLow, toHigh);

	// Note that the render method is
	// called with the spring motion value.
	ctx.render(val);
}

Bring the Spinner and the Spring motion together

First let’s write some settings to easily change the effect.

settings: {
	rebound: {
		tension: 2,
		friction: 5
	},
	spinner: {
		radius: 80,
		sides: 3,
		depth: 4,
		colors: {
			background: '#000000',
			stroke: '#000000',
			base: '#222222',
			child: '#FFFFFF'
		},
		alwaysForward: true, // When false the spring will reverse normally.
		restAt: 0.5, // A number from 0.1 to 0.9 || null for full rotation
		renderBase: true // Optionally render basePolygon
	}
}

Now we just need to make everything work together. In our main file (demo.js) we’ll instantiate Rebound and the Spinner with the settings, then we’ll start the animation either as autoSpin or based on the progress of an ajax request. By calling demo.spinner.setComplete() we’ll complete the animation.

/**
 * Initialize demo.
 */
init() {

	let spinnerTypeAutoSpin = true;

	// Instantiate animation engine and spinner system.
	demo.initRebound();
	demo.initSpinner();

	// Init animation with Rebound Spring System.
	demo.spinner.init(demo.spring, spinnerTypeAutoSpin);

	if (spinnerTypeAutoSpin) {
		// Fake loading time, in a real world just call demo.spinner.setComplete();
		// whenever the loading will be completed.
		setTimeout(() => {
			demo.spinner.setComplete();
		}, 6000);
	} else {
		// Perform real ajax request.
		demo.loadSomething();
	}
},

In the render method of Spinner.js we are going to update the progress using the spring value. Then, once the spring will be at rest we’ll call the spin method where we’ll set a new endValue for the spring so it will begin running and animate the spinner one more time. To make the spinner always move forward we’ll reset the spring value to 0 without any motion using setAtRest(). If a restThreshold is set, we’ll also switch the animation range values used to compute the spring val on each update, by doing this we’ll change the reverse animation and make the Spinner always progress forward with an half way animation at the restThreshold.

/**
 * Spin animation.
 */
_spin() {

	if (this._alwaysForward) {

		let currentValue = this._spring.getCurrentValue();

		// Switch the animation range used to compute the value
		// in the `onSpringUpdate`, so to change the reverse animation
		// of the spring at a certain threshold.
		if (this._restThreshold && currentValue === 1) {
		this._switchSpringRange();
		}

		// In order to keep the motion going forward
		// when spring reach 1 reset to 0 at rest.
		if (currentValue === 1) {
		this._spring.setCurrentValue(0).setAtRest();
		}
	}

	// Restart the spinner.
	this._spring.setEndValue((this._spring.getCurrentValue() === 1) ? 0 : 1);
}

_switchSpringRange() {

	let threshold = this._restThreshold;

	this._springRangeLow = (this._springRangeLow === threshold) ? 0 : threshold;
	this._springRangeHigh = (this._springRangeHigh === threshold) ? 1 : threshold;
}

To complete the animation we’ll simply call the setComplete method once the loading is finished. To keep things simple we’ll just fade out and remove the entire canvas as well as stopping the animation.

/**
 * Start complete animation.
 */
setComplete() {
	this._isCompleting = true;
}

_completeAnimation() {

	// Fade out the canvas.
	this._canvasOpacity -= 0.1;
	this._canvas.style.opacity = this._canvasOpacity;

	// Stop animation and remove canvas.
	if (this._canvasOpacity <= 0) {
		this._isAutoSpin = false;
		this._spring.setAtRest();
		this._canvas.remove();
	}
}

We hope you enjoyed this tutorial and find it inspirational.

Thank you for reading.

Resources

Have a look at the following resources used:

Tagged with:

Claudio Calautti

Claudio is a freelance creative developer based in London. He develops modern, highly interactive websites with cutting edge technologies. In his spare time he has fun with web experiments and enjoys travelling.

Stay up to date with the latest web design and development news and relevant updates from Codrops.

Feedback 12

Comments are closed.
  1. To make the code in javascript effective on loading page and without the settimeout what can i do? thanks

  2. I have a question..
    Is it possible to add gradient background color instead of simple color ??

  3. How can we get this to auto-start without a button click? Thx for ur job =)