diff options
Diffstat (limited to 'devtools/client/inspector/animation/utils/graph-helper.js')
-rw-r--r-- | devtools/client/inspector/animation/utils/graph-helper.js | 332 |
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; |