282 lines
8 KiB
JavaScript
282 lines
8 KiB
JavaScript
/* 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.mjs");
|
|
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
|
|
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
|
|
const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.mjs");
|
|
|
|
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;
|