├── .gitignore ├── tests ├── images │ └── circle.png ├── index.html ├── test_utils.js └── tests.js ├── jsdoc.conf ├── karma.conf.js ├── .travis.yml ├── bower.json ├── package.json ├── LICENSE ├── karma-saucelabs.conf.js ├── docs ├── index.html ├── styles.css └── examples.js ├── dist ├── circle-progress.min.js └── circle-progress.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /node_modules/ 3 | /bower_components/ 4 | -------------------------------------------------------------------------------- /tests/images/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kottenator/jquery-circle-progress/HEAD/tests/images/circle.png -------------------------------------------------------------------------------- /jsdoc.conf: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["plugins/markdown"], 3 | "opts": { 4 | "destination": "docs/api/" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | module.exports = function(config) { 3 | config.set({ 4 | frameworks: ['qunit'], 5 | files: [ 6 | {pattern: 'tests/images/circle.png', served: true, watched: false, included: false}, 7 | 'node_modules/jquery/dist/jquery.min.js', 8 | 'dist/circle-progress.js', 9 | 'tests/test_utils.js', 10 | 'tests/tests.js' 11 | ], 12 | browsers: ['Firefox', 'PhantomJS'], 13 | singleRun: true 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | before_script: 5 | - export DISPLAY=:99.0 6 | - sh -e /etc/init.d/xvfb start 7 | addons: 8 | firefox: latest 9 | env: 10 | global: 11 | - secure: Z6oEIaybr8vkGTZZbDJJT+9wO4SjRdXq5AV1dlgrQZ0j2wSXmP6Q4HeL8jWWt6RFOZvMyNLq+X6KwWSAsIB7B8TRF1mBrX++MBKINj+oUUYAAhPU9yl8iwgvQLh3suER1OvB0BP/LdeFCZ8zSG2UPI1KRqk4ZdFKwEg0u4CSQvo= 12 | - secure: hMgJSvty/MP21BFervWR+uOzpqpXbyXtkVnI2d2126yUkJhcyc6v5cO9imbLerj+/AIhVX1jkz7yK5YuyNmC1/J0V/dem4EokT+4+N0T5OepDsDHO5z6pOS22hwzsDuMIqgwCkcuOhNQvi9KlNwbN1/OyBwIeIaIW6IQEPwG9iY= 13 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tests for jquery-circle-progress 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-circle-progress", 3 | "version": "1.2.2", 4 | "authors": [ 5 | "Rostyslav Bryzgunov " 6 | ], 7 | "description": "Plugin to draw animated circular progress bars", 8 | "license": "MIT", 9 | "main": "dist/circle-progress.js", 10 | "keywords": [ 11 | "jquery", 12 | "canvas", 13 | "progress-bar" 14 | ], 15 | "homepage": "https://kottenator.github.io/jquery-circle-progress/", 16 | "ignore": [ 17 | "**/.*", 18 | "node_modules", 19 | "bower_components", 20 | "tests" 21 | ], 22 | "dependencies": { 23 | "jquery": "*" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-circle-progress", 3 | "version": "1.2.2", 4 | "author": "Rostyslav Bryzgunov ", 5 | "description": "Plugin to draw animated circular progress bars", 6 | "license": "MIT", 7 | "main": "dist/circle-progress.js", 8 | "keywords": [ 9 | "jquery", 10 | "canvas", 11 | "progress-bar" 12 | ], 13 | "dependencies": { 14 | "jquery": "*" 15 | }, 16 | "devDependencies": { 17 | "karma": "~1.2", 18 | "qunitjs": "~2.0", 19 | "karma-qunit": "~1.2", 20 | "karma-firefox-launcher": "~1.0", 21 | "karma-phantomjs-launcher": "~1.0", 22 | "karma-sauce-launcher": "~1.0", 23 | "uglify-js": "~2.7", 24 | "jsdoc": "~3.4" 25 | }, 26 | "scripts": { 27 | "test": "karma start", 28 | "build-min": "uglifyjs dist/circle-progress.js -cmo dist/circle-progress.min.js --comments", 29 | "build-docs": "jsdoc dist/circle-progress.js -c jsdoc.conf" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git://github.com/kottenator/jquery-circle-progress.git" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Rostyslav Bryzgunov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /karma-saucelabs.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | module.exports = function(config) { 3 | var customLaunchers = { 4 | 'Latest Chrome on Windows 10': { 5 | base: 'SauceLabs', 6 | platform: 'Windows 10', 7 | browserName: 'chrome', 8 | version: 'latest' 9 | }, 10 | 'Latest Firefox on Windows 10': { 11 | base: 'SauceLabs', 12 | platform: 'Windows 10', 13 | browserName: 'firefox', 14 | version: 'latest' 15 | }, 16 | 'IE9 on Windows 7': { 17 | base: 'SauceLabs', 18 | platform: 'Windows 7', 19 | browserName: 'internet explorer', 20 | version: '9' 21 | }, 22 | 'Latest Safari on OS X 10.11': { 23 | base: 'SauceLabs', 24 | platform: 'OS X 10.11', 25 | browserName: 'safari', 26 | version: 'latest' 27 | }, 28 | 'iPhone emulator': { 29 | base: 'SauceLabs', 30 | platform: "OS X 10.11", 31 | browserName: 'iphone', 32 | version: "8.1" 33 | }, 34 | 'Android emulator': { 35 | base: 'SauceLabs', 36 | platform: 'Linux', 37 | browserName: 'android', 38 | version: '5.0' 39 | } 40 | }; 41 | 42 | config.set({ 43 | frameworks: ['qunit'], 44 | files: [ 45 | {pattern: 'tests/images/circle.png', served: true, watched: false, included: false}, 46 | 'node_modules/jquery/dist/jquery.min.js', 47 | 'dist/circle-progress.js', 48 | 'tests/test_utils.js', 49 | 'tests/tests.js' 50 | ], 51 | sauceLabs: { 52 | testName: 'Unit tests for jquery-circle-progress' 53 | }, 54 | captureTimeout: 120000, 55 | customLaunchers: customLaunchers, 56 | browsers: Object.keys(customLaunchers), 57 | reporters: ['dots', 'saucelabs'], 58 | singleRun: true 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | jquery-circle-progress - jQuery Plugin to draw animated circular progress bars 8 | 9 | 10 | 11 | 12 | 13 |

14 | jquery-circle-progress 15 |

16 | 17 |
18 |
19 | no
animation
20 |
21 | 22 |
23 | 24 | animation
progress
25 |
26 | 27 |
28 | 29 | value
progress
30 |
31 | 32 |
33 | custom angle,
value update
34 |
35 | 36 |
48 | image fill,
custom sizes
49 |
50 |
51 | 52 |

53 | See these examples code and 54 | read the documentation on GitHub 55 |

56 | 57 |

58 | Download version 1.2.2 59 | bower install jquery-circle-progress 60 | npm install jquery-circle-progress 61 |

62 | 63 |

64 | © 2014-2017 - made by Rostyslav Bryzgunov 65 | 66 | 67 | avatar 71 | 72 |

73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /docs/styles.css: -------------------------------------------------------------------------------- 1 | /* These are just a test styles - you don't need them in your project */ 2 | body { 3 | background-color: #444; 4 | padding-top: 40px; 5 | font: 15px/1.3 Arial, sans-serif; 6 | color: #fff; 7 | text-align: center; 8 | } 9 | 10 | a { 11 | color: orange; 12 | } 13 | 14 | .new-tab-link { 15 | padding-right: 14px; 16 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3ggXDSIzCeRHfQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAA9SURBVBjTY2RAA/+XMvxHF2NkwAOwacCq4P9Shv8suFQzRiNsYUEXwKoJ2VhkNrIaJgYiAAs2N2BVRMirAD6JHi10MCdVAAAAAElFTkSuQmCC) no-repeat right center; 17 | } 18 | 19 | .page-title { 20 | font: 400 40px/1.5 Open Sans, sans-serif; 21 | text-align: center; 22 | } 23 | 24 | .circles { 25 | margin-bottom: -10px; 26 | } 27 | 28 | .circle { 29 | width: 100px; 30 | margin: 6px 6px 20px; 31 | display: inline-block; 32 | position: relative; 33 | text-align: center; 34 | line-height: 1.2; 35 | } 36 | 37 | .circle canvas { 38 | vertical-align: top; 39 | } 40 | 41 | .circle strong { 42 | position: absolute; 43 | top: 30px; 44 | left: 0; 45 | width: 100%; 46 | text-align: center; 47 | line-height: 40px; 48 | font-size: 30px; 49 | } 50 | 51 | .circle strong i { 52 | font-style: normal; 53 | font-size: 0.6em; 54 | font-weight: normal; 55 | } 56 | 57 | .circle span { 58 | display: block; 59 | color: #aaa; 60 | margin-top: 12px; 61 | } 62 | 63 | p { 64 | margin: 40px 0; 65 | } 66 | 67 | .install { 68 | display: inline-block; 69 | } 70 | 71 | .install a { 72 | display: block; 73 | width: auto; 74 | vertical-align: middle; 75 | padding: 6px 12px; 76 | line-height: 20px; 77 | background-color: #111; 78 | color: #fff; 79 | border-radius: 3px; 80 | text-decoration: none; 81 | margin: 5px auto; 82 | transition: all .3s; 83 | } 84 | 85 | .install code { 86 | padding: 6px 12px; 87 | display: block; 88 | font: 13px/20px Courier New, Liberation Mono, monospase; 89 | background-color: #333333; 90 | border-radius: 4px; 91 | margin: 5px 0; 92 | white-space: nowrap; 93 | } 94 | 95 | .install a:hover { 96 | background-color: #222; 97 | } 98 | 99 | .credits { 100 | color: #aaa; 101 | position: fixed; 102 | left: 0; 103 | bottom: 0; 104 | width: 100%; 105 | } 106 | 107 | .credits img { 108 | margin-left: 4px; 109 | border-radius: 4px; 110 | vertical-align: middle; 111 | } 112 | 113 | @media (max-height: 600px), (max-width: 480px) { 114 | .credits { 115 | position: inherit; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /docs/examples.js: -------------------------------------------------------------------------------- 1 | /* Examples */ 2 | (function($) { 3 | /* 4 | * Example 1: 5 | * 6 | * - no animation 7 | * - custom gradient 8 | * 9 | * By the way - you may specify more than 2 colors for the gradient 10 | */ 11 | $('.first.circle').circleProgress({ 12 | value: 0.35, 13 | animation: false, 14 | fill: {gradient: ['#ff1e41', '#ff5f43']} 15 | }); 16 | 17 | /* 18 | * Example 2: 19 | * 20 | * - default gradient 21 | * - listening to `circle-animation-progress` event and display the animation progress: from 0 to 100% 22 | */ 23 | $('.second.circle').circleProgress({ 24 | value: 0.6 25 | }).on('circle-animation-progress', function(event, progress) { 26 | $(this).find('strong').html(Math.round(100 * progress) + '%'); 27 | }); 28 | 29 | /* 30 | * Example 3: 31 | * 32 | * - very custom gradient 33 | * - listening to `circle-animation-progress` event and display the dynamic change of the value: from 0 to 0.8 34 | */ 35 | $('.third.circle').circleProgress({ 36 | value: 0.75, 37 | fill: {gradient: [['#0681c4', .5], ['#4ac5f8', .5]], gradientAngle: Math.PI / 4} 38 | }).on('circle-animation-progress', function(event, progress, stepValue) { 39 | $(this).find('strong').text(stepValue.toFixed(2).substr(1)); 40 | }); 41 | 42 | /* 43 | * Example 4: 44 | * 45 | * - solid color fill 46 | * - custom start angle 47 | * - custom line cap 48 | * - dynamic value set 49 | */ 50 | var c4 = $('.forth.circle'); 51 | 52 | c4.circleProgress({ 53 | startAngle: -Math.PI / 4 * 3, 54 | value: 0.5, 55 | lineCap: 'round', 56 | fill: {color: '#ffa500'} 57 | }); 58 | 59 | // Let's emulate dynamic value update 60 | setTimeout(function() { c4.circleProgress('value', 0.7); }, 1000); 61 | setTimeout(function() { c4.circleProgress('value', 1.0); }, 1100); 62 | setTimeout(function() { c4.circleProgress('value', 0.5); }, 2100); 63 | 64 | /* 65 | * Example 5: 66 | * 67 | * - image fill; image should be squared; it will be stretched to SxS size, where S - size of the widget 68 | * - fallback color fill (when image is not loaded) 69 | * - custom widget size (default is 100px) 70 | * - custom circle thickness (default is 1/14 of the size) 71 | * - reverse drawing mode 72 | * - custom animation start value 73 | * - usage of "data-" attributes 74 | */ 75 | $('.fifth.circle').circleProgress({ 76 | value: 0.7 77 | // all other config options were taken from "data-" attributes 78 | // options passed in config object have higher priority than "data-" attributes 79 | // "data-" attributes are taken into account only on init (not on update/redraw) 80 | // "data-fill" (and other object options) should be in valid JSON format 81 | }); 82 | })(jQuery); 83 | -------------------------------------------------------------------------------- /tests/test_utils.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | QUnit.extend(QUnit.assert, { 3 | pixelRGBA: function(canvas, x, y, expectedRGBA, message) { 4 | return _pixelColor.call(this, canvas, x, y, expectedRGBA, 0, message, _parseRGBA, _dumpRGBA); 5 | }, 6 | 7 | pixelHex: function(canvas, x, y, expectedHex, message) { 8 | return _pixelColor.call(this, canvas, x, y, expectedHex, 0, message, _parseHex, _dumpHex, true); 9 | }, 10 | 11 | pixelCloseRGBA: function(canvas, x, y, expectedRGBA, maxDiff, message) { 12 | if (typeof maxDiff == 'undefined' || maxDiff === null) 13 | maxDiff = 0.01; 14 | return _pixelColor.call(this, canvas, x, y, expectedRGBA, maxDiff, message, _parseRGBA, _dumpRGBA); 15 | }, 16 | 17 | pixelCloseHex: function(canvas, x, y, expectedHex, maxDiff, message) { 18 | if (typeof maxDiff == 'undefined' || maxDiff === null) 19 | maxDiff = 0.015; 20 | return _pixelColor.call(this, canvas, x, y, expectedHex, maxDiff, message, _parseHex, _dumpHex, true); 21 | } 22 | }); 23 | 24 | function _pixelColor(canvas, x, y, expectedColor, maxDiff, message, parseColorFn, dumpColorFn, ignoreAlpha) { 25 | var scaleBy = window.devicePixelRatio || 1, 26 | data = canvas.getContext('2d').getImageData(Math.round(x * scaleBy), Math.round(y * scaleBy), 1, 1).data, 27 | expectedData = parseColorFn(expectedColor), 28 | actualColor = dumpColorFn(data); 29 | 30 | maxDiff = maxDiff || 0; 31 | 32 | var actualDiff = Math.max( 33 | Math.abs(data[0] - expectedData[0]) / 255, 34 | Math.abs(data[1] - expectedData[1]) / 255, 35 | Math.abs(data[2] - expectedData[2]) / 255, 36 | ignoreAlpha ? 0 : Math.abs(data[3] - expectedData[3]) / 255 37 | ); 38 | 39 | var result = actualDiff <= maxDiff; 40 | 41 | if (!message) { 42 | message = "Pixel color at " + x + "×" + y + " should be "; 43 | message += maxDiff ? "close to " + expectedColor : "equal to " + expectedColor; 44 | if (!result) { 45 | message += ". Actual color: " + actualColor; 46 | message += maxDiff ? ". Actual diff: " + actualDiff.toFixed(6) + ". Expected diff: " + maxDiff : ""; 47 | } 48 | } 49 | 50 | this.push(result, actualColor, expectedColor, message); 51 | } 52 | 53 | function _parseRGBA(s) { 54 | s = s.replace(/^rgba\(|\s+|\)$/gi, '').split(','); 55 | return [ 56 | parseInt(s[0], 10), 57 | parseInt(s[1], 10), 58 | parseInt(s[2], 10), 59 | s[3] * 255 60 | ]; 61 | } 62 | 63 | function _dumpRGBA(data) { 64 | return 'rgba(' + data[0] + ', ' + data[1] + ', ' + data[2] + ', ' + data[3] / 255 + ')'; 65 | } 66 | 67 | function _parseHex(s) { 68 | s = s.replace(/[#\s]/gi, ''); 69 | return [ 70 | parseInt(s.substr(0, 2), 16), 71 | parseInt(s.substr(2, 2), 16), 72 | parseInt(s.substr(4, 2), 16), 73 | 255 74 | ]; 75 | } 76 | 77 | function _dumpHex(data) { 78 | var r = '0' + data[0].toString(16), 79 | g = '0' + data[1].toString(16), 80 | b = '0' + data[2].toString(16); 81 | r = r.substr(r.length - 2); 82 | g = g.substr(g.length - 2); 83 | b = b.substr(b.length - 2); 84 | return '#' + r + g + b; 85 | } 86 | })(); 87 | -------------------------------------------------------------------------------- /dist/circle-progress.min.js: -------------------------------------------------------------------------------- 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 6 | * @version 1.2.2 7 | * @licence MIT 8 | * @preserve 9 | */ 10 | !function(i){if("function"==typeof define&&define.amd)define(["jquery"],i);else if("object"==typeof module&&module.exports){var t=require("jquery");i(t),module.exports=t}else i(jQuery)}(function(i){function t(i){this.init(i)}t.prototype={value:0,size:100,startAngle:-Math.PI,thickness:"auto",fill:{gradient:["#3aeabb","#fdd250"]},emptyFill:"rgba(0, 0, 0, .1)",animation:{duration:1200,easing:"circleProgressEasing"},animationStartValue:0,reverse:!1,lineCap:"butt",insertMode:"prepend",constructor:t,el:null,canvas:null,ctx:null,radius:0,arcFill:null,lastFrameValue:0,init:function(t){i.extend(this,t),this.radius=this.size/2,this.initWidget(),this.initFill(),this.draw(),this.el.trigger("circle-inited")},initWidget:function(){this.canvas||(this.canvas=i("")["prepend"==this.insertMode?"prependTo":"appendTo"](this.el)[0]);var t=this.canvas;if(t.width=this.size,t.height=this.size,this.ctx=t.getContext("2d"),window.devicePixelRatio>1){var e=window.devicePixelRatio;t.style.width=t.style.height=this.size+"px",t.width=t.height=this.size*e,this.ctx.scale(e,e)}},initFill:function(){function t(){var t=i("")[0];t.width=e.size,t.height=e.size,t.getContext("2d").drawImage(g,0,0,r,r),e.arcFill=e.ctx.createPattern(t,"no-repeat"),e.drawFrame(e.lastFrameValue)}var e=this,a=this.fill,n=this.ctx,r=this.size;if(!a)throw Error("The fill is not specified!");if("string"==typeof a&&(a={color:a}),a.color&&(this.arcFill=a.color),a.gradient){var s=a.gradient;if(1==s.length)this.arcFill=s[0];else if(s.length>1){for(var l=a.gradientAngle||0,o=a.gradientDirection||[r/2*(1-Math.cos(l)),r/2*(1+Math.sin(l)),r/2*(1+Math.cos(l)),r/2*(1-Math.sin(l))],h=n.createLinearGradient.apply(n,o),c=0;c').appendTo(output).circleProgress(cfg); 8 | } 9 | 10 | QUnit.module("Layout tests, no animation"); 11 | 12 | QUnit.test("Test circle with value = 0 (without any options)", function(assert) { 13 | var canvas = createCircle({ 14 | value: 0 15 | }).circleProgress('widget'); 16 | var $canvas = $(canvas); 17 | var defaultSize = 100; 18 | var defaultThickness = parseInt(defaultSize / 14); // 7 19 | 20 | assert.equal($.circleProgress.defaults.size, defaultSize, "Default circle size: 100 pixels"); 21 | assert.equal($.circleProgress.defaults.thickness, 'auto', "Default circle thickness: 'auto' (i.e. 1/14 of size)"); 22 | assert.equal(canvas.tagName.toLowerCase(), 'canvas', "Method .circleProgress('widget') returns HTMLCanvasElement"); 23 | assert.equal($canvas.width(), defaultSize, "Default width: 100 pixels"); 24 | assert.equal($canvas.height(), defaultSize, "Default height: 100 pixels"); 25 | assert.pixelCloseRGBA(canvas, 0, defaultSize / 2, 'rgba(0, 0, 0, 0.1)'); 26 | assert.pixelCloseRGBA(canvas, defaultThickness - 1, defaultSize / 2, 'rgba(0, 0, 0, 0.1)'); 27 | assert.pixelRGBA(canvas, defaultThickness + 1, defaultSize / 2, 'rgba(0, 0, 0, 0)'); 28 | }); 29 | 30 | QUnit.test("Test circle with value = 0.5 and default fill", function(assert) { 31 | var canvas = createCircle({ 32 | value: 0.5, 33 | animation: false 34 | }).circleProgress('widget'); 35 | var size = $.circleProgress.defaults.size; 36 | 37 | assert.pixelCloseHex(canvas, 1, size / 2 - 1, '#3aeabb'); 38 | assert.pixelCloseRGBA(canvas, 1, size / 2 + 1, 'rgba(0, 0, 0, 0.1)'); 39 | assert.pixelCloseHex(canvas, size - 1, size / 2 - 1, '#fdd250'); 40 | assert.pixelCloseRGBA(canvas, size - 1, size / 2 + 1, 'rgba(0, 0, 0, 0.1)'); 41 | assert.pixelCloseHex(canvas, size / 2, 1, '#9ade85'); 42 | assert.pixelRGBA(canvas, size / 2, 8, 'rgba(0, 0, 0, 0)'); 43 | assert.pixelCloseRGBA(canvas, size / 2, size - 1, 'rgba(0, 0, 0, 0.1)'); 44 | assert.pixelRGBA(canvas, size / 2, size - 9, 'rgba(0, 0, 0, 0)'); 45 | }); 46 | 47 | QUnit.test("Test circle with value = 0.5 and solid fill", function(assert) { 48 | var color = '#ff0000'; 49 | var canvas = createCircle({ 50 | value: 0.5, 51 | fill: { 52 | color: color 53 | }, 54 | animation: false 55 | }).circleProgress('widget'); 56 | var defaultSize = $.circleProgress.defaults.size; 57 | 58 | assert.pixelHex(canvas, 1, defaultSize / 2 - 1, color); 59 | assert.pixelHex(canvas, defaultSize - 1, defaultSize / 2 - 1, color); 60 | }); 61 | 62 | QUnit.module("Layout tests with animation"); 63 | 64 | QUnit.test("Test circle with value = 0.5 and solid fill", function(assert) { 65 | var color = '#00aa55'; 66 | var circle = createCircle({ 67 | value: 0, // value will be updated dynamically 68 | animation: false, // initially we draw an empty circle w/o animation 69 | fill: { 70 | color: color 71 | } 72 | }); 73 | var canvas = circle.circleProgress('widget'); 74 | var size = $.circleProgress.defaults.size; 75 | var done = assert.async(); 76 | 77 | assert.expect(10); 78 | 79 | // Before the animation 80 | assert.pixelCloseRGBA(canvas, 1, size / 2 - 1, 'rgba(0, 0, 0, 0.1)'); 81 | assert.pixelCloseRGBA(canvas, 1, size / 2 + 1, 'rgba(0, 0, 0, 0.1)'); 82 | assert.pixelCloseRGBA(canvas, size / 2 + 1, 1, 'rgba(0, 0, 0, 0.1)'); 83 | assert.pixelCloseRGBA(canvas, size - 2, size / 2 - 1, 'rgba(0, 0, 0, 0.1)'); 84 | assert.pixelCloseRGBA(canvas, size - 2, size / 2 + 1, 'rgba(0, 0, 0, 0.1)'); 85 | 86 | // After the animation 87 | circle.on('circle-animation-end', function() { 88 | assert.pixelHex(canvas, 1, size / 2 - 1, color); 89 | assert.pixelCloseRGBA(canvas, 1, size / 2 + 1, 'rgba(0, 0, 0, 0.1)'); 90 | assert.pixelHex(canvas, size / 2 + 1, 1, color); 91 | assert.pixelHex(canvas, size - 2, size / 2 - 1, color); 92 | assert.pixelCloseRGBA(canvas, size - 2, size / 2 + 1, 'rgba(0, 0, 0, 0.1)'); 93 | done(); 94 | }); 95 | 96 | // Start the animation 97 | circle.circleProgress({ 98 | value: 0.5, 99 | animation: { 100 | duration: 500 101 | } 102 | }); 103 | }); 104 | 105 | QUnit.test("Test circle with value = 0.5, size = 80 and custom gradient", function(assert) { 106 | var circle = createCircle({ 107 | value: 0, // value will be updated dynamically 108 | animation: false, // initially we draw an empty circle w/o animation 109 | size: 80, 110 | fill: { 111 | gradient: ['#ff327a', '#fff430', '#ff8989'] 112 | } 113 | }); 114 | var canvas = circle.circleProgress('widget'); 115 | var size = 80; 116 | var done = assert.async(); 117 | 118 | assert.expect(10); 119 | 120 | // Before the animation 121 | assert.pixelCloseRGBA(canvas, 1, size / 2 - 1, 'rgba(0, 0, 0, 0.1)'); 122 | assert.pixelCloseRGBA(canvas, 1, size / 2 + 1, 'rgba(0, 0, 0, 0.1)'); 123 | assert.pixelCloseRGBA(canvas, size / 2, 1, 'rgba(0, 0, 0, 0.1)'); 124 | assert.pixelCloseRGBA(canvas, size - 2, size / 2 - 1, 'rgba(0, 0, 0, 0.1)'); 125 | assert.pixelCloseRGBA(canvas, size - 2, size / 2 + 1, 'rgba(0, 0, 0, 0.1)'); 126 | 127 | // After the animation 128 | circle.on('circle-animation-end', function() { 129 | assert.pixelCloseHex(canvas, 1, size / 2 - 1, '#ff3777'); 130 | assert.pixelCloseRGBA(canvas, 1, size / 2 + 1, 'rgba(0, 0, 0, 0.1)'); 131 | assert.pixelCloseHex(canvas, size / 2, 1, '#fff330'); 132 | assert.pixelCloseHex(canvas, size - 2, size / 2 - 1, '#ff8c86'); 133 | assert.pixelCloseRGBA(canvas, size - 2, size / 2 + 1, 'rgba(0, 0, 0, 0.1)'); 134 | done(); 135 | }); 136 | 137 | // Start the animation 138 | circle.circleProgress({ 139 | value: 0.5, 140 | animation: { 141 | duration: 500 142 | } 143 | }); 144 | }); 145 | 146 | QUnit.test("Test circle with value = 0.75, custom start angle and custom animation start value", function(assert) { 147 | var circle = createCircle({ 148 | value: 0, // value will be updated dynamically 149 | animation: false, // initially we draw an empty circle w/o animation 150 | startAngle: -Math.PI / 4, 151 | animationStartValue: 0.25 152 | }); 153 | var canvas = circle.circleProgress('widget'); 154 | var done = assert.async(); 155 | 156 | assert.expect(12); 157 | 158 | // Before the animation 159 | assert.pixelCloseRGBA(canvas, 15, 20, 'rgba(0, 0, 0, 0.1)'); 160 | assert.pixelCloseRGBA(canvas, 20, 15, 'rgba(0, 0, 0, 0.1)'); 161 | assert.pixelCloseRGBA(canvas, 80, 15, 'rgba(0, 0, 0, 0.1)'); 162 | assert.pixelCloseRGBA(canvas, 80, 85, 'rgba(0, 0, 0, 0.1)'); 163 | assert.pixelCloseRGBA(canvas, 85, 20, 'rgba(0, 0, 0, 0.1)'); 164 | assert.pixelCloseRGBA(canvas, 85, 80, 'rgba(0, 0, 0, 0.1)'); 165 | 166 | // After the animation 167 | circle.on('circle-animation-end', function() { 168 | assert.pixelCloseHex(canvas, 15, 20, '#57e6aa'); 169 | assert.pixelCloseRGBA(canvas, 20, 15, 'rgba(0, 0, 0, 0.1)'); 170 | assert.pixelCloseRGBA(canvas, 80, 15, 'rgba(0, 0, 0, 0.1)'); 171 | assert.pixelCloseHex(canvas, 80, 85, '#d6d664'); 172 | assert.pixelCloseHex(canvas, 85, 20, '#e0d55f'); 173 | assert.pixelCloseHex(canvas, 85, 80, '#e0d55f'); 174 | done(); 175 | }); 176 | 177 | // Start the animation 178 | circle.circleProgress({ 179 | value: 0.75, 180 | animation: { 181 | duration: 500 182 | } 183 | }); 184 | }); 185 | 186 | QUnit.test("Test circle with value = 0.5, image background and reverse", function(assert) { 187 | var urlPrefix = $('script[src*="tests.js"]').attr('src').replace(/tests\.js.*$/, ''); 188 | var imageUrl = urlPrefix + 'images/circle.png'; 189 | var image = new Image(); 190 | var done = assert.async(); 191 | 192 | assert.expect(12); 193 | 194 | // Start loading the image 195 | image.src = imageUrl; 196 | 197 | $(image).on('load', function() { 198 | var circle = createCircle({ 199 | value: 0, // value will be updated dynamically 200 | animation: false, // initially we draw an empty circle w/o animation 201 | thickness: 20, 202 | fill: { 203 | image: image 204 | }, 205 | reverse: true 206 | }); 207 | var canvas = circle.circleProgress('widget'); 208 | 209 | // Before the animation 210 | assert.pixelRGBA(canvas, 21, 49, 'rgba(0, 0, 0, 0)'); 211 | assert.pixelRGBA(canvas, 78, 49, 'rgba(0, 0, 0, 0)'); 212 | assert.pixelRGBA(canvas, 49, 77, 'rgba(0, 0, 0, 0)'); 213 | assert.pixelCloseRGBA(canvas, 17, 51, 'rgba(0, 0, 0, 0.1)'); 214 | assert.pixelCloseRGBA(canvas, 49, 81, 'rgba(0, 0, 0, 0.1)'); 215 | assert.pixelCloseRGBA(canvas, 81, 51, 'rgba(0, 0, 0, 0.1)'); 216 | 217 | // After the animation 218 | circle.on('circle-animation-end', function() { 219 | assert.pixelRGBA(canvas, 21, 49, 'rgba(0, 0, 0, 0)'); 220 | assert.pixelRGBA(canvas, 78, 49, 'rgba(0, 0, 0, 0)'); 221 | assert.pixelRGBA(canvas, 49, 77, 'rgba(0, 0, 0, 0)'); 222 | assert.pixelCloseHex(canvas, 17, 51, '#00f7ff'); 223 | assert.pixelCloseHex(canvas, 49, 81, '#7700ff'); 224 | assert.pixelCloseHex(canvas, 81, 51, '#ff0008'); 225 | done(); 226 | }); 227 | 228 | // Start the animation 229 | circle.circleProgress({ 230 | value: 0.5, 231 | animation: { 232 | duration: 500 233 | } 234 | }); 235 | }); 236 | }); 237 | 238 | QUnit.test("Test correct rendering on Retina displays", function(assert) { 239 | /** 240 | * Mock devicePixelRatio 241 | */ 242 | window.devicePixelRatio = 2; 243 | 244 | var canvas = createCircle({ 245 | value: 0.75, 246 | size: 50 247 | }).circleProgress('widget'); 248 | 249 | assert.equal(50, $(canvas).width()); 250 | assert.equal(100, canvas.width); 251 | }); 252 | 253 | QUnit.test("Test correct rendering on regular pixel density", function(assert) { 254 | /** 255 | * Mock devicePixelRatio 256 | */ 257 | window.devicePixelRatio = 1; 258 | 259 | var canvas = createCircle({ 260 | value: 0.75, 261 | size: 50 262 | }).circleProgress('widget'); 263 | 264 | assert.equal(50, $(canvas).height()); 265 | assert.equal(50, canvas.width); 266 | }); 267 | })(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jquery-circle-progress 2 | ====================== 3 | 4 | [![Build status](https://travis-ci.org/kottenator/jquery-circle-progress.svg?branch=master)](https://travis-ci.org/kottenator/jquery-circle-progress) 5 | [![Bower version](https://img.shields.io/bower/v/jquery-circle-progress.svg?maxAge=3600)](https://bower.io/search/?q=jquery-circle-progress) 6 | [![NPM version](https://img.shields.io/npm/v/jquery-circle-progress.svg?maxAge=3600)](https://www.npmjs.com/package/jquery-circle-progress) 7 | 8 | jQuery Plugin to draw animated circular progress bars like this: 9 | 10 | ![](http://i.imgur.com/zV5VUQG.png) 11 | 12 | Check out [more examples](http://kottenator.github.io/jquery-circle-progress/)! Or maybe the crazy [one](http://jsbin.com/vatuza/5/)? 13 | 14 | Install 15 | ------- 16 | 17 | Make your choice: 18 | 19 | * Download [latest GitHub release](https://github.com/kottenator/jquery-circle-progress/releases) 20 | * `bower install jquery-circle-progress` 21 | * `npm install jquery-circle-progress` 22 | 23 | Usage 24 | ----- 25 | 26 | ```html 27 | 28 | 29 | 30 |
31 | 32 | 41 | ``` 42 | 43 | If you use AMD or CommonJS with some JS bundler - see the [UMD section](#umd) below. 44 | 45 | Options 46 | ------- 47 | 48 | Specify options like in example above. 49 | 50 | | Option | Description | 51 | | ---- | ---- | 52 | | **value** | This is the only required option. It should be from `0.0` to `1.0`
Default: `0` | 53 | | size | Size of the circle / canvas in pixels
Default: `100` | 54 | | startAngle | Initial angle (for `0` value)
Default: `-Math.PI` | 55 | | reverse | Reverse animation and arc draw
Default: `false` | 56 | | thickness | Width of the arc. By default it's automatically calculated as 1/14 of `size` but you may set your own number
Default: `"auto"` | 57 | | lineCap | Arc line cap: `"butt"`, `"round"` or `"square"` - [read more](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D.lineCap)
Default: `"butt"` 58 | | fill | The arc fill config. You may specify next:
- `"#ff1e41"`
- `{ color: "#ff1e41" }`
- `{ color: 'rgba(255, 255, 255, .3)' }`
- `{ gradient: ["red", "green", "blue"] }`
- `{ gradient: [["red", .2], ["green", .3], ["blue", .8]] }`
- `{ gradient: [ ... ], gradientAngle: Math.PI / 4 }`
- `{ gradient: [ ... ], gradientDirection: [x0, y0, x1, y1] }`
- `{ image: "http://i.imgur.com/pT0i89v.png" }`
- `{ image: imageInstance }`
- `{ color: "lime", image: "http://i.imgur.com/pT0i89v.png" }`
Default: `{ gradient: ["#3aeabb", "#fdd250"] }` | 59 | | emptyFill | Color of the "empty" arc. Only a color fill supported by now
Default: `"rgba(0, 0, 0, .1)"` | 60 | | animation | Animation config. See [jQuery animations](http://api.jquery.com/animate/).
You may also set it to `false`
Default: `{ duration: 1200, easing: "circleProgressEasing" }`
`"circleProgressEasing"` *is just a ease-in-out-cubic easing* | 61 | | animationStartValue | Default animation starts at `0.0` and ends at specified `value`. Let's call this direct animation. If you want to make reversed animation then you should set `animationStartValue` to `1.0`. Also you may specify any other value from `0.0` to `1.0`
Default: `0.0` 62 | | insertMode | Canvas insertion mode: append or prepend it into the parent element?
Default: `"prepend"` | 63 | 64 | From version `1.1.3` you can specify any config option as HTML `data-` attribute. 65 | 66 | It will work *only on init*, i.e. after the widget is inited you may update its properties only via `.circleProgress({/*...*/})` method. `data-` attributes will be ignored. 67 | 68 | Also, object options like `"fill"` or `"animation"` should be valid JSON (and don't forget about HTML-escaping): 69 | 70 | ```html 71 |
83 | ``` 84 | 85 | Events 86 | ------ 87 | 88 | | Event | Description | Handler | 89 | | ---- | ---- | ---- | 90 | | `circle-inited` | Triggered on init or re-init. | `function(event)`:
- `event` - jQuery event | 91 | | `circle-animation-start` | Triggered once the animation is started. | `function(event)`:
- `event` - jQuery event | 92 | | `circle-animation-progress` | Triggered on each [animation tick](http://api.jquery.com/animate/#step). | `function(event, animationProgress, stepValue)`:
- `event` - jQuery event
- `animationProgress` - from `0.0` to `1.0`
- `stepValue` - current step value: from `0.0` to `value` | 93 | | `circle-animation-end` | Triggered once the animation is finished. | `function(event)`:
- `event` - jQuery event | 94 | 95 | Browsers support 96 | ---------------- 97 | 98 | The library uses `` which is supported by all modern browsers *(including mobile browsers)* 99 | and Internet Explorer 9+ ([Can I Use](http://caniuse.com/#search=canvas)). 100 | 101 | I haven't implemented any fallback / polyfill for unsupported browsers yet 102 | *(i.e. for Internet Explorer 8 and older / misc browsers)*. 103 | 104 | UMD 105 | --- 106 | 107 | I use [UMD template for jQuery plugin](https://github.com/umdjs/umd/blob/d31bb6ee7098715e019f52bdfe27b3e4bfd2b97e/templates/jqueryPlugin.js) which combines three things: 108 | 109 | * works fine with _browser globals_ 110 | * works fine with AMD 111 | * works fine with CommonJS 112 | 113 | ### Browser globals 114 | 115 | ```html 116 | 117 | 118 | 123 | ``` 124 | 125 | ### AMD 126 | 127 | Assuming that you have `jquery`, `jquery-circle-progress` and `requirejs` in `libs/` directory: 128 | 129 | ```html 130 | 131 | 144 | ``` 145 | 146 | You can [configure RequireJS](http://requirejs.org/docs/api.html) as you wish, just make `'jquery'` dependency reachable. 147 | 148 | ### CommonJS 149 | 150 | ```js 151 | // script.js 152 | require('jquery-circle-progress'); 153 | var $ = require('jquery'); 154 | $('#circle').circleProgress({ 155 | value: 0.75 156 | }); 157 | ``` 158 | 159 | ```sh 160 | some-js-bundler < script.js > script.bundle.js 161 | ``` 162 | 163 | ```html 164 | 165 | ``` 166 | 167 | You can use any JS bundler ([Webpack](https://webpack.github.io/), [Browserify](http://browserify.org/), etc) - no specific configuration required. 168 | 169 | API 170 | --- 171 | 172 | ### Get/set value 173 | 174 | Get it: 175 | 176 | ```js 177 | $('.circle').circleProgress({ value: 0.5 }); 178 | var value = $('.circle').circleProgress('value'); // 0.5 179 | ``` 180 | 181 | It will return the *first* item's value (by *first* I mean when `$('.circle').length >= 1`). 182 | *It works only if the widget is already inited. Raises an error otherwise*. 183 | 184 | Set it: 185 | 186 | ```js 187 | $('.circle').circleProgress('value', 0.75); // set value to 0.75 & animate the change 188 | ``` 189 | 190 | It will update *all* selected items value and animate the change. 191 | It doesn't *redraw* the widget - it updates the value & animates the changes. 192 | For example, it may be an AJAX loading indicator, which shows the loading progress. 193 | 194 | ### Get `` 195 | 196 | ```js 197 | $('.circle').circleProgress({ value: 0.5 }); 198 | var canvas = $('.circle').circleProgress('widget'); 199 | ``` 200 | 201 | It will return the *first* item's `` (by *first* I mean when `$('.circle').length >= 1`). 202 | *It works only if the widget is already inited. Raises an error otherwise*. 203 | 204 | ### Get `CircleProgress` instance 205 | 206 | ```js 207 | var instance = $('#circle').data('circle-progress'); 208 | ``` 209 | 210 | ### Redraw existing circle 211 | 212 | ```js 213 | $('#circle').circleProgress({ value: 0.5, fill: { color: 'orange' }}); 214 | $('#circle').circleProgress('redraw'); // use current configuration and redraw 215 | $('#circle').circleProgress(); // alias for 'redraw' 216 | $('#circle').circleProgress({ size: 150 }); // set new size and redraw 217 | ``` 218 | 219 | *It works only if the widget is already inited. Raises an error otherwise*. 220 | 221 | ### Change default options 222 | 223 | ```js 224 | $.circleProgress.defaults.size = 50; 225 | ``` 226 | 227 | FAQ 228 | --- 229 | 230 |
231 |
How to start the animation only when the circle appears in browser's view (on scrolling)? 232 |
Here is my proposed solution. 233 |
How to make the size flexible? 234 |
E.g. for responsive design, you can do it in the following way. 235 |
What if I need it to run in IE8? 236 |
There is no full-feature support for IE8 (actually, I didn't imlpement IE8 support at all). But you may follow my recommendations. 237 |
How to stop the animation? 238 |
Here is what you can do. 239 |
Can I handle "click" event? 240 |
It's not in the "core" but you can use my example of mouse/touch events handling. 241 |
May I customize the shape somehow? 242 |
It's a bit "tricky" but possible. Here is my little collection. 243 |
244 | 245 | Development 246 | ----------- 247 | 248 | ### Install 249 | 250 | ```sh 251 | git clone git@github.com:kottenator/jquery-circle-progress.git 252 | npm install 253 | ``` 254 | 255 | ### Modify 256 | 257 | You need to update `dist/circle-progress.min.js` after any change to `dist/circle-progress.js`: 258 | 259 | ```sh 260 | npm run build-min 261 | ``` 262 | 263 | If you're using one of JetBrains IDEs - you can configure a File Watcher. 264 | It's also possible to use some CLI tool like [Watchman](https://facebook.github.io/watchman/). 265 | 266 | ### Test 267 | 268 | ```sh 269 | npm test 270 | ``` 271 | 272 | SauceLabs: 273 | 274 | ```sh 275 | export SAUCE_USERNAME=... 276 | export SAUCE_ACCESS_KEY=... 277 | export BUILD_NUMBER=... 278 | npm test -- karma-saucelabs.conf.js 279 | ``` 280 | 281 | ### Build docs 282 | 283 | The API docs are not complete yet but you can build them: 284 | 285 | ```sh 286 | npm run build-docs 287 | ``` 288 | 289 | They will be generated in `docs/api/`. 290 | 291 | ### Release new version 292 | 293 | * finalize the code 294 | * update the version in `package.json`, `bower.json` and `dist/circle-progress.js` docstring 295 | * update min dist: `npm run build-min` 296 | * update `docs/index.html` - link to the latest dist version _(which doesn't exist yet)_ 297 | * push the changes to `master` branch 298 | * release on Bower: just create a Git tag (e.g.): `git tag v1.2.3 && git push --tags` 299 | * release on GitHub - add release notes to the Git tag 300 | * release on NPM: `npm publish`, but be aware: 301 | 302 | > Once a package is published with a given name and version, that specific name and version combination can never be used again - [NPM docs](https://docs.npmjs.com/cli/publish) 303 | -------------------------------------------------------------------------------- /dist/circle-progress.js: -------------------------------------------------------------------------------- 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 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 | 38 | CircleProgress.prototype = { 39 | //--------------------------------------- public options --------------------------------------- 40 | /** 41 | * This is the only required option. It should be from `0.0` to `1.0`. 42 | * @type {number} 43 | * @default 0.0 44 | */ 45 | value: 0.0, 46 | 47 | /** 48 | * Size of the canvas in pixels. 49 | * It's a square so we need only one dimension. 50 | * @type {number} 51 | * @default 100.0 52 | */ 53 | size: 100.0, 54 | 55 | /** 56 | * Initial angle for `0.0` value in radians. 57 | * @type {number} 58 | * @default -Math.PI 59 | */ 60 | startAngle: -Math.PI, 61 | 62 | /** 63 | * Width of the arc in pixels. 64 | * If it's `'auto'` - the value is calculated as `[this.size]{@link CircleProgress#size} / 14`. 65 | * @type {number|string} 66 | * @default 'auto' 67 | */ 68 | thickness: 'auto', 69 | 70 | /** 71 | * Fill of the arc. You may set it to: 72 | * 73 | * - solid color: 74 | * - `'#3aeabb'` 75 | * - `{ color: '#3aeabb' }` 76 | * - `{ color: 'rgba(255, 255, 255, .3)' }` 77 | * - linear gradient _(left to right)_: 78 | * - `{ gradient: ['#3aeabb', '#fdd250'], gradientAngle: Math.PI / 4 }` 79 | * - `{ gradient: ['red', 'green', 'blue'], gradientDirection: [x0, y0, x1, y1] }` 80 | * - `{ gradient: [["red", .2], ["green", .3], ["blue", .8]] }` 81 | * - image: 82 | * - `{ image: 'http://i.imgur.com/pT0i89v.png' }` 83 | * - `{ image: imageObject }` 84 | * - `{ color: 'lime', image: 'http://i.imgur.com/pT0i89v.png' }` - 85 | * color displayed until the image is loaded 86 | * 87 | * @default {gradient: ['#3aeabb', '#fdd250']} 88 | */ 89 | fill: { 90 | gradient: ['#3aeabb', '#fdd250'] 91 | }, 92 | 93 | /** 94 | * Color of the "empty" arc. Only a color fill supported by now. 95 | * @type {string} 96 | * @default 'rgba(0, 0, 0, .1)' 97 | */ 98 | emptyFill: 'rgba(0, 0, 0, .1)', 99 | 100 | /** 101 | * jQuery Animation config. 102 | * You can pass `false` to disable the animation. 103 | * @see http://api.jquery.com/animate/ 104 | * @type {object|boolean} 105 | * @default {duration: 1200, easing: 'circleProgressEasing'} 106 | */ 107 | animation: { 108 | duration: 1200, 109 | easing: 'circleProgressEasing' 110 | }, 111 | 112 | /** 113 | * Default animation starts at `0.0` and ends at specified `value`. Let's call this _direct animation_. 114 | * If you want to make _reversed animation_ - set `animationStartValue: 1.0`. 115 | * Also you may specify any other value from `0.0` to `1.0`. 116 | * @type {number} 117 | * @default 0.0 118 | */ 119 | animationStartValue: 0.0, 120 | 121 | /** 122 | * Reverse animation and arc draw. 123 | * By default, the arc is filled from `0.0` to `value`, _clockwise_. 124 | * With `reverse: true` the arc is filled from `1.0` to `value`, _counter-clockwise_. 125 | * @type {boolean} 126 | * @default false 127 | */ 128 | reverse: false, 129 | 130 | /** 131 | * Arc line cap: `'butt'`, `'round'` or `'square'` - 132 | * [read more]{@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D.lineCap}. 133 | * @type {string} 134 | * @default 'butt' 135 | */ 136 | lineCap: 'butt', 137 | 138 | /** 139 | * Canvas insertion mode: append or prepend it into the parent element? 140 | * @type {string} 141 | * @default 'prepend' 142 | */ 143 | insertMode: 'prepend', 144 | 145 | //------------------------------ protected properties and methods ------------------------------ 146 | /** 147 | * Link to {@link CircleProgress} constructor. 148 | * @protected 149 | */ 150 | constructor: CircleProgress, 151 | 152 | /** 153 | * Container element. Should be passed into constructor config. 154 | * @protected 155 | * @type {jQuery} 156 | */ 157 | el: null, 158 | 159 | /** 160 | * Canvas element. Automatically generated and prepended to [this.el]{@link CircleProgress#el}. 161 | * @protected 162 | * @type {HTMLCanvasElement} 163 | */ 164 | canvas: null, 165 | 166 | /** 167 | * 2D-context of [this.canvas]{@link CircleProgress#canvas}. 168 | * @protected 169 | * @type {CanvasRenderingContext2D} 170 | */ 171 | ctx: null, 172 | 173 | /** 174 | * Radius of the outer circle. Automatically calculated as `[this.size]{@link CircleProgress#size} / 2`. 175 | * @protected 176 | * @type {number} 177 | */ 178 | radius: 0.0, 179 | 180 | /** 181 | * Fill of the main arc. Automatically calculated, depending on [this.fill]{@link CircleProgress#fill} option. 182 | * @protected 183 | * @type {string|CanvasGradient|CanvasPattern} 184 | */ 185 | arcFill: null, 186 | 187 | /** 188 | * Last rendered frame value. 189 | * @protected 190 | * @type {number} 191 | */ 192 | lastFrameValue: 0.0, 193 | 194 | /** 195 | * Init/re-init the widget. 196 | * 197 | * Throws a jQuery event: 198 | * 199 | * - `circle-inited(jqEvent)` 200 | * 201 | * @param {object} config - You can customize any class member (property or method). 202 | */ 203 | init: function(config) { 204 | $.extend(this, config); 205 | this.radius = this.size / 2; 206 | this.initWidget(); 207 | this.initFill(); 208 | this.draw(); 209 | this.el.trigger('circle-inited'); 210 | }, 211 | 212 | /** 213 | * Initialize ``. 214 | * @protected 215 | */ 216 | initWidget: function() { 217 | if (!this.canvas) 218 | this.canvas = $('')[this.insertMode == 'prepend' ? 'prependTo' : 'appendTo'](this.el)[0]; 219 | 220 | var canvas = this.canvas; 221 | canvas.width = this.size; 222 | canvas.height = this.size; 223 | this.ctx = canvas.getContext('2d'); 224 | 225 | if (window.devicePixelRatio > 1) { 226 | var scaleBy = window.devicePixelRatio; 227 | canvas.style.width = canvas.style.height = this.size + 'px'; 228 | canvas.width = canvas.height = this.size * scaleBy; 229 | this.ctx.scale(scaleBy, scaleBy); 230 | } 231 | }, 232 | 233 | /** 234 | * This method sets [this.arcFill]{@link CircleProgress#arcFill}. 235 | * It could do this async (on image load). 236 | * @protected 237 | */ 238 | initFill: function() { 239 | var self = this, 240 | fill = this.fill, 241 | ctx = this.ctx, 242 | size = this.size; 243 | 244 | if (!fill) 245 | throw Error("The fill is not specified!"); 246 | 247 | if (typeof fill == 'string') 248 | fill = {color: fill}; 249 | 250 | if (fill.color) 251 | this.arcFill = fill.color; 252 | 253 | if (fill.gradient) { 254 | var gr = fill.gradient; 255 | 256 | if (gr.length == 1) { 257 | this.arcFill = gr[0]; 258 | } else if (gr.length > 1) { 259 | var ga = fill.gradientAngle || 0, // gradient direction angle; 0 by default 260 | gd = fill.gradientDirection || [ 261 | size / 2 * (1 - Math.cos(ga)), // x0 262 | size / 2 * (1 + Math.sin(ga)), // y0 263 | size / 2 * (1 + Math.cos(ga)), // x1 264 | size / 2 * (1 - Math.sin(ga)) // y1 265 | ]; 266 | 267 | var lg = ctx.createLinearGradient.apply(ctx, gd); 268 | 269 | for (var i = 0; i < gr.length; i++) { 270 | var color = gr[i], 271 | pos = i / (gr.length - 1); 272 | 273 | if ($.isArray(color)) { 274 | pos = color[1]; 275 | color = color[0]; 276 | } 277 | 278 | lg.addColorStop(pos, color); 279 | } 280 | 281 | this.arcFill = lg; 282 | } 283 | } 284 | 285 | if (fill.image) { 286 | var img; 287 | 288 | if (fill.image instanceof Image) { 289 | img = fill.image; 290 | } else { 291 | img = new Image(); 292 | img.src = fill.image; 293 | } 294 | 295 | if (img.complete) 296 | setImageFill(); 297 | else 298 | img.onload = setImageFill; 299 | } 300 | 301 | function setImageFill() { 302 | var bg = $('')[0]; 303 | bg.width = self.size; 304 | bg.height = self.size; 305 | bg.getContext('2d').drawImage(img, 0, 0, size, size); 306 | self.arcFill = self.ctx.createPattern(bg, 'no-repeat'); 307 | self.drawFrame(self.lastFrameValue); 308 | } 309 | }, 310 | 311 | /** 312 | * Draw the circle. 313 | * @protected 314 | */ 315 | draw: function() { 316 | if (this.animation) 317 | this.drawAnimated(this.value); 318 | else 319 | this.drawFrame(this.value); 320 | }, 321 | 322 | /** 323 | * Draw a single animation frame. 324 | * @protected 325 | * @param {number} v - Frame value. 326 | */ 327 | drawFrame: function(v) { 328 | this.lastFrameValue = v; 329 | this.ctx.clearRect(0, 0, this.size, this.size); 330 | this.drawEmptyArc(v); 331 | this.drawArc(v); 332 | }, 333 | 334 | /** 335 | * Draw the arc (part of the circle). 336 | * @protected 337 | * @param {number} v - Frame value. 338 | */ 339 | drawArc: function(v) { 340 | if (v === 0) 341 | return; 342 | 343 | var ctx = this.ctx, 344 | r = this.radius, 345 | t = this.getThickness(), 346 | a = this.startAngle; 347 | 348 | ctx.save(); 349 | ctx.beginPath(); 350 | 351 | if (!this.reverse) { 352 | ctx.arc(r, r, r - t / 2, a, a + Math.PI * 2 * v); 353 | } else { 354 | ctx.arc(r, r, r - t / 2, a - Math.PI * 2 * v, a); 355 | } 356 | 357 | ctx.lineWidth = t; 358 | ctx.lineCap = this.lineCap; 359 | ctx.strokeStyle = this.arcFill; 360 | ctx.stroke(); 361 | ctx.restore(); 362 | }, 363 | 364 | /** 365 | * Draw the _empty (background)_ arc (part of the circle). 366 | * @protected 367 | * @param {number} v - Frame value. 368 | */ 369 | drawEmptyArc: function(v) { 370 | var ctx = this.ctx, 371 | r = this.radius, 372 | t = this.getThickness(), 373 | a = this.startAngle; 374 | 375 | if (v < 1) { 376 | ctx.save(); 377 | ctx.beginPath(); 378 | 379 | if (v <= 0) { 380 | ctx.arc(r, r, r - t / 2, 0, Math.PI * 2); 381 | } else { 382 | if (!this.reverse) { 383 | ctx.arc(r, r, r - t / 2, a + Math.PI * 2 * v, a); 384 | } else { 385 | ctx.arc(r, r, r - t / 2, a, a - Math.PI * 2 * v); 386 | } 387 | } 388 | 389 | ctx.lineWidth = t; 390 | ctx.strokeStyle = this.emptyFill; 391 | ctx.stroke(); 392 | ctx.restore(); 393 | } 394 | }, 395 | 396 | /** 397 | * Animate the progress bar. 398 | * 399 | * Throws 3 jQuery events: 400 | * 401 | * - `circle-animation-start(jqEvent)` 402 | * - `circle-animation-progress(jqEvent, animationProgress, stepValue)` - multiple event 403 | * animationProgress: from `0.0` to `1.0`; stepValue: from `0.0` to `value` 404 | * - `circle-animation-end(jqEvent)` 405 | * 406 | * @protected 407 | * @param {number} v - Final value. 408 | */ 409 | drawAnimated: function(v) { 410 | var self = this, 411 | el = this.el, 412 | canvas = $(this.canvas); 413 | 414 | // stop previous animation before new "start" event is triggered 415 | canvas.stop(true, false); 416 | el.trigger('circle-animation-start'); 417 | 418 | canvas 419 | .css({animationProgress: 0}) 420 | .animate({animationProgress: 1}, $.extend({}, this.animation, { 421 | step: function(animationProgress) { 422 | var stepValue = self.animationStartValue * (1 - animationProgress) + v * animationProgress; 423 | self.drawFrame(stepValue); 424 | el.trigger('circle-animation-progress', [animationProgress, stepValue]); 425 | } 426 | })) 427 | .promise() 428 | .always(function() { 429 | // trigger on both successful & failure animation end 430 | el.trigger('circle-animation-end'); 431 | }); 432 | }, 433 | 434 | /** 435 | * Get the circle thickness. 436 | * @see CircleProgress#thickness 437 | * @protected 438 | * @returns {number} 439 | */ 440 | getThickness: function() { 441 | return $.isNumeric(this.thickness) ? this.thickness : this.size / 14; 442 | }, 443 | 444 | /** 445 | * Get current value. 446 | * @protected 447 | * @return {number} 448 | */ 449 | getValue: function() { 450 | return this.value; 451 | }, 452 | 453 | /** 454 | * Set current value (with smooth animation transition). 455 | * @protected 456 | * @param {number} newValue 457 | */ 458 | setValue: function(newValue) { 459 | if (this.animation) 460 | this.animationStartValue = this.lastFrameValue; 461 | this.value = newValue; 462 | this.draw(); 463 | } 464 | }; 465 | 466 | //----------------------------------- Initiating jQuery plugin ----------------------------------- 467 | $.circleProgress = { 468 | // Default options (you may override them) 469 | defaults: CircleProgress.prototype 470 | }; 471 | 472 | // ease-in-out-cubic 473 | $.easing.circleProgressEasing = function(x) { 474 | if (x < 0.5) { 475 | x = 2 * x; 476 | return 0.5 * x * x * x; 477 | } else { 478 | x = 2 - 2 * x; 479 | return 1 - 0.5 * x * x * x; 480 | } 481 | }; 482 | 483 | /** 484 | * Creates an instance of {@link CircleProgress}. 485 | * Produces [init event]{@link CircleProgress#init} and [animation events]{@link CircleProgress#drawAnimated}. 486 | * 487 | * @param {object} [configOrCommand] - Config object or command name. 488 | * 489 | * Config example (you can specify any {@link CircleProgress} property): 490 | * 491 | * ```js 492 | * { value: 0.75, size: 50, animation: false } 493 | * ``` 494 | * 495 | * Commands: 496 | * 497 | * ```js 498 | * el.circleProgress('widget'); // get the 499 | * el.circleProgress('value'); // get the value 500 | * el.circleProgress('value', newValue); // update the value 501 | * el.circleProgress('redraw'); // redraw the circle 502 | * el.circleProgress(); // the same as 'redraw' 503 | * ``` 504 | * 505 | * @param {string} [commandArgument] - Some commands (like `'value'`) may require an argument. 506 | * @see CircleProgress 507 | * @alias "$(...).circleProgress" 508 | */ 509 | $.fn.circleProgress = function(configOrCommand, commandArgument) { 510 | var dataName = 'circle-progress', 511 | firstInstance = this.data(dataName); 512 | 513 | if (configOrCommand == 'widget') { 514 | if (!firstInstance) 515 | throw Error('Calling "widget" method on not initialized instance is forbidden'); 516 | return firstInstance.canvas; 517 | } 518 | 519 | if (configOrCommand == 'value') { 520 | if (!firstInstance) 521 | throw Error('Calling "value" method on not initialized instance is forbidden'); 522 | if (typeof commandArgument == 'undefined') { 523 | return firstInstance.getValue(); 524 | } else { 525 | var newValue = arguments[1]; 526 | return this.each(function() { 527 | $(this).data(dataName).setValue(newValue); 528 | }); 529 | } 530 | } 531 | 532 | return this.each(function() { 533 | var el = $(this), 534 | instance = el.data(dataName), 535 | config = $.isPlainObject(configOrCommand) ? configOrCommand : {}; 536 | 537 | if (instance) { 538 | instance.init(config); 539 | } else { 540 | var initialConfig = $.extend({}, el.data()); 541 | if (typeof initialConfig.fill == 'string') 542 | initialConfig.fill = JSON.parse(initialConfig.fill); 543 | if (typeof initialConfig.animation == 'string') 544 | initialConfig.animation = JSON.parse(initialConfig.animation); 545 | config = $.extend(initialConfig, config); 546 | config.el = el; 547 | instance = new CircleProgress(config); 548 | el.data(dataName, instance); 549 | } 550 | }); 551 | }; 552 | }); 553 | --------------------------------------------------------------------------------