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