Bug 1468475 - Part 1. Introduce zeroPositionTime in order to shift the graduation if animations have negative-delay. r?daisuke draft
authorMantaroh Yoshinaga <mantaroh@gmail.com>
Thu, 12 Jul 2018 09:29:08 +0900
changeset 817097 e5469d500f1c36dad4db2601a1878d93f88d86b0
parent 815516 ffb7b5015fc331bdc4c5e6ab52b9de669faa8864
child 817098 af3f2430a563aaab14319ae5acba391aed145641
push id115953
push userbmo:mantaroh@gmail.com
push dateThu, 12 Jul 2018 00:30:42 +0000
reviewersdaisuke
bugs1468475
milestone63.0a1
Bug 1468475 - Part 1. Introduce zeroPositionTime in order to shift the graduation if animations have negative-delay. r?daisuke This patch will introduce the zeroPositionTime, this value means the time that current time of animation will be zero. In front-side, use this value for shifting the graduation in order to display the zero. If target server dones't support currentTimeAtCreated, this patch will use the start time. As result of it, scrubber position will overlap to animation name area if current time is negative. MozReview-Commit-ID: H9n8bAj8mOW
devtools/client/inspector/animation/animation.js
devtools/client/inspector/animation/utils/timescale.js
devtools/server/actors/animation.js
devtools/shared/fronts/animation.js
--- a/devtools/client/inspector/animation/animation.js
+++ b/devtools/client/inspector/animation/animation.js
@@ -372,17 +372,18 @@ class AnimationInspector {
   }
 
   removeAnimationsCurrentTimeListener(listener) {
     this.animationsCurrentTimeListeners =
       this.animationsCurrentTimeListeners.filter(l => l !== listener);
   }
 
   async rewindAnimationsCurrentTime() {
-    await this.setAnimationsCurrentTime(0, true);
+    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) {
@@ -450,17 +451,17 @@ class AnimationInspector {
         await this.inspector.target.actorHasMethod("animations", "pauseSome");
     }
 
     let { animations, timeScale } = this.state;
 
     try {
       if (doPlay && animations.every(animation =>
                       timeScale.getEndTime(animation) <= animation.state.currentTime)) {
-        await this.doSetCurrentTimes(0);
+        await this.doSetCurrentTimes(timeScale.zeroPositionTime);
       }
 
       // If the server does not support pauseSome/playSome function, (which happens
       // when connected to server older than FF62), use pauseAll/playAll instead.
       // See bug 1456857.
       if (this.hasPausePlaySome) {
         if (doPlay) {
           await this.animationsFront.playSome(animations);
--- a/devtools/client/inspector/animation/utils/timescale.js
+++ b/devtools/client/inspector/animation/utils/timescale.js
@@ -7,69 +7,103 @@
 const { getFormatStr } = require("./l10n");
 
 // If total duration for all animations is eqaul to or less than
 // TIME_FORMAT_MAX_DURATION_IN_MS, the text which expresses time is in milliseconds,
 // and seconds otherwise. Use in formatTime function.
 const TIME_FORMAT_MAX_DURATION_IN_MS = 4000;
 
 /**
- * TimeScale object holds the total duration, start time and end time information for all
- * animations which should be displayed, and is used to calculate the displayed area for
- * each animation.
+ * TimeScale object holds the total duration, start time and end time and zero position
+ * time information for all animations which should be displayed, and is used to calculate
+ * the displayed area for each animation.
  */
 class TimeScale {
   constructor(animations) {
     if (!animations.every(animation => animation.state.createdTime)) {
       // Backward compatibility for createdTime.
       return this._initializeWithoutCreatedTime(animations);
     }
 
     let animationsCurrentTime = -Number.MAX_VALUE;
     let minStartTime = Infinity;
     let maxEndTime = 0;
+    let zeroPositionTime = 0;
 
     for (const animation of animations) {
       const {
         createdTime,
         currentTime,
+        currentTimeAtCreated,
         delay,
         duration,
         endDelay = 0,
         iterationCount,
         playbackRate,
       } = animation.state;
 
       const toRate = v => v / playbackRate;
-      const startTime = createdTime + toRate(Math.min(delay, 0));
+      const negativeDelay = toRate(Math.min(delay, 0));
+      let startPositionTime = createdTime + negativeDelay;
+      // If currentTimeAtCreated is not defined (which happens when connected to server
+      // older than FF62), use startPositionTime instead. See bug 1468475.
+      const originalCurrentTime =
+            toRate(currentTimeAtCreated ? currentTimeAtCreated : startPositionTime);
+      const startPositionTimeAtCreated =
+            createdTime + originalCurrentTime;
+      let animationZeroPositionTime = 0;
+
+      // To shift the zero position time is the following two patterns.
+      //  * Animation has negative current time which is smaller than negative dleay.
+      //  * Animation has negative delay.
+      // Furthermore, we should override the zero position time if we will need to
+      // expand the duration due to this negative current time or negative delay of
+      // this target animation.
+      if (originalCurrentTime < negativeDelay &&
+          startPositionTimeAtCreated < minStartTime) {
+        startPositionTime = startPositionTimeAtCreated;
+        animationZeroPositionTime = Math.abs(originalCurrentTime);
+      } else if (negativeDelay < 0 && startPositionTime < minStartTime) {
+        animationZeroPositionTime = Math.abs(negativeDelay);
+      }
+
       let endTime = 0;
 
       if (duration === Infinity) {
         // Set endTime so as to enable the scrubber with keeping the consinstency of UI
         // even the duration was Infinity. In case of delay is longer than zero, handle
         // the graph duration as double of the delay amount. In case of no delay, handle
         // the duration as 1ms which is short enough so as to make the scrubber movable
         // and the limited duration is prioritized.
         endTime = createdTime + (delay > 0 ? delay * 2 : 1);
       } else {
         endTime = createdTime +
                   toRate(delay +
                          duration * (iterationCount || 1) +
                          Math.max(endDelay, 0));
       }
 
-      minStartTime = Math.min(minStartTime, startTime);
       maxEndTime = Math.max(maxEndTime, endTime);
       animationsCurrentTime =
         Math.max(animationsCurrentTime, createdTime + toRate(currentTime));
+
+      if (startPositionTime < minStartTime) {
+        minStartTime = startPositionTime;
+        // Override the previous calculated zero position only if the duration will be
+        // expanded.
+        zeroPositionTime = animationZeroPositionTime;
+      } else {
+        zeroPositionTime = Math.max(zeroPositionTime, animationZeroPositionTime);
+      }
     }
 
     this.minStartTime = minStartTime;
     this.maxEndTime = maxEndTime;
     this.currentTime = animationsCurrentTime;
+    this.zeroPositionTime = zeroPositionTime;
   }
 
   /**
    * Same as the constructor but doesn't use the animation's createdTime property
    * which has only been added in FF62, for backward compatbility reasons.
    *
    * @param {Array} animations
    */
@@ -106,49 +140,48 @@ class TimeScale {
         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);
+      this.zeroPositionTime = this.minStartTime;
     }
   }
 
   /**
-   * Convert a distance in % to a time, in the current time scale.
-   *
-   * @param {Number} distance
-   * @return {Number}
-   */
-  distanceToTime(distance) {
-    return this.minStartTime + (this.getDuration() * distance / 100);
-  }
-
-  /**
-   * Convert a distance in % to a time, in the current time scale.
-   * The time will be relative to the current minimum start time.
+   *  Convert a distance in % to a time, in the current time scale. The time
+   *  will be relative to the zero position time.
+   *  i.e., If zeroPositionTime will be negative and specified time is shorter
+   *  than the absolute value of zero position time, relative time will be
+   *  negative time.
    *
    * @param {Number} distance
    * @return {Number}
    */
   distanceToRelativeTime(distance) {
-    const time = this.distanceToTime(distance);
-    return time - this.minStartTime;
+    return (this.getDuration() * distance / 100)
+           - this.zeroPositionTime;
   }
 
   /**
    * Depending on the time scale, format the given time as milliseconds or
    * seconds.
    *
    * @param {Number} time
    * @return {String} The formatted time string.
    */
   formatTime(time) {
+    // Ignore negative zero
+    if (Math.abs(time) < (1 / 1000)) {
+      time = 0.0;
+    }
+
     // Format in milliseconds if the total duration is short enough.
     if (this.getDuration() <= TIME_FORMAT_MAX_DURATION_IN_MS) {
       return getFormatStr("timeline.timeGraduationLabel", time.toFixed(0));
     }
 
     // Otherwise format in seconds.
     return getFormatStr("player.timeLabel", (time / 1000).toFixed(1));
   }
--- a/devtools/server/actors/animation.js
+++ b/devtools/server/actors/animation.js
@@ -83,16 +83,17 @@ var AnimationPlayerActor = protocol.Acto
     if (this.isPseudoElement) {
       this.observer.observe(this.node.parentElement,
                             {animations: true, subtree: true});
     } else {
       this.observer.observe(this.node, {animations: true});
     }
 
     this.createdTime = createdTime;
+    this.currentTimeAtCreated = player.currentTime;
   },
 
   destroy: function() {
     // 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();
     }
@@ -331,16 +332,18 @@ var AnimationPlayerActor = protocol.Acto
             .some(propState => propState.runningOnCompositor),
       propertyState: this.getPropertiesCompositorStatus(),
       // 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,
     };
   },
 
   /**
    * 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
--- a/devtools/shared/fronts/animation.js
+++ b/devtools/shared/fronts/animation.js
@@ -67,16 +67,17 @@ const AnimationPlayerFront = FrontClassW
       easing: this._form.easing,
       fill: this._form.fill,
       direction: this._form.direction,
       animationTimingFunction: this._form.animationTimingFunction,
       isRunningOnCompositor: this._form.isRunningOnCompositor,
       propertyState: this._form.propertyState,
       documentCurrentTime: this._form.documentCurrentTime,
       createdTime: this._form.createdTime,
+      currentTimeAtCreated: this._form.currentTimeAtCreated,
     };
   },
 
   /**
    * Executed when the AnimationPlayerActor emits a "changed" event. Used to
    * update the local knowledge of the state.
    */
   onChanged: preEvent("changed", function(partialState) {