circle-progress.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. /**
  2. * jquery-circle-progress - jQuery Plugin to draw animated circular progress bars:
  3. * {@link http://kottenator.github.io/jquery-circle-progress/}
  4. *
  5. * @author Rostyslav Bryzgunov <kottenator@gmail.com>
  6. * @version 1.2.2
  7. * @licence MIT
  8. * @preserve
  9. */
  10. // UMD factory - https://github.com/umdjs/umd/blob/d31bb6ee7098715e019f52bdfe27b3e4bfd2b97e/templates/jqueryPlugin.js
  11. // Uses AMD, CommonJS or browser globals to create a jQuery plugin.
  12. (function(factory) {
  13. if (typeof define === 'function' && define.amd) {
  14. // AMD - register as an anonymous module
  15. define(['jquery'], factory);
  16. } else if (typeof module === 'object' && module.exports) {
  17. // Node/CommonJS
  18. var $ = require('jquery');
  19. factory($);
  20. module.exports = $;
  21. } else {
  22. // Browser globals
  23. factory(jQuery);
  24. }
  25. })(function($) {
  26. /**
  27. * Inner implementation of the circle progress bar.
  28. * The class is not exposed _yet_ but you can create an instance through jQuery method call.
  29. *
  30. * @param {object} config - You can customize any class member (property or method).
  31. * @class
  32. * @alias CircleProgress
  33. */
  34. function CircleProgress(config) {
  35. this.init(config);
  36. }
  37. CircleProgress.prototype = {
  38. //--------------------------------------- public options ---------------------------------------
  39. /**
  40. * This is the only required option. It should be from `0.0` to `1.0`.
  41. * @type {number}
  42. * @default 0.0
  43. */
  44. value: 0.0,
  45. /**
  46. * Size of the canvas in pixels.
  47. * It's a square so we need only one dimension.
  48. * @type {number}
  49. * @default 100.0
  50. */
  51. size: 100.0,
  52. /**
  53. * Initial angle for `0.0` value in radians.
  54. * @type {number}
  55. * @default -Math.PI
  56. */
  57. startAngle: -Math.PI,
  58. /**
  59. * Width of the arc in pixels.
  60. * If it's `'auto'` - the value is calculated as `[this.size]{@link CircleProgress#size} / 14`.
  61. * @type {number|string}
  62. * @default 'auto'
  63. */
  64. thickness: 'auto',
  65. /**
  66. * Fill of the arc. You may set it to:
  67. *
  68. * - solid color:
  69. * - `'#3aeabb'`
  70. * - `{ color: '#3aeabb' }`
  71. * - `{ color: 'rgba(255, 255, 255, .3)' }`
  72. * - linear gradient _(left to right)_:
  73. * - `{ gradient: ['#3aeabb', '#fdd250'], gradientAngle: Math.PI / 4 }`
  74. * - `{ gradient: ['red', 'green', 'blue'], gradientDirection: [x0, y0, x1, y1] }`
  75. * - `{ gradient: [["red", .2], ["green", .3], ["blue", .8]] }`
  76. * - image:
  77. * - `{ image: 'http://i.imgur.com/pT0i89v.png' }`
  78. * - `{ image: imageObject }`
  79. * - `{ color: 'lime', image: 'http://i.imgur.com/pT0i89v.png' }` -
  80. * color displayed until the image is loaded
  81. *
  82. * @default {gradient: ['#3aeabb', '#fdd250']}
  83. */
  84. fill: {
  85. gradient: ['#3aeabb', '#fdd250']
  86. },
  87. /**
  88. * Color of the "empty" arc. Only a color fill supported by now.
  89. * @type {string}
  90. * @default 'rgba(0, 0, 0, .1)'
  91. */
  92. emptyFill: 'rgba(0, 0, 0, .1)',
  93. /**
  94. * jQuery Animation config.
  95. * You can pass `false` to disable the animation.
  96. * @see http://api.jquery.com/animate/
  97. * @type {object|boolean}
  98. * @default {duration: 1200, easing: 'circleProgressEasing'}
  99. */
  100. animation: {
  101. duration: 100,
  102. easing: 'circleProgressEasing'
  103. },
  104. /**
  105. * Default animation starts at `0.0` and ends at specified `value`. Let's call this _direct animation_.
  106. * If you want to make _reversed animation_ - set `animationStartValue: 1.0`.
  107. * Also you may specify any other value from `0.0` to `1.0`.
  108. * @type {number}
  109. * @default 0.0
  110. */
  111. animationStartValue: 0.0,
  112. /**
  113. * Reverse animation and arc draw.
  114. * By default, the arc is filled from `0.0` to `value`, _clockwise_.
  115. * With `reverse: true` the arc is filled from `1.0` to `value`, _counter-clockwise_.
  116. * @type {boolean}
  117. * @default false
  118. */
  119. reverse: false,
  120. /**
  121. * Arc line cap: `'butt'`, `'round'` or `'square'` -
  122. * [read more]{@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D.lineCap}.
  123. * @type {string}
  124. * @default 'butt'
  125. */
  126. lineCap: 'butt',
  127. /**
  128. * Canvas insertion mode: append or prepend it into the parent element?
  129. * @type {string}
  130. * @default 'prepend'
  131. */
  132. insertMode: 'prepend',
  133. //------------------------------ protected properties and methods ------------------------------
  134. /**
  135. * Link to {@link CircleProgress} constructor.
  136. * @protected
  137. */
  138. constructor: CircleProgress,
  139. /**
  140. * Container element. Should be passed into constructor config.
  141. * @protected
  142. * @type {jQuery}
  143. */
  144. el: null,
  145. /**
  146. * Canvas element. Automatically generated and prepended to [this.el]{@link CircleProgress#el}.
  147. * @protected
  148. * @type {HTMLCanvasElement}
  149. */
  150. canvas: null,
  151. /**
  152. * 2D-context of [this.canvas]{@link CircleProgress#canvas}.
  153. * @protected
  154. * @type {CanvasRenderingContext2D}
  155. */
  156. ctx: null,
  157. /**
  158. * Radius of the outer circle. Automatically calculated as `[this.size]{@link CircleProgress#size} / 2`.
  159. * @protected
  160. * @type {number}
  161. */
  162. radius: 0.0,
  163. /**
  164. * Fill of the main arc. Automatically calculated, depending on [this.fill]{@link CircleProgress#fill} option.
  165. * @protected
  166. * @type {string|CanvasGradient|CanvasPattern}
  167. */
  168. arcFill: null,
  169. /**
  170. * Last rendered frame value.
  171. * @protected
  172. * @type {number}
  173. */
  174. lastFrameValue: 0.0,
  175. /**
  176. * Init/re-init the widget.
  177. *
  178. * Throws a jQuery event:
  179. *
  180. * - `circle-inited(jqEvent)`
  181. *
  182. * @param {object} config - You can customize any class member (property or method).
  183. */
  184. init: function(config) {
  185. $.extend(this, config);
  186. this.radius = this.size / 2;
  187. this.initWidget();
  188. this.initFill();
  189. this.draw();
  190. this.el.trigger('circle-inited');
  191. },
  192. /**
  193. * Initialize `<canvas>`.
  194. * @protected
  195. */
  196. initWidget: function() {
  197. if (!this.canvas)
  198. this.canvas = $('<canvas>')[this.insertMode == 'prepend' ? 'prependTo' : 'appendTo'](this.el)[0];
  199. var canvas = this.canvas;
  200. canvas.width = this.size;
  201. canvas.height = this.size;
  202. this.ctx = canvas.getContext('2d');
  203. if (window.devicePixelRatio > 1) {
  204. var scaleBy = window.devicePixelRatio;
  205. canvas.style.width = canvas.style.height = this.size + 'px';
  206. canvas.width = canvas.height = this.size * scaleBy;
  207. this.ctx.scale(scaleBy, scaleBy);
  208. }
  209. },
  210. /**
  211. * This method sets [this.arcFill]{@link CircleProgress#arcFill}.
  212. * It could do this async (on image load).
  213. * @protected
  214. */
  215. initFill: function() {
  216. var self = this,
  217. fill = this.fill,
  218. ctx = this.ctx,
  219. size = this.size;
  220. if (!fill)
  221. throw Error("The fill is not specified!");
  222. if (typeof fill == 'string')
  223. fill = {color: fill};
  224. if (fill.color)
  225. this.arcFill = fill.color;
  226. if (fill.gradient) {
  227. var gr = fill.gradient;
  228. if (gr.length == 1) {
  229. this.arcFill = gr[0];
  230. } else if (gr.length > 1) {
  231. var ga = fill.gradientAngle || 0, // gradient direction angle; 0 by default
  232. gd = fill.gradientDirection || [
  233. size / 2 * (1 - Math.cos(ga)), // x0
  234. size / 2 * (1 + Math.sin(ga)), // y0
  235. size / 2 * (1 + Math.cos(ga)), // x1
  236. size / 2 * (1 - Math.sin(ga)) // y1
  237. ];
  238. var lg = ctx.createLinearGradient.apply(ctx, gd);
  239. for (var i = 0; i < gr.length; i++) {
  240. var color = gr[i],
  241. pos = i / (gr.length - 1);
  242. if ($.isArray(color)) {
  243. pos = color[1];
  244. color = color[0];
  245. }
  246. lg.addColorStop(pos, color);
  247. }
  248. this.arcFill = lg;
  249. }
  250. }
  251. if (fill.image) {
  252. var img;
  253. if (fill.image instanceof Image) {
  254. img = fill.image;
  255. } else {
  256. img = new Image();
  257. img.src = fill.image;
  258. }
  259. if (img.complete)
  260. setImageFill();
  261. else
  262. img.onload = setImageFill;
  263. }
  264. function setImageFill() {
  265. var bg = $('<canvas>')[0];
  266. bg.width = self.size;
  267. bg.height = self.size;
  268. bg.getContext('2d').drawImage(img, 0, 0, size, size);
  269. self.arcFill = self.ctx.createPattern(bg, 'no-repeat');
  270. self.drawFrame(self.lastFrameValue);
  271. }
  272. },
  273. /**
  274. * Draw the circle.
  275. * @protected
  276. */
  277. draw: function() {
  278. if (this.animation)
  279. this.drawAnimated(this.value);
  280. else
  281. this.drawFrame(this.value);
  282. },
  283. /**
  284. * Draw a single animation frame.
  285. * @protected
  286. * @param {number} v - Frame value.
  287. */
  288. drawFrame: function(v) {
  289. this.lastFrameValue = v;
  290. this.ctx.clearRect(0, 0, this.size, this.size);
  291. this.drawEmptyArc(v);
  292. this.drawArc(v);
  293. },
  294. /**
  295. * Draw the arc (part of the circle).
  296. * @protected
  297. * @param {number} v - Frame value.
  298. */
  299. drawArc: function(v) {
  300. if (v === 0)
  301. return;
  302. var ctx = this.ctx,
  303. r = this.radius,
  304. t = this.getThickness(),
  305. a = this.startAngle;
  306. ctx.save();
  307. ctx.beginPath();
  308. if (!this.reverse) {
  309. ctx.arc(r, r, r - t / 2, a, a + Math.PI * 2 * v);
  310. } else {
  311. ctx.arc(r, r, r - t / 2, a - Math.PI * 2 * v, a);
  312. }
  313. ctx.lineWidth = t;
  314. ctx.lineCap = this.lineCap;
  315. ctx.strokeStyle = this.arcFill;
  316. ctx.stroke();
  317. ctx.restore();
  318. },
  319. /**
  320. * Draw the _empty (background)_ arc (part of the circle).
  321. * @protected
  322. * @param {number} v - Frame value.
  323. */
  324. drawEmptyArc: function(v) {
  325. var ctx = this.ctx,
  326. r = this.radius,
  327. t = this.getThickness(),
  328. a = this.startAngle;
  329. if (v < 1) {
  330. ctx.save();
  331. ctx.beginPath();
  332. if (v <= 0) {
  333. ctx.arc(r, r, r - t / 2, 0, Math.PI * 2);
  334. } else {
  335. if (!this.reverse) {
  336. ctx.arc(r, r, r - t / 2, a + Math.PI * 2 * v, a);
  337. } else {
  338. ctx.arc(r, r, r - t / 2, a, a - Math.PI * 2 * v);
  339. }
  340. }
  341. ctx.lineWidth = t;
  342. ctx.strokeStyle = this.emptyFill;
  343. ctx.stroke();
  344. ctx.restore();
  345. }
  346. },
  347. /**
  348. * Animate the progress bar.
  349. *
  350. * Throws 3 jQuery events:
  351. *
  352. * - `circle-animation-start(jqEvent)`
  353. * - `circle-animation-progress(jqEvent, animationProgress, stepValue)` - multiple event
  354. * animationProgress: from `0.0` to `1.0`; stepValue: from `0.0` to `value`
  355. * - `circle-animation-end(jqEvent)`
  356. *
  357. * @protected
  358. * @param {number} v - Final value.
  359. */
  360. drawAnimated: function(v) {
  361. var self = this,
  362. el = this.el,
  363. canvas = $(this.canvas);
  364. // stop previous animation before new "start" event is triggered
  365. canvas.stop(true, false);
  366. el.trigger('circle-animation-start');
  367. canvas
  368. .css({animationProgress: 0})
  369. .animate({animationProgress: 1}, $.extend({}, this.animation, {
  370. step: function(animationProgress) {
  371. var stepValue = self.animationStartValue * (1 - animationProgress) + v * animationProgress;
  372. self.drawFrame(stepValue);
  373. el.trigger('circle-animation-progress', [animationProgress, stepValue]);
  374. }
  375. }))
  376. .promise()
  377. .always(function() {
  378. // trigger on both successful & failure animation end
  379. el.trigger('circle-animation-end');
  380. });
  381. },
  382. /**
  383. * Get the circle thickness.
  384. * @see CircleProgress#thickness
  385. * @protected
  386. * @returns {number}
  387. */
  388. getThickness: function() {
  389. return $.isNumeric(this.thickness) ? this.thickness : this.size / 14;
  390. },
  391. /**
  392. * Get current value.
  393. * @protected
  394. * @return {number}
  395. */
  396. getValue: function() {
  397. return this.value;
  398. },
  399. /**
  400. * Set current value (with smooth animation transition).
  401. * @protected
  402. * @param {number} newValue
  403. */
  404. setValue: function(newValue) {
  405. if (this.animation)
  406. this.animationStartValue = this.lastFrameValue;
  407. this.value = newValue;
  408. this.draw();
  409. }
  410. };
  411. //----------------------------------- Initiating jQuery plugin -----------------------------------
  412. $.circleProgress = {
  413. // Default options (you may override them)
  414. defaults: CircleProgress.prototype
  415. };
  416. // ease-in-out-cubic
  417. $.easing.circleProgressEasing = function(x) {
  418. if (x < 0.5) {
  419. x = 2 * x;
  420. return 0.5 * x * x * x;
  421. } else {
  422. x = 2 - 2 * x;
  423. return 1 - 0.5 * x * x * x;
  424. }
  425. };
  426. /**
  427. * Creates an instance of {@link CircleProgress}.
  428. * Produces [init event]{@link CircleProgress#init} and [animation events]{@link CircleProgress#drawAnimated}.
  429. *
  430. * @param {object} [configOrCommand] - Config object or command name.
  431. *
  432. * Config example (you can specify any {@link CircleProgress} property):
  433. *
  434. * ```js
  435. * { value: 0.75, size: 50, animation: false }
  436. * ```
  437. *
  438. * Commands:
  439. *
  440. * ```js
  441. * el.circleProgress('widget'); // get the <canvas>
  442. * el.circleProgress('value'); // get the value
  443. * el.circleProgress('value', newValue); // update the value
  444. * el.circleProgress('redraw'); // redraw the circle
  445. * el.circleProgress(); // the same as 'redraw'
  446. * ```
  447. *
  448. * @param {string} [commandArgument] - Some commands (like `'value'`) may require an argument.
  449. * @see CircleProgress
  450. * @alias "$(...).circleProgress"
  451. */
  452. $.fn.circleProgress = function(configOrCommand, commandArgument) {
  453. var dataName = 'circle-progress',
  454. firstInstance = this.data(dataName);
  455. if (configOrCommand == 'widget') {
  456. if (!firstInstance)
  457. throw Error('Calling "widget" method on not initialized instance is forbidden');
  458. return firstInstance.canvas;
  459. }
  460. if (configOrCommand == 'value') {
  461. if (!firstInstance)
  462. throw Error('Calling "value" method on not initialized instance is forbidden');
  463. if (typeof commandArgument == 'undefined') {
  464. return firstInstance.getValue();
  465. } else {
  466. var newValue = arguments[1];
  467. return this.each(function() {
  468. $(this).data(dataName).setValue(newValue);
  469. });
  470. }
  471. }
  472. return this.each(function() {
  473. var el = $(this),
  474. instance = el.data(dataName),
  475. config = $.isPlainObject(configOrCommand) ? configOrCommand : {};
  476. if (instance) {
  477. instance.init(config);
  478. } else {
  479. var initialConfig = $.extend({}, el.data());
  480. if (typeof initialConfig.fill == 'string')
  481. initialConfig.fill = JSON.parse(initialConfig.fill);
  482. if (typeof initialConfig.animation == 'string')
  483. initialConfig.animation = JSON.parse(initialConfig.animation);
  484. config = $.extend(initialConfig, config);
  485. config.el = el;
  486. instance = new CircleProgress(config);
  487. el.data(dataName, instance);
  488. }
  489. });
  490. };
  491. });