UNPKG

19.8 kBJavaScriptView Raw
1/*
2* Copyright (c) 2011 Róbert Pataki
3*
4* Permission is hereby granted, free of charge, to any person obtaining a copy
5* of this software and associated documentation files (the "Software"), to deal
6* in the Software without restriction, including without limitation the rights
7* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8* copies of the Software, and to permit persons to whom the Software is
9* furnished to do so, subject to the following conditions:
10*
11* The above copyright notice and this permission notice shall be included in
12* all copies or substantial portions of the Software.
13*
14* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20* THE SOFTWARE.
21*
22* ----------------------------------------------------------------------------------------
23*
24* Check out my GitHub: http://github.com/heartcode/
25* Send me an email: heartcode@robertpataki.com
26* Follow me on Twitter: http://twitter.com/#iHeartcode
27* Blog: http://heartcode.robertpataki.com
28*/
29
30/**
31* CanvasLoader uses the HTML5 canvas element in modern browsers and VML in IE6/7/8 to create and animate the most popular preloader shapes (oval, spiral, rectangle, square and rounded rectangle).<br/><br/>
32* It is important to note that CanvasLoader doesn't show up and starts rendering automatically on instantiation. To start rendering and display the loader use the <code>show()</code> method.
33* @module CanvasLoader
34**/
35(function (window) {
36 "use strict";
37 /**
38 * CanvasLoader is a JavaScript UI library that draws and animates circular preloaders using the Canvas HTML object.<br/><br/>
39 * A CanvasLoader instance creates two canvas elements which are placed into a placeholder div (the id of the div has to be passed in the constructor). The second canvas is invisible and used for caching purposes only.<br/><br/>
40 * If no id is passed in the constructor, the canvas objects are paced in the document directly.
41 * @class CanvasLoader
42 * @constructor
43 * @param id {String} The id of the placeholder div
44 * @param opt {Object} Optional parameters<br/><br/>
45 * <strong>Possible values of optional parameters:</strong><br/>
46 * <ul>
47 * <li><strong>id (String):</strong> The id of the CanvasLoader instance</li>
48 * <li><strong>safeVML (Boolean):</strong> If set to true, the amount of CanvasLoader shapes are limited in VML mode. It prevents CPU overkilling when rendering loaders with high density. The default value is true.</li>
49 **/
50 var CanvasLoader = function (parentElm, opt) {
51 if (typeof(opt) == "undefined") { opt = {}; }
52 this.init(parentElm, opt);
53 }, p = CanvasLoader.prototype, engine, engines = ["canvas", "vml"], shapes = ["oval", "spiral", "square", "rect", "roundRect"], cRX = /^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/, ie8 = navigator.appVersion.indexOf("MSIE") !== -1 && parseFloat(navigator.appVersion.split("MSIE")[1]) === 8 ? true : false, canSup = !!document.createElement('canvas').getContext, safeDensity = 40, safeVML = true,
54 /**
55 * Creates a new element with the tag and applies the passed properties on it
56 * @method addEl
57 * @protected
58 * @param tag {String} The tag to be created
59 * @param par {String} The DOM element the new element will be appended to
60 * @param opt {Object} Additional properties passed to the new DOM element
61 * @return {Object} The DOM element
62 */
63 addEl = function (tag, par, opt) {
64 var el = document.createElement(tag), n;
65 for (n in opt) { el[n] = opt[n]; }
66 if(typeof(par) !== "undefined") {
67 par.appendChild(el);
68 }
69 return el;
70 },
71 /**
72 * Sets the css properties on the element
73 * @method setCSS
74 * @protected
75 * @param el {Object} The DOM element to be styled
76 * @param opt {Object} The style properties
77 * @return {Object} The DOM element
78 */
79 setCSS = function (el, opt) {
80 for (var n in opt) { el.style[n] = opt[n]; }
81 return el;
82 },
83 /**
84 * Sets the attributes on the element
85 * @method setAttr
86 * @protected
87 * @param el {Object} The DOM element to add the attributes to
88 * @param opt {Object} The attributes
89 * @return {Object} The DOM element
90 */
91 setAttr = function (el, opt) {
92 for (var n in opt) { el.setAttribute(n, opt[n]); }
93 return el;
94 },
95 /**
96 * Transforms the cache canvas before drawing
97 * @method transCon
98 * @protected
99 * @param x {Object} The canvas context to be transformed
100 * @param x {Number} x translation
101 * @param y {Number} y translation
102 * @param r {Number} Rotation radians
103 */
104 transCon = function(c, x, y, r) {
105 c.save();
106 c.translate(x, y);
107 c.rotate(r);
108 c.translate(-x, -y);
109 c.beginPath();
110 };
111 /**
112 * Initialization method
113 * @method init
114 * @protected
115 * @param id {String} The id of the placeholder div, where the loader will be nested into
116 * @param opt {Object} Optional parameters<br/><br/>
117 * <strong>Possible values of optional parameters:</strong><br/>
118 * <ul>
119 * <li><strong>id (String):</strong> The id of the CanvasLoader instance</li>
120 * <li><strong>safeVML (Boolean):</strong> If set to true, the amount of CanvasLoader shapes are limited in VML mode. It prevents CPU overkilling when rendering loaders with high density. The default value is true.</li>
121 **/
122 p.init = function (parentElm, opt) {
123
124 if (typeof(opt.safeVML) === "boolean") { safeVML = opt.safeVML; }
125
126 /*
127 * Find the containing div by id
128 * If the container element cannot be found we use the document body itself
129 */
130 try {
131 // Look for the parent element
132 if (parentElm !== undefined) {
133 this.mum = parentElm;
134 } else {
135 this.mum = document.body;
136 }
137 } catch (error) {
138 this.mum = document.body;
139 }
140 // Creates the parent div of the loader instance
141 opt.id = typeof (opt.id) !== "undefined" ? opt.id : "canvasLoader";
142 this.cont = addEl("span", this.mum, {id: opt.id});
143 this.cont.setAttribute("class","canvas-loader");
144 if (canSup) {
145 // For browsers with Canvas support...
146 engine = engines[0];
147 // Create the canvas element
148 this.can = addEl("canvas", this.cont);
149 this.con = this.can.getContext("2d");
150 // Create the cache canvas element
151 this.cCan = setCSS(addEl("canvas", this.cont), { display: "none" });
152 this.cCon = this.cCan.getContext("2d");
153 } else {
154 // For browsers without Canvas support...
155 engine = engines[1];
156 // Adds the VML stylesheet
157 if (typeof (CanvasLoader.vmlSheet) === "undefined") {
158 document.getElementsByTagName("head")[0].appendChild(addEl("style"));
159 CanvasLoader.vmlSheet = document.styleSheets[document.styleSheets.length - 1];
160 var a = ["group", "oval", "roundrect", "fill"], n;
161 for (n in a) { CanvasLoader.vmlSheet.addRule(a[n], "behavior:url(#default#VML); position:absolute;"); }
162 }
163 this.vml = addEl("group", this.cont);
164 }
165 // Set the RGB color object
166 this.setColor(this.color);
167 // Draws the shapes on the canvas
168 this.draw();
169 //Hides the preloader
170 setCSS(this.cont, {display: "none"});
171 };
172/////////////////////////////////////////////////////////////////////////////////////////////
173// Property declarations
174 /**
175 * The div we place the canvas object into
176 * @property cont
177 * @protected
178 * @type Object
179 **/
180 p.cont = {};
181 /**
182 * The div we draw the shapes into
183 * @property can
184 * @protected
185 * @type Object
186 **/
187 p.can = {};
188 /**
189 * The canvas context
190 * @property con
191 * @protected
192 * @type Object
193 **/
194 p.con = {};
195 /**
196 * The canvas we use for caching
197 * @property cCan
198 * @protected
199 * @type Object
200 **/
201 p.cCan = {};
202 /**
203 * The context of the cache canvas
204 * @property cCon
205 * @protected
206 * @type Object
207 **/
208 p.cCon = {};
209 /**
210 * Adds a timer for the rendering
211 * @property timer
212 * @protected
213 * @type Boolean
214 **/
215 p.timer = {};
216 /**
217 * The active shape id for rendering
218 * @property activeId
219 * @protected
220 * @type Number
221 **/
222 p.activeId = 0;
223 /**
224 * The diameter of the loader
225 * @property diameter
226 * @protected
227 * @type Number
228 * @default 40
229 **/
230 p.diameter = 40;
231 /**
232 * Sets the diameter of the loader
233 * @method setDiameter
234 * @public
235 * @param diameter {Number} The default value is 40
236 **/
237 p.setDiameter = function (diameter) { this.diameter = Math.round(Math.abs(diameter)); this.redraw(); };
238 /**
239 * Returns the diameter of the loader.
240 * @method getDiameter
241 * @public
242 * @return {Number}
243 **/
244 p.getDiameter = function () { return this.diameter; };
245 /**
246 * The color of the loader shapes in RGB
247 * @property cRGB
248 * @protected
249 * @type Object
250 **/
251 p.cRGB = {};
252 /**
253 * The color of the loader shapes in HEX
254 * @property color
255 * @protected
256 * @type String
257 * @default "#000000"
258 **/
259 p.color = "#000000";
260 /**
261 * Sets hexadecimal color of the loader
262 * @method setColor
263 * @public
264 * @param color {String} The default value is '#000000'
265 **/
266 p.setColor = function (color) { this.color = cRX.test(color) ? color : "#000000"; this.cRGB = this.getRGB(this.color); this.redraw(); };
267 /**
268 * Returns the loader color in a hexadecimal form
269 * @method getColor
270 * @public
271 * @return {String}
272 **/
273 p.getColor = function () { return this.color; };
274 /**
275 * The type of the loader shapes
276 * @property shape
277 * @protected
278 * @type String
279 * @default "oval"
280 **/
281 p.shape = shapes[0];
282 /**
283 * Sets the type of the loader shapes.<br/>
284 * <br/><b>The acceptable values are:</b>
285 * <ul>
286 * <li>'oval'</li>
287 * <li>'spiral'</li>
288 * <li>'square'</li>
289 * <li>'rect'</li>
290 * <li>'roundRect'</li>
291 * </ul>
292 * @method setShape
293 * @public
294 * @param shape {String} The default value is 'oval'
295 **/
296 p.setShape = function (shape) {
297 var n;
298 for (n in shapes) {
299 if (shape === shapes[n]) { this.shape = shape; this.redraw(); break; }
300 }
301 };
302 /**
303 * Returns the type of the loader shapes
304 * @method getShape
305 * @public
306 * @return {String}
307 **/
308 p.getShape = function () { return this.shape; };
309 /**
310 * The number of shapes drawn on the loader canvas
311 * @property density
312 * @protected
313 * @type Number
314 * @default 40
315 **/
316 p.density = 40;
317 /**
318 * Sets the number of shapes drawn on the loader canvas
319 * @method setDensity
320 * @public
321 * @param density {Number} The default value is 40
322 **/
323 p.setDensity = function (density) {
324 if (safeVML && engine === engines[1]) {
325 this.density = Math.round(Math.abs(density)) <= safeDensity ? Math.round(Math.abs(density)) : safeDensity;
326 } else {
327 this.density = Math.round(Math.abs(density));
328 }
329 if (this.density > 360) { this.density = 360; }
330 this.activeId = 0;
331 this.redraw();
332 };
333 /**
334 * Returns the number of shapes drawn on the loader canvas
335 * @method getDensity
336 * @public
337 * @return {Number}
338 **/
339 p.getDensity = function () { return this.density; };
340 /**
341 * The amount of the modified shapes in percent.
342 * @property range
343 * @protected
344 * @type Number
345 **/
346 p.range = 1.3;
347 /**
348 * Sets the amount of the modified shapes in percent.<br/>
349 * With this value the user can set what range of the shapes should be scaled and/or faded. The shapes that are out of this range will be scaled and/or faded with a minimum amount only.<br/>
350 * This minimum amount is 0.1 which means every shape which is out of the range is scaled and/or faded to 10% of the original values.<br/>
351 * The visually acceptable range value should be between 0.4 and 1.5.
352 * @method setRange
353 * @public
354 * @param range {Number} The default value is 1.3
355 **/
356 p.setRange = function (range) { this.range = Math.abs(range); this.redraw(); };
357 /**
358 * Returns the modified shape range in percent
359 * @method getRange
360 * @public
361 * @return {Number}
362 **/
363 p.getRange = function () { return this.range; };
364 /**
365 * The speed of the loader animation
366 * @property speed
367 * @protected
368 * @type Number
369 **/
370 p.speed = 2;
371 /**
372 * Sets the speed of the loader animation.<br/>
373 * This value tells the loader how many shapes to skip by each tick.<br/>
374 * Using the right combination of the <code>setFPS</code> and the <code>setSpeed</code> methods allows the users to optimize the CPU usage of the loader whilst keeping the animation on a visually pleasing level.
375 * @method setSpeed
376 * @public
377 * @param speed {Number} The default value is 2
378 **/
379 p.setSpeed = function (speed) { this.speed = Math.round(Math.abs(speed)); };
380 /**
381 * Returns the speed of the loader animation
382 * @method getSpeed
383 * @public
384 * @return {Number}
385 **/
386 p.getSpeed = function () { return this.speed; };
387 /**
388 * The FPS value of the loader animation rendering
389 * @property fps
390 * @protected
391 * @type Number
392 **/
393 p.fps = 24;
394 /**
395 * Sets the rendering frequency.<br/>
396 * This value tells the loader how many times to refresh and modify the canvas in 1 second.<br/>
397 * Using the right combination of the <code>setSpeed</code> and the <code>setFPS</code> methods allows the users to optimize the CPU usage of the loader whilst keeping the animation on a visually pleasing level.
398 * @method setFPS
399 * @public
400 * @param fps {Number} The default value is 24
401 **/
402 p.setFPS = function (fps) { this.fps = Math.round(Math.abs(fps)); this.reset(); };
403 /**
404 * Returns the fps of the loader
405 * @method getFPS
406 * @public
407 * @return {Number}
408 **/
409 p.getFPS = function () { return this.fps; };
410// End of Property declarations
411/////////////////////////////////////////////////////////////////////////////////////////////
412 /**
413 * Return the RGB values of the passed color
414 * @method getRGB
415 * @protected
416 * @param color {String} The HEX color value to be converted to RGB
417 */
418 p.getRGB = function (c) {
419 c = c.charAt(0) === "#" ? c.substring(1, 7) : c;
420 return {r: parseInt(c.substring(0, 2), 16), g: parseInt(c.substring(2, 4), 16), b: parseInt(c.substring(4, 6), 16) };
421 };
422 /**
423 * Draw the shapes on the canvas
424 * @method draw
425 * @protected
426 */
427 p.draw = function () {
428 var i = 0, size, w, h, x, y, ang, rads, rad, de = this.density, animBits = Math.round(de * this.range), bitMod, minBitMod = 0, s, g, sh, f, d = 1000, arc = 0, c = this.cCon, di = this.diameter, e = 0.47;
429 if (engine === engines[0]) {
430 c.clearRect(0, 0, d, d);
431 setAttr(this.can, {width: di, height: di});
432 setAttr(this.cCan, {width: di, height: di});
433 while (i < de) {
434 bitMod = i <= animBits ? 1 - ((1 - minBitMod) / animBits * i) : bitMod = minBitMod;
435 ang = 270 - 360 / de * i;
436 rads = ang / 180 * Math.PI;
437 c.fillStyle = "rgba(" + this.cRGB.r + "," + this.cRGB.g + "," + this.cRGB.b + "," + bitMod.toString() + ")";
438 switch (this.shape) {
439 case shapes[0]:
440 case shapes[1]:
441 size = di * 0.07;
442 x = di * e + Math.cos(rads) * (di * e - size) - di * e;
443 y = di * e + Math.sin(rads) * (di * e - size) - di * e;
444 c.beginPath();
445 if (this.shape === shapes[1]) { c.arc(di * 0.5 + x, di * 0.5 + y, size * bitMod, 0, Math.PI * 2, false); } else { c.arc(di * 0.5 + x, di * 0.5 + y, size, 0, Math.PI * 2, false); }
446 break;
447 case shapes[2]:
448 size = di * 0.12;
449 x = Math.cos(rads) * (di * e - size) + di * 0.5;
450 y = Math.sin(rads) * (di * e - size) + di * 0.5;
451 transCon(c, x, y, rads);
452 c.fillRect(x, y - size * 0.5, size, size);
453 break;
454 case shapes[3]:
455 case shapes[4]:
456 w = di * 0.3;
457 h = w * 0.27;
458 x = Math.cos(rads) * (h + (di - h) * 0.13) + di * 0.5;
459 y = Math.sin(rads) * (h + (di - h) * 0.13) + di * 0.5;
460 transCon(c, x, y, rads);
461 if(this.shape === shapes[3]) {
462 c.fillRect(x, y - h * 0.5, w, h);
463 } else {
464 rad = h * 0.55;
465 c.moveTo(x + rad, y - h * 0.5);
466 c.lineTo(x + w - rad, y - h * 0.5);
467 c.quadraticCurveTo(x + w, y - h * 0.5, x + w, y - h * 0.5 + rad);
468 c.lineTo(x + w, y - h * 0.5 + h - rad);
469 c.quadraticCurveTo(x + w, y - h * 0.5 + h, x + w - rad, y - h * 0.5 + h);
470 c.lineTo(x + rad, y - h * 0.5 + h);
471 c.quadraticCurveTo(x, y - h * 0.5 + h, x, y - h * 0.5 + h - rad);
472 c.lineTo(x, y - h * 0.5 + rad);
473 c.quadraticCurveTo(x, y - h * 0.5, x + rad, y - h * 0.5);
474 }
475 break;
476 }
477 c.closePath();
478 c.fill();
479 c.restore();
480 ++i;
481 }
482 } else {
483 setCSS(this.cont, {width: di, height: di});
484 setCSS(this.vml, {width: di, height: di});
485 switch (this.shape) {
486 case shapes[0]:
487 case shapes[1]:
488 sh = "oval";
489 size = d * 0.14;
490 break;
491 case shapes[2]:
492 sh = "roundrect";
493 size = d * 0.12;
494 break;
495 case shapes[3]:
496 case shapes[4]:
497 sh = "roundrect";
498 size = d * 0.3;
499 break;
500 }
501 w = h = size;
502 x = d * 0.5 - h;
503 y = -h * 0.5;
504 while (i < de) {
505 bitMod = i <= animBits ? 1 - ((1 - minBitMod) / animBits * i) : bitMod = minBitMod;
506 ang = 270 - 360 / de * i;
507 switch (this.shape) {
508 case shapes[1]:
509 w = h = size * bitMod;
510 x = d * 0.5 - size * 0.5 - size * bitMod * 0.5;
511 y = (size - size * bitMod) * 0.5;
512 break;
513 case shapes[0]:
514 case shapes[2]:
515 if (ie8) {
516 y = 0;
517 if(this.shape === shapes[2]) {
518 x = d * 0.5 -h * 0.5;
519 }
520 }
521 break;
522 case shapes[3]:
523 case shapes[4]:
524 w = size * 0.95;
525 h = w * 0.28;
526 if (ie8) {
527 x = 0;
528 y = d * 0.5 - h * 0.5;
529 } else {
530 x = d * 0.5 - w;
531 y = -h * 0.5;
532 }
533 arc = this.shape === shapes[4] ? 0.6 : 0;
534 break;
535 }
536 g = setAttr(setCSS(addEl("group", this.vml), {width: d, height: d, rotation: ang}), {coordsize: d + "," + d, coordorigin: -d * 0.5 + "," + (-d * 0.5)});
537 s = setCSS(addEl(sh, g, {stroked: false, arcSize: arc}), { width: w, height: h, top: y, left: x});
538 f = addEl("fill", s, {color: this.color, opacity: bitMod});
539 ++i;
540 }
541 }
542 this.tick(true);
543 };
544 /**
545 * Cleans the canvas
546 * @method clean
547 * @protected
548 */
549 p.clean = function () {
550 if (engine === engines[0]) {
551 this.con.clearRect(0, 0, 1000, 1000);
552 } else {
553 var v = this.vml;
554 if (v.hasChildNodes()) {
555 while (v.childNodes.length >= 1) {
556 v.removeChild(v.firstChild);
557 }
558 }
559 }
560 };
561 /**
562 * Redraws the canvas
563 * @method redraw
564 * @protected
565 */
566 p.redraw = function () {
567 this.clean();
568 this.draw();
569 };
570 /**
571 * Resets the timer
572 * @method reset
573 * @protected
574 */
575 p.reset = function () {
576 if (typeof (this.timer) === "number") {
577 this.hide();
578 this.show();
579 }
580 };
581 /**
582 * Renders the loader animation
583 * @method tick
584 * @protected
585 */
586 p.tick = function (init) {
587 var c = this.con, di = this.diameter;
588 if (!init) { this.activeId += 360 / this.density * this.speed; }
589 if (engine === engines[0]) {
590 c.clearRect(0, 0, di, di);
591 transCon(c, di * 0.5, di * 0.5, this.activeId / 180 * Math.PI);
592 c.drawImage(this.cCan, 0, 0, di, di);
593 c.restore();
594 } else {
595 if (this.activeId >= 360) { this.activeId -= 360; }
596 setCSS(this.vml, {rotation:this.activeId});
597 }
598 };
599 /**
600 * Shows the rendering of the loader animation
601 * @method show
602 * @public
603 */
604 p.show = function () {
605 if (typeof (this.timer) !== "number") {
606 var t = this;
607 this.timer = self.setInterval(function () { t.tick(); }, Math.round(1000 / this.fps));
608 setCSS(this.cont, {display: "block"});
609 }
610 };
611 /**
612 * Stops the rendering of the loader animation and hides the loader
613 * @method hide
614 * @public
615 */
616 p.hide = function () {
617 if (typeof (this.timer) === "number") {
618 clearInterval(this.timer);
619 delete this.timer;
620 setCSS(this.cont, {display: "none"});
621 }
622 };
623 /**
624 * Removes the CanvasLoader instance and all its references
625 * @method kill
626 * @public
627 */
628 p.kill = function () {
629 var c = this.cont;
630 if (typeof (this.timer) === "number") { this.hide(); }
631 if (engine === engines[0]) {
632 c.removeChild(this.can);
633 c.removeChild(this.cCan);
634 } else {
635 c.removeChild(this.vml);
636 }
637 var n;
638 for (n in this) { delete this[n]; }
639 };
640 window.CanvasLoader = CanvasLoader;
641}(window));
\No newline at end of file