summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/animation/components/graph/SummaryGraphPath.js282
1 files changed, 282 insertions, 0 deletions
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;