Bug 1309468 - Part 1: Display effect easing curve and keyframes easing curve in summary graph. r=pbro draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Wed, 24 May 2017 10:17:36 +0900
changeset 583424 9a4e02925fce65c929e01791d47251fa5c6ca20c
parent 582730 5bc1c758ab57c1885dceab4e7837e58af27b998c
child 583425 9ab377fb48b565007a02bba2f49383ce4048d9f9
push id60392
push userbmo:dakatsuka@mozilla.com
push dateWed, 24 May 2017 02:19:17 +0000
reviewerspbro
bugs1309468
milestone55.0a1
Bug 1309468 - Part 1: Display effect easing curve and keyframes easing curve in summary graph. r=pbro MozReview-Commit-ID: I6obHi4joDu
devtools/client/animationinspector/components/animation-details.js
devtools/client/animationinspector/components/animation-time-block.js
devtools/client/animationinspector/components/animation-timeline.js
devtools/client/animationinspector/components/keyframes.js
devtools/client/animationinspector/graph-helper.js
devtools/client/animationinspector/test/browser_animation_animated_properties_for_delayed_starttime_animations.js
devtools/client/animationinspector/test/browser_animation_controller_exposes_document_currentTime.js
devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js
devtools/client/animationinspector/test/browser_animation_refresh_on_added_animation.js
devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js
devtools/client/animationinspector/test/browser_animation_timeline_short_duration.js
devtools/client/animationinspector/test/head.js
devtools/client/themes/animationinspector.css
--- a/devtools/client/animationinspector/components/animation-details.js
+++ b/devtools/client/animationinspector/components/animation-details.js
@@ -80,77 +80,16 @@ AnimationDetails.prototype = {
       } else if (!isRunningOnCompositor && warning != "") {
         className = "warning";
       }
     }
     return {className, warning};
   },
 
   /**
-   * Get a list of the tracks of the animation actor
-   * @return {Object} A list of tracks, one per animated property, each
-   * with a list of keyframes
-   */
-  getTracks: Task.async(function* () {
-    let tracks = {};
-
-    /*
-     * getFrames is a AnimationPlayorActor method that returns data about the
-     * keyframes of the animation.
-     * In FF48, the data it returns change, and will hold only longhand
-     * properties ( e.g. borderLeftWidth ), which does not match what we
-     * want to display in the animation detail.
-     * A new AnimationPlayerActor function, getProperties, is introduced,
-     * that returns the animated css properties of the animation and their
-     * keyframes values.
-     * If the animation actor has the getProperties function, we use it, and if
-     * not, we fall back to getFrames, which then returns values we used to
-     * handle.
-     */
-    if (this.serverTraits.hasGetProperties) {
-      let properties = yield this.animation.getProperties();
-      for (let {name, values} of properties) {
-        if (!tracks[name]) {
-          tracks[name] = [];
-        }
-
-        for (let {value, offset, easing, distance} of values) {
-          distance = distance ? distance : 0;
-          tracks[name].push({value, offset, easing, distance});
-        }
-      }
-    } else {
-      let frames = yield this.animation.getFrames();
-      for (let frame of frames) {
-        for (let name in frame) {
-          if (this.NON_PROPERTIES.indexOf(name) != -1) {
-            continue;
-          }
-
-          // We have to change to CSS property name
-          // since GetKeyframes returns JS property name.
-          const propertyCSSName = getCssPropertyName(name);
-          if (!tracks[propertyCSSName]) {
-            tracks[propertyCSSName] = [];
-          }
-
-          tracks[propertyCSSName].push({
-            value: frame[name],
-            offset: frame.computedOffset,
-            easing: frame.easing,
-            distance: 0
-          });
-        }
-      }
-    }
-
-    return tracks;
-  }),
-
-  /**
    * Get animation types of given CSS property names.
    * @param {Array} CSS property names.
    *                e.g. ["background-color", "opacity", ...]
    * @return {Object} Animation type mapped with CSS property name.
    *                  e.g. { "background-color": "color", }
    *                         "opacity": "float", ... }
    */
   getAnimationTypes: Task.async(function* (propertyNames) {
@@ -160,33 +99,31 @@ AnimationDetails.prototype = {
     // Set animation type 'none' since does not support getAnimationTypes.
     const animationTypes = {};
     propertyNames.forEach(propertyName => {
       animationTypes[propertyName] = "none";
     });
     return Promise.resolve(animationTypes);
   }),
 
-  render: Task.async(function* (animation) {
+  render: Task.async(function* (animation, tracks) {
     this.unrender();
 
     if (!animation) {
       return;
     }
     this.animation = animation;
+    this.tracks = tracks;
 
     // We might have been destroyed in the meantime, or the component might
     // have been re-rendered.
     if (!this.containerEl || this.animation !== animation) {
       return;
     }
 
-    // Build an element for each animated property track.
-    this.tracks = yield this.getTracks(animation, this.serverTraits);
-
     // Get animation type for each CSS properties.
     const animationTypes = yield this.getAnimationTypes(Object.keys(this.tracks));
 
     // Render progress indicator.
     this.renderProgressIndicator();
     // Render animated properties header.
     this.renderAnimatedPropertiesHeader();
     // Render animated properties body.
@@ -194,20 +131,16 @@ AnimationDetails.prototype = {
 
     // Create dummy animation to indicate the animation progress.
     const timing = Object.assign({}, animation.state, {
       iterations: animation.state.iterationCount
                   ? animation.state.iterationCount : Infinity
     });
     this.dummyAnimation =
       new this.win.Animation(new this.win.KeyframeEffect(null, null, timing), null);
-
-    // Useful for tests to know when rendering of all animation detail UIs
-    // have been completed.
-    this.emit("animation-detail-rendering-completed");
   }),
 
   renderAnimatedPropertiesHeader: function () {
     // Add animated property header.
     const headerEl = createNode({
       parent: this.containerEl,
       attributes: { "class": "animated-properties-header" }
     });
--- a/devtools/client/animationinspector/components/animation-time-block.js
+++ b/devtools/client/animationinspector/components/animation-time-block.js
@@ -4,27 +4,31 @@
  * 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 EventEmitter = require("devtools/shared/event-emitter");
 const {createNode, createSVGNode, TimeScale, getFormattedAnimationTitle} =
   require("devtools/client/animationinspector/utils");
-const {createPathSegments, appendPathElement, DEFAULT_MIN_PROGRESS_THRESHOLD} =
+const {SummaryGraphHelper, getPreferredKeyframesProgressThreshold,
+       getPreferredProgressThreshold} =
   require("devtools/client/animationinspector/graph-helper");
 
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const L10N =
       new LocalizationHelper("devtools/client/locales/animationinspector.properties");
 
 // Show max 10 iterations for infinite animations
 // to give users a clue that the animation does repeat.
 const MAX_INFINITE_ANIMATIONS_ITERATIONS = 10;
 
+// Minimum opacity for semitransparent fill color for keyframes's easing graph.
+const MIN_KEYFRAMES_EASING_OPACITY = .5;
+
 /**
  * UI component responsible for displaying a single animation timeline, which
  * basically looks like a rectangle that shows the delay and iterations.
  */
 function AnimationTimeBlock() {
   EventEmitter.decorate(this);
   this.onClick = this.onClick.bind(this);
 }
@@ -45,17 +49,17 @@ AnimationTimeBlock.prototype = {
   },
 
   unrender: function () {
     while (this.containerEl.firstChild) {
       this.containerEl.firstChild.remove();
     }
   },
 
-  render: function (animation) {
+  render: function (animation, tracks) {
     this.unrender();
 
     this.animation = animation;
     let {state} = this.animation;
 
     // Create a container element to hold the delay and iterations.
     // It is positioned according to its delay (divided by the playbackrate),
     // and its width is according to its duration (divided by the playbackrate).
@@ -81,126 +85,31 @@ 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 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 = DEFAULT_MIN_PROGRESS_THRESHOLD;
-    // If the easing is step function,
-    // minProgressThreshold should be changed by the steps.
-    const stepFunction = state.easing.match(/steps\((\d+)/);
-    if (stepFunction) {
-      minProgressThreshold = 1 / (parseInt(stepFunction[1], 10) + 1);
-    }
-
-    // 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, 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;
-      }
-    }
-
-    // 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) {
-      renderFirstIteration(summaryEl, state, mainIterationStartTime,
-                           firstSectionCount, minSegmentDuration,
-                           minProgressThreshold, segmentHelper);
-    }
+    const minSegmentDuration = totalDisplayedDuration / this.containerEl.clientWidth;
+    // Minimum progress threshold for effect timing.
+    const minEffectProgressThreshold = getPreferredProgressThreshold(state.easing);
 
-    if (iterationCount === Infinity) {
-      // If the animation repeats infinitely,
-      // we fill the remaining area with iteration paths.
-      renderInfinity(summaryEl, state, mainIterationStartTime,
-                     firstSectionCount, totalDisplayedDuration,
-                     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,
-                           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,
-                             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, segmentHelper);
-      }
-
-      // Append endDelay.
-      if (state.endDelay > 0) {
-        renderEndDelay(summaryEl, state,
-                       mainIterationStartTime, iterationCount, segmentHelper);
-      }
+    // Render summary graph.
+    // The summary graph is constructed from keyframes's easing and effect timing.
+    const graphHelper = new SummaryGraphHelper(this.win, state, minSegmentDuration);
+    renderKeyframesEasingGraph(summaryEl, state, totalDisplayedDuration,
+                               minEffectProgressThreshold, tracks, graphHelper);
+    if (state.easing !== "linear") {
+      renderEffectEasingGraph(summaryEl, state, totalDisplayedDuration,
+                              minEffectProgressThreshold, graphHelper);
     }
-
-    // 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);
-    }
+    graphHelper.destroy();
 
     // The animation name is displayed over the animation.
     createNode({
       parent: createNode({
         parent: this.containerEl,
         attributes: {
           "class": "name",
           "title": this.getTooltipText(state)
@@ -336,114 +245,246 @@ AnimationTimeBlock.prototype = {
   },
 
   get win() {
     return this.containerEl.ownerDocument.defaultView;
   }
 };
 
 /**
+ * Render keyframes's easing graph.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {float} totalDisplayedDuration - Displayable total duration.
+ * @param {float} minEffectProgressThreshold - Minimum progress threshold for effect.
+ * @param {Object} tracks - The value of AnimationsTimeline.getTracks().
+ * @param {Object} graphHelper - SummaryGraphHelper.
+ */
+function renderKeyframesEasingGraph(parentEl, state, totalDisplayedDuration,
+                                    minEffectProgressThreshold, tracks, graphHelper) {
+  const keyframesList = getOffsetAndEasingOnlyKeyframesList(tracks);
+  const keyframeEasingOpacity = Math.max(1 / keyframesList.length,
+                                         MIN_KEYFRAMES_EASING_OPACITY);
+  for (let keyframes of keyframesList) {
+    const minProgressTreshold =
+      Math.min(minEffectProgressThreshold,
+               getPreferredKeyframesProgressThreshold(keyframes));
+    graphHelper.setMinProgressThreshold(minProgressTreshold);
+    graphHelper.setKeyframes(keyframes);
+    graphHelper.setClosePathNeeded(true);
+    const element = renderGraph(parentEl, state, totalDisplayedDuration,
+                                "keyframes-easing", graphHelper);
+    element.style.opacity = keyframeEasingOpacity;
+  }
+}
+
+/**
+ * Render effect easing graph.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {float} totalDisplayedDuration - Displayable total duration.
+ * @param {float} minEffectProgressThreshold - Minimum progress threshold for effect.
+ * @param {Object} graphHelper - SummaryGraphHelper.
+ */
+function renderEffectEasingGraph(parentEl, state, totalDisplayedDuration,
+                                 minEffectProgressThreshold, graphHelper) {
+  graphHelper.setMinProgressThreshold(minEffectProgressThreshold);
+  graphHelper.setKeyframes(null);
+  graphHelper.setClosePathNeeded(false);
+  renderGraph(parentEl, state, totalDisplayedDuration, "effect-easing", graphHelper);
+}
+
+/**
+ * Render a graph of given parameters.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {float} totalDisplayedDuration - Displayable total duration.
+ * @param {String} className - Class name for graph element.
+ * @param {Object} graphHelper - SummaryGraphHelper.
+ */
+function renderGraph(parentEl, state, totalDisplayedDuration, className, graphHelper) {
+  const graphEl = createSVGNode({
+    parent: parentEl,
+    nodeType: "g",
+    attributes: {
+      "class": className,
+    }
+  });
+
+  // Starting time of main iteration.
+  let mainIterationStartTime = 0;
+  let iterationStart = state.iterationStart;
+  let iterationCount = state.iterationCount ? state.iterationCount : Infinity;
+
+  graphHelper.setFillMode(state.fill);
+  graphHelper.setOriginalBehavior(true);
+
+  // Append delay.
+  if (state.delay > 0) {
+    renderDelay(graphEl, state, graphHelper);
+    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) {
+    renderFirstIteration(graphEl, state, mainIterationStartTime,
+                         firstSectionCount, graphHelper);
+  }
+
+  if (iterationCount === Infinity) {
+    // If the animation repeats infinitely,
+    // we fill the remaining area with iteration paths.
+    renderInfinity(graphEl, state, mainIterationStartTime,
+                   firstSectionCount, totalDisplayedDuration, graphHelper);
+  } else {
+    // Otherwise, we show remaining iterations, endDelay and fill.
+
+    // Append forwards fill-mode.
+    if (state.fill === "both" || state.fill === "forwards") {
+      renderForwardsFill(graphEl, state, mainIterationStartTime,
+                         iterationCount, totalDisplayedDuration, graphHelper);
+    }
+
+    // Append middle section of iterations.
+    // e.g.
+    // if { iterationStart: 0.25, iterations: 3 }, middleSectionCount is 2.
+    const middleSectionCount =
+      Math.floor(iterationCount - firstSectionCount);
+    renderMiddleIterations(graphEl, state, mainIterationStartTime,
+                           firstSectionCount, middleSectionCount, graphHelper);
+
+    // 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(graphEl, state, mainIterationStartTime,
+                          firstSectionCount, middleSectionCount,
+                          lastSectionCount, graphHelper);
+    }
+
+    // Append endDelay.
+    if (state.endDelay > 0) {
+      renderEndDelay(graphEl, state,
+                     mainIterationStartTime, iterationCount, graphHelper);
+    }
+  }
+
+  // Append negative delay (which overlap the animation).
+  if (state.delay < 0) {
+    graphHelper.setFillMode("both");
+    graphHelper.setOriginalBehavior(false);
+    renderNegativeDelayHiddenProgress(graphEl, state, graphHelper);
+  }
+  // Append negative endDelay (which overlap the animation).
+  if (state.iterationCount && state.endDelay < 0) {
+    graphHelper.setFillMode("both");
+    graphHelper.setOriginalBehavior(false);
+    renderNegativeEndDelayHiddenProgress(graphEl, state, graphHelper);
+  }
+
+  return graphEl;
+}
+
+/**
  * Render delay section.
  * @param {Element} parentEl - Parent element of this appended path element.
  * @param {Object} state - State of animation.
- * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ * @param {Object} graphHelper - SummaryGraphHelper.
  */
-function renderDelay(parentEl, state, segmentHelper) {
-  const startSegment = segmentHelper.getSegment(0);
+function renderDelay(parentEl, state, graphHelper) {
+  const startSegment = graphHelper.getSegment(0);
   const endSegment = { x: state.delay, y: startSegment.y };
-  appendPathElement(parentEl, [startSegment, endSegment], "delay-path");
+  graphHelper.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 {Object} segmentHelper - The object returned by getSegmentHelper.
+ * @param {Object} graphHelper - SummaryGraphHelper.
  */
 function renderFirstIteration(parentEl, state, mainIterationStartTime,
-                              firstSectionCount, minSegmentDuration,
-                              minProgressThreshold, segmentHelper) {
+                              firstSectionCount, graphHelper) {
   const startTime = mainIterationStartTime;
   const endTime = startTime + firstSectionCount * state.duration;
-  const segments =
-    createPathSegments(startTime, endTime, minSegmentDuration,
-                       minProgressThreshold, segmentHelper);
-  appendPathElement(parentEl, segments, "iteration-path");
+  const segments = graphHelper.createPathSegments(startTime, endTime);
+  graphHelper.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 {Object} segmentHelper - The object returned by getSegmentHelper.
+ * @param {Object} graphHelper - SummaryGraphHelper.
  */
 function renderMiddleIterations(parentEl, state, mainIterationStartTime,
                                 firstSectionCount, middleSectionCount,
-                                minSegmentDuration, minProgressThreshold,
-                                segmentHelper) {
+                                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 =
-      createPathSegments(startTime, endTime, minSegmentDuration,
-                         minProgressThreshold, segmentHelper);
-    appendPathElement(parentEl, segments, "iteration-path");
+    const segments = graphHelper.createPathSegments(startTime, endTime);
+    graphHelper.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 {Object} segmentHelper - The object returned by getSegmentHelper.
+ * @param {Object} graphHelper - SummaryGraphHelper.
  */
 function renderLastIteration(parentEl, state, mainIterationStartTime,
                              firstSectionCount, middleSectionCount,
-                             lastSectionCount, minSegmentDuration,
-                             minProgressThreshold, segmentHelper) {
+                             lastSectionCount, graphHelper) {
   const startTime = mainIterationStartTime +
                       (firstSectionCount + middleSectionCount) * state.duration;
   const endTime = startTime + lastSectionCount * state.duration;
-  const segments =
-    createPathSegments(startTime, endTime, minSegmentDuration,
-                       minProgressThreshold, segmentHelper);
-  appendPathElement(parentEl, segments, "iteration-path");
+  const segments = graphHelper.createPathSegments(startTime, endTime);
+  graphHelper.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 {Object} segmentHelper - The object returned by getSegmentHelper.
+ * @param {Object} graphHelper - SummaryGraphHelper.
  */
 function renderInfinity(parentEl, state, mainIterationStartTime,
-                        firstSectionCount, totalDuration, minSegmentDuration,
-                        minProgressThreshold, segmentHelper) {
+                        firstSectionCount, totalDuration, graphHelper) {
   // 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));
@@ -451,19 +492,18 @@ function renderInfinity(parentEl, state,
     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 =
-    createPathSegments(firstStartTime, firstEndTime, minSegmentDuration,
-                       minProgressThreshold, segmentHelper);
-  appendPathElement(parentEl, firstSegments, "iteration-path infinity");
+    graphHelper.createPathSegments(firstStartTime, firstEndTime);
+  graphHelper.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) {
       // Copy as reverse.
@@ -471,132 +511,106 @@ 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 };
       });
     }
-    appendPathElement(parentEl, segments, "iteration-path infinity copied");
+    graphHelper.appendPathElement(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} segmentHelper - The object returned by getSegmentHelper.
+ * @param {Object} graphHelper - SummaryGraphHelper.
  */
 function renderEndDelay(parentEl, state,
-                        mainIterationStartTime, iterationCount, segmentHelper) {
+                        mainIterationStartTime, iterationCount, graphHelper) {
   const startTime = mainIterationStartTime + iterationCount * state.duration;
-  const startSegment = segmentHelper.getSegment(startTime);
+  const startSegment = graphHelper.getSegment(startTime);
   const endSegment = { x: startTime + state.endDelay, y: startSegment.y };
-  appendPathElement(parentEl, [startSegment, endSegment], "enddelay-path");
+  graphHelper.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 {Object} segmentHelper - The object returned by getSegmentHelper.
+ * @param {Object} graphHelper - SummaryGraphHelper.
  */
 function renderForwardsFill(parentEl, state, mainIterationStartTime,
-                            iterationCount, totalDuration, segmentHelper) {
+                            iterationCount, totalDuration, graphHelper) {
   const startTime = mainIterationStartTime + iterationCount * state.duration +
                       (state.endDelay > 0 ? state.endDelay : 0);
-  const startSegment = segmentHelper.getSegment(startTime);
+  const startSegment = graphHelper.getSegment(startTime);
   const endSegment = { x: totalDuration, y: startSegment.y };
-  appendPathElement(parentEl, [startSegment, endSegment], "fill-forwards-path");
+  graphHelper.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.
+ * @param {Object} graphHelper - SummaryGraphHelper.
  */
-function renderNegativeDelayHiddenProgress(parentEl, state, minSegmentDuration,
-                                           minProgressThreshold,
-                                           segmentHelper) {
+function renderNegativeDelayHiddenProgress(parentEl, state, graphHelper) {
   const startTime = state.delay;
   const endTime = 0;
   const segments =
-    createPathSegments(startTime, endTime, minSegmentDuration,
-                       minProgressThreshold, segmentHelper);
-  appendPathElement(parentEl, segments, "delay-path negative");
+    graphHelper.createPathSegments(startTime, endTime);
+  graphHelper.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.
+ * @param {Object} graphHelper - SummaryGraphHelper.
  */
-function renderNegativeEndDelayHiddenProgress(parentEl, state,
-                                              minSegmentDuration,
-                                              minProgressThreshold,
-                                              segmentHelper) {
+function renderNegativeEndDelayHiddenProgress(parentEl, state, graphHelper) {
   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");
+  const segments = graphHelper.createPathSegments(startTime, endTime);
+  graphHelper.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 {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.
+ * 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.
  */
-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);
-
-  // 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 };
+function getOffsetAndEasingOnlyKeyframesList(tracks) {
+  return Object.keys(tracks).reduce((result, name) => {
+    const track = tracks[name];
+    const exists = result.find(keyframes => {
+      if (track.length !== keyframes.length) {
+        return false;
+      }
+      for (let i = 0; i < track.length; i++) {
+        const keyframe1 = track[i];
+        const keyframe2 = keyframes[i];
+        if (keyframe1.offset !== keyframe2.offset ||
+            keyframe1.easing !== keyframe2.easing) {
+          return false;
         }
-        // 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) };
+      return true;
+    });
+    if (!exists) {
+      const keyframes = track.map(keyframe => {
+        return { offset: keyframe.offset, easing: keyframe.easing };
+      });
+      result.push(keyframes);
     }
-  };
+    return result;
+  }, []);
 }
--- a/devtools/client/animationinspector/components/animation-timeline.js
+++ b/devtools/client/animationinspector/components/animation-timeline.js
@@ -7,17 +7,18 @@
 "use strict";
 
 const {Task} = require("devtools/shared/task");
 const EventEmitter = require("devtools/shared/event-emitter");
 const {
   createNode,
   findOptimalTimeInterval,
   getFormattedAnimationTitle,
-  TimeScale
+  TimeScale,
+  getCssPropertyName
 } = require("devtools/client/animationinspector/utils");
 const {AnimationDetails} = require("devtools/client/animationinspector/components/animation-details");
 const {AnimationTargetNode} = require("devtools/client/animationinspector/components/animation-target-node");
 const {AnimationTimeBlock} = require("devtools/client/animationinspector/components/animation-time-block");
 
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const L10N =
   new LocalizationHelper("devtools/client/locales/animationinspector.properties");
@@ -41,16 +42,17 @@ const TIMELINE_BACKGROUND_RESIZE_DEBOUNC
  * when this happens, the component emits "current-data-changed" events with the
  * new time and state of the timeline.
  *
  * @param {InspectorPanel} inspector.
  * @param {Object} serverTraits The list of server-side capabilities.
  */
 function AnimationsTimeline(inspector, serverTraits) {
   this.animations = [];
+  this.tracksMap = new WeakMap();
   this.targetNodes = [];
   this.timeBlocks = [];
   this.inspector = inspector;
   this.serverTraits = serverTraits;
 
   this.onAnimationStateChanged = this.onAnimationStateChanged.bind(this);
   this.onScrubberMouseDown = this.onScrubberMouseDown.bind(this);
   this.onScrubberMouseUp = this.onScrubberMouseUp.bind(this);
@@ -239,16 +241,17 @@ AnimationsTimeline.prototype = {
       this.onScrubberMouseDown);
     this.scrubberHandleEl.removeEventListener("mousedown",
       this.onScrubberMouseDown);
     this.animationDetailCloseButton.removeEventListener("click",
       this.onDetailCloseButtonClick);
 
     this.rootWrapperEl.remove();
     this.animations = [];
+    this.tracksMap = null;
     this.rootWrapperEl = null;
     this.timeHeaderEl = null;
     this.animationsEl = null;
     this.animatedPropertiesEl = null;
     this.scrubberEl = null;
     this.scrubberHandleEl = null;
     this.win = null;
     this.inspector = null;
@@ -337,20 +340,20 @@ AnimationsTimeline.prototype = {
       this.animatedPropertiesEl.className =
         `animated-properties ${ animation.state.type }`;
     }
 
     // Select and render.
     const selectedAnimationEl = animationEls[index];
     selectedAnimationEl.classList.add("selected");
     this.animationRootEl.classList.add("animation-detail-visible");
+    // Don't render if the detail displays same animation already.
     if (animation !== this.details.animation) {
       this.selectedAnimation = animation;
-      // Don't render if the detail displays same animation already.
-      yield this.details.render(animation);
+      yield this.details.render(animation, this.tracksMap.get(animation));
       this.animationAnimationNameEl.textContent = getFormattedAnimationTitle(animation);
     }
     this.onTimelineDataChanged(null, { time: this.currentTime || 0 });
     this.emit("animation-selected", animation);
   }),
 
   /**
    * When move the scrubber to the corresponding position
@@ -424,21 +427,22 @@ AnimationsTimeline.prototype = {
         state.propertyState.some(propState => !propState.runningOnCompositor)
         ? " some-properties"
         : " all-properties";
     }
 
     return className;
   },
 
-  render: function (animations, documentCurrentTime) {
+  render: Task.async(function* (animations, documentCurrentTime) {
     this.unrenderButLeaveDetailsComponent();
 
     this.animations = animations;
     if (!this.animations.length) {
+      this.emit("animation-timeline-rendering-completed");
       return;
     }
 
     // Loop first to set the time scale for all current animations.
     for (let {state} of animations) {
       TimeScale.addAnimation(state);
     }
 
@@ -476,20 +480,22 @@ AnimationsTimeline.prototype = {
       let timeBlockEl = createNode({
         parent: animationEl,
         attributes: {
           "class": "time-block track-container"
         }
       });
 
       // Draw the animation time block.
+      const tracks = yield this.getTracks(animation);
       let timeBlock = new AnimationTimeBlock();
       timeBlock.init(timeBlockEl);
-      timeBlock.render(animation);
+      timeBlock.render(animation, tracks);
       this.timeBlocks.push(timeBlock);
+      this.tracksMap.set(animation, tracks);
 
       timeBlock.on("selected", this.onAnimationSelected);
     }
 
     // Use the document's current time to position the scrubber (if the server
     // doesn't provide it, hide the scrubber entirely).
     // Note that because the currentTime was sent via the protocol, some time
     // may have gone by since then, and so the scrubber might be a bit late.
@@ -500,32 +506,31 @@ AnimationsTimeline.prototype = {
       this.startAnimatingScrubber(this.wasRewound()
                                   ? TimeScale.minStartTime
                                   : documentCurrentTime);
     }
 
     // To indicate the animation progress in AnimationDetails.
     this.on("timeline-data-changed", this.onTimelineDataChanged);
 
-    // Display animation's detail if there is only one animation,
-    // or the previously displayed animation is included in timeline list.
     if (this.animations.length === 1) {
-      this.onAnimationSelected(null, this.animations[0]);
-      return;
+      // Display animation's detail if there is only one animation,
+      // even if the detail pane is closing.
+      yield this.onAnimationSelected(null, this.animations[0]);
+    } else if (this.animationRootEl.classList.contains("animation-detail-visible") &&
+               this.animations.indexOf(this.selectedAnimation) >= 0) {
+      // animation's detail displays in case of the previously displayed animation is
+      // included in timeline list and the detail pane is not closing.
+      yield this.onAnimationSelected(null, this.selectedAnimation);
+    } else {
+      // Otherwise, close detail pane.
+      this.onDetailCloseButtonClick();
     }
-    if (!this.animationRootEl.classList.contains("animation-detail-visible")) {
-      // Do nothing since the animation detail pane is closing now.
-      return;
-    }
-    if (this.animations.indexOf(this.selectedAnimation) >= 0) {
-      this.onAnimationSelected(null, this.selectedAnimation);
-      return;
-    }
-    this.onDetailCloseButtonClick();
-  },
+    this.emit("animation-timeline-rendering-completed");
+  }),
 
   isAtLeastOneAnimationPlaying: function () {
     return this.animations.some(({state}) => state.playState === "running");
   },
 
   wasRewound: function () {
     return !this.isAtLeastOneAnimationPlaying() &&
            this.animations.every(({state}) => state.currentTime === 0);
@@ -634,12 +639,77 @@ AnimationsTimeline.prototype = {
   onTimelineDataChanged: function (e, { time }) {
     this.currentTime = time;
     const indicateTime =
       TimeScale.minStartTime === Infinity ? 0 : this.currentTime + TimeScale.minStartTime;
     this.details.indicateProgress(indicateTime);
   },
 
   onDetailCloseButtonClick: function (e) {
+    if (!this.animationRootEl.classList.contains("animation-detail-visible")) {
+      return;
+    }
     this.animationRootEl.classList.remove("animation-detail-visible");
     this.emit("animation-detail-closed");
-  }
+  },
+
+  /**
+   * Get a list of the tracks of the animation actor
+   * @param {Object} animation
+   * @return {Object} A list of tracks, one per animated property, each
+   * with a list of keyframes
+   */
+  getTracks: Task.async(function* (animation) {
+    let tracks = {};
+
+    /*
+     * getFrames is a AnimationPlayorActor method that returns data about the
+     * keyframes of the animation.
+     * In FF48, the data it returns change, and will hold only longhand
+     * properties ( e.g. borderLeftWidth ), which does not match what we
+     * want to display in the animation detail.
+     * A new AnimationPlayerActor function, getProperties, is introduced,
+     * that returns the animated css properties of the animation and their
+     * keyframes values.
+     * If the animation actor has the getProperties function, we use it, and if
+     * not, we fall back to getFrames, which then returns values we used to
+     * handle.
+     */
+    if (this.serverTraits.hasGetProperties) {
+      let properties = yield animation.getProperties();
+      for (let {name, values} of properties) {
+        if (!tracks[name]) {
+          tracks[name] = [];
+        }
+
+        for (let {value, offset, easing, distance} of values) {
+          distance = distance ? distance : 0;
+          tracks[name].push({value, offset, easing, distance});
+        }
+      }
+    } else {
+      let frames = yield animation.getFrames();
+      for (let frame of frames) {
+        for (let name in frame) {
+          if (this.NON_PROPERTIES.indexOf(name) != -1) {
+            continue;
+          }
+
+          // We have to change to CSS property name
+          // since GetKeyframes returns JS property name.
+          const propertyCSSName = getCssPropertyName(name);
+          if (!tracks[propertyCSSName]) {
+            tracks[propertyCSSName] = [];
+          }
+
+          tracks[propertyCSSName].push({
+            value: frame[name],
+            offset: frame.computedOffset,
+            easing: frame.easing,
+            distance: 0
+          });
+        }
+      }
+    }
+
+    return tracks;
+  })
 };
--- 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 EventEmitter = require("devtools/shared/event-emitter");
 const {createNode, createSVGNode} =
   require("devtools/client/animationinspector/utils");
-const {ProgressGraphHelper, appendPathElement, DEFAULT_MIN_PROGRESS_THRESHOLD} =
+const {ProgressGraphHelper, appendPathElement, getPreferredKeyframesProgressThreshold} =
          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.
@@ -71,17 +71,17 @@ Keyframes.prototype = {
                           ${ 1 + strokeHeightForViewBox * 2 }`);
 
     // Create graph helper to render the animation property graph.
     const graphHelper =
       new ProgressGraphHelper(this.containerEl.ownerDocument.defaultView,
                               propertyName, animationType, keyframes, totalDuration);
 
     renderPropertyGraph(graphEl, totalDuration, minSegmentDuration,
-                        DEFAULT_MIN_PROGRESS_THRESHOLD, graphHelper);
+                        getPreferredKeyframesProgressThreshold(keyframes), graphHelper);
 
     // Destroy ProgressGraphHelper resources.
     graphHelper.destroy();
 
     // Append elements to display keyframe values.
     this.keyframesEl.classList.add(animation.state.type);
     for (let frame of this.keyframes) {
       createNode({
--- a/devtools/client/animationinspector/graph-helper.js
+++ b/devtools/client/animationinspector/graph-helper.js
@@ -241,16 +241,195 @@ ProgressGraphHelper.prototype = {
            : createPathSegments(startTime, endTime,
                                 minSegmentDuration, minProgressThreshold, this);
   },
 };
 
 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
+ *   "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
+ *   distance is less than this minProgressThreshold.
+ *   So, while setting a low threshold produces a smooth graph,
+ *   it will have an effect on performance.
+ * @param {Object} win - window object.
+ * @param {Object} state - animation state.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ */
+function SummaryGraphHelper(win, state, minSegmentDuration) {
+  this.win = win;
+  const doc = this.win.document;
+  this.targetEl = doc.createElement("div");
+  doc.documentElement.appendChild(this.targetEl);
+
+  const effectTiming = Object.assign({}, state, {
+    iterations: state.iterationCount ? state.iterationCount : Infinity
+  });
+  this.animation = this.targetEl.animate(null, effectTiming);
+  this.animation.pause();
+  this.endTime = this.animation.effect.getComputedTiming().endTime;
+
+  this.minSegmentDuration = minSegmentDuration;
+  this.minProgressThreshold = DEFAULT_MIN_PROGRESS_THRESHOLD;
+}
+
+SummaryGraphHelper.prototype = {
+  /**
+   * Destory this object.
+   */
+  destroy: function () {
+    this.animation.cancel();
+    this.targetEl.remove();
+    this.targetEl = null;
+    this.animation = null;
+    this.win = null;
+  },
+
+  /*
+   * Set keyframes to shape graph by computed style. This method creates new keyframe
+   * object using only offset and easing of given keyframes.
+   * Also, allows null value. In case of null, this graph helper shapes graph using
+   * computed timing progress.
+   * @param {Object} keyframes - Should have offset and easing, or null.
+   */
+  setKeyframes: function (keyframes) {
+    let frames = null;
+    if (keyframes) {
+      // Create new keyframes for opacity as computed style.
+      frames = keyframes.map(keyframe => {
+        return {
+          opacity: keyframe.offset,
+          offset: keyframe.offset,
+          easing: keyframe.easing
+        };
+      });
+    }
+    this.animation.effect.setKeyframes(frames);
+    this.hasFrames = !!frames;
+  },
+
+  /*
+   * Set animation behavior.
+   * In Animation::SetCurrentTime spec, even if current time of animation is over
+   * endTime, the progress is changed. Likewise, in case the time is less than 0.
+   * If set true, we prevent the time to make the same animation behavior as the original.
+   * @param {bool} isOriginalBehavior - true: original behavior
+   *                                    false: according to spec.
+   */
+  setOriginalBehavior: function (isOriginalBehavior) {
+    this.isOriginalBehavior = isOriginalBehavior;
+  },
+
+  /**
+   * 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.
+   * @param {bool} isClosePathNeeded - true: close, false: open.
+   */
+  setClosePathNeeded: function (isClosePathNeeded) {
+    this.isClosePathNeeded = isClosePathNeeded;
+  },
+
+  /**
+   * SummaryGraphHelper searches and creates the summary graph untill the progress
+   * distance is less than this minProgressThreshold.
+   */
+  setMinProgressThreshold: function (minProgressThreshold) {
+    this.minProgressThreshold = minProgressThreshold;
+  },
+
+  /**
+   * Return a segment in graph by given the time.
+   * @return {Object} Computed result which has follwing values.
+   * - x: x value of graph (float)
+   * - y: y value of graph (float between 0 - 1)
+   */
+  getSegment: function (time) {
+    if (this.isOriginalBehavior) {
+      // 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 value = this.hasFrames ? this.getOpacityValue() : this.getProgressValue();
+    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.
+   * @return {Array} path segments -
+   *                 [{x: {Number} time, y: {Number} progress}, ...]
+   */
+  createPathSegments: function (startTime, endTime) {
+    return createPathSegments(startTime, endTime,
+                              this.minSegmentDuration, this.minProgressThreshold, this);
+  },
+
+  /**
+   * 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.
+   * @return {Element} path element.
+   */
+  appendPathElement: function (parentEl, pathSegments, cls) {
+    return appendPathElement(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);
+  },
+
+  /**
+   * Return current computed 'opacity' value of the element which is animating.
+   * @return {float} computed timing progress as float value of Y axis.
+   */
+  getOpacityValue: function () {
+    return this.win.getComputedStyle(this.targetEl).opacity;
+  }
+};
+
+exports.SummaryGraphHelper = SummaryGraphHelper;
+
+/**
  * 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 {Object} segmentHelper
  * - getSegment(time): Helper function that, given a time,
  *                     will calculate the animation progress.
@@ -303,19 +482,20 @@ function createPathSegments(startTime, e
 }
 exports.createPathSegments = createPathSegments;
 
 /**
  * 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. (default true)
  * @return {Element} path element.
  */
-function appendPathElement(parentEl, pathSegments, cls) {
+function appendPathElement(parentEl, pathSegments, cls, isClosePathNeeded = true) {
   // 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;
     }
@@ -325,17 +505,20 @@ function appendPathElement(parentEl, pat
       break;
     }
 
     const nextPathSegment = pathSegments[i + 1];
     path += pathSegment.easing.startsWith("steps")
             ? createStepsPathString(pathSegment, nextPathSegment)
             : createCubicBezierPathString(pathSegment, nextPathSegment);
   }
-  path += ` L${ pathSegments[pathSegments.length - 1].x },0 Z`;
+  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: {
       "d": path,
       "class": cls,
       "vector-effect": "non-scaling-stroke",
@@ -454,8 +637,47 @@ function getRGBADistance(rgba1, rgba2) {
   const endG = rgba2.g * endA;
   const endB = rgba2.b * endA;
   const diffA = startA - endA;
   const diffR = startR - endR;
   const diffG = startG - endG;
   const diffB = startB - endB;
   return Math.sqrt(diffA * diffA + diffR * diffR + diffG * diffG + diffB * diffB);
 }
+
+/**
+ * Return preferred progress threshold for given keyframes.
+ * See the documentation of DURATION_RESOLUTION and DEFAULT_MIN_PROGRESS_THRESHOLD
+ * for more information regarding this.
+ * @param {Array} keyframes - keyframes
+ * @return {float} - preferred progress threshold.
+ */
+function getPreferredKeyframesProgressThreshold(keyframes) {
+  let minProgressTreshold = DEFAULT_MIN_PROGRESS_THRESHOLD;
+  for (let i = 0; i < keyframes.length - 1; i++) {
+    const keyframe = keyframes[i];
+    if (!keyframe.easing) {
+      continue;
+    }
+    let keyframeProgressThreshold = getPreferredProgressThreshold(keyframe.easing);
+    if (keyframeProgressThreshold !== DEFAULT_MIN_PROGRESS_THRESHOLD) {
+      // We should consider the keyframe's duration.
+      keyframeProgressThreshold *=
+        (keyframes[i + 1].offset - keyframe.offset);
+    }
+    minProgressTreshold = Math.min(keyframeProgressThreshold, minProgressTreshold);
+  }
+  return minProgressTreshold;
+}
+exports.getPreferredKeyframesProgressThreshold = getPreferredKeyframesProgressThreshold;
+
+/**
+ * Return preferred progress threshold to render summary graph.
+ * @param {String} - easing e.g. steps(2), linear and so on.
+ * @return {float} - preferred threshold.
+ */
+function getPreferredProgressThreshold(easing) {
+  const stepFunction = easing.match(/steps\((\d+)/);
+  return stepFunction
+       ? 1 / (parseInt(stepFunction[1], 10) + 1)
+       : DEFAULT_MIN_PROGRESS_THRESHOLD;
+}
+exports.getPreferredProgressThreshold = getPreferredProgressThreshold;
--- a/devtools/client/animationinspector/test/browser_animation_animated_properties_for_delayed_starttime_animations.js
+++ b/devtools/client/animationinspector/test/browser_animation_animated_properties_for_delayed_starttime_animations.js
@@ -6,20 +6,20 @@
 
 // Test for animations that have different starting time.
 // We should check progress indicator working well even if the start time is not zero.
 // Also, check that there is no duplication display.
 
 add_task(function* () {
   yield addTab(URL_ROOT + "doc_delayed_starttime_animations.html");
   const { panel } = yield openAnimationInspector();
-  yield setStyleAndWaitForAnimationSelecting(panel, "animation", "anim 100s", "#target2");
-  yield setStyleAndWaitForAnimationSelecting(panel, "animation", "anim 100s", "#target3");
-  yield setStyleAndWaitForAnimationSelecting(panel, "animation", "anim 100s", "#target4");
-  yield setStyleAndWaitForAnimationSelecting(panel, "animation", "anim 100s", "#target5");
+  yield setStyle(null, panel, "animation", "anim 100s", "#target2");
+  yield setStyle(null, panel, "animation", "anim 100s", "#target3");
+  yield setStyle(null, panel, "animation", "anim 100s", "#target4");
+  yield setStyle(null, panel, "animation", "anim 100s", "#target5");
 
   const timelineComponent = panel.animationsTimelineComponent;
   const detailsComponent = timelineComponent.details;
   const headers =
     detailsComponent.containerEl.querySelectorAll(".animated-properties-header");
   is(headers.length, 1, "There should be only one header in the details panel");
 
   // Check indicator.
@@ -37,14 +37,8 @@ add_task(function* () {
      "The progress indicator position should be 50% at half time of animation");
   detailsComponent.indicateProgress(startTime + 99 * 1000);
   is(progressIndicatorEl.style.left, "99%",
      "The progress indicator position should be 99% at 99s");
   detailsComponent.indicateProgress(startTime + 100 * 1000);
   is(progressIndicatorEl.style.left, "0%",
      "The progress indicator position should be 0% at end of animation");
 });
-
-function* setStyleAndWaitForAnimationSelecting(panel, name, value, selector) {
-  const onSelecting = waitForAnimationSelecting(panel);
-  yield setStyle(null, panel, name, value, selector);
-  yield onSelecting;
-}
--- a/devtools/client/animationinspector/test/browser_animation_controller_exposes_document_currentTime.js
+++ b/devtools/client/animationinspector/test/browser_animation_controller_exposes_document_currentTime.js
@@ -34,10 +34,11 @@ function* startNewAnimation(controller, 
   info("Add a new animation to the page and check the time again");
   let onPlayerAdded = controller.once(controller.PLAYERS_UPDATED_EVENT);
   yield executeInContent("devtools:test:setAttribute", {
     selector: ".still",
     attributeName: "class",
     attributeValue: "ball still short"
   });
   yield onPlayerAdded;
+  yield waitForAnimationTimelineRendering(panel);
   yield waitForAllAnimationTargets(panel);
 }
--- a/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js
+++ b/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js
@@ -12,17 +12,19 @@
 add_task(function* () {
   yield addTab(URL_ROOT + "doc_negative_animation.html");
   let {controller, panel} = yield openAnimationInspector();
 
   info("Wait until all animations have been added " +
        "(they're added with setTimeout)");
   while (controller.animationPlayers.length < 3) {
     yield controller.once(controller.PLAYERS_UPDATED_EVENT);
+    yield waitForAnimationTimelineRendering(panel);
   }
+
   yield waitForAllAnimationTargets(panel);
 
   is(panel.animationsTimelineComponent.animations.length, 3,
      "The timeline shows 3 animations too");
 
   // Reduce the known nodeFronts to a set to make them unique.
   let nodeFronts = new Set(panel.animationsTimelineComponent
                                 .targetNodes.map(n => n.previewer.nodeFront));
--- a/devtools/client/animationinspector/test/browser_animation_refresh_on_added_animation.js
+++ b/devtools/client/animationinspector/test/browser_animation_refresh_on_added_animation.js
@@ -17,35 +17,32 @@ add_task(function* () {
 
   assertAnimationsDisplayed(panel, 0);
 
   info("Start an animation on the node");
   yield changeElementAndWait({
     selector: ".still",
     attributeName: "class",
     attributeValue: "ball animated"
-  }, panel, inspector, true);
+  }, panel, inspector);
 
   assertAnimationsDisplayed(panel, 1);
 
   info("Remove the animation class on the node");
   yield changeElementAndWait({
     selector: ".ball.animated",
     attributeName: "class",
     attributeValue: "ball still"
-  }, panel, inspector, false);
+  }, panel, inspector);
 
   assertAnimationsDisplayed(panel, 0);
 });
 
-function* changeElementAndWait(options, panel, inspector, isDetailDisplayed) {
+function* changeElementAndWait(options, panel, inspector) {
   let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
   let onInspectorUpdated = inspector.once("inspector-updated");
-  let onDetailRendered = isDetailDisplayed
-                         ? panel.animationsTimelineComponent
-                                .details.once("animation-detail-rendering-completed")
-                         : Promise.resolve();
 
   yield executeInContent("devtools:test:setAttribute", options);
 
   yield promise.all([onInspectorUpdated, onPanelUpdated,
-                     waitForAllAnimationTargets(panel), onDetailRendered]);
+                     waitForAllAnimationTargets(panel),
+                     waitForAnimationTimelineRendering(panel)]);
 }
--- a/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js
+++ b/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js
@@ -21,25 +21,25 @@ add_task(function* () {
   let {playTimelineButtonEl} = panel;
 
   // ensure the focus is on the animation panel
   window.focus();
 
   info("Simulate spacebar stroke and check playResume button" +
        " is in paused state");
 
-  // sending the key will lead to a UI_UPDATE_EVENT
-  let onUpdated = panel.once(panel.UI_UPDATED_EVENT);
+  // sending the key will lead to render animation timeline
+  let onUpdated = waitForAnimationTimelineRendering(panel);
   EventUtils.sendKey("SPACE", window);
   yield onUpdated;
   ok(playTimelineButtonEl.classList.contains("paused"),
     "The play/resume button is in its paused state");
 
   info("Simulate spacebar stroke and check playResume button" +
        " is in playing state");
 
-  // sending the key will lead to a UI_UPDATE_EVENT
-  onUpdated = panel.once(panel.UI_UPDATED_EVENT);
+  // sending the key will lead to render animation timeline
+  onUpdated = waitForAnimationTimelineRendering(panel);
   EventUtils.sendKey("SPACE", window);
   yield onUpdated;
   ok(!playTimelineButtonEl.classList.contains("paused"),
     "The play/resume button is in its play state again");
 });
--- a/devtools/client/animationinspector/test/browser_animation_timeline_short_duration.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_short_duration.js
@@ -73,17 +73,22 @@ function checkSummaryGraph(el, state, is
                 + thirdLastPathSeg.x);
 
     // The test animation is not 'forwards' fill-mode.
     // Therefore, the y of second last path segment will be 0.
     const secondLastPathSeg =
       pathSegList.getItem(pathSegList.numberOfItems - 3);
     is(secondLastPathSeg.x, endX,
        `The x of second last segment should be ${ endX }`);
-    is(secondLastPathSeg.y, 0, "The y of second last segment should be 0");
+    // We use computed style of 'opacity' to create summary graph.
+    // So, if currentTime is same to the duration, although progress is null opacity is 1.
+    const expectedY =
+      state.iterationCount && expectedIterationCount === index + 1 ? 1 : 0;
+    is(secondLastPathSeg.y, expectedY,
+       `The y of second last segment should be ${ expectedY }`);
 
     const lastPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 2);
     is(lastPathSeg.x, endX, `The x of last segment should be ${ endX }`);
     is(lastPathSeg.y, 0, "The y of last segment should be 0");
 
     const closePathSeg = pathSegList.getItem(pathSegList.numberOfItems - 1);
     is(closePathSeg.pathSegType, closePathSeg.PATHSEG_CLOSEPATH,
        `The actual last segment should be close path`);
--- a/devtools/client/animationinspector/test/head.js
+++ b/devtools/client/animationinspector/test/head.js
@@ -89,20 +89,18 @@ var selectNodeAndWaitForAnimations = Tas
   function* (data, inspector, reason = "test") {
     yield selectNode(data, inspector, reason);
 
     // We want to make sure the rest of the test waits for the animations to
     // be properly displayed (wait for all target DOM nodes to be previewed).
     let {AnimationsPanel} = inspector.sidebar.getWindowForTab(TAB_NAME);
     yield waitForAllAnimationTargets(AnimationsPanel);
 
-    if (AnimationsPanel.animationsTimelineComponent.animations.length === 1) {
-      // Wait for selecting the animation since there is only one animation.
-      yield waitForAnimationSelecting(AnimationsPanel);
-    }
+    // Wait for animation timeline rendering.
+    yield waitForAnimationTimelineRendering(AnimationsPanel);
   }
 );
 
 /**
  * Check if there are the expected number of animations being displayed in the
  * panel right now.
  * @param {AnimationsPanel} panel
  * @param {Number} nbAnimations The expected number of animations.
@@ -158,21 +156,18 @@ var openAnimationInspector = Task.async(
   } else {
     yield AnimationsPanel.once(AnimationsPanel.PANEL_INITIALIZED);
   }
 
   // Make sure we wait for all animations to be loaded (especially their target
   // nodes to be lazily displayed). This is safe to do even if there are no
   // animations displayed.
   yield waitForAllAnimationTargets(AnimationsPanel);
-
-  if (AnimationsPanel.animationsTimelineComponent.animations.length === 1) {
-    // Wait for selecting the animation since there is only one animation.
-    yield waitForAnimationSelecting(AnimationsPanel);
-  }
+  // Also, wait for timeline redering.
+  yield waitForAnimationTimelineRendering(AnimationsPanel);
 
   return {
     toolbox: toolbox,
     inspector: inspector,
     controller: AnimationsController,
     panel: AnimationsPanel,
     window: win
   };
@@ -296,49 +291,54 @@ function* assertScrubberMoving(panel, is
 
 /**
  * Click the play/pause button in the timeline toolbar and wait for animations
  * to update.
  * @param {AnimationsPanel} panel
  */
 function* clickTimelinePlayPauseButton(panel) {
   let onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
+  const onAnimationTimelineRendered = waitForAnimationTimelineRendering(panel);
 
   let btn = panel.playTimelineButtonEl;
   let win = btn.ownerDocument.defaultView;
   EventUtils.sendMouseEvent({type: "click"}, btn, win);
 
   yield onUiUpdated;
+  yield onAnimationTimelineRendered;
   yield waitForAllAnimationTargets(panel);
 }
 
 /**
  * Click the rewind button in the timeline toolbar and wait for animations to
  * update.
  * @param {AnimationsPanel} panel
  */
 function* clickTimelineRewindButton(panel) {
   let onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
+  const onAnimationTimelineRendered = waitForAnimationTimelineRendering(panel);
 
   let btn = panel.rewindTimelineButtonEl;
   let win = btn.ownerDocument.defaultView;
   EventUtils.sendMouseEvent({type: "click"}, btn, win);
 
   yield onUiUpdated;
+  yield onAnimationTimelineRendered;
   yield waitForAllAnimationTargets(panel);
 }
 
 /**
  * Select a rate inside the playback rate selector in the timeline toolbar and
  * wait for animations to update.
  * @param {AnimationsPanel} panel
  * @param {Number} rate The new rate value to be selected
  */
 function* changeTimelinePlaybackRate(panel, rate) {
   let onUiUpdated = panel.once(panel.UI_UPDATED_EVENT);
+  const onAnimationTimelineRendered = waitForAnimationTimelineRendering(panel);
 
   let select = panel.rateSelectorEl.firstChild;
   let win = select.ownerDocument.defaultView;
 
   // Get the right option.
   let option = [...select.options].filter(o => o.value === rate + "")[0];
   if (!option) {
     ok(false,
@@ -347,16 +347,17 @@ function* changeTimelinePlaybackRate(pan
     return;
   }
 
   // Simulate the right events to select the option in the drop-down.
   EventUtils.synthesizeMouseAtCenter(select, {type: "mousedown"}, win);
   EventUtils.synthesizeMouseAtCenter(option, {type: "mouseup"}, win);
 
   yield onUiUpdated;
+  yield onAnimationTimelineRendered;
   yield waitForAllAnimationTargets(panel);
 
   // Simulate a mousemove outside of the rate selector area to avoid subsequent
   // tests from failing because of unwanted mouseover events.
   EventUtils.synthesizeMouseAtCenter(
     win.document.querySelector("#timeline-toolbar"), {type: "mousemove"}, win);
 }
 
@@ -364,16 +365,28 @@ function* changeTimelinePlaybackRate(pan
  * Wait for animation selecting.
  * @param {AnimationsPanel} panel
  */
 function* waitForAnimationSelecting(panel) {
   yield panel.animationsTimelineComponent.once("animation-selected");
 }
 
 /**
+ * Wait for rendering animation timeline.
+ * @param {AnimationsPanel} panel
+ */
+function* waitForAnimationTimelineRendering(panel) {
+  const ready =
+    panel.animationsTimelineComponent.animations.length === 0
+    ? Promise.resolve()
+    : panel.animationsTimelineComponent.once("animation-timeline-rendering-completed");
+  yield ready;
+}
+
+/**
    + * Click the timeline header to update the animation current time.
    + * @param {AnimationsPanel} panel
    + * @param {Number} x position rate on timeline header.
    + *                 This method calculates
    + *                 `position * offsetWidth + offsetLeft of timeline header`
    + *                 as the clientX of MouseEvent.
    + *                 This parameter should be from 0.0 to 1.0.
    + */
@@ -474,9 +487,11 @@ function* setStyle(animation, panel, nam
     propertyName: name,
     propertyValue: value
   });
   yield onAnimationChanged;
 
   // Also wait for the target node previews to be loaded if the panel got
   // refreshed as a result of this animation mutation.
   yield waitForAllAnimationTargets(panel);
+  // And wait for animation timeline rendering.
+  yield waitForAnimationTimelineRendering(panel);
 }
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -397,19 +397,24 @@ body {
 
 /* Animation summary graph */
 .animation-timeline .animation .summary {
   position: absolute;
   width: 100%;
   height: 100%;
 }
 
-.animation-timeline .animation .summary path {
+.animation-timeline .animation .summary .effect-easing path {
+  fill: none;
+  stroke: var(--timeline-border-color);
+  stroke-dasharray: 2px 2px;
+}
+
+.animation-timeline .animation .summary .keyframes-easing path {
   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 {