summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/animation/animation.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/animation/animation.js802
1 files changed, 802 insertions, 0 deletions
diff --git a/devtools/client/inspector/animation/animation.js b/devtools/client/inspector/animation/animation.js
new file mode 100644
index 0000000000..c0f5d389d0
--- /dev/null
+++ b/devtools/client/inspector/animation/animation.js
@@ -0,0 +1,802 @@
+/* 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";
+
+const {
+ createElement,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ Provider,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+const App = createFactory(
+ require("resource://devtools/client/inspector/animation/components/App.js")
+);
+const CurrentTimeTimer = require("resource://devtools/client/inspector/animation/current-time-timer.js");
+
+const animationsReducer = require("resource://devtools/client/inspector/animation/reducers/animations.js");
+const {
+ updateAnimations,
+ updateDetailVisibility,
+ updateElementPickerEnabled,
+ updateHighlightedNode,
+ updatePlaybackRates,
+ updateSelectedAnimation,
+ updateSidebarSize,
+} = require("resource://devtools/client/inspector/animation/actions/animations.js");
+const {
+ hasAnimationIterationCountInfinite,
+ hasRunningAnimation,
+} = require("resource://devtools/client/inspector/animation/utils/utils.js");
+
+class AnimationInspector {
+ constructor(inspector, win) {
+ this.inspector = inspector;
+ this.win = win;
+
+ this.inspector.store.injectReducer("animations", animationsReducer);
+
+ this.addAnimationsCurrentTimeListener =
+ this.addAnimationsCurrentTimeListener.bind(this);
+ this.getAnimatedPropertyMap = this.getAnimatedPropertyMap.bind(this);
+ this.getAnimationsCurrentTime = this.getAnimationsCurrentTime.bind(this);
+ this.getComputedStyle = this.getComputedStyle.bind(this);
+ this.getNodeFromActor = this.getNodeFromActor.bind(this);
+ this.removeAnimationsCurrentTimeListener =
+ this.removeAnimationsCurrentTimeListener.bind(this);
+ this.rewindAnimationsCurrentTime =
+ this.rewindAnimationsCurrentTime.bind(this);
+ this.selectAnimation = this.selectAnimation.bind(this);
+ this.setAnimationsCurrentTime = this.setAnimationsCurrentTime.bind(this);
+ this.setAnimationsPlaybackRate = this.setAnimationsPlaybackRate.bind(this);
+ this.setAnimationsPlayState = this.setAnimationsPlayState.bind(this);
+ this.setDetailVisibility = this.setDetailVisibility.bind(this);
+ this.setHighlightedNode = this.setHighlightedNode.bind(this);
+ this.setSelectedNode = this.setSelectedNode.bind(this);
+ this.simulateAnimation = this.simulateAnimation.bind(this);
+ this.simulateAnimationForKeyframesProgressBar =
+ this.simulateAnimationForKeyframesProgressBar.bind(this);
+ this.toggleElementPicker = this.toggleElementPicker.bind(this);
+ this.update = this.update.bind(this);
+ this.onAnimationStateChanged = this.onAnimationStateChanged.bind(this);
+ this.onAnimationsCurrentTimeUpdated =
+ this.onAnimationsCurrentTimeUpdated.bind(this);
+ this.onAnimationsMutation = this.onAnimationsMutation.bind(this);
+ this.onCurrentTimeTimerUpdated = this.onCurrentTimeTimerUpdated.bind(this);
+ this.onElementPickerStarted = this.onElementPickerStarted.bind(this);
+ this.onElementPickerStopped = this.onElementPickerStopped.bind(this);
+ this.onNavigate = this.onNavigate.bind(this);
+ this.onSidebarResized = this.onSidebarResized.bind(this);
+ this.onSidebarSelectionChanged = this.onSidebarSelectionChanged.bind(this);
+ this.onTargetAvailable = this.onTargetAvailable.bind(this);
+
+ EventEmitter.decorate(this);
+ this.emitForTests = this.emitForTests.bind(this);
+
+ this.initComponents();
+ this.initListeners();
+ }
+
+ initComponents() {
+ const {
+ addAnimationsCurrentTimeListener,
+ emitForTests: emitEventForTest,
+ getAnimatedPropertyMap,
+ getAnimationsCurrentTime,
+ getComputedStyle,
+ getNodeFromActor,
+ isAnimationsRunning,
+ removeAnimationsCurrentTimeListener,
+ rewindAnimationsCurrentTime,
+ selectAnimation,
+ setAnimationsCurrentTime,
+ setAnimationsPlaybackRate,
+ setAnimationsPlayState,
+ setDetailVisibility,
+ setHighlightedNode,
+ setSelectedNode,
+ simulateAnimation,
+ simulateAnimationForKeyframesProgressBar,
+ toggleElementPicker,
+ } = this;
+
+ const direction = this.win.document.dir;
+
+ this.animationsCurrentTimeListeners = [];
+ this.isCurrentTimeSet = false;
+
+ const provider = createElement(
+ Provider,
+ {
+ id: "animationinspector",
+ key: "animationinspector",
+ store: this.inspector.store,
+ },
+ App({
+ addAnimationsCurrentTimeListener,
+ direction,
+ emitEventForTest,
+ getAnimatedPropertyMap,
+ getAnimationsCurrentTime,
+ getComputedStyle,
+ getNodeFromActor,
+ isAnimationsRunning,
+ removeAnimationsCurrentTimeListener,
+ rewindAnimationsCurrentTime,
+ selectAnimation,
+ setAnimationsCurrentTime,
+ setAnimationsPlaybackRate,
+ setAnimationsPlayState,
+ setDetailVisibility,
+ setHighlightedNode,
+ setSelectedNode,
+ simulateAnimation,
+ simulateAnimationForKeyframesProgressBar,
+ toggleElementPicker,
+ })
+ );
+ this.provider = provider;
+ }
+
+ async initListeners() {
+ await this.inspector.commands.targetCommand.watchTargets({
+ types: [this.inspector.commands.targetCommand.TYPES.FRAME],
+ onAvailable: this.onTargetAvailable,
+ });
+
+ this.inspector.on("new-root", this.onNavigate);
+ this.inspector.selection.on("new-node-front", this.update);
+ this.inspector.sidebar.on("select", this.onSidebarSelectionChanged);
+ this.inspector.toolbox.on("select", this.onSidebarSelectionChanged);
+ this.inspector.toolbox.on(
+ "inspector-sidebar-resized",
+ this.onSidebarResized
+ );
+ this.inspector.toolbox.nodePicker.on(
+ "picker-started",
+ this.onElementPickerStarted
+ );
+ this.inspector.toolbox.nodePicker.on(
+ "picker-stopped",
+ this.onElementPickerStopped
+ );
+ }
+
+ destroy() {
+ this.setAnimationStateChangedListenerEnabled(false);
+ this.inspector.off("new-root", this.onNavigate);
+ this.inspector.selection.off("new-node-front", this.update);
+ this.inspector.sidebar.off("select", this.onSidebarSelectionChanged);
+ this.inspector.toolbox.off(
+ "inspector-sidebar-resized",
+ this.onSidebarResized
+ );
+ this.inspector.toolbox.nodePicker.off(
+ "picker-started",
+ this.onElementPickerStarted
+ );
+ this.inspector.toolbox.nodePicker.off(
+ "picker-stopped",
+ this.onElementPickerStopped
+ );
+ this.inspector.toolbox.off("select", this.onSidebarSelectionChanged);
+
+ if (this.animationsFront) {
+ this.animationsFront.off("mutations", this.onAnimationsMutation);
+ }
+
+ if (this.simulatedAnimation) {
+ this.simulatedAnimation.cancel();
+ this.simulatedAnimation = null;
+ }
+
+ if (this.simulatedElement) {
+ this.simulatedElement.remove();
+ this.simulatedElement = null;
+ }
+
+ if (this.simulatedAnimationForKeyframesProgressBar) {
+ this.simulatedAnimationForKeyframesProgressBar.cancel();
+ this.simulatedAnimationForKeyframesProgressBar = null;
+ }
+
+ this.stopAnimationsCurrentTimeTimer();
+
+ this.inspector = null;
+ this.win = null;
+ }
+
+ get state() {
+ return this.inspector.store.getState().animations;
+ }
+
+ addAnimationsCurrentTimeListener(listener) {
+ this.animationsCurrentTimeListeners.push(listener);
+ }
+
+ /**
+ * This function calls AnimationsFront.setCurrentTimes with considering the createdTime.
+ *
+ * @param {Number} currentTime
+ */
+ async doSetCurrentTimes(currentTime) {
+ const { animations, timeScale } = this.state;
+ currentTime = currentTime + timeScale.minStartTime;
+ await this.animationsFront.setCurrentTimes(animations, currentTime, true, {
+ relativeToCreatedTime: true,
+ });
+ }
+
+ /**
+ * Return a map of animated property from given animation actor.
+ *
+ * @param {Object} animation
+ * @return {Map} A map of animated property
+ * key: {String} Animated property name
+ * value: {Array} Array of keyframe object
+ * Also, the keyframe object is consisted as following.
+ * {
+ * value: {String} style,
+ * offset: {Number} offset of keyframe,
+ * easing: {String} easing from this keyframe to next keyframe,
+ * distance: {Number} use as y coordinate in graph,
+ * }
+ */
+ getAnimatedPropertyMap(animation) {
+ const properties = animation.state.properties;
+ const animatedPropertyMap = new Map();
+
+ for (const { name, values } of properties) {
+ const keyframes = values.map(
+ ({ value, offset, easing, distance = 0 }) => {
+ offset = parseFloat(offset.toFixed(3));
+ return { value, offset, easing, distance };
+ }
+ );
+
+ animatedPropertyMap.set(name, keyframes);
+ }
+
+ return animatedPropertyMap;
+ }
+
+ getAnimationsCurrentTime() {
+ return this.currentTime;
+ }
+
+ /**
+ * Return the computed style of the specified property after setting the given styles
+ * to the simulated element.
+ *
+ * @param {String} property
+ * CSS property name (e.g. text-align).
+ * @param {Object} styles
+ * Map of CSS property name and value.
+ * @return {String}
+ * Computed style of property.
+ */
+ getComputedStyle(property, styles) {
+ this.simulatedElement.style.cssText = "";
+
+ for (const propertyName in styles) {
+ this.simulatedElement.style.setProperty(
+ propertyName,
+ styles[propertyName]
+ );
+ }
+
+ return this.win
+ .getComputedStyle(this.simulatedElement)
+ .getPropertyValue(property);
+ }
+
+ getNodeFromActor(actorID) {
+ if (!this.inspector) {
+ return Promise.reject("Animation inspector already destroyed");
+ }
+
+ return this.inspector.walker.getNodeFromActor(actorID, ["node"]);
+ }
+
+ isPanelVisible() {
+ return (
+ this.inspector &&
+ this.inspector.toolbox &&
+ this.inspector.sidebar &&
+ this.inspector.toolbox.currentToolId === "inspector" &&
+ this.inspector.sidebar.getCurrentTabID() === "animationinspector"
+ );
+ }
+
+ onAnimationStateChanged() {
+ // Simply update the animations since the state has already been updated.
+ this.fireUpdateAction([...this.state.animations]);
+ }
+
+ /**
+ * This method should call when the current time is changed.
+ * Then, dispatches the current time to listeners that are registered
+ * by addAnimationsCurrentTimeListener.
+ *
+ * @param {Number} currentTime
+ */
+ onAnimationsCurrentTimeUpdated(currentTime) {
+ this.currentTime = currentTime;
+
+ for (const listener of this.animationsCurrentTimeListeners) {
+ listener(currentTime);
+ }
+ }
+
+ /**
+ * This method is called when the current time proceed by CurrentTimeTimer.
+ *
+ * @param {Number} currentTime
+ * @param {Bool} shouldStop
+ */
+ onCurrentTimeTimerUpdated(currentTime, shouldStop) {
+ if (shouldStop) {
+ this.setAnimationsCurrentTime(currentTime, true);
+ } else {
+ this.onAnimationsCurrentTimeUpdated(currentTime);
+ }
+ }
+
+ async onAnimationsMutation(changes) {
+ let animations = [...this.state.animations];
+ const addedAnimations = [];
+
+ for (const { type, player: animation } of changes) {
+ if (type === "added") {
+ if (!animation.state.type) {
+ // This animation was added but removed immediately.
+ continue;
+ }
+
+ addedAnimations.push(animation);
+ animation.on("changed", this.onAnimationStateChanged);
+ } else if (type === "removed") {
+ const index = animations.indexOf(animation);
+
+ if (index < 0) {
+ // This animation was added but removed immediately.
+ continue;
+ }
+
+ animations.splice(index, 1);
+ animation.off("changed", this.onAnimationStateChanged);
+ }
+ }
+
+ // Update existing other animations as well since the currentTime would be proceeded
+ // sice the scrubber position is related the currentTime.
+ // Also, don't update the state of removed animations since React components
+ // may refer to the same instance still.
+ try {
+ animations = await this.refreshAnimationsState(animations);
+ } catch (_) {
+ console.error(`Updating Animations failed`);
+ return;
+ }
+
+ this.fireUpdateAction(animations.concat(addedAnimations));
+ }
+
+ onElementPickerStarted() {
+ this.inspector.store.dispatch(updateElementPickerEnabled(true));
+ }
+
+ onElementPickerStopped() {
+ this.inspector.store.dispatch(updateElementPickerEnabled(false));
+ }
+
+ onNavigate() {
+ if (!this.isPanelVisible()) {
+ return;
+ }
+
+ this.inspector.store.dispatch(updatePlaybackRates());
+ }
+
+ async onSidebarSelectionChanged() {
+ const isPanelVisibled = this.isPanelVisible();
+
+ if (this.wasPanelVisibled === isPanelVisibled) {
+ // onSidebarSelectionChanged is called some times even same state
+ // from sidebar and toolbar.
+ return;
+ }
+
+ this.wasPanelVisibled = isPanelVisibled;
+
+ if (this.isPanelVisible()) {
+ await this.update();
+ this.onSidebarResized(null, this.inspector.getSidebarSize());
+ } else {
+ this.stopAnimationsCurrentTimeTimer();
+ this.setAnimationStateChangedListenerEnabled(false);
+ }
+ }
+
+ onSidebarResized(size) {
+ if (!this.isPanelVisible()) {
+ return;
+ }
+
+ this.inspector.store.dispatch(updateSidebarSize(size));
+ }
+
+ async onTargetAvailable({ targetFront }) {
+ if (targetFront.isTopLevel) {
+ this.animationsFront = await targetFront.getFront("animations");
+ this.animationsFront.setWalkerActor(this.inspector.walker);
+ this.animationsFront.on("mutations", this.onAnimationsMutation);
+
+ await this.update();
+ }
+ }
+
+ removeAnimationsCurrentTimeListener(listener) {
+ this.animationsCurrentTimeListeners =
+ this.animationsCurrentTimeListeners.filter(l => l !== listener);
+ }
+
+ async rewindAnimationsCurrentTime() {
+ const { timeScale } = this.state;
+ await this.setAnimationsCurrentTime(timeScale.zeroPositionTime, true);
+ }
+
+ selectAnimation(animation) {
+ this.inspector.store.dispatch(updateSelectedAnimation(animation));
+ }
+
+ async setSelectedNode(nodeFront) {
+ if (this.inspector.selection.nodeFront === nodeFront) {
+ return;
+ }
+
+ await this.inspector
+ .getCommonComponentProps()
+ .setSelectedNode(nodeFront, { reason: "animation-panel" });
+ }
+
+ async setAnimationsCurrentTime(currentTime, shouldRefresh) {
+ this.stopAnimationsCurrentTimeTimer();
+ this.onAnimationsCurrentTimeUpdated(currentTime);
+
+ if (!shouldRefresh && this.isCurrentTimeSet) {
+ return;
+ }
+
+ let animations = this.state.animations;
+ this.isCurrentTimeSet = true;
+
+ try {
+ await this.doSetCurrentTimes(currentTime);
+ animations = await this.refreshAnimationsState(animations);
+ } catch (e) {
+ // Expected if we've already been destroyed or other node have been selected
+ // in the meantime.
+ console.error(e);
+ return;
+ }
+
+ this.isCurrentTimeSet = false;
+
+ if (shouldRefresh) {
+ this.fireUpdateAction(animations);
+ }
+ }
+
+ async setAnimationsPlaybackRate(playbackRate) {
+ if (!this.inspector) {
+ return; // Already destroyed or another node selected.
+ }
+
+ let animations = this.state.animations;
+ // "changed" event on each animation will fire respectively when the playback
+ // rate changed. Since for each occurrence of event, change of UI is urged.
+ // To avoid this, disable the listeners once in order to not capture the event.
+ this.setAnimationStateChangedListenerEnabled(false);
+ try {
+ await this.animationsFront.setPlaybackRates(animations, playbackRate);
+ animations = await this.refreshAnimationsState(animations);
+ } catch (e) {
+ // Expected if we've already been destroyed or another node has been
+ // selected in the meantime.
+ console.error(e);
+ return;
+ } finally {
+ this.setAnimationStateChangedListenerEnabled(true);
+ }
+
+ if (animations) {
+ await this.fireUpdateAction(animations);
+ }
+ }
+
+ async setAnimationsPlayState(doPlay) {
+ if (!this.inspector) {
+ return; // Already destroyed or another node selected.
+ }
+
+ let { animations, timeScale } = this.state;
+
+ try {
+ if (
+ doPlay &&
+ animations.every(
+ animation =>
+ timeScale.getEndTime(animation) <= animation.state.currentTime
+ )
+ ) {
+ await this.doSetCurrentTimes(timeScale.zeroPositionTime);
+ }
+
+ if (doPlay) {
+ await this.animationsFront.playSome(animations);
+ } else {
+ await this.animationsFront.pauseSome(animations);
+ }
+
+ animations = await this.refreshAnimationsState(animations);
+ } catch (e) {
+ // Expected if we've already been destroyed or other node have been selected
+ // in the meantime.
+ console.error(e);
+ return;
+ }
+
+ await this.fireUpdateAction(animations);
+ }
+
+ /**
+ * Enable/disable the animation state change listener.
+ * If set true, observe "changed" event on current animations.
+ * Otherwise, quit observing the "changed" event.
+ *
+ * @param {Bool} isEnabled
+ */
+ setAnimationStateChangedListenerEnabled(isEnabled) {
+ if (!this.inspector) {
+ return; // Already destroyed.
+ }
+ if (isEnabled) {
+ for (const animation of this.state.animations) {
+ animation.on("changed", this.onAnimationStateChanged);
+ }
+ } else {
+ for (const animation of this.state.animations) {
+ animation.off("changed", this.onAnimationStateChanged);
+ }
+ }
+ }
+
+ setDetailVisibility(isVisible) {
+ this.inspector.store.dispatch(updateDetailVisibility(isVisible));
+ }
+
+ /**
+ * Persistently highlight the given node identified with a unique selector.
+ * If no node is provided, hide any persistent highlighter.
+ *
+ * @param {NodeFront} nodeFront
+ */
+ async setHighlightedNode(nodeFront) {
+ await this.inspector.highlighters.hideHighlighterType(
+ this.inspector.highlighters.TYPES.SELECTOR
+ );
+
+ if (nodeFront) {
+ const selector = await nodeFront.getUniqueSelector();
+ if (!selector) {
+ console.warn(
+ `Couldn't get unique selector for NodeFront: ${nodeFront.actorID}`
+ );
+ return;
+ }
+
+ /**
+ * NOTE: Using a Selector Highlighter here because only one Box Model Highlighter
+ * can be visible at a time. The Box Model Highlighter is shown when hovering nodes
+ * which would cause this persistent highlighter to be hidden unexpectedly.
+ * This limitation of one highlighter type a time should be solved by switching
+ * to a highlighter by role approach (Bug 1663443).
+ */
+ await this.inspector.highlighters.showHighlighterTypeForNode(
+ this.inspector.highlighters.TYPES.SELECTOR,
+ nodeFront,
+ {
+ hideInfoBar: true,
+ hideGuides: true,
+ selector,
+ }
+ );
+ }
+
+ this.inspector.store.dispatch(updateHighlightedNode(nodeFront));
+ }
+
+ /**
+ * Returns simulatable animation by given parameters.
+ * The returned animation is implementing Animation interface of Web Animation API.
+ * https://drafts.csswg.org/web-animations/#the-animation-interface
+ *
+ * @param {Array} keyframes
+ * e.g. [{ opacity: 0 }, { opacity: 1 }]
+ * @param {Object} effectTiming
+ * e.g. { duration: 1000, fill: "both" }
+ * @param {Boolean} isElementNeeded
+ * true: create animation with an element.
+ * If want to know computed value of the element, turn on.
+ * false: create animation without an element,
+ * If need to know only timing progress.
+ * @return {Animation}
+ * https://drafts.csswg.org/web-animations/#the-animation-interface
+ */
+ simulateAnimation(keyframes, effectTiming, isElementNeeded) {
+ // Don't simulate animation if the animation inspector is already destroyed.
+ if (!this.win) {
+ return null;
+ }
+
+ let targetEl = null;
+
+ if (isElementNeeded) {
+ if (!this.simulatedElement) {
+ this.simulatedElement = this.win.document.createElement("div");
+ this.win.document.documentElement.appendChild(this.simulatedElement);
+ } else {
+ // Reset styles.
+ this.simulatedElement.style.cssText = "";
+ }
+
+ targetEl = this.simulatedElement;
+ }
+
+ if (!this.simulatedAnimation) {
+ this.simulatedAnimation = new this.win.Animation();
+ }
+
+ this.simulatedAnimation.effect = new this.win.KeyframeEffect(
+ targetEl,
+ keyframes,
+ effectTiming
+ );
+
+ return this.simulatedAnimation;
+ }
+
+ /**
+ * Returns a simulatable efect timing animation for the keyframes progress bar.
+ * The returned animation is implementing Animation interface of Web Animation API.
+ * https://drafts.csswg.org/web-animations/#the-animation-interface
+ *
+ * @param {Object} effectTiming
+ * e.g. { duration: 1000, fill: "both" }
+ * @return {Animation}
+ * https://drafts.csswg.org/web-animations/#the-animation-interface
+ */
+ simulateAnimationForKeyframesProgressBar(effectTiming) {
+ if (!this.simulatedAnimationForKeyframesProgressBar) {
+ this.simulatedAnimationForKeyframesProgressBar = new this.win.Animation();
+ }
+
+ this.simulatedAnimationForKeyframesProgressBar.effect =
+ new this.win.KeyframeEffect(null, null, effectTiming);
+
+ return this.simulatedAnimationForKeyframesProgressBar;
+ }
+
+ stopAnimationsCurrentTimeTimer() {
+ if (this.currentTimeTimer) {
+ this.currentTimeTimer.destroy();
+ this.currentTimeTimer = null;
+ }
+ }
+
+ startAnimationsCurrentTimeTimer() {
+ const timeScale = this.state.timeScale;
+ const shouldStopAfterEndTime = !hasAnimationIterationCountInfinite(
+ this.state.animations
+ );
+
+ const currentTimeTimer = new CurrentTimeTimer(
+ timeScale,
+ shouldStopAfterEndTime,
+ this.win,
+ this.onCurrentTimeTimerUpdated
+ );
+ currentTimeTimer.start();
+ this.currentTimeTimer = currentTimeTimer;
+ }
+
+ toggleElementPicker() {
+ this.inspector.toolbox.nodePicker.togglePicker();
+ }
+
+ async update() {
+ if (!this.isPanelVisible()) {
+ return;
+ }
+
+ const done = this.inspector.updating("animationinspector");
+
+ const selection = this.inspector.selection;
+ const animations =
+ selection.isConnected() && selection.isElementNode()
+ ? await this.animationsFront.getAnimationPlayersForNode(
+ selection.nodeFront
+ )
+ : [];
+ this.fireUpdateAction(animations);
+ this.setAnimationStateChangedListenerEnabled(true);
+
+ done();
+ }
+
+ async refreshAnimationsState(animations) {
+ let error = null;
+
+ const promises = animations.map(animation => {
+ return new Promise(resolve => {
+ animation
+ .refreshState()
+ .catch(e => {
+ error = e;
+ })
+ .finally(() => {
+ resolve();
+ });
+ });
+ });
+ await Promise.all(promises);
+
+ if (error) {
+ throw new Error(error);
+ }
+
+ // Even when removal animation on inspected document, refreshAnimationsState
+ // might be called before onAnimationsMutation due to the async timing.
+ // Return the animations as result of refreshAnimationsState after getting rid of
+ // the animations since they should not display.
+ return animations.filter(anim => !!anim.state.type);
+ }
+
+ fireUpdateAction(animations) {
+ // Animation inspector already destroyed
+ if (!this.inspector) {
+ return;
+ }
+
+ this.stopAnimationsCurrentTimeTimer();
+
+ // Although it is not possible to set a delay or end delay of infinity using
+ // the animation API, if the value passed exceeds the limit of our internal
+ // representation of times, it will be treated as infinity. Rather than
+ // adding special case code to represent this very rare case, we simply omit
+ // such animations from the graph.
+ animations = animations.filter(
+ anim =>
+ Math.abs(anim.state.delay) !== Infinity &&
+ Math.abs(anim.state.endDelay) !== Infinity
+ );
+
+ this.inspector.store.dispatch(updateAnimations(animations));
+
+ if (hasRunningAnimation(animations)) {
+ this.startAnimationsCurrentTimeTimer();
+ } else {
+ // Even no running animations, update the current time once
+ // so as to show the state.
+ this.onCurrentTimeTimerUpdated(this.state.timeScale.getCurrentTime());
+ }
+ }
+}
+
+module.exports = AnimationInspector;