Bug 1431573 - Part 4: Implement time label. r?gl draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Tue, 13 Mar 2018 14:31:10 +0900
changeset 766607 e5efa3ea57cbee0e8588fd07299cf8da811f67f7
parent 766606 2752adc19e83b1780fb14e782235e5f56a05d21a
child 766608 04315850686318620452bff10d4a83f2900cc274
push id102370
push userbmo:dakatsuka@mozilla.com
push dateTue, 13 Mar 2018 06:01:02 +0000
reviewersgl
bugs1431573
milestone61.0a1
Bug 1431573 - Part 4: Implement time label. r?gl MozReview-Commit-ID: Cg6A4hNLXnO
devtools/client/inspector/animation/animation.js
devtools/client/inspector/animation/components/AnimationListContainer.js
devtools/client/inspector/animation/components/AnimationToolbar.js
devtools/client/inspector/animation/components/App.js
devtools/client/inspector/animation/components/CurrentTimeLabel.js
devtools/client/inspector/animation/components/PauseResumeButton.js
devtools/client/inspector/animation/components/moz.build
devtools/client/inspector/animation/reducers/animations.js
devtools/client/inspector/animation/utils/timescale.js
devtools/client/inspector/animation/utils/utils.js
--- a/devtools/client/inspector/animation/animation.js
+++ b/devtools/client/inspector/animation/animation.js
@@ -14,33 +14,41 @@ const App = createFactory(require("./com
 
 const {
   updateAnimations,
   updateDetailVisibility,
   updateElementPickerEnabled,
   updateSelectedAnimation,
   updateSidebarSize
 } = require("./actions/animations");
-const { isAllAnimationEqual } = require("./utils/utils");
+const {
+  isAllAnimationEqual,
+  hasPlayingAnimation,
+} = require("./utils/utils");
 
 class AnimationInspector {
   constructor(inspector, win) {
     this.inspector = inspector;
     this.win = win;
 
+    this.addAnimationsCurrentTimeListener =
+      this.addAnimationsCurrentTimeListener.bind(this);
     this.getAnimatedPropertyMap = this.getAnimatedPropertyMap.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.setAnimationsPlayState = this.setAnimationsPlayState.bind(this);
     this.setDetailVisibility = this.setDetailVisibility.bind(this);
     this.simulateAnimation = this.simulateAnimation.bind(this);
     this.toggleElementPicker = this.toggleElementPicker.bind(this);
     this.update = this.update.bind(this);
+    this.onAnimationsCurrentTimeUpdated = this.onAnimationsCurrentTimeUpdated.bind(this);
     this.onElementPickerStarted = this.onElementPickerStarted.bind(this);
     this.onElementPickerStopped = this.onElementPickerStopped.bind(this);
     this.onSidebarResized = this.onSidebarResized.bind(this);
     this.onSidebarSelect = this.onSidebarSelect.bind(this);
 
     EventEmitter.decorate(this);
     this.emit = this.emit.bind(this);
 
@@ -53,45 +61,53 @@ class AnimationInspector {
       onShowBoxModelHighlighterForNode,
     } = this.inspector.getCommonComponentProps();
 
     const {
       onHideBoxModelHighlighter,
     } = this.inspector.getPanel("boxmodel").getComponentProps();
 
     const {
+      addAnimationsCurrentTimeListener,
       emit: emitEventForTest,
       getAnimatedPropertyMap,
       getComputedStyle,
       getNodeFromActor,
+      isAnimationsRunning,
+      removeAnimationsCurrentTimeListener,
       rewindAnimationsCurrentTime,
       selectAnimation,
       setAnimationsPlayState,
       setDetailVisibility,
       simulateAnimation,
       toggleElementPicker,
     } = this;
 
     const target = this.inspector.target;
     this.animationsFront = new AnimationsFront(target.client, target.form);
 
+    this.animationsCurrentTimeListeners = [];
+
     const provider = createElement(Provider,
       {
         id: "newanimationinspector",
         key: "newanimationinspector",
         store: this.inspector.store
       },
       App(
         {
+          addAnimationsCurrentTimeListener,
           emitEventForTest,
           getAnimatedPropertyMap,
           getComputedStyle,
           getNodeFromActor,
+          isAnimationsRunning,
           onHideBoxModelHighlighter,
           onShowBoxModelHighlighterForNode,
+          removeAnimationsCurrentTimeListener,
           rewindAnimationsCurrentTime,
           selectAnimation,
           setAnimationsPlayState,
           setDetailVisibility,
           setSelectedNode,
           simulateAnimation,
           toggleElementPicker,
         }
@@ -118,24 +134,30 @@ class AnimationInspector {
       this.simulatedAnimation = null;
     }
 
     if (this.simulatedElement) {
       this.simulatedElement.remove();
       this.simulatedElement = null;
     }
 
+    this.stopAnimationsCurrentTimeTimer();
+
     this.inspector = null;
     this.win = null;
   }
 
   get state() {
     return this.inspector.store.getState().animations;
   }
 
+  addAnimationsCurrentTimeListener(listener) {
+    this.animationsCurrentTimeListeners.push(listener);
+  }
+
   /**
    * 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.
@@ -196,16 +218,22 @@ class AnimationInspector {
   }
 
   isPanelVisible() {
     return this.inspector && this.inspector.toolbox && this.inspector.sidebar &&
            this.inspector.toolbox.currentToolId === "inspector" &&
            this.inspector.sidebar.getCurrentTabID() === "newanimationinspector";
   }
 
+  onAnimationsCurrentTimeUpdated(currentTime) {
+    for (const listener of this.animationsCurrentTimeListeners) {
+      listener(currentTime);
+    }
+  }
+
   onElementPickerStarted() {
     this.inspector.store.dispatch(updateElementPickerEnabled(true));
   }
 
   onElementPickerStopped() {
     this.inspector.store.dispatch(updateElementPickerEnabled(false));
   }
 
@@ -217,20 +245,26 @@ class AnimationInspector {
   onSidebarResized(type, size) {
     if (!this.isPanelVisible()) {
       return;
     }
 
     this.inspector.store.dispatch(updateSidebarSize(size));
   }
 
+  removeAnimationsCurrentTimeListener(listener) {
+    this.animationsCurrentTimeListeners =
+      this.animationsCurrentTimeListeners.filter(l => l !== listener);
+  }
+
   async rewindAnimationsCurrentTime() {
     const animations = this.state.animations;
     await this.animationsFront.setCurrentTimes(animations, 0, true);
-    this.updateAnimations(animations);
+    await this.updateAnimations(animations);
+    this.onAnimationsCurrentTimeUpdated(0);
   }
 
   selectAnimation(animation) {
     this.inspector.store.dispatch(updateSelectedAnimation(animation));
   }
 
   async setAnimationsPlayState(doPlay) {
     if (doPlay) {
@@ -283,16 +317,29 @@ class AnimationInspector {
     }
 
     this.simulatedAnimation.effect =
       new this.win.KeyframeEffect(targetEl, keyframes, effectTiming);
 
     return this.simulatedAnimation;
   }
 
+  stopAnimationsCurrentTimeTimer() {
+    if (this.currentTimeTimer) {
+      this.currentTimeTimer.destroy();
+      this.currentTimeTimer = null;
+    }
+  }
+
+  startAnimationsCurrentTimeTimer() {
+    const currentTimeTimer = new CurrentTimeTimer(this);
+    currentTimeTimer.start();
+    this.currentTimeTimer = currentTimeTimer;
+  }
+
   toggleElementPicker() {
     this.inspector.toolbox.highlighterUtils.togglePicker();
   }
 
   async update() {
     if (!this.inspector || !this.isPanelVisible()) {
       // AnimationInspector was destroyed already or the panel is hidden.
       return;
@@ -320,21 +367,62 @@ class AnimationInspector {
     });
 
     await Promise.all(promises);
 
     this.updateState([...animations]);
   }
 
   updateState(animations) {
+    this.stopAnimationsCurrentTimeTimer();
+
     this.inspector.store.dispatch(updateAnimations(animations));
     // If number of displayed animations is one, we select the animation automatically.
     // But if selected animation is in given animations, ignores.
     const selectedAnimation = this.state.selectedAnimation;
 
     if (!selectedAnimation ||
         !animations.find(animation => animation.actorID === selectedAnimation.actorID)) {
       this.selectAnimation(animations.length === 1 ? animations[0] : null);
     }
+
+    if (hasPlayingAnimation(animations)) {
+      this.startAnimationsCurrentTimeTimer();
+    }
+  }
+}
+
+class CurrentTimeTimer {
+  constructor(animationInspector) {
+    const timeScale = animationInspector.state.timeScale;
+    this.baseCurrentTime = timeScale.documentCurrentTime - timeScale.minStartTime;
+    this.startTime = animationInspector.win.performance.now();
+    this.animationInspector = animationInspector;
+
+    this.next = this.next.bind(this);
+  }
+
+  destroy() {
+    this.stop();
+    this.animationInspector = null;
+  }
+
+  next() {
+    if (this.doStop) {
+      return;
+    }
+
+    const { onAnimationsCurrentTimeUpdated, win } = this.animationInspector;
+    const currentTime = this.baseCurrentTime + win.performance.now() - this.startTime;
+    onAnimationsCurrentTimeUpdated(currentTime);
+    win.requestAnimationFrame(this.next);
+  }
+
+  start() {
+    this.next();
+  }
+
+  stop() {
+    this.doStop = true;
   }
 }
 
 module.exports = AnimationInspector;
--- a/devtools/client/inspector/animation/components/AnimationListContainer.js
+++ b/devtools/client/inspector/animation/components/AnimationListContainer.js
@@ -7,46 +7,45 @@
 const { createFactory, PureComponent } =
   require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 
 const AnimationList = createFactory(require("./AnimationList"));
 const AnimationListHeader = createFactory(require("./AnimationListHeader"));
 
-const TimeScale = require("../utils/timescale");
-
 class AnimationListContainer extends PureComponent {
   static get propTypes() {
     return {
       animations: PropTypes.arrayOf(PropTypes.object).isRequired,
       emitEventForTest: PropTypes.func.isRequired,
       getAnimatedPropertyMap: PropTypes.func.isRequired,
       getNodeFromActor: PropTypes.func.isRequired,
       onHideBoxModelHighlighter: PropTypes.func.isRequired,
       onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
       selectAnimation: PropTypes.func.isRequired,
       setSelectedNode: PropTypes.func.isRequired,
       simulateAnimation: PropTypes.func.isRequired,
+      timeScale: PropTypes.object.isRequired,
     };
   }
 
   render() {
     const {
       animations,
       emitEventForTest,
       getAnimatedPropertyMap,
       getNodeFromActor,
       onHideBoxModelHighlighter,
       onShowBoxModelHighlighterForNode,
       selectAnimation,
       setSelectedNode,
       simulateAnimation,
+      timeScale,
     } = this.props;
-    const timeScale = new TimeScale(animations);
 
     return dom.div(
       {
         className: "animation-list-container"
       },
       AnimationListHeader(
         {
           timeScale,
--- a/devtools/client/inspector/animation/components/AnimationToolbar.js
+++ b/devtools/client/inspector/animation/components/AnimationToolbar.js
@@ -3,31 +3,36 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
+const CurrentTimeLabel = createFactory(require("./CurrentTimeLabel"));
 const PauseResumeButton = createFactory(require("./PauseResumeButton"));
 const RewindButton = createFactory(require("./RewindButton"));
 
 class AnimationToolbar extends PureComponent {
   static get propTypes() {
     return {
+      addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+      removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       rewindAnimationsCurrentTime: PropTypes.func.isRequired,
       setAnimationsPlayState: PropTypes.func.isRequired,
     };
   }
 
   render() {
     const {
+      addAnimationsCurrentTimeListener,
       animations,
+      removeAnimationsCurrentTimeListener,
       rewindAnimationsCurrentTime,
       setAnimationsPlayState,
     } = this.props;
 
     return dom.div(
       {
         className: "animation-toolbar devtools-toolbar",
       },
@@ -36,14 +41,20 @@ class AnimationToolbar extends PureCompo
           rewindAnimationsCurrentTime,
         }
       ),
       PauseResumeButton(
         {
           animations,
           setAnimationsPlayState,
         }
+      ),
+      CurrentTimeLabel(
+        {
+          addAnimationsCurrentTimeListener,
+          removeAnimationsCurrentTimeListener,
+        }
       )
     );
   }
 }
 
 module.exports = AnimationToolbar;
--- a/devtools/client/inspector/animation/components/App.js
+++ b/devtools/client/inspector/animation/components/App.js
@@ -13,67 +13,75 @@ const AnimationDetailContainer = createF
 const AnimationListContainer = createFactory(require("./AnimationListContainer"));
 const AnimationToolbar = createFactory(require("./AnimationToolbar"));
 const NoAnimationPanel = createFactory(require("./NoAnimationPanel"));
 const SplitBox = createFactory(require("devtools/client/shared/components/splitter/SplitBox"));
 
 class App extends PureComponent {
   static get propTypes() {
     return {
+      addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       animations: PropTypes.arrayOf(PropTypes.object).isRequired,
       detailVisibility: PropTypes.bool.isRequired,
       emitEventForTest: PropTypes.func.isRequired,
       getAnimatedPropertyMap: PropTypes.func.isRequired,
       getComputedStyle: PropTypes.func.isRequired,
       getNodeFromActor: PropTypes.func.isRequired,
       onHideBoxModelHighlighter: PropTypes.func.isRequired,
       onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
+      removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       rewindAnimationsCurrentTime: PropTypes.func.isRequired,
       selectAnimation: PropTypes.func.isRequired,
       setAnimationsPlayState: PropTypes.func.isRequired,
       setDetailVisibility: PropTypes.func.isRequired,
       setSelectedNode: PropTypes.func.isRequired,
       simulateAnimation: PropTypes.func.isRequired,
+      timeScale: PropTypes.object.isRequired,
       toggleElementPicker: PropTypes.func.isRequired,
     };
   }
 
   shouldComponentUpdate(nextProps, nextState) {
     return this.props.animations.length !== 0 || nextProps.animations.length !== 0;
   }
 
   render() {
     const {
+      addAnimationsCurrentTimeListener,
       animations,
       detailVisibility,
       emitEventForTest,
       getAnimatedPropertyMap,
       getComputedStyle,
       getNodeFromActor,
       onHideBoxModelHighlighter,
       onShowBoxModelHighlighterForNode,
+      removeAnimationsCurrentTimeListener,
       rewindAnimationsCurrentTime,
       selectAnimation,
       setAnimationsPlayState,
       setDetailVisibility,
       setSelectedNode,
       simulateAnimation,
+      timeScale,
       toggleElementPicker,
     } = this.props;
 
     return dom.div(
       {
         id: "animation-container",
         className: detailVisibility ? "animation-detail-visible" : "",
       },
       animations.length ?
       [
         AnimationToolbar(
           {
+            addAnimationsCurrentTimeListener,
             animations,
+            removeAnimationsCurrentTimeListener,
             rewindAnimationsCurrentTime,
             setAnimationsPlayState,
           }
         ),
         SplitBox({
           className: "animation-container-splitter",
           endPanel: AnimationDetailContainer(
             {
@@ -93,16 +101,17 @@ class App extends PureComponent {
               emitEventForTest,
               getAnimatedPropertyMap,
               getNodeFromActor,
               onHideBoxModelHighlighter,
               onShowBoxModelHighlighterForNode,
               selectAnimation,
               setSelectedNode,
               simulateAnimation,
+              timeScale,
             }
           ),
           vert: false,
         })
       ]
       :
       NoAnimationPanel(
         {
@@ -112,12 +121,13 @@ class App extends PureComponent {
     );
   }
 }
 
 const mapStateToProps = state => {
   return {
     animations: state.animations.animations,
     detailVisibility: state.animations.detailVisibility,
+    timeScale: state.animations.timeScale,
   };
 };
 
 module.exports = connect(mapStateToProps)(App);
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/CurrentTimeLabel.js
@@ -0,0 +1,83 @@
+/* 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 { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+class CurrentTimeLabel extends PureComponent {
+  static get propTypes() {
+    return {
+      addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+      removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+
+    const { addAnimationsCurrentTimeListener } = props;
+    this.onCurrentTimeUpdated = this.onCurrentTimeUpdated.bind(this);
+
+    this.state = {
+      currentTime: 0,
+    };
+
+    addAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+  }
+
+  componentWillUnmount() {
+    const { removeAnimationsCurrentTimeListener } = this.props;
+    removeAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+  }
+
+  onCurrentTimeUpdated(currentTime) {
+    this.setState({ currentTime });
+  }
+
+  render() {
+    const { currentTime } = this.state;
+
+    return dom.label(
+      {
+        className: "current-time-label",
+      },
+      formatStopwatchTime(currentTime)
+    );
+  }
+}
+
+/**
+ * Format a timestamp (in ms) as a mm:ss.mmm string.
+ *
+ * @param {Number} time
+ * @return {String}
+ */
+function formatStopwatchTime(time) {
+  // Format falsy values as 0
+  if (!time) {
+    return "00:00.000";
+  }
+
+  let milliseconds = parseInt(time % 1000, 10);
+  let seconds = parseInt((time / 1000) % 60, 10);
+  let minutes = parseInt((time / (1000 * 60)), 10);
+
+  let pad = (nb, max) => {
+    if (nb < max) {
+      return new Array((max + "").length - (nb + "").length + 1).join("0") + nb;
+    }
+    return nb;
+  };
+
+  minutes = pad(minutes, 10);
+  seconds = pad(seconds, 10);
+  milliseconds = pad(milliseconds, 100);
+
+  return `${minutes}:${seconds}.${milliseconds}`;
+}
+
+module.exports = CurrentTimeLabel;
--- a/devtools/client/inspector/animation/components/PauseResumeButton.js
+++ b/devtools/client/inspector/animation/components/PauseResumeButton.js
@@ -4,16 +4,17 @@
 
 "use strict";
 
 const { PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const { getStr } = require("../utils/l10n");
+const { hasPlayingAnimation } = require("../utils/utils");
 
 class PauseResumeButton extends PureComponent {
   static get propTypes() {
     return {
       animations: PropTypes.arrayOf(PropTypes.object).isRequired,
       setAnimationsPlayState: PropTypes.func.isRequired,
     };
   }
@@ -38,17 +39,17 @@ class PauseResumeButton extends PureComp
     const { setAnimationsPlayState } = this.props;
     const { isPlaying } = this.state;
 
     setAnimationsPlayState(!isPlaying);
   }
 
   updateState() {
     const { animations } = this.props;
-    const isPlaying = animations.some(({state}) => state.playState === "running");
+    const isPlaying = hasPlayingAnimation(animations);
     this.setState({ isPlaying });
   }
 
   render() {
     const { isPlaying } = this.state;
 
     return dom.button(
       {
--- a/devtools/client/inspector/animation/components/moz.build
+++ b/devtools/client/inspector/animation/components/moz.build
@@ -19,14 +19,15 @@ DevToolsModules(
     'AnimationList.js',
     'AnimationListContainer.js',
     'AnimationListHeader.js',
     'AnimationTarget.js',
     'AnimationTimelineTickItem.js',
     'AnimationTimelineTickList.js',
     'AnimationToolbar.js',
     'App.js',
+    'CurrentTimeLabel.js',
     'KeyframesProgressTickItem.js',
     'KeyframesProgressTickList.js',
     'NoAnimationPanel.js',
     'PauseResumeButton.js',
     'RewindButton.js',
 )
--- a/devtools/client/inspector/animation/reducers/animations.js
+++ b/devtools/client/inspector/animation/reducers/animations.js
@@ -7,31 +7,35 @@
 const {
   UPDATE_ANIMATIONS,
   UPDATE_DETAIL_VISIBILITY,
   UPDATE_ELEMENT_PICKER_ENABLED,
   UPDATE_SELECTED_ANIMATION,
   UPDATE_SIDEBAR_SIZE,
 } = require("../actions/index");
 
+const TimeScale = require("../utils/timescale");
+
 const INITIAL_STATE = {
   animations: [],
   detailVisibility: false,
   elementPickerEnabled: false,
   selectedAnimation: null,
   sidebarSize: {
     height: 0,
     width: 0,
   },
+  timeScale: null,
 };
 
 const reducers = {
   [UPDATE_ANIMATIONS](state, { animations }) {
     return Object.assign({}, state, {
       animations,
+      timeScale: new TimeScale(animations),
     });
   },
 
   [UPDATE_DETAIL_VISIBILITY](state, { detailVisibility }) {
     return Object.assign({}, state, {
       detailVisibility
     });
   },
--- a/devtools/client/inspector/animation/utils/timescale.js
+++ b/devtools/client/inspector/animation/utils/timescale.js
@@ -19,30 +19,33 @@ const TIME_FORMAT_MAX_DURATION_IN_MS = 4
  * For the helper to know how to convert, it needs to know all the animations.
  * Whenever a new animation is added to the panel, addAnimation(state) should be
  * called.
  */
 class TimeScale {
   constructor(animations) {
     this.minStartTime = Infinity;
     this.maxEndTime = 0;
+    this.documentCurrentTime = 0;
+
     for (const animation of animations) {
       this.addAnimation(animation.state);
     }
   }
 
   /**
    * Add a new animation to time scale.
    *
    * @param {Object} state
    *                 A PlayerFront.state object.
    */
   addAnimation(state) {
     let {
       delay,
+      documentCurrentTime,
       duration,
       endDelay = 0,
       iterationCount,
       playbackRate,
       previousStartTime,
     } = state;
 
     const toRate = v => v / playbackRate;
@@ -62,16 +65,18 @@ class TimeScale {
       this.minStartTime,
       previousStartTime +
       relevantDelay +
       Math.min(startTime, 0)
     );
     const length = toRate(delay) + rateRelativeDuration + toRate(minZero(endDelay));
     const endTime = previousStartTime + length;
     this.maxEndTime = Math.max(this.maxEndTime, endTime);
+
+    this.documentCurrentTime = Math.max(this.documentCurrentTime, documentCurrentTime);
   }
 
   /**
    * Convert a distance in % to a time, in the current time scale.
    *
    * @param {Number} distance
    * @return {Number}
    */
--- a/devtools/client/inspector/animation/utils/utils.js
+++ b/devtools/client/inspector/animation/utils/utils.js
@@ -65,16 +65,26 @@ function isAllAnimationEqual(animationsA
       return false;
     }
   }
 
   return true;
 }
 
 /**
+ * Check wether the animations are running at least one.
+ *
+ * @param {Array} animations.
+ * @return {Boolean} true: playing
+ */
+function hasPlayingAnimation(animations) {
+  return animations.some(({state}) => state.playState === "running");
+}
+
+/**
  * Check the equality given states as effect timing.
  *
  * @param {Object} state of animation.
  * @param {Object} same to avobe.
  * @return {Boolean} true: same effect timing
  */
 function isTimingEffectEqual(stateA, stateB) {
   return stateA.delay === stateB.delay &&
@@ -83,10 +93,11 @@ function isTimingEffectEqual(stateA, sta
          stateA.easing === stateB.easing &&
          stateA.endDelay === stateB.endDelay &&
          stateA.fill === stateB.fill &&
          stateA.iterationCount === stateB.iterationCount &&
          stateA.iterationStart === stateB.iterationStart;
 }
 
 exports.findOptimalTimeInterval = findOptimalTimeInterval;
+exports.hasPlayingAnimation = hasPlayingAnimation;
 exports.isAllAnimationEqual = isAllAnimationEqual;
 exports.isTimingEffectEqual = isTimingEffectEqual;