SummaryGraphPath.js (8152B)
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 Component, 9 createFactory, 10 } = require("resource://devtools/client/shared/vendor/react.mjs"); 11 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 12 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 13 const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.mjs"); 14 15 const ComputedTimingPath = createFactory( 16 require("resource://devtools/client/inspector/animation/components/graph/ComputedTimingPath.js") 17 ); 18 const EffectTimingPath = createFactory( 19 require("resource://devtools/client/inspector/animation/components/graph/EffectTimingPath.js") 20 ); 21 const NegativeDelayPath = createFactory( 22 require("resource://devtools/client/inspector/animation/components/graph/NegativeDelayPath.js") 23 ); 24 const NegativeEndDelayPath = createFactory( 25 require("resource://devtools/client/inspector/animation/components/graph/NegativeEndDelayPath.js") 26 ); 27 const { 28 DEFAULT_GRAPH_HEIGHT, 29 } = require("resource://devtools/client/inspector/animation/utils/graph-helper.js"); 30 31 // Minimum opacity for semitransparent fill color for keyframes's easing graph. 32 const MIN_KEYFRAMES_EASING_OPACITY = 0.5; 33 34 class SummaryGraphPath extends Component { 35 static get propTypes() { 36 return { 37 animation: PropTypes.object.isRequired, 38 getAnimatedPropertyMap: PropTypes.object.isRequired, 39 simulateAnimation: PropTypes.func.isRequired, 40 timeScale: PropTypes.object.isRequired, 41 }; 42 } 43 44 constructor(props) { 45 super(props); 46 47 this.state = { 48 // Duration which can display in one pixel. 49 durationPerPixel: 0, 50 // To avoid rendering while the state is updating 51 // since we call an async function in updateState. 52 isStateUpdating: false, 53 // List of keyframe which consists by only offset and easing. 54 keyframesList: [], 55 }; 56 } 57 58 componentDidMount() { 59 // No need to set isStateUpdating state since paint sequence is finish here. 60 this.updateState(this.props); 61 } 62 63 // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 64 UNSAFE_componentWillReceiveProps(nextProps) { 65 this.setState({ isStateUpdating: true }); 66 this.updateState(nextProps); 67 } 68 69 shouldComponentUpdate(nextProps, nextState) { 70 return !nextState.isStateUpdating; 71 } 72 73 /** 74 * Return animatable keyframes list which has only offset and easing. 75 * Also, this method remove duplicate keyframes. 76 * For example, if the given animatedPropertyMap is, 77 * [ 78 * { 79 * key: "color", 80 * values: [ 81 * { 82 * offset: 0, 83 * easing: "ease", 84 * value: "rgb(255, 0, 0)", 85 * }, 86 * { 87 * offset: 1, 88 * value: "rgb(0, 255, 0)", 89 * }, 90 * ], 91 * }, 92 * { 93 * key: "opacity", 94 * values: [ 95 * { 96 * offset: 0, 97 * easing: "ease", 98 * value: 0, 99 * }, 100 * { 101 * offset: 1, 102 * value: 1, 103 * }, 104 * ], 105 * }, 106 * ] 107 * 108 * then this method returns, 109 * [ 110 * [ 111 * { 112 * offset: 0, 113 * easing: "ease", 114 * }, 115 * { 116 * offset: 1, 117 * }, 118 * ], 119 * ] 120 * 121 * @param {Map} animated property map 122 * which can get form getAnimatedPropertyMap in animation.js 123 * @return {Array} list of keyframes which has only easing and offset. 124 */ 125 getOffsetAndEasingOnlyKeyframes(animatedPropertyMap) { 126 return [...animatedPropertyMap.values()] 127 .filter((keyframes1, i, self) => { 128 return ( 129 i !== 130 self.findIndex((keyframes2, j) => { 131 return this.isOffsetAndEasingKeyframesEqual(keyframes1, keyframes2) 132 ? j 133 : -1; 134 }) 135 ); 136 }) 137 .map(keyframes => { 138 return keyframes.map(keyframe => { 139 return { easing: keyframe.easing, offset: keyframe.offset }; 140 }); 141 }); 142 } 143 144 /** 145 * Return true if given keyframes have same length, offset and easing. 146 * 147 * @param {Array} keyframes1 148 * @param {Array} keyframes2 149 * @return {boolean} true: equals 150 */ 151 isOffsetAndEasingKeyframesEqual(keyframes1, keyframes2) { 152 if (keyframes1.length !== keyframes2.length) { 153 return false; 154 } 155 156 for (let i = 0; i < keyframes1.length; i++) { 157 const keyframe1 = keyframes1[i]; 158 const keyframe2 = keyframes2[i]; 159 160 if ( 161 keyframe1.offset !== keyframe2.offset || 162 keyframe1.easing !== keyframe2.easing 163 ) { 164 return false; 165 } 166 } 167 168 return true; 169 } 170 171 updateState(props) { 172 const { animation, getAnimatedPropertyMap, timeScale } = props; 173 174 let animatedPropertyMap = null; 175 let thisEl = null; 176 177 try { 178 animatedPropertyMap = getAnimatedPropertyMap(animation); 179 thisEl = ReactDOM.findDOMNode(this); 180 } catch (e) { 181 // Expected if we've already been destroyed or other node have been selected 182 // in the meantime. 183 console.error(e); 184 return; 185 } 186 187 const keyframesList = 188 this.getOffsetAndEasingOnlyKeyframes(animatedPropertyMap); 189 const totalDuration = 190 timeScale.getDuration() * Math.abs(animation.state.playbackRate); 191 const durationPerPixel = totalDuration / thisEl.parentNode.clientWidth; 192 193 this.setState({ 194 durationPerPixel, 195 isStateUpdating: false, 196 keyframesList, 197 }); 198 } 199 200 render() { 201 const { durationPerPixel, keyframesList } = this.state; 202 const { animation, simulateAnimation, timeScale } = this.props; 203 204 if (!durationPerPixel || !animation.state.type) { 205 // Undefined animation.state.type means that the animation had been removed already. 206 // Even if the animation was removed, we still need the empty svg since the 207 // component might be re-used. 208 return dom.svg(); 209 } 210 211 const { playbackRate } = animation.state; 212 const { createdTime } = animation.state.absoluteValues; 213 const absPlaybackRate = Math.abs(playbackRate); 214 215 // Absorb the playbackRate in viewBox of SVG and offset of child path elements 216 // in order to each graph path components can draw without considering to the 217 // playbackRate. 218 const offset = createdTime * absPlaybackRate; 219 const startTime = timeScale.minStartTime * absPlaybackRate; 220 const totalDuration = timeScale.getDuration() * absPlaybackRate; 221 const opacity = Math.max( 222 1 / keyframesList.length, 223 MIN_KEYFRAMES_EASING_OPACITY 224 ); 225 226 return dom.svg( 227 { 228 className: "animation-summary-graph-path", 229 preserveAspectRatio: "none", 230 viewBox: 231 `${startTime} -${DEFAULT_GRAPH_HEIGHT} ` + 232 `${totalDuration} ${DEFAULT_GRAPH_HEIGHT}`, 233 }, 234 keyframesList.map(keyframes => 235 ComputedTimingPath({ 236 animation, 237 durationPerPixel, 238 keyframes, 239 offset, 240 opacity, 241 simulateAnimation, 242 totalDuration, 243 }) 244 ), 245 animation.state.easing !== "linear" 246 ? EffectTimingPath({ 247 animation, 248 durationPerPixel, 249 offset, 250 simulateAnimation, 251 totalDuration, 252 }) 253 : null, 254 animation.state.delay < 0 255 ? keyframesList.map(keyframes => { 256 return NegativeDelayPath({ 257 animation, 258 durationPerPixel, 259 keyframes, 260 offset, 261 simulateAnimation, 262 totalDuration, 263 }); 264 }) 265 : null, 266 animation.state.iterationCount && animation.state.endDelay < 0 267 ? keyframesList.map(keyframes => { 268 return NegativeEndDelayPath({ 269 animation, 270 durationPerPixel, 271 keyframes, 272 offset, 273 simulateAnimation, 274 totalDuration, 275 }); 276 }) 277 : null 278 ); 279 } 280 } 281 282 module.exports = SummaryGraphPath;