From c3f032fb7df56747883b6f1863279d3a8d3dbfa0 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sun, 21 Dec 2014 17:08:23 +0200 Subject: [PATCH] 0.2.0 release. Adds: - Dynamic binding - Animation presets and ability to choose animation - Some file reorganization --- .gitignore | 1 - Gruntfile.js | 29 ++-- README.md | 49 ++++-- bower.json | 4 +- demo.html => build/index.html | 77 +++++---- build/roundProgress.js | 310 ++++++++++++++++++++++++++++++++++ build/roundProgress.min.js | 2 + package.json | 4 +- roundProgress.js | 193 --------------------- src/module.js | 1 + src/roundProgress.js | 126 ++++++++++++++ src/roundProgressService.js | 151 +++++++++++++++++ src/shim.js | 29 ++++ 13 files changed, 717 insertions(+), 259 deletions(-) rename demo.html => build/index.html (76%) create mode 100644 build/roundProgress.js create mode 100644 build/roundProgress.min.js delete mode 100644 roundProgress.js create mode 100644 src/module.js create mode 100644 src/roundProgress.js create mode 100644 src/roundProgressService.js create mode 100644 src/shim.js diff --git a/.gitignore b/.gitignore index 1c43296e..a1037773 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ bower_components/ node_modules/ -build/ .DS_Store .grunt/ \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index 8721ba50..d21ed91d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,4 +1,11 @@ module.exports = function(grunt) { + var files = [ + 'src/shim.js', + 'src/module.js', + 'src/roundProgressService.js', + 'src/roundProgress.js', + ]; + grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), 'uglify': { @@ -6,19 +13,17 @@ module.exports = function(grunt) { 'banner': '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n' }, 'build': { - 'src': 'roundProgress.js', + 'src': files, 'dest': 'build/roundProgress.min.js' } }, - 'copy': { - 'deploy': { - 'files': [{ - 'src': ['demo.html'], - 'dest': 'build/index.html' - }, { - 'src': ['roundProgress.js'], - 'dest': 'build/' - }] + 'concat': { + 'options': { + separator: '\n', + }, + 'build': { + src: files, + dest: 'build/roundProgress.js', } }, 'gh-pages': { @@ -32,9 +37,9 @@ module.exports = function(grunt) { }); grunt.loadNpmTasks('grunt-contrib-uglify'); - grunt.loadNpmTasks('grunt-contrib-copy'); + grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-gh-pages'); - grunt.registerTask('default', ['uglify:build', 'copy:deploy']); + grunt.registerTask('default', ['concat:build', 'uglify:build']); grunt.registerTask('deploy', ['default', 'gh-pages:deploy']); }; \ No newline at end of file diff --git a/README.md b/README.md index e9ab0cee..b31b8e37 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ AngularJS module that uses SVG to create a circular progressbar ## Install -Include Angular and `roundProgress.js` or `roundProgress.min.js` in your page. You can use bower, or a script-tag: +Include Angular and [roundProgress.min.js](https://raw.githubusercontent.com/crisbeto/angular-svg-round-progressbar/master/build/roundProgress.min.js) or [roundProgress.js](https://raw.githubusercontent.com/crisbeto/angular-svg-round-progressbar/master/build/roundProgress.js) in your page. You can use bower, or a script-tag: `bower install angular-svg-round-progressbar` @@ -31,25 +31,44 @@ angular.module('someModule', ['angular-svg-round-progress']) * `stroke` specifies the thickness of the line * `semi` boolean, specifies whether the progressbar should be a semicircle or a full circle * `iterations` number of iterations for the animation. Set it to 1 for *no animation* and increase for slower animation. *(Optional, 50 by default)* -* To manually trigger a complete re-render of the progressbar, broadcast a "renderCircle" from a parent scope: - -```javascript -$rootScope.$broadcast('renderCircle'); -``` +* `animation` the easing function that will be used. Default value is `easeOutCubic`, possible values: + * linearEase + * easeInQuad + * easeOutQuad + * easeInOutQuad + * easeInCubic + * easeOutCubic + * easeInOutCubic + * easeInQuart + * easeOutQuart + * easeInOutQuart + * easeInQuint + * easeOutQuint + * easeInOutQuint + * easeInSine + * easeOutSine + * easeInOutSine + * easeInExpo + * easeOutExpo + * easeInOutExpo + * easeInCirc + * easeOutCirc + * easeInOutCirc ### Example: ```html
-
+ round-progress + max="max" + current="current" + color="#45ccce" + bgcolor="#eaeaea" + radius="100" + stroke="20" + semi="true" + iterations="50" + animation="easeInOutQuart"> ``` ## Browser support diff --git a/bower.json b/bower.json index fe1193d2..9eb8bbc5 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "angular-svg-round-progressbar", - "version": "0.1.0", + "version": "0.2.0", "homepage": "https://github.com/crisbeto/angular-svg-round-progressbar", "authors": [ "crisbeto" @@ -21,5 +21,5 @@ "dependencies": { "angular": "~1.2.19" }, - "main":"roundProgress.js" + "main":"build/roundProgress.min.js" } diff --git a/demo.html b/build/index.html similarity index 76% rename from demo.html rename to build/index.html index 903974d4..d089cb64 100644 --- a/demo.html +++ b/build/index.html @@ -1,6 +1,6 @@ - + Angular SVG round progressbar demo @@ -57,7 +57,7 @@ form{ text-align: left; - width: 270px; + width: 300px; margin:20px auto; } @@ -65,7 +65,7 @@ margin-bottom: 5px; } - input{ + input, select{ float:right; padding: 5px; } @@ -77,8 +77,8 @@ } - -
+ +
Back to project repo

Sample progressbar

@@ -93,7 +93,8 @@

Sample progressbar

radius="{{ radius }}" semi="isSemi" stroke="{{ stroke }}" - iterations="{{ iterations }}"> + iterations="{{ iterations }}" + animation="{{ currentAnimation }}">
@@ -131,6 +132,16 @@

Customize!

+
+ + +
+ +
+ + +
+
@@ -140,28 +151,23 @@

Customize!

- -
- - -
-
+

Upload progress example

-
{{ ((uploadCurrent/100)*100) | number:0 }}%
+
{{ ((uploadCurrent/100)*100) | number:0 }}%
-
+ stroke="{{ stroke }}" + animation="{{ currentAnimation }}">
@@ -170,20 +176,22 @@

Upload progress example

- + diff --git a/build/roundProgress.js b/build/roundProgress.js new file mode 100644 index 00000000..b5fa2f5f --- /dev/null +++ b/build/roundProgress.js @@ -0,0 +1,310 @@ +// shim layer with setTimeout fallback +// credit Erik Möller and http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ +(function() { + var lastTime = 0; + var vendors = ['webkit', 'moz']; + for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; + window.cancelAnimationFrame = + window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; + } + + if (!window.requestAnimationFrame){ + window.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { callback(currTime + timeToCall); }, + timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + } + + if (!window.cancelAnimationFrame){ + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; + } + +}()); + +angular.module('angular-svg-round-progress', []); + +angular.module('angular-svg-round-progress').service('roundProgressService', [function(){ + var service = {}; + + // credits to http://modernizr.com/ for the feature test + service.isSupported = !!(document.createElementNS && document.createElementNS('http://www.w3.org/2000/svg', "svg").createSVGRect); + + // utility function + var polarToCartesian = function(centerX, centerY, radius, angleInDegrees) { + var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0; + + return { + x: centerX + (radius * Math.cos(angleInRadians)), + y: centerY + (radius * Math.sin(angleInRadians)) + }; + }; + + // credit to http://stackoverflow.com/questions/5736398/how-to-calculate-the-svg-path-for-an-arc-of-a-circle + service.updateState = function(value, total, R, ring, size, isSemicircle) { + + if(!size) return ring; + + var value = value >= total ? total - 0.00001 : value, + type = isSemicircle ? 180 : 359.9999, + perc = total === 0 ? 0 : (value / total) * type, + x = size/2, + start = polarToCartesian(x, x, R, perc), // in this case x and y are the same + end = polarToCartesian(x, x, R, 0), + // arcSweep = endAngle - startAngle <= 180 ? "0" : "1", + arcSweep = (perc <= 180 ? "0" : "1"), + d = [ + "M", start.x, start.y, + "A", R, R, 0, arcSweep, 0, end.x, end.y + ].join(" "); + + return ring.attr('d', d); + }; + + // Easing functions by kirupa. + // Source: http://www.kirupa.com/forum/showthread.php?378287-Robert-Penner-s-Easing-Equations-in-Pure-JS-(no-jQuery) + // License: http://pastebin.com/1Jw1Tbhj + + service.animations = { + linearEase: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * currentIteration / totalIterations + startValue; + }, + + easeInQuad: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * (currentIteration /= totalIterations) * currentIteration + startValue; + }, + + easeOutQuad: function(currentIteration, startValue, changeInValue, totalIterations) { + return -changeInValue * (currentIteration /= totalIterations) * (currentIteration - 2) + startValue; + }, + + easeInOutQuad: function(currentIteration, startValue, changeInValue, totalIterations) { + if ((currentIteration /= totalIterations / 2) < 1) { + return changeInValue / 2 * currentIteration * currentIteration + startValue; + } + return -changeInValue / 2 * ((--currentIteration) * (currentIteration - 2) - 1) + startValue; + }, + + easeInCubic: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * Math.pow(currentIteration / totalIterations, 3) + startValue; + }, + + easeOutCubic: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * (Math.pow(currentIteration / totalIterations - 1, 3) + 1) + startValue; + }, + + easeInOutCubic: function(currentIteration, startValue, changeInValue, totalIterations) { + if ((currentIteration /= totalIterations / 2) < 1) { + return changeInValue / 2 * Math.pow(currentIteration, 3) + startValue; + } + return changeInValue / 2 * (Math.pow(currentIteration - 2, 3) + 2) + startValue; + }, + + easeInQuart: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * Math.pow (currentIteration / totalIterations, 4) + startValue; + }, + + easeOutQuart: function(currentIteration, startValue, changeInValue, totalIterations) { + return -changeInValue * (Math.pow(currentIteration / totalIterations - 1, 4) - 1) + startValue; + }, + + easeInOutQuart: function(currentIteration, startValue, changeInValue, totalIterations) { + if ((currentIteration /= totalIterations / 2) < 1) { + return changeInValue / 2 * Math.pow(currentIteration, 4) + startValue; + } + return -changeInValue / 2 * (Math.pow(currentIteration - 2, 4) - 2) + startValue; + }, + + easeInQuint: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * Math.pow (currentIteration / totalIterations, 5) + startValue; + }, + + easeOutQuint: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * (Math.pow(currentIteration / totalIterations - 1, 5) + 1) + startValue; + }, + + easeInOutQuint: function(currentIteration, startValue, changeInValue, totalIterations) { + if ((currentIteration /= totalIterations / 2) < 1) { + return changeInValue / 2 * Math.pow(currentIteration, 5) + startValue; + } + return changeInValue / 2 * (Math.pow(currentIteration - 2, 5) + 2) + startValue; + }, + + easeInSine: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * (1 - Math.cos(currentIteration / totalIterations * (Math.PI / 2))) + startValue; + }, + + easeOutSine: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * Math.sin(currentIteration / totalIterations * (Math.PI / 2)) + startValue; + }, + + easeInOutSine: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue / 2 * (1 - Math.cos(Math.PI * currentIteration / totalIterations)) + startValue; + }, + + easeInExpo: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * Math.pow(2, 10 * (currentIteration / totalIterations - 1)) + startValue; + }, + + easeOutExpo: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * (-Math.pow(2, -10 * currentIteration / totalIterations) + 1) + startValue; + }, + + easeInOutExpo: function(currentIteration, startValue, changeInValue, totalIterations) { + if ((currentIteration /= totalIterations / 2) < 1) { + return changeInValue / 2 * Math.pow(2, 10 * (currentIteration - 1)) + startValue; + } + return changeInValue / 2 * (-Math.pow(2, -10 * --currentIteration) + 2) + startValue; + }, + + easeInCirc: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * (1 - Math.sqrt(1 - (currentIteration /= totalIterations) * currentIteration)) + startValue; + }, + + easeOutCirc: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * Math.sqrt(1 - (currentIteration = currentIteration / totalIterations - 1) * currentIteration) + startValue; + }, + + easeInOutCirc: function(currentIteration, startValue, changeInValue, totalIterations) { + if ((currentIteration /= totalIterations / 2) < 1) { + return changeInValue / 2 * (1 - Math.sqrt(1 - currentIteration * currentIteration)) + startValue; + } + return changeInValue / 2 * (Math.sqrt(1 - (currentIteration -= 2) * currentIteration) + 1) + startValue; + } + }; + + return service; +}]); + +'use strict'; + +angular.module('angular-svg-round-progress') + .directive('roundProgress', ['$timeout', 'roundProgressService', function($timeout, service){ + + if(!service.isSupported){ + return { + // placeholder element to keep the structure + restrict: 'EA', + template:'
', + replace: true + }; + }; + + return { + restrict: "EA", + scope:{ + current: "=", + max: "=", + semi: "=", + radius: "@", + color: "@", + bgcolor: "@", + stroke: "@", + iterations: "@", + animation: "@" + }, + link: function (scope, element, attrs) { + var ring = element.find('path'), + background = element.find('circle'), + size, + resetValue; + + var renderCircle = function(){ + $timeout(function(){ + var isSemicircle = scope.semi, + radius = scope.radius, + stroke = scope.stroke; + + size = radius*2 + parseInt(stroke)*2; + + element.attr({ + "width": size, + "height": isSemicircle ? size/2 : size + }).css({ + "overflow": "hidden" // on some browsers the background overflows, if in semicircle mode + }); + + ring.attr({ + "stroke": scope.color, + "stroke-width": stroke, + "transform": isSemicircle ? ('translate('+ 0 +','+ size +') rotate(-90)') : '' + }); + + background.attr({ + "cx": radius, + "cy": radius, + "transform": "translate("+ stroke +", "+ stroke +")", + "r": radius, + "stroke": scope.bgcolor, + "stroke-width": stroke + }); + }); + }; + + var renderState = function (newValue, oldValue){ + if(!angular.isDefined(newValue)){ + return false; + }; + + if(newValue < 0){ + resetValue = oldValue; + return scope.current = 0; + }; + + if(newValue > scope.max){ + resetValue = oldValue; + return scope.current = scope.max; + }; + + var max = scope.max, + radius = scope.radius, + isSemicircle = scope.semi, + easingAnimation = service.animations[scope.animation || 'easeOutCubic'], + start = oldValue === newValue ? 0 : (oldValue || 0), // fixes the initial animation + val = newValue - start, + currentIteration = 0, + totalIterations = scope.iterations || 50; + + if(angular.isNumber(resetValue)){ + // the reset value fixes problems with animation, caused when limiting the scope.current + start = resetValue; + val = newValue - resetValue; + resetValue = null; + }; + + (function animation(){ + service.updateState( + easingAnimation(currentIteration, start, val, totalIterations), + max, + radius, + ring, + size, + isSemicircle); + + if(currentIteration < totalIterations){ + requestAnimationFrame(animation); + currentIteration++; + }; + })(); + }; + + scope.$watchCollection('[current, max, semi, radius, color, bgcolor, stroke, iterations]', function(newValue, oldValue){ + renderCircle(); + renderState(newValue[0], oldValue[0]); + }); + }, + replace:true, + template:[ + '', + '', + '', + '' + ].join('\n') + }; + }]); diff --git a/build/roundProgress.min.js b/build/roundProgress.min.js new file mode 100644 index 00000000..9d08fb31 --- /dev/null +++ b/build/roundProgress.min.js @@ -0,0 +1,2 @@ +/*! angular-svg-round-progressbar 2014-12-21 */ +"use strict";!function(){for(var a=0,b=["webkit","moz"],c=0;c=c?c-1e-5:a,h=g?180:359.9999,i=0===c?0:a/c*h,j=f/2,k=b(j,j,d,i),l=b(j,j,d,0),m=180>=i?"0":"1",n=["M",k.x,k.y,"A",d,d,0,m,0,l.x,l.y].join(" ");return e.attr("d",n)},a.animations={linearEase:function(a,b,c,d){return c*a/d+b},easeInQuad:function(a,b,c,d){return c*(a/=d)*a+b},easeOutQuad:function(a,b,c,d){return-c*(a/=d)*(a-2)+b},easeInOutQuad:function(a,b,c,d){return(a/=d/2)<1?c/2*a*a+b:-c/2*(--a*(a-2)-1)+b},easeInCubic:function(a,b,c,d){return c*Math.pow(a/d,3)+b},easeOutCubic:function(a,b,c,d){return c*(Math.pow(a/d-1,3)+1)+b},easeInOutCubic:function(a,b,c,d){return(a/=d/2)<1?c/2*Math.pow(a,3)+b:c/2*(Math.pow(a-2,3)+2)+b},easeInQuart:function(a,b,c,d){return c*Math.pow(a/d,4)+b},easeOutQuart:function(a,b,c,d){return-c*(Math.pow(a/d-1,4)-1)+b},easeInOutQuart:function(a,b,c,d){return(a/=d/2)<1?c/2*Math.pow(a,4)+b:-c/2*(Math.pow(a-2,4)-2)+b},easeInQuint:function(a,b,c,d){return c*Math.pow(a/d,5)+b},easeOutQuint:function(a,b,c,d){return c*(Math.pow(a/d-1,5)+1)+b},easeInOutQuint:function(a,b,c,d){return(a/=d/2)<1?c/2*Math.pow(a,5)+b:c/2*(Math.pow(a-2,5)+2)+b},easeInSine:function(a,b,c,d){return c*(1-Math.cos(a/d*(Math.PI/2)))+b},easeOutSine:function(a,b,c,d){return c*Math.sin(a/d*(Math.PI/2))+b},easeInOutSine:function(a,b,c,d){return c/2*(1-Math.cos(Math.PI*a/d))+b},easeInExpo:function(a,b,c,d){return c*Math.pow(2,10*(a/d-1))+b},easeOutExpo:function(a,b,c,d){return c*(-Math.pow(2,-10*a/d)+1)+b},easeInOutExpo:function(a,b,c,d){return(a/=d/2)<1?c/2*Math.pow(2,10*(a-1))+b:c/2*(-Math.pow(2,-10*--a)+2)+b},easeInCirc:function(a,b,c,d){return c*(1-Math.sqrt(1-(a/=d)*a))+b},easeOutCirc:function(a,b,c,d){return c*Math.sqrt(1-(a=a/d-1)*a)+b},easeInOutCirc:function(a,b,c,d){return(a/=d/2)<1?c/2*(1-Math.sqrt(1-a*a))+b:c/2*(Math.sqrt(1-(a-=2)*a)+1)+b}},a}]),angular.module("angular-svg-round-progress").directive("roundProgress",["$timeout","roundProgressService",function(a,b){return b.isSupported?{restrict:"EA",scope:{current:"=",max:"=",semi:"=",radius:"@",color:"@",bgcolor:"@",stroke:"@",iterations:"@",animation:"@"},link:function(c,d){var e,f,g=d.find("path"),h=d.find("circle"),i=function(){a(function(){var a=c.semi,b=c.radius,f=c.stroke;e=2*b+2*parseInt(f),d.attr({width:e,height:a?e/2:e}).css({overflow:"hidden"}),g.attr({stroke:c.color,"stroke-width":f,transform:a?"translate(0,"+e+") rotate(-90)":""}),h.attr({cx:b,cy:b,transform:"translate("+f+", "+f+")",r:b,stroke:c.bgcolor,"stroke-width":f})})},j=function(a,d){if(!angular.isDefined(a))return!1;if(0>a)return f=d,c.current=0;if(a>c.max)return f=d,c.current=c.max;var h=c.max,i=c.radius,j=c.semi,k=b.animations[c.animation||"easeOutCubic"],l=d===a?0:d||0,m=a-l,n=0,o=c.iterations||50;angular.isNumber(f)&&(l=f,m=a-f,f=null),function p(){b.updateState(k(n,l,m,o),h,i,g,e,j),o>n&&(requestAnimationFrame(p),n++)}()};c.$watchCollection("[current, max, semi, radius, color, bgcolor, stroke, iterations]",function(a,b){i(),j(a[0],b[0])})},replace:!0,template:['','','',""].join("\n")}:{restrict:"EA",template:'
',replace:!0}}]); \ No newline at end of file diff --git a/package.json b/package.json index f7fd10a1..96332e82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-svg-round-progressbar", - "version": "0.1.0", + "version": "0.2.0", "description": "AngularJS module that uses SVG to create a circular progressar", "main": "roundProgress.js", "scripts": {}, @@ -23,7 +23,7 @@ "homepage": "https://github.com/crisbeto/angular-svg-round-progressbar", "devDependencies": { "grunt": "^0.4.5", - "grunt-contrib-copy": "^0.5.0", + "grunt-contrib-concat": "^0.5.0", "grunt-contrib-uglify": "^0.5.0", "grunt-gh-pages": "^0.9.1" } diff --git a/roundProgress.js b/roundProgress.js deleted file mode 100644 index 0eb44ed2..00000000 --- a/roundProgress.js +++ /dev/null @@ -1,193 +0,0 @@ -'use strict'; - -angular.module('angular-svg-round-progress', []) - .directive('roundProgress', ['$timeout', function($timeout){ - //credits to http://modernizr.com/ for the feature test - if(!(!!document.createElementNS && !!document.createElementNS('http://www.w3.org/2000/svg', "svg").createSVGRect)){ - return { - //placeholder element to keep the structure - restrict: 'EA', - template:'
', - replace: true - } - }; - - // shim layer with setTimeout fallback - // credit Erik Möller and http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ - (function() { - var lastTime = 0; - var vendors = ['webkit', 'moz']; - for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { - window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; - window.cancelAnimationFrame = - window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; - } - - if (!window.requestAnimationFrame){ - window.requestAnimationFrame = function(callback, element) { - var currTime = new Date().getTime(); - var timeToCall = Math.max(0, 16 - (currTime - lastTime)); - var id = window.setTimeout(function() { callback(currTime + timeToCall); }, - timeToCall); - lastTime = currTime + timeToCall; - return id; - }; - } - - if (!window.cancelAnimationFrame){ - window.cancelAnimationFrame = function(id) { - clearTimeout(id); - }; - } - - }()); - - - var polarToCartesian = function(centerX, centerY, radius, angleInDegrees) { - var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0; - - return { - x: centerX + (radius * Math.cos(angleInRadians)), - y: centerY + (radius * Math.sin(angleInRadians)) - }; - } - - var updateState = function(value, total, R, ring, size, isSemicircle) { - if(!size){ - return; - }; - - // credit to http://stackoverflow.com/questions/5736398/how-to-calculate-the-svg-path-for-an-arc-of-a-circle - var value = value >= total ? total - 0.00001 : value, - type = isSemicircle ? 180 : 359.9999, - perc = total === 0 ? 0 : (value / total) * type, - x = size/2, - start = polarToCartesian(x, x, R, perc), // in this case x and y are the same - end = polarToCartesian(x, x, R, 0), - // arcSweep = endAngle - startAngle <= 180 ? "0" : "1", - arcSweep = (perc <= 180 ? "0" : "1"), - d = [ - "M", start.x, start.y, - "A", R, R, 0, arcSweep, 0, end.x, end.y - ].join(" "); - - ring.attr('d', d); - }; - - var easeOutCubic = function(currentIteration, startValue, changeInValue, totalIterations) { - // credits to http://www.kirupa.com/forum/showthread.php?378287-Robert-Penner-s-Easing-Equations-in-Pure-JS-(no-jQuery) - return changeInValue * (Math.pow(currentIteration / totalIterations - 1, 3) + 1) + startValue; - }; - - return { - restrict:'EA', - scope:{ - current: "=", - max: "=", - semi: "=", - radius: "@", - color: "@", - bgcolor: "@", - stroke: "@", - iterations: "@" - }, - link: function (scope, element, attrs) { - var ring = element.find('path'), - background = element.find('circle'), - size, - resetValue; - - var renderCircle = function(){ - $timeout(function(){ - var isSemicircle = scope.semi, - radius = scope.radius, - stroke = scope.stroke; - - size = radius*2 + parseInt(stroke)*2; - - element.attr({ - "width": size, - "height": isSemicircle ? size/2 : size - }).css({ - "overflow": "hidden" // on some browsers the background overflows, if in semicircle mode - }); - - ring.attr({ - "stroke": scope.color, - "stroke-width": stroke, - "transform": isSemicircle ? ('translate('+ 0 +','+ size +') rotate(-90)') : '' - }); - - background.attr({ - "cx": radius, - "cy": radius, - "transform": "translate("+ stroke +", "+ stroke +")", - "r": radius, - "stroke": scope.bgcolor, - "stroke-width": stroke - }); - - renderState(scope.current, scope.current); - }); - }; - - var renderState = function (newValue, oldValue){ - if(!angular.isDefined(newValue)){ - return false; - }; - - if(newValue < 0){ - resetValue = oldValue; - return scope.current = 0; - }; - - if(newValue > scope.max){ - resetValue = oldValue; - return scope.current = scope.max; - }; - - var max = scope.max, - radius = scope.radius, - isSemicircle = scope.semi, - start = oldValue === newValue ? 0 : (oldValue || 0), // fixes the initial animation - val = newValue - start, - currentIteration = 0, - totalIterations = scope.iterations || 50; - - if(angular.isNumber(resetValue)){ - // the reset value fixes problems with animation, caused when limiting the scope.current - start = resetValue; - val = newValue - resetValue; - resetValue = null; - }; - - (function animation(){ - if(currentIteration <= totalIterations){ - updateState( - easeOutCubic(currentIteration, start, val, totalIterations), - max, - radius, - ring, - size, - isSemicircle - ); - - requestAnimationFrame(animation); - currentIteration++; - }; - })(); - }; - - scope.$on('renderCircle', renderCircle); - scope.$watch('current', renderState); - - renderCircle(); - }, - replace:true, - template:'\ - \ - \ - \ - ' - }; - }]); diff --git a/src/module.js b/src/module.js new file mode 100644 index 00000000..47894be5 --- /dev/null +++ b/src/module.js @@ -0,0 +1 @@ +angular.module('angular-svg-round-progress', []); diff --git a/src/roundProgress.js b/src/roundProgress.js new file mode 100644 index 00000000..9a803e35 --- /dev/null +++ b/src/roundProgress.js @@ -0,0 +1,126 @@ +'use strict'; + +angular.module('angular-svg-round-progress') + .directive('roundProgress', ['$timeout', 'roundProgressService', function($timeout, service){ + + if(!service.isSupported){ + return { + // placeholder element to keep the structure + restrict: 'EA', + template:'
', + replace: true + }; + }; + + return { + restrict: "EA", + scope:{ + current: "=", + max: "=", + semi: "=", + radius: "@", + color: "@", + bgcolor: "@", + stroke: "@", + iterations: "@", + animation: "@" + }, + link: function (scope, element, attrs) { + var ring = element.find('path'), + background = element.find('circle'), + size, + resetValue; + + var renderCircle = function(){ + $timeout(function(){ + var isSemicircle = scope.semi, + radius = scope.radius, + stroke = scope.stroke; + + size = radius*2 + parseInt(stroke)*2; + + element.attr({ + "width": size, + "height": isSemicircle ? size/2 : size + }).css({ + "overflow": "hidden" // on some browsers the background overflows, if in semicircle mode + }); + + ring.attr({ + "stroke": scope.color, + "stroke-width": stroke, + "transform": isSemicircle ? ('translate('+ 0 +','+ size +') rotate(-90)') : '' + }); + + background.attr({ + "cx": radius, + "cy": radius, + "transform": "translate("+ stroke +", "+ stroke +")", + "r": radius, + "stroke": scope.bgcolor, + "stroke-width": stroke + }); + }); + }; + + var renderState = function (newValue, oldValue){ + if(!angular.isDefined(newValue)){ + return false; + }; + + if(newValue < 0){ + resetValue = oldValue; + return scope.current = 0; + }; + + if(newValue > scope.max){ + resetValue = oldValue; + return scope.current = scope.max; + }; + + var max = scope.max, + radius = scope.radius, + isSemicircle = scope.semi, + easingAnimation = service.animations[scope.animation || 'easeOutCubic'], + start = oldValue === newValue ? 0 : (oldValue || 0), // fixes the initial animation + val = newValue - start, + currentIteration = 0, + totalIterations = scope.iterations || 50; + + if(angular.isNumber(resetValue)){ + // the reset value fixes problems with animation, caused when limiting the scope.current + start = resetValue; + val = newValue - resetValue; + resetValue = null; + }; + + (function animation(){ + service.updateState( + easingAnimation(currentIteration, start, val, totalIterations), + max, + radius, + ring, + size, + isSemicircle); + + if(currentIteration < totalIterations){ + requestAnimationFrame(animation); + currentIteration++; + }; + })(); + }; + + scope.$watchCollection('[current, max, semi, radius, color, bgcolor, stroke, iterations]', function(newValue, oldValue){ + renderCircle(); + renderState(newValue[0], oldValue[0]); + }); + }, + replace:true, + template:[ + '', + '', + '', + '' + ].join('\n') + }; + }]); diff --git a/src/roundProgressService.js b/src/roundProgressService.js new file mode 100644 index 00000000..8aaf507a --- /dev/null +++ b/src/roundProgressService.js @@ -0,0 +1,151 @@ +angular.module('angular-svg-round-progress').service('roundProgressService', [function(){ + var service = {}; + + // credits to http://modernizr.com/ for the feature test + service.isSupported = !!(document.createElementNS && document.createElementNS('http://www.w3.org/2000/svg', "svg").createSVGRect); + + // utility function + var polarToCartesian = function(centerX, centerY, radius, angleInDegrees) { + var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0; + + return { + x: centerX + (radius * Math.cos(angleInRadians)), + y: centerY + (radius * Math.sin(angleInRadians)) + }; + }; + + // credit to http://stackoverflow.com/questions/5736398/how-to-calculate-the-svg-path-for-an-arc-of-a-circle + service.updateState = function(value, total, R, ring, size, isSemicircle) { + + if(!size) return ring; + + var value = value >= total ? total - 0.00001 : value, + type = isSemicircle ? 180 : 359.9999, + perc = total === 0 ? 0 : (value / total) * type, + x = size/2, + start = polarToCartesian(x, x, R, perc), // in this case x and y are the same + end = polarToCartesian(x, x, R, 0), + // arcSweep = endAngle - startAngle <= 180 ? "0" : "1", + arcSweep = (perc <= 180 ? "0" : "1"), + d = [ + "M", start.x, start.y, + "A", R, R, 0, arcSweep, 0, end.x, end.y + ].join(" "); + + return ring.attr('d', d); + }; + + // Easing functions by kirupa. + // Source: http://www.kirupa.com/forum/showthread.php?378287-Robert-Penner-s-Easing-Equations-in-Pure-JS-(no-jQuery) + // License: http://pastebin.com/1Jw1Tbhj + + service.animations = { + linearEase: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * currentIteration / totalIterations + startValue; + }, + + easeInQuad: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * (currentIteration /= totalIterations) * currentIteration + startValue; + }, + + easeOutQuad: function(currentIteration, startValue, changeInValue, totalIterations) { + return -changeInValue * (currentIteration /= totalIterations) * (currentIteration - 2) + startValue; + }, + + easeInOutQuad: function(currentIteration, startValue, changeInValue, totalIterations) { + if ((currentIteration /= totalIterations / 2) < 1) { + return changeInValue / 2 * currentIteration * currentIteration + startValue; + } + return -changeInValue / 2 * ((--currentIteration) * (currentIteration - 2) - 1) + startValue; + }, + + easeInCubic: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * Math.pow(currentIteration / totalIterations, 3) + startValue; + }, + + easeOutCubic: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * (Math.pow(currentIteration / totalIterations - 1, 3) + 1) + startValue; + }, + + easeInOutCubic: function(currentIteration, startValue, changeInValue, totalIterations) { + if ((currentIteration /= totalIterations / 2) < 1) { + return changeInValue / 2 * Math.pow(currentIteration, 3) + startValue; + } + return changeInValue / 2 * (Math.pow(currentIteration - 2, 3) + 2) + startValue; + }, + + easeInQuart: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * Math.pow (currentIteration / totalIterations, 4) + startValue; + }, + + easeOutQuart: function(currentIteration, startValue, changeInValue, totalIterations) { + return -changeInValue * (Math.pow(currentIteration / totalIterations - 1, 4) - 1) + startValue; + }, + + easeInOutQuart: function(currentIteration, startValue, changeInValue, totalIterations) { + if ((currentIteration /= totalIterations / 2) < 1) { + return changeInValue / 2 * Math.pow(currentIteration, 4) + startValue; + } + return -changeInValue / 2 * (Math.pow(currentIteration - 2, 4) - 2) + startValue; + }, + + easeInQuint: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * Math.pow (currentIteration / totalIterations, 5) + startValue; + }, + + easeOutQuint: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * (Math.pow(currentIteration / totalIterations - 1, 5) + 1) + startValue; + }, + + easeInOutQuint: function(currentIteration, startValue, changeInValue, totalIterations) { + if ((currentIteration /= totalIterations / 2) < 1) { + return changeInValue / 2 * Math.pow(currentIteration, 5) + startValue; + } + return changeInValue / 2 * (Math.pow(currentIteration - 2, 5) + 2) + startValue; + }, + + easeInSine: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * (1 - Math.cos(currentIteration / totalIterations * (Math.PI / 2))) + startValue; + }, + + easeOutSine: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * Math.sin(currentIteration / totalIterations * (Math.PI / 2)) + startValue; + }, + + easeInOutSine: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue / 2 * (1 - Math.cos(Math.PI * currentIteration / totalIterations)) + startValue; + }, + + easeInExpo: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * Math.pow(2, 10 * (currentIteration / totalIterations - 1)) + startValue; + }, + + easeOutExpo: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * (-Math.pow(2, -10 * currentIteration / totalIterations) + 1) + startValue; + }, + + easeInOutExpo: function(currentIteration, startValue, changeInValue, totalIterations) { + if ((currentIteration /= totalIterations / 2) < 1) { + return changeInValue / 2 * Math.pow(2, 10 * (currentIteration - 1)) + startValue; + } + return changeInValue / 2 * (-Math.pow(2, -10 * --currentIteration) + 2) + startValue; + }, + + easeInCirc: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * (1 - Math.sqrt(1 - (currentIteration /= totalIterations) * currentIteration)) + startValue; + }, + + easeOutCirc: function(currentIteration, startValue, changeInValue, totalIterations) { + return changeInValue * Math.sqrt(1 - (currentIteration = currentIteration / totalIterations - 1) * currentIteration) + startValue; + }, + + easeInOutCirc: function(currentIteration, startValue, changeInValue, totalIterations) { + if ((currentIteration /= totalIterations / 2) < 1) { + return changeInValue / 2 * (1 - Math.sqrt(1 - currentIteration * currentIteration)) + startValue; + } + return changeInValue / 2 * (Math.sqrt(1 - (currentIteration -= 2) * currentIteration) + 1) + startValue; + } + }; + + return service; +}]); diff --git a/src/shim.js b/src/shim.js new file mode 100644 index 00000000..c43c451c --- /dev/null +++ b/src/shim.js @@ -0,0 +1,29 @@ +// shim layer with setTimeout fallback +// credit Erik Möller and http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ +(function() { + var lastTime = 0; + var vendors = ['webkit', 'moz']; + for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; + window.cancelAnimationFrame = + window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; + } + + if (!window.requestAnimationFrame){ + window.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { callback(currTime + timeToCall); }, + timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + } + + if (!window.cancelAnimationFrame){ + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; + } + +}());