| Index: Source/devtools/front_end/animation/AnimationTimeline.js
 | 
| diff --git a/Source/devtools/front_end/animation/AnimationTimeline.js b/Source/devtools/front_end/animation/AnimationTimeline.js
 | 
| index 1931ef6369e68e23e2603d2094fefe1e51760539..fe9bce5727786b11129c49c3e908d5d5fb0d9150 100644
 | 
| --- a/Source/devtools/front_end/animation/AnimationTimeline.js
 | 
| +++ b/Source/devtools/front_end/animation/AnimationTimeline.js
 | 
| @@ -31,6 +31,10 @@ WebInspector.AnimationTimeline = function()
 | 
|      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 @@ WebInspector.AnimationTimeline.prototype = {
 | 
|      {
 | 
|          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 @@ WebInspector.AnimationTimeline.prototype = {
 | 
|      _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 @@ WebInspector.AnimationTimeline.prototype = {
 | 
|  
 | 
|          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");
 | 
| @@ -316,21 +320,75 @@ WebInspector.AnimationTimeline.prototype = {
 | 
|          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)
 | 
| +        /**
 | 
| +         * @param {!WebInspector.AnimationModel.AnimationGroup} left
 | 
| +         * @param {!WebInspector.AnimationModel.AnimationGroup} right
 | 
| +         */
 | 
| +        function startTimeComparator(left, right)
 | 
| +        {
 | 
| +            return left.startTime() > right.startTime();
 | 
| +        }
 | 
| +
 | 
| +        var newGroup = /** @type {!WebInspector.AnimationModel.AnimationGroup} */(event.data);
 | 
| +        this._groupBuffer.push(newGroup);
 | 
| +        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(newGroup);
 | 
| +        this._previewMap.set(newGroup, preview);
 | 
| +        this._previewContainer.appendChild(preview.element);
 | 
| +        preview.element.addEventListener("click", this._selectAnimationGroup.bind(this, newGroup));
 | 
| +    },
 | 
| +
 | 
| +    /**
 | 
| +     * @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
 | 
| @@ -349,15 +407,11 @@ WebInspector.AnimationTimeline.prototype = {
 | 
|              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) {
 | 
| @@ -423,7 +477,7 @@ WebInspector.AnimationTimeline.prototype = {
 | 
|                  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));
 | 
|              }
 | 
|          }
 | 
| @@ -587,7 +641,8 @@ WebInspector.AnimationTimeline.prototype = {
 | 
|   * @constructor
 | 
|   * @param {!WebInspector.AnimationModel.AnimationEffect} animationEffect
 | 
|   */
 | 
| -WebInspector.AnimationTimeline.NodeUI = function(animationEffect) {
 | 
| +WebInspector.AnimationTimeline.NodeUI = function(animationEffect)
 | 
| +{
 | 
|      /**
 | 
|       * @param {?WebInspector.DOMNode} node
 | 
|       * @this {WebInspector.AnimationTimeline.NodeUI}
 | 
| @@ -694,454 +749,3 @@ WebInspector.AnimationTimeline.StepTimingFunction.parse = function(text) {
 | 
|          return new WebInspector.AnimationTimeline.StepTimingFunction(parseInt(match[1], 10), match[2]);
 | 
|      return null;
 | 
|  }
 | 
| -
 | 
| -/**
 | 
| - * @constructor
 | 
| - * @param {!WebInspector.AnimationModel.Animation} animation
 | 
| - * @param {!WebInspector.AnimationTimeline} timeline
 | 
| - * @param {!Element} parentElement
 | 
| - */
 | 
| -WebInspector.AnimationUI = function(animation, timeline, parentElement) {
 | 
| -    this._animation = animation;
 | 
| -    this._timeline = timeline;
 | 
| -    this._parentElement = parentElement;
 | 
| -
 | 
| -    if (this._animation.source().keyframesRule())
 | 
| -        this._keyframes =  this._animation.source().keyframesRule().keyframes();
 | 
| -
 | 
| -    this._nameElement = parentElement.createChild("div", "animation-name");
 | 
| -    this._nameElement.textContent = this._animation.name();
 | 
| -
 | 
| -    this._svg = parentElement.createSVGChild("svg", "animation-ui");
 | 
| -    this._svg.setAttribute("height", WebInspector.AnimationUI.Options.AnimationSVGHeight);
 | 
| -    this._svg.style.marginLeft = "-" + WebInspector.AnimationUI.Options.AnimationMargin + "px";
 | 
| -    this._svg.addEventListener("mousedown", this._mouseDown.bind(this, WebInspector.AnimationUI.MouseEvents.AnimationDrag, null));
 | 
| -    this._activeIntervalGroup = this._svg.createSVGChild("g");
 | 
| -
 | 
| -    /** @type {!Array.<{group: ?Element, animationLine: ?Element, keyframePoints: !Object.<number, !Element>, keyframeRender: !Object.<number, !Element>}>} */
 | 
| -    this._cachedElements = [];
 | 
| -
 | 
| -    this._movementInMs = 0;
 | 
| -    this.redraw();
 | 
| -}
 | 
| -
 | 
| -/**
 | 
| - * @enum {string}
 | 
| - */
 | 
| -WebInspector.AnimationUI.MouseEvents = {
 | 
| -    AnimationDrag: "AnimationDrag",
 | 
| -    KeyframeMove: "KeyframeMove",
 | 
| -    StartEndpointMove: "StartEndpointMove",
 | 
| -    FinishEndpointMove: "FinishEndpointMove"
 | 
| -}
 | 
| -
 | 
| -WebInspector.AnimationUI.prototype = {
 | 
| -    /**
 | 
| -     * @return {!WebInspector.AnimationModel.Animation}
 | 
| -     */
 | 
| -    animation: function()
 | 
| -    {
 | 
| -        return this._animation;
 | 
| -    },
 | 
| -
 | 
| -    /**
 | 
| -     * @param {?WebInspector.DOMNode} node
 | 
| -     */
 | 
| -    setNode: function(node)
 | 
| -    {
 | 
| -        this._node = node;
 | 
| -    },
 | 
| -
 | 
| -    /**
 | 
| -     * @param {!Element} parentElement
 | 
| -     * @param {string} className
 | 
| -     */
 | 
| -    _createLine: function(parentElement, className)
 | 
| -    {
 | 
| -        var line = parentElement.createSVGChild("line", className);
 | 
| -        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();
 | 
| -        return line;
 | 
| -    },
 | 
| -
 | 
| -    /**
 | 
| -     * @param {number} iteration
 | 
| -     * @param {!Element} parentElement
 | 
| -     */
 | 
| -    _drawAnimationLine: function(iteration, parentElement)
 | 
| -    {
 | 
| -        var cache = this._cachedElements[iteration];
 | 
| -        if (!cache.animationLine)
 | 
| -            cache.animationLine = this._createLine(parentElement, "animation-line");
 | 
| -        cache.animationLine.setAttribute("x2", (this._duration() * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin).toFixed(2));
 | 
| -    },
 | 
| -
 | 
| -    /**
 | 
| -     * @param {!Element} parentElement
 | 
| -     */
 | 
| -    _drawDelayLine: function(parentElement)
 | 
| -    {
 | 
| -        if (!this._delayLine) {
 | 
| -            this._delayLine = this._createLine(parentElement, "animation-delay-line");
 | 
| -            this._endDelayLine = this._createLine(parentElement, "animation-delay-line");
 | 
| -        }
 | 
| -        this._delayLine.setAttribute("x1", WebInspector.AnimationUI.Options.AnimationMargin);
 | 
| -        this._delayLine.setAttribute("x2", (this._delay() * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin).toFixed(2));
 | 
| -        var leftMargin = (this._delay() + this._duration() * this._animation.source().iterations()) * this._timeline.pixelMsRatio();
 | 
| -        this._endDelayLine.style.transform = "translateX(" + Math.min(leftMargin, this._timeline.width()).toFixed(2) + "px)";
 | 
| -        this._endDelayLine.setAttribute("x1", WebInspector.AnimationUI.Options.AnimationMargin);
 | 
| -        this._endDelayLine.setAttribute("x2", (this._animation.source().endDelay() * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin).toFixed(2));
 | 
| -    },
 | 
| -
 | 
| -    /**
 | 
| -     * @param {number} iteration
 | 
| -     * @param {!Element} parentElement
 | 
| -     * @param {number} x
 | 
| -     * @param {number} keyframeIndex
 | 
| -     * @param {boolean} attachEvents
 | 
| -     */
 | 
| -    _drawPoint: function(iteration, parentElement, x, keyframeIndex, attachEvents)
 | 
| -    {
 | 
| -        if (this._cachedElements[iteration].keyframePoints[keyframeIndex]) {
 | 
| -            this._cachedElements[iteration].keyframePoints[keyframeIndex].setAttribute("cx", x.toFixed(2));
 | 
| -            return;
 | 
| -        }
 | 
| -
 | 
| -        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.setAttribute("r", WebInspector.AnimationUI.Options.AnimationMargin / 2);
 | 
| -
 | 
| -        if (keyframeIndex <= 0)
 | 
| -            circle.style.fill = this._color();
 | 
| -
 | 
| -        this._cachedElements[iteration].keyframePoints[keyframeIndex] = circle;
 | 
| -
 | 
| -        if (!attachEvents)
 | 
| -            return;
 | 
| -
 | 
| -        if (keyframeIndex === 0) {
 | 
| -            circle.addEventListener("mousedown", this._mouseDown.bind(this, WebInspector.AnimationUI.MouseEvents.StartEndpointMove, keyframeIndex));
 | 
| -        } else if (keyframeIndex === -1) {
 | 
| -            circle.addEventListener("mousedown", this._mouseDown.bind(this, WebInspector.AnimationUI.MouseEvents.FinishEndpointMove, keyframeIndex));
 | 
| -        } else {
 | 
| -            circle.addEventListener("mousedown", this._mouseDown.bind(this, WebInspector.AnimationUI.MouseEvents.KeyframeMove, keyframeIndex));
 | 
| -        }
 | 
| -    },
 | 
| -
 | 
| -    /**
 | 
| -     * @param {number} iteration
 | 
| -     * @param {number} keyframeIndex
 | 
| -     * @param {!Element} parentElement
 | 
| -     * @param {number} leftDistance
 | 
| -     * @param {number} width
 | 
| -     * @param {string} easing
 | 
| -     */
 | 
| -    _renderKeyframe: function(iteration, keyframeIndex, parentElement, leftDistance, width, easing)
 | 
| -    {
 | 
| -        /**
 | 
| -         * @param {!Element} parentElement
 | 
| -         * @param {number} x
 | 
| -         * @param {string} strokeColor
 | 
| -         */
 | 
| -        function createStepLine(parentElement, x, strokeColor)
 | 
| -        {
 | 
| -            var line = parentElement.createSVGChild("line");
 | 
| -            line.setAttribute("x1", x);
 | 
| -            line.setAttribute("x2", x);
 | 
| -            line.setAttribute("y1", WebInspector.AnimationUI.Options.AnimationMargin);
 | 
| -            line.setAttribute("y2", WebInspector.AnimationUI.Options.AnimationHeight);
 | 
| -            line.style.stroke = strokeColor;
 | 
| -        }
 | 
| -
 | 
| -        var bezier = WebInspector.Geometry.CubicBezier.parse(easing);
 | 
| -        var cache = this._cachedElements[iteration].keyframeRender;
 | 
| -        if (!cache[keyframeIndex])
 | 
| -            cache[keyframeIndex] = bezier ? parentElement.createSVGChild("path", "animation-keyframe") : parentElement.createSVGChild("g", "animation-keyframe-step");
 | 
| -        var group = cache[keyframeIndex];
 | 
| -        group.style.transform = "translateX(" + leftDistance.toFixed(2) + "px)";
 | 
| -
 | 
| -        if (bezier) {
 | 
| -            group.style.fill = this._color();
 | 
| -            WebInspector.BezierUI.drawVelocityChart(bezier, group, width);
 | 
| -        } else {
 | 
| -            var stepFunction = WebInspector.AnimationTimeline.StepTimingFunction.parse(easing);
 | 
| -            group.removeChildren();
 | 
| -            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());
 | 
| -        }
 | 
| -    },
 | 
| -
 | 
| -    redraw: function()
 | 
| -    {
 | 
| -        var durationWithDelay = this._delay() + this._duration() * this._animation.source().iterations() + this._animation.source().endDelay();
 | 
| -        var leftMargin = ((this._animation.startTime() - this._timeline.startTime()) * this._timeline.pixelMsRatio());
 | 
| -        var maxWidth = this._timeline.width() - WebInspector.AnimationUI.Options.AnimationMargin - leftMargin;
 | 
| -        var svgWidth = Math.min(maxWidth, durationWithDelay * this._timeline.pixelMsRatio());
 | 
| -
 | 
| -        this._svg.classList.toggle("animation-ui-canceled", this._animation.playState() === "idle");
 | 
| -        this._svg.setAttribute("width", (svgWidth + 2 * WebInspector.AnimationUI.Options.AnimationMargin).toFixed(2));
 | 
| -        this._svg.style.transform = "translateX(" + leftMargin.toFixed(2)  + "px)";
 | 
| -        this._activeIntervalGroup.style.transform = "translateX(" + (this._delay() * this._timeline.pixelMsRatio()).toFixed(2) + "px)";
 | 
| -
 | 
| -        this._nameElement.style.transform = "translateX(" + (leftMargin + this._delay() * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin).toFixed(2) + "px)";
 | 
| -        this._nameElement.style.width = (this._duration() * this._timeline.pixelMsRatio().toFixed(2)) + "px";
 | 
| -        this._drawDelayLine(this._svg);
 | 
| -
 | 
| -        if (this._animation.type() === "CSSTransition") {
 | 
| -            this._renderTransition();
 | 
| -            return;
 | 
| -        }
 | 
| -
 | 
| -        this._renderIteration(this._activeIntervalGroup, 0);
 | 
| -        if (!this._tailGroup)
 | 
| -            this._tailGroup = this._activeIntervalGroup.createSVGChild("g", "animation-tail-iterations");
 | 
| -        var iterationWidth = this._duration() * this._timeline.pixelMsRatio();
 | 
| -        for (var iteration = 1; iteration < this._animation.source().iterations() && iterationWidth * (iteration - 1) < this._timeline.width(); iteration++)
 | 
| -            this._renderIteration(this._tailGroup, iteration);
 | 
| -        while (iteration < this._cachedElements.length)
 | 
| -            this._cachedElements.pop().group.remove();
 | 
| -    },
 | 
| -
 | 
| -
 | 
| -    _renderTransition: function()
 | 
| -    {
 | 
| -        if (!this._cachedElements[0])
 | 
| -            this._cachedElements[0] = { animationLine: null, keyframePoints: {}, keyframeRender: {}, group: null };
 | 
| -        this._drawAnimationLine(0, this._activeIntervalGroup);
 | 
| -        this._renderKeyframe(0, 0, this._activeIntervalGroup, WebInspector.AnimationUI.Options.AnimationMargin, this._duration() * this._timeline.pixelMsRatio(), this._animation.source().easing());
 | 
| -        this._drawPoint(0, this._activeIntervalGroup, WebInspector.AnimationUI.Options.AnimationMargin, 0, true);
 | 
| -        this._drawPoint(0, this._activeIntervalGroup, this._duration() * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin, -1, true);
 | 
| -    },
 | 
| -
 | 
| -    /**
 | 
| -     * @param {!Element} parentElement
 | 
| -     * @param {number} iteration
 | 
| -     */
 | 
| -    _renderIteration: function(parentElement, iteration)
 | 
| -    {
 | 
| -        if (!this._cachedElements[iteration])
 | 
| -            this._cachedElements[iteration] = { animationLine: null, keyframePoints: {}, keyframeRender: {}, group: parentElement.createSVGChild("g") };
 | 
| -        var group = this._cachedElements[iteration].group;
 | 
| -        group.style.transform = "translateX(" + (iteration * this._duration() * this._timeline.pixelMsRatio()).toFixed(2) + "px)";
 | 
| -        this._drawAnimationLine(iteration, group);
 | 
| -        console.assert(this._keyframes.length > 1);
 | 
| -        for (var i = 0; i < this._keyframes.length - 1; i++) {
 | 
| -            var leftDistance = this._offset(i) * this._duration() * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin;
 | 
| -            var width = this._duration() * (this._offset(i + 1) - this._offset(i)) * this._timeline.pixelMsRatio();
 | 
| -            this._renderKeyframe(iteration, i, group, leftDistance, width, this._keyframes[i].easing());
 | 
| -            if (i || (!i && iteration === 0))
 | 
| -                this._drawPoint(iteration, group, leftDistance, i, iteration === 0);
 | 
| -        }
 | 
| -        this._drawPoint(iteration, group, this._duration() * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin, -1, iteration === 0);
 | 
| -    },
 | 
| -
 | 
| -    /**
 | 
| -     * @return {number}
 | 
| -     */
 | 
| -    _delay: function()
 | 
| -    {
 | 
| -        var delay = this._animation.source().delay();
 | 
| -        if (this._mouseEventType === WebInspector.AnimationUI.MouseEvents.AnimationDrag || this._mouseEventType === WebInspector.AnimationUI.MouseEvents.StartEndpointMove)
 | 
| -            delay += this._movementInMs;
 | 
| -        // FIXME: add support for negative start delay
 | 
| -        return Math.max(0, delay);
 | 
| -    },
 | 
| -
 | 
| -    /**
 | 
| -     * @return {number}
 | 
| -     */
 | 
| -    _duration: function()
 | 
| -    {
 | 
| -        var duration = this._animation.source().duration();
 | 
| -        if (this._mouseEventType === WebInspector.AnimationUI.MouseEvents.FinishEndpointMove)
 | 
| -            duration += this._movementInMs;
 | 
| -        else if (this._mouseEventType === WebInspector.AnimationUI.MouseEvents.StartEndpointMove)
 | 
| -            duration -= Math.max(this._movementInMs, -this._animation.source().delay()); // Cannot have negative delay
 | 
| -        return Math.max(0, duration);
 | 
| -    },
 | 
| -
 | 
| -    /**
 | 
| -     * @param {number} i
 | 
| -     * @return {number} offset
 | 
| -     */
 | 
| -    _offset: function(i)
 | 
| -    {
 | 
| -        var offset = this._keyframes[i].offsetAsNumber();
 | 
| -        if (this._mouseEventType === WebInspector.AnimationUI.MouseEvents.KeyframeMove && i === this._keyframeMoved) {
 | 
| -            console.assert(i > 0 && i < this._keyframes.length - 1, "First and last keyframe cannot be moved");
 | 
| -            offset += this._movementInMs / this._animation.source().duration();
 | 
| -            offset = Math.max(offset, this._keyframes[i - 1].offsetAsNumber());
 | 
| -            offset = Math.min(offset, this._keyframes[i + 1].offsetAsNumber());
 | 
| -        }
 | 
| -        return offset;
 | 
| -    },
 | 
| -
 | 
| -    /**
 | 
| -     * @param {!WebInspector.AnimationUI.MouseEvents} mouseEventType
 | 
| -     * @param {?number} keyframeIndex
 | 
| -     * @param {!Event} event
 | 
| -     */
 | 
| -    _mouseDown: function(mouseEventType, keyframeIndex, event)
 | 
| -    {
 | 
| -        if (this._animation.playState() === "idle")
 | 
| -            return;
 | 
| -        this._mouseEventType = mouseEventType;
 | 
| -        this._keyframeMoved = keyframeIndex;
 | 
| -        this._downMouseX = event.clientX;
 | 
| -        this._mouseMoveHandler = this._mouseMove.bind(this);
 | 
| -        this._mouseUpHandler = this._mouseUp.bind(this);
 | 
| -        this._parentElement.ownerDocument.addEventListener("mousemove", this._mouseMoveHandler);
 | 
| -        this._parentElement.ownerDocument.addEventListener("mouseup", this._mouseUpHandler);
 | 
| -        event.preventDefault();
 | 
| -        event.stopPropagation();
 | 
| -
 | 
| -        if (this._node)
 | 
| -            WebInspector.Revealer.reveal(this._node);
 | 
| -    },
 | 
| -
 | 
| -    /**
 | 
| -     * @param {!Event} event
 | 
| -     */
 | 
| -    _mouseMove: function (event)
 | 
| -    {
 | 
| -        this._movementInMs = (event.clientX - this._downMouseX) / this._timeline.pixelMsRatio();
 | 
| -        if (this._animation.startTime() + this._delay() + this._duration() - this._timeline.startTime() > this._timeline.duration() * 0.8)
 | 
| -            this._timeline.setDuration(this._timeline.duration() * 1.2);
 | 
| -        this.redraw();
 | 
| -    },
 | 
| -
 | 
| -    /**
 | 
| -     * @param {!Event} event
 | 
| -     */
 | 
| -    _mouseUp: function(event)
 | 
| -    {
 | 
| -        this._movementInMs = (event.clientX - this._downMouseX) / this._timeline.pixelMsRatio();
 | 
| -
 | 
| -        // Commit changes
 | 
| -        if (this._mouseEventType === WebInspector.AnimationUI.MouseEvents.KeyframeMove) {
 | 
| -            this._keyframes[this._keyframeMoved].setOffset(this._offset(this._keyframeMoved));
 | 
| -        } else {
 | 
| -            var delay = this._delay();
 | 
| -            var duration = this._duration();
 | 
| -            this._setDelay(delay);
 | 
| -            this._setDuration(duration);
 | 
| -            if (this._animation.type() !== "CSSAnimation") {
 | 
| -                for (var target of WebInspector.targetManager.targets(WebInspector.Target.Type.Page))
 | 
| -                    target.animationAgent().setTiming(this._animation.id(), duration, delay);
 | 
| -            }
 | 
| -        }
 | 
| -
 | 
| -        this._movementInMs = 0;
 | 
| -        this.redraw();
 | 
| -
 | 
| -        this._parentElement.ownerDocument.removeEventListener("mousemove", this._mouseMoveHandler);
 | 
| -        this._parentElement.ownerDocument.removeEventListener("mouseup", this._mouseUpHandler);
 | 
| -        delete this._mouseMoveHandler;
 | 
| -        delete this._mouseUpHandler;
 | 
| -        delete this._mouseEventType;
 | 
| -        delete this._downMouseX;
 | 
| -        delete this._keyframeMoved;
 | 
| -    },
 | 
| -
 | 
| -    /**
 | 
| -     * @param {number} value
 | 
| -     */
 | 
| -    _setDelay: function(value)
 | 
| -    {
 | 
| -        if (!this._node || this._animation.source().delay() == this._delay())
 | 
| -            return;
 | 
| -
 | 
| -        this._animation.source().setDelay(this._delay());
 | 
| -        var propertyName;
 | 
| -        if (this._animation.type() == "CSSTransition")
 | 
| -            propertyName = "transition-delay";
 | 
| -        else if (this._animation.type() == "CSSAnimation")
 | 
| -            propertyName = "animation-delay";
 | 
| -        else
 | 
| -            return;
 | 
| -        this._setNodeStyle(propertyName, Math.round(value) + "ms");
 | 
| -    },
 | 
| -
 | 
| -    /**
 | 
| -     * @param {number} value
 | 
| -     */
 | 
| -    _setDuration: function(value)
 | 
| -    {
 | 
| -        if (!this._node || this._animation.source().duration() == value)
 | 
| -            return;
 | 
| -
 | 
| -        this._animation.source().setDuration(value);
 | 
| -        var propertyName;
 | 
| -        if (this._animation.type() == "CSSTransition")
 | 
| -            propertyName = "transition-duration";
 | 
| -        else if (this._animation.type() == "CSSAnimation")
 | 
| -            propertyName = "animation-duration";
 | 
| -        else
 | 
| -            return;
 | 
| -        this._setNodeStyle(propertyName, Math.round(value) + "ms");
 | 
| -    },
 | 
| -
 | 
| -    /**
 | 
| -     * @param {string} name
 | 
| -     * @param {string} value
 | 
| -     */
 | 
| -    _setNodeStyle: function(name, value)
 | 
| -    {
 | 
| -        var style = this._node.getAttribute("style") || "";
 | 
| -        if (style)
 | 
| -            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;
 | 
| -    }
 | 
| -}
 | 
| -
 | 
| -WebInspector.AnimationUI.Options = {
 | 
| -    AnimationHeight: 32,
 | 
| -    AnimationSVGHeight: 80,
 | 
| -    AnimationMargin: 7,
 | 
| -    EndpointsClickRegionSize: 10,
 | 
| -    GridCanvasHeight: 40
 | 
| -}
 | 
| -
 | 
| -WebInspector.AnimationUI.Colors = {
 | 
| -    "Purple": WebInspector.Color.parse("#9C27B0"),
 | 
| -    "Light Blue": WebInspector.Color.parse("#03A9F4"),
 | 
| -    "Deep Orange": WebInspector.Color.parse("#FF5722"),
 | 
| -    "Blue": WebInspector.Color.parse("#5677FC"),
 | 
| -    "Lime": WebInspector.Color.parse("#CDDC39"),
 | 
| -    "Pink": WebInspector.Color.parse("#E91E63"),
 | 
| -    "Green": WebInspector.Color.parse("#0F9D58"),
 | 
| -    "Brown": WebInspector.Color.parse("#795548"),
 | 
| -    "Cyan": WebInspector.Color.parse("#00BCD4")
 | 
| -}
 | 
| 
 |