summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/animation/components
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/inspector/animation/components
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/inspector/animation/components')
-rw-r--r--devtools/client/inspector/animation/components/AnimatedPropertyItem.js64
-rw-r--r--devtools/client/inspector/animation/components/AnimatedPropertyList.js140
-rw-r--r--devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js90
-rw-r--r--devtools/client/inspector/animation/components/AnimatedPropertyName.js37
-rw-r--r--devtools/client/inspector/animation/components/AnimationDetailContainer.js90
-rw-r--r--devtools/client/inspector/animation/components/AnimationDetailHeader.js52
-rw-r--r--devtools/client/inspector/animation/components/AnimationItem.js121
-rw-r--r--devtools/client/inspector/animation/components/AnimationList.js72
-rw-r--r--devtools/client/inspector/animation/components/AnimationListContainer.js224
-rw-r--r--devtools/client/inspector/animation/components/AnimationTarget.js182
-rw-r--r--devtools/client/inspector/animation/components/AnimationToolbar.js75
-rw-r--r--devtools/client/inspector/animation/components/App.js164
-rw-r--r--devtools/client/inspector/animation/components/CurrentTimeLabel.js76
-rw-r--r--devtools/client/inspector/animation/components/CurrentTimeScrubber.js131
-rw-r--r--devtools/client/inspector/animation/components/KeyframesProgressBar.js108
-rw-r--r--devtools/client/inspector/animation/components/NoAnimationPanel.js61
-rw-r--r--devtools/client/inspector/animation/components/PauseResumeButton.js104
-rw-r--r--devtools/client/inspector/animation/components/PlaybackRateSelector.js108
-rw-r--r--devtools/client/inspector/animation/components/ProgressInspectionPanel.js49
-rw-r--r--devtools/client/inspector/animation/components/RewindButton.js38
-rw-r--r--devtools/client/inspector/animation/components/TickLabels.js46
-rw-r--r--devtools/client/inspector/animation/components/TickLines.js40
-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
-rw-r--r--devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js209
-rw-r--r--devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js245
-rw-r--r--devtools/client/inspector/animation/components/keyframes-graph/DiscretePath.js67
-rw-r--r--devtools/client/inspector/animation/components/keyframes-graph/DistancePath.js34
-rw-r--r--devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerItem.js33
-rw-r--r--devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerList.js37
-rw-r--r--devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js52
-rw-r--r--devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js111
-rw-r--r--devtools/client/inspector/animation/components/keyframes-graph/moz.build14
-rw-r--r--devtools/client/inspector/animation/components/moz.build30
44 files changed, 4325 insertions, 0 deletions
diff --git a/devtools/client/inspector/animation/components/AnimatedPropertyItem.js b/devtools/client/inspector/animation/components/AnimatedPropertyItem.js
new file mode 100644
index 0000000000..4c4ff37254
--- /dev/null
+++ b/devtools/client/inspector/animation/components/AnimatedPropertyItem.js
@@ -0,0 +1,64 @@
+/* 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 AnimatedPropertyName = createFactory(
+ require("resource://devtools/client/inspector/animation/components/AnimatedPropertyName.js")
+);
+const KeyframesGraph = createFactory(
+ require("resource://devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js")
+);
+
+class AnimatedPropertyItem extends PureComponent {
+ static get propTypes() {
+ return {
+ getComputedStyle: PropTypes.func.isRequired,
+ isUnchanged: PropTypes.bool.isRequired,
+ keyframes: PropTypes.array.isRequired,
+ name: PropTypes.string.isRequired,
+ simulateAnimation: PropTypes.func.isRequired,
+ state: PropTypes.object.isRequired,
+ type: PropTypes.string.isRequired,
+ };
+ }
+
+ render() {
+ const {
+ getComputedStyle,
+ isUnchanged,
+ keyframes,
+ name,
+ simulateAnimation,
+ state,
+ type,
+ } = this.props;
+
+ return dom.li(
+ {
+ className: "animated-property-item" + (isUnchanged ? " unchanged" : ""),
+ },
+ AnimatedPropertyName({
+ name,
+ state,
+ }),
+ KeyframesGraph({
+ getComputedStyle,
+ keyframes,
+ name,
+ simulateAnimation,
+ type,
+ })
+ );
+ }
+}
+
+module.exports = AnimatedPropertyItem;
diff --git a/devtools/client/inspector/animation/components/AnimatedPropertyList.js b/devtools/client/inspector/animation/components/AnimatedPropertyList.js
new file mode 100644
index 0000000000..54dff15187
--- /dev/null
+++ b/devtools/client/inspector/animation/components/AnimatedPropertyList.js
@@ -0,0 +1,140 @@
+/* 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 dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const AnimatedPropertyItem = createFactory(
+ require("resource://devtools/client/inspector/animation/components/AnimatedPropertyItem.js")
+);
+
+class AnimatedPropertyList extends Component {
+ static get propTypes() {
+ return {
+ animation: PropTypes.object.isRequired,
+ emitEventForTest: PropTypes.func.isRequired,
+ getAnimatedPropertyMap: PropTypes.func.isRequired,
+ getComputedStyle: PropTypes.func.isRequired,
+ simulateAnimation: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ // Array of object which has the property name, the keyframes, its aniamtion type
+ // and unchanged flag.
+ animatedProperties: null,
+ // To avoid rendering while the state is updating
+ // since we call an async function in updateState.
+ isStateUpdating: false,
+ };
+ }
+
+ componentDidMount() {
+ // No need to set isStateUpdating state since paint sequence is finish here.
+ this.updateState(this.props.animation);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ this.setState({ isStateUpdating: true });
+ this.updateState(nextProps.animation);
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return !nextState.isStateUpdating;
+ }
+
+ getPropertyState(property) {
+ const { animation } = this.props;
+
+ for (const propState of animation.state.propertyState) {
+ if (propState.property === property) {
+ return propState;
+ }
+ }
+
+ return null;
+ }
+
+ async updateState(animation) {
+ const { getAnimatedPropertyMap, emitEventForTest } = this.props;
+
+ let propertyMap = null;
+ let propertyNames = null;
+ let types = null;
+
+ try {
+ propertyMap = getAnimatedPropertyMap(animation);
+ propertyNames = [...propertyMap.keys()];
+ types = await animation.getAnimationTypes(propertyNames);
+ } catch (e) {
+ // Expected if we've already been destroyed or other node have been selected
+ // in the meantime.
+ console.error(e);
+ return;
+ }
+
+ const animatedProperties = propertyNames.map(name => {
+ const keyframes = propertyMap.get(name);
+ const type = types[name];
+ const isUnchanged = keyframes.every(
+ keyframe => keyframe.value === keyframes[0].value
+ );
+ return { isUnchanged, keyframes, name, type };
+ });
+
+ animatedProperties.sort((a, b) => {
+ if (a.isUnchanged === b.isUnchanged) {
+ return a.name > b.name ? 1 : -1;
+ }
+
+ return a.isUnchanged ? 1 : -1;
+ });
+
+ this.setState({
+ animatedProperties,
+ isStateUpdating: false,
+ });
+
+ emitEventForTest("animation-keyframes-rendered");
+ }
+
+ render() {
+ const { getComputedStyle, simulateAnimation } = this.props;
+ const { animatedProperties } = this.state;
+
+ if (!animatedProperties) {
+ return null;
+ }
+
+ return dom.ul(
+ {
+ className: "animated-property-list",
+ },
+ animatedProperties.map(({ isUnchanged, keyframes, name, type }) => {
+ const state = this.getPropertyState(name);
+ return AnimatedPropertyItem({
+ getComputedStyle,
+ isUnchanged,
+ keyframes,
+ name,
+ simulateAnimation,
+ state,
+ type,
+ });
+ })
+ );
+ }
+}
+
+module.exports = AnimatedPropertyList;
diff --git a/devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js b/devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js
new file mode 100644
index 0000000000..965099b37e
--- /dev/null
+++ b/devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js
@@ -0,0 +1,90 @@
+/* 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 AnimatedPropertyList = createFactory(
+ require("resource://devtools/client/inspector/animation/components/AnimatedPropertyList.js")
+);
+const KeyframesProgressBar = createFactory(
+ require("resource://devtools/client/inspector/animation/components/KeyframesProgressBar.js")
+);
+const ProgressInspectionPanel = createFactory(
+ require("resource://devtools/client/inspector/animation/components/ProgressInspectionPanel.js")
+);
+
+const {
+ getFormatStr,
+} = require("resource://devtools/client/inspector/animation/utils/l10n.js");
+
+class AnimatedPropertyListContainer extends PureComponent {
+ static get propTypes() {
+ return {
+ addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ animation: PropTypes.object.isRequired,
+ emitEventForTest: PropTypes.func.isRequired,
+ getAnimatedPropertyMap: PropTypes.func.isRequired,
+ getAnimationsCurrentTime: PropTypes.func.isRequired,
+ getComputedStyle: PropTypes.func.isRequired,
+ removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ simulateAnimation: PropTypes.func.isRequired,
+ simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired,
+ timeScale: PropTypes.object.isRequired,
+ };
+ }
+
+ render() {
+ const {
+ addAnimationsCurrentTimeListener,
+ animation,
+ emitEventForTest,
+ getAnimatedPropertyMap,
+ getAnimationsCurrentTime,
+ getComputedStyle,
+ removeAnimationsCurrentTimeListener,
+ simulateAnimation,
+ simulateAnimationForKeyframesProgressBar,
+ timeScale,
+ } = this.props;
+
+ return dom.div(
+ {
+ className: `animated-property-list-container ${animation.state.type}`,
+ },
+ ProgressInspectionPanel({
+ indicator: KeyframesProgressBar({
+ addAnimationsCurrentTimeListener,
+ animation,
+ getAnimationsCurrentTime,
+ removeAnimationsCurrentTimeListener,
+ simulateAnimationForKeyframesProgressBar,
+ timeScale,
+ }),
+ list: AnimatedPropertyList({
+ animation,
+ emitEventForTest,
+ getAnimatedPropertyMap,
+ getComputedStyle,
+ simulateAnimation,
+ }),
+ ticks: [0, 50, 100].map(position => {
+ const label = getFormatStr(
+ "detail.propertiesHeader.percentage",
+ position
+ );
+ return { position, label };
+ }),
+ })
+ );
+ }
+}
+
+module.exports = AnimatedPropertyListContainer;
diff --git a/devtools/client/inspector/animation/components/AnimatedPropertyName.js b/devtools/client/inspector/animation/components/AnimatedPropertyName.js
new file mode 100644
index 0000000000..e7a777ee12
--- /dev/null
+++ b/devtools/client/inspector/animation/components/AnimatedPropertyName.js
@@ -0,0 +1,37 @@
+/* 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 AnimatedPropertyName extends PureComponent {
+ static get propTypes() {
+ return {
+ name: PropTypes.string.isRequired,
+ state: PropTypes.oneOfType([null, PropTypes.object]).isRequired,
+ };
+ }
+
+ render() {
+ const { name, state } = this.props;
+
+ return dom.div(
+ {
+ className:
+ "animated-property-name" +
+ (state?.runningOnCompositor ? " compositor" : "") +
+ (state?.warning ? " warning" : ""),
+ title: state ? state.warning : "",
+ },
+ dom.span({}, name)
+ );
+ }
+}
+
+module.exports = AnimatedPropertyName;
diff --git a/devtools/client/inspector/animation/components/AnimationDetailContainer.js b/devtools/client/inspector/animation/components/AnimationDetailContainer.js
new file mode 100644
index 0000000000..9a7b4bfd1a
--- /dev/null
+++ b/devtools/client/inspector/animation/components/AnimationDetailContainer.js
@@ -0,0 +1,90 @@
+/* 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 {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+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 AnimationDetailHeader = createFactory(
+ require("resource://devtools/client/inspector/animation/components/AnimationDetailHeader.js")
+);
+const AnimatedPropertyListContainer = createFactory(
+ require("resource://devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js")
+);
+
+class AnimationDetailContainer extends PureComponent {
+ static get propTypes() {
+ return {
+ addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ animation: PropTypes.object.isRequired,
+ emitEventForTest: PropTypes.func.isRequired,
+ getAnimatedPropertyMap: PropTypes.func.isRequired,
+ getAnimationsCurrentTime: PropTypes.func.isRequired,
+ getComputedStyle: PropTypes.func.isRequired,
+ removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ setDetailVisibility: PropTypes.func.isRequired,
+ simulateAnimation: PropTypes.func.isRequired,
+ simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired,
+ timeScale: PropTypes.object.isRequired,
+ };
+ }
+
+ render() {
+ const {
+ addAnimationsCurrentTimeListener,
+ animation,
+ emitEventForTest,
+ getAnimatedPropertyMap,
+ getAnimationsCurrentTime,
+ getComputedStyle,
+ removeAnimationsCurrentTimeListener,
+ setDetailVisibility,
+ simulateAnimation,
+ simulateAnimationForKeyframesProgressBar,
+ timeScale,
+ } = this.props;
+
+ return dom.div(
+ {
+ className: "animation-detail-container",
+ },
+ animation
+ ? AnimationDetailHeader({
+ animation,
+ setDetailVisibility,
+ })
+ : null,
+ animation
+ ? AnimatedPropertyListContainer({
+ addAnimationsCurrentTimeListener,
+ animation,
+ emitEventForTest,
+ getAnimatedPropertyMap,
+ getAnimationsCurrentTime,
+ getComputedStyle,
+ removeAnimationsCurrentTimeListener,
+ simulateAnimation,
+ simulateAnimationForKeyframesProgressBar,
+ timeScale,
+ })
+ : null
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ animation: state.animations.selectedAnimation,
+ };
+};
+
+module.exports = connect(mapStateToProps)(AnimationDetailContainer);
diff --git a/devtools/client/inspector/animation/components/AnimationDetailHeader.js b/devtools/client/inspector/animation/components/AnimationDetailHeader.js
new file mode 100644
index 0000000000..5f3da0acba
--- /dev/null
+++ b/devtools/client/inspector/animation/components/AnimationDetailHeader.js
@@ -0,0 +1,52 @@
+/* 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");
+
+const {
+ getFormattedTitle,
+} = require("resource://devtools/client/inspector/animation/utils/l10n.js");
+
+class AnimationDetailHeader extends PureComponent {
+ static get propTypes() {
+ return {
+ animation: PropTypes.object.isRequired,
+ setDetailVisibility: PropTypes.func.isRequired,
+ };
+ }
+
+ onClick(event) {
+ event.stopPropagation();
+ const { setDetailVisibility } = this.props;
+ setDetailVisibility(false);
+ }
+
+ render() {
+ const { animation } = this.props;
+
+ return dom.div(
+ {
+ className: "animation-detail-header devtools-toolbar",
+ },
+ dom.div(
+ {
+ className: "animation-detail-title",
+ },
+ getFormattedTitle(animation.state)
+ ),
+ dom.button({
+ className: "animation-detail-close-button devtools-button",
+ onClick: this.onClick.bind(this),
+ })
+ );
+ }
+}
+
+module.exports = AnimationDetailHeader;
diff --git a/devtools/client/inspector/animation/components/AnimationItem.js b/devtools/client/inspector/animation/components/AnimationItem.js
new file mode 100644
index 0000000000..45bb09bac6
--- /dev/null
+++ b/devtools/client/inspector/animation/components/AnimationItem.js
@@ -0,0 +1,121 @@
+/* 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 {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const {
+ Component,
+ createFactory,
+} = 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 AnimationTarget = createFactory(
+ require("resource://devtools/client/inspector/animation/components/AnimationTarget.js")
+);
+const SummaryGraph = createFactory(
+ require("resource://devtools/client/inspector/animation/components/graph/SummaryGraph.js")
+);
+
+class AnimationItem extends Component {
+ static get propTypes() {
+ return {
+ animation: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ getAnimatedPropertyMap: PropTypes.func.isRequired,
+ getNodeFromActor: PropTypes.func.isRequired,
+ isDisplayable: PropTypes.bool.isRequired,
+ selectAnimation: PropTypes.func.isRequired,
+ selectedAnimation: PropTypes.object.isRequired,
+ setHighlightedNode: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ simulateAnimation: PropTypes.func.isRequired,
+ timeScale: PropTypes.object.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isSelected: this.isSelected(props),
+ };
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ this.setState({
+ isSelected: this.isSelected(nextProps),
+ });
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ this.props.isDisplayable !== nextProps.isDisplayable ||
+ this.state.isSelected !== nextState.isSelected ||
+ this.props.animation !== nextProps.animation ||
+ this.props.timeScale !== nextProps.timeScale
+ );
+ }
+
+ isSelected(props) {
+ return (
+ props.selectedAnimation &&
+ props.animation.actorID === props.selectedAnimation.actorID
+ );
+ }
+
+ render() {
+ const {
+ animation,
+ dispatch,
+ getAnimatedPropertyMap,
+ getNodeFromActor,
+ isDisplayable,
+ selectAnimation,
+ setHighlightedNode,
+ setSelectedNode,
+ simulateAnimation,
+ timeScale,
+ } = this.props;
+ const { isSelected } = this.state;
+
+ return dom.li(
+ {
+ className:
+ `animation-item ${animation.state.type} ` +
+ (isSelected ? "selected" : ""),
+ },
+ isDisplayable
+ ? [
+ AnimationTarget({
+ animation,
+ dispatch,
+ getNodeFromActor,
+ setHighlightedNode,
+ setSelectedNode,
+ }),
+ SummaryGraph({
+ animation,
+ getAnimatedPropertyMap,
+ selectAnimation,
+ simulateAnimation,
+ timeScale,
+ }),
+ ]
+ : null
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ selectedAnimation: state.animations.selectedAnimation,
+ };
+};
+
+module.exports = connect(mapStateToProps)(AnimationItem);
diff --git a/devtools/client/inspector/animation/components/AnimationList.js b/devtools/client/inspector/animation/components/AnimationList.js
new file mode 100644
index 0000000000..7d0f1caf3f
--- /dev/null
+++ b/devtools/client/inspector/animation/components/AnimationList.js
@@ -0,0 +1,72 @@
+/* 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 AnimationItem = createFactory(
+ require("resource://devtools/client/inspector/animation/components/AnimationItem.js")
+);
+
+class AnimationList extends PureComponent {
+ static get propTypes() {
+ return {
+ animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+ dispatch: PropTypes.func.isRequired,
+ displayableRange: PropTypes.object.isRequired,
+ getAnimatedPropertyMap: PropTypes.func.isRequired,
+ getNodeFromActor: PropTypes.func.isRequired,
+ selectAnimation: PropTypes.func.isRequired,
+ setHighlightedNode: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ simulateAnimation: PropTypes.func.isRequired,
+ timeScale: PropTypes.object.isRequired,
+ };
+ }
+
+ render() {
+ const {
+ animations,
+ dispatch,
+ displayableRange,
+ getAnimatedPropertyMap,
+ getNodeFromActor,
+ selectAnimation,
+ setHighlightedNode,
+ setSelectedNode,
+ simulateAnimation,
+ timeScale,
+ } = this.props;
+
+ const { startIndex, endIndex } = displayableRange;
+
+ return dom.ul(
+ {
+ className: "animation-list",
+ },
+ animations.map((animation, index) =>
+ AnimationItem({
+ animation,
+ dispatch,
+ getAnimatedPropertyMap,
+ getNodeFromActor,
+ isDisplayable: startIndex <= index && index <= endIndex,
+ selectAnimation,
+ setHighlightedNode,
+ setSelectedNode,
+ simulateAnimation,
+ timeScale,
+ })
+ )
+ );
+ }
+}
+
+module.exports = AnimationList;
diff --git a/devtools/client/inspector/animation/components/AnimationListContainer.js b/devtools/client/inspector/animation/components/AnimationListContainer.js
new file mode 100644
index 0000000000..d97b368f70
--- /dev/null
+++ b/devtools/client/inspector/animation/components/AnimationListContainer.js
@@ -0,0 +1,224 @@
+/* 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,
+ createRef,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.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 AnimationList = createFactory(
+ require("resource://devtools/client/inspector/animation/components/AnimationList.js")
+);
+const CurrentTimeScrubber = createFactory(
+ require("resource://devtools/client/inspector/animation/components/CurrentTimeScrubber.js")
+);
+const ProgressInspectionPanel = createFactory(
+ require("resource://devtools/client/inspector/animation/components/ProgressInspectionPanel.js")
+);
+
+const {
+ findOptimalTimeInterval,
+} = require("resource://devtools/client/inspector/animation/utils/utils.js");
+const {
+ getStr,
+} = require("resource://devtools/client/inspector/animation/utils/l10n.js");
+const { throttle } = require("resource://devtools/shared/throttle.js");
+
+// The minimum spacing between 2 time graduation headers in the timeline (px).
+const TIME_GRADUATION_MIN_SPACING = 40;
+
+class AnimationListContainer extends PureComponent {
+ static get propTypes() {
+ return {
+ addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+ direction: PropTypes.string.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ getAnimatedPropertyMap: PropTypes.func.isRequired,
+ getNodeFromActor: PropTypes.func.isRequired,
+ removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ selectAnimation: PropTypes.func.isRequired,
+ setAnimationsCurrentTime: PropTypes.func.isRequired,
+ setHighlightedNode: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ sidebarWidth: PropTypes.number.isRequired,
+ simulateAnimation: PropTypes.func.isRequired,
+ timeScale: PropTypes.object.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this._ref = createRef();
+
+ this.updateDisplayableRange = throttle(
+ this.updateDisplayableRange,
+ 100,
+ this
+ );
+
+ this.state = {
+ // tick labels and lines on the progress inspection panel
+ ticks: [],
+ // Displayable range.
+ displayableRange: { startIndex: 0, endIndex: 0 },
+ };
+ }
+
+ componentDidMount() {
+ this.updateTicks(this.props);
+
+ const current = this._ref.current;
+ this._inspectionPanelEl = current.querySelector(
+ ".progress-inspection-panel"
+ );
+ this._inspectionPanelEl.addEventListener("scroll", () => {
+ this.updateDisplayableRange();
+ });
+
+ this._animationListEl = current.querySelector(".animation-list");
+ const resizeObserver = new current.ownerGlobal.ResizeObserver(() => {
+ this.updateDisplayableRange();
+ });
+ resizeObserver.observe(this._animationListEl);
+
+ const animationItemEl = current.querySelector(".animation-item");
+ this._itemHeight = animationItemEl.offsetHeight;
+
+ this.updateDisplayableRange();
+ }
+
+ componentDidUpdate(prevProps) {
+ const { timeScale, sidebarWidth } = this.props;
+
+ if (
+ timeScale.getDuration() !== prevProps.timeScale.getDuration() ||
+ timeScale.zeroPositionTime !== prevProps.timeScale.zeroPositionTime ||
+ sidebarWidth !== prevProps.sidebarWidth
+ ) {
+ this.updateTicks(this.props);
+ }
+ }
+
+ /**
+ * Since it takes too much time if we render all of animation graphs,
+ * we restrict to render the items that are not in displaying area.
+ * This function calculates the displayable item range.
+ */
+ updateDisplayableRange() {
+ const count =
+ Math.floor(this._animationListEl.offsetHeight / this._itemHeight) + 1;
+ const index = Math.floor(
+ this._inspectionPanelEl.scrollTop / this._itemHeight
+ );
+ this.setState({
+ displayableRange: { startIndex: index, endIndex: index + count },
+ });
+ }
+
+ updateTicks(props) {
+ const { animations, timeScale } = props;
+ const tickLinesEl = this._ref.current.querySelector(".tick-lines");
+ const width = tickLinesEl.offsetWidth;
+ const animationDuration = timeScale.getDuration();
+ const minTimeInterval =
+ (TIME_GRADUATION_MIN_SPACING * animationDuration) / width;
+ const intervalLength = findOptimalTimeInterval(minTimeInterval);
+ const intervalWidth = (intervalLength * width) / animationDuration;
+ const tickCount = parseInt(width / intervalWidth, 10);
+ const isAllDurationInfinity = animations.every(
+ animation => animation.state.duration === Infinity
+ );
+ const zeroBasePosition =
+ width * (timeScale.zeroPositionTime / animationDuration);
+ const shiftWidth = zeroBasePosition % intervalWidth;
+ const needToShift = zeroBasePosition !== 0 && shiftWidth !== 0;
+
+ const ticks = [];
+ // Need to display first graduation since position will be shifted.
+ if (needToShift) {
+ const label = timeScale.formatTime(timeScale.distanceToRelativeTime(0));
+ ticks.push({ position: 0, label, width: shiftWidth });
+ }
+
+ for (let i = 0; i <= tickCount; i++) {
+ const position = ((i * intervalWidth + shiftWidth) * 100) / width;
+ const distance = timeScale.distanceToRelativeTime(position);
+ const label =
+ isAllDurationInfinity && i === tickCount
+ ? getStr("player.infiniteTimeLabel")
+ : timeScale.formatTime(distance);
+ ticks.push({ position, label, width: intervalWidth });
+ }
+
+ this.setState({ ticks });
+ }
+
+ render() {
+ const {
+ addAnimationsCurrentTimeListener,
+ animations,
+ direction,
+ dispatch,
+ getAnimatedPropertyMap,
+ getNodeFromActor,
+ removeAnimationsCurrentTimeListener,
+ selectAnimation,
+ setAnimationsCurrentTime,
+ setHighlightedNode,
+ setSelectedNode,
+ simulateAnimation,
+ timeScale,
+ } = this.props;
+ const { displayableRange, ticks } = this.state;
+
+ return dom.div(
+ {
+ className: "animation-list-container",
+ ref: this._ref,
+ },
+ ProgressInspectionPanel({
+ indicator: CurrentTimeScrubber({
+ addAnimationsCurrentTimeListener,
+ direction,
+ removeAnimationsCurrentTimeListener,
+ setAnimationsCurrentTime,
+ timeScale,
+ }),
+ list: AnimationList({
+ animations,
+ dispatch,
+ displayableRange,
+ getAnimatedPropertyMap,
+ getNodeFromActor,
+ selectAnimation,
+ setHighlightedNode,
+ setSelectedNode,
+ simulateAnimation,
+ timeScale,
+ }),
+ ticks,
+ })
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ sidebarWidth: state.animations.sidebarSize
+ ? state.animations.sidebarSize.width
+ : 0,
+ };
+};
+
+module.exports = connect(mapStateToProps)(AnimationListContainer);
diff --git a/devtools/client/inspector/animation/components/AnimationTarget.js b/devtools/client/inspector/animation/components/AnimationTarget.js
new file mode 100644
index 0000000000..2fa392b3f6
--- /dev/null
+++ b/devtools/client/inspector/animation/components/AnimationTarget.js
@@ -0,0 +1,182 @@
+/* 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,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.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 {
+ translateNodeFrontToGrip,
+} = require("resource://devtools/client/inspector/shared/utils.js");
+
+const {
+ REPS,
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/index.js");
+const { Rep } = REPS;
+const ElementNode = REPS.ElementNode;
+
+const {
+ getInspectorStr,
+} = require("resource://devtools/client/inspector/animation/utils/l10n.js");
+
+const {
+ highlightNode,
+ unhighlightNode,
+} = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js");
+
+class AnimationTarget extends Component {
+ static get propTypes() {
+ return {
+ animation: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ getNodeFromActor: PropTypes.func.isRequired,
+ highlightedNode: PropTypes.string.isRequired,
+ setHighlightedNode: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ nodeFront: null,
+ };
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ this.updateNodeFront(this.props.animation);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ if (this.props.animation.actorID !== nextProps.animation.actorID) {
+ this.updateNodeFront(nextProps.animation);
+ }
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ this.state.nodeFront !== nextState.nodeFront ||
+ this.props.highlightedNode !== nextState.highlightedNode
+ );
+ }
+
+ async updateNodeFront(animation) {
+ const { getNodeFromActor } = this.props;
+
+ // Try and get it from the playerFront directly.
+ let nodeFront = animation.animationTargetNodeFront;
+
+ // Next, get it from the walkerActor if it wasn't found.
+ if (!nodeFront) {
+ try {
+ nodeFront = await getNodeFromActor(animation.actorID);
+ } catch (e) {
+ // If an error occured while getting the nodeFront and if it can't be
+ // attributed to the panel having been destroyed in the meantime, this
+ // error needs to be logged and render needs to stop.
+ console.error(e);
+ this.setState({ nodeFront: null });
+ return;
+ }
+ }
+
+ this.setState({ nodeFront });
+ }
+
+ async ensureNodeFront() {
+ if (!this.state.nodeFront.actorID) {
+ // In case of no actorID, the node front had been destroyed.
+ // This will occur when the pseudo element was re-generated.
+ await this.updateNodeFront(this.props.animation);
+ }
+ }
+
+ async highlight() {
+ await this.ensureNodeFront();
+
+ if (this.state.nodeFront) {
+ this.props.dispatch(
+ highlightNode(this.state.nodeFront, {
+ hideInfoBar: true,
+ hideGuides: true,
+ })
+ );
+ }
+ }
+
+ async select() {
+ await this.ensureNodeFront();
+
+ if (this.state.nodeFront) {
+ this.props.setSelectedNode(this.state.nodeFront);
+ }
+ }
+
+ render() {
+ const { dispatch, highlightedNode, setHighlightedNode } = this.props;
+ const { nodeFront } = this.state;
+
+ if (!nodeFront) {
+ return dom.div({
+ className: "animation-target",
+ });
+ }
+
+ const isHighlighted = nodeFront.actorID === highlightedNode;
+
+ return dom.div(
+ {
+ className: "animation-target" + (isHighlighted ? " highlighting" : ""),
+ },
+ Rep({
+ defaultRep: ElementNode,
+ mode: MODE.TINY,
+ inspectIconTitle: getInspectorStr(
+ "inspector.nodePreview.highlightNodeLabel"
+ ),
+ inspectIconClassName: "highlight-node",
+ object: translateNodeFrontToGrip(nodeFront),
+ onDOMNodeClick: () => this.select(),
+ onDOMNodeMouseOut: () => {
+ if (!isHighlighted) {
+ dispatch(unhighlightNode());
+ }
+ },
+ onDOMNodeMouseOver: () => {
+ if (!isHighlighted) {
+ this.highlight();
+ }
+ },
+ onInspectIconClick: (_, e) => {
+ e.stopPropagation();
+
+ if (!isHighlighted) {
+ // At first, hide highlighter which was created by onDOMNodeMouseOver.
+ dispatch(unhighlightNode());
+ }
+
+ setHighlightedNode(isHighlighted ? null : nodeFront);
+ },
+ })
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ highlightedNode: state.animations.highlightedNode,
+ };
+};
+
+module.exports = connect(mapStateToProps)(AnimationTarget);
diff --git a/devtools/client/inspector/animation/components/AnimationToolbar.js b/devtools/client/inspector/animation/components/AnimationToolbar.js
new file mode 100644
index 0000000000..9b6b7aadc8
--- /dev/null
+++ b/devtools/client/inspector/animation/components/AnimationToolbar.js
@@ -0,0 +1,75 @@
+/* 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 CurrentTimeLabel = createFactory(
+ require("resource://devtools/client/inspector/animation/components/CurrentTimeLabel.js")
+);
+const PauseResumeButton = createFactory(
+ require("resource://devtools/client/inspector/animation/components/PauseResumeButton.js")
+);
+const PlaybackRateSelector = createFactory(
+ require("resource://devtools/client/inspector/animation/components/PlaybackRateSelector.js")
+);
+const RewindButton = createFactory(
+ require("resource://devtools/client/inspector/animation/components/RewindButton.js")
+);
+
+class AnimationToolbar extends PureComponent {
+ static get propTypes() {
+ return {
+ addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+ removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ rewindAnimationsCurrentTime: PropTypes.func.isRequired,
+ setAnimationsPlaybackRate: PropTypes.func.isRequired,
+ setAnimationsPlayState: PropTypes.func.isRequired,
+ timeScale: PropTypes.object.isRequired,
+ };
+ }
+
+ render() {
+ const {
+ addAnimationsCurrentTimeListener,
+ animations,
+ removeAnimationsCurrentTimeListener,
+ rewindAnimationsCurrentTime,
+ setAnimationsPlaybackRate,
+ setAnimationsPlayState,
+ timeScale,
+ } = this.props;
+
+ return dom.div(
+ {
+ className: "animation-toolbar devtools-toolbar",
+ },
+ RewindButton({
+ rewindAnimationsCurrentTime,
+ }),
+ PauseResumeButton({
+ animations,
+ setAnimationsPlayState,
+ }),
+ PlaybackRateSelector({
+ animations,
+ setAnimationsPlaybackRate,
+ }),
+ CurrentTimeLabel({
+ addAnimationsCurrentTimeListener,
+ removeAnimationsCurrentTimeListener,
+ timeScale,
+ })
+ );
+ }
+}
+
+module.exports = AnimationToolbar;
diff --git a/devtools/client/inspector/animation/components/App.js b/devtools/client/inspector/animation/components/App.js
new file mode 100644
index 0000000000..75a000286c
--- /dev/null
+++ b/devtools/client/inspector/animation/components/App.js
@@ -0,0 +1,164 @@
+/* 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 dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const AnimationDetailContainer = createFactory(
+ require("resource://devtools/client/inspector/animation/components/AnimationDetailContainer.js")
+);
+const AnimationListContainer = createFactory(
+ require("resource://devtools/client/inspector/animation/components/AnimationListContainer.js")
+);
+const AnimationToolbar = createFactory(
+ require("resource://devtools/client/inspector/animation/components/AnimationToolbar.js")
+);
+const NoAnimationPanel = createFactory(
+ require("resource://devtools/client/inspector/animation/components/NoAnimationPanel.js")
+);
+const SplitBox = createFactory(
+ require("resource://devtools/client/shared/components/splitter/SplitBox.js")
+);
+
+class App extends Component {
+ static get propTypes() {
+ return {
+ addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+ detailVisibility: PropTypes.bool.isRequired,
+ direction: PropTypes.string.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ emitEventForTest: PropTypes.func.isRequired,
+ getAnimatedPropertyMap: PropTypes.func.isRequired,
+ getAnimationsCurrentTime: PropTypes.func.isRequired,
+ getComputedStyle: PropTypes.func.isRequired,
+ getNodeFromActor: PropTypes.func.isRequired,
+ removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ rewindAnimationsCurrentTime: PropTypes.func.isRequired,
+ selectAnimation: PropTypes.func.isRequired,
+ setAnimationsCurrentTime: PropTypes.func.isRequired,
+ setAnimationsPlaybackRate: PropTypes.func.isRequired,
+ setAnimationsPlayState: PropTypes.func.isRequired,
+ setDetailVisibility: PropTypes.func.isRequired,
+ setHighlightedNode: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ simulateAnimation: PropTypes.func.isRequired,
+ simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired,
+ timeScale: PropTypes.object.isRequired,
+ toggleElementPicker: PropTypes.func.isRequired,
+ toggleLockingHighlight: PropTypes.func.isRequired,
+ };
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ this.props.animations.length !== 0 || nextProps.animations.length !== 0
+ );
+ }
+
+ render() {
+ const {
+ addAnimationsCurrentTimeListener,
+ animations,
+ detailVisibility,
+ direction,
+ dispatch,
+ emitEventForTest,
+ getAnimatedPropertyMap,
+ getAnimationsCurrentTime,
+ getComputedStyle,
+ getNodeFromActor,
+ removeAnimationsCurrentTimeListener,
+ rewindAnimationsCurrentTime,
+ selectAnimation,
+ setAnimationsCurrentTime,
+ setAnimationsPlaybackRate,
+ setAnimationsPlayState,
+ setDetailVisibility,
+ setHighlightedNode,
+ setSelectedNode,
+ simulateAnimation,
+ simulateAnimationForKeyframesProgressBar,
+ timeScale,
+ toggleElementPicker,
+ } = this.props;
+
+ return dom.div(
+ {
+ id: "animation-container",
+ className: detailVisibility ? "animation-detail-visible" : "",
+ tabIndex: -1,
+ },
+ animations.length
+ ? [
+ AnimationToolbar({
+ addAnimationsCurrentTimeListener,
+ animations,
+ removeAnimationsCurrentTimeListener,
+ rewindAnimationsCurrentTime,
+ setAnimationsPlaybackRate,
+ setAnimationsPlayState,
+ timeScale,
+ }),
+ SplitBox({
+ className: "animation-container-splitter",
+ endPanel: AnimationDetailContainer({
+ addAnimationsCurrentTimeListener,
+ emitEventForTest,
+ getAnimatedPropertyMap,
+ getAnimationsCurrentTime,
+ getComputedStyle,
+ removeAnimationsCurrentTimeListener,
+ setDetailVisibility,
+ simulateAnimation,
+ simulateAnimationForKeyframesProgressBar,
+ timeScale,
+ }),
+ endPanelControl: true,
+ initialHeight: "50%",
+ splitterSize: 1,
+ minSize: "30px",
+ startPanel: AnimationListContainer({
+ addAnimationsCurrentTimeListener,
+ animations,
+ direction,
+ dispatch,
+ getAnimatedPropertyMap,
+ getNodeFromActor,
+ removeAnimationsCurrentTimeListener,
+ selectAnimation,
+ setAnimationsCurrentTime,
+ setHighlightedNode,
+ setSelectedNode,
+ simulateAnimation,
+ timeScale,
+ }),
+ vert: false,
+ }),
+ ]
+ : NoAnimationPanel({
+ toggleElementPicker,
+ })
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ animations: state.animations.animations,
+ detailVisibility: state.animations.detailVisibility,
+ timeScale: state.animations.timeScale,
+ };
+};
+
+module.exports = connect(mapStateToProps)(App);
diff --git a/devtools/client/inspector/animation/components/CurrentTimeLabel.js b/devtools/client/inspector/animation/components/CurrentTimeLabel.js
new file mode 100644
index 0000000000..67b2498e8b
--- /dev/null
+++ b/devtools/client/inspector/animation/components/CurrentTimeLabel.js
@@ -0,0 +1,76 @@
+/* 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 {
+ createRef,
+ 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 CurrentTimeLabel extends PureComponent {
+ static get propTypes() {
+ return {
+ addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ timeScale: PropTypes.object.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this._ref = createRef();
+
+ const { addAnimationsCurrentTimeListener } = props;
+ this.onCurrentTimeUpdated = this.onCurrentTimeUpdated.bind(this);
+
+ addAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+ }
+
+ componentWillUnmount() {
+ const { removeAnimationsCurrentTimeListener } = this.props;
+ removeAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+ }
+
+ onCurrentTimeUpdated(currentTime) {
+ const { timeScale } = this.props;
+ const text = formatStopwatchTime(currentTime - timeScale.zeroPositionTime);
+ // onCurrentTimeUpdated is bound to requestAnimationFrame.
+ // As to update the component too frequently has performance issue if React controlled,
+ // update raw component directly. See Bug 1699039.
+ this._ref.current.textContent = text;
+ }
+
+ render() {
+ return dom.label({ className: "current-time-label", ref: this._ref });
+ }
+}
+
+/**
+ * Format a timestamp (in ms) as a mm:ss.mmm string.
+ *
+ * @param {Number} time
+ * @return {String}
+ */
+function formatStopwatchTime(time) {
+ // Format falsy values as 0
+ if (!time) {
+ return "00:00.000";
+ }
+
+ const sign = time < 0 ? "-" : "";
+ let milliseconds = parseInt(Math.abs(time % 1000), 10);
+ let seconds = parseInt(Math.abs(time / 1000) % 60, 10);
+ let minutes = parseInt(Math.abs(time / (1000 * 60)), 10);
+
+ minutes = minutes.toString().padStart(2, "0");
+ seconds = seconds.toString().padStart(2, "0");
+ milliseconds = milliseconds.toString().padStart(3, "0");
+ return `${sign}${minutes}:${seconds}.${milliseconds}`;
+}
+
+module.exports = CurrentTimeLabel;
diff --git a/devtools/client/inspector/animation/components/CurrentTimeScrubber.js b/devtools/client/inspector/animation/components/CurrentTimeScrubber.js
new file mode 100644
index 0000000000..8b87e32eff
--- /dev/null
+++ b/devtools/client/inspector/animation/components/CurrentTimeScrubber.js
@@ -0,0 +1,131 @@
+/* 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 DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+const {
+ createRef,
+ 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 CurrentTimeScrubber extends PureComponent {
+ static get propTypes() {
+ return {
+ addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ direction: PropTypes.string.isRequired,
+ removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ setAnimationsCurrentTime: PropTypes.func.isRequired,
+ timeScale: PropTypes.object.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this._ref = createRef();
+
+ const { addAnimationsCurrentTimeListener } = props;
+ this.onCurrentTimeUpdated = this.onCurrentTimeUpdated.bind(this);
+ this.onMouseDown = this.onMouseDown.bind(this);
+ this.onMouseMove = this.onMouseMove.bind(this);
+ this.onMouseUp = this.onMouseUp.bind(this);
+
+ addAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+ }
+
+ componentDidMount() {
+ const current = this._ref.current;
+ current.addEventListener("mousedown", this.onMouseDown);
+ this._scrubber = current.querySelector(".current-time-scrubber");
+ }
+
+ componentWillUnmount() {
+ const { removeAnimationsCurrentTimeListener } = this.props;
+ removeAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+ }
+
+ onCurrentTimeUpdated(currentTime) {
+ const { timeScale } = this.props;
+
+ const position = currentTime / timeScale.getDuration();
+ // onCurrentTimeUpdated is bound to requestAnimationFrame.
+ // As to update the component too frequently has performance issue if React controlled,
+ // update raw component directly. See Bug 1699039.
+ this._scrubber.style.marginInlineStart = `${position * 100}%`;
+ }
+
+ onMouseDown(event) {
+ event.stopPropagation();
+ const current = this._ref.current;
+ this.controllerArea = current.getBoundingClientRect();
+ this.listenerTarget = DevToolsUtils.getTopWindow(current.ownerGlobal);
+ this.listenerTarget.addEventListener("mousemove", this.onMouseMove);
+ this.listenerTarget.addEventListener("mouseup", this.onMouseUp);
+ this.decorationTarget = current.closest(".animation-list-container");
+ this.decorationTarget.classList.add("active-scrubber");
+
+ this.updateAnimationsCurrentTime(event.pageX, true);
+ }
+
+ onMouseMove(event) {
+ event.stopPropagation();
+ this.isMouseMoved = true;
+ this.updateAnimationsCurrentTime(event.pageX);
+ }
+
+ onMouseUp(event) {
+ event.stopPropagation();
+
+ if (this.isMouseMoved) {
+ this.updateAnimationsCurrentTime(event.pageX, true);
+ this.isMouseMoved = null;
+ }
+
+ this.uninstallListeners();
+ }
+
+ uninstallListeners() {
+ this.listenerTarget.removeEventListener("mousemove", this.onMouseMove);
+ this.listenerTarget.removeEventListener("mouseup", this.onMouseUp);
+ this.listenerTarget = null;
+ this.decorationTarget.classList.remove("active-scrubber");
+ this.decorationTarget = null;
+ this.controllerArea = null;
+ }
+
+ updateAnimationsCurrentTime(pageX, needRefresh) {
+ const { direction, setAnimationsCurrentTime, timeScale } = this.props;
+
+ let progressRate =
+ (pageX - this.controllerArea.x) / this.controllerArea.width;
+
+ if (progressRate < 0.0) {
+ progressRate = 0.0;
+ } else if (progressRate > 1.0) {
+ progressRate = 1.0;
+ }
+
+ const time =
+ direction === "ltr"
+ ? progressRate * timeScale.getDuration()
+ : (1 - progressRate) * timeScale.getDuration();
+
+ setAnimationsCurrentTime(time, needRefresh);
+ }
+
+ render() {
+ return dom.div(
+ {
+ className: "current-time-scrubber-area",
+ ref: this._ref,
+ },
+ dom.div({ className: "indication-bar current-time-scrubber" })
+ );
+ }
+}
+
+module.exports = CurrentTimeScrubber;
diff --git a/devtools/client/inspector/animation/components/KeyframesProgressBar.js b/devtools/client/inspector/animation/components/KeyframesProgressBar.js
new file mode 100644
index 0000000000..b4e9e526de
--- /dev/null
+++ b/devtools/client/inspector/animation/components/KeyframesProgressBar.js
@@ -0,0 +1,108 @@
+/* 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 {
+ createRef,
+ 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 KeyframesProgressBar extends PureComponent {
+ static get propTypes() {
+ return {
+ addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ animation: PropTypes.object.isRequired,
+ getAnimationsCurrentTime: PropTypes.func.isRequired,
+ removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+ simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired,
+ timeScale: PropTypes.object.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this._progressBarRef = createRef();
+
+ this.onCurrentTimeUpdated = this.onCurrentTimeUpdated.bind(this);
+ }
+
+ componentDidMount() {
+ const { addAnimationsCurrentTimeListener } = this.props;
+
+ this.setupAnimation(this.props);
+ addAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ const { animation, getAnimationsCurrentTime, timeScale } = nextProps;
+
+ this.setupAnimation(nextProps);
+ this.updateOffset(getAnimationsCurrentTime(), animation, timeScale);
+ }
+
+ componentWillUnmount() {
+ const { removeAnimationsCurrentTimeListener } = this.props;
+
+ removeAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+ this.simulatedAnimation = null;
+ }
+
+ onCurrentTimeUpdated(currentTime) {
+ const { animation, timeScale } = this.props;
+ this.updateOffset(currentTime, animation, timeScale);
+ }
+
+ updateOffset(currentTime, animation, timeScale) {
+ const { createdTime, playbackRate } = animation.state;
+
+ const time =
+ (timeScale.minStartTime + currentTime - createdTime) * playbackRate;
+
+ if (isNaN(time)) {
+ // Setting an invalid currentTime will throw so bail out if time is not a number for
+ // any reason.
+ return;
+ }
+
+ this.simulatedAnimation.currentTime = time;
+ const position =
+ this.simulatedAnimation.effect.getComputedTiming().progress;
+
+ // onCurrentTimeUpdated is bound to requestAnimationFrame.
+ // As to update the component too frequently has performance issue if React controlled,
+ // update raw component directly. See Bug 1699039.
+ this._progressBarRef.current.style.marginInlineStart = `${position * 100}%`;
+ }
+
+ setupAnimation(props) {
+ const { animation, simulateAnimationForKeyframesProgressBar } = props;
+
+ if (this.simulatedAnimation) {
+ this.simulatedAnimation.cancel();
+ }
+
+ const timing = Object.assign({}, animation.state, {
+ iterations: animation.state.iterationCount || Infinity,
+ });
+
+ this.simulatedAnimation = simulateAnimationForKeyframesProgressBar(timing);
+ }
+
+ render() {
+ return dom.div(
+ { className: "keyframes-progress-bar-area" },
+ dom.div({
+ className: "indication-bar keyframes-progress-bar",
+ ref: this._progressBarRef,
+ })
+ );
+ }
+}
+
+module.exports = KeyframesProgressBar;
diff --git a/devtools/client/inspector/animation/components/NoAnimationPanel.js b/devtools/client/inspector/animation/components/NoAnimationPanel.js
new file mode 100644
index 0000000000..ea034e413d
--- /dev/null
+++ b/devtools/client/inspector/animation/components/NoAnimationPanel.js
@@ -0,0 +1,61 @@
+/* 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,
+} = 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 {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/animationinspector.properties"
+);
+
+class NoAnimationPanel extends Component {
+ static get propTypes() {
+ return {
+ elementPickerEnabled: PropTypes.bool.isRequired,
+ toggleElementPicker: PropTypes.func.isRequired,
+ };
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.props.elementPickerEnabled != nextProps.elementPickerEnabled;
+ }
+
+ render() {
+ const { elementPickerEnabled, toggleElementPicker } = this.props;
+
+ return dom.div(
+ {
+ className: "animation-error-message devtools-sidepanel-no-result",
+ },
+ dom.p(null, L10N.getStr("panel.noAnimation")),
+ dom.button({
+ className:
+ "animation-element-picker devtools-button" +
+ (elementPickerEnabled ? " checked" : ""),
+ "data-standalone": true,
+ onClick: event => {
+ event.stopPropagation();
+ toggleElementPicker();
+ },
+ })
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ elementPickerEnabled: state.animations.elementPickerEnabled,
+ };
+};
+
+module.exports = connect(mapStateToProps)(NoAnimationPanel);
diff --git a/devtools/client/inspector/animation/components/PauseResumeButton.js b/devtools/client/inspector/animation/components/PauseResumeButton.js
new file mode 100644
index 0000000000..bcbb597a39
--- /dev/null
+++ b/devtools/client/inspector/animation/components/PauseResumeButton.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 {
+ createRef,
+ 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 { KeyCodes } = require("resource://devtools/client/shared/keycodes.js");
+
+const {
+ getStr,
+} = require("resource://devtools/client/inspector/animation/utils/l10n.js");
+const {
+ hasRunningAnimation,
+} = require("resource://devtools/client/inspector/animation/utils/utils.js");
+
+class PauseResumeButton extends PureComponent {
+ static get propTypes() {
+ return {
+ animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+ setAnimationsPlayState: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.pauseResumeButtonRef = createRef();
+
+ this.state = {
+ isRunning: false,
+ };
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ this.updateState(this.props);
+ }
+
+ componentDidMount() {
+ const targetEl = this.getKeyEventTarget();
+ targetEl.addEventListener("keydown", this.onKeyDown);
+ }
+
+ componentDidUpdate() {
+ this.updateState();
+ }
+
+ componentWillUnount() {
+ const targetEl = this.getKeyEventTarget();
+ targetEl.removeEventListener("keydown", this.onKeyDown);
+ }
+
+ getKeyEventTarget() {
+ return this.pauseResumeButtonRef.current.closest("#animation-container");
+ }
+
+ onToggleAnimationsPlayState(event) {
+ event.stopPropagation();
+ const { setAnimationsPlayState } = this.props;
+ const { isRunning } = this.state;
+
+ setAnimationsPlayState(!isRunning);
+ }
+
+ onKeyDown(event) {
+ // Prevent to the duplicated call from the key listener and click listener.
+ if (
+ event.keyCode === KeyCodes.DOM_VK_SPACE &&
+ event.target !== this.pauseResumeButtonRef.current
+ ) {
+ this.onToggleAnimationsPlayState(event);
+ }
+ }
+
+ updateState() {
+ const { animations } = this.props;
+ const isRunning = hasRunningAnimation(animations);
+ this.setState({ isRunning });
+ }
+
+ render() {
+ const { isRunning } = this.state;
+
+ return dom.button({
+ className:
+ "pause-resume-button devtools-button" + (isRunning ? "" : " paused"),
+ onClick: this.onToggleAnimationsPlayState.bind(this),
+ title: isRunning
+ ? getStr("timeline.resumedButtonTooltip")
+ : getStr("timeline.pausedButtonTooltip"),
+ ref: this.pauseResumeButtonRef,
+ });
+ }
+}
+
+module.exports = PauseResumeButton;
diff --git a/devtools/client/inspector/animation/components/PlaybackRateSelector.js b/devtools/client/inspector/animation/components/PlaybackRateSelector.js
new file mode 100644
index 0000000000..2d0de53a0c
--- /dev/null
+++ b/devtools/client/inspector/animation/components/PlaybackRateSelector.js
@@ -0,0 +1,108 @@
+/* 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");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const {
+ getFormatStr,
+} = require("resource://devtools/client/inspector/animation/utils/l10n.js");
+
+const PLAYBACK_RATES = [0.1, 0.25, 0.5, 1, 2, 5, 10];
+
+class PlaybackRateSelector extends PureComponent {
+ static get propTypes() {
+ return {
+ animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+ playbackRates: PropTypes.arrayOf(PropTypes.number).isRequired,
+ setAnimationsPlaybackRate: PropTypes.func.isRequired,
+ };
+ }
+
+ static getDerivedStateFromProps(props, state) {
+ const { animations, playbackRates } = props;
+
+ const currentPlaybackRates = sortAndUnique(
+ animations.map(a => a.state.playbackRate)
+ );
+ const options = sortAndUnique([
+ ...PLAYBACK_RATES,
+ ...playbackRates,
+ ...currentPlaybackRates,
+ ]);
+
+ if (currentPlaybackRates.length === 1) {
+ return {
+ options,
+ selected: currentPlaybackRates[0],
+ };
+ }
+
+ // When the animations displayed have mixed playback rates, we can't
+ // select any of the predefined ones.
+ return {
+ options: ["", ...options],
+ selected: "",
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ options: [],
+ selected: 1,
+ };
+ }
+
+ onChange(e) {
+ const { setAnimationsPlaybackRate } = this.props;
+
+ if (!e.target.value) {
+ return;
+ }
+
+ setAnimationsPlaybackRate(e.target.value);
+ }
+
+ render() {
+ const { options, selected } = this.state;
+
+ return dom.select(
+ {
+ className: "playback-rate-selector devtools-button",
+ onChange: this.onChange.bind(this),
+ },
+ options.map(rate => {
+ return dom.option(
+ {
+ selected: rate === selected ? "true" : null,
+ value: rate,
+ },
+ rate ? getFormatStr("player.playbackRateLabel", rate) : "-"
+ );
+ })
+ );
+ }
+}
+
+function sortAndUnique(array) {
+ return [...new Set(array)].sort((a, b) => a > b);
+}
+
+const mapStateToProps = state => {
+ return {
+ playbackRates: state.animations.playbackRates,
+ };
+};
+
+module.exports = connect(mapStateToProps)(PlaybackRateSelector);
diff --git a/devtools/client/inspector/animation/components/ProgressInspectionPanel.js b/devtools/client/inspector/animation/components/ProgressInspectionPanel.js
new file mode 100644
index 0000000000..71c82dd17b
--- /dev/null
+++ b/devtools/client/inspector/animation/components/ProgressInspectionPanel.js
@@ -0,0 +1,49 @@
+/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const TickLabels = createFactory(
+ require("resource://devtools/client/inspector/animation/components/TickLabels.js")
+);
+const TickLines = createFactory(
+ require("resource://devtools/client/inspector/animation/components/TickLines.js")
+);
+
+/**
+ * This component is a panel for the progress of animations or keyframes, supports
+ * displaying the ticks, take the areas of indicator and the list.
+ */
+class ProgressInspectionPanel extends PureComponent {
+ static get propTypes() {
+ return {
+ indicator: PropTypes.any.isRequired,
+ list: PropTypes.any.isRequired,
+ ticks: PropTypes.arrayOf(PropTypes.object).isRequired,
+ };
+ }
+
+ render() {
+ const { indicator, list, ticks } = this.props;
+
+ return dom.div(
+ {
+ className: "progress-inspection-panel",
+ },
+ dom.div({ className: "background" }, TickLines({ ticks })),
+ dom.div({ className: "indicator" }, indicator),
+ dom.div({ className: "header devtools-toolbar" }, TickLabels({ ticks })),
+ list
+ );
+ }
+}
+
+module.exports = ProgressInspectionPanel;
diff --git a/devtools/client/inspector/animation/components/RewindButton.js b/devtools/client/inspector/animation/components/RewindButton.js
new file mode 100644
index 0000000000..f069b42368
--- /dev/null
+++ b/devtools/client/inspector/animation/components/RewindButton.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");
+
+const {
+ getStr,
+} = require("resource://devtools/client/inspector/animation/utils/l10n.js");
+
+class RewindButton extends PureComponent {
+ static get propTypes() {
+ return {
+ rewindAnimationsCurrentTime: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ const { rewindAnimationsCurrentTime } = this.props;
+
+ return dom.button({
+ className: "rewind-button devtools-button",
+ onClick: event => {
+ event.stopPropagation();
+ rewindAnimationsCurrentTime();
+ },
+ title: getStr("timeline.rewindButtonTooltip"),
+ });
+ }
+}
+
+module.exports = RewindButton;
diff --git a/devtools/client/inspector/animation/components/TickLabels.js b/devtools/client/inspector/animation/components/TickLabels.js
new file mode 100644
index 0000000000..019c7b177b
--- /dev/null
+++ b/devtools/client/inspector/animation/components/TickLabels.js
@@ -0,0 +1,46 @@
+/* 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");
+
+/**
+ * This component is intended to show tick labels on the header.
+ */
+class TickLabels extends PureComponent {
+ static get propTypes() {
+ return {
+ ticks: PropTypes.array.isRequired,
+ };
+ }
+
+ render() {
+ const { ticks } = this.props;
+
+ return dom.div(
+ {
+ className: "tick-labels",
+ },
+ ticks.map(tick =>
+ dom.div(
+ {
+ className: "tick-label",
+ style: {
+ marginInlineStart: `${tick.position}%`,
+ maxWidth: `${tick.width}px`,
+ },
+ },
+ tick.label
+ )
+ )
+ );
+ }
+}
+
+module.exports = TickLabels;
diff --git a/devtools/client/inspector/animation/components/TickLines.js b/devtools/client/inspector/animation/components/TickLines.js
new file mode 100644
index 0000000000..d3cbdead98
--- /dev/null
+++ b/devtools/client/inspector/animation/components/TickLines.js
@@ -0,0 +1,40 @@
+/* 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");
+
+/**
+ * This component is intended to show the tick line as the background.
+ */
+class TickLines extends PureComponent {
+ static get propTypes() {
+ return {
+ ticks: PropTypes.array.isRequired,
+ };
+ }
+
+ render() {
+ const { ticks } = this.props;
+
+ return dom.div(
+ {
+ className: "tick-lines",
+ },
+ ticks.map(tick =>
+ dom.div({
+ className: "tick-line",
+ style: { marginInlineStart: `${tick.position}%` },
+ })
+ )
+ );
+ }
+}
+
+module.exports = TickLines;
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",
+)
diff --git a/devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js b/devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js
new file mode 100644
index 0000000000..120b61c73b
--- /dev/null
+++ b/devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js
@@ -0,0 +1,209 @@
+/* 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 { colorUtils } = require("resource://devtools/shared/css/color.js");
+
+const ComputedStylePath = require("resource://devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js");
+
+const DEFAULT_COLOR = { r: 0, g: 0, b: 0, a: 1 };
+
+/* Count for linearGradient ID */
+let LINEAR_GRADIENT_ID_COUNT = 0;
+
+class ColorPath extends ComputedStylePath {
+ constructor(props) {
+ super(props);
+
+ this.state = this.propToState(props);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ this.setState(this.propToState(nextProps));
+ }
+
+ getPropertyName() {
+ return "color";
+ }
+
+ getPropertyValue(keyframe) {
+ return keyframe.value;
+ }
+
+ propToState({ keyframes, name }) {
+ const maxObject = { distance: -Number.MAX_VALUE };
+
+ for (let i = 0; i < keyframes.length - 1; i++) {
+ const value1 = getRGBA(name, keyframes[i].value);
+ for (let j = i + 1; j < keyframes.length; j++) {
+ const value2 = getRGBA(name, keyframes[j].value);
+ const distance = getRGBADistance(value1, value2);
+
+ if (maxObject.distance >= distance) {
+ continue;
+ }
+
+ maxObject.distance = distance;
+ maxObject.value1 = value1;
+ maxObject.value2 = value2;
+ }
+ }
+
+ const maxDistance = maxObject.distance;
+ const baseValue =
+ maxObject.value1 < maxObject.value2 ? maxObject.value1 : maxObject.value2;
+
+ return { baseValue, maxDistance, name };
+ }
+
+ toSegmentValue(computedStyle) {
+ const { baseValue, maxDistance, name } = this.state;
+ const value = getRGBA(name, computedStyle);
+ return getRGBADistance(baseValue, value) / maxDistance;
+ }
+
+ /**
+ * Overide parent's method.
+ */
+ renderEasingHint() {
+ const { easingHintStrokeWidth, graphHeight, keyframes, totalDuration } =
+ this.props;
+
+ const hints = [];
+
+ for (let i = 0; i < keyframes.length - 1; i++) {
+ const startKeyframe = keyframes[i];
+ const endKeyframe = keyframes[i + 1];
+ const startTime = startKeyframe.offset * totalDuration;
+ const endTime = endKeyframe.offset * totalDuration;
+
+ const g = dom.g(
+ {
+ className: "hint",
+ },
+ dom.title({}, startKeyframe.easing),
+ dom.rect({
+ x: startTime,
+ y: -graphHeight,
+ height: graphHeight,
+ width: endTime - startTime,
+ }),
+ dom.line({
+ x1: startTime,
+ y1: -graphHeight,
+ x2: endTime,
+ y2: -graphHeight,
+ style: {
+ "stroke-width": easingHintStrokeWidth,
+ },
+ })
+ );
+ hints.push(g);
+ }
+
+ return hints;
+ }
+
+ /**
+ * Overide parent's method.
+ */
+ renderPathSegments(segments) {
+ for (const segment of segments) {
+ segment.y = 1;
+ }
+
+ const lastSegment = segments[segments.length - 1];
+ const id = `color-property-${LINEAR_GRADIENT_ID_COUNT++}`;
+ const path = super.renderPathSegments(segments, { fill: `url(#${id})` });
+ const linearGradient = dom.linearGradient(
+ { id },
+ segments.map(segment => {
+ return dom.stop({
+ stopColor: segment.computedStyle,
+ offset: segment.x / lastSegment.x,
+ });
+ })
+ );
+
+ return [path, linearGradient];
+ }
+
+ render() {
+ return dom.g(
+ {
+ className: "color-path",
+ },
+ super.renderGraph()
+ );
+ }
+}
+
+/**
+ * Parse given RGBA string.
+ *
+ * @param {String} propertyName
+ * @param {String} colorString
+ * e.g. rgb(0, 0, 0) or rgba(0, 0, 0, 0.5) and so on.
+ * @return {Object}
+ * RGBA {r: r, g: g, b: b, a: a}.
+ */
+function getRGBA(propertyName, colorString) {
+ // Special handling for CSS property which can specify the not normal CSS color value.
+ switch (propertyName) {
+ case "caret-color": {
+ // This property can specify "auto" keyword.
+ if (colorString === "auto") {
+ return DEFAULT_COLOR;
+ }
+ break;
+ }
+ case "scrollbar-color": {
+ // This property can specify "auto", "dark", "light" keywords and multiple colors.
+ if (
+ ["auto", "dark", "light"].includes(colorString) ||
+ colorString.indexOf(" ") > 0
+ ) {
+ return DEFAULT_COLOR;
+ }
+ break;
+ }
+ }
+
+ const color = new colorUtils.CssColor(colorString);
+ return color.getRGBATuple();
+}
+
+/**
+ * Return the distance from give two RGBA.
+ *
+ * @param {Object} rgba1
+ * RGBA (format is same to getRGBA)
+ * @param {Object} rgba2
+ * RGBA (format is same to getRGBA)
+ * @return {Number}
+ * The range is 0 - 1.0.
+ */
+function getRGBADistance(rgba1, rgba2) {
+ const startA = rgba1.a;
+ const startR = rgba1.r * startA;
+ const startG = rgba1.g * startA;
+ const startB = rgba1.b * startA;
+ const endA = rgba2.a;
+ const endR = rgba2.r * endA;
+ const endG = rgba2.g * endA;
+ const endB = rgba2.b * endA;
+ const diffA = startA - endA;
+ const diffR = startR - endR;
+ const diffG = startG - endG;
+ const diffB = startB - endB;
+ return Math.sqrt(
+ diffA * diffA + diffR * diffR + diffG * diffG + diffB * diffB
+ );
+}
+
+module.exports = ColorPath;
diff --git a/devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js b/devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js
new file mode 100644
index 0000000000..1da5c4da96
--- /dev/null
+++ b/devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js
@@ -0,0 +1,245 @@
+/* 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");
+
+const {
+ createPathSegments,
+ DEFAULT_DURATION_RESOLUTION,
+ getPreferredProgressThresholdByKeyframes,
+ toPathString,
+} = require("resource://devtools/client/inspector/animation/utils/graph-helper.js");
+
+/*
+ * This class is an abstraction for computed style path of keyframes.
+ * Subclass of this should implement the following methods:
+ *
+ * getPropertyName()
+ * Returns property name which will be animated.
+ * @return {String}
+ * e.g. opacity
+ *
+ * getPropertyValue(keyframe)
+ * Returns value which uses as animated keyframe value from given parameter.
+ * @param {Object} keyframe
+ * @return {String||Number}
+ * e.g. 0
+ *
+ * toSegmentValue(computedStyle)
+ * Convert computed style to segment value of graph.
+ * @param {String||Number}
+ * e.g. 0
+ * @return {Number}
+ * e.g. 0 (should be 0 - 1.0)
+ */
+class ComputedStylePath extends PureComponent {
+ static get propTypes() {
+ return {
+ componentWidth: PropTypes.number.isRequired,
+ easingHintStrokeWidth: PropTypes.number.isRequired,
+ graphHeight: PropTypes.number.isRequired,
+ keyframes: PropTypes.array.isRequired,
+ simulateAnimation: PropTypes.func.isRequired,
+ totalDuration: PropTypes.number.isRequired,
+ };
+ }
+
+ /**
+ * Return an array containing the path segments between the given start and
+ * end keyframe values.
+ *
+ * @param {Object} startKeyframe
+ * Starting keyframe.
+ * @param {Object} endKeyframe
+ * Ending keyframe.
+ * @return {Array}
+ * Array of path segment.
+ * [{x: {Number} time, y: {Number} segment value}, ...]
+ */
+ getPathSegments(startKeyframe, endKeyframe) {
+ const { componentWidth, simulateAnimation, totalDuration } = this.props;
+
+ const propertyName = this.getPropertyName();
+ const offsetDistance = endKeyframe.offset - startKeyframe.offset;
+ const duration = offsetDistance * totalDuration;
+
+ const keyframes = [startKeyframe, endKeyframe].map((keyframe, index) => {
+ return {
+ offset: index,
+ easing: keyframe.easing,
+ [getJsPropertyName(propertyName)]: this.getPropertyValue(keyframe),
+ };
+ });
+ const effect = {
+ duration,
+ fill: "forwards",
+ };
+
+ const simulatedAnimation = simulateAnimation(keyframes, effect, true);
+
+ if (!simulatedAnimation) {
+ return null;
+ }
+
+ const simulatedElement = simulatedAnimation.effect.target;
+ const win = simulatedElement.ownerGlobal;
+ const threshold = getPreferredProgressThresholdByKeyframes(keyframes);
+
+ const getSegment = time => {
+ simulatedAnimation.currentTime = time;
+ const computedStyle = win
+ .getComputedStyle(simulatedElement)
+ .getPropertyValue(propertyName);
+
+ return {
+ computedStyle,
+ x: time,
+ y: this.toSegmentValue(computedStyle),
+ };
+ };
+
+ const segments = createPathSegments(
+ 0,
+ duration,
+ duration / componentWidth,
+ threshold,
+ DEFAULT_DURATION_RESOLUTION,
+ getSegment
+ );
+ const offset = startKeyframe.offset * totalDuration;
+
+ for (const segment of segments) {
+ segment.x += offset;
+ }
+
+ return segments;
+ }
+
+ /**
+ * Render easing hint from given path segments.
+ *
+ * @param {Array} segments
+ * Path segments.
+ * @return {Element}
+ * Element which represents easing hint.
+ */
+ renderEasingHint(segments) {
+ const { easingHintStrokeWidth, keyframes, totalDuration } = this.props;
+
+ const hints = [];
+
+ for (let i = 0, indexOfSegments = 0; i < keyframes.length - 1; i++) {
+ const startKeyframe = keyframes[i];
+ const endKeyframe = keyframes[i + 1];
+ const endTime = endKeyframe.offset * totalDuration;
+ const hintSegments = [];
+
+ for (; indexOfSegments < segments.length; indexOfSegments++) {
+ const segment = segments[indexOfSegments];
+ hintSegments.push(segment);
+
+ if (startKeyframe.offset === endKeyframe.offset) {
+ hintSegments.push(segments[++indexOfSegments]);
+ break;
+ } else if (segment.x === endTime) {
+ break;
+ }
+ }
+
+ const g = dom.g(
+ {
+ className: "hint",
+ },
+ dom.title({}, startKeyframe.easing),
+ dom.path({
+ d:
+ `M${hintSegments[0].x},${hintSegments[0].y} ` +
+ toPathString(hintSegments),
+ style: {
+ "stroke-width": easingHintStrokeWidth,
+ },
+ })
+ );
+
+ hints.push(g);
+ }
+
+ return hints;
+ }
+
+ /**
+ * Render graph. This method returns React dom.
+ *
+ * @return {Element}
+ */
+ renderGraph() {
+ const { keyframes } = this.props;
+
+ const segments = [];
+
+ for (let i = 0; i < keyframes.length - 1; i++) {
+ const startKeyframe = keyframes[i];
+ const endKeyframe = keyframes[i + 1];
+ const keyframesSegments = this.getPathSegments(
+ startKeyframe,
+ endKeyframe
+ );
+
+ if (!keyframesSegments) {
+ return null;
+ }
+
+ segments.push(...keyframesSegments);
+ }
+
+ return [this.renderPathSegments(segments), this.renderEasingHint(segments)];
+ }
+
+ /**
+ * Return react dom fron given path segments.
+ *
+ * @param {Array} segments
+ * @param {Object} style
+ * @return {Element}
+ */
+ renderPathSegments(segments, style) {
+ const { graphHeight } = this.props;
+
+ for (const segment of segments) {
+ segment.y *= graphHeight;
+ }
+
+ let d = `M${segments[0].x},0 `;
+ d += toPathString(segments);
+ d += `L${segments[segments.length - 1].x},0 Z`;
+
+ return dom.path({ d, style });
+ }
+}
+
+/**
+ * Convert given CSS property name to JavaScript CSS name.
+ *
+ * @param {String} cssPropertyName
+ * CSS property name (e.g. background-color).
+ * @return {String}
+ * JavaScript CSS property name (e.g. backgroundColor).
+ */
+function getJsPropertyName(cssPropertyName) {
+ if (cssPropertyName == "float") {
+ return "cssFloat";
+ }
+ // https://drafts.csswg.org/cssom/#css-property-to-idl-attribute
+ return cssPropertyName.replace(/-([a-z])/gi, (str, group) => {
+ return group.toUpperCase();
+ });
+}
+
+module.exports = ComputedStylePath;
diff --git a/devtools/client/inspector/animation/components/keyframes-graph/DiscretePath.js b/devtools/client/inspector/animation/components/keyframes-graph/DiscretePath.js
new file mode 100644
index 0000000000..26e5373f7c
--- /dev/null
+++ b/devtools/client/inspector/animation/components/keyframes-graph/DiscretePath.js
@@ -0,0 +1,67 @@
+/* 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const ComputedStylePath = require("resource://devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js");
+
+class DiscretePath extends ComputedStylePath {
+ static get propTypes() {
+ return {
+ name: PropTypes.string.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = this.propToState(props);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ this.setState(this.propToState(nextProps));
+ }
+
+ getPropertyName() {
+ return this.props.name;
+ }
+
+ getPropertyValue(keyframe) {
+ return keyframe.value;
+ }
+
+ propToState({ getComputedStyle, keyframes, name }) {
+ const discreteValues = [];
+
+ for (const keyframe of keyframes) {
+ const style = getComputedStyle(name, { [name]: keyframe.value });
+
+ if (!discreteValues.includes(style)) {
+ discreteValues.push(style);
+ }
+ }
+
+ return { discreteValues };
+ }
+
+ toSegmentValue(computedStyle) {
+ const { discreteValues } = this.state;
+ return discreteValues.indexOf(computedStyle) / (discreteValues.length - 1);
+ }
+
+ render() {
+ return dom.g(
+ {
+ className: "discrete-path",
+ },
+ super.renderGraph()
+ );
+ }
+}
+
+module.exports = DiscretePath;
diff --git a/devtools/client/inspector/animation/components/keyframes-graph/DistancePath.js b/devtools/client/inspector/animation/components/keyframes-graph/DistancePath.js
new file mode 100644
index 0000000000..3436366c30
--- /dev/null
+++ b/devtools/client/inspector/animation/components/keyframes-graph/DistancePath.js
@@ -0,0 +1,34 @@
+/* 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 ComputedStylePath = require("resource://devtools/client/inspector/animation/components/keyframes-graph/ComputedStylePath.js");
+
+class DistancePath extends ComputedStylePath {
+ getPropertyName() {
+ return "opacity";
+ }
+
+ getPropertyValue(keyframe) {
+ return keyframe.distance;
+ }
+
+ toSegmentValue(computedStyle) {
+ return computedStyle;
+ }
+
+ render() {
+ return dom.g(
+ {
+ className: "distance-path",
+ },
+ super.renderGraph()
+ );
+ }
+}
+
+module.exports = DistancePath;
diff --git a/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerItem.js b/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerItem.js
new file mode 100644
index 0000000000..4212fdb88f
--- /dev/null
+++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerItem.js
@@ -0,0 +1,33 @@
+/* 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 KeyframeMarkerItem extends PureComponent {
+ static get propTypes() {
+ return {
+ keyframe: PropTypes.object.isRequired,
+ };
+ }
+
+ render() {
+ const { keyframe } = this.props;
+
+ return dom.li({
+ className: "keyframe-marker-item",
+ title: keyframe.value,
+ style: {
+ marginInlineStart: `${keyframe.offset * 100}%`,
+ },
+ });
+ }
+}
+
+module.exports = KeyframeMarkerItem;
diff --git a/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerList.js b/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerList.js
new file mode 100644
index 0000000000..7fd112f641
--- /dev/null
+++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerList.js
@@ -0,0 +1,37 @@
+/* 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 KeyframeMarkerItem = createFactory(
+ require("resource://devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerItem.js")
+);
+
+class KeyframeMarkerList extends PureComponent {
+ static get propTypes() {
+ return {
+ keyframes: PropTypes.array.isRequired,
+ };
+ }
+
+ render() {
+ const { keyframes } = this.props;
+
+ return dom.ul(
+ {
+ className: "keyframe-marker-list",
+ },
+ keyframes.map(keyframe => KeyframeMarkerItem({ keyframe }))
+ );
+ }
+}
+
+module.exports = KeyframeMarkerList;
diff --git a/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js
new file mode 100644
index 0000000000..cbab6806d2
--- /dev/null
+++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraph.js
@@ -0,0 +1,52 @@
+/* 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 KeyframeMarkerList = createFactory(
+ require("resource://devtools/client/inspector/animation/components/keyframes-graph/KeyframeMarkerList.js")
+);
+const KeyframesGraphPath = createFactory(
+ require("resource://devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js")
+);
+
+class KeyframesGraph extends PureComponent {
+ static get propTypes() {
+ return {
+ getComputedStyle: PropTypes.func.isRequired,
+ keyframes: PropTypes.array.isRequired,
+ name: PropTypes.string.isRequired,
+ simulateAnimation: PropTypes.func.isRequired,
+ type: PropTypes.string.isRequired,
+ };
+ }
+
+ render() {
+ const { getComputedStyle, keyframes, name, simulateAnimation, type } =
+ this.props;
+
+ return dom.div(
+ {
+ className: `keyframes-graph ${name}`,
+ },
+ KeyframesGraphPath({
+ getComputedStyle,
+ keyframes,
+ name,
+ simulateAnimation,
+ type,
+ }),
+ KeyframeMarkerList({ keyframes })
+ );
+ }
+}
+
+module.exports = KeyframesGraph;
diff --git a/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js
new file mode 100644
index 0000000000..70c2720194
--- /dev/null
+++ b/devtools/client/inspector/animation/components/keyframes-graph/KeyframesGraphPath.js
@@ -0,0 +1,111 @@
+/* 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 ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.js");
+
+const ColorPath = createFactory(
+ require("resource://devtools/client/inspector/animation/components/keyframes-graph/ColorPath.js")
+);
+const DiscretePath = createFactory(
+ require("resource://devtools/client/inspector/animation/components/keyframes-graph/DiscretePath.js")
+);
+const DistancePath = createFactory(
+ require("resource://devtools/client/inspector/animation/components/keyframes-graph/DistancePath.js")
+);
+
+const {
+ DEFAULT_EASING_HINT_STROKE_WIDTH,
+ DEFAULT_GRAPH_HEIGHT,
+ DEFAULT_KEYFRAMES_GRAPH_DURATION,
+} = require("resource://devtools/client/inspector/animation/utils/graph-helper.js");
+
+class KeyframesGraphPath extends PureComponent {
+ static get propTypes() {
+ return {
+ getComputedStyle: PropTypes.func.isRequired,
+ keyframes: PropTypes.array.isRequired,
+ name: PropTypes.string.isRequired,
+ simulateAnimation: PropTypes.func.isRequired,
+ type: PropTypes.string.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ componentHeight: 0,
+ componentWidth: 0,
+ };
+ }
+
+ componentDidMount() {
+ this.updateState();
+ }
+
+ getPathComponent(type) {
+ switch (type) {
+ case "color":
+ return ColorPath;
+ case "discrete":
+ return DiscretePath;
+ default:
+ return DistancePath;
+ }
+ }
+
+ updateState() {
+ const thisEl = ReactDOM.findDOMNode(this);
+ this.setState({
+ componentHeight: thisEl.parentNode.clientHeight,
+ componentWidth: thisEl.parentNode.clientWidth,
+ });
+ }
+
+ render() {
+ const { getComputedStyle, keyframes, name, simulateAnimation, type } =
+ this.props;
+ const { componentHeight, componentWidth } = this.state;
+
+ if (!componentWidth) {
+ return dom.svg();
+ }
+
+ const pathComponent = this.getPathComponent(type);
+ const strokeWidthInViewBox =
+ (DEFAULT_EASING_HINT_STROKE_WIDTH / 2 / componentHeight) *
+ DEFAULT_GRAPH_HEIGHT;
+
+ return dom.svg(
+ {
+ className: "keyframes-graph-path",
+ preserveAspectRatio: "none",
+ viewBox:
+ `0 -${DEFAULT_GRAPH_HEIGHT + strokeWidthInViewBox} ` +
+ `${DEFAULT_KEYFRAMES_GRAPH_DURATION} ` +
+ `${DEFAULT_GRAPH_HEIGHT + strokeWidthInViewBox * 2}`,
+ },
+ pathComponent({
+ componentWidth,
+ easingHintStrokeWidth: DEFAULT_EASING_HINT_STROKE_WIDTH,
+ getComputedStyle,
+ graphHeight: DEFAULT_GRAPH_HEIGHT,
+ keyframes,
+ name,
+ simulateAnimation,
+ totalDuration: DEFAULT_KEYFRAMES_GRAPH_DURATION,
+ })
+ );
+ }
+}
+
+module.exports = KeyframesGraphPath;
diff --git a/devtools/client/inspector/animation/components/keyframes-graph/moz.build b/devtools/client/inspector/animation/components/keyframes-graph/moz.build
new file mode 100644
index 0000000000..1ff518e21d
--- /dev/null
+++ b/devtools/client/inspector/animation/components/keyframes-graph/moz.build
@@ -0,0 +1,14 @@
+# 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(
+ "ColorPath.js",
+ "ComputedStylePath.js",
+ "DiscretePath.js",
+ "DistancePath.js",
+ "KeyframeMarkerItem.js",
+ "KeyframeMarkerList.js",
+ "KeyframesGraph.js",
+ "KeyframesGraphPath.js",
+)
diff --git a/devtools/client/inspector/animation/components/moz.build b/devtools/client/inspector/animation/components/moz.build
new file mode 100644
index 0000000000..cd8ecc05c4
--- /dev/null
+++ b/devtools/client/inspector/animation/components/moz.build
@@ -0,0 +1,30 @@
+# 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/.
+
+DIRS += ["graph", "keyframes-graph"]
+
+DevToolsModules(
+ "AnimatedPropertyItem.js",
+ "AnimatedPropertyList.js",
+ "AnimatedPropertyListContainer.js",
+ "AnimatedPropertyName.js",
+ "AnimationDetailContainer.js",
+ "AnimationDetailHeader.js",
+ "AnimationItem.js",
+ "AnimationList.js",
+ "AnimationListContainer.js",
+ "AnimationTarget.js",
+ "AnimationToolbar.js",
+ "App.js",
+ "CurrentTimeLabel.js",
+ "CurrentTimeScrubber.js",
+ "KeyframesProgressBar.js",
+ "NoAnimationPanel.js",
+ "PauseResumeButton.js",
+ "PlaybackRateSelector.js",
+ "ProgressInspectionPanel.js",
+ "RewindButton.js",
+ "TickLabels.js",
+ "TickLines.js",
+)