Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / animation / AnimationTimeline.js
blob1691f8c94fce712e9acc81c2809a3db28ac88741
1 // Copyright (c) 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6  * @constructor
7  * @extends {WebInspector.VBox}
8  * @implements {WebInspector.TargetManager.Observer}
9  */
10 WebInspector.AnimationTimeline = function()
12     WebInspector.VBox.call(this, true);
13     this.registerRequiredCSS("animation/animationTimeline.css");
14     this.element.classList.add("animations-timeline");
16     this._grid = this.contentElement.createSVGChild("svg", "animation-timeline-grid");
17     this.contentElement.appendChild(this._createScrubber());
18     WebInspector.installDragHandle(this._timelineScrubberHead, this._scrubberDragStart.bind(this), this._scrubberDragMove.bind(this), this._scrubberDragEnd.bind(this), "move");
19     this._timelineScrubberHead.textContent = WebInspector.UIString(Number.millisToString(0));
21     this._underlyingPlaybackRate = 1;
22     this.contentElement.appendChild(this._createHeader());
23     this._animationsContainer = this.contentElement.createChild("div", "animation-timeline-rows");
25     this._emptyTimelineMessage = this._animationsContainer.createChild("div", "animation-timeline-empty-message");
26     var message = this._emptyTimelineMessage.createChild("div");
27     message.textContent = WebInspector.UIString("Trigger animations on the page to view and tweak them on the animation timeline.");
29     this._duration = this._defaultDuration();
30     this._scrubberRadius = 30;
31     this._timelineControlsWidth = 230;
32     /** @type {!Map.<!DOMAgent.BackendNodeId, !WebInspector.AnimationTimeline.NodeUI>} */
33     this._nodesMap = new Map();
34     this._symbol = Symbol("animationTimeline");
35     /** @type {!Map.<string, !WebInspector.AnimationModel.Animation>} */
36     this._animationsMap = new Map();
37     WebInspector.targetManager.addModelListener(WebInspector.ResourceTreeModel, WebInspector.ResourceTreeModel.EventTypes.MainFrameNavigated, this._mainFrameNavigated, this);
38     WebInspector.targetManager.addModelListener(WebInspector.DOMModel, WebInspector.DOMModel.Events.NodeRemoved, this._nodeRemoved, this);
40     WebInspector.targetManager.observeTargets(this, WebInspector.Target.Type.Page);
43 WebInspector.AnimationTimeline.GlobalPlaybackRates = [0.1, 0.25, 0.5, 1.0];
45 WebInspector.AnimationTimeline.prototype = {
46     wasShown: function()
47     {
48         for (var target of WebInspector.targetManager.targets(WebInspector.Target.Type.Page))
49             this._addEventListeners(target);
50     },
52     willHide: function()
53     {
54         for (var target of WebInspector.targetManager.targets(WebInspector.Target.Type.Page))
55             this._removeEventListeners(target);
56     },
58     /**
59      * @override
60      * @param {!WebInspector.Target} target
61      */
62     targetAdded: function(target)
63     {
64         if (this.isShowing())
65             this._addEventListeners(target);
66     },
68     /**
69      * @override
70      * @param {!WebInspector.Target} target
71      */
72     targetRemoved: function(target)
73     {
74         this._removeEventListeners(target);
75     },
77     /**
78      * @param {!WebInspector.Target} target
79      */
80     _addEventListeners: function(target)
81     {
82         var animationModel = WebInspector.AnimationModel.fromTarget(target);
83         animationModel.ensureEnabled();
84         animationModel.addEventListener(WebInspector.AnimationModel.Events.AnimationCreated, this._animationCreated, this);
85         animationModel.addEventListener(WebInspector.AnimationModel.Events.AnimationCanceled, this._animationCanceled, this);
86     },
88     /**
89      * @param {!WebInspector.Target} target
90      */
91     _removeEventListeners: function(target)
92     {
93         var animationModel = WebInspector.AnimationModel.fromTarget(target);
94         animationModel.removeEventListener(WebInspector.AnimationModel.Events.AnimationCreated, this._animationCreated, this);
95         animationModel.removeEventListener(WebInspector.AnimationModel.Events.AnimationCanceled, this._animationCanceled, this);
96     },
98     /**
99      * @param {?WebInspector.DOMNode} node
100      */
101     setNode: function(node)
102     {
103         for (var nodeUI of this._nodesMap.values())
104             nodeUI.setNode(node);
105     },
107     /**
108      * @return {!Element} element
109      */
110     _createScrubber: function() {
111         this._timelineScrubber = createElementWithClass("div", "animation-scrubber hidden");
112         this._timelineScrubber.createChild("div", "animation-time-overlay");
113         this._timelineScrubber.createChild("div", "animation-scrubber-arrow");
114         this._timelineScrubberHead = this._timelineScrubber.createChild("div", "animation-scrubber-head");
115         var timerContainer = this._timelineScrubber.createChild("div", "animation-timeline-timer");
116         this._timerSpinner = timerContainer.createChild("div", "timer-spinner timer-hemisphere");
117         this._timerFiller = timerContainer.createChild("div", "timer-filler timer-hemisphere");
118         this._timerMask = timerContainer.createChild("div", "timer-mask");
119         return this._timelineScrubber;
120     },
122     /**
123      * @return {!Element}
124      */
125     _createHeader: function()
126     {
127         /**
128          * @param {!Event} event
129          * @this {WebInspector.AnimationTimeline}
130          */
131         function playbackSliderInputHandler(event)
132         {
133             this._underlyingPlaybackRate = WebInspector.AnimationTimeline.GlobalPlaybackRates[event.target.value];
134             this._updatePlaybackControls();
135         }
137         var container = createElementWithClass("div", "animation-timeline-header");
138         var controls = container.createChild("div", "animation-controls");
139         container.createChild("div", "animation-timeline-markers");
141         var toolbar = new WebInspector.Toolbar(controls);
142         toolbar.element.classList.add("animation-controls-toolbar");
143         this._controlButton = new WebInspector.ToolbarButton(WebInspector.UIString("Replay timeline"), "replay-outline-toolbar-item");
144         this._controlButton.addEventListener("click", this._controlButtonToggle.bind(this));
145         toolbar.appendToolbarItem(this._controlButton);
147         this._playbackLabel = controls.createChild("span", "animation-playback-label");
148         this._playbackLabel.createTextChild("1x");
149         this._playbackLabel.addEventListener("keydown", this._playbackLabelInput.bind(this));
150         this._playbackLabel.addEventListener("focusout", this._playbackLabelInput.bind(this));
152         this._playbackSlider = controls.createChild("input", "animation-playback-slider");
153         this._playbackSlider.type = "range";
154         this._playbackSlider.min = 0;
155         this._playbackSlider.max = WebInspector.AnimationTimeline.GlobalPlaybackRates.length - 1;
156         this._playbackSlider.value = this._playbackSlider.max;
157         this._playbackSlider.addEventListener("input", playbackSliderInputHandler.bind(this));
158         this._updateAnimationsPlaybackRate();
160         return container;
161     },
163     /**
164      * @param {!Event} event
165      */
166     _playbackLabelInput: function(event)
167     {
168         var element = /** @type {!Element} */(event.currentTarget);
169         if (event.type !== "focusout" && !WebInspector.handleElementValueModifications(event, element) && !isEnterKey(event))
170             return;
172         var value = parseFloat(this._playbackLabel.textContent);
173         if (!isNaN(value))
174             this._underlyingPlaybackRate = Math.max(0, value);
175         this._updatePlaybackControls();
176         event.consume(true);
177     },
179     _updatePlaybackControls: function()
180     {
181         this._playbackLabel.textContent = this._underlyingPlaybackRate + "x";
182         var playbackSliderValue = 0;
183         for (var rate of WebInspector.AnimationTimeline.GlobalPlaybackRates) {
184             if (this._underlyingPlaybackRate > rate)
185                 playbackSliderValue++;
186         }
187         this._playbackSlider.value = playbackSliderValue;
189         for (var target of WebInspector.targetManager.targets(WebInspector.Target.Type.Page))
190             WebInspector.AnimationModel.fromTarget(target).setPlaybackRate(this._playbackRate());
191         WebInspector.userMetrics.AnimationsPlaybackRateChanged.record();
192         if (this._scrubberPlayer)
193             this._scrubberPlayer.playbackRate = this._playbackRate();
194     },
196     _controlButtonToggle: function()
197     {
198         if (this._emptyTimelineMessage)
199             return;
200         if (this._controlButton.element.classList.contains("play-outline-toolbar-item"))
201             this._togglePause(false);
202         else if (this._controlButton.element.classList.contains("replay-outline-toolbar-item"))
203             this._replay();
204         else
205             this._togglePause(true);
206         this._updateControlButton();
207     },
209     _updateControlButton: function()
210     {
211         this._controlButton.element.classList.remove("play-outline-toolbar-item");
212         this._controlButton.element.classList.remove("replay-outline-toolbar-item");
213         this._controlButton.element.classList.remove("pause-outline-toolbar-item");
214         if (this._paused) {
215             this._controlButton.element.classList.add("play-outline-toolbar-item");
216             this._controlButton.setTitle(WebInspector.UIString("Play timeline"));
217         } else if (!this._scrubberPlayer || this._scrubberPlayer.currentTime >= this.duration() - this._scrubberRadius / this.pixelMsRatio()) {
218             this._controlButton.element.classList.add("replay-outline-toolbar-item");
219             this._controlButton.setTitle(WebInspector.UIString("Replay timeline"));
220         } else {
221             this._controlButton.element.classList.add("pause-outline-toolbar-item");
222             this._controlButton.setTitle(WebInspector.UIString("Pause timeline"));
223         }
224     },
226     _updateAnimationsPlaybackRate: function()
227     {
228         /**
229          * @param {?Protocol.Error} error
230          * @param {number} playbackRate
231          * @this {WebInspector.AnimationTimeline}
232          */
233         function setPlaybackRate(error, playbackRate)
234         {
235             if (playbackRate === 0) {
236                 playbackRate = 1;
237                 if (target)
238                     WebInspector.AnimationModel.fromTarget(target).setPlaybackRate(1);
239             }
240             this._underlyingPlaybackRate = playbackRate;
241             this._updatePlaybackControls();
242         }
244         delete this._paused;
245         for (var target of WebInspector.targetManager.targets(WebInspector.Target.Type.Page))
246             target.animationAgent().getPlaybackRate(setPlaybackRate.bind(this));
247     },
249     /**
250      * @return {number}
251      */
252     _playbackRate: function()
253     {
254         return this._paused ? 0 : this._underlyingPlaybackRate;
255     },
257     /**
258      * @param {boolean} pause
259      */
260     _togglePause: function(pause)
261     {
262         this._paused = pause;
263         for (var target of WebInspector.targetManager.targets(WebInspector.Target.Type.Page))
264             WebInspector.AnimationModel.fromTarget(target).setPlaybackRate(this._playbackRate());
265         WebInspector.userMetrics.AnimationsPlaybackRateChanged.record();
266         if (this._scrubberPlayer)
267             this._scrubberPlayer.playbackRate = this._playbackRate();
268     },
270     _replay: function()
271     {
272         if (this.startTime() === undefined)
273             return;
274         for (var target of WebInspector.targetManager.targets(WebInspector.Target.Type.Page))
275             target.animationAgent().setCurrentTime(/** @type {number} */(this.startTime()));
277         this._animateTime(0);
278     },
280     /**
281      * @return {number}
282      */
283     _defaultDuration: function ()
284     {
285         return 100;
286     },
288     /**
289      * @return {number}
290      */
291     duration: function()
292     {
293         return this._duration;
294     },
296     /**
297      * @param {number} duration
298      */
299     setDuration: function(duration)
300     {
301         this._duration = duration;
302         this.scheduleRedraw();
303     },
305     /**
306      * @return {number|undefined}
307      */
308     startTime: function()
309     {
310         return this._startTime;
311     },
313     _reset: function()
314     {
315         if (!this._nodesMap.size)
316             return;
318         this._nodesMap.clear();
319         this._animationsMap.clear();
320         this._animationsContainer.removeChildren();
321         this._duration = this._defaultDuration();
322         delete this._startTime;
323     },
325     /**
326      * @param {!WebInspector.Event} event
327      */
328     _mainFrameNavigated: function(event)
329     {
330         this._reset();
331         this._updateAnimationsPlaybackRate();
332         if (this._scrubberPlayer)
333             this._scrubberPlayer.cancel();
334         delete this._scrubberPlayer;
335         this._timelineScrubberHead.textContent = WebInspector.UIString(Number.millisToString(0));
336         this._updateControlButton();
337     },
339     /**
340      * @param {!WebInspector.Event} event
341      */
342     _animationCreated: function(event)
343     {
344         this._addAnimation(/** @type {!WebInspector.AnimationModel.Animation} */ (event.data.player), event.data.resetTimeline)
345     },
347     /**
348      * @param {!WebInspector.AnimationModel.Animation} animation
349      * @param {boolean} resetTimeline
350      */
351     _addAnimation: function(animation, resetTimeline)
352     {
353         /**
354          * @param {?WebInspector.DOMNode} node
355          * @this {WebInspector.AnimationTimeline}
356          */
357         function nodeResolved(node)
358         {
359             if (!node)
360                 return;
361             uiAnimation.setNode(node);
362             node[this._symbol] = nodeUI;
363         }
365         if (this._emptyTimelineMessage) {
366             this._emptyTimelineMessage.remove();
367             delete this._emptyTimelineMessage;
368         }
370         if (resetTimeline)
371             this._reset();
373         // Ignore Web Animations custom effects & groups
374         if (animation.type() === "WebAnimation" && animation.source().keyframesRule().keyframes().length === 0)
375             return;
377         if (this._resizeWindow(animation))
378             this.scheduleRedraw();
380         var nodeUI = this._nodesMap.get(animation.source().backendNodeId());
381         if (!nodeUI) {
382             nodeUI = new WebInspector.AnimationTimeline.NodeUI(animation.source());
383             this._animationsContainer.appendChild(nodeUI.element);
384             this._nodesMap.set(animation.source().backendNodeId(), nodeUI);
385         }
386         var nodeRow = nodeUI.findRow(animation);
387         var uiAnimation = new WebInspector.AnimationUI(animation, this, nodeRow.element);
388         animation.source().deferredNode().resolve(nodeResolved.bind(this));
389         nodeRow.animations.push(uiAnimation);
390         this._animationsMap.set(animation.id(), animation);
391     },
393     /**
394      * @param {!WebInspector.Event} event
395      */
396     _animationCanceled: function(event)
397     {
398         this._cancelAnimation(/** @type {string} */ (event.data.id));
399     },
401     /**
402      * @param {string} playerId
403      */
404     _cancelAnimation: function(playerId)
405     {
406         var animation = this._animationsMap.get(playerId);
407         if (!animation)
408             return;
409         animation.setPlayState("idle");
410         this.scheduleRedraw();
411     },
413     /**
414      * @param {!WebInspector.Event} event
415      */
416     _nodeRemoved: function(event)
417     {
418         var node = event.data.node;
419         if (node[this._symbol])
420             node[this._symbol].nodeRemoved();
421     },
423     _renderGrid: function()
424     {
425         const gridSize = 250;
426         this._grid.setAttribute("width", this.width());
427         this._grid.setAttribute("height", this._animationsContainer.offsetHeight + 43);
428         this._grid.setAttribute("shape-rendering", "crispEdges");
429         this._grid.removeChildren();
430         var lastDraw = undefined;
431         for (var time = 0; time < this.duration(); time += gridSize) {
432             var line = this._grid.createSVGChild("rect", "animation-timeline-grid-line");
433             line.setAttribute("x", time * this.pixelMsRatio());
434             line.setAttribute("y", 0);
435             line.setAttribute("height", "100%");
436             line.setAttribute("width", 1);
437         }
438         for (var time = 0; time < this.duration(); time += gridSize) {
439             var gridWidth = time * this.pixelMsRatio();
440             if (!lastDraw || gridWidth - lastDraw > 50) {
441                 lastDraw = gridWidth;
442                 var label = this._grid.createSVGChild("text", "animation-timeline-grid-label");
443                 label.setAttribute("x", gridWidth + 5);
444                 label.setAttribute("y", 35);
445                 label.textContent = WebInspector.UIString(Number.millisToString(time));
446             }
447         }
448     },
450     scheduleRedraw: function() {
451         if (this._redrawing)
452             return;
453         this._redrawing = true;
454         this._animationsContainer.window().requestAnimationFrame(this._redraw.bind(this));
455     },
457     /**
458      * @param {number=} timestamp
459      */
460     _redraw: function(timestamp)
461     {
462         delete this._redrawing;
463         for (var nodeUI of this._nodesMap.values())
464             nodeUI.redraw();
465         this._renderGrid();
466     },
468     onResize: function()
469     {
470         this._cachedTimelineWidth = Math.max(0, this._animationsContainer.offsetWidth - this._timelineControlsWidth) || 0;
471         this.scheduleRedraw();
472         if (this._scrubberPlayer)
473             this._animateTime();
474     },
476     /**
477      * @return {number}
478      */
479     width: function()
480     {
481         return this._cachedTimelineWidth || 0;
482     },
484     /**
485      * @param {!WebInspector.AnimationModel.Animation} animation
486      * @return {boolean}
487      */
488     _resizeWindow: function(animation)
489     {
490         var resized = false;
491         if (!this._startTime)
492             this._startTime = animation.startTime();
494         // This shows at most 3 iterations
495         var duration = animation.source().duration() * Math.min(3, animation.source().iterations());
496         var requiredDuration = animation.startTime() + animation.source().delay() + duration + animation.source().endDelay() - this.startTime();
497         if (requiredDuration > this._duration * 0.8) {
498             resized = true;
499             this._duration = requiredDuration * 1.5;
500             this._timelineScrubber.classList.remove("hidden");
501             this._animateTime(animation.startTime() - this.startTime());
502         }
503         return resized;
504     },
506     /**
507       * @param {number=} time
508       */
509     _animateTime: function(time)
510     {
511         var oldPlayer = this._scrubberPlayer;
513         this._scrubberPlayer = this._timelineScrubber.animate([
514             { transform: "translateX(0px)" },
515             { transform: "translateX(" +  (this.width() - this._scrubberRadius) + "px)" }
516         ], { duration: this.duration() - this._scrubberRadius / this.pixelMsRatio(), fill: "forwards" });
517         this._scrubberPlayer.playbackRate = this._playbackRate();
518         this._scrubberPlayer.onfinish = this._updateControlButton.bind(this);
519         this._updateControlButton();
521         if (time !== undefined)
522             this._scrubberPlayer.currentTime = time;
523         else if (oldPlayer.playState === "finished")
524             this._scrubberPlayer.finish();
525         else
526             this._scrubberPlayer.startTime = oldPlayer.startTime;
528         if (oldPlayer)
529             oldPlayer.cancel();
530         this._timelineScrubber.classList.remove("animation-timeline-end");
531         this._timelineScrubberHead.window().requestAnimationFrame(this._updateScrubber.bind(this));
532     },
534     /**
535      * @return {number}
536      */
537     pixelMsRatio: function()
538     {
539         return this.width() / this.duration() || 0;
540     },
542     /**
543      * @param {number} timestamp
544      */
545     _updateScrubber: function(timestamp)
546     {
547         if (!this._scrubberPlayer)
548             return;
549         this._timelineScrubberHead.textContent = WebInspector.UIString(Number.millisToString(this._scrubberPlayer.currentTime));
550         if (this._scrubberPlayer.playState === "pending" || this._scrubberPlayer.playState === "running") {
551             this._timelineScrubberHead.window().requestAnimationFrame(this._updateScrubber.bind(this));
552         } else if (this._scrubberPlayer.playState === "finished") {
553             this._timelineScrubberHead.textContent = WebInspector.UIString(". . .");
554             this._timelineScrubber.classList.add("animation-timeline-end");
555         }
556     },
558     /**
559      * @param {!Event} event
560      * @return {boolean}
561      */
562     _scrubberDragStart: function(event)
563     {
564         if (!this._scrubberPlayer)
565             return false;
567         this._originalScrubberTime = this._scrubberPlayer.currentTime;
568         this._timelineScrubber.classList.remove("animation-timeline-end");
569         this._scrubberPlayer.pause();
570         this._originalMousePosition = new WebInspector.Geometry.Point(event.x, event.y);
572         this._togglePause(true);
573         this._updateControlButton();
574         return true;
575     },
577     /**
578      * @param {!Event} event
579      */
580     _scrubberDragMove: function(event)
581     {
582         var delta = event.x - this._originalMousePosition.x;
583         this._scrubberPlayer.currentTime = Math.min(this._originalScrubberTime + delta / this.pixelMsRatio(), this.duration() - this._scrubberRadius / this.pixelMsRatio());
584         var currentTime = Math.max(0, Math.round(this._scrubberPlayer.currentTime));
585         this._timelineScrubberHead.textContent = WebInspector.UIString(Number.millisToString(currentTime));
586         for (var target of WebInspector.targetManager.targets(WebInspector.Target.Type.Page))
587             target.animationAgent().setCurrentTime(/** @type {number} */(this.startTime() + currentTime));
588     },
590     /**
591      * @param {!Event} event
592      */
593     _scrubberDragEnd: function(event)
594     {
595         var currentTime = Math.max(0, this._scrubberPlayer.currentTime);
596         this._scrubberPlayer.play();
597         this._scrubberPlayer.currentTime = currentTime;
598         this._timelineScrubberHead.window().requestAnimationFrame(this._updateScrubber.bind(this));
599     },
601     __proto__: WebInspector.VBox.prototype
605  * @constructor
606  * @param {!WebInspector.AnimationModel.AnimationEffect} animationEffect
607  */
608 WebInspector.AnimationTimeline.NodeUI = function(animationEffect) {
609     /**
610      * @param {?WebInspector.DOMNode} node
611      * @this {WebInspector.AnimationTimeline.NodeUI}
612      */
613     function nodeResolved(node)
614     {
615         if (!node)
616             return;
617         this._node = node;
618         WebInspector.DOMPresentationUtils.decorateNodeLabel(node, this._description);
619         this.element.addEventListener("click", WebInspector.Revealer.reveal.bind(WebInspector.Revealer, node, undefined), false);
620     }
622     this._rows = [];
623     this.element = createElementWithClass("div", "animation-node-row");
624     this._description = this.element.createChild("div", "animation-node-description");
625     animationEffect.deferredNode().resolve(nodeResolved.bind(this));
626     this._timelineElement = this.element.createChild("div", "animation-node-timeline");
629 /** @typedef {{element: !Element, animations: !Array<!WebInspector.AnimationUI>}} */
630 WebInspector.AnimationTimeline.NodeRow;
632 WebInspector.AnimationTimeline.NodeUI.prototype = {
633     /**
634      * @param {!WebInspector.AnimationModel.Animation} animation
635      * @return {!WebInspector.AnimationTimeline.NodeRow}
636      */
637     findRow: function(animation)
638     {
639         // Check if it can fit into an existing row
640         var existingRow = this._collapsibleIntoRow(animation);
641         if (existingRow)
642             return existingRow;
644         // Create new row
645         var container = this._timelineElement.createChild("div", "animation-timeline-row");
646         var nodeRow = {element: container, animations: []};
647         this._rows.push(nodeRow);
648         return nodeRow;
649     },
651     redraw: function()
652     {
653         for (var nodeRow of this._rows) {
654             for (var ui of nodeRow.animations)
655                 ui.redraw();
656         }
657     },
659     /**
660      * @param {!WebInspector.AnimationModel.Animation} animation
661      * @return {?WebInspector.AnimationTimeline.NodeRow}
662      */
663     _collapsibleIntoRow: function(animation)
664     {
665         if (animation.endTime() === Infinity)
666             return null;
667         for (var nodeRow of this._rows) {
668             var overlap = false;
669             for (var ui of nodeRow.animations)
670                 overlap |= animation.overlaps(ui.animation());
671             if (!overlap)
672                 return nodeRow;
673         }
674         return null;
675     },
677     nodeRemoved: function()
678     {
679         this.element.classList.add("animation-node-removed");
680     },
682     /**
683      * @param {?WebInspector.DOMNode} node
684      */
685     setNode: function(node)
686     {
687         this.element.classList.toggle("animation-node-selected", node === this._node);
688     }
692  * @constructor
693  * @param {number} steps
694  * @param {string} stepAtPosition
695  */
696 WebInspector.AnimationTimeline.StepTimingFunction = function(steps, stepAtPosition)
698     this.steps = steps;
699     this.stepAtPosition = stepAtPosition;
703  * @param {string} text
704  * @return {?WebInspector.AnimationTimeline.StepTimingFunction}
705  */
706 WebInspector.AnimationTimeline.StepTimingFunction.parse = function(text) {
707     var match = text.match(/^step-(start|middle|end)$/);
708     if (match)
709         return new WebInspector.AnimationTimeline.StepTimingFunction(1, match[1]);
710     match = text.match(/^steps\((\d+), (start|middle|end)\)$/);
711     if (match)
712         return new WebInspector.AnimationTimeline.StepTimingFunction(parseInt(match[1], 10), match[2]);
713     return null;
717  * @constructor
718  * @param {!WebInspector.AnimationModel.Animation} animation
719  * @param {!WebInspector.AnimationTimeline} timeline
720  * @param {!Element} parentElement
721  */
722 WebInspector.AnimationUI = function(animation, timeline, parentElement) {
723     this._animation = animation;
724     this._timeline = timeline;
725     this._parentElement = parentElement;
727     if (this._animation.source().keyframesRule())
728         this._keyframes =  this._animation.source().keyframesRule().keyframes();
730     this._nameElement = parentElement.createChild("div", "animation-name");
731     this._nameElement.textContent = this._animation.name();
733     this._svg = parentElement.createSVGChild("svg", "animation-ui");
734     this._svg.setAttribute("height", WebInspector.AnimationUI.Options.AnimationSVGHeight);
735     this._svg.style.marginLeft = "-" + WebInspector.AnimationUI.Options.AnimationMargin + "px";
736     this._svg.addEventListener("mousedown", this._mouseDown.bind(this, WebInspector.AnimationUI.MouseEvents.AnimationDrag, null));
737     this._activeIntervalGroup = this._svg.createSVGChild("g");
739     /** @type {!Array.<{group: ?Element, animationLine: ?Element, keyframePoints: !Object.<number, !Element>, keyframeRender: !Object.<number, !Element>}>} */
740     this._cachedElements = [];
742     this._movementInMs = 0;
743     this.redraw();
747  * @enum {string}
748  */
749 WebInspector.AnimationUI.MouseEvents = {
750     AnimationDrag: "AnimationDrag",
751     KeyframeMove: "KeyframeMove",
752     StartEndpointMove: "StartEndpointMove",
753     FinishEndpointMove: "FinishEndpointMove"
756 WebInspector.AnimationUI.prototype = {
757     /**
758      * @return {!WebInspector.AnimationModel.Animation}
759      */
760     animation: function()
761     {
762         return this._animation;
763     },
765     /**
766      * @param {?WebInspector.DOMNode} node
767      */
768     setNode: function(node)
769     {
770         this._node = node;
771     },
773     /**
774      * @param {!Element} parentElement
775      * @param {string} className
776      */
777     _createLine: function(parentElement, className)
778     {
779         var line = parentElement.createSVGChild("line", className);
780         line.setAttribute("x1", WebInspector.AnimationUI.Options.AnimationMargin);
781         line.setAttribute("y1", WebInspector.AnimationUI.Options.AnimationHeight);
782         line.setAttribute("y2", WebInspector.AnimationUI.Options.AnimationHeight);
783         line.style.stroke = this._color();
784         return line;
785     },
787     /**
788      * @param {number} iteration
789      * @param {!Element} parentElement
790      */
791     _drawAnimationLine: function(iteration, parentElement)
792     {
793         var cache = this._cachedElements[iteration];
794         if (!cache.animationLine)
795             cache.animationLine = this._createLine(parentElement, "animation-line");
796         cache.animationLine.setAttribute("x2", (this._duration() * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin).toFixed(2));
797     },
799     /**
800      * @param {!Element} parentElement
801      */
802     _drawDelayLine: function(parentElement)
803     {
804         if (!this._delayLine) {
805             this._delayLine = this._createLine(parentElement, "animation-delay-line");
806             this._endDelayLine = this._createLine(parentElement, "animation-delay-line");
807         }
808         this._delayLine.setAttribute("x1", WebInspector.AnimationUI.Options.AnimationMargin);
809         this._delayLine.setAttribute("x2", (this._delay() * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin).toFixed(2));
810         var leftMargin = (this._delay() + this._duration() * this._animation.source().iterations()) * this._timeline.pixelMsRatio();
811         this._endDelayLine.style.transform = "translateX(" + Math.min(leftMargin, this._timeline.width()).toFixed(2) + "px)";
812         this._endDelayLine.setAttribute("x1", WebInspector.AnimationUI.Options.AnimationMargin);
813         this._endDelayLine.setAttribute("x2", (this._animation.source().endDelay() * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin).toFixed(2));
814     },
816     /**
817      * @param {number} iteration
818      * @param {!Element} parentElement
819      * @param {number} x
820      * @param {number} keyframeIndex
821      * @param {boolean} attachEvents
822      */
823     _drawPoint: function(iteration, parentElement, x, keyframeIndex, attachEvents)
824     {
825         if (this._cachedElements[iteration].keyframePoints[keyframeIndex]) {
826             this._cachedElements[iteration].keyframePoints[keyframeIndex].setAttribute("cx", x.toFixed(2));
827             return;
828         }
830         var circle = parentElement.createSVGChild("circle", keyframeIndex <= 0 ? "animation-endpoint" : "animation-keyframe-point");
831         circle.setAttribute("cx", x.toFixed(2));
832         circle.setAttribute("cy", WebInspector.AnimationUI.Options.AnimationHeight);
833         circle.style.stroke = this._color();
834         circle.setAttribute("r", WebInspector.AnimationUI.Options.AnimationMargin / 2);
836         if (keyframeIndex <= 0)
837             circle.style.fill = this._color();
839         this._cachedElements[iteration].keyframePoints[keyframeIndex] = circle;
841         if (!attachEvents)
842             return;
844         if (keyframeIndex === 0) {
845             circle.addEventListener("mousedown", this._mouseDown.bind(this, WebInspector.AnimationUI.MouseEvents.StartEndpointMove, keyframeIndex));
846         } else if (keyframeIndex === -1) {
847             circle.addEventListener("mousedown", this._mouseDown.bind(this, WebInspector.AnimationUI.MouseEvents.FinishEndpointMove, keyframeIndex));
848         } else {
849             circle.addEventListener("mousedown", this._mouseDown.bind(this, WebInspector.AnimationUI.MouseEvents.KeyframeMove, keyframeIndex));
850         }
851     },
853     /**
854      * @param {number} iteration
855      * @param {number} keyframeIndex
856      * @param {!Element} parentElement
857      * @param {number} leftDistance
858      * @param {number} width
859      * @param {string} easing
860      */
861     _renderKeyframe: function(iteration, keyframeIndex, parentElement, leftDistance, width, easing)
862     {
863         /**
864          * @param {!Element} parentElement
865          * @param {number} x
866          * @param {string} strokeColor
867          */
868         function createStepLine(parentElement, x, strokeColor)
869         {
870             var line = parentElement.createSVGChild("line");
871             line.setAttribute("x1", x);
872             line.setAttribute("x2", x);
873             line.setAttribute("y1", WebInspector.AnimationUI.Options.AnimationMargin);
874             line.setAttribute("y2", WebInspector.AnimationUI.Options.AnimationHeight);
875             line.style.stroke = strokeColor;
876         }
878         var bezier = WebInspector.Geometry.CubicBezier.parse(easing);
879         var cache = this._cachedElements[iteration].keyframeRender;
880         if (!cache[keyframeIndex])
881             cache[keyframeIndex] = bezier ? parentElement.createSVGChild("path", "animation-keyframe") : parentElement.createSVGChild("g", "animation-keyframe-step");
882         var group = cache[keyframeIndex];
883         group.style.transform = "translateX(" + leftDistance.toFixed(2) + "px)";
885         if (bezier) {
886             group.style.fill = this._color();
887             WebInspector.BezierUI.drawVelocityChart(bezier, group, width);
888         } else {
889             var stepFunction = WebInspector.AnimationTimeline.StepTimingFunction.parse(easing);
890             group.removeChildren();
891             const offsetMap = {"start": 0, "middle": 0.5, "end": 1};
892             const offsetWeight = offsetMap[stepFunction.stepAtPosition];
893             for (var i = 0; i < stepFunction.steps; i++)
894                 createStepLine(group, (i + offsetWeight) * width / stepFunction.steps, this._color());
895         }
896     },
898     redraw: function()
899     {
900         var durationWithDelay = this._delay() + this._duration() * this._animation.source().iterations() + this._animation.source().endDelay();
901         var leftMargin = ((this._animation.startTime() - this._timeline.startTime()) * this._timeline.pixelMsRatio());
902         var maxWidth = this._timeline.width() - WebInspector.AnimationUI.Options.AnimationMargin - leftMargin;
903         var svgWidth = Math.min(maxWidth, durationWithDelay * this._timeline.pixelMsRatio());
905         this._svg.classList.toggle("animation-ui-canceled", this._animation.playState() === "idle");
906         this._svg.setAttribute("width", (svgWidth + 2 * WebInspector.AnimationUI.Options.AnimationMargin).toFixed(2));
907         this._svg.style.transform = "translateX(" + leftMargin.toFixed(2)  + "px)";
908         this._activeIntervalGroup.style.transform = "translateX(" + (this._delay() * this._timeline.pixelMsRatio()).toFixed(2) + "px)";
910         this._nameElement.style.transform = "translateX(" + (leftMargin + this._delay() * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin).toFixed(2) + "px)";
911         this._nameElement.style.width = (this._duration() * this._timeline.pixelMsRatio().toFixed(2)) + "px";
912         this._drawDelayLine(this._svg);
914         if (this._animation.type() === "CSSTransition") {
915             this._renderTransition();
916             return;
917         }
919         this._renderIteration(this._activeIntervalGroup, 0);
920         if (!this._tailGroup)
921             this._tailGroup = this._activeIntervalGroup.createSVGChild("g", "animation-tail-iterations");
922         var iterationWidth = this._duration() * this._timeline.pixelMsRatio();
923         for (var iteration = 1; iteration < this._animation.source().iterations() && iterationWidth * (iteration - 1) < this._timeline.width(); iteration++)
924             this._renderIteration(this._tailGroup, iteration);
925         while (iteration < this._cachedElements.length)
926             this._cachedElements.pop().group.remove();
927     },
930     _renderTransition: function()
931     {
932         if (!this._cachedElements[0])
933             this._cachedElements[0] = { animationLine: null, keyframePoints: {}, keyframeRender: {}, group: null };
934         this._drawAnimationLine(0, this._activeIntervalGroup);
935         this._renderKeyframe(0, 0, this._activeIntervalGroup, WebInspector.AnimationUI.Options.AnimationMargin, this._duration() * this._timeline.pixelMsRatio(), this._animation.source().easing());
936         this._drawPoint(0, this._activeIntervalGroup, WebInspector.AnimationUI.Options.AnimationMargin, 0, true);
937         this._drawPoint(0, this._activeIntervalGroup, this._duration() * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin, -1, true);
938     },
940     /**
941      * @param {!Element} parentElement
942      * @param {number} iteration
943      */
944     _renderIteration: function(parentElement, iteration)
945     {
946         if (!this._cachedElements[iteration])
947             this._cachedElements[iteration] = { animationLine: null, keyframePoints: {}, keyframeRender: {}, group: parentElement.createSVGChild("g") };
948         var group = this._cachedElements[iteration].group;
949         group.style.transform = "translateX(" + (iteration * this._duration() * this._timeline.pixelMsRatio()).toFixed(2) + "px)";
950         this._drawAnimationLine(iteration, group);
951         console.assert(this._keyframes.length > 1);
952         for (var i = 0; i < this._keyframes.length - 1; i++) {
953             var leftDistance = this._offset(i) * this._duration() * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin;
954             var width = this._duration() * (this._offset(i + 1) - this._offset(i)) * this._timeline.pixelMsRatio();
955             this._renderKeyframe(iteration, i, group, leftDistance, width, this._keyframes[i].easing());
956             if (i || (!i && iteration === 0))
957                 this._drawPoint(iteration, group, leftDistance, i, iteration === 0);
958         }
959         this._drawPoint(iteration, group, this._duration() * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin, -1, iteration === 0);
960     },
962     /**
963      * @return {number}
964      */
965     _delay: function()
966     {
967         var delay = this._animation.source().delay();
968         if (this._mouseEventType === WebInspector.AnimationUI.MouseEvents.AnimationDrag || this._mouseEventType === WebInspector.AnimationUI.MouseEvents.StartEndpointMove)
969             delay += this._movementInMs;
970         // FIXME: add support for negative start delay
971         return Math.max(0, delay);
972     },
974     /**
975      * @return {number}
976      */
977     _duration: function()
978     {
979         var duration = this._animation.source().duration();
980         if (this._mouseEventType === WebInspector.AnimationUI.MouseEvents.FinishEndpointMove)
981             duration += this._movementInMs;
982         else if (this._mouseEventType === WebInspector.AnimationUI.MouseEvents.StartEndpointMove)
983             duration -= Math.max(this._movementInMs, -this._animation.source().delay()); // Cannot have negative delay
984         return Math.max(0, duration);
985     },
987     /**
988      * @param {number} i
989      * @return {number} offset
990      */
991     _offset: function(i)
992     {
993         var offset = this._keyframes[i].offsetAsNumber();
994         if (this._mouseEventType === WebInspector.AnimationUI.MouseEvents.KeyframeMove && i === this._keyframeMoved) {
995             console.assert(i > 0 && i < this._keyframes.length - 1, "First and last keyframe cannot be moved");
996             offset += this._movementInMs / this._animation.source().duration();
997             offset = Math.max(offset, this._keyframes[i - 1].offsetAsNumber());
998             offset = Math.min(offset, this._keyframes[i + 1].offsetAsNumber());
999         }
1000         return offset;
1001     },
1003     /**
1004      * @param {!WebInspector.AnimationUI.MouseEvents} mouseEventType
1005      * @param {?number} keyframeIndex
1006      * @param {!Event} event
1007      */
1008     _mouseDown: function(mouseEventType, keyframeIndex, event)
1009     {
1010         if (this._animation.playState() === "idle")
1011             return;
1012         this._mouseEventType = mouseEventType;
1013         this._keyframeMoved = keyframeIndex;
1014         this._downMouseX = event.clientX;
1015         this._mouseMoveHandler = this._mouseMove.bind(this);
1016         this._mouseUpHandler = this._mouseUp.bind(this);
1017         this._parentElement.ownerDocument.addEventListener("mousemove", this._mouseMoveHandler);
1018         this._parentElement.ownerDocument.addEventListener("mouseup", this._mouseUpHandler);
1019         event.preventDefault();
1020         event.stopPropagation();
1022         if (this._node)
1023             WebInspector.Revealer.reveal(this._node);
1024     },
1026     /**
1027      * @param {!Event} event
1028      */
1029     _mouseMove: function (event)
1030     {
1031         this._movementInMs = (event.clientX - this._downMouseX) / this._timeline.pixelMsRatio();
1032         if (this._animation.startTime() + this._delay() + this._duration() - this._timeline.startTime() > this._timeline.duration() * 0.8)
1033             this._timeline.setDuration(this._timeline.duration() * 1.2);
1034         this.redraw();
1035     },
1037     /**
1038      * @param {!Event} event
1039      */
1040     _mouseUp: function(event)
1041     {
1042         this._movementInMs = (event.clientX - this._downMouseX) / this._timeline.pixelMsRatio();
1044         // Commit changes
1045         if (this._mouseEventType === WebInspector.AnimationUI.MouseEvents.KeyframeMove) {
1046             this._keyframes[this._keyframeMoved].setOffset(this._offset(this._keyframeMoved));
1047         } else {
1048             var delay = this._delay();
1049             var duration = this._duration();
1050             this._setDelay(delay);
1051             this._setDuration(duration);
1052             if (this._animation.type() !== "CSSAnimation") {
1053                 for (var target of WebInspector.targetManager.targets(WebInspector.Target.Type.Page))
1054                     target.animationAgent().setTiming(this._animation.id(), duration, delay);
1055             }
1056         }
1058         this._movementInMs = 0;
1059         this.redraw();
1061         this._parentElement.ownerDocument.removeEventListener("mousemove", this._mouseMoveHandler);
1062         this._parentElement.ownerDocument.removeEventListener("mouseup", this._mouseUpHandler);
1063         delete this._mouseMoveHandler;
1064         delete this._mouseUpHandler;
1065         delete this._mouseEventType;
1066         delete this._downMouseX;
1067         delete this._keyframeMoved;
1068     },
1070     /**
1071      * @param {number} value
1072      */
1073     _setDelay: function(value)
1074     {
1075         if (!this._node || this._animation.source().delay() == this._delay())
1076             return;
1078         this._animation.source().setDelay(this._delay());
1079         var propertyName;
1080         if (this._animation.type() == "CSSTransition")
1081             propertyName = "transition-delay";
1082         else if (this._animation.type() == "CSSAnimation")
1083             propertyName = "animation-delay";
1084         else
1085             return;
1086         this._setNodeStyle(propertyName, Math.round(value) + "ms");
1087     },
1089     /**
1090      * @param {number} value
1091      */
1092     _setDuration: function(value)
1093     {
1094         if (!this._node || this._animation.source().duration() == value)
1095             return;
1097         this._animation.source().setDuration(value);
1098         var propertyName;
1099         if (this._animation.type() == "CSSTransition")
1100             propertyName = "transition-duration";
1101         else if (this._animation.type() == "CSSAnimation")
1102             propertyName = "animation-duration";
1103         else
1104             return;
1105         this._setNodeStyle(propertyName, Math.round(value) + "ms");
1106     },
1108     /**
1109      * @param {string} name
1110      * @param {string} value
1111      */
1112     _setNodeStyle: function(name, value)
1113     {
1114         var style = this._node.getAttribute("style") || "";
1115         if (style)
1116             style = style.replace(new RegExp("\\s*(-webkit-)?" + name + ":[^;]*;?\\s*", "g"), "");
1117         var valueString = name + ": " + value;
1118         this._node.setAttributeValue("style", style + " " + valueString + "; -webkit-" + valueString + ";");
1119     },
1121     /**
1122      * @return {string}
1123      */
1124     _color: function()
1125     {
1126         /**
1127          * @param {string} string
1128          * @return {number}
1129          */
1130         function hash(string)
1131         {
1132             var hash = 0;
1133             for (var i = 0; i < string.length; i++)
1134                 hash = (hash << 5) + hash + string.charCodeAt(i);
1135             return Math.abs(hash);
1136         }
1138         if (!this._selectedColor) {
1139             var names = Object.keys(WebInspector.AnimationUI.Colors);
1140             var color = WebInspector.AnimationUI.Colors[names[hash(this._animation.name() || this._animation.id()) % names.length]];
1141             this._selectedColor = color.asString(WebInspector.Color.Format.RGB);
1142         }
1143         return this._selectedColor;
1144     }
1147 WebInspector.AnimationUI.Options = {
1148     AnimationHeight: 32,
1149     AnimationSVGHeight: 80,
1150     AnimationMargin: 7,
1151     EndpointsClickRegionSize: 10,
1152     GridCanvasHeight: 40
1155 WebInspector.AnimationUI.Colors = {
1156     "Purple": WebInspector.Color.parse("#9C27B0"),
1157     "Light Blue": WebInspector.Color.parse("#03A9F4"),
1158     "Deep Orange": WebInspector.Color.parse("#FF5722"),
1159     "Blue": WebInspector.Color.parse("#5677FC"),
1160     "Lime": WebInspector.Color.parse("#CDDC39"),
1161     "Pink": WebInspector.Color.parse("#E91E63"),
1162     "Green": WebInspector.Color.parse("#0F9D58"),
1163     "Brown": WebInspector.Color.parse("#795548"),
1164     "Cyan": WebInspector.Color.parse("#00BCD4")