animation.js (27543B)
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 createElement, 9 createFactory, 10 } = require("resource://devtools/client/shared/vendor/react.mjs"); 11 const { 12 Provider, 13 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 14 15 const EventEmitter = require("resource://devtools/shared/event-emitter.js"); 16 17 const App = createFactory( 18 require("resource://devtools/client/inspector/animation/components/App.js") 19 ); 20 const CurrentTimeTimer = require("resource://devtools/client/inspector/animation/current-time-timer.js"); 21 22 const animationsReducer = require("resource://devtools/client/inspector/animation/reducers/animations.js"); 23 const { 24 updateAnimations, 25 updateDetailVisibility, 26 updateElementPickerEnabled, 27 updateHighlightedNode, 28 updatePlaybackRates, 29 updateSelectedAnimation, 30 updateSidebarSize, 31 } = require("resource://devtools/client/inspector/animation/actions/animations.js"); 32 const { 33 hasAnimationIterationCountInfinite, 34 hasRunningAnimation, 35 } = require("resource://devtools/client/inspector/animation/utils/utils.js"); 36 37 class AnimationInspector { 38 constructor(inspector, win) { 39 this.inspector = inspector; 40 this.win = win; 41 42 this.inspector.store.injectReducer("animations", animationsReducer); 43 44 this.addAnimationsCurrentTimeListener = 45 this.addAnimationsCurrentTimeListener.bind(this); 46 this.getAnimatedPropertyMap = this.getAnimatedPropertyMap.bind(this); 47 this.getAnimationsCurrentTime = this.getAnimationsCurrentTime.bind(this); 48 this.getComputedStyle = this.getComputedStyle.bind(this); 49 this.getNodeFromActor = this.getNodeFromActor.bind(this); 50 this.removeAnimationsCurrentTimeListener = 51 this.removeAnimationsCurrentTimeListener.bind(this); 52 this.rewindAnimationsCurrentTime = 53 this.rewindAnimationsCurrentTime.bind(this); 54 this.selectAnimation = this.selectAnimation.bind(this); 55 this.setAnimationsCurrentTime = this.setAnimationsCurrentTime.bind(this); 56 this.setAnimationsPlaybackRate = this.setAnimationsPlaybackRate.bind(this); 57 this.setAnimationsPlayState = this.setAnimationsPlayState.bind(this); 58 this.setDetailVisibility = this.setDetailVisibility.bind(this); 59 this.setHighlightedNode = this.setHighlightedNode.bind(this); 60 this.setSelectedNode = this.setSelectedNode.bind(this); 61 this.simulateAnimation = this.simulateAnimation.bind(this); 62 this.simulateAnimationForKeyframesProgressBar = 63 this.simulateAnimationForKeyframesProgressBar.bind(this); 64 this.toggleElementPicker = this.toggleElementPicker.bind(this); 65 this.watchAnimationsForSelectedNode = 66 this.watchAnimationsForSelectedNode.bind(this); 67 this.unwatchAnimationsForSelectedNode = 68 this.unwatchAnimationsForSelectedNode.bind(this); 69 this.onAnimationStateChanged = this.onAnimationStateChanged.bind(this); 70 this.onAnimationsCurrentTimeUpdated = 71 this.onAnimationsCurrentTimeUpdated.bind(this); 72 this.onAnimationsMutation = this.onAnimationsMutation.bind(this); 73 this.onCurrentTimeTimerUpdated = this.onCurrentTimeTimerUpdated.bind(this); 74 this.onElementPickerStarted = this.onElementPickerStarted.bind(this); 75 this.onElementPickerStopped = this.onElementPickerStopped.bind(this); 76 this.onNavigate = this.onNavigate.bind(this); 77 this.onNewNodeFront = this.onNewNodeFront.bind(this); 78 this.onSidebarResized = this.onSidebarResized.bind(this); 79 this.onSidebarSelectionChanged = this.onSidebarSelectionChanged.bind(this); 80 81 EventEmitter.decorate(this); 82 this.emitForTests = this.emitForTests.bind(this); 83 84 this.initComponents(); 85 this.initListeners(); 86 } 87 88 initComponents() { 89 const { 90 addAnimationsCurrentTimeListener, 91 emitForTests: emitEventForTest, 92 getAnimatedPropertyMap, 93 getAnimationsCurrentTime, 94 getComputedStyle, 95 getNodeFromActor, 96 isAnimationsRunning, 97 removeAnimationsCurrentTimeListener, 98 rewindAnimationsCurrentTime, 99 selectAnimation, 100 setAnimationsCurrentTime, 101 setAnimationsPlaybackRate, 102 setAnimationsPlayState, 103 setDetailVisibility, 104 setHighlightedNode, 105 setSelectedNode, 106 simulateAnimation, 107 simulateAnimationForKeyframesProgressBar, 108 toggleElementPicker, 109 } = this; 110 111 const direction = this.win.document.dir; 112 113 this.animationsCurrentTimeListeners = []; 114 this.isCurrentTimeSet = false; 115 116 const provider = createElement( 117 Provider, 118 { 119 id: "animationinspector", 120 key: "animationinspector", 121 store: this.inspector.store, 122 }, 123 App({ 124 addAnimationsCurrentTimeListener, 125 direction, 126 emitEventForTest, 127 getAnimatedPropertyMap, 128 getAnimationsCurrentTime, 129 getComputedStyle, 130 getNodeFromActor, 131 isAnimationsRunning, 132 removeAnimationsCurrentTimeListener, 133 rewindAnimationsCurrentTime, 134 selectAnimation, 135 setAnimationsCurrentTime, 136 setAnimationsPlaybackRate, 137 setAnimationsPlayState, 138 setDetailVisibility, 139 setHighlightedNode, 140 setSelectedNode, 141 simulateAnimation, 142 simulateAnimationForKeyframesProgressBar, 143 toggleElementPicker, 144 }) 145 ); 146 this.provider = provider; 147 } 148 149 async initListeners() { 150 await this.watchAnimationsForSelectedNode({ 151 // During the initialization of the panel, this.isPanelVisible returns false, 152 // since it's not ready yet. 153 // We need to bypass the check in order to retrieve the animationsFront and fetch 154 // the animations for the selected node. 155 force: true, 156 }); 157 158 this.inspector.on("new-root", this.onNavigate); 159 this.inspector.selection.on("new-node-front", this.onNewNodeFront); 160 this.inspector.sidebar.on("select", this.onSidebarSelectionChanged); 161 this.inspector.toolbox.on("select", this.onSidebarSelectionChanged); 162 this.inspector.toolbox.on( 163 "inspector-sidebar-resized", 164 this.onSidebarResized 165 ); 166 this.inspector.toolbox.nodePicker.on( 167 "picker-started", 168 this.onElementPickerStarted 169 ); 170 this.inspector.toolbox.nodePicker.on( 171 "picker-stopped", 172 this.onElementPickerStopped 173 ); 174 } 175 176 destroy() { 177 this.setAnimationStateChangedListenerEnabled(false); 178 this.inspector.off("new-root", this.onNewNodeFront); 179 this.inspector.selection.off( 180 "new-node-front", 181 this.watchAnimationsForSelectedNode 182 ); 183 this.inspector.sidebar.off("select", this.onSidebarSelectionChanged); 184 this.inspector.toolbox.off( 185 "inspector-sidebar-resized", 186 this.onSidebarResized 187 ); 188 this.inspector.toolbox.nodePicker.off( 189 "picker-started", 190 this.onElementPickerStarted 191 ); 192 this.inspector.toolbox.nodePicker.off( 193 "picker-stopped", 194 this.onElementPickerStopped 195 ); 196 this.inspector.toolbox.off("select", this.onSidebarSelectionChanged); 197 198 if (this.animationsFront) { 199 this.animationsFront.off("mutations", this.onAnimationsMutation); 200 } 201 202 if (this.simulatedAnimation) { 203 this.simulatedAnimation.cancel(); 204 this.simulatedAnimation = null; 205 } 206 207 if (this.simulatedElement) { 208 this.simulatedElement.remove(); 209 this.simulatedElement = null; 210 } 211 212 if (this.simulatedAnimationForKeyframesProgressBar) { 213 this.simulatedAnimationForKeyframesProgressBar.cancel(); 214 this.simulatedAnimationForKeyframesProgressBar = null; 215 } 216 217 this.stopAnimationsCurrentTimeTimer(); 218 219 this.inspector = null; 220 this.win = null; 221 } 222 223 get state() { 224 return this.inspector.store.getState().animations; 225 } 226 227 addAnimationsCurrentTimeListener(listener) { 228 this.animationsCurrentTimeListeners.push(listener); 229 } 230 231 /** 232 * This function calls AnimationsFront.setCurrentTimes with considering the createdTime. 233 * 234 * @param {number} currentTime 235 */ 236 async doSetCurrentTimes(currentTime) { 237 // If we don't have an animationsFront, it means that we don't have visible animations 238 // so we can safely bail here. 239 if (!this.animationsFront) { 240 return; 241 } 242 243 const { animations, timeScale } = this.state; 244 currentTime = currentTime + timeScale.minStartTime; 245 await this.animationsFront.setCurrentTimes(animations, currentTime, true, { 246 relativeToCreatedTime: true, 247 }); 248 } 249 250 /** 251 * Return a map of animated property from given animation actor. 252 * 253 * @param {object} animation 254 * @return {Map} A map of animated property 255 * key: {String} Animated property name 256 * value: {Array} Array of keyframe object 257 * Also, the keyframe object is consisted as following. 258 * { 259 * value: {String} style, 260 * offset: {Number} offset of keyframe, 261 * easing: {String} easing from this keyframe to next keyframe, 262 * distance: {Number} use as y coordinate in graph, 263 * } 264 */ 265 getAnimatedPropertyMap(animation) { 266 const properties = animation.state.properties; 267 const animatedPropertyMap = new Map(); 268 269 for (const { name, values } of properties) { 270 const keyframes = values.map( 271 ({ value, offset, easing, distance = 0 }) => { 272 offset = parseFloat(offset.toFixed(3)); 273 return { value, offset, easing, distance }; 274 } 275 ); 276 277 animatedPropertyMap.set(name, keyframes); 278 } 279 280 return animatedPropertyMap; 281 } 282 283 getAnimationsCurrentTime() { 284 return this.currentTime; 285 } 286 287 /** 288 * Return the computed style of the specified property after setting the given styles 289 * to the simulated element. 290 * 291 * @param {string} property 292 * CSS property name (e.g. text-align). 293 * @param {object} styles 294 * Map of CSS property name and value. 295 * @return {string} 296 * Computed style of property. 297 */ 298 getComputedStyle(property, styles) { 299 this.simulatedElement.style.cssText = ""; 300 301 for (const propertyName in styles) { 302 this.simulatedElement.style.setProperty( 303 propertyName, 304 styles[propertyName] 305 ); 306 } 307 308 return this.win 309 .getComputedStyle(this.simulatedElement) 310 .getPropertyValue(property); 311 } 312 313 getNodeFromActor(actorID) { 314 if (!this.inspector) { 315 return Promise.reject("Animation inspector already destroyed"); 316 } 317 318 if (!this.animationsFront?.walker) { 319 return Promise.reject("No animations front walker"); 320 } 321 322 return this.animationsFront.walker.getNodeFromActor(actorID, ["node"]); 323 } 324 325 isPanelVisible() { 326 return ( 327 this.inspector && 328 this.inspector.toolbox && 329 this.inspector.sidebar && 330 this.inspector.toolbox.currentToolId === "inspector" && 331 this.inspector.sidebar.getCurrentTabID() === "animationinspector" 332 ); 333 } 334 335 onAnimationStateChanged() { 336 // Simply update the animations since the state has already been updated. 337 this.fireUpdateAction([...this.state.animations]); 338 } 339 340 /** 341 * This method should call when the current time is changed. 342 * Then, dispatches the current time to listeners that are registered 343 * by addAnimationsCurrentTimeListener. 344 * 345 * @param {number} currentTime 346 */ 347 onAnimationsCurrentTimeUpdated(currentTime) { 348 this.currentTime = currentTime; 349 350 for (const listener of this.animationsCurrentTimeListeners) { 351 listener(currentTime); 352 } 353 } 354 355 /** 356 * This method is called when the current time proceed by CurrentTimeTimer. 357 * 358 * @param {number} currentTime 359 * @param {Bool} shouldStop 360 */ 361 onCurrentTimeTimerUpdated(currentTime, shouldStop) { 362 if (shouldStop) { 363 this.setAnimationsCurrentTime(currentTime, true); 364 } else { 365 this.onAnimationsCurrentTimeUpdated(currentTime); 366 } 367 } 368 369 async onAnimationsMutation(changes) { 370 let animations = [...this.state.animations]; 371 const addedAnimations = []; 372 373 for (const { type, player: animation } of changes) { 374 if (type === "added") { 375 if (!animation.state.type) { 376 // This animation was added but removed immediately. 377 continue; 378 } 379 380 addedAnimations.push(animation); 381 animation.on("changed", this.onAnimationStateChanged); 382 } else if (type === "removed") { 383 const index = animations.indexOf(animation); 384 385 if (index < 0) { 386 // This animation was added but removed immediately. 387 continue; 388 } 389 390 animations.splice(index, 1); 391 animation.off("changed", this.onAnimationStateChanged); 392 } 393 } 394 395 // Update existing other animations as well since the currentTime would be proceeded 396 // sice the scrubber position is related the currentTime. 397 // Also, don't update the state of removed animations since React components 398 // may refer to the same instance still. 399 try { 400 animations = await this.refreshAnimationsState(animations); 401 } catch (_) { 402 console.error(`Updating Animations failed`); 403 return; 404 } 405 406 this.fireUpdateAction(animations.concat(addedAnimations)); 407 } 408 409 onElementPickerStarted() { 410 this.inspector.store.dispatch(updateElementPickerEnabled(true)); 411 } 412 413 onElementPickerStopped() { 414 this.inspector.store.dispatch(updateElementPickerEnabled(false)); 415 } 416 417 onNavigate() { 418 if (!this.isPanelVisible()) { 419 return; 420 } 421 422 this.inspector.store.dispatch(updatePlaybackRates()); 423 } 424 425 async onSidebarSelectionChanged() { 426 const isPanelVisibled = this.isPanelVisible(); 427 428 if (this.wasPanelVisibled === isPanelVisibled) { 429 // onSidebarSelectionChanged is called some times even same state 430 // from sidebar and toolbar. 431 return; 432 } 433 434 this.wasPanelVisibled = isPanelVisibled; 435 436 if (this.isPanelVisible()) { 437 await this.watchAnimationsForSelectedNode(); 438 this.onSidebarResized(null, this.inspector.getSidebarSize()); 439 } else { 440 await this.unwatchAnimationsForSelectedNode(); 441 this.stopAnimationsCurrentTimeTimer(); 442 } 443 } 444 445 onSidebarResized(size) { 446 if (!this.isPanelVisible()) { 447 return; 448 } 449 450 this.inspector.store.dispatch(updateSidebarSize(size)); 451 } 452 453 removeAnimationsCurrentTimeListener(listener) { 454 this.animationsCurrentTimeListeners = 455 this.animationsCurrentTimeListeners.filter(l => l !== listener); 456 } 457 458 async rewindAnimationsCurrentTime() { 459 const { timeScale } = this.state; 460 await this.setAnimationsCurrentTime(timeScale.zeroPositionTime, true); 461 } 462 463 selectAnimation(animation) { 464 this.inspector.store.dispatch(updateSelectedAnimation(animation)); 465 } 466 467 async setSelectedNode(nodeFront) { 468 if (this.inspector.selection.nodeFront === nodeFront) { 469 return; 470 } 471 472 await this.inspector 473 .getCommonComponentProps() 474 .setSelectedNode(nodeFront, { reason: "animation-panel" }); 475 } 476 477 async setAnimationsCurrentTime(currentTime, shouldRefresh) { 478 this.stopAnimationsCurrentTimeTimer(); 479 this.onAnimationsCurrentTimeUpdated(currentTime); 480 481 if (!shouldRefresh && this.isCurrentTimeSet) { 482 return; 483 } 484 485 let animations = this.state.animations; 486 this.isCurrentTimeSet = true; 487 488 try { 489 await this.doSetCurrentTimes(currentTime); 490 animations = await this.refreshAnimationsState(animations); 491 } catch (e) { 492 // Expected if we've already been destroyed or other node have been selected 493 // in the meantime. 494 console.error(e); 495 return; 496 } 497 498 this.isCurrentTimeSet = false; 499 500 if (shouldRefresh) { 501 this.fireUpdateAction(animations); 502 } 503 } 504 505 async setAnimationsPlaybackRate(playbackRate) { 506 if (!this.inspector) { 507 return; // Already destroyed or another node selected. 508 } 509 510 // If we don't have an animationsFront, it means that we don't have visible animations 511 // so we can safely bail here. 512 if (!this.animationsFront) { 513 return; 514 } 515 516 let animations = this.state.animations; 517 // "changed" event on each animation will fire respectively when the playback 518 // rate changed. Since for each occurrence of event, change of UI is urged. 519 // To avoid this, disable the listeners once in order to not capture the event. 520 this.setAnimationStateChangedListenerEnabled(false); 521 try { 522 await this.animationsFront.setPlaybackRates(animations, playbackRate); 523 animations = await this.refreshAnimationsState(animations); 524 } catch (e) { 525 // Expected if we've already been destroyed or another node has been 526 // selected in the meantime. 527 console.error(e); 528 return; 529 } finally { 530 this.setAnimationStateChangedListenerEnabled(true); 531 } 532 533 if (animations) { 534 await this.fireUpdateAction(animations); 535 } 536 } 537 538 async setAnimationsPlayState(doPlay) { 539 if (!this.inspector) { 540 return; // Already destroyed or another node selected. 541 } 542 543 // If we don't have an animationsFront, it means that we don't have visible animations 544 // so we can safely bail here. 545 if (!this.animationsFront) { 546 return; 547 } 548 549 let { animations, timeScale } = this.state; 550 551 try { 552 if ( 553 doPlay && 554 animations.every( 555 animation => 556 timeScale.getEndTime(animation) <= animation.state.currentTime 557 ) 558 ) { 559 await this.doSetCurrentTimes(timeScale.zeroPositionTime); 560 } 561 562 if (doPlay) { 563 await this.animationsFront.playSome(animations); 564 } else { 565 await this.animationsFront.pauseSome(animations); 566 } 567 568 animations = await this.refreshAnimationsState(animations); 569 } catch (e) { 570 // Expected if we've already been destroyed or other node have been selected 571 // in the meantime. 572 console.error(e); 573 return; 574 } 575 576 await this.fireUpdateAction(animations); 577 } 578 579 /** 580 * Enable/disable the animation state change listener. 581 * If set true, observe "changed" event on current animations. 582 * Otherwise, quit observing the "changed" event. 583 * 584 * @param {Bool} isEnabled 585 */ 586 setAnimationStateChangedListenerEnabled(isEnabled) { 587 if (!this.inspector) { 588 return; // Already destroyed. 589 } 590 if (isEnabled) { 591 for (const animation of this.state.animations) { 592 animation.on("changed", this.onAnimationStateChanged); 593 } 594 } else { 595 for (const animation of this.state.animations) { 596 animation.off("changed", this.onAnimationStateChanged); 597 } 598 } 599 } 600 601 setDetailVisibility(isVisible) { 602 this.inspector.store.dispatch(updateDetailVisibility(isVisible)); 603 } 604 605 /** 606 * Persistently highlight the given node identified with a unique selector. 607 * If no node is provided, hide any persistent highlighter. 608 * 609 * @param {NodeFront} nodeFront 610 */ 611 async setHighlightedNode(nodeFront) { 612 await this.inspector.highlighters.hideHighlighterType( 613 this.inspector.highlighters.TYPES.SELECTOR 614 ); 615 616 if (nodeFront) { 617 const selector = await nodeFront.getUniqueSelector(); 618 if (!selector) { 619 console.warn( 620 `Couldn't get unique selector for NodeFront: ${nodeFront.actorID}` 621 ); 622 return; 623 } 624 625 /** 626 * NOTE: Using a Selector Highlighter here because only one Box Model Highlighter 627 * can be visible at a time. The Box Model Highlighter is shown when hovering nodes 628 * which would cause this persistent highlighter to be hidden unexpectedly. 629 * This limitation of one highlighter type a time should be solved by switching 630 * to a highlighter by role approach (Bug 1663443). 631 */ 632 await this.inspector.highlighters.showHighlighterTypeForNode( 633 this.inspector.highlighters.TYPES.SELECTOR, 634 nodeFront, 635 { 636 hideInfoBar: true, 637 hideGuides: true, 638 selector, 639 } 640 ); 641 } 642 643 this.inspector.store.dispatch(updateHighlightedNode(nodeFront)); 644 } 645 646 /** 647 * Returns simulatable animation by given parameters. 648 * The returned animation is implementing Animation interface of Web Animation API. 649 * https://drafts.csswg.org/web-animations/#the-animation-interface 650 * 651 * @param {Array} keyframes 652 * e.g. [{ opacity: 0 }, { opacity: 1 }] 653 * @param {object} effectTiming 654 * e.g. { duration: 1000, fill: "both" } 655 * @param {boolean} isElementNeeded 656 * true: create animation with an element. 657 * If want to know computed value of the element, turn on. 658 * false: create animation without an element, 659 * If need to know only timing progress. 660 * @return {Animation} 661 * https://drafts.csswg.org/web-animations/#the-animation-interface 662 */ 663 simulateAnimation(keyframes, effectTiming, isElementNeeded) { 664 // Don't simulate animation if the animation inspector is already destroyed. 665 if (!this.win) { 666 return null; 667 } 668 669 let targetEl = null; 670 671 if (isElementNeeded) { 672 if (!this.simulatedElement) { 673 this.simulatedElement = this.win.document.createElement("div"); 674 this.win.document.documentElement.appendChild(this.simulatedElement); 675 } else { 676 // Reset styles. 677 this.simulatedElement.style.cssText = ""; 678 } 679 680 targetEl = this.simulatedElement; 681 } 682 683 if (!this.simulatedAnimation) { 684 this.simulatedAnimation = new this.win.Animation(); 685 } 686 687 this.simulatedAnimation.effect = new this.win.KeyframeEffect( 688 targetEl, 689 keyframes, 690 effectTiming 691 ); 692 693 return this.simulatedAnimation; 694 } 695 696 /** 697 * Returns a simulatable efect timing animation for the keyframes progress bar. 698 * The returned animation is implementing Animation interface of Web Animation API. 699 * https://drafts.csswg.org/web-animations/#the-animation-interface 700 * 701 * @param {object} effectTiming 702 * e.g. { duration: 1000, fill: "both" } 703 * @return {Animation} 704 * https://drafts.csswg.org/web-animations/#the-animation-interface 705 */ 706 simulateAnimationForKeyframesProgressBar(effectTiming) { 707 if (!this.simulatedAnimationForKeyframesProgressBar) { 708 this.simulatedAnimationForKeyframesProgressBar = new this.win.Animation(); 709 } 710 711 this.simulatedAnimationForKeyframesProgressBar.effect = 712 new this.win.KeyframeEffect(null, null, effectTiming); 713 714 return this.simulatedAnimationForKeyframesProgressBar; 715 } 716 717 stopAnimationsCurrentTimeTimer() { 718 if (this.currentTimeTimer) { 719 this.currentTimeTimer.destroy(); 720 this.currentTimeTimer = null; 721 } 722 } 723 724 startAnimationsCurrentTimeTimer() { 725 const timeScale = this.state.timeScale; 726 const shouldStopAfterEndTime = !hasAnimationIterationCountInfinite( 727 this.state.animations 728 ); 729 730 const currentTimeTimer = new CurrentTimeTimer( 731 timeScale, 732 shouldStopAfterEndTime, 733 this.win, 734 this.onCurrentTimeTimerUpdated 735 ); 736 currentTimeTimer.start(); 737 this.currentTimeTimer = currentTimeTimer; 738 } 739 740 toggleElementPicker() { 741 this.inspector.toolbox.nodePicker.togglePicker(); 742 } 743 744 onNewNodeFront() { 745 this.watchAnimationsForSelectedNode(); 746 } 747 748 /** 749 * Retrieve animations for the inspector selected node (and its subtree), add an event 750 * listener for animations on the node (and its subtree) and update the panel. 751 * If the panel is not visible (and `force` is not `true`), the panel won't be updated, 752 * and this will remove the previous listener. 753 * 754 * @param {object} options 755 * @param {boolean} options.force: Set to true to force updating the panel, even if 756 * it is not visible. 757 */ 758 async watchAnimationsForSelectedNode({ force = false } = {}) { 759 this.unwatchAnimationsForSelectedNode(); 760 761 if (!this.isPanelVisible() && !force) { 762 return; 763 } 764 765 const done = this.inspector.updating("animationinspector"); 766 const selection = this.inspector.selection; 767 768 let animations; 769 const shouldWatchAnimationForSelectedNode = 770 selection && selection.isConnected() && selection.isElementNode(); 771 if (shouldWatchAnimationForSelectedNode) { 772 // Since the panel only displays the animations for the selected node and its subtree, 773 // we can get the animation front from the selected node target, so we can handle 774 // animations in iframe for example 775 this.animationsFront = 776 await selection.nodeFront.targetFront.getFront("animations"); 777 // At this point, we have a selected node, so the target should have an inspector 778 // and its walker, that we can pass to the animation front 779 this.animationsFront.setWalkerActor( 780 selection.nodeFront.inspectorFront.walker 781 ); 782 // Then we can listen for future animations on the subtree 783 this.animationsFront.on("mutations", this.onAnimationsMutation); 784 // and directly retrieve the existing one, if there are some 785 animations = await this.animationsFront.getAnimationPlayersForNode( 786 selection.nodeFront 787 ); 788 } 789 790 this.fireUpdateAction(animations || []); 791 this.setAnimationStateChangedListenerEnabled(true); 792 793 done(); 794 } 795 796 /** 797 * Nullify animationFront, remove the listener that might have been set on it, as well 798 * as listeners on AnimationPlayer fronts. 799 * 800 * @param {object} options 801 * @param {boolean} options.force: Set to true to force updating the panel, even if 802 * it is not visible. 803 */ 804 unwatchAnimationsForSelectedNode() { 805 if (this.animationsFront) { 806 this.animationsFront.off("mutations", this.onAnimationsMutation); 807 this.animationsFront = null; 808 } 809 this.setAnimationStateChangedListenerEnabled(false); 810 } 811 812 async refreshAnimationsState(animations) { 813 let error = null; 814 815 const promises = animations.map(animation => { 816 return new Promise(resolve => { 817 animation 818 .refreshState() 819 .catch(e => { 820 error = e; 821 }) 822 .finally(() => { 823 resolve(); 824 }); 825 }); 826 }); 827 await Promise.all(promises); 828 829 if (error) { 830 throw new Error(error); 831 } 832 833 // Even when removal animation on inspected document, refreshAnimationsState 834 // might be called before onAnimationsMutation due to the async timing. 835 // Return the animations as result of refreshAnimationsState after getting rid of 836 // the animations since they should not display. 837 return animations.filter(anim => !!anim.state.type); 838 } 839 840 fireUpdateAction(animations) { 841 // Animation inspector already destroyed 842 if (!this.inspector) { 843 return; 844 } 845 846 this.stopAnimationsCurrentTimeTimer(); 847 848 // Although it is not possible to set a delay or end delay of infinity using 849 // the animation API, if the value passed exceeds the limit of our internal 850 // representation of times, it will be treated as infinity. Rather than 851 // adding special case code to represent this very rare case, we simply omit 852 // such animations from the graph. 853 animations = animations.filter( 854 anim => 855 Math.abs(anim.state.delay) !== Infinity && 856 Math.abs(anim.state.endDelay) !== Infinity 857 ); 858 859 this.inspector.store.dispatch(updateAnimations(animations)); 860 861 if (hasRunningAnimation(animations)) { 862 this.startAnimationsCurrentTimeTimer(); 863 } else { 864 // Even no running animations, update the current time once 865 // so as to show the state. 866 this.onCurrentTimeTimerUpdated(this.state.timeScale.getCurrentTime()); 867 } 868 } 869 } 870 871 module.exports = AnimationInspector;