tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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;