graph-helper.js (10232B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 // BOUND_EXCLUDING_TIME should be less than 1ms and is used to exclude start 8 // and end bounds when dividing duration in createPathSegments. 9 const BOUND_EXCLUDING_TIME = 0.001; 10 // We define default graph height since if the height of viewport in SVG is 11 // too small (e.g. 1), vector-effect may not be able to calculate correctly. 12 const DEFAULT_GRAPH_HEIGHT = 100; 13 // Default animation duration for keyframes graph. 14 const DEFAULT_KEYFRAMES_GRAPH_DURATION = 1000; 15 // DEFAULT_MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1. 16 const DEFAULT_MIN_PROGRESS_THRESHOLD = 0.1; 17 // In the createPathSegments function, an animation duration is divided by 18 // DEFAULT_DURATION_RESOLUTION in order to draw the way the animation progresses. 19 // But depending on the timing-function, we may be not able to make the graph 20 // smoothly progress if this resolution is not high enough. 21 // So, if the difference of animation progress between 2 divisions is more than 22 // DEFAULT_MIN_PROGRESS_THRESHOLD * DEFAULT_GRAPH_HEIGHT, then createPathSegments 23 // re-divides by DEFAULT_DURATION_RESOLUTION. 24 // DEFAULT_DURATION_RESOLUTION shoud be integer and more than 2. 25 const DEFAULT_DURATION_RESOLUTION = 4; 26 // Stroke width for easing hint. 27 const DEFAULT_EASING_HINT_STROKE_WIDTH = 5; 28 29 /** 30 * The helper class for creating summary graph. 31 */ 32 class SummaryGraphHelper { 33 /** 34 * Constructor. 35 * 36 * @param {object} state 37 * State of animation. 38 * @param {Array} keyframes 39 * Array of keyframe. 40 * @param {number} totalDuration 41 * Total displayable duration. 42 * @param {number} minSegmentDuration 43 * Minimum segment duration. 44 * @param {Function} getValueFunc 45 * Which returns graph value of given time. 46 * The function should return a number value between 0 - 1. 47 * e.g. time => { return 1.0 }; 48 * @param {Function} toPathStringFunc 49 * Which returns a path string for 'd' attribute for <path> from given segments. 50 */ 51 constructor( 52 state, 53 keyframes, 54 totalDuration, 55 minSegmentDuration, 56 getValueFunc, 57 toPathStringFunc 58 ) { 59 this.totalDuration = totalDuration; 60 this.minSegmentDuration = minSegmentDuration; 61 this.minProgressThreshold = 62 getPreferredProgressThreshold(state, keyframes) * DEFAULT_GRAPH_HEIGHT; 63 this.durationResolution = getPreferredDurationResolution(keyframes); 64 this.getValue = getValueFunc; 65 this.toPathString = toPathStringFunc; 66 67 this.getSegment = this.getSegment.bind(this); 68 } 69 70 /** 71 * Create the path segments from given parameters. 72 * 73 * @param {number} startTime 74 * Starting time of animation. 75 * @param {number} endTime 76 * Ending time of animation. 77 * @return {Array} 78 * Array of path segment. 79 * e.g.[{x: {Number} time, y: {Number} progress}, ...] 80 */ 81 createPathSegments(startTime, endTime) { 82 return createPathSegments( 83 startTime, 84 endTime, 85 this.minSegmentDuration, 86 this.minProgressThreshold, 87 this.durationResolution, 88 this.getSegment 89 ); 90 } 91 92 /** 93 * Return a coordinate as a graph segment at given time. 94 * 95 * @param {number} time 96 * @return {object} 97 * { x: Number, y: Number } 98 */ 99 getSegment(time) { 100 const value = this.getValue(time); 101 return { x: time, y: value * DEFAULT_GRAPH_HEIGHT }; 102 } 103 } 104 105 /** 106 * Create the path segments from given parameters. 107 * 108 * @param {number} startTime 109 * Starting time of animation. 110 * @param {number} endTime 111 * Ending time of animation. 112 * @param {number} minSegmentDuration 113 * Minimum segment duration. 114 * @param {number} minProgressThreshold 115 * Minimum progress threshold. 116 * @param {number} resolution 117 * Duration resolution for first time. 118 * @param {Function} getSegment 119 * A function that calculate the graph segment. 120 * @return {Array} 121 * Array of path segment. 122 * e.g.[{x: {Number} time, y: {Number} progress}, ...] 123 */ 124 function createPathSegments( 125 startTime, 126 endTime, 127 minSegmentDuration, 128 minProgressThreshold, 129 resolution, 130 getSegment 131 ) { 132 // If the duration is too short, early return. 133 if (endTime - startTime < minSegmentDuration) { 134 return [getSegment(startTime), getSegment(endTime)]; 135 } 136 137 // Otherwise, start creating segments. 138 let pathSegments = []; 139 140 // Append the segment for the startTime position. 141 const startTimeSegment = getSegment(startTime); 142 pathSegments.push(startTimeSegment); 143 let previousSegment = startTimeSegment; 144 145 // Split the duration in equal intervals, and iterate over them. 146 // See the definition of DEFAULT_DURATION_RESOLUTION for more information about this. 147 const interval = (endTime - startTime) / resolution; 148 for (let index = 1; index <= resolution; index++) { 149 // Create a segment for this interval. 150 const currentSegment = getSegment(startTime + index * interval); 151 152 // If the distance between the Y coordinate (the animation's progress) of 153 // the previous segment and the Y coordinate of the current segment is too 154 // large, then recurse with a smaller duration to get more details 155 // in the graph. 156 if (Math.abs(currentSegment.y - previousSegment.y) > minProgressThreshold) { 157 // Divide the current interval (excluding start and end bounds 158 // by adding/subtracting BOUND_EXCLUDING_TIME). 159 const nextStartTime = previousSegment.x + BOUND_EXCLUDING_TIME; 160 const nextEndTime = currentSegment.x - BOUND_EXCLUDING_TIME; 161 const segments = createPathSegments( 162 nextStartTime, 163 nextEndTime, 164 minSegmentDuration, 165 minProgressThreshold, 166 DEFAULT_DURATION_RESOLUTION, 167 getSegment 168 ); 169 pathSegments = pathSegments.concat(segments); 170 } 171 172 pathSegments.push(currentSegment); 173 previousSegment = currentSegment; 174 } 175 176 return pathSegments; 177 } 178 179 /** 180 * Create a function which is used as parameter (toPathStringFunc) in constructor 181 * of SummaryGraphHelper. 182 * 183 * @param {number} endTime 184 * end time of animation 185 * e.g. 200 186 * @param {number} playbackRate 187 * playback rate of animation 188 * e.g. -1 189 * @return {Function} 190 */ 191 function createSummaryGraphPathStringFunction(endTime, playbackRate) { 192 return segments => { 193 segments = mapSegmentsToPlaybackRate(segments, endTime, playbackRate); 194 const firstSegment = segments[0]; 195 let pathString = `M${firstSegment.x},0 `; 196 pathString += toPathString(segments); 197 const lastSegment = segments[segments.length - 1]; 198 pathString += `L${lastSegment.x},0 Z`; 199 return pathString; 200 }; 201 } 202 203 /** 204 * Return preferred duration resolution. 205 * This corresponds to narrow interval keyframe offset. 206 * 207 * @param {Array} keyframes 208 * Array of keyframe. 209 * @return {number} 210 * Preferred duration resolution. 211 */ 212 function getPreferredDurationResolution(keyframes) { 213 if (!keyframes) { 214 return DEFAULT_DURATION_RESOLUTION; 215 } 216 217 let durationResolution = DEFAULT_DURATION_RESOLUTION; 218 let previousOffset = 0; 219 for (const keyframe of keyframes) { 220 if (previousOffset && previousOffset != keyframe.offset) { 221 const interval = keyframe.offset - previousOffset; 222 durationResolution = Math.max( 223 durationResolution, 224 Math.ceil(1 / interval) 225 ); 226 } 227 previousOffset = keyframe.offset; 228 } 229 230 return durationResolution; 231 } 232 233 /** 234 * Return preferred progress threshold to render summary graph. 235 * 236 * @param {object} state 237 * State of animation. 238 * @param {Array} keyframes 239 * Array of keyframe. 240 * @return {float} 241 * Preferred threshold. 242 */ 243 function getPreferredProgressThreshold(state, keyframes) { 244 const steps = getStepsCount(state.easing); 245 const threshold = Math.min(DEFAULT_MIN_PROGRESS_THRESHOLD, 1 / (steps + 1)); 246 247 if (!keyframes) { 248 return threshold; 249 } 250 251 return Math.min( 252 threshold, 253 getPreferredProgressThresholdByKeyframes(keyframes) 254 ); 255 } 256 257 /** 258 * Return preferred progress threshold by keyframes. 259 * 260 * @param {Array} keyframes 261 * Array of keyframe. 262 * @return {float} 263 * Preferred threshold. 264 */ 265 function getPreferredProgressThresholdByKeyframes(keyframes) { 266 let threshold = DEFAULT_MIN_PROGRESS_THRESHOLD; 267 268 for (let i = 0; i < keyframes.length - 1; i++) { 269 const keyframe = keyframes[i]; 270 271 if (!keyframe.easing) { 272 continue; 273 } 274 275 const steps = getStepsCount(keyframe.easing); 276 277 if (steps) { 278 const nextKeyframe = keyframes[i + 1]; 279 threshold = Math.min( 280 threshold, 281 (1 / (steps + 1)) * (nextKeyframe.offset - keyframe.offset) 282 ); 283 } 284 } 285 286 return threshold; 287 } 288 289 function getStepsCount(easing) { 290 const stepsFunction = easing.match(/(steps)\((\d+)/); 291 return stepsFunction ? parseInt(stepsFunction[2], 10) : 0; 292 } 293 294 function mapSegmentsToPlaybackRate(segments, endTime, playbackRate) { 295 if (playbackRate > 0) { 296 return segments; 297 } 298 299 return segments.map(segment => { 300 segment.x = endTime - segment.x; 301 return segment; 302 }); 303 } 304 305 /** 306 * Return path string for 'd' attribute for <path> from given segments. 307 * 308 * @param {Array} segments 309 * e.g. [{ x: 100, y: 0 }, { x: 200, y: 1 }] 310 * @return {string} 311 * Path string. 312 * e.g. "L100,0 L200,1" 313 */ 314 function toPathString(segments) { 315 let pathString = ""; 316 segments.forEach(segment => { 317 pathString += `L${segment.x},${segment.y} `; 318 }); 319 return pathString; 320 } 321 322 exports.createPathSegments = createPathSegments; 323 exports.createSummaryGraphPathStringFunction = 324 createSummaryGraphPathStringFunction; 325 exports.DEFAULT_DURATION_RESOLUTION = DEFAULT_DURATION_RESOLUTION; 326 exports.DEFAULT_EASING_HINT_STROKE_WIDTH = DEFAULT_EASING_HINT_STROKE_WIDTH; 327 exports.DEFAULT_GRAPH_HEIGHT = DEFAULT_GRAPH_HEIGHT; 328 exports.DEFAULT_KEYFRAMES_GRAPH_DURATION = DEFAULT_KEYFRAMES_GRAPH_DURATION; 329 exports.getPreferredProgressThresholdByKeyframes = 330 getPreferredProgressThresholdByKeyframes; 331 exports.SummaryGraphHelper = SummaryGraphHelper; 332 exports.toPathString = toPathString;