Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit cf38b29

Browse files
clshortfusemmalerba
authored andcommitted
fix(progressCircular): update animation to spec (#10017)
Previous animation did not properly animate the circle stroke * Use persistent SVG path for all animations * Change `stroke-dashoffset` to match spec on every requested frame * Rotate object -90 degrees every iteration * Correct overall counter clockwise animation timing in SCSS Instead of creating a new SVG path every frame, we will instead simply animate the `stroke-dashoffset` parameter. This allows us to match spec while also increasing performance by simplifying logic per frame. Fixes #9879
1 parent 388a340 commit cf38b29

File tree

4 files changed

+49
-62
lines changed

4 files changed

+49
-62
lines changed

src/components/progressCircular/js/progressCircularDirective.js

Lines changed: 44 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ function MdProgressCircularDirective($window, $mdProgressCircular, $mdTheming,
6262
$window.webkitCancelRequestAnimationFrame ||
6363
angular.noop;
6464

65-
var DEGREE_IN_RADIANS = $window.Math.PI / 180;
6665
var MODE_DETERMINATE = 'determinate';
6766
var MODE_INDETERMINATE = 'indeterminate';
6867
var DISABLED_CLASS = '_md-progress-circular-disabled';
@@ -103,7 +102,7 @@ function MdProgressCircularDirective($window, $mdProgressCircular, $mdTheming,
103102
var path = angular.element(node.querySelector('path'));
104103
var startIndeterminate = $mdProgressCircular.startIndeterminate;
105104
var endIndeterminate = $mdProgressCircular.endIndeterminate;
106-
var rotationIndeterminate = 0;
105+
var iterationCount = 0;
107106
var lastAnimationId = 0;
108107
var lastDrawFrame;
109108
var interval;
@@ -196,40 +195,52 @@ function MdProgressCircularDirective($window, $mdProgressCircular, $mdTheming,
196195
.css('transform-origin', transformOrigin + ' ' + transformOrigin + ' ' + transformOrigin);
197196

198197
element.css(dimensions);
199-
path.css('stroke-width', strokeWidth + 'px');
200198

201-
renderCircle(value, value);
199+
path.attr('stroke-width', strokeWidth);
200+
path.attr('stroke-linecap', 'square');
201+
if (scope.mdMode == MODE_INDETERMINATE) {
202+
path.attr('d', getSvgArc(diameter, true));
203+
path.attr('stroke-dasharray', diameter * $window.Math.PI * 0.75);
204+
path.attr('stroke-dashoffset', getDashLength(diameter, 1, 75));
205+
} else {
206+
path.attr('d', getSvgArc(diameter, false));
207+
path.attr('stroke-dasharray', diameter * $window.Math.PI);
208+
path.attr('stroke-dashoffset', getDashLength(diameter, 0, 100));
209+
renderCircle(value, value);
210+
}
211+
202212
});
203213

204-
function renderCircle(animateFrom, animateTo, easing, duration, rotation) {
214+
function renderCircle(animateFrom, animateTo, easing, duration, iterationCount, maxValue) {
205215
var id = ++lastAnimationId;
206216
var startTime = $mdUtil.now();
207217
var changeInValue = animateTo - animateFrom;
208218
var diameter = getSize(scope.mdDiameter);
209-
var pathDiameter = diameter - getStroke(diameter);
210219
var ease = easing || $mdProgressCircular.easeFn;
211220
var animationDuration = duration || $mdProgressCircular.duration;
221+
var rotation = -90 * (iterationCount || 0);
222+
var dashLimit = maxValue || 100;
212223

213224
// No need to animate it if the values are the same
214225
if (animateTo === animateFrom) {
215-
path.attr('d', getSvgArc(animateTo, diameter, pathDiameter, rotation));
226+
renderFrame(animateTo);
216227
} else {
217228
lastDrawFrame = rAF(function animation() {
218229
var currentTime = $window.Math.max(0, $window.Math.min($mdUtil.now() - startTime, animationDuration));
219230

220-
path.attr('d', getSvgArc(
221-
ease(currentTime, animateFrom, changeInValue, animationDuration),
222-
diameter,
223-
pathDiameter,
224-
rotation
225-
));
231+
renderFrame(ease(currentTime, animateFrom, changeInValue, animationDuration));
226232

227233
// Do not allow overlapping animations
228234
if (id === lastAnimationId && currentTime < animationDuration) {
229235
lastDrawFrame = rAF(animation);
230236
}
231237
});
232238
}
239+
240+
function renderFrame(value) {
241+
path.attr('stroke-dashoffset', getDashLength(diameter, value, dashLimit));
242+
path.attr('transform','rotate(' + (rotation) + ' ' + diameter/2 + ' ' + diameter/2 + ')');
243+
}
233244
}
234245

235246
function animateIndeterminate() {
@@ -238,24 +249,22 @@ function MdProgressCircularDirective($window, $mdProgressCircular, $mdTheming,
238249
endIndeterminate,
239250
$mdProgressCircular.easeFnIndeterminate,
240251
$mdProgressCircular.durationIndeterminate,
241-
rotationIndeterminate
252+
iterationCount,
253+
75
242254
);
243255

244-
// The % 100 technically isn't necessary, but it keeps the rotation
245-
// under 100, instead of becoming a crazy large number.
246-
rotationIndeterminate = (rotationIndeterminate + endIndeterminate) % 100;
256+
// The %4 technically isn't necessary, but it keeps the rotation
257+
// under 360, instead of becoming a crazy large number.
258+
iterationCount = ++iterationCount % 4;
247259

248-
var temp = startIndeterminate;
249-
startIndeterminate = -endIndeterminate;
250-
endIndeterminate = -temp;
251260
}
252261

253262
function startIndeterminateAnimation() {
254263
if (!interval) {
255264
// Note that this interval isn't supposed to trigger a digest.
256265
interval = $interval(
257266
animateIndeterminate,
258-
$mdProgressCircular.durationIndeterminate + 50,
267+
$mdProgressCircular.durationIndeterminate,
259268
0,
260269
false
261270
);
@@ -278,55 +287,32 @@ function MdProgressCircularDirective($window, $mdProgressCircular, $mdTheming,
278287
}
279288

280289
/**
281-
* Generates an arc following the SVG arc syntax.
290+
* Returns SVG path data for progress circle
282291
* Syntax spec: https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
283292
*
284-
* @param {number} current Current value between 0 and 100.
285293
* @param {number} diameter Diameter of the container.
286-
* @param {number} pathDiameter Diameter of the path element.
287-
* @param {number=0} rotation The point at which the semicircle should start rendering.
288-
* Used for doing the indeterminate animation.
294+
* @param {boolean} indeterminate Use if progress circle will be used for indeterminate
289295
*
290296
* @returns {string} String representation of an SVG arc.
291297
*/
292-
function getSvgArc(current, diameter, pathDiameter, rotation) {
293-
// The angle can't be exactly 360, because the arc becomes hidden.
294-
var maximumAngle = 359.99 / 100;
295-
var startPoint = rotation || 0;
298+
function getSvgArc(diameter, indeterminate) {
296299
var radius = diameter / 2;
297-
var pathRadius = pathDiameter / 2;
298-
299-
var startAngle = startPoint * maximumAngle;
300-
var endAngle = current * maximumAngle;
301-
var start = polarToCartesian(radius, pathRadius, startAngle);
302-
var end = polarToCartesian(radius, pathRadius, endAngle + startAngle);
303-
var arcSweep = endAngle < 0 ? 0 : 1;
304-
var largeArcFlag;
305-
306-
if (endAngle < 0) {
307-
largeArcFlag = endAngle >= -180 ? 0 : 1;
308-
} else {
309-
largeArcFlag = endAngle <= 180 ? 0 : 1;
310-
}
311-
312-
return 'M' + start + 'A' + pathRadius + ',' + pathRadius +
313-
' 0 ' + largeArcFlag + ',' + arcSweep + ' ' + end;
300+
return 'M' + radius + ',0'
301+
+ 'A' + radius + ',' + radius + ' 0 1 1 0,' + radius // 75% circle
302+
+ (indeterminate ? '' : 'A' + radius + ',' + radius + ' 0 0 1 ' + radius + ',0');
314303
}
315304

316305
/**
317-
* Converts Polar coordinates to Cartesian.
306+
* Return stroke length for progress circle
318307
*
319-
* @param {number} radius Radius of the container.
320-
* @param {number} pathRadius Radius of the path element
321-
* @param {number} angleInDegress Angle at which to place the point.
308+
* @param {number} diameter Diameter of the container.
309+
* @param {number} value Percentage of circle (between 0 and 100)
310+
* @param {number} limit Max percentage for circle
322311
*
323-
* @returns {string} Cartesian coordinates in the format of `x,y`.
312+
* @returns {number} Stroke length for progres circle
324313
*/
325-
function polarToCartesian(radius, pathRadius, angleInDegrees) {
326-
var angleInRadians = (angleInDegrees - 90) * DEGREE_IN_RADIANS;
327-
328-
return (radius + (pathRadius * $window.Math.cos(angleInRadians))) +
329-
',' + (radius + (pathRadius * $window.Math.sin(angleInRadians)));
314+
function getDashLength(diameter, value, limit) {
315+
return diameter * $window.Math.PI * ( (3 * (limit || 100) / 100) - (value/100) );
330316
}
331317

332318
/**
@@ -363,4 +349,5 @@ function MdProgressCircularDirective($window, $mdProgressCircular, $mdTheming,
363349
function getStroke(diameter) {
364350
return $mdProgressCircular.strokeWidth / 100 * diameter;
365351
}
352+
366353
}

src/components/progressCircular/js/progressCircularProvider.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ function MdProgressCircularProvider() {
4646
duration: 100,
4747
easeFn: linearEase,
4848

49-
durationIndeterminate: 500,
50-
startIndeterminate: 3,
51-
endIndeterminate: 80,
49+
durationIndeterminate: 1333,
50+
startIndeterminate: 1,
51+
endIndeterminate: 149,
5252
easeFnIndeterminate: materialEase,
5353

5454
easingPresets: {

src/components/progressCircular/progress-circular.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
$progress-circular-indeterminate-duration: 2.9s !default;
1+
$progress-circular-indeterminate-duration: 1568.63ms !default;
22

33
@keyframes indeterminate-rotate {
44
0% { transform: rotate(0deg); }

src/components/progressCircular/progress-circular.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ describe('mdProgressCircular', function() {
9595
'<md-progress-circular md-diameter="' + diameter + '"></md-progress-circular>'
9696
).find('path').eq(0);
9797

98-
expect(path.css('stroke-width')).toBe(diameter / ratio + 'px');
98+
expect(parseFloat(path.attr('stroke-width'))).toBe(diameter / ratio);
9999
});
100100

101101
it('should hide the element if is disabled', function() {

0 commit comments

Comments
 (0)