AnimationListContainer.js (7202B)
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 createFactory, 9 createRef, 10 PureComponent, 11 } = require("resource://devtools/client/shared/vendor/react.mjs"); 12 const { 13 connect, 14 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 15 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 16 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 17 18 const AnimationList = createFactory( 19 require("resource://devtools/client/inspector/animation/components/AnimationList.js") 20 ); 21 const CurrentTimeScrubber = createFactory( 22 require("resource://devtools/client/inspector/animation/components/CurrentTimeScrubber.js") 23 ); 24 const ProgressInspectionPanel = createFactory( 25 require("resource://devtools/client/inspector/animation/components/ProgressInspectionPanel.js") 26 ); 27 28 const { 29 findOptimalTimeInterval, 30 } = require("resource://devtools/client/inspector/animation/utils/utils.js"); 31 const { 32 getStr, 33 } = require("resource://devtools/client/inspector/animation/utils/l10n.js"); 34 const { throttle } = require("resource://devtools/shared/throttle.js"); 35 36 // The minimum spacing between 2 time graduation headers in the timeline (px). 37 const TIME_GRADUATION_MIN_SPACING = 40; 38 39 class AnimationListContainer extends PureComponent { 40 static get propTypes() { 41 return { 42 addAnimationsCurrentTimeListener: PropTypes.func.isRequired, 43 animations: PropTypes.arrayOf(PropTypes.object).isRequired, 44 direction: PropTypes.string.isRequired, 45 dispatch: PropTypes.func.isRequired, 46 getAnimatedPropertyMap: PropTypes.func.isRequired, 47 getNodeFromActor: PropTypes.func.isRequired, 48 removeAnimationsCurrentTimeListener: PropTypes.func.isRequired, 49 selectAnimation: PropTypes.func.isRequired, 50 setAnimationsCurrentTime: PropTypes.func.isRequired, 51 setHighlightedNode: PropTypes.func.isRequired, 52 setSelectedNode: PropTypes.func.isRequired, 53 sidebarWidth: PropTypes.number.isRequired, 54 simulateAnimation: PropTypes.func.isRequired, 55 timeScale: PropTypes.object.isRequired, 56 }; 57 } 58 59 constructor(props) { 60 super(props); 61 62 this._ref = createRef(); 63 64 this.updateDisplayableRange = throttle( 65 this.updateDisplayableRange, 66 100, 67 this 68 ); 69 70 this.state = { 71 // tick labels and lines on the progress inspection panel 72 ticks: [], 73 // Displayable range. 74 displayableRange: { startIndex: 0, endIndex: 0 }, 75 }; 76 } 77 78 componentDidMount() { 79 this.updateTicks(this.props); 80 81 const current = this._ref.current; 82 this._inspectionPanelEl = current.querySelector( 83 ".progress-inspection-panel" 84 ); 85 this._inspectionPanelEl.addEventListener("scroll", () => { 86 this.updateDisplayableRange(); 87 }); 88 89 this._animationListEl = current.querySelector(".animation-list"); 90 const resizeObserver = new current.ownerGlobal.ResizeObserver(() => { 91 this.updateDisplayableRange(); 92 }); 93 resizeObserver.observe(this._animationListEl); 94 95 const animationItemEl = current.querySelector(".animation-item"); 96 this._itemHeight = animationItemEl.offsetHeight; 97 98 this.updateDisplayableRange(); 99 } 100 101 componentDidUpdate(prevProps) { 102 const { timeScale, sidebarWidth } = this.props; 103 104 if ( 105 timeScale.getDuration() !== prevProps.timeScale.getDuration() || 106 timeScale.zeroPositionTime !== prevProps.timeScale.zeroPositionTime || 107 sidebarWidth !== prevProps.sidebarWidth 108 ) { 109 this.updateTicks(this.props); 110 } 111 } 112 113 /** 114 * Since it takes too much time if we render all of animation graphs, 115 * we restrict to render the items that are not in displaying area. 116 * This function calculates the displayable item range. 117 */ 118 updateDisplayableRange() { 119 const count = 120 Math.floor(this._animationListEl.offsetHeight / this._itemHeight) + 1; 121 const index = Math.floor( 122 this._inspectionPanelEl.scrollTop / this._itemHeight 123 ); 124 this.setState({ 125 displayableRange: { startIndex: index, endIndex: index + count }, 126 }); 127 } 128 129 updateTicks(props) { 130 const { animations, timeScale } = props; 131 const tickLinesEl = this._ref.current.querySelector(".tick-lines"); 132 const width = tickLinesEl.offsetWidth; 133 const animationDuration = timeScale.getDuration(); 134 const minTimeInterval = 135 (TIME_GRADUATION_MIN_SPACING * animationDuration) / width; 136 const intervalLength = findOptimalTimeInterval(minTimeInterval); 137 const intervalWidth = (intervalLength * width) / animationDuration; 138 const tickCount = parseInt(width / intervalWidth, 10); 139 const isAllDurationInfinity = animations.every( 140 animation => animation.state.duration === Infinity 141 ); 142 const zeroBasePosition = 143 width * (timeScale.zeroPositionTime / animationDuration); 144 const shiftWidth = zeroBasePosition % intervalWidth; 145 const needToShift = zeroBasePosition !== 0 && shiftWidth !== 0; 146 147 const ticks = []; 148 // Need to display first graduation since position will be shifted. 149 if (needToShift) { 150 const label = timeScale.formatTime(timeScale.distanceToRelativeTime(0)); 151 ticks.push({ position: 0, label, width: shiftWidth }); 152 } 153 154 for (let i = 0; i <= tickCount; i++) { 155 const position = ((i * intervalWidth + shiftWidth) * 100) / width; 156 const distance = timeScale.distanceToRelativeTime(position); 157 const label = 158 isAllDurationInfinity && i === tickCount 159 ? getStr("player.infiniteTimeLabel") 160 : timeScale.formatTime(distance); 161 ticks.push({ position, label, width: intervalWidth }); 162 } 163 164 this.setState({ ticks }); 165 } 166 167 render() { 168 const { 169 addAnimationsCurrentTimeListener, 170 animations, 171 direction, 172 dispatch, 173 getAnimatedPropertyMap, 174 getNodeFromActor, 175 removeAnimationsCurrentTimeListener, 176 selectAnimation, 177 setAnimationsCurrentTime, 178 setHighlightedNode, 179 setSelectedNode, 180 simulateAnimation, 181 timeScale, 182 } = this.props; 183 const { displayableRange, ticks } = this.state; 184 185 return dom.div( 186 { 187 className: "animation-list-container", 188 ref: this._ref, 189 }, 190 ProgressInspectionPanel({ 191 indicator: CurrentTimeScrubber({ 192 addAnimationsCurrentTimeListener, 193 direction, 194 removeAnimationsCurrentTimeListener, 195 setAnimationsCurrentTime, 196 timeScale, 197 }), 198 list: AnimationList({ 199 animations, 200 dispatch, 201 displayableRange, 202 getAnimatedPropertyMap, 203 getNodeFromActor, 204 selectAnimation, 205 setHighlightedNode, 206 setSelectedNode, 207 simulateAnimation, 208 timeScale, 209 }), 210 ticks, 211 }) 212 ); 213 } 214 } 215 216 const mapStateToProps = state => { 217 return { 218 sidebarWidth: state.animations.sidebarSize 219 ? state.animations.sidebarSize.width 220 : 0, 221 }; 222 }; 223 224 module.exports = connect(mapStateToProps)(AnimationListContainer);