Devtools Animations: Add buffer and effect selection to animation timeline

This change allows the animation timeline UI to consume the visual effect
information from the backend models. It pulls these effects into a buffer
and allows each effect to be selected to be displayed in the timeline below.
The scrubber is also disabled for now in this change.

BUG=447083

Review URL: https://codereview.chromium.org/1218433007

git-svn-id: svn://svn.chromium.org/blink/trunk@202665 bbb929c8-8fbe-4397-9dbb-9b2b20218538
diff --git a/LayoutTests/http/tests/inspector/elements-test.js b/LayoutTests/http/tests/inspector/elements-test.js
index 9bda745..1407a0b 100644
--- a/LayoutTests/http/tests/inspector/elements-test.js
+++ b/LayoutTests/http/tests/inspector/elements-test.js
@@ -1005,7 +1005,7 @@
 
 InspectorTest.waitForAnimationAdded = function(callback)
 {
-    InspectorTest.addSniffer(WebInspector.AnimationTimeline.prototype, "_addAnimation", callback);
+    InspectorTest.addSniffer(WebInspector.AnimationTimeline.prototype, "_addAnimationGroup", callback);
 }
 
 InspectorTest.dumpAnimationTimeline = function(timeline)
diff --git a/LayoutTests/inspector/animation/animation-timeline.html b/LayoutTests/inspector/animation/animation-timeline.html
index e3f178f..63e8abd2 100644
--- a/LayoutTests/inspector/animation/animation-timeline.html
+++ b/LayoutTests/inspector/animation/animation-timeline.html
@@ -21,7 +21,6 @@
 <script src="../../http/tests/inspector/inspector-test.js"></script>
 <script src="../../http/tests/inspector/elements-test.js"></script>
 <script>
-
 var player;
 
 function startAnimationWithDelay()
@@ -69,7 +68,7 @@
     WebInspector.AnimationTimeline.prototype.width = function() { return 1000; }
     // Override animation color for testing
     // FIXME: Set animation name of Web Animation instead; not supported yet
-    WebInspector.AnimationUI.prototype._color = function() { return "black"; }
+    WebInspector.AnimationUI.Color = function() { return "black"; }
 
     function step1()
     {
@@ -77,8 +76,10 @@
         InspectorTest.evaluateInPage("startAnimationWithDelay()");
     }
 
-    function step2()
+    function step2(group)
     {
+        timeline._selectAnimationGroup(group);
+        timeline._redraw();
         InspectorTest.addResult(">>>> Animation with start delay only");
         InspectorTest.dumpAnimationTimeline(timeline);
         timeline._reset();
@@ -86,8 +87,10 @@
         InspectorTest.evaluateInPage("startAnimationWithEndDelay()");
     }
 
-    function step3()
+    function step3(group)
     {
+        timeline._selectAnimationGroup(group);
+        timeline._redraw();
         InspectorTest.addResult(">>>> Animation with start and end delay");
         InspectorTest.dumpAnimationTimeline(timeline);
         InspectorTest.addSniffer(WebInspector.AnimationTimeline.prototype, "_cancelAnimation", step4);
@@ -105,8 +108,10 @@
         InspectorTest.evaluateInPage("startAnimationWithStepTiming()");
     }
 
-    function step5()
+    function step5(group)
     {
+        timeline._selectAnimationGroup(group);
+        timeline._redraw();
         InspectorTest.addResult(">>>> Animation with step timing function");
         InspectorTest.dumpAnimationTimeline(timeline);
         timeline._reset();
@@ -114,8 +119,10 @@
         InspectorTest.evaluateInPage("startCSSAnimation()");
     }
 
-    function step6()
+    function step6(group)
     {
+        timeline._selectAnimationGroup(group);
+        timeline._redraw();
         InspectorTest.addResult(">>>> CSS animation started");
         InspectorTest.dumpAnimationTimeline(timeline);
         timeline._reset();
@@ -123,8 +130,10 @@
         InspectorTest.evaluateInPage("startCSSTransition()");
     }
 
