Bug 1416106 - Part 8: Implement discrete graph. r?gl draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Wed, 14 Feb 2018 23:18:13 +0900
changeset 755233 5cf82cd2d674ffe2a455e82e4d6b5a6abf9ea9fa
parent 755232 b6ed11306ed800347000d521079cc16eab89d6d8
child 755234 eb69e810f6a6759ac69d2fa563fabc6f6ef63863
push id99127
push userbmo:dakatsuka@mozilla.com
push dateThu, 15 Feb 2018 00:47:03 +0000
reviewersgl
bugs1416106
milestone60.0a1
Bug 1416106 - Part 8: Implement discrete graph. r?gl MozReview-Commit-ID: HxPLpQTQTiS
devtools/client/inspector/animation/animation.js
devtools/client/inspector/animation/components/AnimatedPropertyItem.js
devtools/client/inspector/animation/components/AnimatedPropertyList.js
devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js
devtools/client/inspector/animation/components/AnimationDetailContainer.js
devtools/client/inspector/animation/components/App.js
devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js
devtools/client/inspector/animation/components/keyframes-graph/DiscretePath.js
devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js
devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js
devtools/client/inspector/animation/components/keyframes-graph/moz.build
--- a/devtools/client/inspector/animation/animation.js
+++ b/devtools/client/inspector/animation/animation.js
@@ -22,16 +22,17 @@ const {
 const { isAllAnimationEqual } = require("./utils/utils");
 
 class AnimationInspector {
   constructor(inspector, win) {
     this.inspector = inspector;
     this.win = win;
 
     this.getAnimatedPropertyMap = this.getAnimatedPropertyMap.bind(this);
+    this.getComputedStyle = this.getComputedStyle.bind(this);
     this.getNodeFromActor = this.getNodeFromActor.bind(this);
     this.selectAnimation = this.selectAnimation.bind(this);
     this.setDetailVisibility = this.setDetailVisibility.bind(this);
     this.simulateAnimation = this.simulateAnimation.bind(this);
     this.toggleElementPicker = this.toggleElementPicker.bind(this);
     this.update = this.update.bind(this);
     this.onElementPickerStarted = this.onElementPickerStarted.bind(this);
     this.onElementPickerStopped = this.onElementPickerStopped.bind(this);
@@ -52,16 +53,17 @@ class AnimationInspector {
 
     const {
       onHideBoxModelHighlighter,
     } = this.inspector.getPanel("boxmodel").getComponentProps();
 
     const {
       emit: emitEventForTest,
       getAnimatedPropertyMap,
+      getComputedStyle,
       getNodeFromActor,
       selectAnimation,
       setDetailVisibility,
       simulateAnimation,
       toggleElementPicker,
     } = this;
 
     const target = this.inspector.target;
@@ -72,16 +74,17 @@ class AnimationInspector {
         id: "newanimationinspector",
         key: "newanimationinspector",
         store: this.inspector.store
       },
       App(
         {
           emitEventForTest,
           getAnimatedPropertyMap,
+          getComputedStyle,
           getNodeFromActor,
           onHideBoxModelHighlighter,
           onShowBoxModelHighlighterForNode,
           selectAnimation,
           setDetailVisibility,
           setSelectedNode,
           simulateAnimation,
           toggleElementPicker,
@@ -152,16 +155,37 @@ class AnimationInspector {
       });
 
       animatedPropertyMap.set(name, keyframes);
     }
 
     return animatedPropertyMap;
   }
 
+  /**
+   * Return the computed style of the specified property after setting the given styles
+   * to the simulated element.
+   *
+   * @param {String} property
+   *        CSS property name (e.g. text-align).
+   * @param {Object} styles
+   *        Map of CSS property name and value.
+   * @return {String}
+   *         Computed style of property.
+   */
+  getComputedStyle(property, styles) {
+    this.simulatedElement.style.cssText = "";
+
+    for (let propertyName in styles) {
+      this.simulatedElement.style.setProperty(propertyName, styles[propertyName]);
+    }
+
+    return this.win.getComputedStyle(this.simulatedElement).getPropertyValue(property);
+  }
+
   getNodeFromActor(actorID) {
     return this.inspector.walker.getNodeFromActor(actorID, ["node"]);
   }
 
   isPanelVisible() {
     return this.inspector && this.inspector.toolbox && this.inspector.sidebar &&
            this.inspector.toolbox.currentToolId === "inspector" &&
            this.inspector.sidebar.getCurrentTabID() === "newanimationinspector";
--- a/devtools/client/inspector/animation/components/AnimatedPropertyItem.js
+++ b/devtools/client/inspector/animation/components/AnimatedPropertyItem.js
@@ -9,26 +9,28 @@ const dom = require("devtools/client/sha
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const AnimatedPropertyName = createFactory(require("./AnimatedPropertyName"));
 const KeyframesGraph = createFactory(require("./keyframes-graph/KeyframesGraph"));
 
 class AnimatedPropertyItem extends PureComponent {
   static get propTypes() {
     return {
+      getComputedStyle: PropTypes.func.isRequired,
       property: PropTypes.string.isRequired,
       simulateAnimation: PropTypes.func.isRequired,
       state: PropTypes.object.isRequired,
       type: PropTypes.string.isRequired,
       values: PropTypes.array.isRequired,
     };
   }
 
   render() {
     const {
+      getComputedStyle,
       property,
       simulateAnimation,
       state,
       type,
       values,
     } = this.props;
 
     return dom.li(
@@ -38,16 +40,18 @@ class AnimatedPropertyItem extends PureC
       AnimatedPropertyName(
         {
           property,
           state,
         }
       ),
       KeyframesGraph(
         {
+          getComputedStyle,
+          property,
           simulateAnimation,
           type,
           values,
         }
       )
     );
   }
 }
--- a/devtools/client/inspector/animation/components/AnimatedPropertyList.js
+++ b/devtools/client/inspector/animation/components/AnimatedPropertyList.js
@@ -11,16 +11,17 @@ const PropTypes = require("devtools/clie
 const AnimatedPropertyItem = createFactory(require("./AnimatedPropertyItem"));
 
 class AnimatedPropertyList extends PureComponent {
   static get propTypes() {
     return {
       animation: PropTypes.object.isRequired,
       emitEventForTest: PropTypes.func.isRequired,
       getAnimatedPropertyMap: PropTypes.func.isRequired,
+      getComputedStyle: PropTypes.func.isRequired,
       simulateAnimation: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
@@ -58,16 +59,17 @@ class AnimatedPropertyList extends PureC
 
     this.setState({ animatedPropertyMap, animationTypes });
 
     emitEventForTest("animation-keyframes-rendered");
   }
 
   render() {
     const {
+      getComputedStyle,
       simulateAnimation,
     } = this.props;
     const {
       animatedPropertyMap,
       animationTypes,
     } = this.state;
 
     if (!animatedPropertyMap) {
@@ -78,16 +80,17 @@ class AnimatedPropertyList extends PureC
       {
         className: "animated-property-list"
       },
       [...animatedPropertyMap.entries()].map(([property, values]) => {
         const state = this.getPropertyState(property);
         const type = animationTypes[property];
         return AnimatedPropertyItem(
           {
+            getComputedStyle,
             property,
             simulateAnimation,
             state,
             type,
             values,
           }
         );
       })
--- a/devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js
+++ b/devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js
@@ -12,38 +12,41 @@ const AnimatedPropertyList = createFacto
 const AnimatedPropertyListHeader = createFactory(require("./AnimatedPropertyListHeader"));
 
 class AnimatedPropertyListContainer extends PureComponent {
   static get propTypes() {
     return {
       animation: PropTypes.object.isRequired,
       emitEventForTest: PropTypes.func.isRequired,
       getAnimatedPropertyMap: PropTypes.func.isRequired,
+      getComputedStyle: PropTypes.func.isRequired,
       simulateAnimation: PropTypes.func.isRequired,
     };
   }
 
   render() {
     const {
       animation,
       emitEventForTest,
       getAnimatedPropertyMap,
+      getComputedStyle,
       simulateAnimation,
     } = this.props;
 
     return dom.div(
       {
         className: `animated-property-list-container ${ animation.state.type }`
       },
       AnimatedPropertyListHeader(),
       AnimatedPropertyList(
         {
           animation,
           emitEventForTest,
           getAnimatedPropertyMap,
+          getComputedStyle,
           simulateAnimation,
         }
       )
     );
   }
 }
 
 module.exports = AnimatedPropertyListContainer;
--- a/devtools/client/inspector/animation/components/AnimationDetailContainer.js
+++ b/devtools/client/inspector/animation/components/AnimationDetailContainer.js
@@ -14,26 +14,28 @@ const AnimatedPropertyListContainer =
   createFactory(require("./AnimatedPropertyListContainer"));
 
 class AnimationDetailContainer extends PureComponent {
   static get propTypes() {
     return {
       animation: PropTypes.object.isRequired,
       emitEventForTest: PropTypes.func.isRequired,
       getAnimatedPropertyMap: PropTypes.func.isRequired,
+      getComputedStyle: PropTypes.func.isRequired,
       setDetailVisibility: PropTypes.func.isRequired,
       simulateAnimation: PropTypes.func.isRequired,
     };
   }
 
   render() {
     const {
       animation,
       emitEventForTest,
       getAnimatedPropertyMap,
+      getComputedStyle,
       setDetailVisibility,
       simulateAnimation,
     } = this.props;
 
     return dom.div(
       {
         className: "animation-detail-container"
       },
@@ -47,16 +49,17 @@ class AnimationDetailContainer extends P
       :
         null,
       animation ?
         AnimatedPropertyListContainer(
           {
             animation,
             emitEventForTest,
             getAnimatedPropertyMap,
+            getComputedStyle,
             simulateAnimation,
           }
         )
       :
         null
     );
   }
 }
--- a/devtools/client/inspector/animation/components/App.js
+++ b/devtools/client/inspector/animation/components/App.js
@@ -16,16 +16,17 @@ const SplitBox = createFactory(require("
 
 class App extends PureComponent {
   static get propTypes() {
     return {
       animations: PropTypes.arrayOf(PropTypes.object).isRequired,
       detailVisibility: PropTypes.bool.isRequired,
       emitEventForTest: PropTypes.func.isRequired,
       getAnimatedPropertyMap: PropTypes.func.isRequired,
+      getComputedStyle: PropTypes.func.isRequired,
       getNodeFromActor: PropTypes.func.isRequired,
       onHideBoxModelHighlighter: PropTypes.func.isRequired,
       onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
       selectAnimation: PropTypes.func.isRequired,
       setDetailVisibility: PropTypes.func.isRequired,
       setSelectedNode: PropTypes.func.isRequired,
       simulateAnimation: PropTypes.func.isRequired,
       toggleElementPicker: PropTypes.func.isRequired,
@@ -37,16 +38,17 @@ class App extends PureComponent {
   }
 
   render() {
     const {
       animations,
       detailVisibility,
       emitEventForTest,
       getAnimatedPropertyMap,
+      getComputedStyle,
       getNodeFromActor,
       onHideBoxModelHighlighter,
       onShowBoxModelHighlighterForNode,
       selectAnimation,
       setDetailVisibility,
       setSelectedNode,
       simulateAnimation,
       toggleElementPicker,
@@ -59,16 +61,17 @@ class App extends PureComponent {
       },
       animations.length ?
       SplitBox({
         className: "animation-container-splitter",
         endPanel: AnimationDetailContainer(
           {
             emitEventForTest,
             getAnimatedPropertyMap,
+            getComputedStyle,
             setDetailVisibility,
             simulateAnimation,
           }
         ),
         endPanelControl: true,
         initialHeight: "50%",
         splitterSize: 1,
         startPanel: AnimationListContainer(
--- a/devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js
+++ b/devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js
@@ -70,17 +70,17 @@ class ComputedStylePath extends PureComp
     const propertyName = this.getPropertyName();
     const offsetDistance = endValue.offset - startValue.offset;
     const duration = offsetDistance * totalDuration;
 
     const keyframes = [startValue, endValue].map((keyframe, index) => {
       return {
         offset: index,
         easing: keyframe.easing,
-        [propertyName]: this.getPropertyValue(keyframe),
+        [getJsPropertyName(propertyName)]: this.getPropertyValue(keyframe),
       };
     });
     const effect = {
       duration,
       fill: "forwards",
     };
     const simulatedAnimation = simulateAnimation(keyframes, effect, true);
     const simulatedElement = simulatedAnimation.effect.target;
@@ -146,9 +146,27 @@ class ComputedStylePath extends PureComp
     let d = `M${ segments[0].x },0 `;
     d += toPathString(segments);
     d += `L${ segments[segments.length - 1].x },0 Z`;
 
     return dom.path({ d, style });
   }
 }
 
+/**
+ * Convert given CSS property name to JavaScript CSS name.
+ *
+ * @param {String} cssPropertyName
+ *        CSS property name (e.g. background-color).
+ * @return {String}
+ *         JavaScript CSS property name (e.g. backgroundColor).
+ */
+function getJsPropertyName(cssPropertyName) {
+  if (cssPropertyName == "float") {
+    return "cssFloat";
+  }
+  // https://drafts.csswg.org/cssom/#css-property-to-idl-attribute
+  return cssPropertyName.replace(/-([a-z])/gi, (str, group) => {
+    return group.toUpperCase();
+  });
+}
+
 module.exports = ComputedStylePath;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/keyframes-graph/DiscretePath.js
@@ -0,0 +1,66 @@
+/* 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 dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+const ComputedStylePath = require("./ComputedStylePath");
+
+class DiscretePath extends ComputedStylePath {
+  static get propTypes() {
+    return {
+      property: PropTypes.string.isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+
+    this.state = this.propToState(props);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.setState(this.propToState(nextProps));
+  }
+
+  getPropertyName() {
+    return this.props.property;
+  }
+
+  getPropertyValue(keyframe) {
+    return keyframe.value;
+  }
+
+  propToState({ property, getComputedStyle, values }) {
+    const discreteValues = [];
+
+    for (const keyframe of values) {
+      const style = getComputedStyle(property, { [property]: keyframe.value });
+
+      if (!discreteValues.includes(style)) {
+        discreteValues.push(style);
+      }
+    }
+
+    return { discreteValues };
+  }
+
+  toSegmentValue(computedStyle) {
+    const { discreteValues } = this.state;
+    return discreteValues.indexOf(computedStyle) / (discreteValues.length - 1);
+  }
+
+  render() {
+    return dom.g(
+      {
+        className: "discrete-path",
+      },
+      super.renderGraph()
+    );
+  }
+}
+
+module.exports = DiscretePath;
--- a/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js
+++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js
@@ -8,35 +8,41 @@ const { createFactory, PureComponent } =
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const KeyframesGraphPath = createFactory(require("./KeyframesGraphPath"));
 
 class KeyframesGraph extends PureComponent {
   static get propTypes() {
     return {
+      getComputedStyle: PropTypes.func.isRequired,
+      property: PropTypes.string.isRequired,
       simulateAnimation: PropTypes.func.isRequired,
       type: PropTypes.string.isRequired,
       values: PropTypes.array.isRequired,
     };
   }
 
   render() {
     const {
+      getComputedStyle,
+      property,
       simulateAnimation,
       type,
       values,
     } = this.props;
 
     return dom.div(
       {
         className: "keyframes-graph",
       },
       KeyframesGraphPath(
         {
+          getComputedStyle,
+          property,
           simulateAnimation,
           type,
           values,
         }
       )
     );
   }
 }
--- a/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js
+++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js
@@ -5,26 +5,29 @@
 "use strict";
 
 const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 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_GRAPH_HEIGHT,
   DEFAULT_KEYFRAMES_GRAPH_DURATION,
 } = require("../../utils/graph-helper");
 
 class KeyframesGraphPath extends PureComponent {
   static get propTypes() {
     return {
+      getComputedStyle: PropTypes.func.isRequired,
+      property: PropTypes.string.isRequired,
       simulateAnimation: PropTypes.func.isRequired,
       type: PropTypes.string.isRequired,
       values: PropTypes.array.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
@@ -37,28 +40,32 @@ class KeyframesGraphPath extends PureCom
   componentDidMount() {
     this.updateState();
   }
 
   getPathComponent(type) {
     switch (type) {
       case "color" :
         return ColorPath;
+      case "discrete" :
+        return DiscretePath;
       default :
         return DistancePath;
     }
   }
 
   updateState() {
     const thisEl = ReactDOM.findDOMNode(this);
     this.setState({ componentWidth: thisEl.parentNode.clientWidth });
   }
 
   render() {
     const {
+      getComputedStyle,
+      property,
       simulateAnimation,
       type,
       values,
     } = this.props;
     const { componentWidth } = this.state;
 
     if (!componentWidth) {
       return dom.svg();
@@ -71,17 +78,19 @@ class KeyframesGraphPath extends PureCom
         className: "keyframes-graph-path",
         preserveAspectRatio: "none",
         viewBox: `0 -${ DEFAULT_GRAPH_HEIGHT } `
                  + `${ DEFAULT_KEYFRAMES_GRAPH_DURATION } ${ DEFAULT_GRAPH_HEIGHT }`,
       },
       pathComponent(
         {
           componentWidth,
+          getComputedStyle,
           graphHeight: DEFAULT_GRAPH_HEIGHT,
+          property,
           simulateAnimation,
           totalDuration: DEFAULT_KEYFRAMES_GRAPH_DURATION,
           values,
         }
       )
     );
   }
 }
--- a/devtools/client/inspector/animation/components/keyframes-graph/moz.build
+++ b/devtools/client/inspector/animation/components/keyframes-graph/moz.build
@@ -1,11 +1,12 @@
 # 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(
     'ColorPath.js',
     'ComputedStylePath.js',
+    'DiscretePath.js',
     'DistancePath.js',
     'KeyframesGraph.js',
     'KeyframesGraphPath.js',
 )