Bug 1406285 - Part 4: Implement getting tracks(keyframes) from server. r?gl draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Thu, 18 Jan 2018 12:17:11 +0900
changeset 721933 82078177a1cb16d6cf0609fbe68b59067fdc675d
parent 721932 61f56507a676eb5543c3a87172cc1f0378c3dc65
child 721934 f859326021ebef09e67cc1db7da5e0aaa8a3ac0e
push id96003
push userbmo:dakatsuka@mozilla.com
push dateThu, 18 Jan 2018 05:23:36 +0000
reviewersgl
bugs1406285
milestone59.0a1
Bug 1406285 - Part 4: Implement getting tracks(keyframes) from server. r?gl MozReview-Commit-ID: KmnnFLZIs9a
devtools/client/inspector/animation/animation.js
devtools/client/inspector/animation/components/graph/ComputedTimingPath.js
devtools/client/inspector/animation/components/graph/SummaryGraphPath.js
devtools/client/inspector/animation/components/graph/moz.build
--- a/devtools/client/inspector/animation/animation.js
+++ b/devtools/client/inspector/animation/animation.js
@@ -85,52 +85,100 @@ class AnimationInspector {
     this.inspector.sidebar.off("newanimationinspector-selected", this.onSidebarSelect);
     this.inspector.toolbox.off("inspector-sidebar-resized", this.onSidebarResized);
     this.inspector.toolbox.off("picker-started", this.onElementPickerStarted);
     this.inspector.toolbox.off("picker-stopped", this.onElementPickerStopped);
 
     this.inspector = null;
   }
 
+  /**
+   * Return a map of animated property from given animation actor.
+   *
+   * @param {Object} animation
+   * @return {Map} A map of animated property
+   *         key: {String} Animated property name
+   *         value: {Array} Array of keyframe object
+   *         Also, the keyframe object is consisted as following.
+   *         {
+   *           value: {String} style,
+   *           offset: {Number} offset of keyframe,
+   *           easing: {String} easing from this keyframe to next keyframe,
+   *           distance: {Number} use as y coordinate in graph,
+   *         }
+   */
+  async getAnimatedPropertyMap(animation) {
+    let properties = [];
+
+    try {
+      properties = await animation.getProperties();
+    } catch (e) {
+      // Expected if we've already been destroyed in the meantime.
+      console.error(e);
+    }
+
+    const animatedPropertyMap = new Map();
+
+    for (const { name, values } of properties) {
+      const keyframes = values.map(({ value, offset, easing, distance = 0 }) => {
+        offset = parseFloat(offset.toFixed(3));
+        return { value, offset, easing, distance };
+      });
+
+      animatedPropertyMap.set(name, keyframes);
+    }
+
+    return animatedPropertyMap;
+  }
+
+  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";
+  }
+
+  toggleElementPicker() {
+    this.inspector.toolbox.highlighterUtils.togglePicker();
+  }
+
   async update() {
     if (!this.inspector || !this.isPanelVisible()) {
       // AnimationInspector was destroyed already or the panel is hidden.
       return;
     }
 
     const done = this.inspector.updating("newanimationinspector");
 
     const selection = this.inspector.selection;
     const animations =
       selection.isConnected() && selection.isElementNode()
       ? await this.animationsFront.getAnimationPlayersForNode(selection.nodeFront)
       : [];
 
     if (!this.animations || !isAllAnimationEqual(animations, this.animations)) {
+      await Promise.all(animations.map(animation => {
+        return new Promise(resolve => {
+          this.getAnimatedPropertyMap(animation).then(animatedPropertyMap => {
+            animation.animatedPropertyMap = animatedPropertyMap;
+            resolve();
+          });
+        });
+      }));
+
       this.inspector.store.dispatch(updateAnimations(animations));
       this.animations = animations;
     }
 
     done();
   }
 
