Bug 1406285 - Part 5: Implement computed timing graph of summary graph. r?gl draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Thu, 18 Jan 2018 12:42:31 +0900
changeset 721934 f859326021ebef09e67cc1db7da5e0aaa8a3ac0e
parent 721933 82078177a1cb16d6cf0609fbe68b59067fdc675d
child 721935 0706ac027461f154083fb4f2f079ab209855ec5c
push id96003
push userbmo:dakatsuka@mozilla.com
push dateThu, 18 Jan 2018 05:23:36 +0000
reviewersgl
bugs1406285
milestone59.0a1
Bug 1406285 - Part 5: Implement computed timing graph of summary graph. r?gl MozReview-Commit-ID: C91ORqTRSfj
devtools/client/inspector/animation/animation.js
devtools/client/inspector/animation/components/AnimationItem.js
devtools/client/inspector/animation/components/AnimationList.js
devtools/client/inspector/animation/components/AnimationListContainer.js
devtools/client/inspector/animation/components/App.js
devtools/client/inspector/animation/components/graph/ComputedTimingPath.js
devtools/client/inspector/animation/components/graph/SummaryGraph.js
devtools/client/inspector/animation/components/graph/SummaryGraphPath.js
devtools/client/inspector/animation/components/graph/TimingPath.js
devtools/client/inspector/animation/components/graph/moz.build
devtools/client/inspector/animation/utils/graph-helper.js
devtools/client/inspector/animation/utils/moz.build
devtools/client/inspector/inspector.js
devtools/client/themes/animation.css
--- a/devtools/client/inspector/animation/animation.js
+++ b/devtools/client/inspector/animation/animation.js
@@ -13,20 +13,22 @@ const EventEmitter = require("devtools/s
 const App = createFactory(require("./components/App"));
 
 const { updateAnimations } = require("./actions/animations");
 const { updateElementPickerEnabled } = require("./actions/element-picker");
 const { updateSidebarSize } = require("./actions/sidebar");
 const { isAllAnimationEqual } = require("./utils/utils");
 
 class AnimationInspector {
-  constructor(inspector) {
+  constructor(inspector, win) {
     this.inspector = inspector;
+    this.win = win;
 
     this.getNodeFromActor = this.getNodeFromActor.bind(this);
+    this.simulateAnimation = this.simulateAnimation.bind(this);
     this.toggleElementPicker = this.toggleElementPicker.bind(this);
     this.update = this.update.bind(this);
     this.onElementPickerStarted = this.onElementPickerStarted.bind(this);
     this.onElementPickerStopped = this.onElementPickerStopped.bind(this);
     this.onSidebarResized = this.onSidebarResized.bind(this);
     this.onSidebarSelect = this.onSidebarSelect.bind(this);
 
     EventEmitter.decorate(this);
@@ -43,16 +45,17 @@ class AnimationInspector {
 
     const {
       onHideBoxModelHighlighter,
     } = this.inspector.getPanel("boxmodel").getComponentProps();
 
     const {
       emit: emitEventForTest,
       getNodeFromActor,
+      simulateAnimation,
       toggleElementPicker,
     } = this;
 
     const target = this.inspector.target;
     this.animationsFront = new AnimationsFront(target.client, target.form);
 
     const provider = createElement(Provider,
       {
@@ -62,16 +65,17 @@ class AnimationInspector {
       },
       App(
         {
           emitEventForTest,
           getNodeFromActor,
           onHideBoxModelHighlighter,
           onShowBoxModelHighlighterForNode,
           setSelectedNode,
+          simulateAnimation,
           toggleElementPicker,
         }
       )
     );
     this.provider = provider;
 
     this.inspector.selection.on("new-node-front", this.update);
     this.inspector.sidebar.on("newanimationinspector-selected", this.onSidebarSelect);
@@ -82,17 +86,28 @@ class AnimationInspector {
 
   destroy() {
     this.inspector.selection.off("new-node-front", this.update);
     this.inspector.sidebar.off("newanimationinspector-selected", this.onSidebarSelect);
     this.inspector.toolbox.off("inspector-sidebar-resized", this.onSidebarResized);
     this.inspector.toolbox.off("picker-started", this.onElementPickerStarted);
     this.inspector.toolbox.off("picker-stopped", this.onElementPickerStopped);
 
+    if (this.simulatedAnimation) {
+      this.simulatedAnimation.cancel();
+      this.simulatedAnimation = null;
+    }
+
+    if (this.simulatedElement) {
+      this.simulatedElement.remove();
+      this.simulatedElement = null;
+    }
+
     this.inspector = null;
+    this.win = null;
   }
 
   /**
    * Return a map of animated property from given animation actor.
    *
    * @param {Object} animation
    * @return {Map} A map of animated property
    *         key: {String} Animated property name
@@ -134,16 +149,58 @@ class AnimationInspector {
   }
 
   isPanelVisible() {
     return this.inspector && this.inspector.toolbox && this.inspector.sidebar &&
            this.inspector.toolbox.currentToolId === "inspector" &&
            this.inspector.sidebar.getCurrentTabID() === "newanimationinspector";
   }
 
+  /**
+   * Returns simulatable animation by given parameters.
+   * The returned animation is implementing Animation interface of Web Animation API.
+   * https://drafts.csswg.org/web-animations/#the-animation-interface
+   *
+   * @param {Array} keyframes
+   *        e.g. [{ opacity: 0 }, { opacity: 1 }]
+   * @param {Object} effectTiming
+   *        e.g. { duration: 1000, fill: "both" }
+   * @param {Boolean} isElementNeeded
+   *        true:  create animation with an element.
+   *               If want to know computed value of the element, turn on.
+   *        false: create animation without an element,
+   *               If need to know only timing progress.
+   * @return {Animation}
+   *         https://drafts.csswg.org/web-animations/#the-animation-interface
+   */
+  simulateAnimation(keyframes, effectTiming, isElementNeeded) {
+    let targetEl = null;
+
+    if (isElementNeeded) {
+      if (!this.simulatedElement) {
+        this.simulatedElement = this.win.document.createElement("div");
+        this.win.document.documentElement.appendChild(this.simulatedElement);
+      } else {
+        // Reset styles.
+        this.simulatedElement.style.cssText = "";
+      }
+
+      targetEl = this.simulatedElement;
+    }
+
+    if (!this.simulatedAnimation) {
+      this.simulatedAnimation = new this.win.Animation();
+    }
+
+    this.simulatedAnimation.effect =
+      new this.win.KeyframeEffect(targetEl, keyframes, effectTiming);
+
+    return this.simulatedAnimation;
+  }
+
   toggleElementPicker() {
     this.inspector.toolbox.highlighterUtils.togglePicker();
   }
 
   async update() {
     if (!this.inspector || !this.isPanelVisible()) {
       // AnimationInspector was destroyed already or the panel is hidden.
       return;
--- a/devtools/client/inspector/animation/components/AnimationItem.js
+++ b/devtools/client/inspector/animation/components/AnimationItem.js
@@ -15,28 +15,30 @@ class AnimationItem extends PureComponen
   static get propTypes() {
     return {
       animation: PropTypes.object.isRequired,
       emitEventForTest: PropTypes.func.isRequired,
       getNodeFromActor: PropTypes.func.isRequired,
       onHideBoxModelHighlighter: PropTypes.func.isRequired,
       onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
       setSelectedNode: PropTypes.func.isRequired,
+      simulateAnimation: PropTypes.func.isRequired,
       timeScale: PropTypes.object.isRequired,
     };
   }
 
   render() {
     const {
       animation,
       emitEventForTest,
       getNodeFromActor,
       onHideBoxModelHighlighter,
       onShowBoxModelHighlighterForNode,
       setSelectedNode,
+      simulateAnimation,
       timeScale,
     } = this.props;
 
     return dom.li(
       {
         className: "animation-item"
       },
       AnimationTarget(
@@ -47,16 +49,17 @@ class AnimationItem extends PureComponen
           onHideBoxModelHighlighter,
           onShowBoxModelHighlighterForNode,
           setSelectedNode,
         }
       ),
       SummaryGraph(
         {
           animation,
+          simulateAnimation,
           timeScale,
         }
       )
     );
   }
 }
 
 module.exports = AnimationItem;
--- a/devtools/client/inspector/animation/components/AnimationList.js
+++ b/devtools/client/inspector/animation/components/AnimationList.js
@@ -14,44 +14,47 @@ class AnimationList extends PureComponen
   static get propTypes() {
     return {
       animations: PropTypes.arrayOf(PropTypes.object).isRequired,
       emitEventForTest: PropTypes.func.isRequired,
       getNodeFromActor: PropTypes.func.isRequired,
       onHideBoxModelHighlighter: PropTypes.func.isRequired,
       onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
       setSelectedNode: PropTypes.func.isRequired,
+      simulateAnimation: PropTypes.func.isRequired,
       timeScale: PropTypes.object.isRequired,
     };
   }
 
   render() {
     const {
       animations,
       emitEventForTest,
       getNodeFromActor,
       onHideBoxModelHighlighter,
       onShowBoxModelHighlighterForNode,
       setSelectedNode,
+      simulateAnimation,
       timeScale,
     } = this.props;
 
     return dom.ul(
       {
         className: "animation-list"
       },
       animations.map(animation =>
         AnimationItem(
           {
             animation,
             emitEventForTest,
             getNodeFromActor,
             onHideBoxModelHighlighter,
             onShowBoxModelHighlighterForNode,
             setSelectedNode,
+            simulateAnimation,
             timeScale,
           }
         )
       )
     );
   }
 }
 
--- a/devtools/client/inspector/animation/components/AnimationListContainer.js
+++ b/devtools/client/inspector/animation/components/AnimationListContainer.js
@@ -18,27 +18,29 @@ class AnimationListContainer extends Pur
   static get propTypes() {
     return {
       animations: PropTypes.arrayOf(PropTypes.object).isRequired,
       emitEventForTest: PropTypes.func.isRequired,
       getNodeFromActor: PropTypes.func.isRequired,
       onHideBoxModelHighlighter: PropTypes.func.isRequired,
       onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
       setSelectedNode: PropTypes.func.isRequired,
+      simulateAnimation: PropTypes.func.isRequired,
     };
   }
 
   render() {
     const {
       animations,
       emitEventForTest,
       getNodeFromActor,
       onHideBoxModelHighlighter,
       onShowBoxModelHighlighterForNode,
       setSelectedNode,
+      simulateAnimation,
     } = this.props;
     const timeScale = new TimeScale(animations);
 
     return dom.div(
       {
         className: "animation-list-container"
       },
       AnimationListHeader(
@@ -49,16 +51,17 @@ class AnimationListContainer extends Pur
       AnimationList(
         {
           animations,
           emitEventForTest,
           getNodeFromActor,
           onHideBoxModelHighlighter,
           onShowBoxModelHighlighterForNode,
           setSelectedNode,
+          simulateAnimation,
           timeScale,
         }
       )
     );
   }
 }
 
 module.exports = AnimationListContainer;
--- a/devtools/client/inspector/animation/components/App.js
+++ b/devtools/client/inspector/animation/components/App.js
@@ -16,48 +16,51 @@ class App extends PureComponent {
   static get propTypes() {
     return {
       animations: PropTypes.arrayOf(PropTypes.object).isRequired,
       emitEventForTest: PropTypes.func.isRequired,
       getNodeFromActor: PropTypes.func.isRequired,
       onHideBoxModelHighlighter: PropTypes.func.isRequired,
       onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
       setSelectedNode: PropTypes.func.isRequired,
+      simulateAnimation: PropTypes.func.isRequired,
       toggleElementPicker: PropTypes.func.isRequired,
     };
   }
 
   shouldComponentUpdate(nextProps, nextState) {
     return this.props.animations.length !== 0 || nextProps.animations.length !== 0;
   }
 
   render() {
     const {
       animations,
       emitEventForTest,
       getNodeFromActor,
       onHideBoxModelHighlighter,
       onShowBoxModelHighlighterForNode,
       setSelectedNode,
+      simulateAnimation,
       toggleElementPicker,
     } = this.props;
 
     return dom.div(
       {
         id: "animation-container"
       },
       animations.length ?
       AnimationListContainer(
         {
           animations,
           emitEventForTest,
           getNodeFromActor,
           onHideBoxModelHighlighter,
           onShowBoxModelHighlighterForNode,
           setSelectedNode,
+          simulateAnimation,
         }
       )
       :
       NoAnimationPanel(
         {
           toggleElementPicker
         }
       )
--- a/devtools/client/inspector/animation/components/graph/ComputedTimingPath.js
+++ b/devtools/client/inspector/animation/components/graph/ComputedTimingPath.js
@@ -1,26 +1,94 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-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");
 
-class ComputedTimingPath extends PureComponent {
+const { SummaryGraphHelper, toPathString } = 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,
-      totalDisplayedDuration: PropTypes.number.isRequired,
+      opacity: PropTypes.number.isRequired,
+      simulateAnimation: PropTypes.func.isRequired,
+      totalDuration: PropTypes.number.isRequired,
     };
   }
 
   render() {
-    return dom.g({});
+    const {
+      animation,
+      durationPerPixel,
+      keyframes,
+      opacity,
+      simulateAnimation,
+      totalDuration,
+    } = this.props;
+
+    const { state } = animation;
+    const effectTiming = Object.assign({}, state, {
+      iterations: state.iterationCount ? state.iterationCount : Infinity
+    });
+
+    // Create new keyframes for opacity as computed style.
+    // The reason why we use computed value instead of computed timing progress is to
+    // include the easing in keyframes as well. Although the computed timing progress
+    // is not affected by the easing in keyframes at all, computed value reflects that.
+    const frames = keyframes.map(keyframe => {
+      return {
+        opacity: keyframe.offset,
+        offset: keyframe.offset,
+        easing: keyframe.easing
+      };
+    });
+    const simulatedAnimation = simulateAnimation(frames, effectTiming, true);
+    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 => {
+      if (time < 0) {
+        return { x: time, y: 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 helper = new SummaryGraphHelper(state, keyframes,
+                                          totalDuration, durationPerPixel,
+                                          getValueFunc, toPathStringFunc);
+    const offset = state.previousStartTime ? state.previousStartTime : 0;
+
+    return dom.g(
+      {
+        className: "animation-computed-timing-path",
+        style: { opacity },
+        transform: `translate(${ offset })`
+      },
+      super.renderGraph(state, helper)
+    );
   }
 }
 
 module.exports = ComputedTimingPath;
--- a/devtools/client/inspector/animation/components/graph/SummaryGraph.js
+++ b/devtools/client/inspector/animation/components/graph/SummaryGraph.js
@@ -9,33 +9,36 @@ const dom = require("devtools/client/sha
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const SummaryGraphPath = createFactory(require("./SummaryGraphPath"));
 
 class SummaryGraph extends PureComponent {
   static get propTypes() {
     return {
       animation: PropTypes.object.isRequired,
+      simulateAnimation: PropTypes.func.isRequired,
       timeScale: PropTypes.object.isRequired,
     };
   }
 
   render() {
     const {
       animation,
+      simulateAnimation,
       timeScale,
     } = this.props;
 
     return dom.div(
       {
         className: "animation-summary-graph",
       },
       SummaryGraphPath(
         {
           animation,
+          simulateAnimation,
           timeScale,
         }
       )
     );
   }
 }
 
 module.exports = SummaryGraph;
--- a/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js
+++ b/devtools/client/inspector/animation/components/graph/SummaryGraphPath.js
@@ -5,21 +5,24 @@
 "use strict";
 
 const { createFactory, 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 ReactDOM = require("devtools/client/shared/vendor/react-dom");
 
 const ComputedTimingPath = createFactory(require("./ComputedTimingPath"));
+// Minimum opacity for semitransparent fill color for keyframes's easing graph.
+const MIN_KEYFRAMES_EASING_OPACITY = 0.5;
 
 class SummaryGraphPath extends PureComponent {
   static get propTypes() {
     return {
       animation: PropTypes.object.isRequired,
+      simulateAnimation: PropTypes.func.isRequired,
       timeScale: PropTypes.object.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
@@ -141,36 +144,40 @@ class SummaryGraphPath extends PureCompo
     const { durationPerPixel } = this.state;
 
     if (!durationPerPixel) {
       return dom.svg();
     }
 
     const {
       animation,
+      simulateAnimation,
       timeScale,
     } = this.props;
 
     const totalDuration = this.getTotalDuration(animation, timeScale);
     const startTime = timeScale.minStartTime;
     const keyframesList =
       this.getOffsetAndEasingOnlyKeyframes(animation.animatedPropertyMap);
+    const opacity = Math.max(1 / keyframesList.length, MIN_KEYFRAMES_EASING_OPACITY);
 
     return dom.svg(
       {
         className: "animation-summary-graph-path",
         preserveAspectRatio: "none",
         viewBox: `${ startTime } -1 ${ totalDuration } 1`
       },
       keyframesList.map(keyframes =>
         ComputedTimingPath(
           {
             animation,
             durationPerPixel,
             keyframes,
+            opacity,
+            simulateAnimation,
             totalDuration,
           }
         )
       )
     );
   }
 }
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/graph/TimingPath.js
@@ -0,0 +1,349 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+// Show max 10 iterations for infinite animations
+// to give users a clue that the animation does repeat.
+const MAX_INFINITE_ANIMATIONS_ITERATIONS = 10;
+
+class TimingPath extends PureComponent {
+  /**
+   * Render a graph of given parameters and return as <path> element list.
+   *
+   * @param {Object} state
+   *        State of animation.
+   * @param {SummaryGraphHelper} helper
+   *        Instance of SummaryGraphHelper.
+   * @return {Array}
+   *         list of <path> element.
+   */
+  renderGraph(state, helper) {
+    // Starting time of main iteration.
+    let mainIterationStartTime = 0;
+    let iterationStart = state.iterationStart;
+    let iterationCount = state.iterationCount ? state.iterationCount : Infinity;
+
+    const pathList = [];
+
+    // Append delay.
+    if (state.delay > 0) {
+      this.renderDelay(pathList, state, helper);
+      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;
+      }
+    }
+
+    // Append 1st section of iterations,
+    // This section is only useful in cases where iterationStart has decimals.
+    // 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) {
+      this.renderFirstIteration(pathList, state,
+                                mainIterationStartTime, firstSectionCount, helper);
+    }
+
+    if (iterationCount === Infinity) {
+      // If the animation repeats infinitely,
+      // we fill the remaining area with iteration paths.
+      this.renderInfinity(pathList, state,
+                          mainIterationStartTime, firstSectionCount, helper);
+    } else {
+      // Otherwise, we show remaining iterations, endDelay and fill.
+
+      // Append forwards fill-mode.
+      if (state.fill === "both" || state.fill === "forwards") {
+        this.renderForwardsFill(pathList, state,
+                                mainIterationStartTime, iterationCount, helper);
+      }
+
+      // Append middle section of iterations.
+      // e.g.
+      // if { iterationStart: 0.25, iterations: 3 }, middleSectionCount is 2.
+      const middleSectionCount = Math.floor(iterationCount - firstSectionCount);
+      this.renderMiddleIterations(pathList, state, mainIterationStartTime,
+                                  firstSectionCount, middleSectionCount, helper);
+
+      // 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) {
+        this.renderLastIteration(pathList, state, mainIterationStartTime,
+                                 firstSectionCount, middleSectionCount,
+                                 lastSectionCount, helper);
+      }
+
+      // Append endDelay.
+      if (state.endDelay > 0) {
+        this.renderEndDelay(pathList, state,
+                            mainIterationStartTime, iterationCount, helper);
+      }
+    }
+    return pathList;
+  }
+
+  /**
+   * Render 'delay' part in animation and add a <path> element to given pathList.
+   *
+   * @param {Array} pathList
+   *        Add rendered <path> element to this array.
+   * @param {Object} state
+   *        State of animation.
+   * @param {SummaryGraphHelper} helper
+   *        Instance of SummaryGraphHelper.
+   */
+  renderDelay(pathList, state, helper) {
+    const startSegment = helper.getSegment(0);
+    const endSegment = { x: state.delay, y: startSegment.y };
+    const segments = [startSegment, endSegment];
+    pathList.push(
+      dom.path(
+        {
+          className: "animation-delay-path",
+          d: helper.toPathString(segments),
+        }
+      )
+    );
+  }
+
+  /**
+   * Render 1st section of iterations and add a <path> element to given pathList.
+   * This section is only useful in cases where iterationStart has decimals.
+   *
+   * @param {Array} pathList
+   *        Add rendered <path> element to this array.
+   * @param {Object} state
+   *        State of animation.
+   * @param {Number} mainIterationStartTime
+   *        Start time of main iteration.
+   * @param {Number} firstSectionCount
+   *        Iteration count of first section.
+   * @param {SummaryGraphHelper} helper
+   *        Instance of SummaryGraphHelper.
+   */
+  renderFirstIteration(pathList, state, mainIterationStartTime,
+                       firstSectionCount, helper) {
+    const startTime = mainIterationStartTime;
+    const endTime = startTime + firstSectionCount * state.duration;
+    const segments = helper.createPathSegments(startTime, endTime);
+    pathList.push(
+      dom.path(
+        {
+          className: "animation-iteration-path",
+          d: helper.toPathString(segments),
+        }
+      )
+    );
+  }
+
+  /**
+   * Render middle iterations and add <path> elements to given pathList.
+   *
+   * @param {Array} pathList
+   *        Add rendered <path> elements to this array.
+   * @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 {SummaryGraphHelper} helper
+   *        Instance of SummaryGraphHelper.
+   */
+  renderMiddleIterations(pathList, state, mainIterationStartTime,
+                         firstSectionCount, middleSectionCount, helper) {
+    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 = helper.createPathSegments(startTime, endTime);
+      pathList.push(
+        dom.path(
+          {
+            className: "animation-iteration-path",
+            d: helper.toPathString(segments),
+          }
+        )
+      );
+    }
+  }
+
+  /**
+   * Render last section of iterations and add a <path> element to given pathList.
+   * This section is only useful in cases where iterationStart has decimals.
+   *
+   * @param {Array} pathList
+   *        Add rendered <path> elements to this array.
+   * @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 {SummaryGraphHelper} helper
+   *        Instance of SummaryGraphHelper.
+   */
+  renderLastIteration(pathList, state, mainIterationStartTime, firstSectionCount,
+                      middleSectionCount, lastSectionCount, helper) {
+    const startTime = mainIterationStartTime
+                    + (firstSectionCount + middleSectionCount) * state.duration;
+    const endTime = startTime + lastSectionCount * state.duration;
+    const segments = helper.createPathSegments(startTime, endTime);
+    pathList.push(
+      dom.path(
+        {
+          className: "animation-iteration-path",
+          d: helper.toPathString(segments),
+        }
+      )
+    );
+  }
+
+  /**
+   * Render infinity iterations and add <path> elements to given pathList.
+   *
+   * @param {Array} pathList
+   *        Add rendered <path> elements to this array.
+   * @param {Object} state
+   *        State of animation.
+   * @param {Number} mainIterationStartTime
+   *        Starting time of main iteration.
+   * @param {Number} firstSectionCount
+   *        Iteration count of first section.
+   * @param {SummaryGraphHelper} helper
+   *        Instance of SummaryGraphHelper.
+   */
+  renderInfinity(pathList, state, mainIterationStartTime, firstSectionCount, helper) {
+    // Calculate the number of iterations to display,
+    // with a maximum of MAX_INFINITE_ANIMATIONS_ITERATIONS
+    let uncappedInfinityIterationCount =
+      (helper.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));
+    const infinityIterationCount = Math.min(MAX_INFINITE_ANIMATIONS_ITERATIONS,
+                                            Math.ceil(uncappedInfinityIterationCount));
+
+    // Append first full iteration path.
+    const firstStartTime =
+      mainIterationStartTime + firstSectionCount * state.duration;
+    const firstEndTime = firstStartTime + state.duration;
+    const firstSegments = helper.createPathSegments(firstStartTime, firstEndTime);
+    pathList.push(
+      dom.path(
+        {
+          className: "animation-iteration-path",
+          d: helper.toPathString(firstSegments),
+        }
+      )
+    );
+
+    // 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.
+        segments = firstSegments.map(segment => {
+          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 };
+        });
+      }
+      pathList.push(
+        dom.path(
+          {
+            className: "animation-iteration-path infinity",
+            d: helper.toPathString(segments),
+          }
+        )
+      );
+    }
+  }
+
+  /**
+   * Render 'endDelay' part in animation and add a <path> element to given pathList.
+   *
+   * @param {Array} pathList
+   *        Add rendered <path> element to this array.
+   * @param {Object} state
+   *        State of animation.
+   * @param {Number} mainIterationStartTime
+   *        Starting time of main iteration.
+   * @param {Number} iterationCount
+   *        Iteration count of whole animation.
+   * @param {SummaryGraphHelper} helper
+   *        Instance of SummaryGraphHelper.
+   */
+  renderEndDelay(pathList, state, mainIterationStartTime, iterationCount, helper) {
+    const startTime = mainIterationStartTime + iterationCount * state.duration;
+    const startSegment = helper.getSegment(startTime);
+    const endSegment = { x: startTime + state.endDelay, y: startSegment.y };
+    pathList.push(
+      dom.path(
+        {
+          className: "animation-enddelay-path",
+          d: helper.toPathString([startSegment, endSegment]),
+        }
+      )
+    );
+  }
+
+  /**
+   * Render 'fill' for forwards part in animation and
+   * add a <path> element to given pathList.
+   *
+   * @param {Array} pathList
+   *        Add rendered <path> element to this array.
+   * @param {Object} state
+   *        State of animation.
+   * @param {Number} mainIterationStartTime
+   *        Starting time of main iteration.
+   * @param {Number} iterationCount
+   *        Iteration count of whole animation.
+   * @param {SummaryGraphHelper} helper
+   *        Instance of SummaryGraphHelper.
+   */
+  renderForwardsFill(pathList, state, mainIterationStartTime, iterationCount, helper) {
+    const startTime = mainIterationStartTime + iterationCount * state.duration
+                    + (state.endDelay > 0 ? state.endDelay : 0);
+    const startSegment = helper.getSegment(startTime);
+    const endSegment = { x: helper.totalDuration, y: startSegment.y };
+    pathList.push(
+      dom.path(
+        {
+          className: "animation-fill-forwards-path",
+          d: helper.toPathString([startSegment, endSegment]),
+        }
+      )
+    );
+  }
+}
+
+module.exports = TimingPath;
--- a/devtools/client/inspector/animation/components/graph/moz.build
+++ b/devtools/client/inspector/animation/components/graph/moz.build
@@ -1,9 +1,10 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'ComputedTimingPath.js',
     'SummaryGraph.js',
-    'SummaryGraphPath.js'
+    'SummaryGraphPath.js',
+    'TimingPath.js'
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/utils/graph-helper.js
@@ -0,0 +1,239 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// BOUND_EXCLUDING_TIME should be less than 1ms and is used to exclude start
+// and end bounds when dividing  duration in createPathSegments.
+const BOUND_EXCLUDING_TIME = 0.001;
+// DEFAULT_MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
+const DEFAULT_MIN_PROGRESS_THRESHOLD = 0.1;
+// In the createPathSegments function, an animation duration is divided by
+// DURATION_RESOLUTION in order to draw the way the animation progresses.
+// But depending on the timing-function, we may be not able to make the graph
+// smoothly progress if this resolution is not high enough.
+// So, if the difference of animation progress between 2 divisions is more than
+// DEFAULT_MIN_PROGRESS_THRESHOLD, then createPathSegments re-divides
+// by DURATION_RESOLUTION.
+// DURATION_RESOLUTION shoud be integer and more than 2.
+const DURATION_RESOLUTION = 4;
+
+/**
+ * The helper class for creating summary graph.
+ */
+class SummaryGraphHelper {
+  /**
+   * Constructor.
+   *
+   * @param {Object} state
+   *        State of animation.
+   * @param {Array} keyframes
+   *        Array of keyframe.
+   * @param {Number} totalDuration
+   *        Total displayable duration.
+   * @param {Number} minSegmentDuration
+   *        Minimum segment duration.
+   * @param {Function} getValueFunc
+   *        Which returns graph value of given time.
+   *        The function should return a number value between 0 - 1.
+   *        e.g. time => { return 1.0 };
+   * @param {Function} toPathStringFunc
+   *        Which returns a path string for 'd' attribute for <path> from given segments.
+   */
+  constructor(state, keyframes, totalDuration, minSegmentDuration,
+              getValueFunc, toPathStringFunc) {
+    this.totalDuration = totalDuration;
+    this.minSegmentDuration = minSegmentDuration;
+    this.minProgressThreshold = getPreferredProgressThreshold(state, keyframes);
+    this.durationResolution = getPreferredDurationResolution(keyframes);
+    this.getValue = getValueFunc;
+    this.toPathString = toPathStringFunc;
+
+    this.getSegment = this.getSegment.bind(this);
+  }
+
+  /**
+   * Create the path segments from given parameters.
+   *
+   * @param {Number} startTime
+   *        Starting time of animation.
+   * @param {Number} endTime
+   *        Ending time of animation.
+   * @return {Array}
+   *         Array of path segment.
+   *         e.g.[{x: {Number} time, y: {Number} progress}, ...]
+   */
+  createPathSegments(startTime, endTime) {
+    return createPathSegments(startTime, endTime,
+                              this.minSegmentDuration, this.minProgressThreshold,
+                              this.durationResolution, this.getSegment);
+  }
+
+  /**
+   * Return a coordinate as a graph segment at given time.
+   *
+   * @param {Number} time
+   * @return {Object}
+   *         { x: Number, y: Number }
+   */
+  getSegment(time) {
+    const value = this.getValue(time);
+    return { x: time, y: value };
+  }
+}
+
+/**
+ * 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 {Number} resolution
+ *        Duration resolution for first time.
+ * @param {Function} getSegment
+ *        A function that calculate the graph segment.
+ * @return {Array}
+ *         Array of path segment.
+ *         e.g.[{x: {Number} time, y: {Number} progress}, ...]
+ */
+function createPathSegments(startTime, endTime, minSegmentDuration,
+                            minProgressThreshold, resolution, getSegment) {
+  // If the duration is too short, early return.
+  if (endTime - startTime < minSegmentDuration) {
+    return [getSegment(startTime), getSegment(endTime)];
+  }
+
+  // Otherwise, start creating segments.
+  let pathSegments = [];
+
+  // Append the segment for the startTime position.
+  const startTimeSegment = 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) / resolution;
+  for (let index = 1; index <= resolution; index++) {
+    // Create a segment for this interval.
+    const currentSegment = 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 BOUND_EXCLUDING_TIME).
+      const nextStartTime = previousSegment.x + BOUND_EXCLUDING_TIME;
+      const nextEndTime = currentSegment.x - BOUND_EXCLUDING_TIME;
+      const segments =
+        createPathSegments(nextStartTime, nextEndTime, minSegmentDuration,
+                           minProgressThreshold, DURATION_RESOLUTION, getSegment);
+      pathSegments = pathSegments.concat(segments);
+    }
+
+    pathSegments.push(currentSegment);
+    previousSegment = currentSegment;
+  }
+
+  return pathSegments;
+}
+
+/**
+ * Return preferred duration resolution.
+ * This corresponds to narrow interval keyframe offset.
+ *
+ * @param {Array} keyframes
+ *        Array of keyframe.
+ * @return {Number}
+ *         Preferred duration resolution.
+ */
+function getPreferredDurationResolution(keyframes) {
+  if (!keyframes) {
+    return DURATION_RESOLUTION;
+  }
+
+  let durationResolution = DURATION_RESOLUTION;
+  let previousOffset = 0;
+  for (let keyframe of keyframes) {
+    if (previousOffset && previousOffset != keyframe.offset) {
+      const interval = keyframe.offset - previousOffset;
+      durationResolution = Math.max(durationResolution, Math.ceil(1 / interval));
+    }
+    previousOffset = keyframe.offset;
+  }
+
+  return durationResolution;
+}
+
+/**
+ * Return preferred progress threshold to render summary graph.
+ *
+ * @param {Object} state
+ *        State of animation.
+ * @param {Array} keyframes
+ *        Array of keyframe.
+ * @return {float}
+ *         Preferred threshold.
+ */
+function getPreferredProgressThreshold(state, keyframes) {
+  let threshold = DEFAULT_MIN_PROGRESS_THRESHOLD;
+  let stepsOrFrames;
+
+  if ((stepsOrFrames = getStepsOrFramesCount(state.easing))) {
+    threshold = Math.min(threshold, (1 / (stepsOrFrames + 1)));
+  }
+
+  if (!keyframes) {
+    return threshold;
+  }
+
+  for (let i = 0; i < keyframes.length - 1; i++) {
+    const keyframe = keyframes[i];
+
+    if (!keyframe.easing) {
+      continue;
+    }
+
+    if ((stepsOrFrames = getStepsOrFramesCount(keyframe.easing))) {
+      const nextKeyframe = keyframes[i + 1];
+      threshold =
+        Math.min(threshold,
+                 1 / (stepsOrFrames + 1) * (nextKeyframe.offset - keyframe.offset));
+    }
+  }
+
+  return threshold;
+}
+
+function getStepsOrFramesCount(easing) {
+  const stepsOrFramesFunction = easing.match(/(steps|frames)\((\d+)/);
+  return stepsOrFramesFunction ? parseInt(stepsOrFramesFunction[2], 10) : 0;
+}
+
+/**
+ * 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"
+ */
+function toPathString(segments) {
+  let pathString = "";
+  segments.forEach(segment => {
+    pathString += `L${ segment.x },${ segment.y } `;
+  });
+  return pathString;
+}
+
+exports.SummaryGraphHelper = SummaryGraphHelper;
+exports.toPathString = toPathString;
--- a/devtools/client/inspector/animation/utils/moz.build
+++ b/devtools/client/inspector/animation/utils/moz.build
@@ -1,9 +1,10 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
+    'graph-helper.js',
     'l10n.js',
     'timescale.js',
     'utils.js',
 )
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -802,17 +802,17 @@ Inspector.prototype = {
           {
             props: {
               id: animationId,
               title: animationTitle
             },
             panel: () => {
               const AnimationInspector =
                 this.browserRequire("devtools/client/inspector/animation/animation");
-              this.animationinspector = new AnimationInspector(this);
+              this.animationinspector = new AnimationInspector(this, this.panelWin);
               return this.animationinspector.provider;
             }
           },
           defaultTab == animationId);
       } else {
         this.sidebar.addFrameTab(
           "animationinspector",
           animationTitle,
--- a/devtools/client/themes/animation.css
+++ b/devtools/client/themes/animation.css
@@ -67,24 +67,35 @@
 
 .animation-target .tag-name {
   cursor: default;
 }
 
 /* Summary Graph */
 .animation-summary-graph {
   height: 100%;
+  padding-top: 5px;
   width: calc(100% - var(--sidebar-width) - var(--graph-right-offset));
 }
 
 .animation-summary-graph-path {
   height: 100%;
   width: 100%;
 }
 
+.animation-computed-timing-path path {
+  fill: lime;
+  vector-effect: non-scaling-stroke;
+  transform: scale(1, -1);
+}
+
+.animation-computed-timing-path path.infinity:nth-child(n+2) {
+  opacity: 0.3;
+}
+
 /* No Animation Panel */
 .animation-error-message {
   overflow: auto;
 }
 
 .animation-error-message > p {
   white-space: pre;
 }