From b1f7dc412a414a8cd87d7ffc0561a1d3a94fbeb4 Mon Sep 17 00:00:00 2001 From: Thomas Burleson Date: Mon, 31 Aug 2015 18:08:07 -0500 Subject: [PATCH] fix(progressCircular): css reduction with js animations reduce the generated css by removing the scss for loops for animations of determinate indicators; use JS animations. * enable animation disables simply by setting md-mode = "" * hide div.md-spinner-wrapper when the md-mode is empty or not valid * use class selectors instead of attribute selectors (ie perfs) * create `animateIndicator()` for determinate indicators * remove SCSS for loops for css generation; ~54k reduction in CSS size BREAKING-CHANGES * ProgressCircular now uses Class selectors instead of `value` Attribute selectors. * `Determinate` animations are now JS based to set the CSS programmatically. Before: ```css md-progress-circular[value="1"] .md-inner .md-left .md-half-circle { -webkit-transform: rotate(135deg); transform: rotate(135deg); } md-progress-circular[value="1"] .md-inner .md-right .md-half-circle { transition: -webkit-transform 0.1s linear; transition: transform 0.1s linear; -webkit-transform: rotate(-131.4deg); transform: rotate(-131.4deg); } md-progress-circular[md-mode=indeterminate] .md-spinner-wrapper { -webkit-animation: outer-rotate 2.91667s linear infinite; } ``` Now: ```css // [Value]-based animation CSS generated in JS md-progress-circular.md-mode-indeterminate .md-spinner-wrapper { -webkit-animation: outer-rotate 2.91667s linear infinite; } ``` --- .../demoBasicUsage/index.html | 25 ++- .../progressCircular/demoBasicUsage/script.js | 7 +- .../progressCircular/progress-circular.js | 85 +++++++++- .../progressCircular/progress-circular.scss | 155 +++++++----------- src/core/util/animation/animate.js | 3 + 5 files changed, 154 insertions(+), 121 deletions(-) diff --git a/src/components/progressCircular/demoBasicUsage/index.html b/src/components/progressCircular/demoBasicUsage/index.html index 660ef88d6db..1a58493d0cb 100644 --- a/src/components/progressCircular/demoBasicUsage/index.html +++ b/src/components/progressCircular/demoBasicUsage/index.html @@ -14,28 +14,27 @@

Indeterminate

Theming

-

Use `md-warn` or `md-accent` to apply theme colors to your loader.

+

Your current theme colors can be used to easily colorize your progress indicator with `md-warn` or `md-accent` colors:

- +
-
+
-

Show Progress Circular Indicators:

+

Show Progress Circular Indicators:

