Bug 1406285 - Part 7: Implement effect timing graph. r?gl draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Thu, 18 Jan 2018 12:46:38 +0900
changeset 721936 362bdadea7a37f5c869c22a27a765cc6abd120a8
parent 721935 0706ac027461f154083fb4f2f079ab209855ec5c
child 721937 70dd7b7a8217d4bfe8e2ddb8627a5f7ab1ae1b95
push id96003
push userbmo:dakatsuka@mozilla.com
push dateThu, 18 Jan 2018 05:23:36 +0000
reviewersgl
bugs1406285
milestone59.0a1
Bug 1406285 - Part 7: Implement effect timing graph. r?gl MozReview-Commit-ID: DIrt9PdY2Nd
devtools/client/inspector/animation/components/graph/EffectTimingPath.js
devtools/client/inspector/animation/components/graph/SummaryGraphPath.js
devtools/client/inspector/animation/components/graph/moz.build
devtools/client/inspector/animation/utils/graph-helper.js
devtools/client/themes/animation.css
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/graph/EffectTimingPath.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+const { SummaryGraphHelper, toPathString } = require("../../utils/graph-helper");
+const TimingPath = require("./TimingPath");
+
+class EffectTimingPath extends TimingPath {
+  static get propTypes() {
+    return {
+      animation: PropTypes.object.isRequired,
+      durationPerPixel: PropTypes.number.isRequired,
+      simulateAnimation: PropTypes.func.isRequired,
+      totalDuration: PropTypes.number.isRequired,
+    };
+  }
+
+  render() {
+    const {
+      animation,
+      durationPerPixel,
+      simulateAnimation,
+      totalDuration,
+    } = this.props;
+
+    const { state } = animation;
+    const effectTiming = Object.assign({}, state, {
+      iterations: state.iterationCount ? state.iterationCount : Infinity
+    });
+
+    const simulatedAnimation = simulateAnimation(null, effectTiming, false);
+    const endTime = simulatedAnimation.effect.getComputedTiming().endTime;
+
+    const getValueFunc = time => {
+      if (time < 0) {
+        return { x: time, y: 0 };
+      }
+
+      simulatedAnimation.currentTime = time < endTime ? time : endTime;
+      return Math.max(simulatedAnimation.effect.getComputedTiming().progress, 0);
+    };
+
+    const toPathStringFunc = segments => {
+      const firstSegment = segments[0];
+      let pathString = `M${ firstSegment.x },0 `;
+      pathString += toPathString(segments);
+      const lastSegment = segments[segments.length - 1];
+      pathString += `L${ lastSegment.x },0`;
+      return pathString;
+    };
+
+    const helper = new SummaryGraphHelper(state, null,
+                                          totalDuration, durationPerPixel,
+                                          getValueFunc, toPathStringFunc);
+    const offset = state.previousStartTime ? state.previousStartTime : 0;
+
+    return dom.g(
+      {
+        className: "animation-effect-timing-path",
+        transform: `translate(${ offset })`
+      },
+      super.renderGraph(state, helper)
+    );
+  }
+}
+
+module.exports = EffectTimingPath;
--- a/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js
+++ b/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js
@@ -5,16 +5,19 @@
 "use strict";
 
 const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 
 const ComputedTimingPath = createFactory(require("./ComputedTimingPath"));
+const EffectTimingPath = createFactory(require("./EffectTimingPath"));
+const { DEFAULT_GRAPH_HEIGHT } = require("../../utils/graph-helper");
+
 // Minimum opacity for semitransparent fill color for keyframes's easing graph.
 const MIN_KEYFRAMES_EASING_OPACITY = 0.5;
 
 class SummaryGraphPath extends PureComponent {
   static get propTypes() {
     return {
       animation: PropTypes.object.isRequired,
       simulateAnimation: PropTypes.func.isRequired,
@@ -158,27 +161,39 @@ class SummaryGraphPath extends PureCompo
     const keyframesList =
       this.getOffsetAndEasingOnlyKeyframes(animation.animatedPropertyMap);
     const opacity = Math.max(1 / keyframesList.length, MIN_KEYFRAMES_EASING_OPACITY);
 
     return dom.svg(
       {
         className: "animation-summary-graph-path",
         preserveAspectRatio: "none",
-        viewBox: `${ startTime } -1 ${ totalDuration } 1`
+        viewBox: `${ startTime } -${ DEFAULT_GRAPH_HEIGHT } `
+                 + `${ totalDuration } ${ DEFAULT_GRAPH_HEIGHT }`,
       },
       keyframesList.map(keyframes =>
         ComputedTimingPath(
           {
             animation,
             durationPerPixel,
             keyframes,
             opacity,
             simulateAnimation,
             totalDuration,
           }
         )
+      ),
+      animation.state.easing !== "linear" ?
+      EffectTimingPath(
+        {
+          animation,
+          durationPerPixel,
+          simulateAnimation,
+          totalDuration,
+        }
       )
+      :
+      null
     );
   }
 }
 
 module.exports = SummaryGraphPath;
--- a/devtools/client/inspector/animation/components/graph/moz.build
+++ b/devtools/client/inspector/animation/components/graph/moz.build
@@ -1,10 +1,11 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'ComputedTimingPath.js',
+    'EffectTimingPath.js',
     'SummaryGraph.js',
     'SummaryGraphPath.js',
     'TimingPath.js'
 )