-    function step7()
+    function step7(group)
     {
+        timeline._selectAnimationGroup(group);
+        timeline._redraw();
         InspectorTest.addResult(">>>> CSS transition started");
         InspectorTest.dumpAnimationTimeline(timeline);
         InspectorTest.completeTest();
diff --git a/Source/devtools/devtools.gypi b/Source/devtools/devtools.gypi
index 937d660..83fad55 100644
--- a/Source/devtools/devtools.gypi
+++ b/Source/devtools/devtools.gypi
@@ -357,6 +357,7 @@
         'devtools_animation_js_files': [
             'front_end/animation/animationTimeline.css',
             'front_end/animation/AnimationControlPane.js',
+            'front_end/animation/AnimationGroupPreviewUI.js',
             'front_end/animation/AnimationModel.js',
             'front_end/animation/AnimationTimeline.js',
         ],
diff --git a/Source/devtools/front_end/animation/AnimationGroupPreviewUI.js b/Source/devtools/front_end/animation/AnimationGroupPreviewUI.js
new file mode 100644
index 0000000..4c4f12d
--- /dev/null
+++ b/Source/devtools/front_end/animation/AnimationGroupPreviewUI.js
@@ -0,0 +1,53 @@
+// Copyright (c) 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @constructor
+ * @param {!WebInspector.AnimationModel.AnimationGroup} model
+ */
+WebInspector.AnimationGroupPreviewUI = function(model)
+{
+    this._model = model;
+    this.element = createElementWithClass("div", "animation-buffer-preview");
+    this._svg = this.element.createSVGChild("svg");
+    this._svg.setAttribute("width", "100%");
+    this._svg.setAttribute("preserveAspectRatio", "none");
+    this._svg.setAttribute("height", "100%");
+    this._svg.setAttribute("viewBox", "0 0 100 27");
+    this._svg.setAttribute("shape-rendering", "crispEdges");
+    this._render();
+}
+
+WebInspector.AnimationGroupPreviewUI.prototype = {
+    /**
+     * @return {number}
+     */
+    _groupDuration: function()
+    {
+        var duration = 0;
+        for (var anim of this._model.animations()) {
+            var animDuration = anim.source().delay() + anim.source().duration();
+            if (animDuration > duration)
+                duration = animDuration;
+        }
+        return duration;
+    },
+
+    _render: function()
+    {
+        this._svg.removeChildren();
+        const numberOfAnimations = Math.min(this._model.animations().length, 10);
+        var timeToPixelRatio = 100 / Math.max(this._groupDuration(), 300);
+        for (var i = 0; i < numberOfAnimations; i++) {
+            var effect = this._model.animations()[i].source();
+            var line = this._svg.createSVGChild("line");
+            line.setAttribute("x1", effect.delay() * timeToPixelRatio);
+            line.setAttribute("x2", (effect.delay() + effect.duration()) * timeToPixelRatio);
+            var y = Math.floor(27 / numberOfAnimations * i) + 1;
+            line.setAttribute("y1", y);
+            line.setAttribute("y2", y);
+            line.style.stroke = WebInspector.AnimationUI.Color(this._model.animations()[i]);
+        }
+    }
+}
\ No newline at end of file
diff --git a/Source/devtools/front_end/animation/AnimationModel.js b/Source/devtools/front_end/animation/AnimationModel.js
index 09e30fd..b692148 100644
--- a/Source/devtools/front_end/animation/AnimationModel.js
+++ b/Source/devtools/front_end/animation/AnimationModel.js
@@ -22,7 +22,7 @@
 }
 
 WebInspector.AnimationModel.Events = {
-    AnimationCreated: "AnimationCreated",
+    AnimationGroupStarted: "AnimationGroupStarted",
     AnimationCanceled: "AnimationCanceled"
 }
 