-
Off
- -
On
-
-
+
Off
+ +
On
+
+
diff --git a/src/components/progressCircular/demoBasicUsage/script.js b/src/components/progressCircular/demoBasicUsage/script.js index 4668eacdf03..b1643ebbe1c 100644 --- a/src/components/progressCircular/demoBasicUsage/script.js +++ b/src/components/progressCircular/demoBasicUsage/script.js @@ -4,8 +4,7 @@ angular function($scope, $interval) { var self = this, j= 0, counter = 0; - self.mode = 'query'; - self.modes = new Array(4); + self.modes = [ ]; self.activated = true; self.determinateValue = 30; @@ -13,8 +12,8 @@ angular * Turn off or on the 5 themed loaders */ self.toggleActivation = function() { - if ( !self.activated ) self.modes = [ ]; - if ( self.activated ) j = counter = 0; + if ( !self.activated ) self.modes = [ ]; + if ( self.activated ) j = counter = 0; }; // Iterate every 100ms, non-stop diff --git a/src/components/progressCircular/progress-circular.js b/src/components/progressCircular/progress-circular.js index a7de0a550d8..2da4ac7a69f 100644 --- a/src/components/progressCircular/progress-circular.js +++ b/src/components/progressCircular/progress-circular.js @@ -44,12 +44,17 @@ angular.module('material.components.progressCircular', [ * * */ -function MdProgressCircularDirective($mdConstant, $mdTheming) { +function MdProgressCircularDirective($mdConstant, $mdTheming, $mdUtil) { var DEFAULT_PROGRESS_SIZE = 100; var DEFAULT_SCALING = 0.5; + var MODE_DETERMINATE = "determinate", + MODE_INDETERMINATE = "indeterminate"; + + return { restrict: 'E', + scope : true, template: // The progress 'circle' is composed of two half-circles: the left side and the right // side. Each side has CSS applied to 'fill-in' the half-circle to the appropriate progress. @@ -79,19 +84,79 @@ function MdProgressCircularDirective($mdConstant, $mdTheming) { function postLink(scope, element, attr) { $mdTheming(element); + var circle = element[0]; + var spinnerWrapper = angular.element(element.children()[0]); + + var lastMode, toVendorCSS = $mdUtil.dom.animator.toCss; + + // Update size/scaling of the progress indicator + // Watch the "value" and "md-mode" attributes + circle.style[$mdConstant.CSS.TRANSFORM] = 'scale(' + getDiameterRatio() + ')'; attr.$observe('value', function(value) { var percentValue = clamp(value); element.attr('aria-valuenow', percentValue); + + if (attr.mdMode == "determinate") { + animateIndicator(percentValue); + } }); - var spinnerWrapper = angular.element(element.children()[0]); attr.$observe('mdMode',function(mode){ - spinnerWrapper[mode ? 'removeClass' : 'addClass']('ng-hide'); + switch( mode ) { + case MODE_DETERMINATE: + case MODE_INDETERMINATE: + spinnerWrapper.removeClass('ng-hide'); + + // Inject class selector instead of attribute selector + // (@see layout.js changes for IE performance issues) + + if ( lastMode ) spinnerWrapper.removeClass( lastMode ); + lastMode = "md-mode-" + mode; + if ( lastMode ) spinnerWrapper.addClass( lastMode ); + + break; + default: + spinnerWrapper.addClass('ng-hide'); + } }); + var leftC, rightC, gap; + + /** + * Manually animate the Determinate indicator based on the specified + * percentage value (0-100). + * + * Note: this animation was previously done using SCSS. + * - generated 54K of styles + * - use attribute selectors which had poor performances in IE + */ + function animateIndicator(value) { + leftC = leftC || angular.element(element[0].querySelector('.md-left > .md-half-circle')); + rightC = rightC || angular.element(element[0].querySelector('.md-right > .md-half-circle')); + gap = gap || angular.element(element[0].querySelector('.md-gap')); + + var gapStyles = removeEmptyValues({ + borderBottomColor: (value <= 50) ? "transparent !important" : "", + transition: (value <= 50) ? "" : "borderBottomColor 0.1s linear" + }), + leftStyles = removeEmptyValues({ + transition: (value <= 50) ? "transform 0.1s linear" : "", + transform: $mdUtil.supplant("rotate({0}deg)", [value <= 50 ? 135 : ((((value - 50) / 50) * 180) + 135)]) + }), + rightStyles = removeEmptyValues({ + transition: (value >= 50) ? "transform 0.1s linear" : "", + transform: $mdUtil.supplant("rotate({0}deg)", [value >= 50 ? 45 : (value / 50 * 180 - 135)]) + }); + + leftC.css(toVendorCSS(leftStyles)); + rightC.css(toVendorCSS(rightStyles)); + gap.css(toVendorCSS(gapStyles)); + + } + /** * We will scale the progress circle based on the default diameter. * @@ -102,9 +167,7 @@ function MdProgressCircularDirective($mdConstant, $mdTheming) { if ( !attr.mdDiameter ) return DEFAULT_SCALING; var match = /([0-9]*)%/.exec(attr.mdDiameter); - var value = match && match[1]/100; - - value = Math.max(0, value || parseFloat(attr.mdDiameter)); + var value = Math.max(0, (match && match[1]/100) || parseFloat(attr.mdDiameter)); // should return ratio; DEFAULT_PROGRESS_SIZE === 100px is default size return (value > 1) ? value / DEFAULT_PROGRESS_SIZE : value; @@ -119,4 +182,14 @@ function MdProgressCircularDirective($mdConstant, $mdTheming) { function clamp(value) { return Math.max(0, Math.min(value || 0, 100)); } + + function removeEmptyValues(target) { + for (var key in target) { + if (target.hasOwnProperty(key)) { + if ( target[key] == "" ) delete target[key]; + } + } + + return target; + } } diff --git a/src/components/progressCircular/progress-circular.scss b/src/components/progressCircular/progress-circular.scss index 75fc2c13922..b99d119a911 100644 --- a/src/components/progressCircular/progress-circular.scss +++ b/src/components/progressCircular/progress-circular.scss @@ -6,28 +6,6 @@ $progress-circular-sporadic-duration : $progress-circular-duration !default; $progress-border-width : 10px; $progress-circular-size : 10 * $progress-border-width !default; -@keyframes outer-rotate { - 100% { transform: rotate(360deg); } -} -@keyframes left-wobble { - 0%, 100% { transform: rotate(130deg); } - 50% { transform: rotate( -5deg); } -} -@keyframes right-wobble { - 0%, 100% { transform: rotate(-130deg); } - 50% { transform: rotate( 5deg); } -} -@keyframes sporadic-rotate { - 12.5% { transform: rotate( 135deg); } - 25% { transform: rotate( 270deg); } - 37.5% { transform: rotate( 405deg); } - 50% { transform: rotate( 540deg); } - 62.5% { transform: rotate( 675deg); } - 75% { transform: rotate( 810deg); } - 87.5% { transform: rotate( 945deg); } - 100% { transform: rotate(1080deg); } -} - md-progress-circular { width: $progress-circular-size; height: $progress-circular-size; @@ -86,79 +64,32 @@ md-progress-circular { } } - // TODO: This while-loop generates about 2 kilobytes of css after gzip. - // Refactor pr ogressCircular to animate with javascript. - $i: 0; - @while $i <= 100 { - &[value="#{$i}"] { - .md-inner { - .md-left { - .md-half-circle { - @if $i < 50 { - transform: rotate(135deg); - } @else { - transition: transform 0.1s linear; - $deg: ((($i - 50) / 50) * 180) + 135; - transform: rotate(#{$deg}deg); - } - } - } - .md-right { - .md-half-circle { - @if $i <= 50 { - transition: transform 0.1s linear; - $deg: $i / 50 * 180 - 135; - transform: rotate(#{$deg}deg); - } @else { - transform: rotate(45deg); - } - } - } - .md-gap { - border-bottom-width: $progress-border-width; - border-bottom-style: solid; - @if $i <= 50 { - border-bottom-color: transparent !important; - } @else { - transition: border-bottom-color 0.1s linear; - } + .md-spinner-wrapper.md-mode-indeterminate { + animation: outer-rotate $progress-circular-outer-duration linear infinite; + .md-inner { + animation: sporadic-rotate $progress-circular-sporadic-duration $progress-circular-ease-in-out infinite; + .md-left, .md-right { + .md-half-circle { + animation-iteration-count: infinite; + animation-duration: ($progress-circular-duration * 0.25); + animation-timing-function: $progress-circular-ease-in-out; } } - } - $i: $i + 1; - } - - &[md-mode=indeterminate], .md-mode-indeterminate { - .md-spinner-wrapper { - animation: outer-rotate $progress-circular-outer-duration linear infinite; - .md-inner { - animation: sporadic-rotate $progress-circular-sporadic-duration $progress-circular-ease-in-out infinite; - .md-left, .md-right { - .md-half-circle { - animation-iteration-count: infinite; - animation-duration: ($progress-circular-duration * 0.25); - animation-timing-function: $progress-circular-ease-in-out; - } - } - .md-left { - .md-half-circle { - animation-name: left-wobble; - } + .md-left { + .md-half-circle { + animation-name: left-wobble; } - .md-right { - .md-half-circle { - animation-name: right-wobble; - } + } + .md-right { + .md-half-circle { + animation-name: right-wobble; } } } } -} -.ng-hide md-progress-circular, -md-progress-circular.ng-hide, { - &[md-mode=indeterminate], .md-mode-indeterminate { - .md-spinner-wrapper { + .ng-hide md-progress-circular, md-progress-circular.ng-hide { + .md-spinner-wrapper { animation: none; .md-inner { animation: none; @@ -175,21 +106,49 @@ md-progress-circular.ng-hide, { } } } -} -.md-spinner-wrapper.ng-hide { - animation: none; - .md-inner { + .md-spinner-wrapper.ng-hide { animation: none; - .md-left { - .md-half-circle { - animation-name: none; + .md-inner { + animation: none; + .md-left { + .md-half-circle { + animation-name: none; + } } - } - .md-right { - .md-half-circle { - animation-name: none; + .md-right { + .md-half-circle { + animation-name: none; + } } } } + +} + + +// +// Keyframe animation for the Indeterminate Progress +// +@keyframes outer-rotate { + 100% { transform: rotate(360deg); } } +@keyframes left-wobble { + 0%, 100% { transform: rotate(130deg); } + 50% { transform: rotate( -5deg); } +} +@keyframes right-wobble { + 0%, 100% { transform: rotate(-130deg); } + 50% { transform: rotate( 5deg); } +} +@keyframes sporadic-rotate { + 12.5% { transform: rotate( 135deg); } + 25% { transform: rotate( 270deg); } + 37.5% { transform: rotate( 405deg); } + 50% { transform: rotate( 540deg); } + 62.5% { transform: rotate( 675deg); } + 75% { transform: rotate( 810deg); } + 87.5% { transform: rotate( 945deg); } + 100% { transform: rotate(1080deg); } +} + diff --git a/src/core/util/animation/animate.js b/src/core/util/animation/animate.js index 93be841d2f0..975d9e2fb62 100644 --- a/src/core/util/animation/animate.js +++ b/src/core/util/animation/animate.js @@ -124,6 +124,9 @@ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { css[key] = value + 'px'; } else { switch (key) { + case 'transition': + convertToVendor(key, $mdConstant.CSS.TRANSITION, value); + break; case 'transform': convertToVendor(key, $mdConstant.CSS.TRANSFORM, value); break;