Bug 1494847 - Part 1: Show proper graph for negative playback rate. r=pbro
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Sat, 13 Oct 2018 05:39:18 +0000
changeset 489439 bc60f6bb6d22db3d7d6d3f1849e43cfec4544981
parent 489438 f17dfd86498ec12d828373d6b7b2d76f29983d22
child 489440 3d151cbf0921c253e980f94a68535c199babb042
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewerspbro
bugs1494847
milestone64.0a1
Bug 1494847 - Part 1: Show proper graph for negative playback rate. r=pbro Differential Revision: https://phabricator.services.mozilla.com/D7685
devtools/client/inspector/animation/components/graph/ComputedTimingPath.js
devtools/client/inspector/animation/components/graph/DelaySign.js
devtools/client/inspector/animation/components/graph/EffectTimingPath.js
devtools/client/inspector/animation/components/graph/EndDelaySign.js
devtools/client/inspector/animation/components/graph/NegativePath.js
devtools/client/inspector/animation/components/graph/SummaryGraph.js
devtools/client/inspector/animation/components/graph/SummaryGraphPath.js
devtools/client/inspector/animation/utils/graph-helper.js
devtools/client/inspector/animation/utils/timescale.js
devtools/shared/fronts/animation.js
--- a/devtools/client/inspector/animation/components/graph/ComputedTimingPath.js
+++ b/devtools/client/inspector/animation/components/graph/ComputedTimingPath.js
@@ -2,17 +2,20 @@
  * 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 PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 
