/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/** * Set of actors that expose the Web Animations API to devtools protocol * clients. * * The |Animations| actor is the main entry point. It is used to discover * animation players on given nodes. * There should only be one instance per devtools server. * * The |AnimationPlayer| actor provides attributes and methods to inspect an * animation as well as pause/resume/seek it. * * The Web Animation spec implementation is ongoing in Gecko, and so this set * of actors should evolve when the implementation progresses. * * References: * - WebAnimation spec: * http://drafts.csswg.org/web-animations/ * - WebAnimation WebIDL files: * /dom/webidl/Animation*.webidl
*/
function getAnimationTypeForLonghand(property) { // If this is a custom property, return "custom" for now as it's not straightforward // to retrieve the proper animation type. // TODO: We could compute the animation type from the registered property syntax (Bug 1875435) if (property.startsWith("--")) { return"custom";
}
for (const [type, props] of ANIMATION_TYPE_FOR_LONGHANDS) { if (props.has(property)) { return type;
}
} thrownew Error("Unknown longhand property name");
}
exports.getAnimationTypeForLonghand = getAnimationTypeForLonghand;
/** * The AnimationPlayerActor provides information about a given animation: its * startTime, currentTime, current state, etc. * * Since the state of a player changes as the animation progresses it is often * useful to call getCurrentState at regular intervals to get the current state. * * This actor also allows playing, pausing and seeking the animation.
*/ class AnimationPlayerActor extends Actor { /** * @param {AnimationsActor} The main AnimationsActor instance * @param {AnimationPlayer} The player object returned by getAnimationPlayers * @param {Number} Time which animation created
*/
constructor(animationsActor, player, createdTime) { super(animationsActor.conn, animationPlayerSpec);
// Listen to animation mutations on the node to alert the front when the // current animation changes. // If the node is a pseudo-element, then we listen on its parent with // subtree:true (there's no risk of getting too many notifications in // onAnimationMutation since we filter out events that aren't for the // current animation). this.observer = newthis.window.MutationObserver(this.onAnimationMutation); if (this.isPseudoElement) { this.observer.observe(this.node.parentElement, {
animations: true,
subtree: true,
});
} else { this.observer.observe(this.node, { animations: true });
}
destroy() { // Only try to disconnect the observer if it's not already dead (i.e. if the // container view hasn't navigated since). if (this.observer && !Cu.isDeadWrapper(this.observer)) { this.observer.disconnect();
} this.player = this.observer = this.walker = null;
super.destroy();
}
get isPseudoElement() { return !!this.player.effect.pseudoElement;
}
get pseudoElemenName() { if (!this.isPseudoElement) { returnnull;
}
// When the animated node is a pseudo-element, we need to walk the children // of the target node and look for it. for (
let next = treeWalker.firstChild();
next;
next = treeWalker.nextSibling()
) { if (next.nodeName === pseudoElementName) { return next;
}
}
console.warn(
`Pseudo element ${this.player.effect.pseudoElement} is not found`
); return originatingElem;
}
get document() { returnthis.node.ownerDocument;
}
get window() { returnthis.document.defaultView;
}
/** * Release the actor, when it isn't needed anymore. * Protocol.js uses this release method to call the destroy method.
*/
release() {}
form() { const data = this.getCurrentState();
data.actor = this.actorID;
// If we know the WalkerActor, and if the animated node is known by it, then // return its corresponding NodeActor ID too. if (this.walker && this.walker.hasNode(this.node)) {
data.animationTargetNodeActorID = this.walker.getNode(this.node).actorID;
}
return data;
}
isCssAnimation(player = this.player) { return player instanceofthis.window.CSSAnimation;
}
isCssTransition(player = this.player) { return player instanceofthis.window.CSSTransition;
}
isScriptAnimation(player = this.player) { return (
player instanceofthis.window.Animation &&
!(
player instanceofthis.window.CSSAnimation ||
player instanceofthis.window.CSSTransition
)
);
}
/** * Get the name of this animation. This can be either the animation.id * property if it was set, or the keyframe rule name or the transition * property. * @return {String}
*/
getName() { if (this.player.id) { returnthis.player.id;
} elseif (this.isCssAnimation()) { returnthis.player.animationName;
} elseif (this.isCssTransition()) { returnthis.player.transitionProperty;
}
return"";
}
/** * Get the animation duration from this player, in milliseconds. * @return {Number}
*/
getDuration() { returnthis.player.effect.getComputedTiming().duration;
}
/** * Get the animation delay from this player, in milliseconds. * @return {Number}
*/
getDelay() { returnthis.player.effect.getComputedTiming().delay;
}
/** * Get the animation endDelay from this player, in milliseconds. * @return {Number}
*/
getEndDelay() { returnthis.player.effect.getComputedTiming().endDelay;
}
/** * Get the animation iteration count for this player. That is, how many times * is the animation scheduled to run. * @return {Number} The number of iterations, or null if the animation repeats * infinitely.
*/
getIterationCount() { const iterations = this.player.effect.getComputedTiming().iterations; return iterations === Infinity ? null : iterations;
}
/** * Get the animation iterationStart from this player, in ratio. * That is offset of starting position of the animation. * @return {Number}
*/
getIterationStart() { returnthis.player.effect.getComputedTiming().iterationStart;
}
/** * Get the animation easing from this player. * @return {String}
*/
getEasing() { returnthis.player.effect.getComputedTiming().easing;
}
/** * Get the animation fill mode from this player. * @return {String}
*/
getFill() { returnthis.player.effect.getComputedTiming().fill;
}
/** * Get the animation direction from this player. * @return {String}
*/
getDirection() { returnthis.player.effect.getComputedTiming().direction;
}
/** * Get animation-timing-function from animated element if CSS Animations. * @return {String}
*/
getAnimationTimingFunction() { if (!this.isCssAnimation()) { returnnull;
}
let pseudo = null;
let target = this.player.effect.target; if (target.type) { // Animated element is a pseudo element.
pseudo = target.type;
target = target.element;
} returnthis.window.getComputedStyle(target, pseudo).animationTimingFunction;
}
/** * Return the current start of the Animation. * @return {Object}
*/
getState() { const compositorStatus = this.getPropertiesCompositorStatus(); // Note that if you add a new property to the state object, make sure you // add the corresponding property in the AnimationPlayerFront' initialState // getter. return {
type: this.getType(), // startTime is null whenever the animation is paused or waiting to start.
startTime: this.player.startTime,
currentTime: this.player.currentTime,
playState: this.player.playState,
playbackRate: this.player.playbackRate,
name: this.getName(),
duration: this.getDuration(),
delay: this.getDelay(),
endDelay: this.getEndDelay(),
iterationCount: this.getIterationCount(),
iterationStart: this.getIterationStart(),
fill: this.getFill(),
easing: this.getEasing(),
direction: this.getDirection(),
animationTimingFunction: this.getAnimationTimingFunction(), // animation is hitting the fast path or not. Returns false whenever the // animation is paused as it is taken off the compositor then.
isRunningOnCompositor: compositorStatus.some(
propState => propState.runningOnCompositor
),
propertyState: compositorStatus, // The document timeline's currentTime is being sent along too. This is // not strictly related to the node's animationPlayer, but is useful to // know the current time of the animation with respect to the document's.
documentCurrentTime: this.node.ownerDocument.timeline.currentTime, // The time which this animation created.
createdTime: this.createdTime, // The time which an animation's current time when this animation has created.
currentTimeAtCreated: this.currentTimeAtCreated,
properties: this.getProperties(),
};
}
/** * Get the current state of the AnimationPlayer (currentTime, playState, ...). * Note that the initial state is returned as the form of this actor when it * is initialized. * This protocol method only returns a trimed down version of this state in * case some properties haven't changed since last time (since the front can * reconstruct those). If you want the full state, use the getState method. * @return {Object}
*/
getCurrentState() { const newState = this.getState();
// If we've saved a state before, compare and only send what has changed. // It's expected of the front to also save old states to re-construct the // full state when an incomplete one is received. // This is to minimize protocol traffic.
let sentState = {}; if (this.currentState) { for (const key in newState) { if ( typeofthis.currentState[key] === "undefined" || this.currentState[key] !== newState[key]
) {
sentState[key] = newState[key];
}
}
} else {
sentState = newState;
} this.currentState = newState;
return sentState;
}
/** * Executed when the current animation changes, used to emit the new state * the the front.
*/
onAnimationMutation(mutations) { const isCurrentAnimation = animation => animation === this.player; const hasCurrentAnimation = animations =>
animations.some(isCurrentAnimation);
let hasChanged = false;
for (const { removedAnimations, changedAnimations } of mutations) { if (hasCurrentAnimation(removedAnimations)) { // Reset the local copy of the state on removal, since the animation can // be kept on the client and re-added, its state needs to be sent in // full. this.currentState = null;
}
if (hasCurrentAnimation(changedAnimations)) { // Only consider the state has having changed if any of effect timing properties, // animationTimingFunction or playbackRate has changed. const newState = this.getState(); const oldState = this.currentState;
hasChanged =
newState.delay !== oldState.delay ||
newState.iterationCount !== oldState.iterationCount ||
newState.iterationStart !== oldState.iterationStart ||
newState.duration !== oldState.duration ||
newState.endDelay !== oldState.endDelay ||
newState.direction !== oldState.direction ||
newState.easing !== oldState.easing ||
newState.fill !== oldState.fill ||
newState.animationTimingFunction !==
oldState.animationTimingFunction ||
newState.playbackRate !== oldState.playbackRate; break;
}
}
if (hasChanged) { this.emit("changed", this.getCurrentState());
}
}
/** * Get data about the animated properties of this animation player. * @return {Array} Returns a list of animated properties. * Each property contains a list of values, their offsets and distances.
*/
getProperties() { const properties = this.player.effect.getProperties().map(property => { return { name: property.property, values: property.values };
});
const DOMWindowUtils = this.window.windowUtils;
// Fill missing keyframe with computed value. for (const property of properties) {
let underlyingValue = null; // Check only 0% and 100% keyframes.
[0, property.values.length - 1].forEach(index => { const values = property.values[index]; if (values.value !== undefined) { return;
} if (!underlyingValue) {
let pseudo = null;
let target = this.player.effect.target; if (target.type) { // This target is a pseudo element.
pseudo = target.type;
target = target.element;
} const value = DOMWindowUtils.getUnanimatedComputedStyle(
target,
pseudo,
property.name,
DOMWindowUtils.FLUSH_NONE
); const animationType = getAnimationTypeForLonghand(property.name);
underlyingValue =
animationType === "float" ? parseFloat(value, 10) : value;
}
values.value = underlyingValue;
});
}
// Calculate the distance. for (const property of properties) { const propertyName = property.name; const maxObject = { distance: -1 }; for (let i = 0; i < property.values.length - 1; i++) { const value1 = property.values[i].value; for (let j = i + 1; j < property.values.length; j++) { const value2 = property.values[j].value; const distance = this.getDistance( this.node,
propertyName,
value1,
value2,
DOMWindowUtils
); if (maxObject.distance >= distance) { continue;
}
maxObject.distance = distance;
maxObject.value1 = value1;
maxObject.value2 = value2;
}
} if (maxObject.distance === 0) { // Distance is zero means that no values change or can't calculate the distance. // In this case, we use the keyframe offset as the distance.
property.values.reduce((previous, current) => { // If the current value is same as previous value, use previous distance.
current.distance =
current.value === previous.value
? previous.distance
: current.offset; return current;
}, property.values[0]); continue;
} const baseValue =
maxObject.value1 < maxObject.value2
? maxObject.value1
: maxObject.value2; for (const values of property.values) { const value = values.value; const distance = this.getDistance( this.node,
propertyName,
baseValue,
value,
DOMWindowUtils
);
values.distance = distance / maxObject.distance;
}
} return properties;
}
/** * Get the animation types for a given list of CSS property names. * @param {Array} propertyNames - CSS property names (e.g. background-color) * @return {Object} Returns animation types (e.g. {"background-color": "rgb(0, 0, 0)"}.
*/
getAnimationTypes(propertyNames) { const animationTypes = {}; for (const propertyName of propertyNames) {
animationTypes[propertyName] = getAnimationTypeForLonghand(propertyName);
} return animationTypes;
}
/** * Returns the distance of between value1, value2. * @param {Object} target - dom element * @param {String} propertyName - e.g. transform * @param {String} value1 - e.g. translate(0px) * @param {String} value2 - e.g. translate(10px) * @param {Object} DOMWindowUtils * @param {float} distance
*/
getDistance(target, propertyName, value1, value2, DOMWindowUtils) { if (value1 === value2) { return 0;
} try { const distance = DOMWindowUtils.computeAnimationDistance(
target,
propertyName,
value1,
value2
); return distance;
} catch (e) { // We can't compute the distance such the 'discrete' animation, // 'auto' keyword and so on. return 0;
}
}
}
/** * The Animations actor lists animation players for a given node.
*/
exports.AnimationsActor = class AnimationsActor extends Actor {
constructor(conn, targetActor) { super(conn, animationsSpec); this.targetActor = targetActor;
/** * Clients can optionally call this with a reference to their WalkerActor. * If they do, then AnimationPlayerActor's forms are going to also include * NodeActor IDs when the corresponding NodeActors do exist. * This, in turns, is helpful for clients to avoid having to go back once more * to the server to get a NodeActor for a particular animation. * @param {WalkerActor} walker
*/
setWalkerActor(walker) { this.walker = walker;
}
/** * Retrieve the list of AnimationPlayerActor actors for currently running * animations on a node and its descendants. * Note that calling this method a second time will destroy all previously * retrieved AnimationPlayerActors. Indeed, the lifecycle of these actors * is managed here on the server and tied to getAnimationPlayersForNode * being called. * @param {NodeActor} nodeActor The NodeActor as defined in * /devtools/server/actors/inspector
*/
getAnimationPlayersForNode(nodeActor) { const animations = nodeActor.rawNode.getAnimations({ subtree: true });
// Destroy previously stored actors if (this.actors) { for (const actor of this.actors) {
actor.destroy();
}
}
this.actors = [];
for (const animation of animations) { const createdTime = this.getCreatedTime(animation); const actor = new AnimationPlayerActor(this, animation, createdTime); this.actors.push(actor);
}
// When a front requests the list of players for a node, start listening // for animation mutations on this node to send updates to the front, until // either getAnimationPlayersForNode is called again or // stopAnimationPlayerUpdates is called. this.stopAnimationPlayerUpdates(); // ownerGlobal doesn't exist in content privileged windows. // eslint-disable-next-line mozilla/use-ownerGlobal const win = nodeActor.rawNode.ownerDocument.defaultView; this.observer = new win.MutationObserver(this.onAnimationMutation); this.observer.observe(nodeActor.rawNode, {
animations: true,
subtree: true,
});
for (const { addedAnimations, removedAnimations } of mutations) { for (const player of removedAnimations) { // Note that animations are reported as removed either when they are // actually removed from the node (e.g. css class removed) or when they // are finished and don't have forwards animation-fill-mode. // In the latter case, we don't send an event, because the corresponding // animation can still be seeked/resumed, so we want the client to keep // its reference to the AnimationPlayerActor. if (player.playState !== "idle") { continue;
}
for (const player of addedAnimations) { // If the added player already exists, it means we previously filtered // it out when it was reported as removed. So filter it out here too. if (this.actors.find(a => a.player === player)) { continue;
}
// If the added player has the same name and target node as a player we // already have, it means it's a transition that's re-starting. So send // a "removed" event for the one we already have. const index = this.actors.findIndex(a => { const isSameType = a.player.constructor === player.constructor; const isSameName =
(a.isCssAnimation() &&
a.player.animationName === player.animationName) ||
(a.isCssTransition() &&
a.player.transitionProperty === player.transitionProperty); const isSameNode = a.player.effect.target === player.effect.target;
if (eventData.length) { // Let's wait for all added animations to be ready before telling the // front-end.
Promise.all(readyPromises).then(() => { this.emit("mutations", eventData);
});
}
}
/** * After the client has called getAnimationPlayersForNode for a given DOM * node, the actor starts sending animation mutations for this node. If the * client doesn't want this to happen anymore, it should call this method.
*/
stopAnimationPlayerUpdates() { if (this.observer && !Cu.isDeadWrapper(this.observer)) { this.observer.disconnect();
}
}
onWillNavigate({ isTopLevel }) { if (isTopLevel) { this.stopAnimationPlayerUpdates();
}
}
/** * Pause given animations. * * @param {Array} actors A list of AnimationPlayerActor.
*/
pauseSome(actors) { for (const { player } of actors) { this.pauseSync(player);
}
returnthis.waitForNextFrame(actors);
}
/** * Play given animations. * * @param {Array} actors A list of AnimationPlayerActor.
*/
playSome(actors) { for (const { player } of actors) { this.playSync(player);
}
returnthis.waitForNextFrame(actors);
}
/** * Set the current time of several animations at the same time. * @param {Array} players A list of AnimationPlayerActor. * @param {Number} time The new currentTime. * @param {Boolean} shouldPause Should the players be paused too.
*/
setCurrentTimes(players, time, shouldPause) { for (const actor of players) { const player = actor.player;
/** * Set the playback rate of several animations at the same time. * @param {Array} actors A list of AnimationPlayerActor. * @param {Number} rate The new rate.
*/
setPlaybackRates(players, rate) { return Promise.all(
players.map(({ player }) => {
player.updatePlaybackRate(rate); return player.ready;
})
);
}
/** * Pause given player synchronously. * * @param {Object} player
*/
pauseSync(player) {
player.startTime = null;
}
/** * Play given player synchronously. * * @param {Object} player
*/
playSync(player) { if (!player.playbackRate) { // We can not play with playbackRate zero. return;
}
// Play animation in a synchronous fashion by setting the start time directly. const currentTime = player.currentTime || 0;
player.startTime =
player.timeline.currentTime - currentTime / player.playbackRate;
}
/** * Return created fime of given animaiton. * * @param {Object} animation
*/
getCreatedTime(animation) { return (
animation.startTime ||
animation.timeline.currentTime -
animation.currentTime / animation.playbackRate
);
}
/** * Wait for next animation frame. * * @param {Array} actors * @return {Promise} which waits for next frame
*/
waitForNextFrame(actors) { const promises = actors.map(actor => { const doc = actor.document; const win = actor.window; const timeAtCurrent = doc.timeline.currentTime;
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.