diff --git a/devtools/client/inspector/animation/animation.js b/devtools/client/inspector/animation/animation.js index a3f691fd1075..e2682f09eaa9 100644 --- a/devtools/client/inspector/animation/animation.js +++ b/devtools/client/inspector/animation/animation.js @@ -90,6 +90,59 @@ class AnimationInspector { 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. @@ -105,6 +158,15 @@ class AnimationInspector { : []; 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; } @@ -112,20 +174,6 @@ class AnimationInspector { 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)); } diff --git a/devtools/client/inspector/animation/components/graph/ComputedTimingPath.js b/devtools/client/inspector/animation/components/graph/ComputedTimingPath.js new file mode 100644 index 000000000000..17b192d1f34d --- /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; diff --git a/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js b/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js index f7916b78f5a2..2ea079b99d05 100644 --- a/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js +++ b/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js @@ -4,9 +4,12 @@ "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() { @@ -16,21 +19,157 @@ class SummaryGraphPath extends PureComponent { }; } - 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, + } + ) + ) ); } } diff --git a/devtools/client/inspector/animation/components/graph/moz.build b/devtools/client/inspector/animation/components/graph/moz.build index 9ba5edd34687..d10e084de2fa 100644 --- a/devtools/client/inspector/animation/components/graph/moz.build +++ b/devtools/client/inspector/animation/components/graph/moz.build @@ -3,6 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. DevToolsModules( + 'ComputedTimingPath.js', 'SummaryGraph.js', 'SummaryGraphPath.js' )