-const { SummaryGraphHelper, toPathString } = require("../../utils/graph-helper");
+const {
+  createSummaryGraphPathStringFunction,
+  SummaryGraphHelper,
+} = require("../../utils/graph-helper");
 const TimingPath = require("./TimingPath");
 
 class ComputedTimingPath extends TimingPath {
   static get propTypes() {
     return {
       animation: PropTypes.object.isRequired,
       durationPerPixel: PropTypes.number.isRequired,
       keyframes: PropTypes.object.isRequired,
@@ -62,32 +65,25 @@ class ComputedTimingPath extends TimingP
     const endTime = simulatedAnimation.effect.getComputedTiming().endTime;
 
     // Set the underlying opacity to zero so that if we sample the animation's output
     // during the delay phase and it is not filling backwards, we get zero.
     simulatedElement.style.opacity = 0;
 
     const getValueFunc = time => {
       if (time < 0) {
-        return { x: time, y: 0 };
+        return 0;
       }
 
       simulatedAnimation.currentTime = time < endTime ? time : endTime;
       return win.getComputedStyle(simulatedElement).opacity;
     };
 
-    const toPathStringFunc = segments => {
-      const firstSegment = segments[0];
-      let pathString = `M${ firstSegment.x },0 `;
-      pathString += toPathString(segments);
-      const lastSegment = segments[segments.length - 1];
-      pathString += `L${ lastSegment.x },0 Z`;
-      return pathString;
-    };
-
+    const toPathStringFunc =
+      createSummaryGraphPathStringFunction(endTime, state.playbackRate);
     const helper = new SummaryGraphHelper(state, keyframes,
                                           totalDuration, durationPerPixel,
                                           getValueFunc, toPathStringFunc);
 
     return dom.g(
       {
         className: "animation-computed-timing-path",
         style: { opacity },
--- a/devtools/client/inspector/animation/components/graph/DelaySign.js
+++ b/devtools/client/inspector/animation/components/graph/DelaySign.js
@@ -17,37 +17,30 @@ class DelaySign extends PureComponent {
   }
 
   render() {
     const {
       animation,
       timeScale,
     } = this.props;
     const {
-      createdTime,
       delay,
-      fill,
-      playbackRate,
-    } = animation.state;
+      isDelayFilled,
+      startTime,
+    } = animation.state.absoluteValues;
 
-    const toRate = v => v / playbackRate;
-    // If createdTime is not defined (which happens when connected to server older
-    // than FF62), use previousStartTime instead. See bug 1454392
-    const baseTime = typeof createdTime === "undefined"
-                       ? (animation.state.previousStartTime || 0)
-                       : createdTime;
-    const startTime = baseTime + toRate(Math.min(delay, 0)) - timeScale.minStartTime;
-    const offset = startTime / timeScale.getDuration() * 100;
-    const width = Math.abs(toRate(delay)) / timeScale.getDuration() * 100;
+    const toPercentage = v => v / timeScale.getDuration() * 100;
+    const offset = toPercentage(startTime - timeScale.minStartTime);
+    const width = toPercentage(Math.abs(delay));
 
     return dom.div(
       {
         className: "animation-delay-sign" +
-                   (delay < 0 ? " negative" : "") +
-                   (fill === "both" || fill === "backwards" ? " fill" : ""),
+                    (delay < 0 ? " negative" : "") +
+                    (isDelayFilled ? " fill" : ""),
         style: {
           width: `${ width }%`,
           marginInlineStart: `${ offset }%`,
         },
       }
     );
   }
 }
--- a/devtools/client/inspector/animation/components/graph/EffectTimingPath.js
+++ b/devtools/client/inspector/animation/components/graph/EffectTimingPath.js
@@ -2,17 +2,20 @@
  * 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 PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 
-const { SummaryGraphHelper, toPathString } = require("../../utils/graph-helper");
+const {
+  createSummaryGraphPathStringFunction,
+  SummaryGraphHelper,
+} = require("../../utils/graph-helper");
 const TimingPath = require("./TimingPath");
 
 class EffectTimingPath extends TimingPath {
   static get propTypes() {
     return {
       animation: PropTypes.object.isRequired,
       durationPerPixel: PropTypes.number.isRequired,
       offset: PropTypes.number.isRequired,
@@ -40,32 +43,25 @@ class EffectTimingPath extends TimingPat
     if (!simulatedAnimation) {
       return null;
     }
 
     const endTime = simulatedAnimation.effect.getComputedTiming().endTime;
 
     const getValueFunc = time => {
       if (time < 0) {
-        return { x: time, y: 0 };
+        return 0;
       }
 
       simulatedAnimation.currentTime = time < endTime ? time : endTime;
       return Math.max(simulatedAnimation.effect.getComputedTiming().progress, 0);
     };
 
-    const toPathStringFunc = segments => {
-      const firstSegment = segments[0];
-      let pathString = `M${ firstSegment.x },0 `;
-      pathString += toPathString(segments);
-      const lastSegment = segments[segments.length - 1];
-      pathString += `L${ lastSegment.x },0`;
-      return pathString;
-    };
-
+    const toPathStringFunc =
+      createSummaryGraphPathStringFunction(endTime, state.playbackRate);
     const helper = new SummaryGraphHelper(state, null,
                                           totalDuration, durationPerPixel,
                                           getValueFunc, toPathStringFunc);
 
     return dom.g(
       {
         className: "animation-effect-timing-path",
         transform: `translate(${ offset })`
--- a/devtools/client/inspector/animation/components/graph/EndDelaySign.js
+++ b/devtools/client/inspector/animation/components/graph/EndDelaySign.js
@@ -17,41 +17,31 @@ class EndDelaySign extends PureComponent
   }
 
   render() {
     const {
       animation,
       timeScale,
     } = this.props;
     const {
-      createdTime,
-      delay,
-      duration,
       endDelay,
-      fill,
-      iterationCount,
-      playbackRate,
-    } = animation.state;
+      endTime,
+      isEndDelayFilled,
+   } = animation.state.absoluteValues;
 
-    const toRate = v => v / playbackRate;
-    // If createdTime is not defined (which happens when connected to server older
-    // than FF62), use previousStartTime instead. See bug 1454392
-    const baseTime = typeof createdTime === "undefined"
-                       ? (animation.state.previousStartTime || 0)
-                       : createdTime;
-    const startTime = baseTime - timeScale.minStartTime;
-    const endTime = toRate(delay + duration * iterationCount + Math.min(endDelay, 0));
-    const offset = (startTime + endTime) / timeScale.getDuration() * 100;
-    const width = Math.abs(toRate(endDelay)) / timeScale.getDuration() * 100;
+    const toPercentage = v => v / timeScale.getDuration() * 100;
+    const absEndDelay = Math.abs(endDelay);
+    const offset = toPercentage(endTime - absEndDelay - timeScale.minStartTime);
+    const width = toPercentage(absEndDelay);
 
     return dom.div(
       {
         className: "animation-end-delay-sign" +
-                   (endDelay < 0 ? " negative" : "") +
-                   (fill === "both" || fill === "forwards" ? " fill" : ""),
+                    (endDelay < 0 ? " negative" : "") +
+                    (isEndDelayFilled ? " fill" : ""),
         style: {
           width: `${ width }%`,
           marginInlineStart: `${ offset }%`,
         },
       }
     );
   }
 }
--- a/devtools/client/inspector/animation/components/graph/NegativePath.js
+++ b/devtools/client/inspector/animation/components/graph/NegativePath.js
@@ -3,17 +3,20 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { 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 { SummaryGraphHelper, toPathString } = require("../../utils/graph-helper");
+const {
+  createSummaryGraphPathStringFunction,
+  SummaryGraphHelper,
+} = require("../../utils/graph-helper");
 
 class NegativePath extends PureComponent {
   static get propTypes() {
     return {
       animation: PropTypes.object.isRequired,
       className: PropTypes.string.isRequired,
       durationPerPixel: PropTypes.number.isRequired,
       keyframes: PropTypes.object.isRequired,
@@ -54,35 +57,29 @@ class NegativePath extends PureComponent
     const simulatedAnimation = simulateAnimation(frames, effectTiming, true);
 
     if (!simulatedAnimation) {
       return null;
     }
 
     const simulatedElement = simulatedAnimation.effect.target;
     const win = simulatedElement.ownerGlobal;
+    const endTime = simulatedAnimation.effect.getComputedTiming().endTime;
 
     // Set the underlying opacity to zero so that if we sample the animation's output
     // during the delay phase and it is not filling backwards, we get zero.
     simulatedElement.style.opacity = 0;
 
     const getValueFunc = time => {
       simulatedAnimation.currentTime = time;
       return win.getComputedStyle(simulatedElement).opacity;
     };
 
-    const toPathStringFunc = segments => {
-      const firstSegment = segments[0];
-      let pathString = `M${ firstSegment.x },0 `;
-      pathString += toPathString(segments);
-      const lastSegment = segments[segments.length - 1];
-      pathString += `L${ lastSegment.x },0 Z`;
-      return pathString;
-    };
-
+    const toPathStringFunc =
+      createSummaryGraphPathStringFunction(endTime, state.playbackRate);
     const helper = new SummaryGraphHelper(state, keyframes,
                                           totalDuration, durationPerPixel,
                                           getValueFunc, toPathStringFunc);
 
     return dom.g(
       {
         className: this.getClassName(),
         transform: `translate(${ offset })`
--- a/devtools/client/inspector/animation/components/graph/SummaryGraph.js
+++ b/devtools/client/inspector/animation/components/graph/SummaryGraph.js
@@ -141,42 +141,45 @@ class SummaryGraph extends PureComponent
     const {
       animation,
       emitEventForTest,
       getAnimatedPropertyMap,
       simulateAnimation,
       timeScale,
     } = this.props;
 
+    const { iterationCount } = animation.state;
+    const { delay, endDelay } = animation.state.absoluteValues;
+
     return dom.div(
       {
         className: "animation-summary-graph" +
                    (animation.state.isRunningOnCompositor ? " compositor" : ""),
         onClick: this.onClick,
         title: this.getTitleText(animation.state),
       },
       SummaryGraphPath(
         {
           animation,
           emitEventForTest,
           getAnimatedPropertyMap,
           simulateAnimation,
           timeScale,
         }
       ),
-      animation.state.delay ?
+      delay ?
         DelaySign(
           {
             animation,
             timeScale,
           }
         )
       :
       null,
-      animation.state.iterationCount && animation.state.endDelay ?
+      iterationCount && endDelay ?
         EndDelaySign(
           {
             animation,
             timeScale,
           }
         )
       :
       null,
--- a/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js
+++ b/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js
@@ -163,17 +163,18 @@ class SummaryGraphPath extends Component
     } catch (e) {
       // Expected if we've already been destroyed or other node have been selected
       // in the meantime.
       console.error(e);
       return;
     }
 
     const keyframesList = this.getOffsetAndEasingOnlyKeyframes(animatedPropertyMap);
-    const totalDuration = timeScale.getDuration() * animation.state.playbackRate;
+    const totalDuration =
+      timeScale.getDuration() * Math.abs(animation.state.playbackRate);
     const durationPerPixel = totalDuration / thisEl.parentNode.clientWidth;
 
     this.setState(
       {
         durationPerPixel,
         isStateUpdating: false,
         keyframesList
       }
@@ -188,29 +189,26 @@ class SummaryGraphPath extends Component
 
     if (!durationPerPixel || !animation.state.type) {
       // Undefined animation.state.type means that the animation had been removed already.
       // Even if the animation was removed, we still need the empty svg since the
       // component might be re-used.
       return dom.svg();
     }
 
-    const { createdTime, playbackRate } = animation.state;
+    const { playbackRate } = animation.state;
+    const { createdTime } = animation.state.absoluteValues;
+    const absPlaybackRate = Math.abs(playbackRate);
 
-    // If createdTime is not defined (which happens when connected to server older
-    // than FF62), use previousStartTime instead. See bug 1454392
-    const baseTime = typeof createdTime === "undefined"
-                       ? (animation.state.previousStartTime || 0)
-                       : createdTime;
     // Absorb the playbackRate in viewBox of SVG and offset of child path elements
     // in order to each graph path components can draw without considering to the
     // playbackRate.
-    const offset = baseTime * playbackRate;
-    const startTime = timeScale.minStartTime * playbackRate;
-    const totalDuration = timeScale.getDuration() * playbackRate;
+    const offset = createdTime * absPlaybackRate;
+    const startTime = timeScale.minStartTime * absPlaybackRate;
+    const totalDuration = timeScale.getDuration() * absPlaybackRate;
     const opacity = Math.max(1 / keyframesList.length, MIN_KEYFRAMES_EASING_OPACITY);
 
     return dom.svg(
       {
         className: "animation-summary-graph-path",
         preserveAspectRatio: "none",
         viewBox: `${ startTime } -${ DEFAULT_GRAPH_HEIGHT } `
                  + `${ totalDuration } ${ DEFAULT_GRAPH_HEIGHT }`,
--- a/devtools/client/inspector/animation/utils/graph-helper.js
+++ b/devtools/client/inspector/animation/utils/graph-helper.js
@@ -150,16 +150,40 @@ function createPathSegments(startTime, e
     pathSegments.push(currentSegment);
     previousSegment = currentSegment;
   }
 
   return pathSegments;
 }
 
 /**
+ * Create a function which is used as parameter (toPathStringFunc) in constructor
+ * of SummaryGraphHelper.
+ *
+ * @param {Number} endTime
+ *        end time of animation
+ *        e.g. 200
+ * @param {Number} playbackRate
+ *        playback rate of animation
+ *        e.g. -1
+ * @return {Function}
+ */
+function createSummaryGraphPathStringFunction(endTime, playbackRate) {
+  return segments => {
+    segments = mapSegmentsToPlaybackRate(segments, endTime, playbackRate);
+    const firstSegment = segments[0];
+    let pathString = `M${ firstSegment.x },0 `;
+    pathString += toPathString(segments);
+    const lastSegment = segments[segments.length - 1];
+    pathString += `L${ lastSegment.x },0 Z`;
+    return pathString;
+  };
+}
+
+/**
  * Return preferred duration resolution.
  * This corresponds to narrow interval keyframe offset.
  *
  * @param {Array} keyframes
  *        Array of keyframe.
  * @return {Number}
  *         Preferred duration resolution.
  */
@@ -238,16 +262,27 @@ function getPreferredProgressThresholdBy
   return threshold;
 }
 
 function getStepsOrFramesCount(easing) {
   const stepsOrFramesFunction = easing.match(/(steps|frames)\((\d+)/);
   return stepsOrFramesFunction ? parseInt(stepsOrFramesFunction[2], 10) : 0;
 }
 
+function mapSegmentsToPlaybackRate(segments, endTime, playbackRate) {
+  if (playbackRate > 0) {
+    return segments;
+  }
+
+  return segments.map(segment => {
+    segment.x = endTime - segment.x;
+    return segment;
+  });
+}
+
 /**
  * Return path string for 'd' attribute for <path> from given segments.
  *
  * @param {Array} segments
  *        e.g. [{ x: 100, y: 0 }, { x: 200, y: 1 }]
  * @return {String}
  *         Path string.
  *         e.g. "L100,0 L200,1"
@@ -256,16 +291,17 @@ function toPathString(segments) {
   let pathString = "";
   segments.forEach(segment => {
     pathString += `L${ segment.x },${ segment.y } `;
   });
   return pathString;
 }
 
 exports.createPathSegments = createPathSegments;
+exports.createSummaryGraphPathStringFunction = createSummaryGraphPathStringFunction;
 exports.DEFAULT_DURATION_RESOLUTION = DEFAULT_DURATION_RESOLUTION;
 exports.DEFAULT_EASING_HINT_STROKE_WIDTH = DEFAULT_EASING_HINT_STROKE_WIDTH;
 exports.DEFAULT_GRAPH_HEIGHT = DEFAULT_GRAPH_HEIGHT;
 exports.DEFAULT_KEYFRAMES_GRAPH_DURATION = DEFAULT_KEYFRAMES_GRAPH_DURATION;
 exports.getPreferredProgressThresholdByKeyframes =
   getPreferredProgressThresholdByKeyframes;
 exports.SummaryGraphHelper = SummaryGraphHelper;
 exports.toPathString = toPathString;
--- a/devtools/client/inspector/animation/utils/timescale.js
+++ b/devtools/client/inspector/animation/utils/timescale.js
@@ -18,92 +18,64 @@ const TIME_FORMAT_MAX_DURATION_IN_MS = 4
  */
 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;
+    let resultCurrentTime = -Number.MAX_VALUE;
+    let resultMinStartTime = Infinity;
+    let resultMaxEndTime = 0;
+    let resultZeroPositionTime = 0;
 
     for (const animation of animations) {
       const {
-        createdTime,
         currentTime,
         currentTimeAtCreated,
         delay,
-        duration,
-        endDelay = 0,
-        iterationCount,
-        playbackRate,
-      } = animation.state;
+        endTime,
+        startTimeAtCreated,
+      } = animation.state.absoluteValues;
+      let { startTime } = animation.state.absoluteValues;
 
-      const toRate = v => v / playbackRate;
-      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;
+      const negativeDelay = Math.min(delay, 0);
+      let zeroPositionTime = 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);
+      if (currentTimeAtCreated < negativeDelay) {
+        startTime = startTimeAtCreated;
+        zeroPositionTime = Math.abs(currentTimeAtCreated);
+      } else if (negativeDelay < 0) {
+        zeroPositionTime = 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);
+      if (startTime < resultMinStartTime) {
+        resultMinStartTime = startTime;
+        // Override the previous calculated zero position only if the duration will be
+        // expanded.
+        resultZeroPositionTime = zeroPositionTime;
       } else {
-        endTime = createdTime +
-                  toRate(delay +
-                         duration * (iterationCount || 1) +
-                         Math.max(endDelay, 0));
+        resultZeroPositionTime = Math.max(resultZeroPositionTime, zeroPositionTime);
       }
 
-      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);
-      }
+      resultMaxEndTime = Math.max(resultMaxEndTime, endTime);
+      resultCurrentTime = Math.max(resultCurrentTime, currentTime);
     }
 
-    this.minStartTime = minStartTime;
-    this.maxEndTime = maxEndTime;
-    this.currentTime = animationsCurrentTime;
-    this.zeroPositionTime = zeroPositionTime;
+    this.minStartTime = resultMinStartTime;
+    this.maxEndTime = resultMaxEndTime;
+    this.currentTime = resultCurrentTime;
+    this.zeroPositionTime = resultZeroPositionTime;
   }
 
   /**
    * 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
    */
--- a/devtools/shared/fronts/animation.js
+++ b/devtools/shared/fronts/animation.js
@@ -67,16 +67,17 @@ const AnimationPlayerFront = FrontClassW
       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,
+      absoluteValues: this.calculateAbsoluteValues(this._form),
     };
   },
 
   /**
    * Executed when the AnimationPlayerActor emits a "changed" event. Used to
    * update the local knowledge of the state.
    */
   onChanged: preEvent("changed", function(partialState) {
@@ -116,18 +117,91 @@ const AnimationPlayerFront = FrontClassW
     for (const key in this.state) {
       if (typeof data[key] === "undefined") {
         data[key] = this.state[key];
       } else if (data[key] !== this.state[key]) {
         hasChanged = true;
       }
     }
 
+    data.absoluteValues = this.calculateAbsoluteValues(data);
     return {state: data, hasChanged};
-  }
+  },
+
+  calculateAbsoluteValues(data) {
+    const {
+      createdTime,
+      currentTime,
+      currentTimeAtCreated,
+      delay,
+      duration,
+      endDelay = 0,
+      fill,
+      iterationCount,
+      playbackRate,
+    } = data;
+
+    const toRate = v => v / Math.abs(playbackRate);
+    const isPositivePlaybackRate = playbackRate > 0;
+    let absoluteDelay = 0;
+    let absoluteEndDelay = 0;
+    let isDelayFilled = false;
+    let isEndDelayFilled = false;
+
+    if (isPositivePlaybackRate) {
+      absoluteDelay = toRate(delay);
+      absoluteEndDelay = toRate(endDelay);
+      isDelayFilled = fill === "both" || fill === "backwards";
+      isEndDelayFilled = fill === "both" || fill === "forwards";
+    } else {
+      absoluteDelay = toRate(endDelay);
+      absoluteEndDelay = toRate(delay);
+      isDelayFilled = fill === "both" || fill === "forwards";
+      isEndDelayFilled = fill === "both" || fill === "backwards";
+    }
+
+    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 = (absoluteDelay > 0 ? absoluteDelay * 2 : 1);
+    } else {
+      endTime = absoluteDelay +
+                toRate(duration * (iterationCount || 1)) +
+                absoluteEndDelay;
+    }
+
+    const absoluteCreatedTime =
+      isPositivePlaybackRate ? createdTime : createdTime - endTime;
+    const absoluteCurrentTimeAtCreated =
+      isPositivePlaybackRate ? currentTimeAtCreated : endTime - currentTimeAtCreated;
+    const absoluteCurrentTime = absoluteCreatedTime + toRate(currentTime);
+    const absoluteStartTime = absoluteCreatedTime + Math.min(absoluteDelay, 0);
+    const absoluteStartTimeAtCreated = absoluteCreatedTime + absoluteCurrentTimeAtCreated;
+    // To show whole graph with endDelay, we add negative endDelay amount to endTime.
+    const endTimeWithNegativeEndDelay = endTime - Math.min(absoluteEndDelay, 0);
+    const absoluteEndTime = absoluteCreatedTime + endTimeWithNegativeEndDelay;
+
+    return {
+      createdTime: absoluteCreatedTime,
+      currentTime: absoluteCurrentTime,
+      currentTimeAtCreated: absoluteCurrentTimeAtCreated,
+      delay: absoluteDelay,
+      endDelay: absoluteEndDelay,
+      endTime: absoluteEndTime,
+      isDelayFilled,
+      isEndDelayFilled,
+      startTime: absoluteStartTime,
+      startTimeAtCreated: absoluteStartTimeAtCreated,
+    };
+  },
 });
 
 exports.AnimationPlayerFront = AnimationPlayerFront;
 
 const AnimationsFront = FrontClassWithSpec(animationsSpec, {
   initialize: function(client, {animationsActor}) {
     Front.prototype.initialize.call(this, client, {actor: animationsActor});
     this.manage(this);