Source code

Revision control

Copy as Markdown

Other Tools

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