summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/animation/utils/graph-helper.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/animation/utils/graph-helper.js332
1 files changed, 332 insertions, 0 deletions
diff --git a/devtools/client/inspector/animation/utils/graph-helper.js b/devtools/client/inspector/animation/utils/graph-helper.js
new file mode 100644
index 0000000000..cca2713254
--- /dev/null
+++ b/devtools/client/inspector/animation/utils/graph-helper.js
@@ -0,0 +1,332 @@
+/* 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";
+
+// BOUND_EXCLUDING_TIME should be less than 1ms and is used to exclude start
+// and end bounds when dividing duration in createPathSegments.
+const BOUND_EXCLUDING_TIME = 0.001;
+// We define default graph height since if the height of viewport in SVG is
+// too small (e.g. 1), vector-effect may not be able to calculate correctly.
+const DEFAULT_GRAPH_HEIGHT = 100;
+// Default animation duration for keyframes graph.
+const DEFAULT_KEYFRAMES_GRAPH_DURATION = 1000;
+// DEFAULT_MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
+const DEFAULT_MIN_PROGRESS_THRESHOLD = 0.1;
+// In the createPathSegments function, an animation duration is divided by
+// DEFAULT_DURATION_RESOLUTION in order to draw the way the animation progresses.
+// But depending on the timing-function, we may be not able to make the graph
+// smoothly progress if this resolution is not high enough.
+// So, if the difference of animation progress between 2 divisions is more than
+// DEFAULT_MIN_PROGRESS_THRESHOLD * DEFAULT_GRAPH_HEIGHT, then createPathSegments
+// re-divides by DEFAULT_DURATION_RESOLUTION.
+// DEFAULT_DURATION_RESOLUTION shoud be integer and more than 2.
+const DEFAULT_DURATION_RESOLUTION = 4;
+// Stroke width for easing hint.
+const DEFAULT_EASING_HINT_STROKE_WIDTH = 5;
+
+/**
+ * The helper class for creating summary graph.
+ */
+class SummaryGraphHelper {
+ /**
+ * Constructor.
+ *
+ * @param {Object} state
+ * State of animation.
+ * @param {Array} keyframes
+ * Array of keyframe.
+ * @param {Number} totalDuration
+ * Total displayable duration.
+ * @param {Number} minSegmentDuration
+ * Minimum segment duration.
+ * @param {Function} getValueFunc
+ * Which returns graph value of given time.
+ * The function should return a number value between 0 - 1.
+ * e.g. time => { return 1.0 };
+ * @param {Function} toPathStringFunc
+ * Which returns a path string for 'd' attribute for <path> from given segments.
+ */
+ constructor(
+ state,
+ keyframes,
+ totalDuration,
+ minSegmentDuration,
+ getValueFunc,
+ toPathStringFunc
+ ) {
+ this.totalDuration = totalDuration;
+ this.minSegmentDuration = minSegmentDuration;
+ this.minProgressThreshold =
+ getPreferredProgressThreshold(state, keyframes) * DEFAULT_GRAPH_HEIGHT;
+ this.durationResolution = getPreferredDurationResolution(keyframes);
+ this.getValue = getValueFunc;
+ this.toPathString = toPathStringFunc;
+
+ this.getSegment = this.getSegment.bind(this);
+ }
+
+ /**
+ * Create the path segments from given parameters.
+ *
+ * @param {Number} startTime
+ * Starting time of animation.
+ * @param {Number} endTime
+ * Ending time of animation.
+ * @return {Array}
+ * Array of path segment.
+ * e.g.[{x: {Number} time, y: {Number} progress}, ...]
+ */
+ createPathSegments(startTime, endTime) {
+ return createPathSegments(
+ startTime,
+ endTime,
+ this.minSegmentDuration,
+ this.minProgressThreshold,
+ this.durationResolution,
+ this.getSegment
+ );
+ }
+
+ /**
+ * Return a coordinate as a graph segment at given time.
+ *
+ * @param {Number} time
+ * @return {Object}
+ * { x: Number, y: Number }
+ */
+ getSegment(time) {
+ const value = this.getValue(time);
+ return { x: time, y: value * DEFAULT_GRAPH_HEIGHT };
+ }
+}
+
+/**
+ * Create the path segments from given parameters.
+ *
+ * @param {Number} startTime
+ * Starting time of animation.
+ * @param {Number} endTime
+ * Ending time of animation.
+ * @param {Number} minSegmentDuration
+ * Minimum segment duration.
+ * @param {Number} minProgressThreshold
+ * Minimum progress threshold.
+ * @param {Number} resolution
+ * Duration resolution for first time.
+ * @param {Function} getSegment
+ * A function that calculate the graph segment.
+ * @return {Array}
+ * Array of path segment.
+ * e.g.[{x: {Number} time, y: {Number} progress}, ...]
+ */
+function createPathSegments(
+ startTime,
+ endTime,
+ minSegmentDuration,
+ minProgressThreshold,
+ resolution,
+ getSegment
+) {
+ // If the duration is too short, early return.
+ if (endTime - startTime < minSegmentDuration) {
+ return [getSegment(startTime), getSegment(endTime)];
+ }
+
+ // Otherwise, start creating segments.
+ let pathSegments = [];
+
+ // Append the segment for the startTime position.
+ const startTimeSegment = getSegment(startTime);
+ pathSegments.push(startTimeSegment);
+ let previousSegment = startTimeSegment;
+
+ // Split the duration in equal intervals, and iterate over them.
+ // See the definition of DEFAULT_DURATION_RESOLUTION for more information about this.
+ const interval = (endTime - startTime) / resolution;
+ for (let index = 1; index <= resolution; index++) {
+ // Create a segment for this interval.
+ const currentSegment = getSegment(startTime + index * interval);
+
+ // If the distance between the Y coordinate (the animation's progress) of
+ // the previous segment and the Y coordinate of the current segment is too
+ // large, then recurse with a smaller duration to get more details
+ // in the graph.
+ if (Math.abs(currentSegment.y - previousSegment.y) > minProgressThreshold) {
+ // Divide the current interval (excluding start and end bounds
+ // by adding/subtracting BOUND_EXCLUDING_TIME).
+ const nextStartTime = previousSegment.x + BOUND_EXCLUDING_TIME;
+ const nextEndTime = currentSegment.x - BOUND_EXCLUDING_TIME;
+ const segments = createPathSegments(
+ nextStartTime,
+ nextEndTime,
+ minSegmentDuration,
+ minProgressThreshold,
+ DEFAULT_DURATION_RESOLUTION,
+ getSegment
+ );
+ pathSegments = pathSegments.concat(segments);
+ }
+
+ pathSegments.push(currentSegment);
+ previousSegment = currentSegment;
+ }
+
+ return pathSegments;
+}
+
+/**
+ * Create a function which is used as parameter (toPathStringFunc) in constructor
+ * of SummaryGraphHelper.
+ *
+ * @param {Number} endTime
+ * end time of animation
+ * e.g. 200
+ * @param {Number} playbackRate
+ * playback rate of animation
+ * e.g. -1
+ * @return {Function}
+ */
+function createSummaryGraphPathStringFunction(endTime, playbackRate) {
+ return segments => {
+ segments = mapSegmentsToPlaybackRate(segments, endTime, playbackRate);
+ const firstSegment = segments[0];
+ let pathString = `M${firstSegment.x},0 `;
+ pathString += toPathString(segments);
+ const lastSegment = segments[segments.length - 1];
+ pathString += `L${lastSegment.x},0 Z`;
+ return pathString;
+ };
+}
+
+/**
+ * Return preferred duration resolution.
+ * This corresponds to narrow interval keyframe offset.
+ *
+ * @param {Array} keyframes
+ * Array of keyframe.
+ * @return {Number}
+ * Preferred duration resolution.
+ */
+function getPreferredDurationResolution(keyframes) {
+ if (!keyframes) {
+ return DEFAULT_DURATION_RESOLUTION;
+ }
+
+ let durationResolution = DEFAULT_DURATION_RESOLUTION;
+ let previousOffset = 0;
+ for (const keyframe of keyframes) {
+ if (previousOffset && previousOffset != keyframe.offset) {
+ const interval = keyframe.offset - previousOffset;
+ durationResolution = Math.max(
+ durationResolution,
+ Math.ceil(1 / interval)
+ );
+ }
+ previousOffset = keyframe.offset;
+ }
+
+ return durationResolution;
+}
+
+/**
+ * Return preferred progress threshold to render summary graph.
+ *
+ * @param {Object} state
+ * State of animation.
+ * @param {Array} keyframes
+ * Array of keyframe.
+ * @return {float}
+ * Preferred threshold.
+ */
+function getPreferredProgressThreshold(state, keyframes) {
+ const steps = getStepsCount(state.easing);
+ const threshold = Math.min(DEFAULT_MIN_PROGRESS_THRESHOLD, 1 / (steps + 1));
+
+ if (!keyframes) {
+ return threshold;
+ }
+
+ return Math.min(
+ threshold,
+ getPreferredProgressThresholdByKeyframes(keyframes)
+ );
+}
+
+/**
+ * Return preferred progress threshold by keyframes.
+ *
+ * @param {Array} keyframes
+ * Array of keyframe.
+ * @return {float}
+ * Preferred threshold.
+ */
+function getPreferredProgressThresholdByKeyframes(keyframes) {
+ let threshold = DEFAULT_MIN_PROGRESS_THRESHOLD;
+
+ for (let i = 0; i < keyframes.length - 1; i++) {
+ const keyframe = keyframes[i];
+
+ if (!keyframe.easing) {
+ continue;
+ }
+
+ const steps = getStepsCount(keyframe.easing);
+
+ if (steps) {
+ const nextKeyframe = keyframes[i + 1];
+ threshold = Math.min(
+ threshold,
+ (1 / (steps + 1)) * (nextKeyframe.offset - keyframe.offset)
+ );
+ }
+ }
+
+ return threshold;
+}
+
+function getStepsCount(easing) {
+ const stepsFunction = easing.match(/(steps)\((\d+)/);
+ return stepsFunction ? parseInt(stepsFunction[2], 10) : 0;
+}
+
+function mapSegmentsToPlaybackRate(segments, endTime, playbackRate) {
+ if (playbackRate > 0) {
+ return segments;
+ }
+
+ return segments.map(segment => {
+ segment.x = endTime - segment.x;
+ return segment;
+ });
+}
+
+/**
+ * Return path string for 'd' attribute for <path> from given segments.
+ *
+ * @param {Array} segments
+ * e.g. [{ x: 100, y: 0 }, { x: 200, y: 1 }]
+ * @return {String}
+ * Path string.
+ * e.g. "L100,0 L200,1"
+ */
+function toPathString(segments) {
+ let pathString = "";
+ segments.forEach(segment => {
+ pathString += `L${segment.x},${segment.y} `;
+ });
+ return pathString;
+}
+
+exports.createPathSegments = createPathSegments;
+exports.createSummaryGraphPathStringFunction =
+ createSummaryGraphPathStringFunction;
+exports.DEFAULT_DURATION_RESOLUTION = DEFAULT_DURATION_RESOLUTION;
+exports.DEFAULT_EASING_HINT_STROKE_WIDTH = DEFAULT_EASING_HINT_STROKE_WIDTH;
+exports.DEFAULT_GRAPH_HEIGHT = DEFAULT_GRAPH_HEIGHT;
+exports.DEFAULT_KEYFRAMES_GRAPH_DURATION = DEFAULT_KEYFRAMES_GRAPH_DURATION;
+exports.getPreferredProgressThresholdByKeyframes =
+ getPreferredProgressThresholdByKeyframes;
+exports.SummaryGraphHelper = SummaryGraphHelper;
+exports.toPathString = toPathString;