Bug 1210795 - Part 3: Changes delay and endDelay expression. r=pbro
authorDaisuke Akatsuka <daisuke@mozilla-japan.org>
Wed, 26 Oct 2016 16:37:07 +0900
changeset 319806 4f2a78576e79352669810610f2fde5ff4bec856f
parent 319805 e3f96551347dddaf90cb22c3e886e3de04adeaa5
child 319807 a1811cc95604375b097adf14ec2addb860fc768d
push id20748
push userphilringnalda@gmail.com
push dateFri, 28 Oct 2016 03:39:55 +0000
treeherderfx-team@715360440695 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1210795
milestone52.0a1
Bug 1210795 - Part 3: Changes delay and endDelay expression. r=pbro MozReview-Commit-ID: DqBZxMBxjto
devtools/client/animationinspector/components/animation-time-block.js
devtools/client/animationinspector/test/browser_animation_timeline_shows_delay.js
devtools/client/animationinspector/test/browser_animation_timeline_shows_endDelay.js
devtools/client/themes/animationinspector.css
--- a/devtools/client/animationinspector/components/animation-time-block.js
+++ b/devtools/client/animationinspector/components/animation-time-block.js
@@ -95,17 +95,17 @@ AnimationTimeBlock.prototype = {
     // Set viewBox
     summaryEl.setAttribute("viewBox",
                            `${ state.delay < 0 ? state.delay : 0 }
                             -${ 1 + strokeHeightForViewBox }
                             ${ totalDisplayedDuration }
                             ${ 1 + strokeHeightForViewBox * 2 }`);
 
     // Get a helper function that returns the path segment of timing-function.
-    const segmentHelperFn = getSegmentHelper(state, this.win);
+    const segmentHelper = getSegmentHelper(state, this.win);
 
     // Minimum segment duration is the duration of one pixel.
     const minSegmentDuration =
       totalDisplayedDuration / this.containerEl.clientWidth;
     // Minimum progress threshold.
     let minProgressThreshold = MIN_PROGRESS_THRESHOLD;
     // If the easing is step function,
     // minProgressThreshold should be changed by the steps.
@@ -116,17 +116,17 @@ AnimationTimeBlock.prototype = {
 
     // Starting time of main iteration.
     let mainIterationStartTime = 0;
     let iterationStart = state.iterationStart;
     let iterationCount = state.iterationCount ? state.iterationCount : Infinity;
 
     // Append delay.
     if (state.delay > 0) {
-      renderDelay(summaryEl, state, segmentHelperFn);
+      renderDelay(summaryEl, state, segmentHelper);
       mainIterationStartTime = state.delay;
     } else {
       const negativeDelayCount = -state.delay / state.duration;
       // Move to forward the starting point for negative delay.
       iterationStart += negativeDelayCount;
       // Consume iteration count by negative delay.
       if (iterationCount !== Infinity) {
         iterationCount -= negativeDelayCount;
@@ -138,64 +138,83 @@ AnimationTimeBlock.prototype = {
     // e.g.
     // if { iterationStart: 0.25, iterations: 3 }, firstSectionCount is 0.75.
     const firstSectionCount =
       iterationStart % 1 === 0
       ? 0 : Math.min(iterationCount, 1) - iterationStart % 1;
     if (firstSectionCount) {
       renderFirstIteration(summaryEl, state, mainIterationStartTime,
                            firstSectionCount, minSegmentDuration,
-                           minProgressThreshold, segmentHelperFn);
+                           minProgressThreshold, segmentHelper);
     }
 
     if (iterationCount === Infinity) {
       // If the animation repeats infinitely,
       // we fill the remaining area with iteration paths.
       renderInfinity(summaryEl, state, mainIterationStartTime,
                      firstSectionCount, totalDisplayedDuration,
-                     minSegmentDuration, minProgressThreshold, segmentHelperFn);
+                     minSegmentDuration, minProgressThreshold, segmentHelper);
     } else {
       // Otherwise, we show remaining iterations, endDelay and fill.
 
       // Append forwards fill-mode.
       if (state.fill === "both" || state.fill === "forwards") {
         renderForwardsFill(summaryEl, state, mainIterationStartTime,
                            iterationCount, totalDisplayedDuration,
-                           segmentHelperFn);
+                           segmentHelper);
       }
 
       // Append middle section of iterations.
       // e.g.
       // if { iterationStart: 0.25, iterations: 3 }, middleSectionCount is 2.
       const middleSectionCount =
         Math.floor(iterationCount - firstSectionCount);
       renderMiddleIterations(summaryEl, state, mainIterationStartTime,
                              firstSectionCount, middleSectionCount,
                              minSegmentDuration, minProgressThreshold,
-                             segmentHelperFn);
+                             segmentHelper);
 
       // Append last section of iterations, if there is remaining iteration.
       // e.g.
       // if { iterationStart: 0.25, iterations: 3 }, lastSectionCount is 0.25.
       const lastSectionCount =
         iterationCount - middleSectionCount - firstSectionCount;
       if (lastSectionCount) {
         renderLastIteration(summaryEl, state, mainIterationStartTime,
                             firstSectionCount, middleSectionCount,
                             lastSectionCount, minSegmentDuration,
-                            minProgressThreshold, segmentHelperFn);
+                            minProgressThreshold, segmentHelper);
       }
 
       // Append endDelay.
       if (state.endDelay > 0) {
         renderEndDelay(summaryEl, state,
-                       mainIterationStartTime, iterationCount, segmentHelperFn);
+                       mainIterationStartTime, iterationCount, segmentHelper);
       }
     }
 
