tor-browser

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

animation.js (31822B)


      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 /**
      8 * Set of actors that expose the Web Animations API to devtools protocol
      9 * clients.
     10 *
     11 * The |Animations| actor is the main entry point. It is used to discover
     12 * animation players on given nodes.
     13 * There should only be one instance per devtools server.
     14 *
     15 * The |AnimationPlayer| actor provides attributes and methods to inspect an
     16 * animation as well as pause/resume/seek it.
     17 *
     18 * The Web Animation spec implementation is ongoing in Gecko, and so this set
     19 * of actors should evolve when the implementation progresses.
     20 *
     21 * References:
     22 * - WebAnimation spec:
     23 *   http://drafts.csswg.org/web-animations/
     24 * - WebAnimation WebIDL files:
     25 *   /dom/webidl/Animation*.webidl
     26 */
     27 
     28 const { Actor } = require("resource://devtools/shared/protocol.js");
     29 const {
     30  animationPlayerSpec,
     31  animationsSpec,
     32 } = require("resource://devtools/shared/specs/animation.js");
     33 
     34 const {
     35  ANIMATION_TYPE_FOR_LONGHANDS,
     36 } = require("resource://devtools/server/actors/animation-type-longhand.js");
     37 
     38 loader.lazyRequireGetter(
     39  this,
     40  "getNodeDisplayName",
     41  "resource://devtools/server/actors/inspector/utils.js",
     42  true
     43 );
     44 
     45 // Types of animations.
     46 const ANIMATION_TYPES = {
     47  CSS_ANIMATION: "cssanimation",
     48  CSS_TRANSITION: "csstransition",
     49  SCRIPT_ANIMATION: "scriptanimation",
     50  UNKNOWN: "unknown",
     51 };
     52 exports.ANIMATION_TYPES = ANIMATION_TYPES;
     53 
     54 function getAnimationTypeForLonghand(property) {
     55  // If this is a custom property, return "custom" for now as it's not straightforward
     56  // to retrieve the proper animation type.
     57  // TODO: We could compute the animation type from the registered property syntax (Bug 1875435)
     58  if (property.startsWith("--")) {
     59    return "custom";
     60  }
     61 
     62  for (const [type, props] of ANIMATION_TYPE_FOR_LONGHANDS) {
     63    if (props.has(property)) {
     64      return type;
     65    }
     66  }
     67  throw new Error("Unknown longhand property name");
     68 }
     69 exports.getAnimationTypeForLonghand = getAnimationTypeForLonghand;
     70 
     71 /**
     72 * The AnimationPlayerActor provides information about a given animation: its
     73 * startTime, currentTime, current state, etc.
     74 *
     75 * Since the state of a player changes as the animation progresses it is often
     76 * useful to call getCurrentState at regular intervals to get the current state.
     77 *
     78 * This actor also allows playing, pausing and seeking the animation.
     79 */
     80 class AnimationPlayerActor extends Actor {
     81  /**
     82   * @param {AnimationsActor} The main AnimationsActor instance
     83   * @param {AnimationPlayer} The player object returned by getAnimationPlayers
     84   * @param {number} Time which animation created
     85   */
     86  constructor(animationsActor, player, createdTime) {
     87    super(animationsActor.conn, animationPlayerSpec);
     88 
     89    this.onAnimationMutation = this.onAnimationMutation.bind(this);
     90 
     91    this.animationsActor = animationsActor;
     92    this.walker = animationsActor.walker;
     93    this.player = player;
     94    // getting the node might need to traverse the DOM, let's only do this once, when
     95    // the Actor gets created
     96    this.node = this.getNode();
     97 
     98    // Listen to animation mutations on the node to alert the front when the
     99    // current animation changes.
    100    this.observer = new this.window.MutationObserver(this.onAnimationMutation);
    101    if (this.isPseudoElement) {
    102      // If the node is a pseudo-element, then we listen on its binding element (which is
    103      // this.player.effect.target here), with `subtree:true` (there's no risk of getting
    104      // too many notifications in onAnimationTargetMutation since we filter out events
    105      // that aren't for the current animation).
    106      this.observer.observe(this.player.effect.target, {
    107        animations: true,
    108        subtree: true,
    109      });
    110    } else {
    111      this.observer.observe(this.node, { animations: true });
    112    }
    113 
    114    this.createdTime = createdTime;
    115    this.currentTimeAtCreated = player.currentTime;
    116  }
    117 
    118  destroy() {
    119    // Only try to disconnect the observer if it's not already dead (i.e. if the
    120    // container view hasn't navigated since).
    121    if (this.observer && !Cu.isDeadWrapper(this.observer)) {
    122      this.observer.disconnect();
    123    }
    124    this.player = this.observer = this.walker = this.animationsActor = null;
    125 
    126    super.destroy();
    127  }
    128 
    129  get isPseudoElement() {
    130    return !!this.player.effect.pseudoElement;
    131  }
    132 
    133  getNode() {
    134    if (!this.isPseudoElement) {
    135      return this.player.effect.target;
    136    }
    137 
    138    const originatingElem = this.player.effect.target;
    139    const treeWalker = this.walker.getDocumentWalker(originatingElem);
    140 
    141    // When the animated node is a pseudo-element, we need to walk the children
    142    // of the target node and look for it.
    143    for (
    144      let next = treeWalker.firstChild();
    145      next;
    146      // Use `nextNode` (and not `nextSibling`) as we might need to traverse the whole
    147      // children tree to find nested elements (e.g. `::view-transition-group(root)`).
    148      next = treeWalker.nextNode()
    149    ) {
    150      if (!next.implementedPseudoElement) {
    151        continue;
    152      }
    153 
    154      if (this.player.effect.pseudoElement === getNodeDisplayName(next)) {
    155        return next;
    156      }
    157    }
    158 
    159    console.warn(
    160      `Pseudo element ${this.player.effect.pseudoElement} is not found`
    161    );
    162 
    163    return null;
    164  }
    165 
    166  get document() {
    167    return this.player.effect.target.ownerDocument;
    168  }
    169 
    170  get window() {
    171    return this.document.defaultView;
    172  }
    173 
    174  /**
    175   * Release the actor, when it isn't needed anymore.
    176   * Protocol.js uses this release method to call the destroy method.
    177   */
    178  release() {}
    179 
    180  form() {
    181    const data = this.getCurrentState();
    182    data.actor = this.actorID;
    183 
    184    // If we know the WalkerActor, and if the animated node is known by it, then
    185    // return its corresponding NodeActor ID too.
    186    if (this.walker && this.walker.hasNode(this.node)) {
    187      data.animationTargetNodeActorID = this.walker.getNode(this.node).actorID;
    188    }
    189 
    190    return data;
    191  }
    192 
    193  isCssAnimation(player = this.player) {
    194    return this.window.CSSAnimation.isInstance(player);
    195  }
    196 
    197  isCssTransition(player = this.player) {
    198    return this.window.CSSTransition.isInstance(player);
    199  }
    200 
    201  isScriptAnimation(player = this.player) {
    202    return (
    203      this.window.Animation.isInstance(player) &&
    204      !(
    205        this.window.CSSAnimation.isInstance(player) ||
    206        this.window.CSSTransition.isInstance(player)
    207      )
    208    );
    209  }
    210 
    211  getType() {
    212    if (this.isCssAnimation()) {
    213      return ANIMATION_TYPES.CSS_ANIMATION;
    214    } else if (this.isCssTransition()) {
    215      return ANIMATION_TYPES.CSS_TRANSITION;
    216    } else if (this.isScriptAnimation()) {
    217      return ANIMATION_TYPES.SCRIPT_ANIMATION;
    218    }
    219 
    220    return ANIMATION_TYPES.UNKNOWN;
    221  }
    222 
    223  /**
    224   * Get the name of this animation. This can be either the animation.id
    225   * property if it was set, or the keyframe rule name or the transition
    226   * property.
    227   *
    228   * @return {string}
    229   */
    230  getName() {
    231    if (this.player.id) {
    232      return this.player.id;
    233    } else if (this.isCssAnimation()) {
    234      return this.player.animationName;
    235    } else if (this.isCssTransition()) {
    236      return this.player.transitionProperty;
    237    }
    238 
    239    return "";
    240  }
    241 
    242  /**
    243   * Get the animation duration from this player, in milliseconds.
    244   *
    245   * @return {number}
    246   */
    247  getDuration() {
    248    return this.player.effect.getComputedTiming().duration;
    249  }
    250 
    251  /**
    252   * Get the animation delay from this player, in milliseconds.
    253   *
    254   * @return {number}
    255   */
    256  getDelay() {
    257    return this.player.effect.getComputedTiming().delay;
    258  }
    259 
    260  /**
    261   * Get the animation endDelay from this player, in milliseconds.
    262   *
    263   * @return {number}
    264   */
    265  getEndDelay() {
    266    return this.player.effect.getComputedTiming().endDelay;
    267  }
    268 
    269  /**
    270   * Get the animation iteration count for this player. That is, how many times
    271   * is the animation scheduled to run.
    272   *
    273   * @return {number} The number of iterations, or null if the animation repeats
    274   * infinitely.
    275   */
    276  getIterationCount() {
    277    const iterations = this.player.effect.getComputedTiming().iterations;
    278    return iterations === Infinity ? null : iterations;
    279  }
    280 
    281  /**
    282   * Get the animation iterationStart from this player, in ratio.
    283   * That is offset of starting position of the animation.
    284   *
    285   * @return {number}
    286   */
    287  getIterationStart() {
    288    return this.player.effect.getComputedTiming().iterationStart;
    289  }
    290 
    291  /**
    292   * Get the animation easing from this player.
    293   *
    294   * @return {string}
    295   */
    296  getEasing() {
    297    return this.player.effect.getComputedTiming().easing;
    298  }
    299 
    300  /**
    301   * Get the animation fill mode from this player.
    302   *
    303   * @return {string}
    304   */
    305  getFill() {
    306    return this.player.effect.getComputedTiming().fill;
    307  }
    308 
    309  /**
    310   * Get the animation direction from this player.
    311   *
    312   * @return {string}
    313   */
    314  getDirection() {
    315    return this.player.effect.getComputedTiming().direction;
    316  }
    317 
    318  /**
    319   * Get animation-timing-function from animated element if CSS Animations.
    320   *
    321   * @return {string}
    322   */
    323  getAnimationTimingFunction() {
    324    if (!this.isCssAnimation()) {
    325      return null;
    326    }
    327 
    328    const { target, pseudoElement } = this.player.effect;
    329    return this.window.getComputedStyle(target, pseudoElement)
    330      .animationTimingFunction;
    331  }
    332 
    333  getPropertiesCompositorStatus() {
    334    const properties = this.player.effect.getProperties();
    335    return properties.map(prop => {
    336      return {
    337        property: prop.property,
    338        runningOnCompositor: prop.runningOnCompositor,
    339        warning: prop.warning,
    340      };
    341    });
    342  }
    343 
    344  /**
    345   * Return the current start of the Animation.
    346   *
    347   * @return {object}
    348   */
    349  getState() {
    350    const compositorStatus = this.getPropertiesCompositorStatus();
    351    // Note that if you add a new property to the state object, make sure you
    352    // add the corresponding property in the AnimationPlayerFront' initialState
    353    // getter.
    354    return {
    355      // Don't include the type if the animation was removed (e.g. it isn't handled by the
    356      // AnimationsActor anymore). The client filters out animations without type as a
    357      // result of its calls to AnimationPlayerFront#refreshState.
    358      type: this.animationRemoved ? null : this.getType(),
    359      // startTime is null whenever the animation is paused or waiting to start.
    360      startTime: this.player.startTime,
    361      currentTime: this.player.currentTime,
    362      playState: this.player.playState,
    363      playbackRate: this.player.playbackRate,
    364      name: this.getName(),
    365      duration: this.getDuration(),
    366      delay: this.getDelay(),
    367      endDelay: this.getEndDelay(),
    368      iterationCount: this.getIterationCount(),
    369      iterationStart: this.getIterationStart(),
    370      fill: this.getFill(),
    371      easing: this.getEasing(),
    372      direction: this.getDirection(),
    373      animationTimingFunction: this.getAnimationTimingFunction(),
    374      // animation is hitting the fast path or not. Returns false whenever the
    375      // animation is paused as it is taken off the compositor then.
    376      isRunningOnCompositor: compositorStatus.some(
    377        propState => propState.runningOnCompositor
    378      ),
    379      propertyState: compositorStatus,
    380      // The document timeline's currentTime is being sent along too. This is
    381      // not strictly related to the node's animationPlayer, but is useful to
    382      // know the current time of the animation with respect to the document's.
    383      documentCurrentTime: this.document.timeline.currentTime,
    384      // The time which this animation created.
    385      createdTime: this.createdTime,
    386      // The time which an animation's current time when this animation has created.
    387      currentTimeAtCreated: this.currentTimeAtCreated,
    388      properties: this.getProperties(),
    389    };
    390  }
    391 
    392  /**
    393   * Get the current state of the AnimationPlayer (currentTime, playState, ...).
    394   * Note that the initial state is returned as the form of this actor when it
    395   * is initialized.
    396   * This protocol method only returns a trimed down version of this state in
    397   * case some properties haven't changed since last time (since the front can
    398   * reconstruct those). If you want the full state, use the getState method.
    399   *
    400   * @return {object}
    401   */
    402  getCurrentState() {
    403    const newState = this.getState();
    404 
    405    // If we've saved a state before, compare and only send what has changed.
    406    // It's expected of the front to also save old states to re-construct the
    407    // full state when an incomplete one is received.
    408    // This is to minimize protocol traffic.
    409    let sentState = {};
    410    if (this.currentState) {
    411      for (const key in newState) {
    412        if (
    413          typeof this.currentState[key] === "undefined" ||
    414          this.currentState[key] !== newState[key]
    415        ) {
    416          sentState[key] = newState[key];
    417        }
    418      }
    419    } else {
    420      sentState = newState;
    421    }
    422    this.currentState = newState;
    423 
    424    return sentState;
    425  }
    426 
    427  /**
    428   * Executed when the current animation changes, used to emit the new state
    429   * the the front.
    430   */
    431  onAnimationMutation(mutations) {
    432    const isCurrentAnimation = animation => animation === this.player;
    433    const hasCurrentAnimation = animations =>
    434      animations.some(isCurrentAnimation);
    435    let hasChanged = false;
    436 
    437    for (const { removedAnimations, changedAnimations } of mutations) {
    438      if (hasCurrentAnimation(removedAnimations)) {
    439        // Reset the local copy of the state on removal, since the animation can
    440        // be kept on the client and re-added, its state needs to be sent in
    441        // full.
    442        this.currentState = null;
    443      }
    444 
    445      if (hasCurrentAnimation(changedAnimations)) {
    446        // Only consider the state has having changed if any of effect timing properties,
    447        // animationTimingFunction or playbackRate has changed.
    448        const newState = this.getState();
    449        const oldState = this.currentState;
    450        hasChanged =
    451          newState.delay !== oldState.delay ||
    452          newState.iterationCount !== oldState.iterationCount ||
    453          newState.iterationStart !== oldState.iterationStart ||
    454          newState.duration !== oldState.duration ||
    455          newState.endDelay !== oldState.endDelay ||
    456          newState.direction !== oldState.direction ||
    457          newState.easing !== oldState.easing ||
    458          newState.fill !== oldState.fill ||
    459          newState.animationTimingFunction !==
    460            oldState.animationTimingFunction ||
    461          newState.playbackRate !== oldState.playbackRate;
    462        break;
    463      }
    464    }
    465 
    466    if (hasChanged) {
    467      this.emit("changed", this.getCurrentState());
    468    }
    469  }
    470 
    471  onAnimationRemoved() {
    472    this.animationRemoved = true;
    473  }
    474 
    475  /**
    476   * Get data about the animated properties of this animation player.
    477   *
    478   * @return {Array} Returns a list of animated properties.
    479   * Each property contains a list of values, their offsets and distances.
    480   */
    481  getProperties() {
    482    const properties = this.player.effect.getProperties().map(property => {
    483      return { name: property.property, values: property.values };
    484    });
    485 
    486    // If the node isn't connected, the call to DOMWindowUtils.getUnanimatedComputedStyle
    487    // below would throw. So early return from here, we'll miss the distance but that
    488    // seems fine.
    489    if (!this.node?.isConnected) {
    490      return properties;
    491    }
    492 
    493    const DOMWindowUtils = this.window.windowUtils;
    494 
    495    // Fill missing keyframe with computed value.
    496    for (const property of properties) {
    497      let underlyingValue = null;
    498      // Check only 0% and 100% keyframes.
    499      [0, property.values.length - 1].forEach(index => {
    500        const values = property.values[index];
    501        if (values.value !== undefined) {
    502          return;
    503        }
    504        if (!underlyingValue) {
    505          const { target, pseudoElement } = this.player.effect;
    506          const value = DOMWindowUtils.getUnanimatedComputedStyle(
    507            target,
    508            pseudoElement,
    509            property.name,
    510            DOMWindowUtils.FLUSH_NONE
    511          );
    512          const animationType = getAnimationTypeForLonghand(property.name);
    513          underlyingValue =
    514            animationType === "float" ? parseFloat(value, 10) : value;
    515        }
    516        values.value = underlyingValue;
    517      });
    518    }
    519 
    520    // Calculate the distance.
    521    for (const property of properties) {
    522      const propertyName = property.name;
    523      const maxObject = { distance: -1 };
    524      for (let i = 0; i < property.values.length - 1; i++) {
    525        const value1 = property.values[i].value;
    526        for (let j = i + 1; j < property.values.length; j++) {
    527          const value2 = property.values[j].value;
    528          const distance = this.getDistance(
    529            this.node,
    530            propertyName,
    531            value1,
    532            value2,
    533            DOMWindowUtils
    534          );
    535          if (maxObject.distance >= distance) {
    536            continue;
    537          }
    538          maxObject.distance = distance;
    539          maxObject.value1 = value1;
    540          maxObject.value2 = value2;
    541        }
    542      }
    543      if (maxObject.distance === 0) {
    544        // Distance is zero means that no values change or can't calculate the distance.
    545        // In this case, we use the keyframe offset as the distance.
    546        property.values.reduce((previous, current) => {
    547          // If the current value is same as previous value, use previous distance.
    548          current.distance =
    549            current.value === previous.value
    550              ? previous.distance
    551              : current.offset;
    552          return current;
    553        }, property.values[0]);
    554        continue;
    555      }
    556      const baseValue =
    557        maxObject.value1 < maxObject.value2
    558          ? maxObject.value1
    559          : maxObject.value2;
    560      for (const values of property.values) {
    561        const value = values.value;
    562        const distance = this.getDistance(
    563          this.node,
    564          propertyName,
    565          baseValue,
    566          value,
    567          DOMWindowUtils
    568        );
    569        values.distance = distance / maxObject.distance;
    570      }
    571    }
    572    return properties;
    573  }
    574 
    575  /**
    576   * Get the animation types for a given list of CSS property names.
    577   *
    578   * @param {Array} propertyNames - CSS property names (e.g. background-color)
    579   * @return {object} Returns animation types (e.g. {"background-color": "rgb(0, 0, 0)"}.
    580   */
    581  getAnimationTypes(propertyNames) {
    582    const animationTypes = {};
    583    for (const propertyName of propertyNames) {
    584      animationTypes[propertyName] = getAnimationTypeForLonghand(propertyName);
    585    }
    586    return animationTypes;
    587  }
    588 
    589  /**
    590   * Returns the distance of between value1, value2.
    591   *
    592   * @param {object} target - dom element
    593   * @param {string} propertyName - e.g. transform
    594   * @param {string} value1 - e.g. translate(0px)
    595   * @param {string} value2 - e.g. translate(10px)
    596   * @param {object} DOMWindowUtils
    597   * @param {float} distance
    598   */
    599  getDistance(target, propertyName, value1, value2, DOMWindowUtils) {
    600    if (value1 === value2) {
    601      return 0;
    602    }
    603    try {
    604      const distance = DOMWindowUtils.computeAnimationDistance(
    605        target,
    606        propertyName,
    607        value1,
    608        value2
    609      );
    610      return distance;
    611    } catch (e) {
    612      // We can't compute the distance such the 'discrete' animation,
    613      // 'auto' keyword and so on.
    614      return 0;
    615    }
    616  }
    617 }
    618 
    619 exports.AnimationPlayerActor = AnimationPlayerActor;
    620 
    621 /**
    622 * The Animations actor lists animation players for a given node.
    623 */
    624 exports.AnimationsActor = class AnimationsActor extends Actor {
    625  constructor(conn, targetActor) {
    626    super(conn, animationsSpec);
    627    this.targetActor = targetActor;
    628 
    629    this.onWillNavigate = this.onWillNavigate.bind(this);
    630    this.onNavigate = this.onNavigate.bind(this);
    631    this.onAnimationMutation = this.onAnimationMutation.bind(this);
    632 
    633    this.allAnimationsPaused = false;
    634    this.targetActor.on("will-navigate", this.onWillNavigate);
    635    this.targetActor.on("navigate", this.onNavigate);
    636  }
    637 
    638  destroy() {
    639    super.destroy();
    640    this.targetActor.off("will-navigate", this.onWillNavigate);
    641    this.targetActor.off("navigate", this.onNavigate);
    642 
    643    this.stopAnimationPlayerUpdates();
    644    this.targetActor = this.observer = this.actors = this.walker = null;
    645  }
    646 
    647  /**
    648   * Clients can optionally call this with a reference to their WalkerActor.
    649   * If they do, then AnimationPlayerActor's forms are going to also include
    650   * NodeActor IDs when the corresponding NodeActors do exist.
    651   * This, in turns, is helpful for clients to avoid having to go back once more
    652   * to the server to get a NodeActor for a particular animation.
    653   *
    654   * @param {WalkerActor} walker
    655   */
    656  setWalkerActor(walker) {
    657    this.walker = walker;
    658  }
    659 
    660  /**
    661   * Retrieve the list of AnimationPlayerActor actors for currently running
    662   * animations on a node and its descendants.
    663   * Note that calling this method a second time will destroy all previously
    664   * retrieved AnimationPlayerActors. Indeed, the lifecycle of these actors
    665   * is managed here on the server and tied to getAnimationPlayersForNode
    666   * being called.
    667   *
    668   * @param {NodeActor} nodeActor The NodeActor as defined in
    669   * /devtools/server/actors/inspector
    670   */
    671  getAnimationPlayersForNode(nodeActor) {
    672    let { rawNode } = nodeActor;
    673 
    674    // If the selected node is a ::view-transition child, we want to show all the view-transition
    675    // animations so the user can't play only "parts" of the transition.
    676    const viewTransitionNode = this.#closestViewTransitionNode(rawNode);
    677    if (viewTransitionNode) {
    678      rawNode = viewTransitionNode;
    679    }
    680 
    681    const animations = rawNode.getAnimations({ subtree: true });
    682 
    683    // Destroy previously stored actors
    684    if (this.actors) {
    685      for (const actor of this.actors) {
    686        actor.destroy();
    687      }
    688    }
    689 
    690    this.actors = [];
    691 
    692    for (const animation of animations) {
    693      const createdTime = this.getCreatedTime(animation);
    694      const actor = new AnimationPlayerActor(this, animation, createdTime);
    695      this.actors.push(actor);
    696    }
    697 
    698    // When a front requests the list of players for a node, start listening
    699    // for animation mutations on this node to send updates to the front, until
    700    // either getAnimationPlayersForNode is called again or
    701    // stopAnimationPlayerUpdates is called.
    702    this.stopAnimationPlayerUpdates();
    703    // ownerGlobal doesn't exist in content privileged windows.
    704    // eslint-disable-next-line mozilla/use-ownerGlobal
    705    const win = rawNode.ownerDocument.defaultView;
    706    this.observer = new win.MutationObserver(this.onAnimationMutation);
    707    this.observer.observe(rawNode, {
    708      animations: true,
    709      subtree: true,
    710    });
    711 
    712    return this.actors;
    713  }
    714 
    715  /**
    716   * Returns the passed node closest ::view-transition node if it exists, null otherwise
    717   *
    718   * @param {Element} rawNode
    719   * @returns {Element|null}
    720   */
    721  #closestViewTransitionNode(rawNode) {
    722    const { implementedPseudoElement } = rawNode;
    723    if (
    724      !implementedPseudoElement ||
    725      !implementedPseudoElement?.startsWith("::view-transition")
    726    ) {
    727      return null;
    728    }
    729    // Look up for the root ::view-transition node
    730    while (
    731      rawNode &&
    732      rawNode.implementedPseudoElement &&
    733      rawNode.implementedPseudoElement !== "::view-transition"
    734    ) {
    735      rawNode = rawNode.parentElement;
    736    }
    737 
    738    return rawNode;
    739  }
    740 
    741  onAnimationMutation(mutations) {
    742    const eventData = [];
    743    const readyPromises = [];
    744 
    745    for (const { addedAnimations, removedAnimations } of mutations) {
    746      for (const player of removedAnimations) {
    747        // Note that animations are reported as removed either when they are
    748        // actually removed from the node (e.g. css class removed) or when they
    749        // are finished and don't have forwards animation-fill-mode.
    750        // In the latter case, we don't send an event, because the corresponding
    751        // animation can still be seeked/resumed, so we want the client to keep
    752        // its reference to the AnimationPlayerActor.
    753        if (player.playState !== "idle") {
    754          continue;
    755        }
    756 
    757        const index = this.actors.findIndex(a => a.player === player);
    758        if (index !== -1) {
    759          eventData.push({
    760            type: "removed",
    761            player: this.actors[index],
    762          });
    763          this.actors[index].onAnimationRemoved();
    764          this.actors.splice(index, 1);
    765        }
    766      }
    767 
    768      for (const player of addedAnimations) {
    769        // If the added player already exists, it means we previously filtered
    770        // it out when it was reported as removed. So filter it out here too.
    771        if (this.actors.find(a => a.player === player)) {
    772          continue;
    773        }
    774 
    775        // If the added player has the same name and target node as a player we
    776        // already have, it means it's a transition that's re-starting. So send
    777        // a "removed" event for the one we already have.
    778        const index = this.actors.findIndex(a => {
    779          const isSameType = a.player.constructor === player.constructor;
    780          const isSameName =
    781            (a.isCssAnimation() &&
    782              a.player.animationName === player.animationName) ||
    783            (a.isCssTransition() &&
    784              a.player.transitionProperty === player.transitionProperty);
    785          const isSameNode =
    786            a.player.effect.target === player.effect.target &&
    787            a.player.effect.pseudoElement === player.effect.pseudoElement;
    788 
    789          return isSameType && isSameNode && isSameName;
    790        });
    791        if (index !== -1) {
    792          eventData.push({
    793            type: "removed",
    794            player: this.actors[index],
    795          });
    796          this.actors[index].onAnimationRemoved();
    797          this.actors.splice(index, 1);
    798        }
    799 
    800        const createdTime = this.getCreatedTime(player);
    801        const actor = new AnimationPlayerActor(this, player, createdTime);
    802        this.actors.push(actor);
    803        eventData.push({
    804          type: "added",
    805          player: actor,
    806        });
    807        readyPromises.push(player.ready);
    808      }
    809    }
    810 
    811    if (eventData.length) {
    812      // Let's wait for all added animations to be ready before telling the
    813      // front-end.
    814      Promise.all(readyPromises).then(() => {
    815        this.emit("mutations", eventData);
    816      });
    817    }
    818  }
    819 
    820  /**
    821   * After the client has called getAnimationPlayersForNode for a given DOM
    822   * node, the actor starts sending animation mutations for this node. If the
    823   * client doesn't want this to happen anymore, it should call this method.
    824   */
    825  stopAnimationPlayerUpdates() {
    826    if (this.observer && !Cu.isDeadWrapper(this.observer)) {
    827      this.observer.disconnect();
    828    }
    829  }
    830 
    831  onWillNavigate({ isTopLevel }) {
    832    if (isTopLevel) {
    833      this.stopAnimationPlayerUpdates();
    834    }
    835  }
    836 
    837  onNavigate({ isTopLevel }) {
    838    if (isTopLevel) {
    839      this.allAnimationsPaused = false;
    840    }
    841  }
    842 
    843  /**
    844   * Pause given animations.
    845   *
    846   * @param {Array} actors A list of AnimationPlayerActor.
    847   */
    848  pauseSome(actors) {
    849    const handledActors = [];
    850    for (const actor of actors) {
    851      // The client could call this with actors that we no longer handle, as it might
    852      // not have received the mutations event yet for removed animations.
    853      // In such case, ignore the actor, as pausing the animation again might trigger a
    854      // new mutation, which would cause problems here and on the client.
    855      if (!this.actors.includes(actor)) {
    856        continue;
    857      }
    858      this.pauseSync(actor.player);
    859      handledActors.push(actor);
    860    }
    861 
    862    return this.waitForNextFrame(handledActors);
    863  }
    864 
    865  /**
    866   * Play given animations.
    867   *
    868   * @param {Array} actors A list of AnimationPlayerActor.
    869   */
    870  playSome(actors) {
    871    const handledActors = [];
    872    for (const actor of actors) {
    873      // The client could call this with actors that we no longer handle, as it might
    874      // not have received the mutations event yet for removed animations.
    875      // In such case, ignore the actor, as playing the animation again might trigger a
    876      // new mutation, which would cause problems here and on the client.
    877      if (!this.actors.includes(actor)) {
    878        continue;
    879      }
    880      this.playSync(actor.player);
    881      handledActors.push(actor);
    882    }
    883 
    884    return this.waitForNextFrame(handledActors);
    885  }
    886 
    887  /**
    888   * Set the current time of several animations at the same time.
    889   *
    890   * @param {Array} actors A list of AnimationPlayerActor.
    891   * @param {number} time The new currentTime.
    892   * @param {boolean} shouldPause Should the players be paused too.
    893   */
    894  setCurrentTimes(actors, time, shouldPause) {
    895    const handledActors = [];
    896    for (const actor of actors) {
    897      // The client could call this with actors that we no longer handle, as it might
    898      // not have received the mutations event yet for removed animations.
    899      // In such case, ignore the actor, as setting the time might trigger a
    900      // new mutation, which would cause problems here and on the client.
    901      if (!this.actors.includes(actor)) {
    902        continue;
    903      }
    904      const player = actor.player;
    905 
    906      if (shouldPause) {
    907        player.startTime = null;
    908      }
    909 
    910      const currentTime =
    911        player.playbackRate > 0
    912          ? time - actor.createdTime
    913          : actor.createdTime - time;
    914      player.currentTime = currentTime * Math.abs(player.playbackRate);
    915      handledActors.push(actor);
    916    }
    917 
    918    return this.waitForNextFrame(handledActors);
    919  }
    920 
    921  /**
    922   * Set the playback rate of several animations at the same time.
    923   *
    924   * @param {Array} actors A list of AnimationPlayerActor.
    925   * @param {number} rate The new rate.
    926   */
    927  setPlaybackRates(actors, rate) {
    928    const readyPromises = [];
    929    for (const actor of actors) {
    930      // The client could call this with actors that we no longer handle, as it might
    931      // not have received the mutations event yet for removed animations.
    932      // In such case, ignore the actor, as setting the playback rate might trigger a
    933      // new mutation, which would cause problems here and on the client.
    934      if (!this.actors.includes(actor)) {
    935        continue;
    936      }
    937      actor.player.updatePlaybackRate(rate);
    938      readyPromises.push(actor.player.ready);
    939    }
    940    return Promise.all(readyPromises);
    941  }
    942 
    943  /**
    944   * Pause given player synchronously.
    945   *
    946   * @param {object} player
    947   */
    948  pauseSync(player) {
    949    player.startTime = null;
    950  }
    951 
    952  /**
    953   * Play given player synchronously.
    954   *
    955   * @param {object} player
    956   */
    957  playSync(player) {
    958    if (!player.playbackRate) {
    959      // We can not play with playbackRate zero.
    960      return;
    961    }
    962 
    963    // Play animation in a synchronous fashion by setting the start time directly.
    964    const currentTime = player.currentTime || 0;
    965    player.startTime =
    966      player.timeline.currentTime - currentTime / player.playbackRate;
    967  }
    968 
    969  /**
    970   * Return created fime of given animaiton.
    971   *
    972   * @param {object} animation
    973   */
    974  getCreatedTime(animation) {
    975    return (
    976      animation.startTime ||
    977      animation.timeline.currentTime -
    978        animation.currentTime / animation.playbackRate
    979    );
    980  }
    981 
    982  /**
    983   * Wait for next animation frame.
    984   *
    985   * @param {Array} actors
    986   * @return {Promise} which waits for next frame
    987   */
    988  waitForNextFrame(actors) {
    989    const promises = actors.map(actor => {
    990      const doc = actor.document;
    991      const win = actor.window;
    992      const timeAtCurrent = doc.timeline.currentTime;
    993 
    994      return new Promise(resolve => {
    995        win.requestAnimationFrame(() => {
    996          if (timeAtCurrent === doc.timeline.currentTime) {
    997            win.requestAnimationFrame(resolve);
    998          } else {
    999            resolve();
   1000          }
   1001        });
   1002      });
   1003    });
   1004 
   1005    return Promise.all(promises);
   1006  }
   1007 };