ComputedStylePath.js (6710B)
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 const { 8 PureComponent, 9 } = require("resource://devtools/client/shared/vendor/react.mjs"); 10 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 11 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 12 13 const { 14 createPathSegments, 15 DEFAULT_DURATION_RESOLUTION, 16 getPreferredProgressThresholdByKeyframes, 17 toPathString, 18 } = require("resource://devtools/client/inspector/animation/utils/graph-helper.js"); 19 20 /** 21 * This class is an abstraction for computed style path of keyframes. 22 * Subclass of this should implement the following methods: 23 * 24 * getPropertyName() 25 * Returns property name which will be animated. 26 * 27 * @return {string} 28 * e.g. opacity 29 * 30 * getPropertyValue(keyframe) 31 * Returns value which uses as animated keyframe value from given parameter. 32 * 33 * @param {object} keyframe 34 * @return {String||Number} 35 * e.g. 0 36 * 37 * toSegmentValue(computedStyle) 38 * Convert computed style to segment value of graph. 39 * 40 * @param {String||Number} 41 * e.g. 0 42 * @return {number} 43 * e.g. 0 (should be 0 - 1.0) 44 */ 45 class ComputedStylePath extends PureComponent { 46 static get propTypes() { 47 return { 48 componentWidth: PropTypes.number.isRequired, 49 easingHintStrokeWidth: PropTypes.number.isRequired, 50 graphHeight: PropTypes.number.isRequired, 51 keyframes: PropTypes.array.isRequired, 52 simulateAnimation: PropTypes.func.isRequired, 53 totalDuration: PropTypes.number.isRequired, 54 }; 55 } 56 57 /** 58 * Return an array containing the path segments between the given start and 59 * end keyframe values. 60 * 61 * @param {object} startKeyframe 62 * Starting keyframe. 63 * @param {object} endKeyframe 64 * Ending keyframe. 65 * @return {Array} 66 * Array of path segment. 67 * [{x: {Number} time, y: {Number} segment value}, ...] 68 */ 69 getPathSegments(startKeyframe, endKeyframe) { 70 const { componentWidth, simulateAnimation, totalDuration } = this.props; 71 72 const propertyName = this.getPropertyName(); 73 const offsetDistance = endKeyframe.offset - startKeyframe.offset; 74 const duration = offsetDistance * totalDuration; 75 76 const keyframes = [startKeyframe, endKeyframe].map((keyframe, index) => { 77 return { 78 offset: index, 79 easing: keyframe.easing, 80 [getJsPropertyName(propertyName)]: this.getPropertyValue(keyframe), 81 }; 82 }); 83 const effect = { 84 duration, 85 fill: "forwards", 86 }; 87 88 const simulatedAnimation = simulateAnimation(keyframes, effect, true); 89 90 if (!simulatedAnimation) { 91 return null; 92 } 93 94 const simulatedElement = simulatedAnimation.effect.target; 95 const win = simulatedElement.ownerGlobal; 96 const threshold = getPreferredProgressThresholdByKeyframes(keyframes); 97 98 const getSegment = time => { 99 simulatedAnimation.currentTime = time; 100 const computedStyle = win 101 .getComputedStyle(simulatedElement) 102 .getPropertyValue(propertyName); 103 104 return { 105 computedStyle, 106 x: time, 107 y: this.toSegmentValue(computedStyle), 108 }; 109 }; 110 111 const segments = createPathSegments( 112 0, 113 duration, 114 duration / componentWidth, 115 threshold, 116 DEFAULT_DURATION_RESOLUTION, 117 getSegment 118 ); 119 const offset = startKeyframe.offset * totalDuration; 120 121 for (const segment of segments) { 122 segment.x += offset; 123 } 124 125 return segments; 126 } 127 128 /** 129 * Render easing hint from given path segments. 130 * 131 * @param {Array} segments 132 * Path segments. 133 * @return {Element} 134 * Element which represents easing hint. 135 */ 136 renderEasingHint(segments) { 137 const { easingHintStrokeWidth, keyframes, totalDuration } = this.props; 138 139 const hints = []; 140 141 for (let i = 0, indexOfSegments = 0; i < keyframes.length - 1; i++) { 142 const startKeyframe = keyframes[i]; 143 const endKeyframe = keyframes[i + 1]; 144 const endTime = endKeyframe.offset * totalDuration; 145 const hintSegments = []; 146 147 for (; indexOfSegments < segments.length; indexOfSegments++) { 148 const segment = segments[indexOfSegments]; 149 hintSegments.push(segment); 150 151 if (startKeyframe.offset === endKeyframe.offset) { 152 hintSegments.push(segments[++indexOfSegments]); 153 break; 154 } else if (segment.x === endTime) { 155 break; 156 } 157 } 158 159 const g = dom.g( 160 { 161 className: "hint", 162 }, 163 dom.title({}, startKeyframe.easing), 164 dom.path({ 165 d: 166 `M${hintSegments[0].x},${hintSegments[0].y} ` + 167 toPathString(hintSegments), 168 style: { 169 "stroke-width": easingHintStrokeWidth, 170 }, 171 }) 172 ); 173 174 hints.push(g); 175 } 176 177 return hints; 178 } 179 180 /** 181 * Render graph. This method returns React dom. 182 * 183 * @return {Element} 184 */ 185 renderGraph() { 186 const { keyframes } = this.props; 187 188 const segments = []; 189 190 for (let i = 0; i < keyframes.length - 1; i++) { 191 const startKeyframe = keyframes[i]; 192 const endKeyframe = keyframes[i + 1]; 193 const keyframesSegments = this.getPathSegments( 194 startKeyframe, 195 endKeyframe 196 ); 197 198 if (!keyframesSegments) { 199 return null; 200 } 201 202 segments.push(...keyframesSegments); 203 } 204 205 return [this.renderPathSegments(segments), this.renderEasingHint(segments)]; 206 } 207 208 /** 209 * Return react dom fron given path segments. 210 * 211 * @param {Array} segments 212 * @param {object} style 213 * @return {Element} 214 */ 215 renderPathSegments(segments, style) { 216 const { graphHeight } = this.props; 217 218 for (const segment of segments) { 219 segment.y *= graphHeight; 220 } 221 222 let d = `M${segments[0].x},0 `; 223 d += toPathString(segments); 224 d += `L${segments[segments.length - 1].x},0 Z`; 225 226 return dom.path({ d, style }); 227 } 228 } 229 230 /** 231 * Convert given CSS property name to JavaScript CSS name. 232 * 233 * @param {string} cssPropertyName 234 * CSS property name (e.g. background-color). 235 * @return {string} 236 * JavaScript CSS property name (e.g. backgroundColor). 237 */ 238 function getJsPropertyName(cssPropertyName) { 239 if (cssPropertyName == "float") { 240 return "cssFloat"; 241 } 242 // https://drafts.csswg.org/cssom/#css-property-to-idl-attribute 243 return cssPropertyName.replace(/-([a-z])/gi, (str, group) => { 244 return group.toUpperCase(); 245 }); 246 } 247 248 module.exports = ComputedStylePath;