Bug 1406285 - Part 4: Implement getting tracks(keyframes) from server. r=gl
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Thu, 18 Jan 2018 12:17:11 +0900
changeset 454260 bbe12d4a23d941924202b87c5409f9436f77020f
parent 454259 ad4a9cbf32d40ac0c62d86e0d0d94b9f05ce7bfd
child 454261 fcac2692bb2e52de0018efbffa872b598a7d4745
push id1648
push usermtabara@mozilla.com
push dateThu, 01 Mar 2018 12:45:47 +0000
treeherdermozilla-release@cbb9688c2eeb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgl
bugs1406285
milestone59.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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'
 )