Bug 1210796 - Part 2: Visualize each properties. r=pbro draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Tue, 18 Apr 2017 12:15:54 +0900
changeset 564115 719249dd0df6d4f7671bdb0464ae0c8b426d86c0
parent 564114 2df4cd749dc9ee28e11c17e11d0883a5d50d88b5
child 564116 b758b92e81307c53ba3892cbd3c0a005ef5749e0
push id54524
push userbmo:dakatsuka@mozilla.com
push dateTue, 18 Apr 2017 09:24:06 +0000
reviewerspbro
bugs1210796
milestone55.0a1
Bug 1210796 - Part 2: Visualize each properties. r=pbro MozReview-Commit-ID: Hjb1QyOMNZR
devtools/client/animationinspector/animation-controller.js
devtools/client/animationinspector/components/animation-details.js
devtools/client/animationinspector/components/keyframes.js
devtools/client/animationinspector/graph-helper.js
devtools/client/animationinspector/moz.build
devtools/client/animationinspector/test/head.js
devtools/client/animationinspector/utils.js
devtools/client/shared/test/unit/test_cubicBezier.js
devtools/client/shared/widgets/ColorWidget.js
devtools/client/shared/widgets/CubicBezierWidget.js
devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
devtools/client/themes/animationinspector.css
devtools/server/actors/animation.js
devtools/shared/css/color.js
devtools/shared/specs/animation.js
--- a/devtools/client/animationinspector/animation-controller.js
+++ b/devtools/client/animationinspector/animation-controller.js
@@ -95,16 +95,18 @@ var getServerTraits = Task.async(functio
     { name: "hasSetCurrentTimes", actor: "animations",
       method: "setCurrentTimes" },
     { name: "hasGetFrames", actor: "animationplayer",
       method: "getFrames" },
     { name: "hasGetProperties", actor: "animationplayer",
       method: "getProperties" },
     { name: "hasSetWalkerActor", actor: "animations",
       method: "setWalkerActor" },
+    { name: "hasGetAnimationTypes", actor: "animationplayer",
+      method: "getAnimationTypes" },
   ];
 
   let traits = {};
   for (let {name, actor, method} of config) {
     traits[name] = yield target.actorHasMethod(actor, method);
   }
 
   return traits;
