Bug 1210795 - Part 1: Display animations' timing-functions in the animation-inspector. r=pbro
authorDaisuke Akatsuka <daisuke@mozilla-japan.org>
Tue, 25 Oct 2016 17:35:56 +0900
changeset 319804 55ac24f3f8ff2fd06655b23e7f0f92d1a9ca9211
parent 319803 7f3a92667acf1dbed3f12941e01607e8230b29d8
child 319805 e3f96551347dddaf90cb22c3e886e3de04adeaa5
push id20748
push userphilringnalda@gmail.com
push dateFri, 28 Oct 2016 03:39:55 +0000
treeherderfx-team@715360440695 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1210795
milestone52.0a1
Bug 1210795 - Part 1: Display animations' timing-functions in the animation-inspector. r=pbro MozReview-Commit-ID: CO5tVZ31RrL
devtools/client/animationinspector/components/animation-time-block.js
devtools/client/animationinspector/test/browser_animation_timeline_iterationStart.js
devtools/client/animationinspector/test/browser_animation_timeline_shows_iterations.js
devtools/client/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js
devtools/client/animationinspector/test/browser_animation_timeline_ui.js
devtools/client/animationinspector/utils.js
devtools/client/themes/animationinspector.css
devtools/server/actors/animation.js
devtools/server/tests/browser/animation.html
devtools/server/tests/browser/browser_animation_playerState.js
devtools/shared/fronts/animation.js
--- a/devtools/client/animationinspector/components/animation-time-block.js
+++ b/devtools/client/animationinspector/components/animation-time-block.js
@@ -7,16 +7,34 @@
 "use strict";
 
 const EventEmitter = require("devtools/shared/event-emitter");
 const {createNode, TimeScale} = require("devtools/client/animationinspector/utils");
 
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper("devtools/locale/animationinspector.properties");
 
