summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/animation/components/keyframes-graph
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/keyframes-graph
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/keyframes-graph')
-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
9 files changed, 802 insertions, 0 deletions
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",
+)