-  isPanelVisible() {
-    return this.inspector && this.inspector.toolbox && this.inspector.sidebar &&
-           this.inspector.toolbox.currentToolId === "inspector" &&
-           this.inspector.sidebar.getCurrentTabID() === "newanimationinspector";
-  }
-
-  getNodeFromActor(actorID) {
-    return this.inspector.walker.getNodeFromActor(actorID, ["node"]);
-  }
-
-  toggleElementPicker() {
-    this.inspector.toolbox.highlighterUtils.togglePicker();
-  }
-
   onElementPickerStarted() {
     this.inspector.store.dispatch(updateElementPickerEnabled(true));
   }
 
   onElementPickerStopped() {
     this.inspector.store.dispatch(updateElementPickerEnabled(false));
   }
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/graph/ComputedTimingPath.js
@@ -0,0 +1,26 @@
+/* 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 { 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");
+
+class ComputedTimingPath extends PureComponent {
+  static get propTypes() {
+    return {
+      animation: PropTypes.object.isRequired,
+      durationPerPixel: PropTypes.number.isRequired,
+      keyframes: PropTypes.object.isRequired,
+      totalDisplayedDuration: PropTypes.number.isRequired,
+    };
+  }
+
+  render() {
+    return dom.g({});
+  }
+}
+
+module.exports = ComputedTimingPath;
--- a/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js
+++ b/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js
@@ -1,38 +1,177 @@
 /* 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 { PureComponent } = require("devtools/client/shared/vendor/react");
+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"));
 
 class SummaryGraphPath extends PureComponent {
   static get propTypes() {
     return {
       animation: PropTypes.object.isRequired,
       timeScale: PropTypes.object.isRequired,
     };
   }
 
-  render() {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      durationPerPixel: 0,
+    };
+  }
+
+  componentDidMount() {
+    this.updateDurationPerPixel();
+  }
+
+  /**
+   * Return animatable keyframes list which has only offset and easing.
+   * Also, this method remove duplicate keyframes.
+   * For example, if the given animatedPropertyMap is,
+   * [
+   *   {
+   *     key: "color",
+   *     values: [
+   *       {
+   *         offset: 0,
+   *         easing: "ease",
+   *         value: "rgb(255, 0, 0)",
+   *       },
+   *       {
+   *         offset: 1,
+   *         value: "rgb(0, 255, 0)",
+   *       },
+   *     ],
+   *   },
+   *   {
+   *     key: "opacity",
+   *     values: [
+   *       {
+   *         offset: 0,
+   *         easing: "ease",
+   *         value: 0,
+   *       },
+   *       {
+   *         offset: 1,
+   *         value: 1,
+   *       },
+   *     ],
+   *   },
+   * ]
+   *
+   * then this method returns,
+   * [
+   *   [
+   *     {
+   *       offset: 0,
+   *       easing: "ease",
+   *     },
+   *     {
+   *       offset: 1,
+   *     },
+   *   ],
+   * ]
+   *
+   * @param {Map} animated property map
+   *        which can get form getAnimatedPropertyMap in animation.js
+   * @return {Array} list of keyframes which has only easing and offset.
+   */
+  getOffsetAndEasingOnlyKeyframes(animatedPropertyMap) {
+    return [...animatedPropertyMap.values()].filter((keyframes1, i, self) => {
+      return i !== self.findIndex((keyframes2, j) => {
+        return this.isOffsetAndEasingKeyframesEqual(keyframes1, keyframes2) ? j : -1;
+      });
+    }).map(keyframes => {
+      return keyframes.map(keyframe => {
+        return { easing: keyframe.easing, offset: keyframe.offset };
+      });
+    });
+  }
+
+  getTotalDuration(animation, timeScale) {
+    return animation.state.playbackRate * timeScale.getDuration();
+  }
+
+  /**
+   * Return true if given keyframes have same length, offset and easing.
+   *
+   * @param {Array} keyframes1
+   * @param {Array} keyframes2
+   * @return {Boolean} true: equals
+   */
+  isOffsetAndEasingKeyframesEqual(keyframes1, keyframes2) {
+    if (keyframes1.length !== keyframes2.length) {
+      return false;
+    }
+
+    for (let i = 0; i < keyframes1.length; i++) {
+      const keyframe1 = keyframes1[i];
+      const keyframe2 = keyframes2[i];
+
+      if (keyframe1.offset !== keyframe2.offset ||
+          keyframe1.easing !== keyframe2.easing) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  updateDurationPerPixel() {
     const {
       animation,
       timeScale,
     } = this.props;
 
-    const totalDisplayedDuration = animation.state.playbackRate * timeScale.getDuration();
+    const thisEl = ReactDOM.findDOMNode(this);
+    const totalDuration = this.getTotalDuration(animation, timeScale);
+    const durationPerPixel = totalDuration / thisEl.parentNode.clientWidth;
+
+    this.setState({ durationPerPixel });
+  }
+
+  render() {
+    const { durationPerPixel } = this.state;
+
+    if (!durationPerPixel) {
+      return dom.svg();
+    }
+
+    const {
+      animation,
+      timeScale,
+    } = this.props;
+
+    const totalDuration = this.getTotalDuration(animation, timeScale);
     const startTime = timeScale.minStartTime;
+    const keyframesList =
+      this.getOffsetAndEasingOnlyKeyframes(animation.animatedPropertyMap);
 
     return dom.svg(
       {
         className: "animation-summary-graph-path",
         preserveAspectRatio: "none",
-        viewBox: `${ startTime } -1 ${ totalDisplayedDuration } 1`
-      }
+        viewBox: `${ startTime } -1 ${ totalDuration } 1`
+      },
+      keyframesList.map(keyframes =>
+        ComputedTimingPath(
+          {
+            animation,
+            durationPerPixel,
+            keyframes,
+            totalDuration,
+          }
+        )
+      )
     );
   }
 }
 
 module.exports = SummaryGraphPath;
--- a/devtools/client/inspector/animation/components/graph/moz.build
+++ b/devtools/client/inspector/animation/components/graph/moz.build
@@ -1,8 +1,9 @@
 # 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',
     'SummaryGraph.js',
     'SummaryGraphPath.js'
 )