Bug 1406285 - Part 4: Implement getting tracks(keyframes) from server. r?gl
MozReview-Commit-ID: KmnnFLZIs9a
--- 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'
)