/** * jquery-circle-progress - jQuery Plugin to draw animated circular progress bars: * {@link http://kottenator.github.io/jquery-circle-progress/} * * @author Rostyslav Bryzgunov * @version 1.2.1 * @licence MIT * @preserve */ // UMD factory - https://github.com/umdjs/umd/blob/d31bb6ee7098715e019f52bdfe27b3e4bfd2b97e/templates/jqueryPlugin.js // Uses AMD, CommonJS or browser globals to create a jQuery plugin. (function(factory) { if (typeof define === 'function' && define.amd) { // AMD - register as an anonymous module define(['jquery'], factory); } else if (typeof module === 'object' && module.exports) { // Node/CommonJS var $ = require('jquery'); factory($); module.exports = $; } else { // Browser globals factory(jQuery); } })(function($) { /** * Inner implementation of the circle progress bar. * The class is not exposed _yet_ but you can create an instance through jQuery method call. * * @param {object} config - You can customize any class member (property or method). * @class * @alias CircleProgress */ function CircleProgress(config) { this.init(config); } CircleProgress.prototype = { //--------------------------------------- public options --------------------------------------- /** * This is the only required option. It should be from `0.0` to `1.0`. * @type {number} * @default 0.0 */ value: 0.0, /** * Size of the canvas in pixels. * It's a square so we need only one dimension. * @type {number} * @default 100.0 */ size: 100.0, /** * Initial angle for `0.0` value in radians. * @type {number} * @default -Math.PI */ startAngle: -Math.PI, /** * Width of the arc in pixels. * If it's `'auto'` - the value is calculated as `[this.size]{@link CircleProgress#size} / 14`. * @type {number|string} * @default 'auto' */ thickness: 'auto', /** * Fill of the arc. You may set it to: * * - solid color: * - `'#3aeabb'` * - `{ color: '#3aeabb' }` * - `{ color: 'rgba(255, 255, 255, .3)' }` * - linear gradient _(left to right)_: * - `{ gradient: ['#3aeabb', '#fdd250'], gradientAngle: Math.PI / 4 }` * - `{ gradient: ['red', 'green', 'blue'], gradientDirection: [x0, y0, x1, y1] }` * - `{ gradient: [["red", .2], ["green", .3], ["blue", .8]] }` * - image: * - `{ image: 'http://i.imgur.com/pT0i89v.png' }` * - `{ image: imageObject }` * - `{ color: 'lime', image: 'http://i.imgur.com/pT0i89v.png' }` - * color displayed until the image is loaded * * @default {gradient: ['#3aeabb', '#fdd250']} */ fill: { gradient: ['#3aeabb', '#fdd250'] }, /** * Color of the "empty" arc. Only a color fill supported by now. * @type {string} * @default 'rgba(0, 0, 0, .1)' */ emptyFill: 'rgba(0, 0, 0, .1)', /** * jQuery Animation config. * You can pass `false` to disable the animation. * @see http://api.jquery.com/animate/ * @type {object|boolean} * @default {duration: 1200, easing: 'circleProgressEasing'} */ animation: { duration: 1200, easing: 'circleProgressEasing' }, /** * Default animation starts at `0.0` and ends at specified `value`. Let's call this _direct animation_. * If you want to make _reversed animation_ - set `animationStartValue: 1.0`. * Also you may specify any other value from `0.0` to `1.0`. * @type {number} * @default 0.0 */ animationStartValue: 0.0, /** * Reverse animation and arc draw. * By default, the arc is filled from `0.0` to `value`, _clockwise_. * With `reverse: true` the arc is filled from `1.0` to `value`, _counter-clockwise_. * @type {boolean} * @default false */ reverse: false, /** * Arc line cap: `'butt'`, `'round'` or `'square'` - * [read more]{@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D.lineCap}. * @type {string} * @default 'butt' */ lineCap: 'butt', /** * Canvas insertion mode: append or prepend it into the parent element? * @type {string} * @default 'prepend' */ insertMode: 'prepend', //------------------------------ protected properties and methods ------------------------------ /** * Link to {@link CircleProgress} constructor. * @protected */ constructor: CircleProgress, /** * Container element. Should be passed into constructor config. * @protected * @type {jQuery} */ el: null, /** * Canvas element. Automatically generated and prepended to [this.el]{@link CircleProgress#el}. * @protected * @type {HTMLCanvasElement} */ canvas: null, /** * 2D-context of [this.canvas]{@link CircleProgress#canvas}. * @protected * @type {CanvasRenderingContext2D} */ ctx: null, /** * Radius of the outer circle. Automatically calculated as `[this.size]{@link CircleProgress#size} / 2`. * @protected * @type {number} */ radius: 0.0, /** * Fill of the main arc. Automatically calculated, depending on [this.fill]{@link CircleProgress#fill} option. * @protected * @type {string|CanvasGradient|CanvasPattern} */ arcFill: null, /** * Last rendered frame value. * @protected * @type {number} */ lastFrameValue: 0.0, /** * Init/re-init the widget. * * Throws a jQuery event: * * - `circle-inited(jqEvent)` * * @param {object} config - You can customize any class member (property or method). */ init: function(config) { $.extend(this, config); this.radius = this.size / 2; this.initWidget(); this.initFill(); this.draw(); this.el.trigger('circle-inited'); }, /** * Initialize ``. * @protected */ initWidget: function() { if (!this.canvas) this.canvas = $('')[this.insertMode == 'prepend' ? 'prependTo' : 'appendTo'](this.el)[0]; var canvas = this.canvas; canvas.width = this.size; canvas.height = this.size; this.ctx = canvas.getContext('2d'); if (window.devicePixelRatio > 1) { var scaleBy = window.devicePixelRatio; canvas.style.width = canvas.style.height = this.size + 'px'; canvas.width = canvas.height = this.size * scaleBy; this.ctx.scale(scaleBy, scaleBy); } }, /** * This method sets [this.arcFill]{@link CircleProgress#arcFill}. * It could do this async (on image load). * @protected */ initFill: function() { var self = this, fill = this.fill, ctx = this.ctx, size = this.size; if (!fill) throw Error("The fill is not specified!"); if (typeof fill == 'string') fill = {color: fill}; if (fill.color) this.arcFill = fill.color; if (fill.gradient) { var gr = fill.gradient; if (gr.length == 1) { this.arcFill = gr[0]; } else if (gr.length > 1) { var ga = fill.gradientAngle || 0, // gradient direction angle; 0 by default gd = fill.gradientDirection || [ size / 2 * (1 - Math.cos(ga)), // x0 size / 2 * (1 + Math.sin(ga)), // y0 size / 2 * (1 + Math.cos(ga)), // x1 size / 2 * (1 - Math.sin(ga)) // y1 ]; var lg = ctx.createLinearGradient.apply(ctx, gd); for (var i = 0; i < gr.length; i++) { var color = gr[i], pos = i / (gr.length - 1); if ($.isArray(color)) { pos = color[1]; color = color[0]; } lg.addColorStop(pos, color); } this.arcFill = lg; } } if (fill.image) { var img; if (fill.image instanceof Image) { img = fill.image; } else { img = new Image(); img.src = fill.image; } if (img.complete) setImageFill(); else img.onload = setImageFill; } function setImageFill() { var bg = $('')[0]; bg.width = self.size; bg.height = self.size; bg.getContext('2d').drawImage(img, 0, 0, size, size); self.arcFill = self.ctx.createPattern(bg, 'no-repeat'); self.drawFrame(self.lastFrameValue); } }, /** * Draw the circle. * @protected */ draw: function() { if (this.animation) this.drawAnimated(this.value); else this.drawFrame(this.value); }, /** * Draw a single animation frame. * @protected * @param {number} v - Frame value. */ drawFrame: function(v) { this.lastFrameValue = v; this.ctx.clearRect(0, 0, this.size, this.size); this.drawEmptyArc(v); this.drawArc(v); }, /** * Draw the arc (part of the circle). * @protected * @param {number} v - Frame value. */ drawArc: function(v) { if (v === 0) return; var ctx = this.ctx, r = this.radius, t = this.getThickness(), a = this.startAngle; ctx.save(); ctx.beginPath(); if (!this.reverse) { ctx.arc(r, r, r - t / 2, a, a + Math.PI * 2 * v); } else { ctx.arc(r, r, r - t / 2, a - Math.PI * 2 * v, a); } ctx.lineWidth = t; ctx.lineCap = this.lineCap; ctx.strokeStyle = this.arcFill; ctx.stroke(); ctx.restore(); }, /** * Draw the _empty (background)_ arc (part of the circle). * @protected * @param {number} v - Frame value. */ drawEmptyArc: function(v) { var ctx = this.ctx, r = this.radius, t = this.getThickness(), a = this.startAngle; if (v < 1) { ctx.save(); ctx.beginPath(); if (v <= 0) { ctx.arc(r, r, r - t / 2, 0, Math.PI * 2); } else { if (!this.reverse) { ctx.arc(r, r, r - t / 2, a + Math.PI * 2 * v, a); } else { ctx.arc(r, r, r - t / 2, a, a - Math.PI * 2 * v); } } ctx.lineWidth = t; ctx.strokeStyle = this.emptyFill; ctx.stroke(); ctx.restore(); } }, /** * Animate the progress bar. * * Throws 3 jQuery events: * * - `circle-animation-start(jqEvent)` * - `circle-animation-progress(jqEvent, animationProgress, stepValue)` - multiple event * animationProgress: from `0.0` to `1.0`; stepValue: from `0.0` to `value` * - `circle-animation-end(jqEvent)` * * @protected * @param {number} v - Final value. */ drawAnimated: function(v) { var self = this, el = this.el, canvas = $(this.canvas); // stop previous animation before new "start" event is triggered canvas.stop(true, false); el.trigger('circle-animation-start'); canvas .css({animationProgress: 0}) .animate({animationProgress: 1}, $.extend({}, this.animation, { step: function(animationProgress) { var stepValue = self.animationStartValue * (1 - animationProgress) + v * animationProgress; self.drawFrame(stepValue); el.trigger('circle-animation-progress', [animationProgress, stepValue]); } })) .promise() .always(function() { // trigger on both successful & failure animation end el.trigger('circle-animation-end'); }); }, /** * Get the circle thickness. * @see CircleProgress#thickness * @protected * @returns {number} */ getThickness: function() { return $.isNumeric(this.thickness) ? this.thickness : this.size / 14; }, /** * Get current value. * @protected * @return {number} */ getValue: function() { return this.value; }, /** * Set current value (with smooth animation transition). * @protected * @param {number} newValue */ setValue: function(newValue) { if (this.animation) this.animationStartValue = this.lastFrameValue; this.value = newValue; this.draw(); } }; //----------------------------------- Initiating jQuery plugin ----------------------------------- $.circleProgress = { // Default options (you may override them) defaults: CircleProgress.prototype }; // ease-in-out-cubic $.easing.circleProgressEasing = function(x, t, b, c, d) { if ((t /= d / 2) < 1) return c / 2 * t * t * t + b; return c / 2 * ((t -= 2) * t * t + 2) + b; }; /** * Creates an instance of {@link CircleProgress}. * Produces [init event]{@link CircleProgress#init} and [animation events]{@link CircleProgress#drawAnimated}. * * @param {object} [configOrCommand] - Config object or command name. * * Config example (you can specify any {@link CircleProgress} property): * * ```js * { value: 0.75, size: 50, animation: false } * ``` * * Commands: * * ```js * el.circleProgress('widget'); // get the * el.circleProgress('value'); // get the value * el.circleProgress('value', newValue); // update the value * el.circleProgress('redraw'); // redraw the circle * el.circleProgress(); // the same as 'redraw' * ``` * * @param {string} [commandArgument] - Some commands (like `'value'`) may require an argument. * @see CircleProgress * @alias "$(...).circleProgress" */ $.fn.circleProgress = function(configOrCommand, commandArgument) { var dataName = 'circle-progress', firstInstance = this.data(dataName); if (configOrCommand == 'widget') { if (!firstInstance) throw Error('Calling "widget" method on not initialized instance is forbidden'); return firstInstance.canvas; } if (configOrCommand == 'value') { if (!firstInstance) throw Error('Calling "value" method on not initialized instance is forbidden'); if (typeof commandArgument == 'undefined') { return firstInstance.getValue(); } else { var newValue = arguments[1]; return this.each(function() { $(this).data(dataName).setValue(newValue); }); } } return this.each(function() { var el = $(this), instance = el.data(dataName), config = $.isPlainObject(configOrCommand) ? configOrCommand : {}; if (instance) { instance.init(config); } else { var initialConfig = $.extend({}, el.data()); if (typeof initialConfig.fill == 'string') initialConfig.fill = JSON.parse(initialConfig.fill); if (typeof initialConfig.animation == 'string') initialConfig.animation = JSON.parse(initialConfig.animation); config = $.extend(initialConfig, config); config.el = el; instance = new CircleProgress(config); el.data(dataName, instance); } }); }; });