+// 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
+// MIN_PROGRESS_THRESHOLD, then createPathSegments re-divides
+// by DURATION_RESOLUTION.
+// DURATION_RESOLUTION shoud be integer and more than 2.
+const DURATION_RESOLUTION = 4;
+// MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
+const MIN_PROGRESS_THRESHOLD = 0.1;
+// SVG namespace
+const SVG_NS = "http://www.w3.org/2000/svg";
+// ID of mask for fade effect.
+const FADE_MASK_ID = "animationinspector-fade-mask";
+// ID of gradient element.
+const FADE_GRADIENT_ID = "animationinspector-fade-gradient";
+
 /**
  * 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);
 }
@@ -52,32 +70,99 @@ AnimationTimeBlock.prototype = {
     // It is positioned according to its delay (divided by the playbackrate),
     // and its width is according to its duration (divided by the playbackrate).
     let {x, iterationW, delayX, delayW, negativeDelayW, endDelayX, endDelayW} =
       TimeScale.getAnimationDimensions(animation);
 
     // background properties for .iterations element
     let backgroundIterations = TimeScale.getIterationsBackgroundData(animation);
 
-    createNode({
+    // Displayed total duration
+    const totalDuration = TimeScale.getDuration() * state.playbackRate;
+
+    // Get a helper function that returns the path segment of timing-function.
+    const segmentHelperFn = getSegmentHelper(state, this.win);
+
+    // Minimum segment duration is the duration of one pixel.
+    const minSegmentDuration = totalDuration / this.containerEl.clientWidth;
+    // Minimum progress threshold.
+    let minProgressThreshold = 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);
+    }
+
+    // Calculate stroke height in viewBox to display stroke of path.
+    const strokeHeightForViewBox = 0.5 / this.containerEl.clientHeight;
+
+    // Animation summary graph element.
+    const summaryEl = createNode({
       parent: this.containerEl,
+      namespace: SVG_NS,
+      nodeType: "svg",
       attributes: {
-        "class": "iterations" + (state.iterationCount ? "" : " infinite"),
-        // Individual iterations are represented by setting the size of the
-        // repeating linear-gradient.
-        // The background-size, background-position, background-repeat represent
-        // iterationCount and iterationStart.
-        "style": `left:${x}%;
-                  width:${iterationW}%;
-                  background-size:${backgroundIterations.size}% 100%;
-                  background-position:${backgroundIterations.position}% 0;
-                  background-repeat:${backgroundIterations.repeat};`
+        "class": "summary",
+        "viewBox": `${ state.delay < 0 ? state.delay : 0 }
+                    -${ 1 + strokeHeightForViewBox }
+                    ${ totalDuration }
+                    ${ 1 + strokeHeightForViewBox * 2 }`,
+        "preserveAspectRatio": "none",
+        "style": `left: ${ x - (state.delay > 0 ? delayW : 0) }%;`,
       }
     });
 
+    // Iteration count
+    const iterationCount = state.iterationCount ? state.iterationCount : 1;
+
+    // Append forwards fill-mode.
+    if (state.fill === "both" || state.fill === "forwards") {
+      renderForwardsFill(summaryEl, state, iterationCount,
+                         totalDuration, segmentHelperFn);
+    }
+
+    // Append delay.
+    if (state.delay > 0) {
+      renderDelay(summaryEl, state, segmentHelperFn);
+    }
+
+    // Append 1st section of iterations,
+    // if this animation has decimal iterationStart.
+    const firstSectionCount =
+      state.iterationStart % 1 === 0
+      ? 0 : Math.min(iterationCount, 1) - state.iterationStart % 1;
+    if (firstSectionCount) {
+      renderFirstIteration(summaryEl, state, firstSectionCount,
+                           minSegmentDuration, minProgressThreshold,
+                           segmentHelperFn);
+    }
+
+    // Append middle section of iterations.
+    const middleSectionCount =
+      Math.floor(state.iterationCount - firstSectionCount);
+    renderMiddleIterations(summaryEl, state, firstSectionCount,
+                           middleSectionCount, minSegmentDuration,
+                           minProgressThreshold, segmentHelperFn);
+
+    // Append last section of iterations, if there is remaining iteration.
+    const lastSectionCount =
+      iterationCount - middleSectionCount - firstSectionCount;
+    if (lastSectionCount) {
+      renderLastIteration(summaryEl, state, firstSectionCount,
+                          middleSectionCount, lastSectionCount,
+                          minSegmentDuration, minProgressThreshold,
+                          segmentHelperFn);
+    }
+
+    // Append endDelay.
+    if (state.endDelay > 0) {
+      renderEndDelay(summaryEl, state, iterationCount, segmentHelperFn);
+    }
+
     // The animation name is displayed over the iterations.
     // Note that in case of negative delay, it is pushed towards the right so
     // the delay element does not overlap.
     createNode({
       parent: createNode({
         parent: this.containerEl,
         attributes: {
           "class": "name",
@@ -93,38 +178,36 @@ AnimationTimeBlock.prototype = {
 
     // Delay.
     if (state.delay) {
       // Negative delays need to start at 0.
       createNode({
         parent: this.containerEl,
         attributes: {
           "class": "delay" + (state.delay < 0 ? " negative" : ""),
-          "style": `left:${delayX}%;
-                    width:${delayW}%;`
+          "style": `left:${ delayX }%; width:${ delayW }%;`
         }
       });
     }
 
     // endDelay
     if (state.endDelay) {
       createNode({
         parent: this.containerEl,
         attributes: {
           "class": "end-delay" + (state.endDelay < 0 ? " negative" : ""),
-          "style": `left:${endDelayX}%;
-                    width:${endDelayW}%;`
+          "style": `left:${ endDelayX }%; width:${ endDelayW }%;`
         }
       });
     }
   },
 
   getTooltipText: function (state) {
     let getTime = time => L10N.getFormatStr("player.timeLabel",
-                            L10N.numberWithDecimals(time / 1000, 2));
+                                            L10N.numberWithDecimals(time / 1000, 2));
 
     let text = "";
 
     // Adding the name.
     text += getFormattedAnimationTitle({state});
     text += "\n";
 
     // Adding the delay.
@@ -169,32 +252,36 @@ AnimationTimeBlock.prototype = {
       text += state.playbackRate;
       text += "\n";
     }
 
     // Adding a note that the animation is running on the compositor thread if
     // needed.
     if (state.propertyState) {
       if (state.propertyState
-          .every(propState => propState.runningOnCompositor)) {
+               .every(propState => propState.runningOnCompositor)) {
         text += L10N.getStr("player.allPropertiesOnCompositorTooltip");
       } else if (state.propertyState
-                 .some(propState => propState.runningOnCompositor)) {
+                      .some(propState => propState.runningOnCompositor)) {
         text += L10N.getStr("player.somePropertiesOnCompositorTooltip");
       }
     } else if (state.isRunningOnCompositor) {
       text += L10N.getStr("player.runningOnCompositorTooltip");
     }
 
     return text;
   },
 
   onClick: function (e) {
     e.stopPropagation();
     this.emit("selected", this.animation);
+  },
+
+  get win() {
+    return this.containerEl.ownerDocument.defaultView;
   }
 };
 
 /**
  * Get a formatted title for this animation. This will be either:
  * "some-name", "some-name : CSS Transition", "some-name : CSS Animation",
  * "some-name : Script Animation", or "Script Animation", depending
  * if the server provides the type, what type it is and if the animation
@@ -211,8 +298,296 @@ function getFormattedAnimationTitle({sta
 
   // Script-generated animations may not have a name.
   if (state.type === "scriptanimation" && !state.name) {
     return L10N.getStr("timeline.scriptanimation.unnamedLabel");
   }
 
   return L10N.getFormatStr(`timeline.${state.type}.nameLabel`, state.name);
 }
+
+/**
+ * Render delay section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {function} getSegment - The function of getSegmentHelper.
+ */
+function renderDelay(parentEl, state, getSegment) {
+  const startSegment = getSegment(0);
+  const endSegment = { x: state.delay, y: startSegment.y };
+  appendPathElement(parentEl, [startSegment, endSegment], "delay-path");
+}
+
+/**
+ * Render first iteration section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} firstSectionCount - Iteration count of first section.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {function} getSegment - The function of getSegmentHelper.
+ */
+function renderFirstIteration(parentEl, state, firstSectionCount,
+                              minSegmentDuration, minProgressThreshold,
+                              getSegment) {
+  const startTime = state.delay;
+  const endTime = startTime + firstSectionCount * state.duration;
+  const segments =
+    createPathSegments(startTime, endTime, minSegmentDuration,
+                       minProgressThreshold, getSegment);
+  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} firstSectionCount - Iteration count of first section.
+ * @param {Number} middleSectionCount - Iteration count of middle section.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {function} getSegment - The function of getSegmentHelper.
+ */
+function renderMiddleIterations(parentEl, state, firstSectionCount,
+                                middleSectionCount, minSegmentDuration,
+                                minProgressThreshold, getSegment) {
+  const offset = state.delay + firstSectionCount * state.duration;
+  for (let i = 0; i < middleSectionCount; i++) {
+    // Get the path segments of each iteration.
+    const startTime = offset + i * state.duration;
+    const endTime = startTime + state.duration;
+    const segments =
+      createPathSegments(startTime, endTime, minSegmentDuration,
+                         minProgressThreshold, getSegment);
+    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} firstSectionCount - Iteration count of first section.
+ * @param {Number} middleSectionCount - Iteration count of middle section.
+ * @param {Number} lastSectionCount - Iteration count of last section.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {function} getSegment - The function of getSegmentHelper.
+ */
+function renderLastIteration(parentEl, state, firstSectionCount,
+                             middleSectionCount, lastSectionCount,
+                             minSegmentDuration, minProgressThreshold,
+                             getSegment) {
+  const startTime = state.delay + firstSectionCount * state.duration
+                    + middleSectionCount * state.duration;
+  const endTime = startTime + lastSectionCount * state.duration;
+  const segments =
+    createPathSegments(startTime, endTime, minSegmentDuration,
+                       minProgressThreshold, getSegment);
+  appendPathElement(parentEl, segments, "iteration-path");
+}
+
+/**
+ * Render endDelay section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} iterationCount - Whole iteration count.
+ * @param {function} getSegment - The function of getSegmentHelper.
+ */
+function renderEndDelay(parentEl, state, iterationCount, getSegment) {
+  const startTime = state.delay + iterationCount * state.duration;
+  const startSegment = getSegment(startTime);
+  const endSegment = { x: startTime + state.endDelay, y: startSegment.y };
+  appendPathElement(parentEl, [startSegment, endSegment], "enddelay-path");
+}
+
+/**
+ * Render forwards fill section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} iterationCount - Whole iteration count.
+ * @param {Number} totalDuration - Displayed max duration.
+ * @param {function} getSegment - The function of getSegmentHelper.
+ */
+function renderForwardsFill(parentEl, state, iterationCount,
+                            totalDuration, getSegment) {
+  const startTime = state.delay + iterationCount * state.duration +
+                    (state.endDelay > 0 ? state.endDelay : 0);
+  const startSegment = getSegment(startTime);
+  const endSegment = { x: totalDuration, y: startSegment.y };
+  const pathEl = appendPathElement(parentEl, [startSegment, endSegment],
+                                   "fill-forwards-path");
+  appendFadeEffect(parentEl, pathEl);
+}
+
+/**
+ * Get a helper function which returns the segment coord from given time.
+ * @param {Object} state - animation state
+ * @param {Object} win - window object
+ * @return {function} getSegmentHelper
+ */
+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 : 1
+  });
+  // Create a dummy Animation with the given timing.
+  const dummyAnimation =
+    new win.Animation(new win.KeyframeEffect(null, null, timing), null);
+  const endTime = dummyAnimation.effect.getComputedTiming().endTime;
+  // Return a helper function that, given a time,
+  // will calculate the progress through the dummy animation.
+  return time => {
+    // If the given time is less than 0, returned progress is 0.
+    if (time < 0) {
+      return { x: time, y: 0 };
+    }
+    dummyAnimation.currentTime =
+      time < endTime ? time : endTime;
+    const progress = dummyAnimation.effect.getComputedTiming().progress;
+    return { x: time, y: Math.max(progress, 0) };
+  };
+}
+
+/**
+ * Create the path segments from given parameters.
+ * @param {Number} startTime - Starting time of animation.
+ * @param {Number} endTime - Ending time of animation.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {function} getSegment - The function of getSegmentHelper.
+ * @return {Array} path segments -
+ *                 [{x: {Number} time, y: {Number} progress}, ...]
+ */
+function createPathSegments(startTime, endTime, minSegmentDuration,
+                            minProgressThreshold, 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) / DURATION_RESOLUTION;
+  for (let index = 1; index <= DURATION_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 1ms).
+      pathSegments = pathSegments.concat(
+        createPathSegments(previousSegment.x + 1, currentSegment.x - 1,
+                           minSegmentDuration, minProgressThreshold,
+                           getSegment));
+    }
+
+    pathSegments.push(currentSegment);
+    previousSegment = currentSegment;
+  }
+
+  return pathSegments;
+}
+
+/**
+ * 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.
+ */
+function appendPathElement(parentEl, pathSegments, cls) {
+  // Create path string.
+  let path = `M${ pathSegments[0].x },0`;
+  pathSegments.forEach(pathSegment => {
+    path += ` L${ pathSegment.x },${ pathSegment.y }`;
+  });
+  path += ` L${ pathSegments[pathSegments.length - 1].x },0 Z`;
+  // Append and return the path element.
+  return createNode({
+    parent: parentEl,
+    namespace: SVG_NS,
+    nodeType: "path",
+    attributes: {
+      "d": path,
+      "class": cls,
+      "vector-effect": "non-scaling-stroke",
+      "transform": "scale(1, -1)"
+    }
+  });
+}
+
+/**
+ * Append fade effect.
+ * @param {Element} parentEl - Parent element of this appended element.
+ * @param {Element} el - Append fade effect to this element.
+ */
+function appendFadeEffect(parentEl, el) {
+  // We can re-use mask element for fade.
+  // Keep the defs element in SVG element of given parentEl.
+  if (!parentEl.ownerDocument.body.querySelector(`#${ FADE_MASK_ID }`)) {
+    const svgEl = parentEl.closest(".summary");
+    const defsEl = createNode({
+      parent: svgEl,
+      namespace: SVG_NS,
+      nodeType: "defs"
+    });
+    const gradientEl = createNode({
+      parent: defsEl,
+      namespace: SVG_NS,
+      nodeType: "linearGradient",
+      attributes: {
+        "id": FADE_GRADIENT_ID
+      }
+    });
+    createNode({
+      parent: gradientEl,
+      namespace: SVG_NS,
+      nodeType: "stop",
+      attributes: {
+        "offset": 0
+      }
+    });
+    createNode({
+      parent: gradientEl,
+      namespace: SVG_NS,
+      nodeType: "stop",
+      attributes: {
+        "offset": 1
+      }
+    });
+
+    const maskEl = createNode({
+      parent: defsEl,
+      namespace: SVG_NS,
+      nodeType: "mask",
+      attributes: {
+        "id": FADE_MASK_ID,
+        "maskContentUnits": "objectBoundingBox",
+      }
+    });
+    createNode({
+      parent: maskEl,
+      namespace: SVG_NS,
+      nodeType: "rect",
+      attributes: {
+        "y": `${ 1 + svgEl.viewBox.animVal.y }`,
+        "width": "1",
+        "height": `${ svgEl.viewBox.animVal.height }`,
+        "fill": `url(#${ FADE_GRADIENT_ID })`
+      }
+    });
+  }
+  el.setAttribute("mask", `url(#${ FADE_MASK_ID })`);
+}
--- a/devtools/client/animationinspector/test/browser_animation_timeline_iterationStart.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_iterationStart.js
@@ -16,17 +16,17 @@ add_task(function* () {
   for (let i = 0; i < timeBlockComponents.length; i++) {
     info(`Expand time block ${i} so its keyframes are visible`);
     yield clickOnAnimation(panel, i);
 
     info(`Check the state of time block ${i}`);
     let {containerEl, animation: {state}} = timeBlockComponents[i];
 
     checkAnimationTooltip(containerEl, state);
-    checkIterationBackground(containerEl, state);
+    checkProgressAtStartingTime(containerEl, state);
 
     // Get the first set of keyframes (there's only one animated property
     // anyway), and the first frame element from there, we're only interested in
     // its offset.
     let keyframeComponent = detailsComponents[i].keyframeComponents[0];
     let frameEl = keyframeComponent.keyframesEl.querySelector(".frame");
     checkKeyframeOffset(containerEl, frameEl, state);
   }
@@ -43,37 +43,24 @@ function checkAnimationTooltip(el, {iter
   }).replace(".", "\\.");
   let iterationStartString = iterationStart.toString().replace(".", "\\.");
 
   let regex = new RegExp("Iteration start: " + iterationStartString +
                          " \\(" + iterationStartTimeString + "s\\)");
   ok(title.match(regex), "The tooltip shows the expected iteration start");
 }
 
-function checkIterationBackground(el, {iterationCount, iterationStart}) {
-  info("Check the background-image used to display iterations is offset " +
-       "correctly to represent the iterationStart");
-
-  let iterationsEl = el.querySelector(".iterations");
-  let start = getIterationStartFromBackground(iterationsEl, iterationCount);
-  is(start, iterationStart % 1,
-     "The right background-position for iteration start");
-}
-
-function getIterationStartFromBackground(el, iterationCount) {
-  if (iterationCount == 1) {
-    let size = parseFloat(/([.\d]+)%/.exec(el.style.backgroundSize)[1]);
-    return 1 - size / 100;
-  }
-
-  let size = parseFloat(/([.\d]+)%/.exec(el.style.backgroundSize)[1]);
-  let position = parseFloat(/([-\d]+)%/.exec(el.style.backgroundPosition)[1]);
-  let iterationStartW = -position / size * (100 - size);
-  let rounded = Math.round(iterationStartW * 100);
-  return rounded / 10000;
+function checkProgressAtStartingTime(el, { iterationStart }) {
+  info("Check the progress of starting time");
+  const pathEl = el.querySelector(".iteration-path");
+  const pathSegList = pathEl.pathSegList;
+  const pathSeg = pathSegList.getItem(1);
+  const progress = pathSeg.y;
+  is(progress, iterationStart % 1,
+     `The progress at starting point should be ${ iterationStart % 1 }`);
 }
 
 function checkKeyframeOffset(timeBlockEl, frameEl, {iterationStart}) {
   info("Check that the first keyframe is offset correctly");
 
   let start = getIterationStartFromLeft(frameEl);
   is(start, iterationStart % 1, "The frame offset for iteration start");
 }
--- a/devtools/client/animationinspector/test/browser_animation_timeline_shows_iterations.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_iterations.js
@@ -12,41 +12,31 @@ requestLongerTimeout(2);
 add_task(function* () {
   yield addTab(URL_ROOT + "doc_simple_animation.html");
   let {inspector, panel} = yield openAnimationInspector();
 
   info("Selecting the test node");
   yield selectNodeAndWaitForAnimations(".delayed", inspector);
 
   info("Getting the animation element from the panel");
-  let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
+  const timelineComponent = panel.animationsTimelineComponent;
+  const timelineEl = timelineComponent.rootWrapperEl;
   let animation = timelineEl.querySelector(".time-block");
-  let iterations = animation.querySelector(".iterations");
-
-  // Iterations are rendered with a repeating linear-gradient, so we need to
-  // calculate how many iterations are represented by looking at the background
-  // size.
-  let iterationCount = getIterationCountFromBackground(iterations);
+  // Get iteration count from summary graph path.
+  let iterationCount = getIterationCount(animation);
 
   is(iterationCount, 10,
      "The animation timeline contains the right number of iterations");
-  ok(!iterations.classList.contains("infinite"),
-     "The iteration element doesn't have the infinite class");
 
   info("Selecting another test node with an infinite animation");
   yield selectNodeAndWaitForAnimations(".animated", inspector);
 
   info("Getting the animation element from the panel again");
   animation = timelineEl.querySelector(".time-block");
-  iterations = animation.querySelector(".iterations");
-
-  iterationCount = getIterationCountFromBackground(iterations);
+  iterationCount = getIterationCount(animation);
 
   is(iterationCount, 1,
      "The animation timeline contains just one iteration");
-  ok(iterations.classList.contains("infinite"),
-     "The iteration element has the infinite class");
 });
 
-function getIterationCountFromBackground(el) {
-  let backgroundSize = parseFloat(el.style.backgroundSize.split(" ")[0]);
-  return Math.round(100 / backgroundSize);
+function getIterationCount(timeblockEl) {
+  return timeblockEl.querySelectorAll(".iteration-path").length;
 }
--- a/devtools/client/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js
@@ -19,25 +19,63 @@ add_task(function* () {
   let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
 
   let timeBlocks = timelineEl.querySelectorAll(".time-block");
   is(timeBlocks.length, 2, "2 animations are displayed");
 
   info("The first animation has its rate set to 1, let's measure it");
 
   let el = timeBlocks[0];
-  let duration = parseInt(el.querySelector(".iterations").style.width, 10);
+  let duration = getDuration(el.querySelector("path"));
   let delay = parseInt(el.querySelector(".delay").style.width, 10);
 
   info("The second animation has its rate set to 2, so should be shorter");
 
   let el2 = timeBlocks[1];
-  let duration2 = parseInt(el2.querySelector(".iterations").style.width, 10);
+  let duration2 = getDuration(el2.querySelector("path"));
   let delay2 = parseInt(el2.querySelector(".delay").style.width, 10);
 
   // The width are calculated by the animation-inspector dynamically depending
   // on the size of the panel, and therefore depends on the test machine/OS.
   // Let's not try to be too precise here and compare numbers.
   let durationDelta = (2 * duration2) - duration;
   ok(durationDelta <= 1, "The duration width is correct");
   let delayDelta = (2 * delay2) - delay;
   ok(delayDelta <= 1, "The delay width is correct");
 });
+
+function getDuration(pathEl) {
+  const pathSegList = pathEl.pathSegList;
+  // Find the index of starting iterations.
+  let startingIterationIndex = 0;
+  const firstPathSeg = pathSegList.getItem(1);
+  for (let i = 2, n = pathSegList.numberOfItems - 2; i < n; i++) {
+    // Changing point of the progress acceleration is the time.
+    const pathSeg = pathSegList.getItem(i);
+    if (firstPathSeg.y != pathSeg.y) {
+      startingIterationIndex = i;
+      break;
+    }
+  }
+  // Find the index of ending iterations.
+  let endingIterationIndex = 0;
+  let previousPathSegment = pathSegList.getItem(startingIterationIndex);
+  for (let i = startingIterationIndex + 1, n = pathSegList.numberOfItems - 2;
+       i < n; i++) {
+    // Find forwards fill-mode.
+    const pathSeg = pathSegList.getItem(i);
+    if (previousPathSegment.y == pathSeg.y) {
+      endingIterationIndex = i;
+      break;
+    }
+    previousPathSegment = pathSeg;
+  }
+  if (endingIterationIndex) {
+    // Not forwards fill-mode
+    endingIterationIndex = pathSegList.numberOfItems - 2;
+  }
+  // Return the distance of starting and ending
+  const startingIterationPathSegment =
+    pathSegList.getItem(startingIterationIndex);
+  const endingIterationPathSegment =
+    pathSegList.getItem(startingIterationIndex);
+  return endingIterationPathSegment.x - startingIterationPathSegment.x;
+}
--- a/devtools/client/animationinspector/test/browser_animation_timeline_ui.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_ui.js
@@ -32,12 +32,12 @@ add_task(function* () {
 
     ok(animationEl.querySelector(".target"),
        "The animated node target element is in the DOM");
     ok(animationEl.querySelector(".time-block"),
        "The timeline element is in the DOM");
     is(animationEl.querySelector(".name").textContent,
        animation.state.name,
        "The name on the timeline is correct");
-    ok(animationEl.querySelector(".iterations"),
-       "The timeline has iterations displayed");
+    ok(animationEl.querySelector("svg path"),
+       "The timeline has svg and path element as summary graph");
   }
 });
--- a/devtools/client/animationinspector/utils.js
+++ b/devtools/client/animationinspector/utils.js
@@ -22,25 +22,29 @@ const MILLIS_TIME_FORMAT_MAX_DURATION = 
 /**
  * DOM node creation helper function.
  * @param {Object} Options to customize the node to be created.
  * - nodeType {String} Optional, defaults to "div",
  * - attributes {Object} Optional attributes object like
  *   {attrName1:value1, attrName2: value2, ...}
  * - parent {DOMNode} Mandatory node to append the newly created node to.
  * - textContent {String} Optional text for the node.
+ * - namespace {String} Optional namespace
  * @return {DOMNode} The newly created node.
  */
 function createNode(options) {
   if (!options.parent) {
     throw new Error("Missing parent DOMNode to create new node");
   }
 
   let type = options.nodeType || "div";
-  let node = options.parent.ownerDocument.createElement(type);
+  let node =
+    options.namespace
+    ? options.parent.ownerDocument.createElementNS(options.namespace, type)
+    : options.parent.ownerDocument.createElement(type);
 
   for (let name in options.attributes || {}) {
     let value = options.attributes[name];
     node.setAttribute(name, value);
   }
 
   if (options.textContent) {
     node.textContent = options.textContent;
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -339,57 +339,26 @@ body {
 .animation-timeline .animation-target {
   background-color: transparent;
 }
 
 .animation-timeline .animation .time-block {
   cursor: pointer;
 }
 
-/* Animation iterations */
-
-.animation-timeline .animation .iterations {
+/* Animation summary graph */
+.animation-timeline .animation .summary {
   position: absolute;
+  width: 100%;
   height: 100%;
-  box-sizing: border-box;
-
-  /* Iterations of the animation are displayed with a repeating linear-gradient
-     which size is dynamically changed from JS. The gradient only draws 1px
-     borders between each iteration. These borders must have the same color as
-     the border of this element */
-  background-image:
-    linear-gradient(to left,
-                    var(--timeline-border-color) 0,
-                    var(--timeline-border-color) 1px,
-                    transparent 1px,
-                    transparent 2px);
-  border: 1px solid var(--timeline-border-color);
-  /* Border-right is already handled by the gradient */
-  border-width: 1px 0 1px 1px;
-
-  /* The background color is set independently */
-  background-color: var(--timeline-background-color);
 }
 
-.animation-timeline .animation .iterations.infinite::before,
-.animation-timeline .animation .iterations.infinite::after {
-  content: "";
-  position: absolute;
-  top: 0;
-  right: 0;
-  width: 0;
-  height: 0;
-  border-right: 4px solid var(--theme-body-background);
-  border-top: 4px solid transparent;
-  border-bottom: 4px solid transparent;
-}
-
-.animation-timeline .animation .iterations.infinite::after {
-  bottom: 0;
-  top: unset;
+.animation-timeline .animation .summary path {
+  fill: var(--timeline-background-color);
+  stroke: var(--timeline-border-color);
 }
 
 .animation-timeline .animation .name {
   position: absolute;
   color: var(--theme-selection-color);
   height: 100%;
   display: flex;
   align-items: center;
@@ -468,16 +437,27 @@ body {
 .animation-timeline .animation .end-delay.negative {
   /* Negative delays are displayed on top of the animation, so they need a
      right border. Whereas normal delays are displayed just before the
      animation, so there's already the animation's left border that serves as
      a separation. */
   border-width: 1px;
 }
 
+/* Use for fade mask */
+#animationinspector-fade-gradient stop {
+  /* opaque */
+  stop-color: white;
+}
+
+#animationinspector-fade-gradient stop:nth-child(2) {
+  /* transparent */
+  stop-color: black;
+}
+
 /* Animation target node gutter, contains a preview of the dom node */
 
 .animation-target {
   background-color: var(--theme-toolbar-background);
   padding: 0 4px;
   box-sizing: border-box;
   overflow: hidden;
   text-overflow: ellipsis;
--- a/devtools/server/actors/animation.js
+++ b/devtools/server/actors/animation.js
@@ -233,16 +233,40 @@ var AnimationPlayerActor = protocol.Acto
    * Get the animation iterationStart from this player, in ratio.
    * That is offset of starting position of the animation.
    * @return {Number}
    */
   getIterationStart: function () {
     return this.player.effect.getComputedTiming().iterationStart;
   },
 
+  /**
+   * Get the animation easing from this player.
+   * @return {String}
+   */
+  getEasing: function () {
+    return this.player.effect.timing.easing;
+  },
+
+  /**
+   * Get the animation fill mode from this player.
+   * @return {String}
+   */
+  getFill: function () {
+    return this.player.effect.getComputedTiming().fill;
+  },
+
+  /**
+   * Get the animation direction from this player.
+   * @return {String}
+   */
+  getDirection: function () {
+    return this.player.effect.getComputedTiming().direction;
+  },
+
   getPropertiesCompositorStatus: function () {
     let properties = this.player.effect.getProperties();
     return properties.map(prop => {
       return {
         property: prop.property,
         runningOnCompositor: prop.runningOnCompositor,
         warning: prop.warning
       };
@@ -275,16 +299,19 @@ var AnimationPlayerActor = protocol.Acto
       playState: this.player.playState,
       playbackRate: this.player.playbackRate,
       name: this.getName(),
       duration: this.getDuration(),
       delay: this.getDelay(),
       endDelay: this.getEndDelay(),
       iterationCount: this.getIterationCount(),
       iterationStart: this.getIterationStart(),
+      fill: this.getFill(),
+      easing: this.getEasing(),
+      direction: this.getDirection(),
       // animation is hitting the fast path or not. Returns false whenever the
       // animation is paused as it is taken off the compositor then.
       isRunningOnCompositor:
         this.getPropertiesCompositorStatus()
             .some(propState => propState.runningOnCompositor),
       propertyState: this.getPropertiesCompositorStatus(),
       // The document timeline's currentTime is being sent along too. This is
       // not strictly related to the node's animationPlayer, but is useful to
--- a/devtools/server/tests/browser/animation.html
+++ b/devtools/server/tests/browser/animation.html
@@ -23,28 +23,31 @@
   .multiple-animations {
     display: inline-block;
 
     width: 50px;
     height: 50px;
     border-radius: 50%;
     background: #eee;
 
-    animation: move 200s infinite, glow 100s 5;
+    animation: move 200s infinite , glow 100s 5;
+    animation-timing-function: ease-out;
+    animation-direction: reverse;
+    animation-fill-mode: both;
   }
 
   .transition {
     display: inline-block;
 
     width: 50px;
     height: 50px;
     border-radius: 50%;
     background: #f06;
 
-    transition: width 500s;
+    transition: width 500s ease-out;
   }
   .transition.get-round {
     width: 200px;
   }
 
   .long-animation {
     display: inline-block;
 
--- a/devtools/server/tests/browser/browser_animation_playerState.js
+++ b/devtools/server/tests/browser/browser_animation_playerState.js
@@ -26,71 +26,98 @@ function* playerHasAnInitialState(walker
   ok("currentTime" in player.initialState, "Player's state has currentTime");
   ok("playState" in player.initialState, "Player's state has playState");
   ok("playbackRate" in player.initialState, "Player's state has playbackRate");
   ok("name" in player.initialState, "Player's state has name");
   ok("duration" in player.initialState, "Player's state has duration");
   ok("delay" in player.initialState, "Player's state has delay");
   ok("iterationCount" in player.initialState,
      "Player's state has iterationCount");
+  ok("fill" in player.initialState, "Player's state has fill");
+  ok("easing" in player.initialState, "Player's state has easing");
+  ok("direction" in player.initialState, "Player's state has direction");
   ok("isRunningOnCompositor" in player.initialState,
      "Player's state has isRunningOnCompositor");
   ok("type" in player.initialState, "Player's state has type");
   ok("documentCurrentTime" in player.initialState,
      "Player's state has documentCurrentTime");
 }
 
 function* playerStateIsCorrect(walker, animations) {
   info("Checking the state of the simple animation");
 
-  let state = yield getAnimationStateForNode(walker, animations,
-                                             ".simple-animation", 0);
+  let player = yield getAnimationPlayerForNode(walker, animations,
+                                               ".simple-animation", 0);
+  let state = yield player.getCurrentState();
   is(state.name, "move", "Name is correct");
   is(state.duration, 200000, "Duration is correct");
   // null = infinite count
   is(state.iterationCount, null, "Iteration count is correct");
+  is(state.fill, "none", "Fill is correct");
+  is(state.easing, "linear", "Easing is correct");
+  is(state.direction, "normal", "Direction is correct");
   is(state.playState, "running", "PlayState is correct");
   is(state.playbackRate, 1, "PlaybackRate is correct");
   is(state.type, "cssanimation", "Type is correct");
 
   info("Checking the state of the transition");
 
-  state = yield getAnimationStateForNode(walker, animations, ".transition", 0);
+  player =
+    yield getAnimationPlayerForNode(walker, animations, ".transition", 0);
+  state = yield player.getCurrentState();
   is(state.name, "width", "Transition name matches transition property");
   is(state.duration, 500000, "Transition duration is correct");
   // transitions run only once
   is(state.iterationCount, 1, "Transition iteration count is correct");
+  is(state.fill, "backwards", "Transition fill is correct");
+  is(state.easing, "linear", "Transition easing is correct");
+  is(state.direction, "normal", "Transition direction is correct");
   is(state.playState, "running", "Transition playState is correct");
   is(state.playbackRate, 1, "Transition playbackRate is correct");
   is(state.type, "csstransition", "Transition type is correct");
+  // chech easing in keyframe
+  let keyframes = yield player.getFrames();
+  is(keyframes.length, 2, "Transition length of keyframe is correct");
+  is(keyframes[0].easing,
+     "ease-out", "Transition kerframes's easing is correct");
 
   info("Checking the state of one of multiple animations on a node");
 
   // Checking the 2nd player
-  state = yield getAnimationStateForNode(walker, animations,
-                                         ".multiple-animations", 1);
+  player = yield getAnimationPlayerForNode(walker, animations,
+                                           ".multiple-animations", 1);
+  state = yield player.getCurrentState();
   is(state.name, "glow", "The 2nd animation's name is correct");
   is(state.duration, 100000, "The 2nd animation's duration is correct");
   is(state.iterationCount, 5, "The 2nd animation's iteration count is correct");
+  is(state.fill, "both", "The 2nd animation's fill is correct");
+  is(state.easing, "linear", "The 2nd animation's easing is correct");
+  is(state.direction, "reverse", "The 2nd animation's direction is correct");
   is(state.playState, "running", "The 2nd animation's playState is correct");
   is(state.playbackRate, 1, "The 2nd animation's playbackRate is correct");
+  // chech easing in keyframe
+  keyframes = yield player.getFrames();
+  is(keyframes.length, 2, "The 2nd animation's length of keyframe is correct");
+  is(keyframes[0].easing,
+     "ease-out", "The 2nd animation's easing of kerframes is correct");
 
   info("Checking the state of an animation with delay");
 
-  state = yield getAnimationStateForNode(walker, animations,
-                                         ".delayed-animation", 0);
+  player = yield getAnimationPlayerForNode(walker, animations,
+                                           ".delayed-animation", 0);
+  state = yield player.getCurrentState();
   is(state.delay, 5000, "The animation delay is correct");
 
   info("Checking the state of an transition with delay");
 
-  state = yield getAnimationStateForNode(walker, animations,
-                                         ".delayed-transition", 0);
+  player = yield getAnimationPlayerForNode(walker, animations,
+                                           ".delayed-transition", 0);
+  state = yield player.getCurrentState();
   is(state.delay, 3000, "The transition delay is correct");
 }
 
-function* getAnimationStateForNode(walker, animations, nodeSelector, index) {
+function* getAnimationPlayerForNode(walker, animations, nodeSelector, index) {
   let node = yield walker.querySelector(walker.rootNode, nodeSelector);
   let players = yield animations.getAnimationPlayersForNode(node);
   let player = players[index];
   yield player.ready();
-  let state = yield player.getCurrentState();
-  return state;
+  return player;
 }
--- a/devtools/shared/fronts/animation.js
+++ b/devtools/shared/fronts/animation.js
@@ -60,16 +60,19 @@ const AnimationPlayerFront = FrontClassW
       playState: this._form.playState,
       playbackRate: this._form.playbackRate,
       name: this._form.name,
       duration: this._form.duration,
       delay: this._form.delay,
       endDelay: this._form.endDelay,
       iterationCount: this._form.iterationCount,
       iterationStart: this._form.iterationStart,
+      easing: this._form.easing,
+      fill: this._form.fill,
+      direction: this._form.direction,
       isRunningOnCompositor: this._form.isRunningOnCompositor,
       propertyState: this._form.propertyState,
       documentCurrentTime: this._form.documentCurrentTime
     };
   },
 
   /**
    * Executed when the AnimationPlayerActor emits a "changed" event. Used to