Source code

Revision control

Copy as Markdown

Other Tools

/* 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;