Bug 1431573 - Part 10: Reflect to stop animation. r?gl draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Mon, 12 Mar 2018 16:11:13 +0900
changeset 766121 9d4469efaae2b330d907118ce6d0a79e56b2c584
parent 766120 b3a76122631fa682a9f659c6ea2228985983c123
child 766122 7aaf68204a81fc0b7f8905fece374fe3a22b21f9
push id102230
push userbmo:dakatsuka@mozilla.com
push dateMon, 12 Mar 2018 09:02:24 +0000
reviewersgl
bugs1431573
milestone60.0a1
Bug 1431573 - Part 10: Reflect to stop animation. r?gl MozReview-Commit-ID: DZ4itacGnV4
devtools/client/inspector/animation/animation.js
devtools/client/inspector/animation/components/PauseResumeButton.js
devtools/client/inspector/animation/current-time-timer.js
devtools/client/inspector/animation/moz.build
devtools/client/inspector/animation/utils/utils.js
--- a/devtools/client/inspector/animation/animation.js
+++ b/devtools/client/inspector/animation/animation.js
@@ -6,27 +6,29 @@
 
 const { AnimationsFront } = require("devtools/shared/fronts/animation");
 const { createElement, createFactory } = require("devtools/client/shared/vendor/react");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
 const EventEmitter = require("devtools/shared/event-emitter");
 
 const App = createFactory(require("./components/App"));
+const CurrentTimeTimer = require("./current-time-timer");
 
 const {
   updateAnimations,
   updateDetailVisibility,
   updateElementPickerEnabled,
   updateSelectedAnimation,
   updateSidebarSize
 } = require("./actions/animations");
 const {
   isAllAnimationEqual,
-  hasPlayingAnimation,
+  hasAnimationIterationCountInfinite,
+  hasRunningAnimation,
 } = require("./utils/utils");
 
 class AnimationInspector {
   constructor(inspector, win) {
     this.inspector = inspector;
     this.win = win;
 
     this.addAnimationsCurrentTimeListener =
@@ -44,16 +46,17 @@ class AnimationInspector {
     this.setAnimationsPlayState = this.setAnimationsPlayState.bind(this);
     this.setDetailVisibility = this.setDetailVisibility.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.onAnimationsCurrentTimeUpdated = this.onAnimationsCurrentTimeUpdated.bind(this);
+    this.onCurrentTimeTimerUpdated = this.onCurrentTimeTimerUpdated.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);
 
@@ -256,16 +259,30 @@ class AnimationInspector {
   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);
+    }
+  }
+
   onElementPickerStarted() {
     this.inspector.store.dispatch(updateElementPickerEnabled(true));
   }
 
   onElementPickerStopped() {
     this.inspector.store.dispatch(updateElementPickerEnabled(false));
   }
 
