This tutorial will guide you through the development of a simple SVG loading spinner for use on mobile web sites. Visual indicators like the spinner built in this tutorial are used to indicate background-thread activity and are a crucial part of strong user experience design!
Prerequisites
This tutorial is assumes that you already have basic knowledge in Scalable Vector Graphics (SVG), HTML, CSS, JavaScript, and jQuery. However, the content is presented in a step-by-step fashion that should be easy enough to follow along.
What about Raphaël? We will use the Raphaël project for performing the SVG drawing in this tutorial. To quote from the official Raphaël project web site:
Raphaël uses the SVG W3C Recommendation and VML as a base for creating graphics. This means every graphical object you create is also a DOM object, so you can attach JavaScript event handlers or modify them later. Raphaël’s goal is to provide an adapter that will make drawing vector art compatible cross-browser and easy.
To use Raphaël in your project, you just need to follow these steps:
- Import the library into your webpage.
- Create the raphael object, passing the id of the div where your SVG will be drawn, like so:
var paper = Raphael(divID, width, height);
- Create the elements you need into the recently created raphael object, for example:
// Creates circle at x = 50, y = 40, with radius 10 var circle = paper.circle(50, 40, 10); // Sets the fill attribute of the circle to red (#f00) circle.attr("fill", "#f00");
Enough theory! Let's start coding!
Step 1: Page Creation with HTML
Let's start by first building our demo page in HTML. It should look like the following:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Loading Spinner Example</title> <!-- CSS --> <link href="spinner/spinner.css" rel="stylesheet"> </head> <body> <p> <a id="createSpinner" href="">Unleash</a> the power of the loading spinner. </p> <!-- SPINNER --> <div id="spinnerFullScreen"> <div id="floater"> <div id="spinner"></div> </div> </div> <!-- Placed at the end of the document so the pages load faster and without blocking --> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js" type="text/javascript"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js" type="text/javascript"></script> <script src="spinner/spinner.js" type="text/javascript"></script> </body> </html>
Last, but not the least, we add a link where you can click to "unleash" the spinner (i.e. begin the spinning animation).
<a id="createSpinner">Unleash</a> the power of the loading spinner.
Step 2: CSS Styling
Now that we have our markup ready, we need to begin filling in the missing style.
In terms of CSS, the outermost div (i.e. id="spinnerFullScreen") must be black and occupy the whole screen on top of all elements that don't belong to the spinner.
The other two divs (i.e. id="floater" and id="spinner") uses a slight "hack" in order to properly center the spinner in the middle of the screen no matter what the screen size is or where the scroll is set. I will not explain it on this tutorial since the CSS only relates to a "dummy" demo page, not the central purpose of this tutorial.
In the end, the spinner.css file should look like this:
#spinnerFullScreen { display: none; width: 100%; height: 100%; position: fixed; top: 0px; left: 0px; background-color: black; opacity: 0; z-index: 9999998; } #floater { display: table; width: 100%; height: 100%; } #spinner { display: table-cell; vertical-align: middle; text-align: center; z-index:9999999; }
Step 3: Adding Behavior with JavaScript
In theory, our spinner is composed of a certain number of sectors (8 in the image) that have a length ("sectorLength") and a width ("sectorWidth"). Of course, these sectors have a distance to the center as well ("centerRadius").
But is this static? And what about the animation? Well, the animation is just a little trick: having all the sector opacities ranging from 0.0 to 1.0, we continuously change the opacity of each sector to be equal to the opacity of the next sector. Confused? It will likely become more transparent once you see the implementation in JavaScript.
In order to create a reusable library, we will use an Object Oriented paradigm implemented in JavaScript. The library is built around a constructor (function Spinner(data)
) and two distinct functions:
- create – using the instance variables defined in the constructor, it builds the SVG spinner and animates it as well.
- destroy – destroys the SVG spinner and hides the full screen view.
In the spinner.js file created previously, we first create the constructor of the Spinner, enabling the user of the library to set some values like the number of sectors, the distance of the sectors to the center, and so forth.
/** * creates the object Spinner with data values or default values in the case they are missing * @param data * @constructor */ function Spinner(data) { //number of sectors of the spinner - default = 12 this.sectorsCount = data.sectorsCount || 12; //the distance from each sector to the center - default = 70 this.centerRadius = data.centerRadius || 70; //the length/height of each sector - default = 120 this.sectorLength = data.sectorLength || 120; //the width of each sector of the spinner - default = 25 this.sectorWidth = data.sectorWidth || 25; //color of the spinner - default = white this.color = data.color || 'white'; //the opacity of the fullScreen this.fullScreenOpacity = data.fullScreenOpacity; //array of spinner sectors, each spinner is a svg path this.sectors = []; //array with the opacity of each sector this.opacity = []; //the raphael spinner object this.spinnerObject = null; //id of the timeout function for the rotating animation this.spinnerTick = null; }
Now on to the biggest method of the spinner object, the create method. This method is called every time the user wants to show the spinner. Note the use of jQuery to select our elements. This is where the id's we talked about above come in:
Spinner.prototype.create = function() { //shows the full screen spinner div $('#spinnerFullScreen').show(); //animates the opacity of the full screen div containing the spinner from 0 to 0.8 $('#spinnerFullScreen').animate({ opacity: this.fullScreenOpacity }, 1000, function() { });
Continuing along with the create method, we do some initial calculations, like the total size of the spinner, and prepare the Raphael object to draw the sections:
//center point of the canvas/spinner/raphael object var spinnerCenter = this.centerRadius + this.sectorLength + this.sectorWidth; //angle difference/step between each sector var beta = 2 * Math.PI / this.sectorsCount; //params for each sector/path (stroke-color, stroke-width, stroke-linecap) var pathParams = { "stroke": this.color, "stroke-width": this.sectorWidth, "stroke-linecap": "round" }; /** * creates the Raphael object with a width and a height * equals to the double of the spinner center * “spinner” is the id of the div where the elements will be drawn */ var paperSize = 2 * spinnerCenter; this.spinnerObject = Raphael('spinner', paperSize, paperSize);
Next is the drawing of the cycle and the building of an array with the current opacity of each sector:
//builds the sectors and the respective opacity for (var i = 0; i < this.sectorsCount; i++) { //angle of the current sector var alpha = beta * i; var cos = Math.cos(alpha); var sin = Math.sin(alpha); //opacity of the current sector this.opacity[i] = 1 / this.sectorsCount * i; /** * builds each sector, which in reality is a SVG path * note that Upper case letter means that the command is absolute, * lower case means relative to the current position. * (http://www.w3.org/TR/SVG/paths.html#PathData) * we move the "cursor" to the center of the spinner * and add the centerRadius to center to move to the beginning of each sector * and draws a line with length = sectorLength to the final point * (which takes into account the current drawing angle) */ this.sectors[i] = this.spinnerObject.path([ ["M", spinnerCenter + this.centerRadius * cos, spinnerCenter + this.centerRadius * sin], ["l", this.sectorLength * cos, this.sectorLength * sin] ]).attr(pathParams); }
Now that we have our spinner built and displayed, we need to animate it. This is the last part of the create method:
/** * does an animation step and calls itself again * @param spinnerObject this param needs to be passed * because of scope changes when called through setTimeout function */ (function animationStep(spinnerObject) { //shifts to the right the opacity of the sectors spinnerObject.opacity.unshift(spinnerObject.opacity.pop()); //updates the opacity of the sectors for (var i = 0; i < spinnerObject.sectorsCount; i++) { spinnerObject.sectors[i].attr("opacity", spinnerObject.opacity[i]); } /** * safari browser helper * There is an inconvenient rendering bug in Safari (WebKit): * sometimes the rendering should be forced. * This method should help with dealing with this bug. * source: http://raphaeljs.com/reference.html#Paper.safari */ spinnerObject.spinnerObject.safari(); /** * calls the animation step again * it's called in each second, the number of sectors the spinner has. * So the spinner gives a round each second, independently the number of sectors it has * note: doesn't work on IE passing parameter with the settimeout function :( */ spinnerObject.spinnerTick = setTimeout(animationStep, 1000 / spinnerObject.sectorsCount, spinnerObject); })(this); };//end of the create method
Finally, the destroy method of our spinner:
/** * destroys the spinner and hides the full screen div */ Spinner.prototype.destroy = function() { //stops the animation function clearTimeout(this.spinnerTick); //removes the Raphael spinner object this.spinnerObject.remove(); this.spinnerObject = null; //animates the opacity of the div to 0 again and hides it (display:none) in the end $('#spinnerFullScreen').animate({ opacity: 0 }, 2000, function() { $('#spinnerFullScreen').hide(); }); };
Step 4: Unleash the power!
With the spinning code in place, it's now time to attach an event to the link, so that when the user clicks it, we show the spinner for a 6 second interval. Personally, I use this for asynchronous requests to the server, and when the request is over I simply remove the spinner.
Note that this code can only be used after all the libraries that the spinner depends on are loaded. You can add this code in the end of the spinner.js file or in another JavaScript file if you want to keep the spinner.js file independent and reusable for other projects.
$(document).ready(function() { $('#createSpinner').click(unleashSpinner); }); function unleashSpinner() { var data = {}; data.centerRadius = 35; data.sectorLength = 50; data.sectorsCount = 10; data.sectorWidth = 20; data.color = 'white'; data.fullScreenOpacity = 0.8; var spinner = new Spinner(data); spinner.create(); setTimeout(function(){spinner.destroy();}, 6000); return false; }
We can reuse the spinner variable as many times as we want.
Wrap Up
The spinner demonstrated in this tutorial can be used in web pages designed not only for mobile devices, but also for "normal" web pages. I already tried this with both methods, and it worked just fine!
In order to test your knowledge, you can work on improving the current spinner implementation in a few unique ways. For instance, you could try changing the format/shape of the sections, enabling clockwise or anti-clockwise movement, or enable a developer to pick any id for the spinner in order to avoid id clashes.
That's it for this time. I hope you enjoyed this tutorial!
Comments