summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/animation/components/graph
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/animation/components/graph/AnimationName.js38
-rw-r--r--devtools/client/inspector/animation/components/graph/ComputedTimingPath.js104
-rw-r--r--devtools/client/inspector/animation/components/graph/DelaySign.js42
-rw-r--r--devtools/client/inspector/animation/components/graph/EffectTimingPath.js84
-rw-r--r--devtools/client/inspector/animation/components/graph/EndDelaySign.js44
-rw-r--r--devtools/client/inspector/animation/components/graph/NegativeDelayPath.js27
-rw-r--r--devtools/client/inspector/animation/components/graph/NegativeEndDelayPath.js27
-rw-r--r--devtools/client/inspector/animation/components/graph/NegativePath.js101
-rw-r--r--devtools/client/inspector/animation/components/graph/SummaryGraph.js205
-rw-r--r--devtools/client/inspector/animation/components/graph/SummaryGraphPath.js282
-rw-r--r--devtools/client/inspector/animation/components/graph/TimingPath.js450
-rw-r--r--devtools/client/inspector/animation/components/graph/moz.build17
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",
+)