@@ -51,9 +51,7 @@
         while (this._pendingAnimations.length) {
             var group = this._createGroupFromPendingAnimations();
             this._animationGroups.set(group.id(), group);
-            // TODO(samli): Dispatch single group event.
-            for (var anim of group.animations())
-                this.dispatchEventToListeners(WebInspector.AnimationModel.Events.AnimationCreated, { "player": anim, "resetTimeline": anim.id() === group.id() });
+            this.dispatchEventToListeners(WebInspector.AnimationModel.Events.AnimationGroupStarted, group);
         }
     },
 
@@ -519,6 +517,14 @@
         return this._animations;
     },
 
+    /**
+     * @return {number}
+     */
+    startTime: function()
+    {
+        return this._animations[0].startTime();
+    },
+
     __proto__: WebInspector.SDKObject.prototype
 }
 
diff --git a/Source/devtools/front_end/animation/AnimationTimeline.js b/Source/devtools/front_end/animation/AnimationTimeline.js
index 1691f8c..419b42c 100644
--- a/Source/devtools/front_end/animation/AnimationTimeline.js
+++ b/Source/devtools/front_end/animation/AnimationTimeline.js
@@ -31,6 +31,10 @@
     this._timelineControlsWidth = 230;
     /** @type {!Map.<!DOMAgent.BackendNodeId, !WebInspector.AnimationTimeline.NodeUI>} */
     this._nodesMap = new Map();
+    this._groupBuffer = [];
+    this._groupBufferSize = 8;
+    /** @type {!Map.<!WebInspector.AnimationModel.AnimationGroup, !WebInspector.AnimationGroupPreviewUI>} */
+    this._previewMap = new Map();
     this._symbol = Symbol("animationTimeline");
     /** @type {!Map.<string, !WebInspector.AnimationModel.Animation>} */
     this._animationsMap = new Map();
@@ -81,7 +85,7 @@
     {
         var animationModel = WebInspector.AnimationModel.fromTarget(target);
         animationModel.ensureEnabled();
-        animationModel.addEventListener(WebInspector.AnimationModel.Events.AnimationCreated, this._animationCreated, this);
+        animationModel.addEventListener(WebInspector.AnimationModel.Events.AnimationGroupStarted, this._animationGroupStarted, this);
         animationModel.addEventListener(WebInspector.AnimationModel.Events.AnimationCanceled, this._animationCanceled, this);
     },
 
@@ -91,7 +95,7 @@
     _removeEventListeners: function(target)
     {
         var animationModel = WebInspector.AnimationModel.fromTarget(target);
-        animationModel.removeEventListener(WebInspector.AnimationModel.Events.AnimationCreated, this._animationCreated, this);
+        animationModel.removeEventListener(WebInspector.AnimationModel.Events.AnimationGroupStarted, this._animationGroupStarted, this);
         animationModel.removeEventListener(WebInspector.AnimationModel.Events.AnimationCanceled, this._animationCanceled, this);
     },
 
@@ -136,7 +140,7 @@
 
         var container = createElementWithClass("div", "animation-timeline-header");
         var controls = container.createChild("div", "animation-controls");
-        container.createChild("div", "animation-timeline-markers");
+        this._previewContainer = container.createChild("div", "animation-timeline-buffer");
 
         var toolbar = new WebInspector.Toolbar(controls);
         toolbar.element.classList.add("animation-controls-toolbar");
@@ -334,21 +338,82 @@
         delete this._scrubberPlayer;
         this._timelineScrubberHead.textContent = WebInspector.UIString(Number.millisToString(0));
         this._updateControlButton();
+        this._groupBuffer = [];
+        this._previewMap.clear();
+        this._previewContainer.removeChildren();
     },
 
     /**
      * @param {!WebInspector.Event} event
      */
