diff options
Diffstat (limited to 'devtools/client/inspector/animation/components/graph')
12 files changed, 1421 insertions, 0 deletions
diff --git a/devtools/client/inspector/animation/components/graph/AnimationName.js b/devtools/client/inspector/animation/components/graph/AnimationName.js new file mode 100644 index 0000000000..1ef2ebd829 --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/AnimationName.js @@ -0,0 +1,38 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class AnimationName extends PureComponent { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + }; + } + + render() { + const { animation } = this.props; + + return dom.svg( + { + className: "animation-name", + }, + dom.text( + { + y: "50%", + x: "100%", + }, + animation.state.name + ) + ); + } +} + +module.exports = AnimationName; 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 0000000000..fed25e161e --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/ComputedTimingPath.js @@ -0,0 +1,104 @@ +/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const { + createSummaryGraphPathStringFunction, + SummaryGraphHelper, +} = require("resource://devtools/client/inspector/animation/utils/graph-helper.js"); +const TimingPath = require("resource://devtools/client/inspector/animation/components/graph/TimingPath.js"); + +class ComputedTimingPath extends TimingPath { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + durationPerPixel: PropTypes.number.isRequired, + keyframes: PropTypes.object.isRequired, + offset: PropTypes.number.isRequired, + opacity: PropTypes.number.isRequired, + simulateAnimation: PropTypes.func.isRequired, + totalDuration: PropTypes.number.isRequired, + }; + } + + render() { + const { + animation, + durationPerPixel, + keyframes, + offset, + opacity, + simulateAnimation, + totalDuration, + } = this.props; + + const { state } = animation; + const effectTiming = Object.assign({}, state, { + iterations: state.iterationCount ? state.iterationCount : Infinity, + }); + + // Create new keyframes for opacity as computed style. + // The reason why we use computed value instead of computed timing progress is to + // include the easing in keyframes as well. Although the computed timing progress + // is not affected by the easing in keyframes at all, computed value reflects that. + const frames = keyframes.map(keyframe => { + return { + opacity: keyframe.offset, + offset: keyframe.offset, + easing: keyframe.easing, + }; + }); + + const simulatedAnimation = simulateAnimation(frames, effectTiming, true); + + if (!simulatedAnimation) { + return null; + } + + const simulatedElement = simulatedAnimation.effect.target; + const win = simulatedElement.ownerGlobal; + const endTime = simulatedAnimation.effect.getComputedTiming().endTime; + + // Set the underlying opacity to zero so that if we sample the animation's output + // during the delay phase and it is not filling backwards, we get zero. + simulatedElement.style.opacity = 0; + + const getValueFunc = time => { + if (time < 0) { + return 0; + } + + simulatedAnimation.currentTime = time < endTime ? time : endTime; + return win.getComputedStyle(simulatedElement).opacity; + }; + + const toPathStringFunc = createSummaryGraphPathStringFunction( + endTime, + state.playbackRate + ); + const helper = new SummaryGraphHelper( + state, + keyframes, + totalDuration, + durationPerPixel, + getValueFunc, + toPathStringFunc + ); + + return dom.g( + { + className: "animation-computed-timing-path", + style: { opacity }, + transform: `translate(${offset})`, + }, + super.renderGraph(state, helper) + ); + } +} + +module.exports = ComputedTimingPath; diff --git a/devtools/client/inspector/animation/components/graph/DelaySign.js b/devtools/client/inspector/animation/components/graph/DelaySign.js new file mode 100644 index 0000000000..4e817a9d61 --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/DelaySign.js @@ -0,0 +1,42 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class DelaySign extends PureComponent { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + render() { + const { animation, timeScale } = this.props; + const { delay, isDelayFilled, startTime } = animation.state.absoluteValues; + + const toPercentage = v => (v / timeScale.getDuration()) * 100; + const offset = toPercentage(startTime - timeScale.minStartTime); + const width = toPercentage(Math.abs(delay)); + + return dom.div({ + className: + "animation-delay-sign" + + (delay < 0 ? " negative" : "") + + (isDelayFilled ? " fill" : ""), + style: { + width: `${width}%`, + marginInlineStart: `${offset}%`, + }, + }); + } +} + +module.exports = DelaySign; diff --git a/devtools/client/inspector/animation/components/graph/EffectTimingPath.js b/devtools/client/inspector/animation/components/graph/EffectTimingPath.js new file mode 100644 index 0000000000..4a984bc61c --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/EffectTimingPath.js @@ -0,0 +1,84 @@ +/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const { + createSummaryGraphPathStringFunction, + SummaryGraphHelper, +} = require("resource://devtools/client/inspector/animation/utils/graph-helper.js"); +const TimingPath = require("resource://devtools/client/inspector/animation/components/graph/TimingPath.js"); + +class EffectTimingPath extends TimingPath { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + durationPerPixel: PropTypes.number.isRequired, + offset: PropTypes.number.isRequired, + simulateAnimation: PropTypes.func.isRequired, + totalDuration: PropTypes.number.isRequired, + }; + } + + render() { + const { + animation, + durationPerPixel, + offset, + simulateAnimation, + totalDuration, + } = this.props; + + const { state } = animation; + const effectTiming = Object.assign({}, state, { + iterations: state.iterationCount ? state.iterationCount : Infinity, + }); + + const simulatedAnimation = simulateAnimation(null, effectTiming, false); + + if (!simulatedAnimation) { + return null; + } + + const endTime = simulatedAnimation.effect.getComputedTiming().endTime; + + const getValueFunc = time => { + if (time < 0) { + return 0; + } + + simulatedAnimation.currentTime = time < endTime ? time : endTime; + return Math.max( + simulatedAnimation.effect.getComputedTiming().progress, + 0 + ); + }; + + const toPathStringFunc = createSummaryGraphPathStringFunction( + endTime, + state.playbackRate + ); + const helper = new SummaryGraphHelper( + state, + null, + totalDuration, + durationPerPixel, + getValueFunc, + toPathStringFunc + ); + + return dom.g( + { + className: "animation-effect-timing-path", + transform: `translate(${offset})`, + }, + super.renderGraph(state, helper) + ); + } +} + +module.exports = EffectTimingPath; diff --git a/devtools/client/inspector/animation/components/graph/EndDelaySign.js b/devtools/client/inspector/animation/components/graph/EndDelaySign.js new file mode 100644 index 0000000000..f843123cbf --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/EndDelaySign.js @@ -0,0 +1,44 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class EndDelaySign extends PureComponent { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + render() { + const { animation, timeScale } = this.props; + const { endDelay, endTime, isEndDelayFilled } = + animation.state.absoluteValues; + + const toPercentage = v => (v / timeScale.getDuration()) * 100; + const absEndDelay = Math.abs(endDelay); + const offset = toPercentage(endTime - absEndDelay - timeScale.minStartTime); + const width = toPercentage(absEndDelay); + + return dom.div({ + className: + "animation-end-delay-sign" + + (endDelay < 0 ? " negative" : "") + + (isEndDelayFilled ? " fill" : ""), + style: { + width: `${width}%`, + marginInlineStart: `${offset}%`, + }, + }); + } +} + +module.exports = EndDelaySign; diff --git a/devtools/client/inspector/animation/components/graph/NegativeDelayPath.js b/devtools/client/inspector/animation/components/graph/NegativeDelayPath.js new file mode 100644 index 0000000000..0cd87dd320 --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/NegativeDelayPath.js @@ -0,0 +1,27 @@ +/* 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("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const NegativePath = require("resource://devtools/client/inspector/animation/components/graph/NegativePath.js"); + +class NegativeDelayPath extends NegativePath { + getClassName() { + return "animation-negative-delay-path"; + } + + renderGraph(state, helper) { + const startTime = state.delay; + const endTime = 0; + const segments = helper.createPathSegments(startTime, endTime); + + return dom.path({ + d: helper.toPathString(segments), + }); + } +} + +module.exports = NegativeDelayPath; diff --git a/devtools/client/inspector/animation/components/graph/NegativeEndDelayPath.js b/devtools/client/inspector/animation/components/graph/NegativeEndDelayPath.js new file mode 100644 index 0000000000..88f5f65037 --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/NegativeEndDelayPath.js @@ -0,0 +1,27 @@ +/* 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("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const NegativePath = require("resource://devtools/client/inspector/animation/components/graph/NegativePath.js"); + +class NegativeEndDelayPath extends NegativePath { + getClassName() { + return "animation-negative-end-delay-path"; + } + + renderGraph(state, helper) { + const endTime = state.delay + state.iterationCount * state.duration; + const startTime = endTime + state.endDelay; + const segments = helper.createPathSegments(startTime, endTime); + + return dom.path({ + d: helper.toPathString(segments), + }); + } +} + +module.exports = NegativeEndDelayPath; diff --git a/devtools/client/inspector/animation/components/graph/NegativePath.js b/devtools/client/inspector/animation/components/graph/NegativePath.js new file mode 100644 index 0000000000..4f1d0af9e6 --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/NegativePath.js @@ -0,0 +1,101 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const { + createSummaryGraphPathStringFunction, + SummaryGraphHelper, +} = require("resource://devtools/client/inspector/animation/utils/graph-helper.js"); + +class NegativePath extends PureComponent { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + className: PropTypes.string.isRequired, + durationPerPixel: PropTypes.number.isRequired, + keyframes: PropTypes.object.isRequired, + offset: PropTypes.number.isRequired, + simulateAnimation: PropTypes.func.isRequired, + totalDuration: PropTypes.number.isRequired, + }; + } + + render() { + const { + animation, + durationPerPixel, + keyframes, + offset, + simulateAnimation, + totalDuration, + } = this.props; + + const { state } = animation; + const effectTiming = Object.assign({}, state, { + fill: "both", + iterations: state.iterationCount ? state.iterationCount : Infinity, + }); + + // Create new keyframes for opacity as computed style. + // The reason why we use computed value instead of computed timing progress is to + // include the easing in keyframes as well. Although the computed timing progress + // is not affected by the easing in keyframes at all, computed value reflects that. + const frames = keyframes.map(keyframe => { + return { + opacity: keyframe.offset, + offset: keyframe.offset, + easing: keyframe.easing, + }; + }); + + const simulatedAnimation = simulateAnimation(frames, effectTiming, true); + + if (!simulatedAnimation) { + return null; + } + + const simulatedElement = simulatedAnimation.effect.target; + const win = simulatedElement.ownerGlobal; + const endTime = simulatedAnimation.effect.getComputedTiming().endTime; + + // Set the underlying opacity to zero so that if we sample the animation's output + // during the delay phase and it is not filling backwards, we get zero. + simulatedElement.style.opacity = 0; + + const getValueFunc = time => { + simulatedAnimation.currentTime = time; + return win.getComputedStyle(simulatedElement).opacity; + }; + + const toPathStringFunc = createSummaryGraphPathStringFunction( + endTime, + state.playbackRate + ); + const helper = new SummaryGraphHelper( + state, + keyframes, + totalDuration, + durationPerPixel, + getValueFunc, + toPathStringFunc + ); + + return dom.g( + { + className: this.getClassName(), + transform: `translate(${offset})`, + }, + this.renderGraph(state, helper) + ); + } +} + +module.exports = NegativePath; diff --git a/devtools/client/inspector/animation/components/graph/SummaryGraph.js b/devtools/client/inspector/animation/components/graph/SummaryGraph.js new file mode 100644 index 0000000000..bac54ea26f --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/SummaryGraph.js @@ -0,0 +1,205 @@ +/* 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 { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const AnimationName = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/AnimationName.js") +); +const DelaySign = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/DelaySign.js") +); +const EndDelaySign = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/EndDelaySign.js") +); +const SummaryGraphPath = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/SummaryGraphPath.js") +); + +const { + getFormattedTitle, + getFormatStr, + getStr, + numberWithDecimals, +} = require("resource://devtools/client/inspector/animation/utils/l10n.js"); + +class SummaryGraph extends PureComponent { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + getAnimatedPropertyMap: PropTypes.func.isRequired, + selectAnimation: PropTypes.func.isRequired, + simulateAnimation: PropTypes.func.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + constructor(props) { + super(props); + + this.onClick = this.onClick.bind(this); + } + + onClick(event) { + event.stopPropagation(); + this.props.selectAnimation(this.props.animation); + } + + getTitleText(state) { + const getTime = time => + getFormatStr("player.timeLabel", numberWithDecimals(time / 1000, 2)); + const getTimeOrInfinity = time => + time === Infinity ? getStr("player.infiniteDurationText") : getTime(time); + + let text = ""; + + // Adding the name. + text += getFormattedTitle(state); + text += "\n"; + + // Adding the delay. + if (state.delay) { + text += getStr("player.animationDelayLabel") + " "; + text += getTime(state.delay); + text += "\n"; + } + + // Adding the duration. + text += getStr("player.animationDurationLabel") + " "; + text += getTimeOrInfinity(state.duration); + text += "\n"; + + // Adding the endDelay. + if (state.endDelay) { + text += getStr("player.animationEndDelayLabel") + " "; + text += getTime(state.endDelay); + text += "\n"; + } + + // Adding the iteration count (the infinite symbol, or an integer). + if (state.iterationCount !== 1) { + text += getStr("player.animationIterationCountLabel") + " "; + text += + state.iterationCount || getStr("player.infiniteIterationCountText"); + text += "\n"; + } + + // Adding the iteration start. + if (state.iterationStart !== 0) { + text += getFormatStr( + "player.animationIterationStartLabel2", + state.iterationStart, + getTimeOrInfinity(state.iterationStart * state.duration) + ); + text += "\n"; + } + + // Adding the easing if it is not "linear". + if (state.easing && state.easing !== "linear") { + text += getStr("player.animationOverallEasingLabel") + " "; + text += state.easing; + text += "\n"; + } + + // Adding the fill mode. + if (state.fill && state.fill !== "none") { + text += getStr("player.animationFillLabel") + " "; + text += state.fill; + text += "\n"; + } + + // Adding the direction mode if it is not "normal". + if (state.direction && state.direction !== "normal") { + text += getStr("player.animationDirectionLabel") + " "; + text += state.direction; + text += "\n"; + } + + // Adding the playback rate if it's different than 1. + if (state.playbackRate !== 1) { + text += getStr("player.animationRateLabel") + " "; + text += state.playbackRate; + text += "\n"; + } + + // Adding the animation-timing-function + // if it is not "ease" which is default value for CSS Animations. + if ( + state.animationTimingFunction && + state.animationTimingFunction !== "ease" + ) { + text += getStr("player.animationTimingFunctionLabel") + " "; + text += state.animationTimingFunction; + text += "\n"; + } + + // Adding a note that the animation is running on the compositor thread if + // needed. + if (state.propertyState) { + if ( + state.propertyState.every(propState => propState.runningOnCompositor) + ) { + text += getStr("player.allPropertiesOnCompositorTooltip"); + } else if ( + state.propertyState.some(propState => propState.runningOnCompositor) + ) { + text += getStr("player.somePropertiesOnCompositorTooltip"); + } + } else if (state.isRunningOnCompositor) { + text += getStr("player.runningOnCompositorTooltip"); + } + + return text; + } + + render() { + const { animation, getAnimatedPropertyMap, simulateAnimation, timeScale } = + this.props; + + const { iterationCount } = animation.state; + const { delay, endDelay } = animation.state.absoluteValues; + + return dom.div( + { + className: + "animation-summary-graph" + + (animation.state.isRunningOnCompositor ? " compositor" : ""), + onClick: this.onClick, + title: this.getTitleText(animation.state), + }, + SummaryGraphPath({ + animation, + getAnimatedPropertyMap, + simulateAnimation, + timeScale, + }), + delay + ? DelaySign({ + animation, + timeScale, + }) + : null, + iterationCount && endDelay + ? EndDelaySign({ + animation, + timeScale, + }) + : null, + animation.state.name + ? AnimationName({ + animation, + }) + : null + ); + } +} + +module.exports = SummaryGraph; diff --git a/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js b/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js new file mode 100644 index 0000000000..0183c7413e --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js @@ -0,0 +1,282 @@ +/* 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 { + Component, + createFactory, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.js"); + +const ComputedTimingPath = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/ComputedTimingPath.js") +); +const EffectTimingPath = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/EffectTimingPath.js") +); +const NegativeDelayPath = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/NegativeDelayPath.js") +); +const NegativeEndDelayPath = createFactory( + require("resource://devtools/client/inspector/animation/components/graph/NegativeEndDelayPath.js") +); +const { + DEFAULT_GRAPH_HEIGHT, +} = require("resource://devtools/client/inspector/animation/utils/graph-helper.js"); + +// Minimum opacity for semitransparent fill color for keyframes's easing graph. +const MIN_KEYFRAMES_EASING_OPACITY = 0.5; + +class SummaryGraphPath extends Component { + static get propTypes() { + return { + animation: PropTypes.object.isRequired, + getAnimatedPropertyMap: PropTypes.object.isRequired, + simulateAnimation: PropTypes.func.isRequired, + timeScale: PropTypes.object.isRequired, + }; + } + + constructor(props) { + super(props); + + this.state = { + // Duration which can display in one pixel. + durationPerPixel: 0, + // To avoid rendering while the state is updating + // since we call an async function in updateState. + isStateUpdating: false, + // List of keyframe which consists by only offset and easing. + keyframesList: [], + }; + } + + componentDidMount() { + // No need to set isStateUpdating state since paint sequence is finish here. + this.updateState(this.props); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + this.setState({ isStateUpdating: true }); + this.updateState(nextProps); + } + + shouldComponentUpdate(nextProps, nextState) { + return !nextState.isStateUpdating; + } + + /** + * 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 }; + }); + }); + } + + /** + * 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; + } + + updateState(props) { + const { animation, getAnimatedPropertyMap, timeScale } = props; + + let animatedPropertyMap = null; + let thisEl = null; + + try { + animatedPropertyMap = getAnimatedPropertyMap(animation); + thisEl = ReactDOM.findDOMNode(this); + } catch (e) { + // Expected if we've already been destroyed or other node have been selected + // in the meantime. + console.error(e); + return; + } + + const keyframesList = + this.getOffsetAndEasingOnlyKeyframes(animatedPropertyMap); + const totalDuration = + timeScale.getDuration() * Math.abs(animation.state.playbackRate); + const durationPerPixel = totalDuration / thisEl.parentNode.clientWidth; + + this.setState({ + durationPerPixel, + isStateUpdating: false, + keyframesList, + }); + } + + render() { + const { durationPerPixel, keyframesList } = this.state; + const { animation, simulateAnimation, timeScale } = this.props; + + if (!durationPerPixel || !animation.state.type) { + // Undefined animation.state.type means that the animation had been removed already. + // Even if the animation was removed, we still need the empty svg since the + // component might be re-used. + return dom.svg(); + } + + const { playbackRate } = animation.state; + const { createdTime } = animation.state.absoluteValues; + const absPlaybackRate = Math.abs(playbackRate); + + // Absorb the playbackRate in viewBox of SVG and offset of child path elements + // in order to each graph path components can draw without considering to the + // playbackRate. + const offset = createdTime * absPlaybackRate; + const startTime = timeScale.minStartTime * absPlaybackRate; + const totalDuration = timeScale.getDuration() * absPlaybackRate; + const opacity = Math.max( + 1 / keyframesList.length, + MIN_KEYFRAMES_EASING_OPACITY + ); + + return dom.svg( + { + className: "animation-summary-graph-path", + preserveAspectRatio: "none", + viewBox: + `${startTime} -${DEFAULT_GRAPH_HEIGHT} ` + + `${totalDuration} ${DEFAULT_GRAPH_HEIGHT}`, + }, + keyframesList.map(keyframes => + ComputedTimingPath({ + animation, + durationPerPixel, + keyframes, + offset, + opacity, + simulateAnimation, + totalDuration, + }) + ), + animation.state.easing !== "linear" + ? EffectTimingPath({ + animation, + durationPerPixel, + offset, + simulateAnimation, + totalDuration, + }) + : null, + animation.state.delay < 0 + ? keyframesList.map(keyframes => { + return NegativeDelayPath({ + animation, + durationPerPixel, + keyframes, + offset, + simulateAnimation, + totalDuration, + }); + }) + : null, + animation.state.iterationCount && animation.state.endDelay < 0 + ? keyframesList.map(keyframes => { + return NegativeEndDelayPath({ + animation, + durationPerPixel, + keyframes, + offset, + simulateAnimation, + totalDuration, + }); + }) + : null + ); + } +} + +module.exports = SummaryGraphPath; diff --git a/devtools/client/inspector/animation/components/graph/TimingPath.js b/devtools/client/inspector/animation/components/graph/TimingPath.js new file mode 100644 index 0000000000..7949527988 --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/TimingPath.js @@ -0,0 +1,450 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +// Show max 10 iterations for infinite animations +// to give users a clue that the animation does repeat. +const MAX_INFINITE_ANIMATIONS_ITERATIONS = 10; + +class TimingPath extends PureComponent { + /** + * Render a graph of given parameters and return as <path> element list. + * + * @param {Object} state + * State of animation. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + * @return {Array} + * list of <path> element. + */ + renderGraph(state, helper) { + // Starting time of main iteration. + let mainIterationStartTime = 0; + let iterationStart = state.iterationStart; + let iterationCount = state.iterationCount ? state.iterationCount : Infinity; + + const pathList = []; + + // Append delay. + if (state.delay > 0) { + this.renderDelay(pathList, state, helper); + mainIterationStartTime = state.delay; + } else { + const negativeDelayCount = -state.delay / state.duration; + // Move to forward the starting point for negative delay. + iterationStart += negativeDelayCount; + // Consume iteration count by negative delay. + if (iterationCount !== Infinity) { + iterationCount -= negativeDelayCount; + } + } + + if (state.duration === Infinity) { + this.renderInfinityDuration( + pathList, + state, + mainIterationStartTime, + helper + ); + return pathList; + } + + // Append 1st section of iterations, + // This section is only useful in cases where iterationStart has decimals. + // e.g. + // if { iterationStart: 0.25, iterations: 3 }, firstSectionCount is 0.75. + const firstSectionCount = + iterationStart % 1 === 0 + ? 0 + : Math.min(1 - (iterationStart % 1), iterationCount); + + if (firstSectionCount) { + this.renderFirstIteration( + pathList, + state, + mainIterationStartTime, + firstSectionCount, + helper + ); + } + + if (iterationCount === Infinity) { + // If the animation repeats infinitely, + // we fill the remaining area with iteration paths. + this.renderInfinity( + pathList, + state, + mainIterationStartTime, + firstSectionCount, + helper + ); + } else { + // Otherwise, we show remaining iterations, endDelay and fill. + + // Append forwards fill-mode. + if (state.fill === "both" || state.fill === "forwards") { + this.renderForwardsFill( + pathList, + state, + mainIterationStartTime, + iterationCount, + helper + ); + } + + // Append middle section of iterations. + // e.g. + // if { iterationStart: 0.25, iterations: 3 }, middleSectionCount is 2. + const middleSectionCount = Math.floor(iterationCount - firstSectionCount); + this.renderMiddleIterations( + pathList, + state, + mainIterationStartTime, + firstSectionCount, + middleSectionCount, + helper + ); + + // Append last section of iterations, if there is remaining iteration. + // e.g. + // if { iterationStart: 0.25, iterations: 3 }, lastSectionCount is 0.25. + const lastSectionCount = + iterationCount - middleSectionCount - firstSectionCount; + if (lastSectionCount) { + this.renderLastIteration( + pathList, + state, + mainIterationStartTime, + firstSectionCount, + middleSectionCount, + lastSectionCount, + helper + ); + } + + // Append endDelay. + if (state.endDelay > 0) { + this.renderEndDelay( + pathList, + state, + mainIterationStartTime, + iterationCount, + helper + ); + } + } + return pathList; + } + + /** + * Render 'delay' part in animation and add a <path> element to given pathList. + * + * @param {Array} pathList + * Add rendered <path> element to this array. + * @param {Object} state + * State of animation. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + */ + renderDelay(pathList, state, helper) { + const startSegment = helper.getSegment(0); + const endSegment = { x: state.delay, y: startSegment.y }; + const segments = [startSegment, endSegment]; + pathList.push( + dom.path({ + className: "animation-delay-path", + d: helper.toPathString(segments), + }) + ); + } + + /** + * Render 1st section of iterations and add a <path> element to given pathList. + * This section is only useful in cases where iterationStart has decimals. + * + * @param {Array} pathList + * Add rendered <path> element to this array. + * @param {Object} state + * State of animation. + * @param {Number} mainIterationStartTime + * Start time of main iteration. + * @param {Number} firstSectionCount + * Iteration count of first section. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + */ + renderFirstIteration( + pathList, + state, + mainIterationStartTime, + firstSectionCount, + helper + ) { + const startTime = mainIterationStartTime; + const endTime = startTime + firstSectionCount * state.duration; + const segments = helper.createPathSegments(startTime, endTime); + pathList.push( + dom.path({ + className: "animation-iteration-path", + d: helper.toPathString(segments), + }) + ); + } + + /** + * Render middle iterations and add <path> elements to given pathList. + * + * @param {Array} pathList + * Add rendered <path> elements to this array. + * @param {Object} state + * State of animation. + * @param {Number} mainIterationStartTime + * Starting time of main iteration. + * @param {Number} firstSectionCount + * Iteration count of first section. + * @param {Number} middleSectionCount + * Iteration count of middle section. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + */ + renderMiddleIterations( + pathList, + state, + mainIterationStartTime, + firstSectionCount, + middleSectionCount, + helper + ) { + const offset = mainIterationStartTime + firstSectionCount * state.duration; + for (let i = 0; i < middleSectionCount; i++) { + // Get the path segments of each iteration. + const startTime = offset + i * state.duration; + const endTime = startTime + state.duration; + const segments = helper.createPathSegments(startTime, endTime); + pathList.push( + dom.path({ + className: "animation-iteration-path", + d: helper.toPathString(segments), + }) + ); + } + } + + /** + * Render last section of iterations and add a <path> element to given pathList. + * This section is only useful in cases where iterationStart has decimals. + * + * @param {Array} pathList + * Add rendered <path> elements to this array. + * @param {Object} state + * State of animation. + * @param {Number} mainIterationStartTime + * Starting time of main iteration. + * @param {Number} firstSectionCount + * Iteration count of first section. + * @param {Number} middleSectionCount + * Iteration count of middle section. + * @param {Number} lastSectionCount + * Iteration count of last section. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + */ + renderLastIteration( + pathList, + state, + mainIterationStartTime, + firstSectionCount, + middleSectionCount, + lastSectionCount, + helper + ) { + const startTime = + mainIterationStartTime + + (firstSectionCount + middleSectionCount) * state.duration; + const endTime = startTime + lastSectionCount * state.duration; + const segments = helper.createPathSegments(startTime, endTime); + pathList.push( + dom.path({ + className: "animation-iteration-path", + d: helper.toPathString(segments), + }) + ); + } + + /** + * Render infinity iterations and add <path> elements to given pathList. + * + * @param {Array} pathList + * Add rendered <path> elements to this array. + * @param {Object} state + * State of animation. + * @param {Number} mainIterationStartTime + * Starting time of main iteration. + * @param {Number} firstSectionCount + * Iteration count of first section. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + */ + renderInfinity( + pathList, + state, + mainIterationStartTime, + firstSectionCount, + helper + ) { + // Calculate the number of iterations to display, + // with a maximum of MAX_INFINITE_ANIMATIONS_ITERATIONS + let uncappedInfinityIterationCount = + (helper.totalDuration - firstSectionCount * state.duration) / + state.duration; + // If there is a small floating point error resulting in, e.g. 1.0000001 + // ceil will give us 2 so round first. + uncappedInfinityIterationCount = parseFloat( + uncappedInfinityIterationCount.toPrecision(6) + ); + const infinityIterationCount = Math.min( + MAX_INFINITE_ANIMATIONS_ITERATIONS, + Math.ceil(uncappedInfinityIterationCount) + ); + + // Append first full iteration path. + const firstStartTime = + mainIterationStartTime + firstSectionCount * state.duration; + const firstEndTime = firstStartTime + state.duration; + const firstSegments = helper.createPathSegments( + firstStartTime, + firstEndTime + ); + pathList.push( + dom.path({ + className: "animation-iteration-path", + d: helper.toPathString(firstSegments), + }) + ); + + // Append other iterations. We can copy first segments. + const isAlternate = state.direction.match(/alternate/); + for (let i = 1; i < infinityIterationCount; i++) { + const startTime = firstStartTime + i * state.duration; + let segments; + if (isAlternate && i % 2) { + // Copy as reverse. + segments = firstSegments.map(segment => { + return { x: firstEndTime - segment.x + startTime, y: segment.y }; + }); + } else { + // Copy as is. + segments = firstSegments.map(segment => { + return { x: segment.x - firstStartTime + startTime, y: segment.y }; + }); + } + pathList.push( + dom.path({ + className: "animation-iteration-path infinity", + d: helper.toPathString(segments), + }) + ); + } + } + + /** + * Render infinity duration. + * + * @param {Array} pathList + * Add rendered <path> elements to this array. + * @param {Object} state + * State of animation. + * @param {Number} mainIterationStartTime + * Starting time of main iteration. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + */ + renderInfinityDuration(pathList, state, mainIterationStartTime, helper) { + const startSegment = helper.getSegment(mainIterationStartTime); + const endSegment = { x: helper.totalDuration, y: startSegment.y }; + const segments = [startSegment, endSegment]; + pathList.push( + dom.path({ + className: "animation-iteration-path infinity-duration", + d: helper.toPathString(segments), + }) + ); + } + + /** + * Render 'endDelay' part in animation and add a <path> element to given pathList. + * + * @param {Array} pathList + * Add rendered <path> element to this array. + * @param {Object} state + * State of animation. + * @param {Number} mainIterationStartTime + * Starting time of main iteration. + * @param {Number} iterationCount + * Iteration count of whole animation. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + */ + renderEndDelay( + pathList, + state, + mainIterationStartTime, + iterationCount, + helper + ) { + const startTime = mainIterationStartTime + iterationCount * state.duration; + const startSegment = helper.getSegment(startTime); + const endSegment = { x: startTime + state.endDelay, y: startSegment.y }; + pathList.push( + dom.path({ + className: "animation-enddelay-path", + d: helper.toPathString([startSegment, endSegment]), + }) + ); + } + + /** + * Render 'fill' for forwards part in animation and + * add a <path> element to given pathList. + * + * @param {Array} pathList + * Add rendered <path> element to this array. + * @param {Object} state + * State of animation. + * @param {Number} mainIterationStartTime + * Starting time of main iteration. + * @param {Number} iterationCount + * Iteration count of whole animation. + * @param {SummaryGraphHelper} helper + * Instance of SummaryGraphHelper. + */ + renderForwardsFill( + pathList, + state, + mainIterationStartTime, + iterationCount, + helper + ) { + const startTime = + mainIterationStartTime + + iterationCount * state.duration + + (state.endDelay > 0 ? state.endDelay : 0); + const startSegment = helper.getSegment(startTime); + const endSegment = { x: helper.totalDuration, y: startSegment.y }; + pathList.push( + dom.path({ + className: "animation-fill-forwards-path", + d: helper.toPathString([startSegment, endSegment]), + }) + ); + } +} + +module.exports = TimingPath; diff --git a/devtools/client/inspector/animation/components/graph/moz.build b/devtools/client/inspector/animation/components/graph/moz.build new file mode 100644 index 0000000000..866bdd30ce --- /dev/null +++ b/devtools/client/inspector/animation/components/graph/moz.build @@ -0,0 +1,17 @@ +# 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( + "AnimationName.js", + "ComputedTimingPath.js", + "DelaySign.js", + "EffectTimingPath.js", + "EndDelaySign.js", + "NegativeDelayPath.js", + "NegativeEndDelayPath.js", + "NegativePath.js", + "SummaryGraph.js", + "SummaryGraphPath.js", + "TimingPath.js", +) |