@@ -402,17 +419,23 @@ class AnimationInspector {
   stopAnimationsCurrentTimeTimer() {
     if (this.currentTimeTimer) {
       this.currentTimeTimer.destroy();
       this.currentTimeTimer = null;
     }
   }
 
   startAnimationsCurrentTimeTimer() {
-    const currentTimeTimer = new CurrentTimeTimer(this);
+    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.highlighterUtils.togglePicker();
   }
 
@@ -446,50 +469,15 @@ class AnimationInspector {
     await Promise.all(promises);
   }
 
   updateState(animations) {
     this.stopAnimationsCurrentTimeTimer();
 
     this.inspector.store.dispatch(updateAnimations(animations));
 
-    if (hasPlayingAnimation(animations)) {
+    if (hasRunningAnimation(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/PauseResumeButton.js
+++ b/devtools/client/inspector/animation/components/PauseResumeButton.js
@@ -7,33 +7,33 @@
 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 ReactDOM = require("devtools/client/shared/vendor/react-dom");
 
 const { KeyCodes } = require("devtools/client/shared/keycodes");
 
 const { getStr } = require("../utils/l10n");
-const { hasPlayingAnimation } = require("../utils/utils");
+const { hasRunningAnimation } = require("../utils/utils");
 
 class PauseResumeButton extends PureComponent {
   static get propTypes() {
     return {
       animations: PropTypes.arrayOf(PropTypes.object).isRequired,
       setAnimationsPlayState: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.onKeyDown = this.onKeyDown.bind(this);
 
     this.state = {
-      isPlaying: false,
+      isRunning: false,
     };
   }
 
   componentWillMount() {
     this.updateState(this.props);
   }
 
   componentDidMount() {
@@ -51,43 +51,43 @@ class PauseResumeButton extends PureComp
   }
 
   getKeyEventTarget() {
     return ReactDOM.findDOMNode(this).closest("#animation-container");
   }
 
   onToggleAnimationsPlayState() {
     const { setAnimationsPlayState } = this.props;
-    const { isPlaying } = this.state;
+    const { isRunning } = this.state;
 
-    setAnimationsPlayState(!isPlaying);
+    setAnimationsPlayState(!isRunning);
   }
 
   onKeyDown(e) {
     if (e.keyCode === KeyCodes.DOM_VK_SPACE) {
       this.onToggleAnimationsPlayState();
       e.preventDefault();
     }
   }
 
   updateState() {
     const { animations } = this.props;
-    const isPlaying = hasPlayingAnimation(animations);
-    this.setState({ isPlaying });
+    const isRunning = hasRunningAnimation(animations);
+    this.setState({ isRunning });
   }
 
   render() {
-    const { isPlaying } = this.state;
+    const { isRunning } = this.state;
 
     return dom.button(
       {
         className: "pause-resume-button devtools-button" +
-                   (isPlaying ? "" : " paused"),
+                   (isRunning ? "" : " paused"),
         onClick: this.onToggleAnimationsPlayState.bind(this),
-        title: isPlaying ?
+        title: isRunning ?
                  getStr("timeline.resumedButtonTooltip") :
                  getStr("timeline.pausedButtonTooltip"),
       }
     );
   }
 }
 
 module.exports = PauseResumeButton;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/current-time-timer.js
@@ -0,0 +1,75 @@
+/* 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";
+
+/**
+ * In animation inspector, the scrubber and the progress bar moves along the current time
+ * of animation. However, the processing which sync with actual animations is heavy since
+ * we have to communication by the actor. The role of this class is to make the pseudo
+ * current time in animation inspector to proceed.
+ */
+class CurrentTimeTimer {
+  /**
+   * Constructor.
+   *
+   * @param {Object} timeScale
+   * @param {Bool} shouldStopAfterEndTime
+   *               If need to stop the timer after animation end time, set true.
+   * @param {window} win
+   *                 Be used for requestAnimationFrame and performance.
+   * @param {Function} onUpdated
+   *                   Listener function to get updating.
+   *                   This function is called with 2 parameters.
+   *                   1st: current time
+   *                   2nd: if shouldStopAfterEndTime is true and
+   *                        the current time is over the end time, true is given.
+   */
+  constructor(timeScale, shouldStopAfterEndTime, win, onUpdated) {
+    this.baseCurrentTime = timeScale.documentCurrentTime - timeScale.minStartTime;
+    this.endTime = timeScale.maxEndTime - timeScale.minStartTime;
+    this.timerStartTime = win.performance.now();
+
+    this.shouldStopAfterEndTime = shouldStopAfterEndTime;
+    this.onUpdated = onUpdated;
+    this.win = win;
+    this.next = this.next.bind(this);
+  }
+
+  destroy() {
+    this.stop();
+    this.onUpdated = null;
+    this.win = null;
+  }
+
+  /**
+   * Proceed the pseudo current time.
+   */
+  next() {
+    if (this.doStop) {
+      return;
+    }
+
+    const currentTime =
+      this.baseCurrentTime + this.win.performance.now() - this.timerStartTime;
+
+    if (this.endTime < currentTime && this.shouldStopAfterEndTime) {
+      this.onUpdated(this.endTime, true);
+      return;
+    }
+
+    this.onUpdated(currentTime);
+    this.win.requestAnimationFrame(this.next);
+  }
+
+  start() {
+    this.next();
+  }
+
+  stop() {
+    this.doStop = true;
+  }
+}
+
+module.exports = CurrentTimeTimer;
--- a/devtools/client/inspector/animation/moz.build
+++ b/devtools/client/inspector/animation/moz.build
@@ -7,10 +7,11 @@ DIRS += [
     'components',
     'reducers',
     'utils'
 ]
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
 DevToolsModules(
-    'animation.js'
+    'animation.js',
+    'current-time-timer.js'
 )
--- a/devtools/client/inspector/animation/utils/utils.js
+++ b/devtools/client/inspector/animation/utils/utils.js
@@ -65,22 +65,33 @@ function isAllAnimationEqual(animationsA
       return false;
     }
   }
 
   return true;
 }
 
 /**
+ * Check whether or not the given list of animations has an iteration count of infinite.
+ *
+ * @param {Array} animations.
+ * @return {Boolean} true if there is an animation in the  list of animations
+ *                   whose animation iteration count is infinite.
+ */
+function hasAnimationIterationCountInfinite(animations) {
+  return animations.some(({state}) => !state.iterationCount);
+}
+
+/**
  * Check wether the animations are running at least one.
  *
  * @param {Array} animations.
- * @return {Boolean} true: playing
+ * @return {Boolean} true: running
  */
-function hasPlayingAnimation(animations) {
+function hasRunningAnimation(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.
@@ -93,11 +104,12 @@ 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.hasAnimationIterationCountInfinite = hasAnimationIterationCountInfinite;
+exports.hasRunningAnimation = hasRunningAnimation;
 exports.isAllAnimationEqual = isAllAnimationEqual;
 exports.isTimingEffectEqual = isTimingEffectEqual;