-    _animationCreated: function(event)
+    _animationGroupStarted: function(event)
     {
-        this._addAnimation(/** @type {!WebInspector.AnimationModel.Animation} */ (event.data.player), event.data.resetTimeline)
+        this._addAnimationGroup(/** @type {!WebInspector.AnimationModel.AnimationGroup} */(event.data));
+    },
+
+    /**
+     * @param {!WebInspector.AnimationModel.AnimationGroup} group
+     */
+    _addAnimationGroup: function(group)
+    {
+        /**
+         * @param {!WebInspector.AnimationModel.AnimationGroup} left
+         * @param {!WebInspector.AnimationModel.AnimationGroup} right
+         */
+        function startTimeComparator(left, right)
+        {
+            return left.startTime() > right.startTime();
+        }
+
+        this._groupBuffer.push(group);
+        this._groupBuffer.sort(startTimeComparator);
+        // Discard oldest groups from buffer if necessary
+        var groupsToDiscard = [];
+        while (this._groupBuffer.length > this._groupBufferSize) {
+            var toDiscard = this._groupBuffer.splice(this._groupBuffer[0] === this._selectedGroup ? 1 : 0, 1);
+            groupsToDiscard.push(toDiscard[0]);
+        }
+        for (var g of groupsToDiscard) {
+            this._previewMap.get(g).element.remove();
+            this._previewMap.delete(g);
+            // TODO(samli): needs to discard model too
+        }
+        // Generate preview
+        var preview = new WebInspector.AnimationGroupPreviewUI(group);
+        this._previewMap.set(group, preview);
+        this._previewContainer.appendChild(preview.element);
+        preview.element.addEventListener("click", this._selectAnimationGroup.bind(this, group));
+    },
+
+    /**
+     * @param {!WebInspector.AnimationModel.AnimationGroup} group
+     */
+    _selectAnimationGroup: function(group)
+    {
+        /**
+         * @param {!WebInspector.AnimationGroupPreviewUI} ui
+         * @param {!WebInspector.AnimationModel.AnimationGroup} group
+         * @this {!WebInspector.AnimationTimeline}
+         */
+        function applySelectionClass(ui, group)
+        {
+            ui.element.classList.toggle("selected", this._selectedGroup === group);
+        }
+
+        if (this._selectedGroup === group)
+            return;
+        this._selectedGroup = group;
+        this._previewMap.forEach(applySelectionClass, this);
+        this._reset();
+        for (var anim of group.animations())
+            this._addAnimation(anim);
+        this.scheduleRedraw();
     },
 
     /**
      * @param {!WebInspector.AnimationModel.Animation} animation
-     * @param {boolean} resetTimeline
      */
