1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
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;
|