Bug 1383974 - Part 1: Display easing in keyframes. r?pbro draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Mon, 25 Sep 2017 08:44:05 +0900
changeset 669597 4bf4080e3c94b2f453f921962749666508d13fe8
parent 669596 7e962631ba4298bcefa571008661983d77c3e652
child 669598 cb3483b4a7e78d30e00656b1573143070e72157b
push id81372
push userbmo:dakatsuka@mozilla.com
push dateMon, 25 Sep 2017 00:37:37 +0000
reviewerspbro
bugs1383974
milestone58.0a1
Bug 1383974 - Part 1: Display easing in keyframes. r?pbro MozReview-Commit-ID: 8pIUMAurfS3
devtools/client/animationinspector/components/animation-time-block.js
devtools/client/animationinspector/components/keyframes.js
devtools/client/animationinspector/graph-helper.js
devtools/client/themes/animationinspector.css
--- a/devtools/client/animationinspector/components/animation-time-block.js
+++ b/devtools/client/animationinspector/components/animation-time-block.js
@@ -457,33 +457,33 @@ function renderGraph(parentEl, state, to
  * Render delay section.
  * @param {Element} parentEl - Parent element of this appended path element.
  * @param {Object} state - State of animation.
  * @param {Object} graphHelper - SummaryGraphHelper.
  */
 function renderDelay(parentEl, state, graphHelper) {
   const startSegment = graphHelper.getSegment(0);
   const endSegment = { x: state.delay, y: startSegment.y };
-  graphHelper.appendPathElement(parentEl, [startSegment, endSegment], "delay-path");
+  graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
  */
 function renderFirstIteration(parentEl, state, mainIterationStartTime,
                               firstSectionCount, graphHelper) {
   const startTime = mainIterationStartTime;
   const endTime = startTime + firstSectionCount * state.duration;
   const segments = graphHelper.createPathSegments(startTime, endTime);
-  graphHelper.appendPathElement(parentEl, segments, "iteration-path");
+  graphHelper.appendShapePath(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.
@@ -494,17 +494,17 @@ function renderMiddleIterations(parentEl
                                 firstSectionCount, middleSectionCount,
                                 graphHelper) {
   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 = graphHelper.createPathSegments(startTime, endTime);
-    graphHelper.appendPathElement(parentEl, segments, "iteration-path");
+    graphHelper.appendShapePath(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.
@@ -515,17 +515,17 @@ function renderMiddleIterations(parentEl
  */
 function renderLastIteration(parentEl, state, mainIterationStartTime,
                              firstSectionCount, middleSectionCount,
                              lastSectionCount, graphHelper) {
   const startTime = mainIterationStartTime +
                       (firstSectionCount + middleSectionCount) * state.duration;
   const endTime = startTime + lastSectionCount * state.duration;
   const segments = graphHelper.createPathSegments(startTime, endTime);
-  graphHelper.appendPathElement(parentEl, segments, "iteration-path");
+  graphHelper.appendShapePath(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.
@@ -547,17 +547,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 =
     graphHelper.createPathSegments(firstStartTime, firstEndTime);
-  graphHelper.appendPathElement(parentEl, firstSegments, "iteration-path infinity");
+  graphHelper.appendShapePath(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) {
       // Copy as reverse.
@@ -565,34 +565,34 @@ function renderInfinity(parentEl, state,
         return { x: firstEndTime - segment.x + startTime, y: segment.y };
       });
     } else {
       // Copy as is.
       segments = firstSegments.map(segment => {
         return { x: segment.x - firstStartTime + startTime, y: segment.y };
       });
     }
-    graphHelper.appendPathElement(parentEl, segments, "iteration-path infinity copied");
+    graphHelper.appendShapePath(parentEl, segments, "iteration-path infinity copied");
   }
 }
 
 /**
  * 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 {Object} graphHelper - SummaryGraphHelper.
  */
 function renderEndDelay(parentEl, state,
                         mainIterationStartTime, iterationCount, graphHelper) {
   const startTime = mainIterationStartTime + iterationCount * state.duration;
   const startSegment = graphHelper.getSegment(startTime);
   const endSegment = { x: startTime + state.endDelay, y: startSegment.y };
-  graphHelper.appendPathElement(parentEl, [startSegment, endSegment], "enddelay-path");
+  graphHelper.appendShapePath(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.
@@ -600,45 +600,44 @@ function renderEndDelay(parentEl, state,
  * @param {Object} graphHelper - SummaryGraphHelper.
  */
 function renderForwardsFill(parentEl, state, mainIterationStartTime,
                             iterationCount, totalDuration, graphHelper) {
   const startTime = mainIterationStartTime + iterationCount * state.duration +
                       (state.endDelay > 0 ? state.endDelay : 0);
   const startSegment = graphHelper.getSegment(startTime);
   const endSegment = { x: totalDuration, y: startSegment.y };
-  graphHelper.appendPathElement(parentEl, [startSegment, endSegment],
-                                "fill-forwards-path");
+  graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
  */
 function renderNegativeDelayHiddenProgress(parentEl, state, graphHelper) {
   const startTime = state.delay;
   const endTime = 0;
   const segments =
     graphHelper.createPathSegments(startTime, endTime);
-  graphHelper.appendPathElement(parentEl, segments, "delay-path negative");
+  graphHelper.appendShapePath(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 {Object} graphHelper - SummaryGraphHelper.
  */
 function renderNegativeEndDelayHiddenProgress(parentEl, state, graphHelper) {
   const endTime = state.delay + state.iterationCount * state.duration;
   const startTime = endTime + state.endDelay;
   const segments = graphHelper.createPathSegments(startTime, endTime);
-  graphHelper.appendPathElement(parentEl, segments, "enddelay-path negative");
+  graphHelper.appendShapePath(parentEl, segments, "enddelay-path negative");
 }
 
 /**
  * Create new keyframes object which has only offset and easing.
  * Also, the returned value has no duplication.
  * @param {Object} tracks - The value of AnimationsTimeline.getTracks().
  * @return {Array} keyframes list.
  */
--- a/devtools/client/animationinspector/components/keyframes.js
+++ b/devtools/client/animationinspector/components/keyframes.js
@@ -4,17 +4,17 @@
  * 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 {createNode, createSVGNode} =
   require("devtools/client/animationinspector/utils");
 const {ProgressGraphHelper, getPreferredKeyframesProgressThreshold} =
-         require("devtools/client/animationinspector/graph-helper.js");
+  require("devtools/client/animationinspector/graph-helper.js");
 
 // Counter for linearGradient ID.
 let LINEAR_GRADIENT_ID_COUNTER = 0;
 
 /**
  * UI component responsible for displaying a list of keyframes.
  * Also, shows a graphical graph for the animation progress of one iteration.
  */
@@ -50,39 +50,44 @@ Keyframes.prototype = {
         "preserveAspectRatio": "none"
       }
     });
 
     // This visual is only one iteration,
     // so we use animation.state.duration as total duration.
     const totalDuration = animation.state.duration;
 
-    // Calculate stroke height in viewBox to display stroke of path.
-    const strokeHeightForViewBox = 0.5 / this.containerEl.clientHeight;
     // Minimum segment duration is the duration of one pixel.
     const minSegmentDuration =
       totalDuration / this.containerEl.clientWidth;
 
-    // Set viewBox.
-    graphEl.setAttribute("viewBox",
-                         `0 -${ 1 + strokeHeightForViewBox }
-                          ${ totalDuration }
-                          ${ 1 + strokeHeightForViewBox * 2 }`);
-
     // Create graph helper to render the animation property graph.
+    const win = this.containerEl.ownerGlobal;
     const graphHelper =
-      new ProgressGraphHelper(this.containerEl.ownerDocument.defaultView,
-                              propertyName, animationType, keyframes, totalDuration);
+      new ProgressGraphHelper(win, propertyName, animationType, keyframes, totalDuration);
 
     renderPropertyGraph(graphEl, totalDuration, minSegmentDuration,
                         getPreferredKeyframesProgressThreshold(keyframes), graphHelper);
 
     // Destroy ProgressGraphHelper resources.
     graphHelper.destroy();
 
+    // Set viewBox which includes invisible stroke width.
+    // At first, calculate invisible stroke width from maximum width.
+    // The reason why divide by 2 is that half of stroke width will be invisible
+    // if we use 0 or 1 for y axis.
+    const maxStrokeWidth =
+      win.getComputedStyle(graphEl.querySelector(".keyframes svg .hint")).strokeWidth;
+    const invisibleStrokeWidthInViewBox =
+      maxStrokeWidth / 2 / this.containerEl.clientHeight;
+    graphEl.setAttribute("viewBox",
+                         `0 -${ 1 + invisibleStrokeWidthInViewBox }
+                          ${ totalDuration }
+                          ${ 1 + invisibleStrokeWidthInViewBox * 2 }`);
+
     // Append elements to display keyframe values.
     this.keyframesEl.classList.add(animation.state.type);
     for (let frame of this.keyframes) {
       createNode({
         parent: this.keyframesEl,
         attributes: {
           "class": "frame",
           "style": `left:${frame.offset * 100}%;`,
@@ -105,25 +110,26 @@ Keyframes.prototype = {
  */
 function renderPropertyGraph(parentEl, duration, minSegmentDuration,
                              minProgressThreshold, graphHelper) {
   const segments = graphHelper.createPathSegments(0, duration, minSegmentDuration,
                                                   minProgressThreshold);
 
   const graphType = graphHelper.getGraphType();
   if (graphType !== "color") {
-    graphHelper.appendPathElement(parentEl, segments, graphType);
+    graphHelper.appendShapePath(parentEl, segments, graphType);
+    renderEasingHint(parentEl, segments, graphHelper);
     return;
   }
 
   // Append the color to the path.
   segments.forEach(segment => {
     segment.y = 1;
   });
-  const path = graphHelper.appendPathElement(parentEl, segments, graphType);
+  const path = graphHelper.appendShapePath(parentEl, segments, graphType);
   const defEl = createSVGNode({
     parent: parentEl,
     nodeType: "def"
   });
   const id = `color-property-${ LINEAR_GRADIENT_ID_COUNTER++ }`;
   const linearGradientEl = createSVGNode({
     parent: defEl,
     nodeType: "linearGradient",
@@ -137,9 +143,110 @@ function renderPropertyGraph(parentEl, d
       nodeType: "stop",
       attributes: {
         "stop-color": segment.style,
         "offset": segment.x / duration
       }
     });
   });
   path.style.fill = `url(#${ id })`;
+
+  renderEasingHintForColor(parentEl, graphHelper);
 }
+
+/**
+ * Renders the easing hint.
+ * This method renders an emphasized path over the easing path for a keyframe.
+ * It appears when hovering over the easing.
+ * It also renders a tooltip that appears when hovering.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Array} path segments - [{x: {Number} time, y: {Number} progress}, ...]
+ * @param {ProgressGraphHelper} graphHelper - The object of ProgressGraphHelper.
+ */
+function renderEasingHint(parentEl, segments, helper) {
+  const keyframes = helper.getKeyframes();
+  const duration = helper.getDuration();
+
+  // Split segments for each keyframe.
+  for (let i = 0, indexOfSegments = 0; i < keyframes.length - 1; i++) {
+    const startKeyframe = keyframes[i];
+    const startTime = startKeyframe.offset * duration;
+    const endKeyframe = keyframes[i + 1];
+    const endTime = endKeyframe.offset * duration;
+
+    const keyframeSegments = [];
+    for (; indexOfSegments < segments.length; indexOfSegments++) {
+      const segment = segments[indexOfSegments];
+      if (segment.x < startTime) {
+        // If previous easings were linear, we need to increment the indexOfSegments.
+        continue;
+      }
+      if (segment.x > endTime) {
+        indexOfSegments -= 1;
+        break;
+      }
+      keyframeSegments.push(segment);
+    }
+
+    // If keyframeSegments does not have segment which is at startTime,
+    // get and set the segment.
+    if (keyframeSegments[0].x !== startTime) {
+      keyframeSegments.unshift(helper.getSegment(startTime));
+    }
+    // Also, endTime.
+    if (keyframeSegments[keyframeSegments.length - 1].x !== endTime) {
+      keyframeSegments.push(helper.getSegment(endTime));
+    }
+
+    // Append easing hint as text and emphasis path.
+    const gEl = createSVGNode({
+      parent: parentEl,
+      nodeType: "g"
+    });
+    createSVGNode({
+      parent: gEl,
+      nodeType: "title",
+      textContent: startKeyframe.easing
+    });
+    helper.appendLinePath(gEl, keyframeSegments, `${helper.getGraphType()} hint`);
+  }
+}
+
+/**
+ * Render easing hint for properties that are represented by color.
+ * This method render as text only.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {ProgressGraphHelper} graphHelper - The object of ProgressGraphHalper.
+ */
+function renderEasingHintForColor(parentEl, helper) {
+  const keyframes = helper.getKeyframes();
+  const duration = helper.getDuration();
+
+  // Split segments for each keyframe.
+  for (let i = 0; i < keyframes.length - 1; i++) {
+    const startKeyframe = keyframes[i];
+    const startTime = startKeyframe.offset * duration;
+    const endKeyframe = keyframes[i + 1];
+    const endTime = endKeyframe.offset * duration;
+
+    // Append easing hint.
+    const gEl = createSVGNode({
+      parent: parentEl,
+      nodeType: "g"
+    });
+    createSVGNode({
+      parent: gEl,
+      nodeType: "title",
+      textContent: startKeyframe.easing
+    });
+    createSVGNode({
+      parent: gEl,
+      nodeType: "rect",
+      attributes: {
+        x: startTime,
+        y: -1,
+        width: endTime - startTime,
+        height: 1,
+        class: "hint",
+      }
+    });
+  }
+}
--- a/devtools/client/animationinspector/graph-helper.js
+++ b/devtools/client/animationinspector/graph-helper.js
@@ -81,16 +81,32 @@ ProgressGraphHelper.prototype = {
     this.propertyCSSName = null;
     this.propertyJSName = null;
     this.animationType = null;
     this.keyframes = null;
     this.win = null;
   },
 
   /**
+   * Return animation duration.
+   * @return {Number} duration
+   */
+  getDuration: function () {
+    return this.animation.effect.timing.duration;
+  },
+
+  /**
+   * Return animation's keyframe.
+   * @return {Object} keyframe
+   */
+  getKeyframes: function () {
+    return this.keyframes;
+  },
+
+  /**
    * Return graph type.
    * @return {String} if property is 'opacity' or 'transform', return that value.
    *                  Otherwise, return given animation type in constructor.
    */
   getGraphType: function () {
     return (this.propertyJSName === "opacity" || this.propertyJSName === "transform")
            ? this.propertyJSName : this.animationType;
   },
@@ -243,40 +259,53 @@ ProgressGraphHelper.prototype = {
                                 minSegmentDuration, minProgressThreshold) {
     return !this.valueHelperFunction
            ? createKeyframesPathSegments(endTime - startTime, this.devtoolsKeyframes)
            : createPathSegments(startTime, endTime,
                                 minSegmentDuration, minProgressThreshold, this);
   },
 
   /**
-   * Append path element.
+   * Append path element as shape. Also, this method appends two segment
+   * that are {start x, 0} and {end x, 0} to make shape.
    * @param {Element} parentEl - Parent element of this appended path element.
    * @param {Array} pathSegments - Path segments. Please see createPathSegments.
    * @param {String} cls - Class name.
    * @return {Element} path element.
    */
-  appendPathElement: function (parentEl, pathSegments, cls) {
-    return appendPathElement(parentEl, pathSegments, cls);
+  appendShapePath: function (parentEl, pathSegments, cls) {
+    return appendShapePath(parentEl, pathSegments, cls);
+  },
+
+  /**
+   * Append path element as line.
+   * @param {Element} parentEl - Parent element of this appended path element.
+   * @param {Array} pathSegments - Path segments. Please see createPathSegments.
+   * @param {String} cls - Class name.
+   * @return {Element} path element.
+   */
+  appendLinePath: function (parentEl, pathSegments, cls) {
+    const isClosePathNeeded = false;
+    return appendPathElement(parentEl, pathSegments, cls, isClosePathNeeded);
   },
 };
 
 exports.ProgressGraphHelper = ProgressGraphHelper;
 
 /**
  * This class is used for creating the summary graph in animation-timeline.
  * The shape of the graph can be changed by using the following methods:
  * setKeyframes:
  *   If null, the shape is by computed timing progress.
  *   Otherwise, by computed style of 'opacity' to combine effect easing and
  *   keyframe's easing.
  * setFillMode:
  *   Animation fill-mode (e.g. "none", "backwards", "forwards" or "both")
  * setClosePathNeeded:
- *   If true, appendPathElement make the last segment of <path> element to
+ *   If true, appendShapePath make the last segment of <path> element to
  *   "close" segment("Z").
  *   Therefore, if don't need under-line of graph, please set false.
  * setOriginalBehavior:
  *   In Animation::SetCurrentTime spec, even if current time of animation is over
  *   the endTime, the progress is changed. Likewise, in case the time is less than 0.
  *   If set true, prevent the time to make the same animation behavior as the original.
  * setMinProgressThreshold:
  *   SummaryGraphHelper searches and creates the summary graph until the progress
@@ -355,17 +384,17 @@ SummaryGraphHelper.prototype = {
    * Set animation fill mode.
    * @param {String} fill - "both", "forwards", "backwards" or "both"
    */
   setFillMode: function (fill) {
     this.animation.effect.timing.fill = fill;
   },
 
   /**
-   * Set true if need to close path in appendPathElement.
+   * Set true if need to close path in appendShapePath.
    * @param {bool} isClosePathNeeded - true: close, false: open.
    */
   setClosePathNeeded: function (isClosePathNeeded) {
     this.isClosePathNeeded = isClosePathNeeded;
   },
 
   /**
    * SummaryGraphHelper searches and creates the summary graph untill the progress
@@ -406,24 +435,25 @@ SummaryGraphHelper.prototype = {
    *                 [{x: {Number} time, y: {Number} progress}, ...]
    */
   createPathSegments: function (startTime, endTime) {
     return createPathSegments(startTime, endTime,
                               this.minSegmentDuration, this.minProgressThreshold, this);
   },
 
   /**
-   * Append path element.
+   * Append path element as shape. Also, this method appends two segment
+   * that are {start x, 0} and {end x, 0} to make shape.
    * @param {Element} parentEl - Parent element of this appended path element.
    * @param {Array} pathSegments - Path segments. Please see createPathSegments.
    * @param {String} cls - Class name.
    * @return {Element} path element.
    */
-  appendPathElement: function (parentEl, pathSegments, cls) {
-    return appendPathElement(parentEl, pathSegments, cls, this.isClosePathNeeded);
+  appendShapePath: function (parentEl, pathSegments, cls) {
+    return appendShapePath(parentEl, pathSegments, cls, this.isClosePathNeeded);
   },
 
   /**
    * Return current computed timing progress of the animation.
    * @return {float} computed timing progress as float value of Y axis.
    */
   getProgressValue: function () {
     return Math.max(this.animation.effect.getComputedTiming().progress, 0);
@@ -493,50 +523,60 @@ function createPathSegments(startTime, e
     pathSegments.push(currentSegment);
     previousSegment = currentSegment;
   }
 
   return pathSegments;
 }
 
 /**
- * Append path element.
+ * Append path element as shape. Also, this method appends two segment
+ * that are {start x, 0} and {end x, 0} to make shape.
+ * But does not affect given pathSegments.
  * @param {Element} parentEl - Parent element of this appended path element.
  * @param {Array} pathSegments - Path segments. Please see createPathSegments.
  * @param {String} cls - Class name.
  * @param {bool} isClosePathNeeded - Set true if need to close the path. (default true)
  * @return {Element} path element.
  */
-function appendPathElement(parentEl, pathSegments, cls, isClosePathNeeded = true) {
+function appendShapePath(parentEl, pathSegments, cls, isClosePathNeeded = true) {
+  const segments = [
+    { x: pathSegments[0].x, y: 0 },
+    ...pathSegments,
+    { x: pathSegments[pathSegments.length - 1].x, y: 0 }
+  ];
+  return appendPathElement(parentEl, segments, cls, isClosePathNeeded);
+}
+
+/**
+ * Append path element.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Array} pathSegments - Path segments. Please see createPathSegments.
+ * @param {String} cls - Class name.
+ * @param {bool} isClosePathNeeded - Set true if need to close the path.
+ * @return {Element} path element.
+ */
+function appendPathElement(parentEl, pathSegments, cls, isClosePathNeeded) {
   // Create path string.
-  let path = `M${ pathSegments[0].x },0`;
-  for (let i = 0; i < pathSegments.length; i++) {
-    const pathSegment = pathSegments[i];
-    if (!pathSegment.easing || pathSegment.easing === "linear") {
-      path += createLinePathString(pathSegment);
-      continue;
-    }
-
-    if (i + 1 === pathSegments.length) {
-      // We already create steps or cubic-bezier path string in previous.
-      break;
+  let currentSegment = pathSegments[0];
+  let path = `M${ currentSegment.x },${ currentSegment.y }`;
+  for (let i = 1; i < pathSegments.length; i++) {
+    const currentEasing = currentSegment.easing ? currentSegment.easing : "linear";
+    const nextSegment = pathSegments[i];
+    if (currentEasing === "linear") {
+      path += createLinePathString(nextSegment);
+    } else if (currentEasing.startsWith("steps")) {
+      path += createStepsPathString(currentSegment, nextSegment);
+    } else if (currentEasing.startsWith("frames")) {
+      path += createFramesPathString(currentSegment, nextSegment);
+    } else {
+      path += createCubicBezierPathString(currentSegment, nextSegment);
     }
-
-    const nextPathSegment = pathSegments[i + 1];
-    let createPathFunction;
-    if (pathSegment.easing.startsWith("steps")) {
-      createPathFunction = createStepsPathString;
-    } else if (pathSegment.easing.startsWith("frames")) {
-      createPathFunction = createFramesPathString;
-    } else {
-      createPathFunction = createCubicBezierPathString;
-    }
-    path += createPathFunction(pathSegment, nextPathSegment);
+    currentSegment = nextSegment;
   }
-  path += ` L${ pathSegments[pathSegments.length - 1].x },0`;
   if (isClosePathNeeded) {
     path += " Z";
   }
   // Append and return the path element.
   return createSVGNode({
     parent: parentEl,
     nodeType: "path",
     attributes: {
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -692,16 +692,34 @@ body {
   stroke: var(--transform-border-color);
 }
 
 .keyframes svg path.color {
   stroke: none;
   height: 100%;
 }
 
+.keyframes svg .hint {
+  stroke-opacity: 0;
+  stroke-linecap: round;
+  stroke-width: 5;
+}
+
+.keyframes svg path.hint {
+  fill: none;
+}
+
+.keyframes svg path.hint:hover {
+  stroke-opacity: 1;
+}
+
+.keyframes svg rect.hint {
+  fill-opacity: .1;
+}
+
 .animation-detail {
   position: relative;
   width: 100%;
   background-color: var(--theme-body-background);
   z-index: 5;
 }
 
 .animation-detail .animation-detail-header {