-    _addAnimation: function(animation, resetTimeline)
+    _addAnimation: function(animation)
     {
         /**
          * @param {?WebInspector.DOMNode} node
@@ -367,15 +432,11 @@
             delete this._emptyTimelineMessage;
         }
 
-        if (resetTimeline)
-            this._reset();
-
         // Ignore Web Animations custom effects & groups
         if (animation.type() === "WebAnimation" && animation.source().keyframesRule().keyframes().length === 0)
             return;
 
-        if (this._resizeWindow(animation))
-            this.scheduleRedraw();
+        this._resizeWindow(animation);
 
         var nodeUI = this._nodesMap.get(animation.source().backendNodeId());
         if (!nodeUI) {
@@ -441,7 +502,7 @@
                 lastDraw = gridWidth;
                 var label = this._grid.createSVGChild("text", "animation-timeline-grid-label");
                 label.setAttribute("x", gridWidth + 5);
-                label.setAttribute("y", 35);
+                label.setAttribute("y", 15);
                 label.textContent = WebInspector.UIString(Number.millisToString(time));
             }
         }
@@ -605,7 +666,8 @@
  * @constructor
  * @param {!WebInspector.AnimationModel.AnimationEffect} animationEffect
  */
-WebInspector.AnimationTimeline.NodeUI = function(animationEffect) {
+WebInspector.AnimationTimeline.NodeUI = function(animationEffect)
+{
     /**
      * @param {?WebInspector.DOMNode} node
      * @this {WebInspector.AnimationTimeline.NodeUI}
@@ -740,7 +802,7 @@
     this._cachedElements = [];
 
     this._movementInMs = 0;
-    this.redraw();
+    this._color = WebInspector.AnimationUI.Color(this._animation);
 }
 
 /**
@@ -780,7 +842,7 @@
         line.setAttribute("x1", WebInspector.AnimationUI.Options.AnimationMargin);
         line.setAttribute("y1", WebInspector.AnimationUI.Options.AnimationHeight);
         line.setAttribute("y2", WebInspector.AnimationUI.Options.AnimationHeight);
-        line.style.stroke = this._color();
+        line.style.stroke = this._color;
         return line;
     },
 
@@ -830,11 +892,11 @@
         var circle = parentElement.createSVGChild("circle", keyframeIndex <= 0 ? "animation-endpoint" : "animation-keyframe-point");
         circle.setAttribute("cx", x.toFixed(2));
         circle.setAttribute("cy", WebInspector.AnimationUI.Options.AnimationHeight);
-        circle.style.stroke = this._color();
+        circle.style.stroke = this._color;
         circle.setAttribute("r", WebInspector.AnimationUI.Options.AnimationMargin / 2);
 
         if (keyframeIndex <= 0)
-            circle.style.fill = this._color();
+            circle.style.fill = this._color;
 
         this._cachedElements[iteration].keyframePoints[keyframeIndex] = circle;
 
@@ -883,7 +945,7 @@
         group.style.transform = "translateX(" + leftDistance.toFixed(2) + "px)";
 
         if (bezier) {
-            group.style.fill = this._color();
+            group.style.fill = this._color;
             WebInspector.BezierUI.drawVelocityChart(bezier, group, width);
         } else {
             var stepFunction = WebInspector.AnimationTimeline.StepTimingFunction.parse(easing);
@@ -891,7 +953,7 @@
             const offsetMap = {"start": 0, "middle": 0.5, "end": 1};
             const offsetWeight = offsetMap[stepFunction.stepAtPosition];
             for (var i = 0; i < stepFunction.steps; i++)
-                createStepLine(group, (i + offsetWeight) * width / stepFunction.steps, this._color());
+                createStepLine(group, (i + offsetWeight) * width / stepFunction.steps, this._color);
         }
     },
 
@@ -1050,7 +1112,8 @@
             this._setDelay(delay);
             this._setDuration(duration);
             if (this._animation.type() !== "CSSAnimation") {
-                for (var target of WebInspector.targetManager.targets(WebInspector.Target.Type.Page))
+                var target = WebInspector.targetManager.mainTarget();
+                if (target)
                     target.animationAgent().setTiming(this._animation.id(), duration, delay);
             }
         }
@@ -1116,31 +1179,6 @@
             style = style.replace(new RegExp("\\s*(-webkit-)?" + name + ":[^;]*;?\\s*", "g"), "");
         var valueString = name + ": " + value;
         this._node.setAttributeValue("style", style + " " + valueString + "; -webkit-" + valueString + ";");
-    },
-
-    /**
-     * @return {string}
-     */
-    _color: function()
-    {
-        /**
-         * @param {string} string
-         * @return {number}
-         */
-        function hash(string)
-        {
-            var hash = 0;
-            for (var i = 0; i < string.length; i++)
-                hash = (hash << 5) + hash + string.charCodeAt(i);
-            return Math.abs(hash);
-        }
-
-        if (!this._selectedColor) {
-            var names = Object.keys(WebInspector.AnimationUI.Colors);
-            var color = WebInspector.AnimationUI.Colors[names[hash(this._animation.name() || this._animation.id()) % names.length]];
-            this._selectedColor = color.asString(WebInspector.Color.Format.RGB);
-        }
-        return this._selectedColor;
     }
 }
 
@@ -1158,8 +1196,33 @@
     "Deep Orange": WebInspector.Color.parse("#FF5722"),
     "Blue": WebInspector.Color.parse("#5677FC"),
     "Lime": WebInspector.Color.parse("#CDDC39"),
+    "Blue Grey": WebInspector.Color.parse("#607D8B"),
     "Pink": WebInspector.Color.parse("#E91E63"),
     "Green": WebInspector.Color.parse("#0F9D58"),
     "Brown": WebInspector.Color.parse("#795548"),
     "Cyan": WebInspector.Color.parse("#00BCD4")
 }
+
+
+/**
+ * @param {!WebInspector.AnimationModel.Animation} animation
+ * @return {string}
+ */
+WebInspector.AnimationUI.Color = function(animation)
+{
+    /**
+     * @param {string} string
+     * @return {number}
+     */
+    function hash(string)
+    {
+        var hash = 0;
+        for (var i = 0; i < string.length; i++)
+            hash = (hash << 5) + hash + string.charCodeAt(i);
+        return Math.abs(hash);
+    }
+
+    var names = Object.keys(WebInspector.AnimationUI.Colors);
+    var color = WebInspector.AnimationUI.Colors[names[hash(animation.name() || animation.id()) % names.length]];
+    return color.asString(WebInspector.Color.Format.RGB);
+}
diff --git a/Source/devtools/front_end/animation/animationTimeline.css b/Source/devtools/front_end/animation/animationTimeline.css
index 27efcb0..8fd88a8 100644
--- a/Source/devtools/front_end/animation/animationTimeline.css
+++ b/Source/devtools/front_end/animation/animationTimeline.css
@@ -10,6 +10,14 @@
     border-bottom: 1px dashed #ccc;
 }
 
