332 lines
10 KiB
JavaScript
332 lines
10 KiB
JavaScript
/* 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;
|