--- a/devtools/client/animationinspector/components/animation-details.js
+++ b/devtools/client/animationinspector/components/animation-details.js
@@ -28,17 +28,18 @@ function AnimationDetails(serverTraits) 
   this.serverTraits = serverTraits;
 }
 
 exports.AnimationDetails = AnimationDetails;
 
 AnimationDetails.prototype = {
   // These are part of frame objects but are not animated properties. This
   // array is used to skip them.
-  NON_PROPERTIES: ["easing", "composite", "computedOffset", "offset"],
+  NON_PROPERTIES: ["easing", "composite", "computedOffset",
+                   "offset", "simulateComputeValuesFailure"],
 
   init: function (containerEl) {
     this.containerEl = containerEl;
   },
 
   destroy: function () {
     this.unrender();
     this.containerEl = null;
@@ -103,43 +104,69 @@ AnimationDetails.prototype = {
      */
     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} of values) {
-          tracks[name].push({value, offset});
+        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;
           }
 
-          if (!tracks[name]) {
-            tracks[name] = [];
+          // We have to change to CSS property name
+          // since GetKeyframes returns JS property name.
+          const propertyCSSName = getCssPropertyName(name);
+          if (!tracks[propertyCSSName]) {
+            tracks[propertyCSSName] = [];
           }
 
-          tracks[name].push({
+          tracks[propertyCSSName].push({
             value: frame[name],
-            offset: frame.computedOffset
+            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) {
+    if (this.serverTraits.hasGetAnimationTypes) {
+      return yield this.animation.getAnimationTypes(propertyNames);
+    }
+    // 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) {
     this.unrender();
 
     if (!animation) {
       return;
     }
     this.animation = animation;
 
@@ -147,18 +174,18 @@ AnimationDetails.prototype = {
     // 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);
 
-    // Useful for tests to know when the keyframes have been retrieved.
-    this.emit("keyframes-retrieved");
+    // Get animation type for each CSS properties.
+    const animationTypes = yield this.getAnimationTypes(Object.keys(this.tracks));
 
     for (let propertyName in this.tracks) {
       let line = createNode({
         parent: this.containerEl,
         attributes: {"class": "property"}
       });
       let {warning, className} =
         this.getPerfDataForProperty(animation, propertyName);
@@ -191,22 +218,26 @@ AnimationDetails.prototype = {
       framesEl.style.left = `${x}%`;
       framesEl.style.width = `${w}%`;
 
       let keyframesComponent = new Keyframes();
       keyframesComponent.init(framesEl);
       keyframesComponent.render({
         keyframes: this.tracks[propertyName],
         propertyName: propertyName,
-        animation: animation
+        animation: animation,
+        animationType: animationTypes[propertyName]
       });
       keyframesComponent.on("frame-selected", this.onFrameSelected);
-
       this.keyframeComponents.push(keyframesComponent);
     }
+
+    // Useful for tests to know when rendering of all animation detail UIs
+    // have been completed.
+    this.emit("animation-detail-rendering-completed");
   }),
 
   onFrameSelected: function (e, args) {
     // Relay the event up, it's needed in parents too.
     this.emit(e, args);
   }
 };
 
--- a/devtools/client/animationinspector/components/keyframes.js
+++ b/devtools/client/animationinspector/components/keyframes.js
@@ -2,20 +2,27 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const EventEmitter = require("devtools/shared/event-emitter");
-const {createNode} = require("devtools/client/animationinspector/utils");
+const {createNode, createSVGNode} =
+  require("devtools/client/animationinspector/utils");
+const {ProgressGraphHelper, appendPathElement, DEFAULT_MIN_PROGRESS_THRESHOLD} =
+         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.
  */
 function Keyframes() {
   EventEmitter.decorate(this);
   this.onClick = this.onClick.bind(this);
 }
 
 exports.Keyframes = Keyframes;
 
@@ -32,26 +39,63 @@ Keyframes.prototype = {
   },
 
   destroy: function () {
     this.containerEl.removeEventListener("click", this.onClick);
     this.keyframesEl.remove();
     this.containerEl = this.keyframesEl = this.animation = null;
   },
 
-  render: function ({keyframes, propertyName, animation}) {
+  render: function ({keyframes, propertyName, animation, animationType}) {
     this.keyframes = keyframes;
     this.propertyName = propertyName;
     this.animation = animation;
 
     let iterationStartOffset =
       animation.state.iterationStart % 1 == 0
       ? 0
       : 1 - animation.state.iterationStart % 1;
 
+    // Create graph element.
+    const graphEl = createSVGNode({
+      parent: this.keyframesEl,
+      nodeType: "svg",
+      attributes: {
+        "preserveAspectRatio": "none"
+      }
+    });
+
+    // This visual is only one iteration,
+    // so we use animation.state.duration as total duration.
+    const totalDuration = animation.state.duration;
+
+    // Calculate stroke height in viewBox to display stroke of path.
+    const strokeHeightForViewBox = 0.5 / this.containerEl.clientHeight;
+    // Minimum segment duration is the duration of one pixel.
+    const minSegmentDuration =
+      totalDuration / this.containerEl.clientWidth;
+
+    // Set viewBox.
+    graphEl.setAttribute("viewBox",
+                         `0 -${ 1 + strokeHeightForViewBox }
+                          ${ totalDuration }
+                          ${ 1 + strokeHeightForViewBox * 2 }`);
+
+    // Create graph helper to render the animation property graph.
+    const graphHelper =
+      new ProgressGraphHelper(this.containerEl.ownerDocument.defaultView,
+                              propertyName, animationType, keyframes, totalDuration);
+
+    renderPropertyGraph(graphEl, totalDuration, minSegmentDuration,
+                        DEFAULT_MIN_PROGRESS_THRESHOLD, 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) {
       let offset = frame.offset + iterationStartOffset;
       createNode({
         parent: this.keyframesEl,
         attributes: {
           "class": "frame",
           "style": `left:${offset * 100}%;`,
@@ -74,8 +118,57 @@ Keyframes.prototype = {
       animation: this.animation,
       propertyName: this.propertyName,
       offset: parseFloat(e.target.dataset.offset),
       value: e.target.getAttribute("title"),
       x: e.target.offsetLeft + e.target.closest(".frames").offsetLeft
     });
   }
 };
+
+/**
+ * Render a graph representing the progress of the animation over one iteration.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Number} duration - Duration of one iteration.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {ProgressGraphHelper} graphHelper - The object of ProgressGraphHalper.
+ */
+function renderPropertyGraph(parentEl, duration, minSegmentDuration,
+                             minProgressThreshold, graphHelper) {
+  const segments = graphHelper.createPathSegments(0, duration, minSegmentDuration,
+                                                  minProgressThreshold);
+
+  const graphType = graphHelper.getGraphType();
+  if (graphType !== "color") {
+    appendPathElement(parentEl, segments, graphType);
+    return;
+  }
+
+  // Append the color to the path.
+  segments.forEach(segment => {
+    segment.y = 1;
+  });
+  const path = appendPathElement(parentEl, segments, graphType);
+  const defEl = createSVGNode({
+    parent: parentEl,
+    nodeType: "def"
+  });
+  const id = `color-property-${ LINEAR_GRADIENT_ID_COUNTER++ }`;
+  const linearGradientEl = createSVGNode({
+    parent: defEl,
+    nodeType: "linearGradient",
+    attributes: {
+      "id": id
+    }
+  });
+  segments.forEach(segment => {
+    createSVGNode({
+      parent: linearGradientEl,
+      nodeType: "stop",
+      attributes: {
+        "stop-color": segment.style,
+        "offset": segment.x / duration
+      }
+    });
+  });
+  path.style.fill = `url(#${ id })`;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/graph-helper.js
@@ -0,0 +1,461 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {createSVGNode, getJsPropertyName} =
+  require("devtools/client/animationinspector/utils");
+const {colorUtils} = require("devtools/shared/css/color.js");
+const {parseTimingFunction} = require("devtools/client/shared/widgets/CubicBezierWidget");
+
+// In the createPathSegments function, an animation duration is divided by
+// DURATION_RESOLUTION in order to draw the way the animation progresses.
+// But depending on the timing-function, we may be not able to make the graph
+// smoothly progress if this resolution is not high enough.
+// So, if the difference of animation progress between 2 divisions is more than
+// DEFAULT_MIN_PROGRESS_THRESHOLD, then createPathSegments re-divides
+// by DURATION_RESOLUTION.
+// DURATION_RESOLUTION shoud be integer and more than 2.
+const DURATION_RESOLUTION = 4;
+// DEFAULT_MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
+const DEFAULT_MIN_PROGRESS_THRESHOLD = 0.1;
+exports.DEFAULT_MIN_PROGRESS_THRESHOLD = DEFAULT_MIN_PROGRESS_THRESHOLD;
+// BOUND_EXCLUDING_TIME should be less than 1ms and is used to exclude start
+// and end bounds when dividing  duration in createPathSegments.
+const BOUND_EXCLUDING_TIME = 0.001;
+
+/**
+ * This helper return the segment coordinates and style for property graph,
+ * also return the graph type.
+ * Parameters of constructor are below.
+ * @param {Window} win - window object to animate.
+ * @param {String} propertyCSSName - CSS property name (e.g. background-color).
+ * @param {String} animationType - Animation type of CSS property.
+ * @param {Object} keyframes - AnimationInspector's keyframes object.
+ * @param {float}  duration - Duration of animation.
+ */
+function ProgressGraphHelper(win, propertyCSSName, animationType, keyframes, duration) {
+  this.win = win;
+  const doc = this.win.document;
+  this.targetEl = doc.createElement("div");
+  doc.documentElement.appendChild(this.targetEl);
+
+  this.propertyCSSName = propertyCSSName;
+  this.propertyJSName = getJsPropertyName(this.propertyCSSName);
+  this.animationType = animationType;
+
+  // Create keyframe object to make dummy animation.
+  const keyframesObject = keyframes.map(keyframe => {
+    const keyframeObject = Object.assign({}, keyframe);
+    keyframeObject[this.propertyJSName] = keyframe.value;
+    return keyframeObject;
+  });
+
+  // Create effect timing object to make dummy animation.
+  const effectTiming = {
+    duration: duration,
+    fill: "forwards"
+  };
+
+  this.keyframes = keyframesObject;
+  this.devtoolsKeyframes = keyframes;
+  this.animation = this.targetEl.animate(this.keyframes, effectTiming);
+  this.animation.pause();
+  this.valueHelperFunction = this.getValueHelperFunction();
+}
+
+ProgressGraphHelper.prototype = {
+  /**
+   * Destory this object.
+   */
+  destroy: function () {
+    this.targetEl.remove();
+    this.animation.cancel();
+
+    this.targetEl = null;
+    this.animation = null;
+    this.valueHelperFunction = null;
+    this.propertyCSSName = null;
+    this.propertyJSName = null;
+    this.animationType = null;
+    this.keyframes = null;
+    this.win = null;
+  },
+
+  /**
+   * Return graph type.
+   * @return {String} if property is 'opacity' or 'transform', return that value.
+   *                  Otherwise, return given animation type in constructor.
+   */
+  getGraphType: function () {
+    return (this.propertyJSName === "opacity" || this.propertyJSName === "transform")
+           ? this.propertyJSName : this.animationType;
+  },
+
+  /**
+   * 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)
+   * - style: the computed style value of the property at the time
+   */
+  getSegment: function (time) {
+    this.animation.currentTime = time;
+    const style = this.win.getComputedStyle(this.targetEl)[this.propertyJSName];
+    const value = this.valueHelperFunction(style);
+    return { x: time, y: value, style: style };
+  },
+
+  /**
+   * Get a value helper function which calculates the value of Y axis by animation type.
+   * @return {function} ValueHelperFunction returns float value of Y axis
+   *                    from given progress and style (e.g. rgb(0, 0, 0))
+   */
+  getValueHelperFunction: function () {
+    switch (this.animationType) {
+      case "none": {
+        return () => 1;
+      }
+      case "float": {
+        return this.getFloatValueHelperFunction();
+      }
+      case "coord": {
+        return this.getCoordinateValueHelperFunction();
+      }
+      case "color": {
+        return this.getColorValueHelperFunction();
+      }
+      case "discrete": {
+        return this.getDiscreteValueHelperFunction();
+      }
+    }
+    return null;
+  },
+
+  /**
+   * Return value helper function of animation type 'float'.
+   * @param {Object} keyframes - This object shoud be same as
+   *                             the parameter of getGraphHelper.
+   * @return {function} ValueHelperFunction returns float value of Y axis
+   *                    from given float (e.g. 1.0, 0.5 and so on)
+   */
+  getFloatValueHelperFunction: function () {
+    let maxValue = 0;
+    let minValue = Infinity;
+    this.keyframes.forEach(keyframe => {
+      maxValue = Math.max(maxValue, keyframe.value);
+      minValue = Math.min(minValue, keyframe.value);
+    });
+    const distance = maxValue - minValue;
+    return value => {
+      return (value - minValue) / distance;
+    };
+  },
+
+  /**
+   * Return value helper function of animation type 'coord'.
+   * @return {function} ValueHelperFunction returns float value of Y axis
+   *                    from given style (e.g. 100px)
+   */
+  getCoordinateValueHelperFunction: function () {
+    let maxValue = 0;
+    let minValue = Infinity;
+    for (let i = 0, n = this.keyframes.length; i < n; i++) {
+      if (this.keyframes[i].value.match(/calc/)) {
+        return null;
+      }
+      const value = parseFloat(this.keyframes[i].value);
+      minValue = Math.min(minValue, value);
+      maxValue = Math.max(maxValue, value);
+    }
+    const distance = maxValue - minValue;
+    return value => {
+      return (parseFloat(value) - minValue) / distance;
+    };
+  },
+
+  /**
+   * Return value helper function of animation type 'color'.
+   * @param {Object} keyframes - This object shoud be same as
+   *                             the parameter of getGraphHelper.
+   * @return {function} ValueHelperFunction returns float value of Y axis
+   *                    from given color (e.g. rgb(0, 0, 0))
+   */
+  getColorValueHelperFunction: function () {
+    const maxObject = { distance: 0 };
+    for (let i = 0; i < this.keyframes.length - 1; i++) {
+      const value1 = getRGBA(this.keyframes[i].value);
+      for (let j = i + 1; j < this.keyframes.length; j++) {
+        const value2 = getRGBA(this.keyframes[j].value);
+        const distance = getRGBADistance(value1, value2);
+        if (maxObject.distance >= distance) {
+          continue;
+        }
+        maxObject.distance = distance;
+        maxObject.value1 = value1;
+        maxObject.value2 = value2;
+      }
+    }
+    const baseValue =
+      maxObject.value1 < maxObject.value2 ? maxObject.value1 : maxObject.value2;
+
+    return value => {
+      const colorValue = getRGBA(value);
+      return getRGBADistance(baseValue, colorValue) / maxObject.distance;
+    };
+  },
+
+  /**
+   * Return value helper function of animation type 'discrete'.
+   * @return {function} ValueHelperFunction returns float value of Y axis
+   *                    from given style (e.g. center)
+   */
+  getDiscreteValueHelperFunction: function () {
+    const discreteValues = [];
+    this.keyframes.forEach(keyframe => {
+      if (!discreteValues.includes(keyframe.value)) {
+        discreteValues.push(keyframe.value);
+      }
+    });
+    return value => {
+      return discreteValues.indexOf(value) / (discreteValues.length - 1);
+    };
+  },
+
+  /**
+   * 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,
+                                minSegmentDuration, minProgressThreshold) {
+    return !this.valueHelperFunction
+           ? createKeyframesPathSegments(endTime - startTime, this.devtoolsKeyframes)
+           : createPathSegments(startTime, endTime,
+                                minSegmentDuration, minProgressThreshold, this);
+  },
+};
+
+exports.ProgressGraphHelper = ProgressGraphHelper;
+
+/**
+ * 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.
+ * @return {Array} path segments -
+ *                 [{x: {Number} time, y: {Number} progress}, ...]
+ */
+function createPathSegments(startTime, endTime, minSegmentDuration,
+                            minProgressThreshold, segmentHelper) {
+  // If the duration is too short, early return.
+  if (endTime - startTime < minSegmentDuration) {
+    return [segmentHelper.getSegment(startTime),
+            segmentHelper.getSegment(endTime)];
+  }
+
+  // Otherwise, start creating segments.
+  let pathSegments = [];
+
+  // Append the segment for the startTime position.
+  const startTimeSegment = segmentHelper.getSegment(startTime);
+  pathSegments.push(startTimeSegment);
+  let previousSegment = startTimeSegment;
+
+  // Split the duration in equal intervals, and iterate over them.
+  // See the definition of DURATION_RESOLUTION for more information about this.
+  const interval = (endTime - startTime) / DURATION_RESOLUTION;
+  for (let index = 1; index <= DURATION_RESOLUTION; index++) {
+    // Create a segment for this interval.
+    const currentSegment =
+      segmentHelper.getSegment(startTime + index * interval);
+
+    // If the distance between the Y coordinate (the animation's progress) of
+    // the previous segment and the Y coordinate of the current segment is too
+    // large, then recurse with a smaller duration to get more details
+    // in the graph.
+    if (Math.abs(currentSegment.y - previousSegment.y) > minProgressThreshold) {
+      // Divide the current interval (excluding start and end bounds
+      // by adding/subtracting BOUND_EXCLUDING_TIME).
+      pathSegments = pathSegments.concat(
+        createPathSegments(previousSegment.x + BOUND_EXCLUDING_TIME,
+                           currentSegment.x - BOUND_EXCLUDING_TIME,
+                           minSegmentDuration, minProgressThreshold,
+                           segmentHelper));
+    }
+
+    pathSegments.push(currentSegment);
+    previousSegment = currentSegment;
+  }
+
+  return pathSegments;
+}
+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.
+ * @return {Element} path element.
+ */
+function appendPathElement(parentEl, pathSegments, cls) {
+  // Create path string.
+  let path = `M${ pathSegments[0].x },0`;
+  for (let i = 0; i < pathSegments.length; i++) {
+    const pathSegment = pathSegments[i];
+    if (!pathSegment.easing || pathSegment.easing === "linear") {
+      path += createLinePathString(pathSegment);
+      continue;
+    }
+
+    if (i + 1 === pathSegments.length) {
+      // We already create steps or cubic-bezier path string in previous.
+      break;
+    }
+
+    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`;
+  // Append and return the path element.
+  return createSVGNode({
+    parent: parentEl,
+    nodeType: "path",
+    attributes: {
+      "d": path,
+      "class": cls,
+      "vector-effect": "non-scaling-stroke",
+      "transform": "scale(1, -1)"
+    }
+  });
+}
+exports.appendPathElement = appendPathElement;
+
+/**
+ * Create the path segments from given keyframes.
+ * @param {Number} duration - Duration of animation.
+ * @param {Object} Keyframes of devtool's format.
+ * @return {Array} path segments -
+ *                 [{x: {Number} time, y: {Number} distance,
+ *                  easing: {String} keyframe's easing,
+ *                  style: {String} keyframe's value}, ...]
+ */
+function createKeyframesPathSegments(duration, keyframes) {
+  return keyframes.map(keyframe => {
+    return {
+      x: keyframe.offset * duration,
+      y: keyframe.distance,
+      easing: keyframe.easing,
+      style: keyframe.value
+    };
+  });
+}
+
+/**
+ * Create a line path string.
+ * @param {Object} segment - e.g. { x: 100, y: 1 }
+ * @return {String} path string - e.g. "L100,1"
+ */
+function createLinePathString(segment) {
+  return ` L${ segment.x },${ segment.y }`;
+}
+
+/**
+ * Create a path string to represents a step function.
+ * @param {Object} currentSegment - e.g. { x: 0, y: 0, easing: "steps(2)" }
+ * @param {Object} nextSegment - e.g. { x: 1, y: 1 }
+ * @return {String} path string - e.g. "C 0.25 0.1, 0.25 1, 1 1"
+ */
+function createStepsPathString(currentSegment, nextSegment) {
+  const matches =
+    currentSegment.easing.match(/^steps\((\d+)(,\sstart)?\)/);
+  const stepNumber = parseInt(matches[1], 10);
+  const oneStepX = (nextSegment.x - currentSegment.x) / stepNumber;
+  const oneStepY = (nextSegment.y - currentSegment.y) / stepNumber;
+  const isStepStart = matches[2];
+  const stepOffsetY = isStepStart ? 1 : 0;
+  let path = "";
+  for (let step = 0; step < stepNumber; step++) {
+    const sx = currentSegment.x + step * oneStepX;
+    const ex = sx + oneStepX;
+    const y = currentSegment.y + (step + stepOffsetY) * oneStepY;
+    path += ` L${ sx },${ y } L${ ex },${ y }`;
+  }
+  if (!isStepStart) {
+    path += ` L${ nextSegment.x },${ nextSegment.y }`;
+  }
+  return path;
+}
+
+/**
+ * Create a path string to represents a bezier curve.
+ * @param {Object} currentSegment - e.g. { x: 0, y: 0, easing: "ease" }
+ * @param {Object} nextSegment - e.g. { x: 1, y: 1 }
+ * @return {String} path string - e.g. "C 0.25 0.1, 0.25 1, 1 1"
+ */
+function createCubicBezierPathString(currentSegment, nextSegment) {
+  const controlPoints = parseTimingFunction(currentSegment.easing);
+  if (!controlPoints) {
+    // Just return line path string since we could not parse this easing.
+    return createLinePathString(currentSegment);
+  }
+
+  const cp1x = controlPoints[0];
+  const cp1y = controlPoints[1];
+  const cp2x = controlPoints[2];
+  const cp2y = controlPoints[3];
+
+  const diffX = nextSegment.x - currentSegment.x;
+  const diffY = nextSegment.y - currentSegment.y;
+  let path =
+    ` C ${ currentSegment.x + diffX * cp1x } ${ currentSegment.y + diffY * cp1y }`;
+  path += `, ${ currentSegment.x + diffX * cp2x } ${ currentSegment.y + diffY * cp2y }`;
+  path += `, ${ nextSegment.x } ${ nextSegment.y }`;
+  return path;
+}
+
+/**
+ * Parse given RGBA string.
+ * @param {String} colorString - e.g. rgb(0, 0, 0) or rgba(0, 0, 0, 0.5) and so on.
+ * @return {Object} RGBA {r: r, g: g, b: b, a: a}.
+ */
+function getRGBA(colorString) {
+  const color = new colorUtils.CssColor(colorString);
+  return color.getRGBATuple();
+}
+
+/**
+ * Return the distance from give two RGBA.
+ * @param {Object} rgba1 - RGBA (format is same to getRGBA)
+ * @param {Object} rgba2 - RGBA (format is same to getRGBA)
+ * @return {float} distance.
+ */
+function getRGBADistance(rgba1, rgba2) {
+  const startA = rgba1.a;
+  const startR = rgba1.r * startA;
+  const startG = rgba1.g * startA;
+  const startB = rgba1.b * startA;
+  const endA = rgba2.a;
+  const endR = rgba2.r * endA;
+  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);
+}
--- a/devtools/client/animationinspector/moz.build
+++ b/devtools/client/animationinspector/moz.build
@@ -7,13 +7,14 @@
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 
 DIRS += [
     'components'
 ]
 
 DevToolsModules(
-    'utils.js',
+    'graph-helper.js',
+    'utils.js'
 )
 
-with Files('**'):
-    BUG_COMPONENT = ('Firefox', 'Developer Tools: Animation Inspector')
+with Files('**'):
+    BUG_COMPONENT = ('Firefox', 'Developer Tools: Animation Inspector')
--- a/devtools/client/animationinspector/test/head.js
+++ b/devtools/client/animationinspector/test/head.js
@@ -376,21 +376,21 @@ function disableHighlighter(toolbox) {
 function* clickOnAnimation(panel, index, shouldClose) {
   let timeline = panel.animationsTimelineComponent;
 
   // Expect a selection event.
   let onSelectionChanged = timeline.once(shouldClose
                                          ? "animation-unselected"
                                          : "animation-selected");
 
-  // If we're opening the animation, also wait for the keyframes-retrieved
-  // event.
+  // If we're opening the animation, also wait for
+  // the animation-detail-rendering-completed event.
   let onReady = shouldClose
                 ? Promise.resolve()
-                : timeline.details[index].once("keyframes-retrieved");
+                : timeline.details[index].once("animation-detail-rendering-completed");
 
   info("Click on animation " + index + " in the timeline");
   let timeBlock = timeline.rootWrapperEl.querySelectorAll(".time-block")[index];
   EventUtils.sendMouseEvent({type: "click"}, timeBlock,
                             timeBlock.ownerDocument.defaultView);
 
   yield onReady;
   return yield onSelectionChanged;
--- a/devtools/client/animationinspector/utils.js
+++ b/devtools/client/animationinspector/utils.js
@@ -15,16 +15,19 @@ const L10N =
 // How many times, maximum, can we loop before we find the optimal time
 // interval in the timeline graph.
 const OPTIMAL_TIME_INTERVAL_MAX_ITERS = 100;
 // Time graduations should be multiple of one of these number.
 const OPTIMAL_TIME_INTERVAL_MULTIPLES = [1, 2.5, 5];
 
 const MILLIS_TIME_FORMAT_MAX_DURATION = 4000;
 
+// SVG namespace
+const SVG_NS = "http://www.w3.org/2000/svg";
+
 /**
  * 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.
@@ -53,16 +56,32 @@ function createNode(options) {
 
   options.parent.appendChild(node);
   return node;
 }
 
 exports.createNode = createNode;
 
 /**
+ * SVG 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.
+ * @return {DOMNode} The newly created node.
+ */
+function createSVGNode(options) {
+  options.namespace = SVG_NS;
+  return createNode(options);
+}
+exports.createSVGNode = createSVGNode;
+
+/**
  * Find the optimal interval between time graduations in the animation timeline
  * graph based on a minimum time interval
  * @param {Number} minTimeInterval Minimum time in ms in one interval
  * @return {Number} The optimal interval time in ms
  */
 function findOptimalTimeInterval(minTimeInterval) {
   let numIters = 0;
   let multiplier = 1;
@@ -268,8 +287,24 @@ var TimeScale = {
                                  : x + iterationW;
 
     return {x, w, iterationW, delayX, delayW, negativeDelayW,
             endDelayX, endDelayW};
   }
 };
 
 exports.TimeScale = TimeScale;
+
+/**
+ * Convert given CSS property name to JavaScript CSS name.
+ * @param {String} CSS property name (e.g. background-color).
+ * @return {String} JavaScript CSS property name (e.g. backgroundColor).
+ */
+function getJsPropertyName(cssPropertyName) {
+  if (cssPropertyName == "float") {
+    return "cssFloat";
+  }
+  // https://drafts.csswg.org/cssom/#css-property-to-idl-attribute
+  return cssPropertyName.replace(/-([a-z])/gi, (str, group) => {
+    return group.toUpperCase();
+  });
+}
+exports.getJsPropertyName = getJsPropertyName;
--- a/devtools/client/shared/test/unit/test_cubicBezier.js
+++ b/devtools/client/shared/test/unit/test_cubicBezier.js
@@ -4,17 +4,17 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Tests the CubicBezier API in the CubicBezierWidget module
 
 var Cu = Components.utils;
 var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
-var {CubicBezier, _parseTimingFunction} = require("devtools/client/shared/widgets/CubicBezierWidget");
+var {CubicBezier, parseTimingFunction} = require("devtools/client/shared/widgets/CubicBezierWidget");
 
 function run_test() {
   throwsWhenMissingCoordinates();
   throwsWhenIncorrectCoordinates();
   convertsStringCoordinates();
   coordinatesToStringOutputsAString();
   pointGettersReturnPointCoordinatesArrays();
   toStringOutputsCubicBezierValue();
@@ -107,32 +107,32 @@ function toStringOutputsCssPresetValues(
   c = new CubicBezier([0.42, 0, 0.58, 1]);
   do_check_eq(c.toString(), "ease-in-out");
 }
 
 function testParseTimingFunction() {
   do_print("test parseTimingFunction");
 
   for (let test of ["ease", "linear", "ease-in", "ease-out", "ease-in-out"]) {
-    ok(_parseTimingFunction(test), test);
+    ok(parseTimingFunction(test), test);
   }
 
-  ok(!_parseTimingFunction("something"), "non-function token");
-  ok(!_parseTimingFunction("something()"), "non-cubic-bezier function");
-  ok(!_parseTimingFunction("cubic-bezier(something)",
+  ok(!parseTimingFunction("something"), "non-function token");
+  ok(!parseTimingFunction("something()"), "non-cubic-bezier function");
+  ok(!parseTimingFunction("cubic-bezier(something)",
                            "cubic-bezier with non-numeric argument"));
-  ok(!_parseTimingFunction("cubic-bezier(1,2,3:7)",
+  ok(!parseTimingFunction("cubic-bezier(1,2,3:7)",
                            "did not see comma"));
-  ok(!_parseTimingFunction("cubic-bezier(1,2,3,7:",
-                           "did not see close paren"));
-  ok(!_parseTimingFunction("cubic-bezier(1,2", "early EOF after number"));
-  ok(!_parseTimingFunction("cubic-bezier(1,2,", "early EOF after comma"));
-  deepEqual(_parseTimingFunction("cubic-bezier(1,2,3,7)"), [1, 2, 3, 7],
+  ok(!parseTimingFunction("cubic-bezier(1,2,3,7:",
+                          "did not see close paren"));
+  ok(!parseTimingFunction("cubic-bezier(1,2", "early EOF after number"));
+  ok(!parseTimingFunction("cubic-bezier(1,2,", "early EOF after comma"));
+  deepEqual(parseTimingFunction("cubic-bezier(1,2,3,7)"), [1, 2, 3, 7],
             "correct invocation");
-  deepEqual(_parseTimingFunction("cubic-bezier(1,  /* */ 2,3,   7  )"),
+  deepEqual(parseTimingFunction("cubic-bezier(1,  /* */ 2,3,   7  )"),
             [1, 2, 3, 7],
             "correct with comments and whitespace");
 }
 
 function do_check_throws(cb, info) {
   do_print(info);
 
   let hasThrown = false;
--- a/devtools/client/shared/widgets/ColorWidget.js
+++ b/devtools/client/shared/widgets/ColorWidget.js
@@ -457,17 +457,17 @@ ColorWidget.prototype = {
 
   onHexInputChange: function (event) {
     const hex = event.target.value;
     const color = new colorUtils.CssColor(hex, true);
     if (!color.rgba) {
       return;
     }
 
-    const { r, g, b, a } = color._getRGBATuple();
+    const { r, g, b, a } = color.getRGBATuple();
     this.rgb = [r, g, b, a];
     this.updateUI();
     this.onChange();
   },
 
   onRgbaInputChange: function (event) {
     const field = event.target.dataset.id;
     const value = event.target.value.toString();
@@ -526,17 +526,17 @@ ColorWidget.prototype = {
         hsl[2] = value;
         break;
       case "a":
         hsl[3] = Math.min(value, 1);
         break;
     }
 
     const cssString = ColorWidget.hslToCssString(hsl[0], hsl[1], hsl[2], hsl[3]);
-    const { r, g, b, a } = new colorUtils.CssColor(cssString)._getRGBATuple();
+    const { r, g, b, a } = new colorUtils.CssColor(cssString).getRGBATuple();
 
     this.rgb = [r, g, b, a];
 
     this.hsl = hsl;
 
     this.updateUI();
     this.onChange();
   },
--- a/devtools/client/shared/widgets/CubicBezierWidget.js
+++ b/devtools/client/shared/widgets/CubicBezierWidget.js
@@ -868,18 +868,17 @@ function parseTimingFunction(value) {
         token.text !== (i == 3 ? ")" : ",")) {
       return undefined;
     }
   }
 
   return result;
 }
 
-// This is exported for testing.
-exports._parseTimingFunction = parseTimingFunction;
+exports.parseTimingFunction = parseTimingFunction;
 
 /**
  * Removes a class from a node and adds it to another.
  * @param {String} className the class to swap
  * @param {DOMNode} from the node to remove the class from
  * @param {DOMNode} to the node to add the class to
  */
 function swapClassName(className, from, to) {
--- a/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
@@ -189,17 +189,17 @@ SwatchColorPickerTooltip.prototype = Her
 
   _onEyeDropperDone: function () {
     this.eyedropperOpen = false;
     this.activeSwatch = null;
   },
 
   _colorToRgba: function (color) {
     color = new colorUtils.CssColor(color, this.cssColor4);
-    let rgba = color._getRGBATuple();
+    let rgba = color.getRGBATuple();
     return [rgba.r, rgba.g, rgba.b, rgba.a];
   },
 
   _toDefaultType: function (color) {
     let colorObj = new colorUtils.CssColor(color);
     colorObj.setAuthoredUnitFromColor(this._originalColor, this.cssColor4);
     return colorObj.toString();
   },
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -5,16 +5,25 @@
 /* Animation-inspector specific theme variables */
 
 .theme-dark {
   --even-animation-timeline-background-color: rgba(255,255,255,0.03);
   --command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
   --pause-image: url(chrome://devtools/skin/images/pause.svg);
   --rewind-image: url(chrome://devtools/skin/images/rewind.svg);
   --play-image: url(chrome://devtools/skin/images/play.svg);
+  /* The color for animation type 'opacity' */
+  --opacity-border-color: var(--theme-highlight-pink);
+  --opacity-background-color: #df80ff80;
+  /* The color for animation type 'transform' */
+  --transform-border-color: var(--theme-graphs-yellow);
+  --transform-background-color: #d99b2880;
+  /* The color for other animation type */
+  --other-border-color: var(--theme-graphs-bluegrey);
+  --other-background-color: #5e88b080;
 }
 
 .theme-light {
   --even-animation-timeline-background-color: rgba(128,128,128,0.03);
   --command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
   --pause-image: url(chrome://devtools/skin/images/pause.svg);
   --rewind-image: url(chrome://devtools/skin/images/rewind.svg);
   --play-image: url(chrome://devtools/skin/images/play.svg);
@@ -23,16 +32,28 @@
 .theme-firebug {
   --even-animation-timeline-background-color: rgba(128,128,128,0.03);
   --command-pick-image: url(chrome://devtools/skin/images/firebug/command-pick.svg);
   --pause-image: url(chrome://devtools/skin/images/firebug/pause.svg);
   --rewind-image: url(chrome://devtools/skin/images/firebug/rewind.svg);
   --play-image: url(chrome://devtools/skin/images/firebug/play.svg);
 }
 
+.theme-light, .theme-firebug {
+  /* The color for animation type 'opacity' */
+  --opacity-border-color: var(--theme-highlight-pink);
+  --opacity-background-color: #b82ee580;
+  /* The color for animation type 'transform' */
+  --transform-border-color: var(--theme-graphs-orange);
+  --transform-background-color: #efc05280;
+  /* The color for other animation type */
+  --other-border-color: var(--theme-graphs-bluegrey);
+  --other-background-color: #0072ab80;
+}
+
 :root {
   /* How high should toolbars be */
   --toolbar-height: 20px;
   /* How wide should the sidebar be (should be wide enough to contain long
      property names like 'border-bottom-right-radius' without ellipsis) */
   --timeline-sidebar-width: 200px;
   /* How high should animations displayed in the timeline be */
   --timeline-animation-height: 20px;
@@ -577,47 +598,78 @@ body {
 
 /* Keyframes diagram, displayed below the timeline, inside the animation-details
    element. */
 
 .keyframes {
   /* Actual keyframe markers are positioned absolutely within this container and
      their position is relative to its size (we know the offset of each frame
      in percentage) */
-  position: relative;
+  position: absolute;
+  left: 0;
+  top: 0;
   width: 100%;
-  height: 0;
-}
+  height: 100%;
 
-.keyframes.cssanimation {
-  background-color: var(--theme-contrast-background);
-}
-
-.keyframes.csstransition {
-  background-color: var(--theme-highlight-blue);
-}
-
-.keyframes.scriptanimation {
-  background-color: var(--theme-graphs-green);
 }
 
 .keyframes .frame {
   position: absolute;
-  top: 0;
+  top: 50%;
   width: 0;
   height: 0;
   background-color: inherit;
   cursor: pointer;
+  z-index: 1;
 }
 
 .keyframes .frame::before {
   content: "";
   display: block;
   transform:
     translateX(calc(var(--keyframes-marker-size) * -.5))
     /* The extra pixel on the Y axis is so that markers are centered on the
        horizontal line in the keyframes diagram. */
     translateY(calc(var(--keyframes-marker-size) * -.5 + 1px));
   width: var(--keyframes-marker-size);
   height: var(--keyframes-marker-size);
   border-radius: 100%;
+  border: 1px solid var(--theme-highlight-gray);
   background-color: inherit;
 }
+
+.keyframes.cssanimation .frame {
+  background-color: var(--theme-contrast-background);
+}
+
+.keyframes.csstransition .frame {
+  background-color: var(--theme-highlight-blue);
+}
+
+.keyframes.scriptanimation .frame {
+  background-color: var(--theme-graphs-green);
+}
+
+.keyframes svg {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+}
+
+.keyframes svg path {
+  fill: var(--other-background-color);
+  stroke: var(--other-border-color);
+}
+
+/* color of path is decided by the animation type */
+.keyframes svg path.opacity {
+  fill: var(--opacity-background-color);
+  stroke: var(--opacity-border-color);
+}
+
+.keyframes svg path.transform {
+  fill: var(--transform-background-color);
+  stroke: var(--transform-border-color);
+}
+
+.keyframes svg path.color {
+  stroke: none;
+}
--- a/devtools/server/actors/animation.js
+++ b/devtools/server/actors/animation.js
@@ -20,17 +20,17 @@
  *
  * References:
  * - WebAnimation spec:
  *   http://w3c.github.io/web-animations/
  * - WebAnimation WebIDL files:
  *   /dom/webidl/Animation*.webidl
  */
 
-const {Cu} = require("chrome");
+const {Cu, Ci} = require("chrome");
 const promise = require("promise");
 const protocol = require("devtools/shared/protocol");
 const {Actor} = protocol;
 const {animationPlayerSpec, animationsSpec} = require("devtools/shared/specs/animation");
 const events = require("sdk/event/core");
 
 // Types of animations.
 const ANIMATION_TYPES = {
@@ -456,22 +456,132 @@ var AnimationPlayerActor = protocol.Acto
    */
   getFrames: function () {
     return this.player.effect.getKeyframes();
   },
 
   /**
    * Get data about the animated properties of this animation player.
    * @return {Array} Returns a list of animated properties.
-   * Each property contains a list of values and their offsets
+   * Each property contains a list of values, their offsets and distances.
    */
   getProperties: function () {
-    return this.player.effect.getProperties().map(property => {
+    const properties = this.player.effect.getProperties().map(property => {
       return {name: property.property, values: property.values};
     });
+
+    const DOMWindowUtils =
+      this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+          .getInterface(Ci.nsIDOMWindowUtils);
+
+    // Fill missing keyframe with computed value.
+    for (let property of properties) {
+      let underlyingValue = null;
+      // Check only 0% and 100% keyframes.
+      [0, property.values.length - 1].forEach(index => {
+        const values = property.values[index];
+        if (values.value !== undefined) {
+          return;
+        }
+        if (!underlyingValue) {
+          let pseudo = null;
+          let target = this.player.effect.target;
+          if (target.type) {
+            // This target is a pseudo element.
+            pseudo = target.type;
+            target = target.parentElement;
+          }
+          const value =
+            DOMWindowUtils.getUnanimatedComputedStyle(target, pseudo, property.name);
+          const animationType = DOMWindowUtils.getAnimationTypeForLonghand(property.name);
+          underlyingValue = animationType === "float" ? parseFloat(value, 10) : value;
+        }
+        values.value = underlyingValue;
+      });
+    }
+
+    // Calculate the distance.
+    for (let property of properties) {
+      const propertyName = property.name;
+      const maxObject = { distance: -1 };
+      for (let i = 0; i < property.values.length - 1; i++) {
+        const value1 = property.values[i].value;
+        for (let j = i + 1; j < property.values.length; j++) {
+          const value2 = property.values[j].value;
+          const distance = this.getDistance(this.player.effect.target, propertyName,
+                                            value1, value2, DOMWindowUtils);
+          if (maxObject.distance >= distance) {
+            continue;
+          }
+          maxObject.distance = distance;
+          maxObject.value1 = value1;
+          maxObject.value2 = value2;
+        }
+      }
+      if (maxObject.distance === 0) {
+        // Distance is zero means that no values change or can't calculate the distance.
+        // In this case, we use the keyframe offset as the distance.
+        property.values.reduce((previous, current) => {
+          // If the current value is same as previous value, use previous distance.
+          current.distance =
+            current.value === previous.value ? previous.distance : current.offset;
+          return current;
+        }, property.values[0]);
+        continue;
+      }
+      const baseValue =
+        maxObject.value1 < maxObject.value2 ? maxObject.value1 : maxObject.value2;
+      for (let values of property.values) {
+        const value = values.value;
+        const distance = this.getDistance(this.player.effect.target, propertyName,
+                                          baseValue, value, DOMWindowUtils);
+        values.distance = distance / maxObject.distance;
+      }
+    }
+    return properties;
+  },
+
+  /**
+   * Get the animation types for a given list of CSS property names.
+   * @param {Array} propertyNames - CSS property names (e.g. background-color)
+   * @return {Object} Returns animation types (e.g. {"background-color": "rgb(0, 0, 0)"}.
+   */
+  getAnimationTypes: function (propertyNames) {
+    const DOMWindowUtils = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+                               .getInterface(Ci.nsIDOMWindowUtils);
+    const animationTypes = {};
+    for (let propertyName of propertyNames) {
+      animationTypes[propertyName] =
+        DOMWindowUtils.getAnimationTypeForLonghand(propertyName);
+    }
+    return animationTypes;
+  },
+
+  /**
+   * Returns the distance of between value1, value2.
+   * @param {Object} target - dom element
+   * @param {String} propertyName - e.g. transform
+   * @param {String} value1 - e.g. translate(0px)
+   * @param {String} value2 - e.g. translate(10px)
+   * @param {Object} DOMWindowUtils
+   * @param {float} distance
+   */
+  getDistance: function (target, propertyName, value1, value2, DOMWindowUtils) {
+    if (value1 === value2) {
+      return 0;
+    }
+    try {
+      const distance =
+        DOMWindowUtils.computeAnimationDistance(target, propertyName, value1, value2);
+      return distance;
+    } catch (e) {
+      // We can't compute the distance such the 'discrete' animation,
+      // 'auto' keyword and so on.
+      return 0;
+    }
   }
 });
 
 exports.AnimationPlayerActor = AnimationPlayerActor;
 
 /**
  * The Animations actor lists animation players for a given node.
  */
--- a/devtools/shared/css/color.js
+++ b/devtools/shared/css/color.js
@@ -136,29 +136,29 @@ CssColor.prototype = {
       this._setColorUnitUppercase(color);
     }
   },
 
   get hasAlpha() {
     if (!this.valid) {
       return false;
     }
-    return this._getRGBATuple().a !== 1;
+    return this.getRGBATuple().a !== 1;
   },
 
   get valid() {
     return isValidCSSColor(this.authored, this.cssColor4);
   },
 
   /**
    * Return true for all transparent values e.g. rgba(0, 0, 0, 0).
    */
   get transparent() {
     try {
-      let tuple = this._getRGBATuple();
+      let tuple = this.getRGBATuple();
       return !(tuple.r || tuple.g || tuple.b || tuple.a);
     } catch (e) {
       return false;
     }
   },
 
   get specialValue() {
     return SPECIALVALUES.has(this.lowerCased) ? this.authored : null;
@@ -166,17 +166,17 @@ CssColor.prototype = {
 
   get name() {
     let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
     if (invalidOrSpecialValue !== false) {
       return invalidOrSpecialValue;
     }
 
     try {
-      let tuple = this._getRGBATuple();
+      let tuple = this.getRGBATuple();
 
       if (tuple.a !== 1) {
         return this.hex;
       }
       let {r, g, b} = tuple;
       return rgbToColorName(r, g, b);
     } catch (e) {
       return this.hex;
@@ -222,59 +222,59 @@ CssColor.prototype = {
     let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
     if (invalidOrSpecialValue !== false) {
       return invalidOrSpecialValue;
     }
     if (this.hasAlpha) {
       return this.longAlphaHex;
     }
 
-    let tuple = this._getRGBATuple();
+    let tuple = this.getRGBATuple();
     return "#" + ((1 << 24) + (tuple.r << 16) + (tuple.g << 8) +
                   (tuple.b << 0)).toString(16).substr(-6);
   },
 
   get longAlphaHex() {
     let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
     if (invalidOrSpecialValue !== false) {
       return invalidOrSpecialValue;
     }
 
-    let tuple = this._getRGBATuple();
+    let tuple = this.getRGBATuple();
     return "#" + ((1 << 24) + (tuple.r << 16) + (tuple.g << 8) +
                   (tuple.b << 0)).toString(16).substr(-6) +
                   Math.round(tuple.a * 255).toString(16).padEnd(2, "0");
   },
 
   get rgb() {
     let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
     if (invalidOrSpecialValue !== false) {
       return invalidOrSpecialValue;
     }
     if (!this.hasAlpha) {
       if (this.lowerCased.startsWith("rgb(")) {
         // The color is valid and begins with rgb(.
         return this.authored;
       }
-      let tuple = this._getRGBATuple();
+      let tuple = this.getRGBATuple();
       return "rgb(" + tuple.r + ", " + tuple.g + ", " + tuple.b + ")";
     }
     return this.rgba;
   },
 
   get rgba() {
     let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
     if (invalidOrSpecialValue !== false) {
       return invalidOrSpecialValue;
     }
     if (this.lowerCased.startsWith("rgba(")) {
       // The color is valid and begins with rgba(.
       return this.authored;
     }
-    let components = this._getRGBATuple();
+    let components = this.getRGBATuple();
     return "rgba(" + components.r + ", " +
                      components.g + ", " +
                      components.b + ", " +
                      components.a + ")";
   },
 
   get hsl() {
     let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
@@ -296,17 +296,17 @@ CssColor.prototype = {
     if (invalidOrSpecialValue !== false) {
       return invalidOrSpecialValue;
     }
     if (this.lowerCased.startsWith("hsla(")) {
       // The color is valid and begins with hsla(.
       return this.authored;
     }
     if (this.hasAlpha) {
-      let a = this._getRGBATuple().a;
+      let a = this.getRGBATuple().a;
       return this._hsl(a);
     }
     return this._hsl(1);
   },
 
   /**
    * Check whether the current color value is in the special list e.g.
    * transparent or invalid.
@@ -396,17 +396,17 @@ CssColor.prototype = {
 
     return color;
   },
 
   /**
    * Returns a RGBA 4-Tuple representation of a color or transparent as
    * appropriate.
    */
-  _getRGBATuple: function () {
+  getRGBATuple: function () {
     let tuple = colorToRGBA(this.authored, this.cssColor4);
 
     tuple.a = parseFloat(tuple.a.toFixed(1));
 
     return tuple;
   },
 
   /**
@@ -427,17 +427,17 @@ CssColor.prototype = {
   },
 
   _hsl: function (maybeAlpha) {
     if (this.lowerCased.startsWith("hsl(") && maybeAlpha === undefined) {
       // We can use it as-is.
       return this.authored;
     }
 
-    let {r, g, b} = this._getRGBATuple();
+    let {r, g, b} = this.getRGBATuple();
     let [h, s, l] = rgbToHsl([r, g, b]);
     if (maybeAlpha !== undefined) {
       return "hsla(" + h + ", " + s + "%, " + l + "%, " + maybeAlpha + ")";
     }
     return "hsl(" + h + ", " + s + "%, " + l + "%)";
   },
 
   /**
@@ -529,17 +529,17 @@ function setAlpha(colorValue, alpha, use
     throw new Error("Invalid color.");
   }
 
   // If an invalid alpha valid, just set to 1.
   if (!(alpha >= 0 && alpha <= 1)) {
     alpha = 1;
   }
 
-  let { r, g, b } = color._getRGBATuple();
+  let { r, g, b } = color.getRGBATuple();
   return "rgba(" + r + ", " + g + ", " + b + ", " + alpha + ")";
 }
 
 /**
  * Given a color, classify its type as one of the possible color
  * units, as known by |CssColor.colorUnit|.
  *
  * @param  {String} value
--- a/devtools/shared/specs/animation.js
+++ b/devtools/shared/specs/animation.js
@@ -70,16 +70,24 @@ const animationPlayerSpec = generateActo
         frames: RetVal("json")
       }
     },
     getProperties: {
       request: {},
       response: {
         properties: RetVal("array:json")
       }
+    },
+    getAnimationTypes: {
+      request: {
+        propertyNames: Arg(0, "array:string")
+      },
+      response: {
+        animationTypes: RetVal("json")
+      }
     }
   }
 });
 
 exports.animationPlayerSpec = animationPlayerSpec;
 
 const animationsSpec = generateActorSpec({
   typeName: "animations",
@@ -143,9 +151,8 @@ const animationsSpec = generateActorSpec
         rate: Arg(1, "number")
       },
       response: {}
     }
   }
 });
 
 exports.animationsSpec = animationsSpec;
-