+.animation-node-row:first-child > .animation-node-description {
+    line-height: 45px;
+}
+
+.animation-node-row:first-child > .animation-node-timeline {
+    padding-top: 5px;
+}
+
 .animation-node-description {
     display: inline-block;
     min-width: 200px;
@@ -86,6 +94,7 @@
     height: 44px;
     border-bottom: 1px solid #ccc;
     flex-shrink: 0;
+    display: flex;
 }
 
 .animation-timeline-header:after {
@@ -100,8 +109,7 @@
 }
 
 .animation-controls {
-    width: 201px;
-    max-width: 201px;
+    flex: 0 0 200px;
     padding: 10px;
     height: 100%;
     line-height: 22px;
@@ -109,10 +117,12 @@
     border-right: 1px solid hsl(0, 0%, 90%);
 }
 
-.animation-timeline-markers {
+.animation-timeline-buffer {
     height: 100%;
-    width: calc(100% - 200px);
-    display: inline-block;
+    flex-grow: 1;
+    border-left: 1px solid #ccc;
+    display: flex;
+    padding: 0 2px;
 }
 
 .animation-time-overlay {
@@ -186,6 +196,7 @@
     width: calc(100% - 200px);
     top: 43px;
     border-left: 1px solid rgba(0,0,0,0.5);
+    display: none;
 }
 
 .animation-scrubber.animation-timeline-end {
@@ -247,6 +258,7 @@
 svg.animation-timeline-grid {
     position: absolute;
     left: 230px;
+    top: 43px;
 }
 
 rect.animation-timeline-grid-line {
@@ -325,4 +337,23 @@
     width: 100%;
     height: calc(100% - 44px);
     display: flex;
-}
\ No newline at end of file
+}
+
+.animation-buffer-preview {
+    height: 35px;
+    margin: 4px 2px;
+    background-color: #F3F3F3;
+    border-radius: 2px;
+    flex: 1 1;
+    padding: 4px;
+    max-width: 100px;
+}
+.animation-buffer-preview.selected {
+    background-color: hsl(217, 89%, 61%);
+}
+.animation-buffer-preview.selected > svg > line {
+    stroke: white !important;
+}
+.animation-buffer-preview > svg > line {
+    stroke-width: 1px;
+}
diff --git a/Source/devtools/front_end/animation/module.json b/Source/devtools/front_end/animation/module.json
index 51d11e8..fa969cd 100644
--- a/Source/devtools/front_end/animation/module.json
+++ b/Source/devtools/front_end/animation/module.json
@@ -12,6 +12,7 @@
     ],
     "scripts": [
         "AnimationModel.js",
+        "AnimationGroupPreviewUI.js",
         "AnimationTimeline.js",
         "AnimationControlPane.js"
     ],