+    // Append negative delay (which overlap the animation).
+    if (state.delay < 0) {
+      segmentHelper.animation.effect.timing.fill = "both";
+      segmentHelper.asOriginalBehavior = false;
+      renderNegativeDelayHiddenProgress(summaryEl, state, minSegmentDuration,
+                                        minProgressThreshold, segmentHelper);
+    }
+    // Append negative endDelay (which overlap the animation).
+    if (state.iterationCount && state.endDelay < 0) {
+      if (segmentHelper.asOriginalBehavior) {
+        segmentHelper.animation.effect.timing.fill = "both";
+        segmentHelper.asOriginalBehavior = false;
+      }
+      renderNegativeEndDelayHiddenProgress(summaryEl, state,
+                                           minSegmentDuration,
+                                           minProgressThreshold,
+                                           segmentHelper);
+    }
+
     // The animation name is displayed over the iterations.
     // Note that in case of negative delay, it is pushed towards the right so
     // the delay element does not overlap.
     createNode({
       parent: createNode({
         parent: this.containerEl,
         attributes: {
           "class": "name",
@@ -210,28 +229,34 @@ AnimationTimeBlock.prototype = {
     });
 
     // Delay.
     if (state.delay) {
       // Negative delays need to start at 0.
       createNode({
         parent: this.containerEl,
         attributes: {
-          "class": "delay" + (state.delay < 0 ? " negative" : ""),
+          "class": "delay"
+                   + (state.delay < 0 ? " negative" : " positive")
+                   + (state.fill === "both" ||
+                      state.fill === "backwards" ? " fill" : ""),
           "style": `left:${ delayX }%; width:${ delayW }%;`
         }
       });
     }
 
     // endDelay
-    if (state.endDelay) {
+    if (state.iterationCount && state.endDelay) {
       createNode({
         parent: this.containerEl,
         attributes: {
-          "class": "end-delay" + (state.endDelay < 0 ? " negative" : ""),
+          "class": "end-delay"
+                   + (state.endDelay < 0 ? " negative" : " positive")
+                   + (state.fill === "both" ||
+                      state.fill === "forwards" ? " fill" : ""),
           "style": `left:${ endDelayX }%; width:${ endDelayW }%;`
         }
       });
     }
   },
 
   getTooltipText: function (state) {
     let getTime = time => L10N.getFormatStr("player.timeLabel",
@@ -336,111 +361,111 @@ function getFormattedAnimationTitle({sta
 
   return L10N.getFormatStr(`timeline.${state.type}.nameLabel`, state.name);
 }
 
 /**
  * Render delay section.
  * @param {Element} parentEl - Parent element of this appended path element.
  * @param {Object} state - State of animation.
- * @param {function} getSegment - The function of getSegmentHelper.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
  */
-function renderDelay(parentEl, state, getSegment) {
-  const startSegment = getSegment(0);
+function renderDelay(parentEl, state, segmentHelper) {
+  const startSegment = segmentHelper.getSegment(0);
   const endSegment = { x: state.delay, y: startSegment.y };
   appendPathElement(parentEl, [startSegment, endSegment], "delay-path");
 }
 
 /**
  * Render first iteration section.
  * @param {Element} parentEl - Parent element of this appended path element.
  * @param {Object} state - State of animation.
  * @param {Number} mainIterationStartTime - Starting time of main iteration.
  * @param {Number} firstSectionCount - Iteration count of first section.
  * @param {Number} minSegmentDuration - Minimum segment duration.
  * @param {Number} minProgressThreshold - Minimum progress threshold.
- * @param {function} getSegment - The function of getSegmentHelper.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
  */
 function renderFirstIteration(parentEl, state, mainIterationStartTime,
                               firstSectionCount, minSegmentDuration,
-                              minProgressThreshold, getSegment) {
+                              minProgressThreshold, segmentHelper) {
   const startTime = mainIterationStartTime;
   const endTime = startTime + firstSectionCount * state.duration;
   const segments =
     createPathSegments(startTime, endTime, minSegmentDuration,
-                       minProgressThreshold, getSegment);
+                       minProgressThreshold, segmentHelper);
   appendPathElement(parentEl, segments, "iteration-path");
 }
 
 /**
  * Render middle iterations section.
  * @param {Element} parentEl - Parent element of this appended path element.
  * @param {Object} state - State of animation.
  * @param {Number} mainIterationStartTime - Starting time of main iteration.
  * @param {Number} firstSectionCount - Iteration count of first section.
  * @param {Number} middleSectionCount - Iteration count of middle section.
  * @param {Number} minSegmentDuration - Minimum segment duration.
  * @param {Number} minProgressThreshold - Minimum progress threshold.
- * @param {function} getSegment - The function of getSegmentHelper.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
  */
 function renderMiddleIterations(parentEl, state, mainIterationStartTime,
                                 firstSectionCount, middleSectionCount,
                                 minSegmentDuration, minProgressThreshold,
-                                getSegment) {
+                                segmentHelper) {
   const offset = mainIterationStartTime + firstSectionCount * state.duration;
   for (let i = 0; i < middleSectionCount; i++) {
     // Get the path segments of each iteration.
     const startTime = offset + i * state.duration;
     const endTime = startTime + state.duration;
     const segments =
       createPathSegments(startTime, endTime, minSegmentDuration,
-                         minProgressThreshold, getSegment);
+                         minProgressThreshold, segmentHelper);
     appendPathElement(parentEl, segments, "iteration-path");
   }
 }
 
 /**
  * Render last iteration section.
  * @param {Element} parentEl - Parent element of this appended path element.
  * @param {Object} state - State of animation.
  * @param {Number} mainIterationStartTime - Starting time of main iteration.
  * @param {Number} firstSectionCount - Iteration count of first section.
  * @param {Number} middleSectionCount - Iteration count of middle section.
  * @param {Number} lastSectionCount - Iteration count of last section.
  * @param {Number} minSegmentDuration - Minimum segment duration.
  * @param {Number} minProgressThreshold - Minimum progress threshold.
- * @param {function} getSegment - The function of getSegmentHelper.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
  */
 function renderLastIteration(parentEl, state, mainIterationStartTime,
                              firstSectionCount, middleSectionCount,
                              lastSectionCount, minSegmentDuration,
-                             minProgressThreshold, getSegment) {
+                             minProgressThreshold, segmentHelper) {
   const startTime = mainIterationStartTime +
                       (firstSectionCount + middleSectionCount) * state.duration;
   const endTime = startTime + lastSectionCount * state.duration;
   const segments =
     createPathSegments(startTime, endTime, minSegmentDuration,
-                       minProgressThreshold, getSegment);
+                       minProgressThreshold, segmentHelper);
   appendPathElement(parentEl, segments, "iteration-path");
 }
 
 /**
  * Render Infinity iterations.
  * @param {Element} parentEl - Parent element of this appended path element.
  * @param {Object} state - State of animation.
  * @param {Number} mainIterationStartTime - Starting time of main iteration.
  * @param {Number} firstSectionCount - Iteration count of first section.
  * @param {Number} totalDuration - Displayed max duration.
  * @param {Number} minSegmentDuration - Minimum segment duration.
  * @param {Number} minProgressThreshold - Minimum progress threshold.
- * @param {function} getSegment - The function of getSegmentHelper.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
  */
 function renderInfinity(parentEl, state, mainIterationStartTime,
                         firstSectionCount, totalDuration, minSegmentDuration,
-                        minProgressThreshold, getSegment) {
+                        minProgressThreshold, segmentHelper) {
   // Calculate the number of iterations to display,
   // with a maximum of MAX_INFINITE_ANIMATIONS_ITERATIONS
   let uncappedInfinityIterationCount =
     (totalDuration - firstSectionCount * state.duration) / state.duration;
   // If there is a small floating point error resulting in, e.g. 1.0000001
   // ceil will give us 2 so round first.
   uncappedInfinityIterationCount =
     parseFloat(uncappedInfinityIterationCount.toPrecision(6));
@@ -449,17 +474,17 @@ function renderInfinity(parentEl, state,
              Math.ceil(uncappedInfinityIterationCount));
 
   // Append first full iteration path.
   const firstStartTime =
     mainIterationStartTime + firstSectionCount * state.duration;
   const firstEndTime = firstStartTime + state.duration;
   const firstSegments =
     createPathSegments(firstStartTime, firstEndTime, minSegmentDuration,
-                       minProgressThreshold, getSegment);
+                       minProgressThreshold, segmentHelper);
   appendPathElement(parentEl, firstSegments, "iteration-path infinity");
 
   // Append other iterations. We can copy first segments.
   const isAlternate = state.direction.match(/alternate/);
   for (let i = 1; i < infinityIterationCount; i++) {
     const startTime = firstStartTime + i * state.duration;
     let segments;
     if (isAlternate && i % 2) {
@@ -478,117 +503,176 @@ function renderInfinity(parentEl, state,
 }
 
 /**
  * Render endDelay section.
  * @param {Element} parentEl - Parent element of this appended path element.
  * @param {Object} state - State of animation.
  * @param {Number} mainIterationStartTime - Starting time of main iteration.
  * @param {Number} iterationCount - Whole iteration count.
- * @param {function} getSegment - The function of getSegmentHelper.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
  */
 function renderEndDelay(parentEl, state,
-                        mainIterationStartTime, iterationCount, getSegment) {
+                        mainIterationStartTime, iterationCount, segmentHelper) {
   const startTime = mainIterationStartTime + iterationCount * state.duration;
-  const startSegment = getSegment(startTime);
+  const startSegment = segmentHelper.getSegment(startTime);
   const endSegment = { x: startTime + state.endDelay, y: startSegment.y };
   appendPathElement(parentEl, [startSegment, endSegment], "enddelay-path");
 }
 
 /**
  * Render forwards fill section.
  * @param {Element} parentEl - Parent element of this appended path element.
  * @param {Object} state - State of animation.
  * @param {Number} mainIterationStartTime - Starting time of main iteration.
  * @param {Number} iterationCount - Whole iteration count.
  * @param {Number} totalDuration - Displayed max duration.
- * @param {function} getSegment - The function of getSegmentHelper.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
  */
 function renderForwardsFill(parentEl, state, mainIterationStartTime,
-                            iterationCount, totalDuration, getSegment) {
+                            iterationCount, totalDuration, segmentHelper) {
   const startTime = mainIterationStartTime + iterationCount * state.duration +
                       (state.endDelay > 0 ? state.endDelay : 0);
-  const startSegment = getSegment(startTime);
+  const startSegment = segmentHelper.getSegment(startTime);
   const endSegment = { x: totalDuration, y: startSegment.y };
   appendPathElement(parentEl, [startSegment, endSegment], "fill-forwards-path");
 }
 
 /**
+ * Render hidden progress of negative delay.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderNegativeDelayHiddenProgress(parentEl, state, minSegmentDuration,
+                                           minProgressThreshold,
+                                           segmentHelper) {
+  const startTime = state.delay;
+  const endTime = 0;
+  const segments =
+    createPathSegments(startTime, endTime, minSegmentDuration,
+                       minProgressThreshold, segmentHelper);
+  appendPathElement(parentEl, segments, "delay-path negative");
+}
+
+/**
+ * Render hidden progress of negative endDelay.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderNegativeEndDelayHiddenProgress(parentEl, state,
+                                              minSegmentDuration,
+                                              minProgressThreshold,
+                                              segmentHelper) {
+  const endTime = state.delay + state.iterationCount * state.duration;
+  const startTime = endTime + state.endDelay;
+  const segments =
+    createPathSegments(startTime, endTime, minSegmentDuration,
+                       minProgressThreshold, segmentHelper);
+  appendPathElement(parentEl, segments, "enddelay-path negative");
+}
+
+/**
  * Get a helper function which returns the segment coord from given time.
  * @param {Object} state - animation state
  * @param {Object} win - window object
- * @return {function} getSegmentHelper
+ * @return {Object} A segmentHelper object that has the following properties:
+ * - animation: The script animation used to get the progress
+ * - endTime: The end time of the animation
+ * - asOriginalBehavior: The spec is that the progress of animation is changed
+ *                       if the time of setCurrentTime is during the endDelay.
+ *                       Likewise, in case the time is less than 0.
+ *                       If this flag is true, we prevent the time
+ *                       to make the same animation behavior as the original.
+ * - getSegment: Helper function that, given a time,
+ *               will calculate the progress through the dummy animation.
  */
 function getSegmentHelper(state, win) {
   // Create a dummy Animation timing data as the
   // state object we're being passed in.
   const timing = Object.assign({}, state, {
     iterations: state.iterationCount ? state.iterationCount : Infinity
   });
+
   // Create a dummy Animation with the given timing.
   const dummyAnimation =
     new win.Animation(new win.KeyframeEffect(null, null, timing), null);
-  const endTime = dummyAnimation.effect.getComputedTiming().endTime;
-  // Return a helper function that, given a time,
-  // will calculate the progress through the dummy animation.
-  return time => {
-    // If the given time is less than 0, returned progress is 0.
-    if (time < 0) {
-      return { x: time, y: 0 };
+
+  // Returns segment helper object.
+  return {
+    animation: dummyAnimation,
+    endTime: dummyAnimation.effect.getComputedTiming().endTime,
+    asOriginalBehavior: true,
+    getSegment: function (time) {
+      if (this.asOriginalBehavior) {
+        // If the given time is less than 0, returned progress is 0.
+        if (time < 0) {
+          return { x: time, y: 0 };
+        }
+        // Avoid to apply over endTime.
+        this.animation.currentTime = time < this.endTime ? time : this.endTime;
+      } else {
+        this.animation.currentTime = time;
+      }
+      const progress = this.animation.effect.getComputedTiming().progress;
+      return { x: time, y: Math.max(progress, 0) };
     }
-    dummyAnimation.currentTime =
-      time < endTime ? time : endTime;
-    const progress = dummyAnimation.effect.getComputedTiming().progress;
-    return { x: time, y: Math.max(progress, 0) };
   };
 }
 
 /**
  * Create the path segments from given parameters.
  * @param {Number} startTime - Starting time of animation.
  * @param {Number} endTime - Ending time of animation.
  * @param {Number} minSegmentDuration - Minimum segment duration.
  * @param {Number} minProgressThreshold - Minimum progress threshold.
- * @param {function} getSegment - The function of getSegmentHelper.
+ * @param {Object} segmentHelper - The object of getSegmentHelper.
  * @return {Array} path segments -
  *                 [{x: {Number} time, y: {Number} progress}, ...]
  */
 function createPathSegments(startTime, endTime, minSegmentDuration,
-                            minProgressThreshold, getSegment) {
+                            minProgressThreshold, segmentHelper) {
   // If the duration is too short, early return.
   if (endTime - startTime < minSegmentDuration) {
-    return [getSegment(startTime), getSegment(endTime)];
+    return [segmentHelper.getSegment(startTime),
+            segmentHelper.getSegment(endTime)];
   }
 
   // Otherwise, start creating segments.
   let pathSegments = [];
 
   // Append the segment for the startTime position.
-  const startTimeSegment = getSegment(startTime);
+  const startTimeSegment = segmentHelper.getSegment(startTime);
   pathSegments.push(startTimeSegment);
   let previousSegment = startTimeSegment;
 
   // Split the duration in equal intervals, and iterate over them.
   // See the definition of DURATION_RESOLUTION for more information about this.
   const interval = (endTime - startTime) / DURATION_RESOLUTION;
   for (let index = 1; index <= DURATION_RESOLUTION; index++) {
     // Create a segment for this interval.
-    const currentSegment = getSegment(startTime + index * interval);
+    const currentSegment =
+      segmentHelper.getSegment(startTime + index * interval);
 
     // If the distance between the Y coordinate (the animation's progress) of
     // the previous segment and the Y coordinate of the current segment is too
     // large, then recurse with a smaller duration to get more details
     // in the graph.
     if (Math.abs(currentSegment.y - previousSegment.y) > minProgressThreshold) {
       // Divide the current interval (excluding start and end bounds
       // by adding/subtracting 1ms).
       pathSegments = pathSegments.concat(
         createPathSegments(previousSegment.x + 1, currentSegment.x - 1,
                            minSegmentDuration, minProgressThreshold,
-                           getSegment));
+                           segmentHelper));
     }
 
     pathSegments.push(currentSegment);
     previousSegment = currentSegment;
   }
 
   return pathSegments;
 }
--- a/devtools/client/animationinspector/test/browser_animation_timeline_shows_delay.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_delay.js
@@ -14,24 +14,33 @@ requestLongerTimeout(2);
 add_task(function* () {
   yield addTab(URL_ROOT + "doc_simple_animation.html");
   let {inspector, panel} = yield openAnimationInspector();
 
   info("Selecting a delayed animated node");
   yield selectNodeAndWaitForAnimations(".delayed", inspector);
   let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
   checkDelayAndName(timelineEl, true);
+  let animationEl = timelineEl.querySelector(".animation");
+  let state = panel.animationsTimelineComponent.timeBlocks[0].animation.state;
+  checkPath(animationEl, state);
 
   info("Selecting a no-delay animated node");
   yield selectNodeAndWaitForAnimations(".animated", inspector);
   checkDelayAndName(timelineEl, false);
+  animationEl = timelineEl.querySelector(".animation");
+  state = panel.animationsTimelineComponent.timeBlocks[0].animation.state;
+  checkPath(animationEl, state);
 
   info("Selecting a negative-delay animated node");
   yield selectNodeAndWaitForAnimations(".negative-delay", inspector);
   checkDelayAndName(timelineEl, true);
+  animationEl = timelineEl.querySelector(".animation");
+  state = panel.animationsTimelineComponent.timeBlocks[0].animation.state;
+  checkPath(animationEl, state);
 });
 
 function checkDelayAndName(timelineEl, hasDelay) {
   let delay = timelineEl.querySelector(".delay");
 
   is(!!delay, hasDelay, "The timeline " +
                         (hasDelay ? "contains" : "does not contain") +
                         " a delay element, as expected");
@@ -48,8 +57,47 @@ function checkDelayAndName(timelineEl, h
 
     // Check that the delay is not displayed on top of the name.
     let delayRight = Math.round(delay.getBoundingClientRect().right);
     let nameLeft = Math.round(name.getBoundingClientRect().left);
     ok(delayRight <= nameLeft,
        "The delay element does not span over the name element");
   }
 }
+
+function checkPath(animationEl, state) {
+  // Check existance of delay path.
+  const delayPathEl = animationEl.querySelector(".delay-path");
+  if (!state.iterationCount && state.delay < 0) {
+    // Infinity
+    ok(!delayPathEl, "The delay path for Infinity should not exist");
+    return;
+  }
+  if (state.delay === 0) {
+    ok(!delayPathEl, "The delay path for zero delay should not exist");
+    return;
+  }
+  ok(delayPathEl, "The delay path should exist");
+
+  // Check delay path coordinates.
+  const pathSegList = delayPathEl.pathSegList;
+  const startingPathSeg = pathSegList.getItem(0);
+  const endingPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 2);
+  if (state.delay < 0) {
+    ok(delayPathEl.classList.contains("negative"),
+       "The delay path should have 'negative' class");
+    const startingX = state.delay;
+    const endingX = 0;
+    is(startingPathSeg.x, startingX,
+       `The x of starting point should be ${ startingX }`);
+    is(endingPathSeg.x, endingX,
+       `The x of ending point should be ${ endingX }`);
+  } else {
+    ok(!delayPathEl.classList.contains("negative"),
+       "The delay path should not have 'negative' class");
+    const startingX = 0;
+    const endingX = state.delay;
+    is(startingPathSeg.x, startingX,
+       `The x of starting point should be ${ startingX }`);
+    is(endingPathSeg.x, endingX,
+       `The x of ending point should be ${ endingX }`);
+  }
+}
--- a/devtools/client/animationinspector/test/browser_animation_timeline_shows_endDelay.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_endDelay.js
@@ -15,18 +15,21 @@ add_task(function* () {
   yield addTab(URL_ROOT + "doc_end_delay.html");
   let {inspector, panel} = yield openAnimationInspector();
 
   let selectors = ["#target1", "#target2", "#target3", "#target4"];
   for (let i = 0; i < selectors.length; i++) {
     let selector = selectors[i];
     yield selectNode(selector, inspector);
     let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
-    let animationEl = timelineEl.querySelectorAll(".animation")[0];
+    let animationEl = timelineEl.querySelector(".animation");
     checkEndDelayAndName(animationEl);
+    const state =
+      panel.animationsTimelineComponent.timeBlocks[0].animation.state;
+    checkPath(animationEl, state);
   }
 });
 
 function checkEndDelayAndName(animationEl) {
   let endDelay = animationEl.querySelector(".end-delay");
   let name = animationEl.querySelector(".name");
   let targetNode = animationEl.querySelector(".target");
 
@@ -37,8 +40,39 @@ function checkEndDelayAndName(animationE
      "The endDelay element isn't displayed over the sidebar");
 
   // Check that the endDelay is not displayed on top of the name.
   let endDelayRight = Math.round(endDelay.getBoundingClientRect().right);
   let nameLeft = Math.round(name.getBoundingClientRect().left);
   ok(endDelayRight >= nameLeft,
      "The endDelay element does not span over the name element");
 }
+
+function checkPath(animationEl, state) {
+  // Check existance of enddelay path.
+  const endDelayPathEl = animationEl.querySelector(".enddelay-path");
+  ok(endDelayPathEl, "The endDelay path should exist");
+
+  // Check enddelay path coordinates.
+  const pathSegList = endDelayPathEl.pathSegList;
+  const startingPathSeg = pathSegList.getItem(0);
+  const endingPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 2);
+  if (state.endDelay < 0) {
+    ok(endDelayPathEl.classList.contains("negative"),
+       "The endDelay path should have 'negative' class");
+    const endingX = state.delay + state.iterationCount * state.duration;
+    const startingX = endingX + state.endDelay;
+    is(startingPathSeg.x, startingX,
+       `The x of starting point should be ${ startingX }`);
+    is(endingPathSeg.x, endingX,
+       `The x of ending point should be ${ endingX }`);
+  } else {
+    ok(!endDelayPathEl.classList.contains("negative"),
+       "The endDelay path should not have 'negative' class");
+    const startingX =
+      state.delay + state.iterationCount * state.duration;
+    const endingX = startingX + state.endDelay;
+    is(startingPathSeg.x, startingX,
+       `The x of starting point should be ${ startingX }`);
+    is(endingPathSeg.x, endingX,
+       `The x of ending point should be ${ endingX }`);
+  }
+}
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -40,16 +40,22 @@
   --keyframes-marker-size: 10px;
   /* The color of the time graduation borders */
   --time-graduation-border-color: rgba(128, 136, 144, .5);
 }
 
 .animation {
   --timeline-border-color: var(--theme-body-color);
   --timeline-background-color: var(--theme-splitter-color);
+  /* The color of the endDelay hidden progress */
+  --enddelay-hidden-progress-color: var(--theme-graphs-grey);
+  /* The color of none fill mode */
+  --fill-none-color: var(--theme-highlight-gray);
+  /* The color of enable fill mode */
+  --fill-enable-color: var(--timeline-border-color);
 }
 
 .animation.cssanimation {
   --timeline-border-color: var(--theme-highlight-lightorange);
   --timeline-background-color: var(--theme-contrast-background);
 }
 
 .animation.csstransition {
@@ -355,16 +361,23 @@ body {
   fill: var(--timeline-background-color);
   stroke: var(--timeline-border-color);
 }
 
 .animation-timeline .animation .summary .infinity.copied {
   opacity: .3;
 }
 
+.animation-timeline .animation .summary path.delay-path.negative,
+.animation-timeline .animation .summary path.enddelay-path.negative {
+  fill: none;
+  stroke: var(--enddelay-hidden-progress-color);
+  stroke-dasharray: 2, 2;
+}
+
 .animation-timeline .animation .name {
   position: absolute;
   color: var(--theme-selection-color);
   height: 100%;
   display: flex;
   align-items: center;
   padding: 0 2px;
   box-sizing: border-box;
@@ -407,48 +420,51 @@ body {
   clip-path: url(images/animation-fast-track.svg#thunderbolt);
   background-repeat: no-repeat;
   background-position: center;
 }
 
 .animation-timeline .animation .delay,
 .animation-timeline .animation .end-delay {
   position: absolute;
-  height: 100%;
-  border: 1px solid var(--timeline-border-color);
-  box-sizing: border-box;
+  border-bottom: 3px solid var(--fill-none-color);
+  bottom: -0.5px;
 }
 
-.animation-timeline .animation .delay {
-  border-width: 1px 0 1px 1px;
-  background-image: repeating-linear-gradient(45deg,
-                                              transparent,
-                                              transparent 1px,
-                                              var(--theme-selection-color) 1px,
-                                              var(--theme-selection-color) 4px);
-  background-color: var(--timeline-border-color);
+.animation-timeline .animation .delay::after,
+.animation-timeline .animation .end-delay::after {
+  content: "";
+  position: absolute;
+  top: -2px;
+  width: 3px;
+  height: 3px;
+  border: 2px solid var(--fill-none-color);
+  background-color: var(--fill-none-color);
+  border-radius: 50%;
 }
 
-.animation-timeline .animation .end-delay {
-  border-width: 1px 1px 1px 0;
-  background-image: repeating-linear-gradient(
-                      -45deg,
-                      transparent,
-                      transparent 3px,
-                      var(--timeline-border-color) 3px,
-                      var(--timeline-border-color) 4px);
+.animation-timeline .animation .negative.delay::after,
+.animation-timeline .animation .positive.end-delay::after {
+  right: -3px;
+}
+
+.animation-timeline .animation .positive.delay::after,
+.animation-timeline .animation .negative.end-delay::after {
+  left: -3px;
 }
 
-.animation-timeline .animation .delay.negative,
-.animation-timeline .animation .end-delay.negative {
-  /* Negative delays are displayed on top of the animation, so they need a
-     right border. Whereas normal delays are displayed just before the
-     animation, so there's already the animation's left border that serves as
-     a separation. */
-  border-width: 1px;
+.animation-timeline .animation .fill.delay,
+.animation-timeline .animation .fill.end-delay {
+  border-color: var(--fill-enable-color);
+}
+
+.animation-timeline .animation .fill.delay::after,
+.animation-timeline .animation .fill.end-delay::after {
+  border-color: var(--fill-enable-color);
+  background-color: var(--fill-enable-color);
 }
 
 /* Animation target node gutter, contains a preview of the dom node */
 
 .animation-target {
   background-color: var(--theme-toolbar-background);
   padding: 0 4px;
   box-sizing: border-box;