/* 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;