circle-progress.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. /*
  2. jquery-circle-progress - jQuery Plugin to draw animated circular progress bars
  3. URL: http://kottenator.github.io/jquery-circle-progress/
  4. Author: Rostyslav Bryzgunov <kottenator@gmail.com>
  5. Version: 1.1.3
  6. License: MIT
  7. */
  8. (function($) {
  9. function CircleProgress(config) {
  10. this.init(config);
  11. }
  12. CircleProgress.prototype = {
  13. //----------------------------------------------- public options -----------------------------------------------
  14. /**
  15. * This is the only required option. It should be from 0.0 to 1.0
  16. * @type {number}
  17. */
  18. value: 0.0,
  19. /**
  20. * Size of the circle / canvas in pixels
  21. * @type {number}
  22. */
  23. size: 100.0,
  24. /**
  25. * Initial angle for 0.0 value in radians
  26. * @type {number}
  27. */
  28. startAngle: -Math.PI,
  29. /**
  30. * Width of the arc. By default it's auto-calculated as 1/14 of size, but you may set it explicitly in pixels
  31. * @type {number|string}
  32. */
  33. thickness: 'auto',
  34. /**
  35. * Fill of the arc. You may set it to:
  36. * - solid color:
  37. * - { color: '#3aeabb' }
  38. * - { color: 'rgba(255, 255, 255, .3)' }
  39. * - linear gradient (left to right):
  40. * - { gradient: ['#3aeabb', '#fdd250'], gradientAngle: Math.PI / 4 }
  41. * - { gradient: ['red', 'green', 'blue'], gradientDirection: [x0, y0, x1, y1] }
  42. * - image:
  43. * - { image: 'http://i.imgur.com/pT0i89v.png' }
  44. * - { image: imageObject }
  45. * - { color: 'lime', image: 'http://i.imgur.com/pT0i89v.png' } - color displayed until the image is loaded
  46. */
  47. fill: {
  48. gradient: ['#3aeabb', '#fdd250']
  49. },
  50. /**
  51. * Color of the "empty" arc. Only a color fill supported by now
  52. * @type {string}
  53. */
  54. emptyFill: 'rgba(0, 0, 0, .1)',
  55. /**
  56. * Animation config (see jQuery animations: http://api.jquery.com/animate/)
  57. */
  58. animation: {
  59. duration: 1200,
  60. easing: 'circleProgressEasing'
  61. },
  62. /**
  63. * Default animation starts at 0.0 and ends at specified `value`. Let's call this direct animation.
  64. * If you want to make reversed animation then you should set `animationStartValue` to 1.0.
  65. * Also you may specify any other value from 0.0 to 1.0
  66. * @type {number}
  67. */
  68. animationStartValue: 0.0,
  69. /**
  70. * Reverse animation and arc draw
  71. * @type {boolean}
  72. */
  73. reverse: false,
  74. /**
  75. * Arc line cap ('butt', 'round' or 'square')
  76. * Read more: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D.lineCap
  77. * @type {string}
  78. */
  79. lineCap: 'butt',
  80. //-------------------------------------- protected properties and methods --------------------------------------
  81. /**
  82. * @protected
  83. */
  84. constructor: CircleProgress,
  85. /**
  86. * Container element. Should be passed into constructor config
  87. * @protected
  88. * @type {jQuery}
  89. */
  90. el: null,
  91. /**
  92. * Canvas element. Automatically generated and prepended to the {@link CircleProgress.el container}
  93. * @protected
  94. * @type {HTMLCanvasElement}
  95. */
  96. canvas: null,
  97. /**
  98. * 2D-context of the {@link CircleProgress.canvas canvas}
  99. * @protected
  100. * @type {CanvasRenderingContext2D}
  101. */
  102. ctx: null,
  103. /**
  104. * Radius of the outer circle. Automatically calculated as {@link CircleProgress.size} / 2
  105. * @protected
  106. * @type {number}
  107. */
  108. radius: 0.0,
  109. /**
  110. * Fill of the main arc. Automatically calculated, depending on {@link CircleProgress.fill} option
  111. * @protected
  112. * @type {string|CanvasGradient|CanvasPattern}
  113. */
  114. arcFill: null,
  115. /**
  116. * Last rendered frame value
  117. * @protected
  118. * @type {number}
  119. */
  120. lastFrameValue: 0.0,
  121. /**
  122. * Init/re-init the widget
  123. * @param {object} config - Config
  124. */
  125. init: function(config) {
  126. $.extend(this, config);
  127. this.radius = this.size / 2;
  128. this.initWidget();
  129. this.initFill();
  130. this.draw();
  131. },
  132. /**
  133. * @protected
  134. */
  135. initWidget: function() {
  136. var canvas = this.canvas = this.canvas || $('<canvas>').prependTo(this.el)[0];
  137. canvas.width = this.size;
  138. canvas.height = this.size;
  139. this.ctx = canvas.getContext('2d');
  140. },
  141. /**
  142. * This method sets {@link CircleProgress.arcFill}
  143. * It could do this async (on image load)
  144. * @protected
  145. */
  146. initFill: function() {
  147. var self = this,
  148. fill = this.fill,
  149. ctx = this.ctx,
  150. size = this.size;
  151. if (!fill)
  152. throw Error("The fill is not specified!");
  153. if (fill.color)
  154. this.arcFill = fill.color;
  155. if (fill.gradient) {
  156. var gr = fill.gradient;
  157. if (gr.length == 1) {
  158. this.arcFill = gr[0];
  159. } else if (gr.length > 1) {
  160. var ga = fill.gradientAngle || 0, // gradient direction angle; 0 by default
  161. gd = fill.gradientDirection || [
  162. size / 2 * (1 - Math.cos(ga)), // x0
  163. size / 2 * (1 + Math.sin(ga)), // y0
  164. size / 2 * (1 + Math.cos(ga)), // x1
  165. size / 2 * (1 - Math.sin(ga)) // y1
  166. ];
  167. var lg = ctx.createLinearGradient.apply(ctx, gd);
  168. for (var i = 0; i < gr.length; i++) {
  169. var color = gr[i],
  170. pos = i / (gr.length - 1);
  171. if ($.isArray(color)) {
  172. pos = color[1];
  173. color = color[0];
  174. }
  175. lg.addColorStop(pos, color);
  176. }
  177. this.arcFill = lg;
  178. }
  179. }
  180. if (fill.image) {
  181. var img;
  182. if (fill.image instanceof Image) {
  183. img = fill.image;
  184. } else {
  185. img = new Image();
  186. img.src = fill.image;
  187. }
  188. if (img.complete)
  189. setImageFill();
  190. else
  191. img.onload = setImageFill;
  192. }
  193. function setImageFill() {
  194. var bg = $('<canvas>')[0];
  195. bg.width = self.size;
  196. bg.height = self.size;
  197. bg.getContext('2d').drawImage(img, 0, 0, size, size);
  198. self.arcFill = self.ctx.createPattern(bg, 'no-repeat');
  199. self.drawFrame(self.lastFrameValue);
  200. }
  201. },
  202. draw: function() {
  203. if (this.animation)
  204. this.drawAnimated(this.value);
  205. else
  206. this.drawFrame(this.value);
  207. },
  208. /**
  209. * @protected
  210. * @param {number} v - Frame value
  211. */
  212. drawFrame: function(v) {
  213. this.lastFrameValue = v;
  214. this.ctx.clearRect(0, 0, this.size, this.size);
  215. this.drawEmptyArc(v);
  216. this.drawArc(v);
  217. },
  218. /**
  219. * @protected
  220. * @param {number} v - Frame value
  221. */
  222. drawArc: function(v) {
  223. var ctx = this.ctx,
  224. r = this.radius,
  225. t = this.getThickness(),
  226. a = this.startAngle;
  227. ctx.save();
  228. ctx.beginPath();
  229. if (!this.reverse) {
  230. ctx.arc(r, r, r - t / 2, a, a + Math.PI * 2 * v);
  231. } else {
  232. ctx.arc(r, r, r - t / 2, a - Math.PI * 2 * v, a);
  233. }
  234. ctx.lineWidth = t;
  235. ctx.lineCap = this.lineCap;
  236. ctx.strokeStyle = this.arcFill;
  237. ctx.stroke();
  238. ctx.restore();
  239. },
  240. /**
  241. * @protected
  242. * @param {number} v - Frame value
  243. */
  244. drawEmptyArc: function(v) {
  245. var ctx = this.ctx,
  246. r = this.radius,
  247. t = this.getThickness(),
  248. a = this.startAngle;
  249. if (v < 1) {
  250. ctx.save();
  251. ctx.beginPath();
  252. if (v <= 0) {
  253. ctx.arc(r, r, r - t / 2, 0, Math.PI * 2);
  254. } else {
  255. if (!this.reverse) {
  256. ctx.arc(r, r, r - t / 2, a + Math.PI * 2 * v, a);
  257. } else {
  258. ctx.arc(r, r, r - t / 2, a, a - Math.PI * 2 * v);
  259. }
  260. }
  261. ctx.lineWidth = t;
  262. ctx.strokeStyle = this.emptyFill;
  263. ctx.stroke();
  264. ctx.restore();
  265. }
  266. },
  267. /**
  268. * @protected
  269. * @param {number} v - Value
  270. */
  271. drawAnimated: function(v) {
  272. var self = this,
  273. el = this.el,
  274. canvas = $(this.canvas);
  275. // stop previous animation before new "start" event is triggered
  276. canvas.stop(true, false);
  277. el.trigger('circle-animation-start');
  278. canvas
  279. .css({ animationProgress: 0 })
  280. .animate({ animationProgress: 1 }, $.extend({}, this.animation, {
  281. step: function (animationProgress) {
  282. var stepValue = self.animationStartValue * (1 - animationProgress) + v * animationProgress;
  283. self.drawFrame(stepValue);
  284. el.trigger('circle-animation-progress', [animationProgress, stepValue]);
  285. }
  286. }))
  287. .promise()
  288. .always(function() {
  289. // trigger on both successful & failure animation end
  290. el.trigger('circle-animation-end');
  291. });
  292. },
  293. /**
  294. * @protected
  295. * @returns {number}
  296. */
  297. getThickness: function() {
  298. return $.isNumeric(this.thickness) ? this.thickness : this.size / 14;
  299. },
  300. getValue: function() {
  301. return this.value;
  302. },
  303. setValue: function(newValue) {
  304. if (this.animation)
  305. this.animationStartValue = this.lastFrameValue;
  306. this.value = newValue;
  307. this.draw();
  308. }
  309. };
  310. //-------------------------------------------- Initiating jQuery plugin --------------------------------------------
  311. $.circleProgress = {
  312. // Default options (you may override them)
  313. defaults: CircleProgress.prototype
  314. };
  315. // ease-in-out-cubic
  316. $.easing.circleProgressEasing = function(x, t, b, c, d) {
  317. if ((t /= d / 2) < 1)
  318. return c / 2 * t * t * t + b;
  319. return c / 2 * ((t -= 2) * t * t + 2) + b;
  320. };
  321. /**
  322. * Draw animated circular progress bar.
  323. *
  324. * Appends <canvas> to the element or updates already appended one.
  325. *
  326. * If animated, throws 3 events:
  327. *
  328. * - circle-animation-start(jqEvent)
  329. * - circle-animation-progress(jqEvent, animationProgress, stepValue) - multiple event;
  330. * animationProgress: from 0.0 to 1.0;
  331. * stepValue: from 0.0 to value
  332. * - circle-animation-end(jqEvent)
  333. *
  334. * @param configOrCommand - Config object or command name
  335. * Example: { value: 0.75, size: 50, animation: false };
  336. * you may set any public property (see above);
  337. * `animation` may be set to false;
  338. * you may use .circleProgress('widget') to get the canvas
  339. * you may use .circleProgress('value', newValue) to dynamically update the value
  340. *
  341. * @param commandArgument - Some commands (like 'value') may require an argument
  342. */
  343. $.fn.circleProgress = function(configOrCommand, commandArgument) {
  344. var dataName = 'circle-progress',
  345. firstInstance = this.data(dataName);
  346. if (configOrCommand == 'widget') {
  347. if (!firstInstance)
  348. throw Error('Calling "widget" method on not initialized instance is forbidden');
  349. return firstInstance.canvas;
  350. }
  351. if (configOrCommand == 'value') {
  352. if (!firstInstance)
  353. throw Error('Calling "value" method on not initialized instance is forbidden');
  354. if (typeof commandArgument == 'undefined') {
  355. return firstInstance.getValue();
  356. } else {
  357. var newValue = arguments[1];
  358. return this.each(function() {
  359. $(this).data(dataName).setValue(newValue);
  360. });
  361. }
  362. }
  363. return this.each(function() {
  364. var el = $(this),
  365. instance = el.data(dataName),
  366. config = $.isPlainObject(configOrCommand) ? configOrCommand : {};
  367. if (instance) {
  368. instance.init(config);
  369. } else {
  370. var initialConfig = $.extend({}, el.data());
  371. if (typeof initialConfig.fill == 'string')
  372. initialConfig.fill = JSON.parse(initialConfig.fill);
  373. if (typeof initialConfig.animation == 'string')
  374. initialConfig.animation = JSON.parse(initialConfig.animation);
  375. config = $.extend(initialConfig, config);
  376. config.el = el;
  377. instance = new CircleProgress(config);
  378. el.data(dataName, instance);
  379. }
  380. });
  381. };
  382. })(jQuery);