summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/animation/utils
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/animation/utils/graph-helper.js332
-rw-r--r--devtools/client/inspector/animation/utils/l10n.js46
-rw-r--r--devtools/client/inspector/animation/utils/moz.build10
-rw-r--r--devtools/client/inspector/animation/utils/timescale.js145
-rw-r--r--devtools/client/inspector/animation/utils/utils.js70
5 files changed, 603 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;
diff --git a/devtools/client/inspector/animation/utils/l10n.js b/devtools/client/inspector/animation/utils/l10n.js
new file mode 100644
index 0000000000..6fffa98b65
--- /dev/null
+++ b/devtools/client/inspector/animation/utils/l10n.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 { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/animationinspector.properties"
+);
+const INSPECTOR_L10N = new LocalizationHelper(
+ "devtools/client/locales/inspector.properties"
+);
+
+/**
+ * Get a formatted title for this animation. This will be either:
+ * "%S", "%S : CSS Transition", "%S : CSS Animation",
+ * "%S : Script Animation", or "Script Animation", depending
+ * if the server provides the type, what type it is and if the animation
+ * has a name.
+ *
+ * @param {Object} state
+ */
+function getFormattedTitle(state) {
+ // Older servers don't send a type, and only know about
+ // CSSAnimations and CSSTransitions, so it's safe to use
+ // just the name.
+ if (!state.type) {
+ return state.name;
+ }
+
+ // Script-generated animations may not have a name.
+ if (state.type === "scriptanimation" && !state.name) {
+ return L10N.getStr("timeline.scriptanimation.unnamedLabel");
+ }
+
+ return L10N.getFormatStr(`timeline.${state.type}.nameLabel`, state.name);
+}
+
+module.exports = {
+ getFormatStr: (...args) => L10N.getFormatStr(...args),
+ getFormattedTitle,
+ getInspectorStr: (...args) => INSPECTOR_L10N.getStr(...args),
+ getStr: (...args) => L10N.getStr(...args),
+ numberWithDecimals: (...args) => L10N.numberWithDecimals(...args),
+};
diff --git a/devtools/client/inspector/animation/utils/moz.build b/devtools/client/inspector/animation/utils/moz.build
new file mode 100644
index 0000000000..ae73627a29
--- /dev/null
+++ b/devtools/client/inspector/animation/utils/moz.build
@@ -0,0 +1,10 @@
+# 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(
+ "graph-helper.js",
+ "l10n.js",
+ "timescale.js",
+ "utils.js",
+)
diff --git a/devtools/client/inspector/animation/utils/timescale.js b/devtools/client/inspector/animation/utils/timescale.js
new file mode 100644
index 0000000000..a831f1267e
--- /dev/null
+++ b/devtools/client/inspector/animation/utils/timescale.js
@@ -0,0 +1,145 @@
+/* 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 {
+ getFormatStr,
+} = require("resource://devtools/client/inspector/animation/utils/l10n.js");
+
+// If total duration for all animations is eqaul to or less than
+// TIME_FORMAT_MAX_DURATION_IN_MS, the text which expresses time is in milliseconds,
+// and seconds otherwise. Use in formatTime function.
+const TIME_FORMAT_MAX_DURATION_IN_MS = 4000;
+
+/**
+ * TimeScale object holds the total duration, start time and end time and zero position
+ * time information for all animations which should be displayed, and is used to calculate
+ * the displayed area for each animation.
+ */
+class TimeScale {
+ constructor(animations) {
+ let resultCurrentTime = -Number.MAX_VALUE;
+ let resultMinStartTime = Infinity;
+ let resultMaxEndTime = 0;
+ let resultZeroPositionTime = 0;
+
+ for (const animation of animations) {
+ const {
+ currentTime,
+ currentTimeAtCreated,
+ delay,
+ endTime,
+ startTimeAtCreated,
+ } = animation.state.absoluteValues;
+ let { startTime } = animation.state.absoluteValues;
+
+ const negativeDelay = Math.min(delay, 0);
+ let zeroPositionTime = 0;
+
+ // To shift the zero position time is the following two patterns.
+ // * Animation has negative current time which is smaller than negative dleay.
+ // * Animation has negative delay.
+ // Furthermore, we should override the zero position time if we will need to
+ // expand the duration due to this negative current time or negative delay of
+ // this target animation.
+ if (currentTimeAtCreated < negativeDelay) {
+ startTime = startTimeAtCreated;
+ zeroPositionTime = Math.abs(currentTimeAtCreated);
+ } else if (negativeDelay < 0) {
+ zeroPositionTime = Math.abs(negativeDelay);
+ }
+
+ if (startTime < resultMinStartTime) {
+ resultMinStartTime = startTime;
+ // Override the previous calculated zero position only if the duration will be
+ // expanded.
+ resultZeroPositionTime = zeroPositionTime;
+ } else {
+ resultZeroPositionTime = Math.max(
+ resultZeroPositionTime,
+ zeroPositionTime
+ );
+ }
+
+ resultMaxEndTime = Math.max(resultMaxEndTime, endTime);
+ resultCurrentTime = Math.max(resultCurrentTime, currentTime);
+ }
+
+ this.minStartTime = resultMinStartTime;
+ this.maxEndTime = resultMaxEndTime;
+ this.currentTime = resultCurrentTime;
+ this.zeroPositionTime = resultZeroPositionTime;
+ }
+
+ /**
+ * Convert a distance in % to a time, in the current time scale. The time
+ * will be relative to the zero position time.
+ * i.e., If zeroPositionTime will be negative and specified time is shorter
+ * than the absolute value of zero position time, relative time will be
+ * negative time.
+ *
+ * @param {Number} distance
+ * @return {Number}
+ */
+ distanceToRelativeTime(distance) {
+ return (this.getDuration() * distance) / 100 - this.zeroPositionTime;
+ }
+
+ /**
+ * Depending on the time scale, format the given time as milliseconds or
+ * seconds.
+ *
+ * @param {Number} time
+ * @return {String} The formatted time string.
+ */
+ formatTime(time) {
+ // Ignore negative zero
+ if (Math.abs(time) < 1 / 1000) {
+ time = 0.0;
+ }
+
+ // Format in milliseconds if the total duration is short enough.
+ if (this.getDuration() <= TIME_FORMAT_MAX_DURATION_IN_MS) {
+ return getFormatStr("timeline.timeGraduationLabel", time.toFixed(0));
+ }
+
+ // Otherwise format in seconds.
+ return getFormatStr("player.timeLabel", (time / 1000).toFixed(1));
+ }
+
+ /**
+ * Return entire animations duration.
+ *
+ * @return {Number} duration
+ */
+ getDuration() {
+ return this.maxEndTime - this.minStartTime;
+ }
+
+ /**
+ * Return current time of this time scale represents.
+ *
+ * @return {Number}
+ */
+ getCurrentTime() {
+ return this.currentTime - this.minStartTime;
+ }
+
+ /**
+ * Return end time of given animation.
+ * This time does not include playbackRate and cratedTime.
+ * Also, if the animation has infinite iterations, this returns Infinity.
+ *
+ * @param {Object} animation
+ * @return {Numbber} end time
+ */
+ getEndTime({ state }) {
+ return state.iterationCount
+ ? state.delay + state.duration * state.iterationCount + state.endDelay
+ : Infinity;
+ }
+}
+
+module.exports = TimeScale;
diff --git a/devtools/client/inspector/animation/utils/utils.js b/devtools/client/inspector/animation/utils/utils.js
new file mode 100644
index 0000000000..9040c27213
--- /dev/null
+++ b/devtools/client/inspector/animation/utils/utils.js
@@ -0,0 +1,70 @@
+/* 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";
+
+// The maximum number of times we can loop before we find the optimal time interval in the
+// timeline graph.
+const OPTIMAL_TIME_INTERVAL_MAX_ITERS = 100;
+// Time graduations should be multiple of one of these number.
+const OPTIMAL_TIME_INTERVAL_MULTIPLES = [1, 2.5, 5];
+
+/**
+ * Find the optimal interval between time graduations in the animation timeline
+ * graph based on a minimum time interval.
+ *
+ * @param {Number} minTimeInterval
+ * Minimum time in ms in one interval
+ * @return {Number} The optimal interval time in ms
+ */
+function findOptimalTimeInterval(minTimeInterval) {
+ if (!minTimeInterval) {
+ return 0;
+ }
+
+ let numIters = 0;
+ let multiplier = 1;
+ let interval;
+
+ while (true) {
+ for (let i = 0; i < OPTIMAL_TIME_INTERVAL_MULTIPLES.length; i++) {
+ interval = OPTIMAL_TIME_INTERVAL_MULTIPLES[i] * multiplier;
+
+ if (minTimeInterval <= interval) {
+ return interval;
+ }
+ }
+
+ if (++numIters > OPTIMAL_TIME_INTERVAL_MAX_ITERS) {
+ return interval;
+ }
+
+ multiplier *= 10;
+ }
+}
+
+/**
+ * Check whether or not the given list of animations has an iteration count of infinite.
+ *
+ * @param {Array} animations.
+ * @return {Boolean} true if there is an animation in the list of animations
+ * whose animation iteration count is infinite.
+ */
+function hasAnimationIterationCountInfinite(animations) {
+ return animations.some(({ state }) => !state.iterationCount);
+}
+
+/**
+ * Check wether the animations are running at least one.
+ *
+ * @param {Array} animations.
+ * @return {Boolean} true: running
+ */
+function hasRunningAnimation(animations) {
+ return animations.some(({ state }) => state.playState === "running");
+}
+
+exports.findOptimalTimeInterval = findOptimalTimeInterval;
+exports.hasAnimationIterationCountInfinite = hasAnimationIterationCountInfinite;
+exports.hasRunningAnimation = hasRunningAnimation;