--- a/devtools/client/inspector/animation/utils/graph-helper.js
+++ b/devtools/client/inspector/animation/utils/graph-helper.js
@@ -2,25 +2,28 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 // BOUND_EXCLUDING_TIME should be less than 1ms and is used to exclude start
 // and end bounds when dividing  duration in createPathSegments.
 const BOUND_EXCLUDING_TIME = 0.001;
+// We define default graph height since if the height of viewport in SVG is
+// too small (e.g. 1), vector-effect may not be able to calculate correctly.
+const DEFAULT_GRAPH_HEIGHT = 100;
 // DEFAULT_MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
 const DEFAULT_MIN_PROGRESS_THRESHOLD = 0.1;
 // In the createPathSegments function, an animation duration is divided by
 // DURATION_RESOLUTION in order to draw the way the animation progresses.
 // But depending on the timing-function, we may be not able to make the graph
 // smoothly progress if this resolution is not high enough.
 // So, if the difference of animation progress between 2 divisions is more than
-// DEFAULT_MIN_PROGRESS_THRESHOLD, then createPathSegments re-divides
-// by DURATION_RESOLUTION.
+// DEFAULT_MIN_PROGRESS_THRESHOLD * DEFAULT_GRAPH_HEIGHT, then createPathSegments
+// re-divides by DURATION_RESOLUTION.
 // DURATION_RESOLUTION shoud be integer and more than 2.
 const DURATION_RESOLUTION = 4;
 
 /**
  * The helper class for creating summary graph.
  */
 class SummaryGraphHelper {
   /**
@@ -40,17 +43,18 @@ class SummaryGraphHelper {
    *        e.g. time => { return 1.0 };
    * @param {Function} toPathStringFunc
    *        Which returns a path string for 'd' attribute for <path> from given segments.
    */
   constructor(state, keyframes, totalDuration, minSegmentDuration,
               getValueFunc, toPathStringFunc) {
     this.totalDuration = totalDuration;
     this.minSegmentDuration = minSegmentDuration;
-    this.minProgressThreshold = getPreferredProgressThreshold(state, keyframes);
+    this.minProgressThreshold =
+      getPreferredProgressThreshold(state, keyframes) * DEFAULT_GRAPH_HEIGHT;
     this.durationResolution = getPreferredDurationResolution(keyframes);
     this.getValue = getValueFunc;
     this.toPathString = toPathStringFunc;
 
     this.getSegment = this.getSegment.bind(this);
   }
 
   /**
@@ -74,17 +78,17 @@ class SummaryGraphHelper {
    * Return a coordinate as a graph segment at given time.
    *
    * @param {Number} time
    * @return {Object}
    *         { x: Number, y: Number }
    */
   getSegment(time) {
     const value = this.getValue(time);
-    return { x: time, y: value };
+    return { x: time, y: value * DEFAULT_GRAPH_HEIGHT };
   }
 }
 
 /**
  * Create the path segments from given parameters.
  *
  * @param {Number} startTime
  *        Starting time of animation.
@@ -230,10 +234,11 @@ function getStepsOrFramesCount(easing) {
 function toPathString(segments) {
   let pathString = "";
   segments.forEach(segment => {
     pathString += `L${ segment.x },${ segment.y } `;
   });
   return pathString;
 }
 
+module.exports.DEFAULT_GRAPH_HEIGHT = DEFAULT_GRAPH_HEIGHT;
 exports.SummaryGraphHelper = SummaryGraphHelper;
 exports.toPathString = toPathString;
--- a/devtools/client/themes/animation.css
+++ b/devtools/client/themes/animation.css
@@ -53,24 +53,27 @@
 }
 
 .animation-item:nth-child(2n+1) {
   background-color: var(--animation-even-background-color);
 }
 
 .animation-item.cssanimation {
   --computed-timing-graph-color: var(--theme-contrast-background);
+  --effect-timing-graph-color: var(--theme-highlight-lightorange);
 }
 
 .animation-item.csstransition {
   --computed-timing-graph-color: var(--theme-highlight-blue);
+  --effect-timing-graph-color: var(--theme-highlight-bluegrey);
 }
 
 .animation-item.scriptanimation {
   --computed-timing-graph-color: var(--theme-graphs-green);
+  --effect-timing-graph-color: var(--theme-highlight-green);
 }
 
 /* Animation Target */
 .animation-target {
   align-items: center;
   display: flex;
   height: 100%;
   padding-left: 4px;
@@ -98,16 +101,28 @@
   vector-effect: non-scaling-stroke;
   transform: scale(1, -1);
 }
 
 .animation-computed-timing-path path.infinity:nth-child(n+2) {
   opacity: 0.3;
 }
 
+.animation-effect-timing-path path {
+  fill: none;
+  stroke: var(--effect-timing-graph-color);
+  stroke-dasharray: 2px 2px;
+  transform: scale(1, -1);
+  vector-effect: non-scaling-stroke;
+}
+
+.animation-effect-timing-path path.infinity:nth-child(n+2) {
+  opacity: 0.3;
+}
+
 /* No Animation Panel */
 .animation-error-message {
   overflow: auto;
 }
 
 .animation-error-message > p {
   white-space: pre;
 }