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.
7 * @extends {WebInspector.VBox}
8 * @implements {WebInspector.TargetManager.Observer}
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 = {
48 for (var target of WebInspector.targetManager.targets(WebInspector.Target.Type.Page))
49 this._addEventListeners(target);
54 for (var target of WebInspector.targetManager.targets(WebInspector.Target.Type.Page))
55 this._removeEventListeners(target);
60 * @param {!WebInspector.Target} target
62 targetAdded: function(target)
65 this._addEventListeners(target);
70 * @param {!WebInspector.Target} target
72 targetRemoved: function(target)
74 this._removeEventListeners(target);
78 * @param {!WebInspector.Target} target
80 _addEventListeners: function(target)
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);
89 * @param {!WebInspector.Target} target
91 _removeEventListeners: function(target)
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);
99 * @param {?WebInspector.DOMNode} node
101 setNode: function(node)
103 for (var nodeUI of this._nodesMap.values())
104 nodeUI.setNode(node);
108 * @return {!Element} element
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;
125 _createHeader: function()
128 * @param {!Event} event
129 * @this {WebInspector.AnimationTimeline}
131 function playbackSliderInputHandler(event)
133 this._underlyingPlaybackRate = WebInspector.AnimationTimeline.GlobalPlaybackRates[event.target.value];
134 this._updatePlaybackControls();
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();
164 * @param {!Event} event
166 _playbackLabelInput: function(event)
168 var element = /** @type {!Element} */(event.currentTarget);
169 if (event.type !== "focusout" && !WebInspector.handleElementValueModifications(event, element) && !isEnterKey(event))
172 var value = parseFloat(this._playbackLabel.textContent);
174 this._underlyingPlaybackRate = Math.max(0, value);
175 this._updatePlaybackControls();
179 _updatePlaybackControls: function()
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++;
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();
196 _controlButtonToggle: function()
198 if (this._emptyTimelineMessage)
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"))
205 this._togglePause(true);
206 this._updateControlButton();
209 _updateControlButton: function()
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");
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"));
221 this._controlButton.element.classList.add("pause-outline-toolbar-item");
222 this._controlButton.setTitle(WebInspector.UIString("Pause timeline"));
226 _updateAnimationsPlaybackRate: function()
229 * @param {?Protocol.Error} error
230 * @param {number} playbackRate
231 * @this {WebInspector.AnimationTimeline}
233 function setPlaybackRate(error, playbackRate)
235 if (playbackRate === 0) {
238 WebInspector.AnimationModel.fromTarget(target).setPlaybackRate(1);
240 this._underlyingPlaybackRate = playbackRate;
241 this._updatePlaybackControls();
245 for (var target of WebInspector.targetManager.targets(WebInspector.Target.Type.Page))
246 target.animationAgent().getPlaybackRate(setPlaybackRate.bind(this));
252 _playbackRate: function()
254 return this._paused ? 0 : this._underlyingPlaybackRate;
258 * @param {boolean} pause
260 _togglePause: function(pause)
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();
272 if (this.startTime() === undefined)
274 for (var target of WebInspector.targetManager.targets(WebInspector.Target.Type.Page))
275 target.animationAgent().setCurrentTime(/** @type {number} */(this.startTime()));
277 this._animateTime(0);
283 _defaultDuration: function ()
293 return this._duration;
297 * @param {number} duration
299 setDuration: function(duration)
301 this._duration = duration;
302 this.scheduleRedraw();
306 * @return {number|undefined}
308 startTime: function()
310 return this._startTime;
315 if (!this._nodesMap.size)
318 this._nodesMap.clear();
319 this._animationsMap.clear();
320 this._animationsContainer.removeChildren();
321 this._duration = this._defaultDuration();
322 delete this._startTime;
326 * @param {!WebInspector.Event} event
328 _mainFrameNavigated: function(event)
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();
340 * @param {!WebInspector.Event} event
342 _animationCreated: function(event)
344 this._addAnimation(/** @type {!WebInspector.AnimationModel.Animation} */ (event.data.player), event.data.resetTimeline)
348 * @param {!WebInspector.AnimationModel.Animation} animation
349 * @param {boolean} resetTimeline
351 _addAnimation: function(animation, resetTimeline)
354 * @param {?WebInspector.DOMNode} node
355 * @this {WebInspector.AnimationTimeline}
357 function nodeResolved(node)
361 uiAnimation.setNode(node);
362 node[this._symbol] = nodeUI;
365 if (this._emptyTimelineMessage) {
366 this._emptyTimelineMessage.remove();
367 delete this._emptyTimelineMessage;
373 // Ignore Web Animations custom effects & groups
374 if (animation.type() === "WebAnimation" && animation.source().keyframesRule().keyframes().length === 0)
377 if (this._resizeWindow(animation))
378 this.scheduleRedraw();
380 var nodeUI = this._nodesMap.get(animation.source().backendNodeId());
382 nodeUI = new WebInspector.AnimationTimeline.NodeUI(animation.source());
383 this._animationsContainer.appendChild(nodeUI.element);
384 this._nodesMap.set(animation.source().backendNodeId(), nodeUI);
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);
394 * @param {!WebInspector.Event} event
396 _animationCanceled: function(event)
398 this._cancelAnimation(/** @type {string} */ (event.data.id));
402 * @param {string} playerId
404 _cancelAnimation: function(playerId)
406 var animation = this._animationsMap.get(playerId);
409 animation.setPlayState("idle");
410 this.scheduleRedraw();
414 * @param {!WebInspector.Event} event
416 _nodeRemoved: function(event)
418 var node = event.data.node;
419 if (node[this._symbol])
420 node[this._symbol].nodeRemoved();
423 _renderGrid: function()
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);
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));
450 scheduleRedraw: function() {
453 this._redrawing = true;
454 this._animationsContainer.window().requestAnimationFrame(this._redraw.bind(this));
458 * @param {number=} timestamp
460 _redraw: function(timestamp)
462 delete this._redrawing;
463 for (var nodeUI of this._nodesMap.values())
470 this._cachedTimelineWidth = Math.max(0, this._animationsContainer.offsetWidth - this._timelineControlsWidth) || 0;
471 this.scheduleRedraw();
472 if (this._scrubberPlayer)
481 return this._cachedTimelineWidth || 0;
485 * @param {!WebInspector.AnimationModel.Animation} animation
488 _resizeWindow: function(animation)
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) {
499 this._duration = requiredDuration * 1.5;
500 this._timelineScrubber.classList.remove("hidden");
501 this._animateTime(animation.startTime() - this.startTime());
507 * @param {number=} time
509 _animateTime: function(time)
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();
526 this._scrubberPlayer.startTime = oldPlayer.startTime;
530 this._timelineScrubber.classList.remove("animation-timeline-end");
531 this._timelineScrubberHead.window().requestAnimationFrame(this._updateScrubber.bind(this));
537 pixelMsRatio: function()
539 return this.width() / this.duration() || 0;
543 * @param {number} timestamp
545 _updateScrubber: function(timestamp)
547 if (!this._scrubberPlayer)
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");
559 * @param {!Event} event
562 _scrubberDragStart: function(event)
564 if (!this._scrubberPlayer)
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();
578 * @param {!Event} event
580 _scrubberDragMove: function(event)
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));
591 * @param {!Event} event
593 _scrubberDragEnd: function(event)
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));
601 __proto__: WebInspector.VBox.prototype
606 * @param {!WebInspector.AnimationModel.AnimationEffect} animationEffect
608 WebInspector.AnimationTimeline.NodeUI = function(animationEffect) {
610 * @param {?WebInspector.DOMNode} node
611 * @this {WebInspector.AnimationTimeline.NodeUI}
613 function nodeResolved(node)
618 WebInspector.DOMPresentationUtils.decorateNodeLabel(node, this._description);
619 this.element.addEventListener("click", WebInspector.Revealer.reveal.bind(WebInspector.Revealer, node, undefined), false);
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 = {
634 * @param {!WebInspector.AnimationModel.Animation} animation
635 * @return {!WebInspector.AnimationTimeline.NodeRow}
637 findRow: function(animation)
639 // Check if it can fit into an existing row
640 var existingRow = this._collapsibleIntoRow(animation);
645 var container = this._timelineElement.createChild("div", "animation-timeline-row");
646 var nodeRow = {element: container, animations: []};
647 this._rows.push(nodeRow);
653 for (var nodeRow of this._rows) {
654 for (var ui of nodeRow.animations)
660 * @param {!WebInspector.AnimationModel.Animation} animation
661 * @return {?WebInspector.AnimationTimeline.NodeRow}
663 _collapsibleIntoRow: function(animation)
665 if (animation.endTime() === Infinity)
667 for (var nodeRow of this._rows) {
669 for (var ui of nodeRow.animations)
670 overlap |= animation.overlaps(ui.animation());
677 nodeRemoved: function()
679 this.element.classList.add("animation-node-removed");
683 * @param {?WebInspector.DOMNode} node
685 setNode: function(node)
687 this.element.classList.toggle("animation-node-selected", node === this._node);
693 * @param {number} steps
694 * @param {string} stepAtPosition
696 WebInspector.AnimationTimeline.StepTimingFunction = function(steps, stepAtPosition)
699 this.stepAtPosition = stepAtPosition;
703 * @param {string} text
704 * @return {?WebInspector.AnimationTimeline.StepTimingFunction}
706 WebInspector.AnimationTimeline.StepTimingFunction.parse = function(text) {
707 var match = text.match(/^step-(start|middle|end)$/);
709 return new WebInspector.AnimationTimeline.StepTimingFunction(1, match[1]);
710 match = text.match(/^steps\((\d+), (start|middle|end)\)$/);
712 return new WebInspector.AnimationTimeline.StepTimingFunction(parseInt(match[1], 10), match[2]);
718 * @param {!WebInspector.AnimationModel.Animation} animation
719 * @param {!WebInspector.AnimationTimeline} timeline
720 * @param {!Element} parentElement
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;
749 WebInspector.AnimationUI.MouseEvents = {
750 AnimationDrag: "AnimationDrag",
751 KeyframeMove: "KeyframeMove",
752 StartEndpointMove: "StartEndpointMove",
753 FinishEndpointMove: "FinishEndpointMove"
756 WebInspector.AnimationUI.prototype = {
758 * @return {!WebInspector.AnimationModel.Animation}
760 animation: function()
762 return this._animation;
766 * @param {?WebInspector.DOMNode} node
768 setNode: function(node)
774 * @param {!Element} parentElement
775 * @param {string} className
777 _createLine: function(parentElement, className)
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();
788 * @param {number} iteration
789 * @param {!Element} parentElement
791 _drawAnimationLine: function(iteration, parentElement)
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));
800 * @param {!Element} parentElement
802 _drawDelayLine: function(parentElement)
804 if (!this._delayLine) {
805 this._delayLine = this._createLine(parentElement, "animation-delay-line");
806 this._endDelayLine = this._createLine(parentElement, "animation-delay-line");
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));
817 * @param {number} iteration
818 * @param {!Element} parentElement
820 * @param {number} keyframeIndex
821 * @param {boolean} attachEvents
823 _drawPoint: function(iteration, parentElement, x, keyframeIndex, attachEvents)
825 if (this._cachedElements[iteration].keyframePoints[keyframeIndex]) {
826 this._cachedElements[iteration].keyframePoints[keyframeIndex].setAttribute("cx", x.toFixed(2));
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;
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));
849 circle.addEventListener("mousedown", this._mouseDown.bind(this, WebInspector.AnimationUI.MouseEvents.KeyframeMove, keyframeIndex));
854 * @param {number} iteration
855 * @param {number} keyframeIndex
856 * @param {!Element} parentElement
857 * @param {number} leftDistance
858 * @param {number} width
859 * @param {string} easing
861 _renderKeyframe: function(iteration, keyframeIndex, parentElement, leftDistance, width, easing)
864 * @param {!Element} parentElement
866 * @param {string} strokeColor
868 function createStepLine(parentElement, x, strokeColor)
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;
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)";
886 group.style.fill = this._color();
887 WebInspector.BezierUI.drawVelocityChart(bezier, group, width);
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());
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();
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();
930 _renderTransition: function()
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);
941 * @param {!Element} parentElement
942 * @param {number} iteration
944 _renderIteration: function(parentElement, iteration)
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);
959 this._drawPoint(iteration, group, this._duration() * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin, -1, iteration === 0);
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);
977 _duration: function()
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);
989 * @return {number} offset
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());
1004 * @param {!WebInspector.AnimationUI.MouseEvents} mouseEventType
1005 * @param {?number} keyframeIndex
1006 * @param {!Event} event
1008 _mouseDown: function(mouseEventType, keyframeIndex, event)
1010 if (this._animation.playState() === "idle")
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();
1023 WebInspector.Revealer.reveal(this._node);
1027 * @param {!Event} event
1029 _mouseMove: function (event)
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);
1038 * @param {!Event} event
1040 _mouseUp: function(event)
1042 this._movementInMs = (event.clientX - this._downMouseX) / this._timeline.pixelMsRatio();
1045 if (this._mouseEventType === WebInspector.AnimationUI.MouseEvents.KeyframeMove) {
1046 this._keyframes[this._keyframeMoved].setOffset(this._offset(this._keyframeMoved));
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);
1058 this._movementInMs = 0;
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;
1071 * @param {number} value
1073 _setDelay: function(value)
1075 if (!this._node || this._animation.source().delay() == this._delay())
1078 this._animation.source().setDelay(this._delay());
1080 if (this._animation.type() == "CSSTransition")
1081 propertyName = "transition-delay";
1082 else if (this._animation.type() == "CSSAnimation")
1083 propertyName = "animation-delay";
1086 this._setNodeStyle(propertyName, Math.round(value) + "ms");
1090 * @param {number} value
1092 _setDuration: function(value)
1094 if (!this._node || this._animation.source().duration() == value)
1097 this._animation.source().setDuration(value);
1099 if (this._animation.type() == "CSSTransition")
1100 propertyName = "transition-duration";
1101 else if (this._animation.type() == "CSSAnimation")
1102 propertyName = "animation-duration";
1105 this._setNodeStyle(propertyName, Math.round(value) + "ms");
1109 * @param {string} name
1110 * @param {string} value
1112 _setNodeStyle: function(name, value)
1114 var style = this._node.getAttribute("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 + ";");
1127 * @param {string} string
1130 function hash(string)
1133 for (var i = 0; i < string.length; i++)
1134 hash = (hash << 5) + hash + string.charCodeAt(i);
1135 return Math.abs(hash);
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);
1143 return this._selectedColor;
1147 WebInspector.AnimationUI.Options = {
1148 AnimationHeight: 32,
1149 AnimationSVGHeight: 80,
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")