Bug 1416106 - Part 10: Implement easing hit. r?gl draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Wed, 14 Feb 2018 23:18:13 +0900
changeset 755235 61c4770af2d8b8e31f66ec20a8279da544b805fc
parent 755234 eb69e810f6a6759ac69d2fa563fabc6f6ef63863
child 755236 7b245fa7c2ec0d7e1dbad53ba8fe83a74adc4781
push id99127
push userbmo:dakatsuka@mozilla.com
push dateThu, 15 Feb 2018 00:47:03 +0000
reviewersgl
bugs1416106
milestone60.0a1
Bug 1416106 - Part 10: Implement easing hit. r?gl MozReview-Commit-ID: 5d6f1dysdxm
devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js
devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js
devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js
devtools/client/inspector/animation/utils/graph-helper.js
devtools/client/themes/animation.css
--- a/devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js
+++ b/devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js
@@ -62,16 +62,58 @@ class ColorPath extends ComputedStylePat
     const { baseValue, maxDistance } = this.state;
     const value = getRGBA(computedStyle);
     return getRGBADistance(baseValue, value) / maxDistance;
   }
 
   /**
    * Overide parent's method.
    */
+  renderEasingHint() {
+    const {
+      easingHintStrokeWidth,
+      graphHeight,
+      totalDuration,
+      values,
+    } = this.props;
+
+    const hints = [];
+
+    for (let i = 0; i < values.length - 1; i++) {
+      const startKeyframe = values[i];
+      const endKeyframe = values[i + 1];
+      const startTime = startKeyframe.offset * totalDuration;
+      const endTime = endKeyframe.offset * totalDuration;
+
+      const g = dom.g(
+        {
+          className: "hint"
+        },
+        dom.title({}, startKeyframe.easing),
+        dom.rect(
+          {
+            x: startTime,
+            y: -graphHeight,
+            height: graphHeight,
+            width: endTime - startTime,
+            style: {
+              "stroke-width": easingHintStrokeWidth,
+            },
+          }
+        )
+      );
+      hints.push(g);
+    }
+
+    return hints;
+  }
+
+  /**
+   * Overide parent's method.
+   */
   renderPathSegments(segments) {
     for (const segment of segments) {
       segment.y = 1;
     }
 
     const lastSegment = segments[segments.length - 1];
     const id = `color-property-${ LINEAR_GRADIENT_ID_COUNT++ }`;
     const path = super.renderPathSegments(segments, { fill: `url(#${ id })` });
--- a/devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js
+++ b/devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js
@@ -36,16 +36,17 @@ const {
  *          e.g. 0
  *   @return {Number}
  *          e.g. 0 (should be 0 - 1.0)
  */
 class ComputedStylePath extends PureComponent {
   static get propTypes() {
     return {
       componentWidth: PropTypes.number.isRequired,
+      easingHintStrokeWidth: PropTypes.number.isRequired,
       graphHeight: PropTypes.number.isRequired,
       simulateAnimation: PropTypes.func.isRequired,
       totalDuration: PropTypes.number.isRequired,
       values: PropTypes.array.isRequired,
     };
   }
 
   /**
@@ -106,32 +107,92 @@ class ComputedStylePath extends PureComp
     for (const segment of segments) {
       segment.x += offset;
     }
 
     return segments;
   }
 
   /**
+   * Render easing hint from given path segments.
+   *
+   * @param {Array} segments
+   *        Path segments.
+   * @return {Element}
+   *         Element which represents easing hint.
+   */
+  renderEasingHint(segments) {
+    const {
+      easingHintStrokeWidth,
+      totalDuration,
+      values,
+    } = this.props;
+
+    const hints = [];
+
+    for (let i = 0, indexOfSegments = 0; i < values.length - 1; i++) {
+      const startKeyframe = values[i];
+      const endKeyframe = values[i + 1];
+      const endTime = endKeyframe.offset * totalDuration;
+      const hintSegments = [];
+
+      for (; indexOfSegments < segments.length; indexOfSegments++) {
+        const segment = segments[indexOfSegments];
+        hintSegments.push(segment);
+
+        if (startKeyframe.offset === endKeyframe.offset) {
+          hintSegments.push(segments[++indexOfSegments]);
+          break;
+        } else if (segment.x === endTime) {
+          break;
+        }
+      }
+
+      const g = dom.g(
+        {
+          className: "hint"
+        },
+        dom.title({}, startKeyframe.easing),
+        dom.path(
+          {
+            d: `M${ hintSegments[0].x },${ hintSegments[0].y } ` +
+               toPathString(hintSegments),
+            style: {
+              "stroke-width": easingHintStrokeWidth,
+            }
+          }
+        )
+      );
+
+      hints.push(g);
+    }
+
+    return hints;
+  }
+
+  /**
    * Render graph. This method returns React dom.
    *
    * @return {Element}
    */
   renderGraph() {
     const { values } = this.props;
 
     const segments = [];
 
     for (let i = 0; i < values.length - 1; i++) {
       const startValue = values[i];
       const endValue = values[i + 1];
       segments.push(...this.getPathSegments(startValue, endValue));
     }
 
-    return this.renderPathSegments(segments);
+    return [
+      this.renderPathSegments(segments),
+      this.renderEasingHint(segments)
+    ];
   }
 
   /**
    * Return react dom fron given path segments.
    *
    * @param {Array} segments
    * @param {Object} style
    * @return {Element}
--- a/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js
+++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js
@@ -9,16 +9,17 @@ const dom = require("devtools/client/sha
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 
 const ColorPath = createFactory(require("./ColorPath"));
 const DiscretePath = createFactory(require("./DiscretePath"));
 const DistancePath = createFactory(require("./DistancePath"));
 
 const {
+  DEFAULT_EASING_HINT_STROKE_WIDTH,
   DEFAULT_GRAPH_HEIGHT,
   DEFAULT_KEYFRAMES_GRAPH_DURATION,
 } = require("../../utils/graph-helper");
 
 class KeyframesGraphPath extends PureComponent {
   static get propTypes() {
     return {
       getComputedStyle: PropTypes.func.isRequired,
@@ -28,16 +29,17 @@ class KeyframesGraphPath extends PureCom
       values: PropTypes.array.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
+      componentHeight: 0,
       componentWidth: 0,
     };
   }
 
   componentDidMount() {
     this.updateState();
   }
 
@@ -49,45 +51,55 @@ class KeyframesGraphPath extends PureCom
         return DiscretePath;
       default :
         return DistancePath;
     }
   }
 
   updateState() {
     const thisEl = ReactDOM.findDOMNode(this);
-    this.setState({ componentWidth: thisEl.parentNode.clientWidth });
+    this.setState({
+      componentHeight: thisEl.parentNode.clientHeight,
+      componentWidth: thisEl.parentNode.clientWidth,
+    });
   }
 
   render() {
     const {
       getComputedStyle,
       property,
       simulateAnimation,
       type,
       values,
     } = this.props;
-    const { componentWidth } = this.state;
+    const {
+      componentHeight,
+      componentWidth,
+    } = this.state;
 
     if (!componentWidth) {
       return dom.svg();
     }
 
     const pathComponent = this.getPathComponent(type);
+    const strokeWidthInViewBox =
+      DEFAULT_EASING_HINT_STROKE_WIDTH / 2 / componentHeight * DEFAULT_GRAPH_HEIGHT;
 
     return dom.svg(
       {
         className: "keyframes-graph-path",
         preserveAspectRatio: "none",
-        viewBox: `0 -${ DEFAULT_GRAPH_HEIGHT } `
-                 + `${ DEFAULT_KEYFRAMES_GRAPH_DURATION } ${ DEFAULT_GRAPH_HEIGHT }`,
+        viewBox: `0 -${ DEFAULT_GRAPH_HEIGHT + strokeWidthInViewBox } ` +
+                 `${ DEFAULT_KEYFRAMES_GRAPH_DURATION } ` +
+                 `${ DEFAULT_GRAPH_HEIGHT + strokeWidthInViewBox * 2 }`,
       },
       pathComponent(
         {
           componentWidth,
+          easingHintStrokeWidth: DEFAULT_EASING_HINT_STROKE_WIDTH,
           getComputedStyle,
           graphHeight: DEFAULT_GRAPH_HEIGHT,
           property,
           simulateAnimation,
           totalDuration: DEFAULT_KEYFRAMES_GRAPH_DURATION,
           values,
         }
       )
--- a/devtools/client/inspector/animation/utils/graph-helper.js
+++ b/devtools/client/inspector/animation/utils/graph-helper.js
@@ -18,16 +18,18 @@ const DEFAULT_MIN_PROGRESS_THRESHOLD = 0
 // DEFAULT_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 * DEFAULT_GRAPH_HEIGHT, then createPathSegments
 // re-divides by DEFAULT_DURATION_RESOLUTION.
 // DEFAULT_DURATION_RESOLUTION shoud be integer and more than 2.
 const DEFAULT_DURATION_RESOLUTION = 4;
+// Stroke width for easing hint.
+const DEFAULT_EASING_HINT_STROKE_WIDTH = 5;
 
 /**
  * The helper class for creating summary graph.
  */
 class SummaryGraphHelper {
   /**
    * Constructor.
    *
@@ -255,14 +257,15 @@ function toPathString(segments) {
   segments.forEach(segment => {
     pathString += `L${ segment.x },${ segment.y } `;
   });
   return pathString;
 }
 
 exports.createPathSegments = createPathSegments;
 exports.DEFAULT_DURATION_RESOLUTION = DEFAULT_DURATION_RESOLUTION;
+exports.DEFAULT_EASING_HINT_STROKE_WIDTH = DEFAULT_EASING_HINT_STROKE_WIDTH;
 exports.DEFAULT_GRAPH_HEIGHT = DEFAULT_GRAPH_HEIGHT;
 exports.DEFAULT_KEYFRAMES_GRAPH_DURATION = DEFAULT_KEYFRAMES_GRAPH_DURATION;
 exports.getPreferredProgressThresholdByKeyframes =
   getPreferredProgressThresholdByKeyframes;
 exports.SummaryGraphHelper = SummaryGraphHelper;
 exports.toPathString = toPathString;
--- a/devtools/client/themes/animation.css
+++ b/devtools/client/themes/animation.css
@@ -355,17 +355,17 @@
 
 .animated-property-name.warning span {
   text-decoration: underline dotted;
 }
 
 /* Keyframes Graph */
 .keyframes-graph {
   height: 100%;
-  padding-top: 5px;
+  padding-top: 3px;
   width: calc(100% - var(--sidebar-width) - var(--graph-right-offset));
 }
 
 .keyframes-graph-path {
   height: 100%;
   width: 100%;
 }
 
@@ -385,16 +385,37 @@
   fill: #ea800088;
   stroke: #ea8000;
 }
 
 .keyframes-graph-path .color-path path {
   stroke: none;
 }
 
+.keyframes-graph .keyframes-graph-path .hint path {
+  fill: none;
+  stroke-linecap: round;
+  stroke-opacity: 0;
+}
+
+.keyframes-graph-path .hint path:hover {
+  stroke-opacity: 1;
+}
+
+.keyframes-graph-path .hint rect {
+  fill-opacity: 0.1;
+  stroke: #00b0bd;
+  stroke-opacity: 0;
+  vector-effect: non-scaling-stroke;
+}
+
+.keyframes-graph-path .hint rect:hover {
+  stroke-opacity: 1;
+}
+
 /* No Animation Panel */
 .animation-error-message {
   overflow: auto;
 }
 
 .animation-error-message > p {
   white-space: pre;
 }