Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(177)

Side by Side Diff: Source/devtools/front_end/elements/AnimationTimeline.js

Issue 1151263007: Devtools: Move animation to separate module (Closed) Base URL: svn://svn.chromium.org/blink/trunk
Patch Set: Created 5 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
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.
4
5 /**
6 * @constructor
7 * @extends {WebInspector.VBox}
8 * @implements {WebInspector.TargetManager.Observer}
9 */
10 WebInspector.AnimationTimeline = function()
11 {
12 WebInspector.VBox.call(this, true);
13 this.registerRequiredCSS("elements/animationTimeline.css");
14 this.element.classList.add("animations-timeline");
15
16 this._grid = this.contentElement.createSVGChild("svg", "animation-timeline-g rid");
17 this.contentElement.appendChild(this._createScrubber());
18 WebInspector.installDragHandle(this._timelineScrubberHead, this._scrubberDra gStart.bind(this), this._scrubberDragMove.bind(this), this._scrubberDragEnd.bind (this), "move");
19 this._timelineScrubberHead.textContent = WebInspector.UIString(Number.millis ToString(0));
20
21 this._underlyingPlaybackRate = 1;
22 this.contentElement.appendChild(this._createHeader());
23 this._animationsContainer = this.contentElement.createChild("div", "animatio n-timeline-rows");
24 this._duration = this._defaultDuration();
25 this._scrubberRadius = 25;
26 this._timelineControlsWidth = 200;
27 /** @type {!Map.<!DOMAgent.BackendNodeId, !WebInspector.AnimationTimeline.No deUI>} */
28 this._nodesMap = new Map();
29 this._symbol = Symbol("animationTimeline");
30 /** @type {!Map.<string, !WebInspector.AnimationModel.AnimationPlayer>} */
31 this._animationsMap = new Map();
32 WebInspector.targetManager.addModelListener(WebInspector.ResourceTreeModel, WebInspector.ResourceTreeModel.EventTypes.MainFrameNavigated, this._mainFrameNav igated, this);
33 WebInspector.targetManager.addModelListener(WebInspector.DOMModel, WebInspec tor.DOMModel.Events.NodeRemoved, this._nodeRemoved, this);
34
35 WebInspector.targetManager.observeTargets(this, WebInspector.Target.Type.Pag e);
36 }
37
38 WebInspector.AnimationTimeline.GlobalPlaybackRates = [0.1, 0.25, 0.5, 1.0];
39
40 WebInspector.AnimationTimeline.prototype = {
41 wasShown: function()
42 {
43 for (var target of WebInspector.targetManager.targets(WebInspector.Targe t.Type.Page))
44 this._addEventListeners(target);
45 },
46
47 willHide: function()
48 {
49 for (var target of WebInspector.targetManager.targets(WebInspector.Targe t.Type.Page))
50 this._removeEventListeners(target);
51 },
52
53 /**
54 * @override
55 * @param {!WebInspector.Target} target
56 */
57 targetAdded: function(target)
58 {
59 if (this.isShowing())
60 this._addEventListeners(target);
61 },
62
63 /**
64 * @override
65 * @param {!WebInspector.Target} target
66 */
67 targetRemoved: function(target)
68 {
69 this._removeEventListeners(target);
70 },
71
72 /**
73 * @param {!WebInspector.Target} target
74 */
75 _addEventListeners: function(target)
76 {
77 target.animationModel.ensureEnabled();
78 target.animationModel.addEventListener(WebInspector.AnimationModel.Event s.AnimationPlayerCreated, this._animationCreated, this);
79 target.animationModel.addEventListener(WebInspector.AnimationModel.Event s.AnimationPlayerCanceled, this._animationCanceled, this);
80 },
81
82 /**
83 * @param {!WebInspector.Target} target
84 */
85 _removeEventListeners: function(target)
86 {
87 target.animationModel.removeEventListener(WebInspector.AnimationModel.Ev ents.AnimationPlayerCreated, this._animationCreated, this);
88 target.animationModel.removeEventListener(WebInspector.AnimationModel.Ev ents.AnimationPlayerCanceled, this._animationCanceled, this);
89 },
90
91 /**
92 * @param {?WebInspector.DOMNode} node
93 */
94 setNode: function(node)
95 {
96 for (var nodeUI of this._nodesMap.values())
97 nodeUI.setNode(node);
98 },
99
100 /**
101 * @return {!Element} element
102 */
103 _createScrubber: function() {
104 this._timelineScrubber = createElementWithClass("div", "animation-scrubb er hidden");
105 this._timelineScrubber.createChild("div", "animation-time-overlay");
106 this._timelineScrubber.createChild("div", "animation-scrubber-arrow");
107 this._timelineScrubberHead = this._timelineScrubber.createChild("div", " animation-scrubber-head");
108 var timerContainer = this._timelineScrubber.createChild("div", "animatio n-timeline-timer");
109 this._timerSpinner = timerContainer.createChild("div", "timer-spinner ti mer-hemisphere");
110 this._timerFiller = timerContainer.createChild("div", "timer-filler time r-hemisphere");
111 this._timerMask = timerContainer.createChild("div", "timer-mask");
112 return this._timelineScrubber;
113 },
114
115 /**
116 * @return {!Element}
117 */
118 _createHeader: function()
119 {
120 /**
121 * @param {!Event} event
122 * @this {WebInspector.AnimationTimeline}
123 */
124 function playbackSliderInputHandler(event)
125 {
126 this._underlyingPlaybackRate = WebInspector.AnimationTimeline.Global PlaybackRates[event.target.value];
127 var target = WebInspector.targetManager.mainTarget();
128 if (target)
129 target.animationModel.setPlaybackRate(this._playbackRate());
130 this._playbackLabel.textContent = this._underlyingPlaybackRate + "✕" ;
131 WebInspector.userMetrics.AnimationsPlaybackRateChanged.record();
132 this._updateTimelineAnimations();
133 }
134
135 var container = createElementWithClass("div", "animation-timeline-header ");
136 var controls = container.createChild("div", "animation-controls");
137 container.createChild("div", "animation-timeline-markers");
138
139 var toolbar = new WebInspector.Toolbar(controls);
140 toolbar.element.classList.add("animation-controls-toolbar");
141 this._pauseButton = new WebInspector.ToolbarButton(WebInspector.UIString ("Pause timeline"), "pause-toolbar-item");
142 this._pauseButton.addEventListener("click", this._togglePause.bind(this) );
143 toolbar.appendToolbarItem(this._pauseButton);
144 var replayButton = new WebInspector.ToolbarButton(WebInspector.UIString( "Replay timeline"), "replay-toolbar-item");
145 replayButton.addEventListener("click", this._replay.bind(this));
146 toolbar.appendToolbarItem(replayButton);
147
148 this._playbackLabel = controls.createChild("div", "animation-playback-la bel");
149 this._playbackLabel.createTextChild("1x");
150
151 this._playbackSlider = controls.createChild("input", "animation-playback -slider");
152 this._playbackSlider.type = "range";
153 this._playbackSlider.min = 0;
154 this._playbackSlider.max = WebInspector.AnimationTimeline.GlobalPlayback Rates.length - 1;
155 this._playbackSlider.value = this._playbackSlider.max;
156 this._playbackSlider.addEventListener("input", playbackSliderInputHandle r.bind(this));
157 this._updateAnimationsPlaybackRate();
158
159 return container;
160 },
161
162 _updateAnimationsPlaybackRate: function()
163 {
164 /**
165 * @param {?Protocol.Error} error
166 * @param {number} playbackRate
167 * @this {WebInspector.AnimationTimeline}
168 */
169 function setPlaybackRate(error, playbackRate)
170 {
171 if (playbackRate === 0) {
172 playbackRate = 1;
173 target.animationAgent().setPlaybackRate(1);
174 }
175 this._underlyingPlaybackRate = playbackRate;
176 this._playbackSlider.value = WebInspector.AnimationTimeline.GlobalPl aybackRates.indexOf(playbackRate);
177 this._playbackLabel.textContent = playbackRate + "✕";
178 }
179
180 var target = WebInspector.targetManager.mainTarget();
181 if (target)
182 target.animationAgent().getPlaybackRate(setPlaybackRate.bind(this));
183 },
184
185 /**
186 * @return {number}
187 */
188 _playbackRate: function()
189 {
190 return this._paused ? 0 : this._underlyingPlaybackRate;
191 },
192
193 _updateTimelineAnimations: function()
194 {
195 if (this._scrubberPlayer)
196 this._scrubberPlayer.playbackRate = this._playbackRate();
197 if (this._timerSpinnerPlayer) {
198 this._timerSpinnerPlayer.playbackRate = this._playbackRate();
199 this._timerFillerPlayer.playbackRate = this._playbackRate();
200 this._timerMaskPlayer.playbackRate = this._playbackRate();
201 }
202 },
203
204 _togglePause: function()
205 {
206 this._paused = !this._paused;
207 var target = WebInspector.targetManager.mainTarget();
208 if (target)
209 target.animationModel.setPlaybackRate(this._playbackRate());
210 WebInspector.userMetrics.AnimationsPlaybackRateChanged.record();
211 this._pauseButton.element.classList.toggle("pause-toolbar-item");
212 this._pauseButton.element.classList.toggle("play-toolbar-item");
213 this._updateTimelineAnimations();
214 },
215
216 _replay: function()
217 {
218 if (this.startTime() === undefined)
219 return;
220 var targets = WebInspector.targetManager.targets();
221 for (var target of targets)
222 target.animationAgent().setCurrentTime(/** @type {number} */(this.st artTime()));
223 this._animateTime(0);
224 },
225
226 /**
227 * @return {number}
228 */
229 _defaultDuration: function ()
230 {
231 return 100;
232 },
233
234 /**
235 * @return {number}
236 */
237 duration: function()
238 {
239 return this._duration;
240 },
241
242 /**
243 * @param {number} duration
244 */
245 setDuration: function(duration)
246 {
247 this._duration = duration;
248 this.scheduleRedraw();
249 },
250
251 /**
252 * @return {number|undefined}
253 */
254 startTime: function()
255 {
256 return this._startTime;
257 },
258
259 _reset: function()
260 {
261 if (!this._nodesMap.size)
262 return;
263
264 this._nodesMap.clear();
265 this._animationsMap.clear();
266 this._animationsContainer.removeChildren();
267 this._duration = this._defaultDuration();
268 delete this._startTime;
269 },
270
271 /**
272 * @param {!WebInspector.Event} event
273 */
274 _mainFrameNavigated: function(event)
275 {
276 this._reset();
277 this._updateAnimationsPlaybackRate();
278 if (this._scrubberPlayer)
279 this._scrubberPlayer.cancel();
280 delete this._scrubberPlayer;
281 this._timelineScrubberHead.textContent = WebInspector.UIString(Number.mi llisToString(0));
282 },
283
284 /**
285 * @param {!WebInspector.Event} event
286 */
287 _animationCreated: function(event)
288 {
289 this._addAnimation(/** @type {!WebInspector.AnimationModel.AnimationPlay er} */ (event.data.player), event.data.resetTimeline)
290 },
291
292 /**
293 * @param {!WebInspector.AnimationModel.AnimationPlayer} animation
294 * @param {boolean} resetTimeline
295 */
296 _addAnimation: function(animation, resetTimeline)
297 {
298 /**
299 * @param {?WebInspector.DOMNode} node
300 * @this {WebInspector.AnimationTimeline}
301 */
302 function nodeResolved(node)
303 {
304 uiAnimation.setNode(node);
305 node[this._symbol] = nodeUI;
306 }
307
308 if (resetTimeline)
309 this._reset();
310
311 // Ignore Web Animations custom effects & groups
312 if (animation.type() === "WebAnimation" && animation.source().keyframesR ule().keyframes().length === 0)
313 return;
314
315 if (this._resizeWindow(animation))
316 this.scheduleRedraw();
317 else
318 this._resetTimerAnimation();
319
320 var nodeUI = this._nodesMap.get(animation.source().backendNodeId());
321 if (!nodeUI) {
322 nodeUI = new WebInspector.AnimationTimeline.NodeUI(animation.source( ));
323 this._animationsContainer.appendChild(nodeUI.element);
324 this._nodesMap.set(animation.source().backendNodeId(), nodeUI);
325 }
326 var nodeRow = nodeUI.findRow(animation);
327 var uiAnimation = new WebInspector.AnimationUI(animation, this, nodeRow. element);
328 animation.source().deferredNode().resolve(nodeResolved.bind(this));
329 nodeRow.animations.push(uiAnimation);
330 this._animationsMap.set(animation.id(), animation);
331 },
332
333 /**
334 * @param {!WebInspector.Event} event
335 */
336 _animationCanceled: function(event)
337 {
338 this._cancelAnimation(/** @type {string} */ (event.data.playerId));
339 },
340
341 /**
342 * @param {string} playerId
343 */
344 _cancelAnimation: function(playerId)
345 {
346 var animation = this._animationsMap.get(playerId);
347 if (!animation)
348 return;
349 animation.setPlayState("idle");
350 this.scheduleRedraw();
351 },
352
353 /**
354 * @param {!WebInspector.Event} event
355 */
356 _nodeRemoved: function(event)
357 {
358 var node = event.data.node;
359 if (node[this._symbol])
360 node[this._symbol].nodeRemoved();
361 },
362
363 _renderGrid: function()
364 {
365 const gridSize = 250;
366 this._grid.setAttribute("width", this.width());
367 this._grid.setAttribute("height", this._animationsContainer.offsetHeight + 43);
368 this._grid.setAttribute("shape-rendering", "crispEdges");
369 this._grid.removeChildren();
370 var lastDraw = undefined;
371 for (var time = 0; time < this.duration(); time += gridSize) {
372 var line = this._grid.createSVGChild("rect", "animation-timeline-gri d-line");
373 line.setAttribute("x", time * this.pixelMsRatio());
374 line.setAttribute("y", 0);
375 line.setAttribute("height", "100%");
376 line.setAttribute("width", 1);
377 }
378 for (var time = 0; time < this.duration(); time += gridSize) {
379 var gridWidth = time * this.pixelMsRatio();
380 if (time && (!lastDraw || gridWidth - lastDraw > 50)) {
381 lastDraw = gridWidth;
382 var label = this._grid.createSVGChild("text", "animation-timelin e-grid-label");
383 label.setAttribute("x", gridWidth + 5);
384 label.setAttribute("y", 35);
385 label.textContent = WebInspector.UIString(Number.millisToString( time));
386 }
387 }
388 },
389
390 scheduleRedraw: function() {
391 if (this._redrawing)
392 return;
393 this._redrawing = true;
394 this._animationsContainer.window().requestAnimationFrame(this._redraw.bi nd(this));
395 },
396
397 /**
398 * @param {number=} timestamp
399 */
400 _redraw: function(timestamp)
401 {
402 delete this._redrawing;
403 for (var nodeUI of this._nodesMap.values())
404 nodeUI.redraw();
405 this._renderGrid();
406 },
407
408 onResize: function()
409 {
410 this._cachedTimelineWidth = Math.max(0, this._animationsContainer.offset Width - this._timelineControlsWidth) || 0;
411 this.scheduleRedraw();
412 if (this._scrubberPlayer)
413 this._animateTime();
414 },
415
416 /**
417 * @return {number}
418 */
419 width: function()
420 {
421 return this._cachedTimelineWidth || 0;
422 },
423
424 /**
425 * @param {!WebInspector.AnimationModel.AnimationPlayer} animation
426 * @return {boolean}
427 */
428 _resizeWindow: function(animation)
429 {
430 var resized = false;
431 if (!this._startTime)
432 this._startTime = animation.startTime();
433
434 // This shows at most 3 iterations
435 var duration = animation.source().duration() * Math.min(3, animation.sou rce().iterations());
436 var requiredDuration = animation.startTime() + animation.source().delay( ) + duration + animation.source().endDelay() - this.startTime();
437 if (requiredDuration > this._duration * 0.8) {
438 resized = true;
439 this._duration = requiredDuration * 1.5;
440 this._timelineScrubber.classList.remove("hidden");
441 this._animateTime(animation.startTime() - this.startTime(), true);
442 }
443 return resized;
444 },
445
446 _startTimerAnimation: function()
447 {
448 var timerDuration = 1000;
449 this._timerSpinnerPlayer = this._timerSpinner.animate([{ transform: "rot ate(0deg)" }, { transform: "rotate(360deg)" }], timerDuration);
450 this._timerSpinnerPlayer.playbackRate = this._playbackRate();
451 this._timerSpinnerPlayer.onfinish = this._timerFinished.bind(this, this. _timerSpinnerPlayer);
452 var keyframes = [{ opacity: 0 }, { opacity: 1 }];
453 this._timerFillerPlayer = this._timerFiller.animate(keyframes, { duratio n: timerDuration, easing: "steps(1, middle)" });
454 this._timerFillerPlayer.playbackRate = this._playbackRate();
455 this._timerMaskPlayer = this._timerMask.animate(keyframes, { duration: t imerDuration, easing: "steps(1, middle)", direction: "reverse" });
456 this._timerMaskPlayer.playbackRate = this._playbackRate();
457 },
458
459 _resetTimerAnimation: function()
460 {
461 if (!this._timerSpinnerPlayer)
462 return;
463 this._timerSpinnerPlayer.currentTime = 0;
464 this._timerFillerPlayer.currentTime = 0;
465 this._timerMaskPlayer.currentTime = 0;
466 },
467
468 /**
469 * @param {!Object} timerPlayer
470 */
471 _timerFinished: function(timerPlayer)
472 {
473 if (this._timerSpinnerPlayer !== timerPlayer)
474 return;
475 this._timelineScrubber.classList.add("animation-timeline-end");
476 delete this._timerSpinnerPlayer;
477 delete this._timerFillerPlayer;
478 delete this._timerMaskPlayer;
479 },
480
481 /**
482 * @param {number=} time
483 * @param {boolean=} timelineCapturing
484 */
485 _animateTime: function(time, timelineCapturing)
486 {
487 var oldPlayer = this._scrubberPlayer;
488 this._timelineScrubber.classList.toggle("animation-timeline-capturing", timelineCapturing);
489 if (timelineCapturing)
490 this._startTimerAnimation();
491
492 this._scrubberPlayer = this._timelineScrubber.animate([
493 { transform: "translateX(0px)" },
494 { transform: "translateX(" + (this.width() - this._scrubberRadius) + "px)" }
495 ], { duration: this.duration() - this._scrubberRadius / this.pixelMsRati o(), fill: "forwards" });
496 this._scrubberPlayer.playbackRate = this._playbackRate();
497
498 if (time !== undefined)
499 this._scrubberPlayer.currentTime = time;
500 else if (oldPlayer.playState === "finished")
501 this._scrubberPlayer.finish();
502 else
503 this._scrubberPlayer.startTime = oldPlayer.startTime;
504
505 if (oldPlayer)
506 oldPlayer.cancel();
507 this._timelineScrubber.classList.remove("animation-timeline-end");
508 this._timelineScrubberHead.window().requestAnimationFrame(this._updateSc rubber.bind(this));
509 },
510
511 /**
512 * @return {number}
513 */
514 pixelMsRatio: function()
515 {
516 return this.width() / this.duration() || 0;
517 },
518
519 /**
520 * @param {number} timestamp
521 */
522 _updateScrubber: function(timestamp)
523 {
524 if (!this._scrubberPlayer)
525 return;
526 this._timelineScrubberHead.textContent = WebInspector.UIString(Number.mi llisToString(this._scrubberPlayer.currentTime));
527 if (this._scrubberPlayer.playState === "pending" || this._scrubberPlayer .playState === "running") {
528 this._timelineScrubberHead.window().requestAnimationFrame(this._upda teScrubber.bind(this));
529 } else if (this._scrubberPlayer.playState === "finished") {
530 this._timelineScrubberHead.textContent = WebInspector.UIString(". . .");
531 if (!this._timerSpinnerPlayer)
532 this._timelineScrubber.classList.add("animation-timeline-end");
533 }
534 },
535
536 /**
537 * @param {!Event} event
538 * @return {boolean}
539 */
540 _scrubberDragStart: function(event)
541 {
542 if (!this._scrubberPlayer)
543 return false;
544
545 this._originalScrubberTime = this._scrubberPlayer.currentTime;
546 this._timelineScrubber.classList.remove("animation-timeline-end");
547 this._timelineScrubber.classList.remove("animation-timeline-capturing");
548 this._scrubberPlayer.pause();
549 this._originalMousePosition = new WebInspector.Geometry.Point(event.x, e vent.y);
550
551 var target = WebInspector.targetManager.mainTarget();
552 if (target)
553 target.animationModel.setPlaybackRate(0);
554 return true;
555 },
556
557 /**
558 * @param {!Event} event
559 */
560 _scrubberDragMove: function(event)
561 {
562 var delta = event.x - this._originalMousePosition.x;
563 this._scrubberPlayer.currentTime = Math.min(this._originalScrubberTime + delta / this.pixelMsRatio(), this.duration() - this._scrubberRadius / this.pixe lMsRatio());
564 var currentTime = Math.max(0, Math.round(this._scrubberPlayer.currentTim e));
565 this._timelineScrubberHead.textContent = WebInspector.UIString(Number.mi llisToString(currentTime));
566 var targets = WebInspector.targetManager.targets();
567 for (var target of targets)
568 target.animationAgent().setCurrentTime(/** @type {number} */(this.st artTime() + currentTime));
569 },
570
571 /**
572 * @param {!Event} event
573 */
574 _scrubberDragEnd: function(event)
575 {
576 if (this._scrubberPlayer.currentTime < this.duration() - this._scrubberR adius / this.pixelMsRatio())
577 this._scrubberPlayer.play();
578 this._timelineScrubberHead.window().requestAnimationFrame(this._updateSc rubber.bind(this));
579 var target = WebInspector.targetManager.mainTarget();
580 if (target)
581 target.animationModel.setPlaybackRate(this._playbackRate());
582 },
583
584 __proto__: WebInspector.VBox.prototype
585 }
586
587 /**
588 * @constructor
589 * @param {!WebInspector.AnimationModel.AnimationNode} animationNode
590 */
591 WebInspector.AnimationTimeline.NodeUI = function(animationNode) {
592 /**
593 * @param {?WebInspector.DOMNode} node
594 * @this {WebInspector.AnimationTimeline.NodeUI}
595 */
596 function nodeResolved(node)
597 {
598 this._node = node;
599 this._description.appendChild(WebInspector.DOMPresentationUtils.linkifyN odeReference(node));
600 this.element.addEventListener("click", WebInspector.Revealer.reveal.bind (WebInspector.Revealer, node, undefined), false);
601 }
602
603 this._rows = [];
604 this.element = createElementWithClass("div", "animation-node-row");
605 this._description = this.element.createChild("div", "animation-node-descript ion");
606 animationNode.deferredNode().resolve(nodeResolved.bind(this));
607 this._timelineElement = this.element.createChild("div", "animation-node-time line");
608 }
609
610 /** @typedef {{element: !Element, animations: !Array<!WebInspector.AnimationUI>} } */
611 WebInspector.AnimationTimeline.NodeRow;
612
613 WebInspector.AnimationTimeline.NodeUI.prototype = {
614 /**
615 * @param {!WebInspector.AnimationModel.AnimationPlayer} animation
616 * @return {!WebInspector.AnimationTimeline.NodeRow}
617 */
618 findRow: function(animation)
619 {
620 // Check if it can fit into an existing row
621 var existingRow = this._collapsibleIntoRow(animation);
622 if (existingRow)
623 return existingRow;
624
625 // Create new row
626 var container = this._timelineElement.createChild("div", "animation-time line-row");
627 var nodeRow = {element: container, animations: []};
628 this._rows.push(nodeRow);
629 return nodeRow;
630 },
631
632 redraw: function()
633 {
634 for (var nodeRow of this._rows) {
635 for (var ui of nodeRow.animations)
636 ui.redraw();
637 }
638 },
639
640 /**
641 * @param {!WebInspector.AnimationModel.AnimationPlayer} animation
642 * @return {?WebInspector.AnimationTimeline.NodeRow}
643 */
644 _collapsibleIntoRow: function(animation)
645 {
646 if (animation.endTime() === Infinity)
647 return null;
648 for (var nodeRow of this._rows) {
649 var overlap = false;
650 for (var ui of nodeRow.animations)
651 overlap |= animation.overlaps(ui.animation());
652 if (!overlap)
653 return nodeRow;
654 }
655 return null;
656 },
657
658 nodeRemoved: function()
659 {
660 this.element.classList.add("animation-node-removed");
661 },
662
663 /**
664 * @param {?WebInspector.DOMNode} node
665 */
666 setNode: function(node)
667 {
668 this.element.classList.toggle("animation-node-selected", node === this._ node);
669 }
670 }
671
672 /**
673 * @constructor
674 * @param {number} steps
675 * @param {string} stepAtPosition
676 */
677 WebInspector.AnimationTimeline.StepTimingFunction = function(steps, stepAtPositi on)
678 {
679 this.steps = steps;
680 this.stepAtPosition = stepAtPosition;
681 }
682
683 /**
684 * @param {string} text
685 * @return {?WebInspector.AnimationTimeline.StepTimingFunction}
686 */
687 WebInspector.AnimationTimeline.StepTimingFunction.parse = function(text) {
688 var match = text.match(/^step-(start|middle|end)$/);
689 if (match)
690 return new WebInspector.AnimationTimeline.StepTimingFunction(1, match[1] );
691 match = text.match(/^steps\((\d+), (start|middle|end)\)$/);
692 if (match)
693 return new WebInspector.AnimationTimeline.StepTimingFunction(parseInt(ma tch[1], 10), match[2]);
694 return null;
695 }
696
697 /**
698 * @constructor
699 * @param {!WebInspector.AnimationModel.AnimationPlayer} animation
700 * @param {!WebInspector.AnimationTimeline} timeline
701 * @param {!Element} parentElement
702 */
703 WebInspector.AnimationUI = function(animation, timeline, parentElement) {
704 this._animation = animation;
705 this._timeline = timeline;
706 this._parentElement = parentElement;
707
708 if (this._animation.source().keyframesRule())
709 this._keyframes = this._animation.source().keyframesRule().keyframes();
710
711 this._nameElement = parentElement.createChild("div", "animation-name");
712 this._nameElement.textContent = this._animation.name();
713
714 this._svg = parentElement.createSVGChild("svg", "animation-ui");
715 this._svg.setAttribute("height", WebInspector.AnimationUI.Options.AnimationS VGHeight);
716 this._svg.style.marginLeft = "-" + WebInspector.AnimationUI.Options.Animatio nMargin + "px";
717 this._svg.addEventListener("mousedown", this._mouseDown.bind(this, WebInspec tor.AnimationUI.MouseEvents.AnimationDrag, null));
718 this._activeIntervalGroup = this._svg.createSVGChild("g");
719
720 /** @type {!Array.<{group: ?Element, animationLine: ?Element, keyframePoints : !Object.<number, !Element>, keyframeRender: !Object.<number, !Element>}>} */
721 this._cachedElements = [];
722
723 this._movementInMs = 0;
724 this.redraw();
725 }
726
727 /**
728 * @enum {string}
729 */
730 WebInspector.AnimationUI.MouseEvents = {
731 AnimationDrag: "AnimationDrag",
732 KeyframeMove: "KeyframeMove",
733 StartEndpointMove: "StartEndpointMove",
734 FinishEndpointMove: "FinishEndpointMove"
735 }
736
737 WebInspector.AnimationUI.prototype = {
738 /**
739 * @return {!WebInspector.AnimationModel.AnimationPlayer}
740 */
741 animation: function()
742 {
743 return this._animation;
744 },
745
746 /**
747 * @param {?WebInspector.DOMNode} node
748 */
749 setNode: function(node)
750 {
751 this._node = node;
752 },
753
754 /**
755 * @param {!Element} parentElement
756 * @param {string} className
757 */
758 _createLine: function(parentElement, className)
759 {
760 var line = parentElement.createSVGChild("line", className);
761 line.setAttribute("x1", WebInspector.AnimationUI.Options.AnimationMargin );
762 line.setAttribute("y1", WebInspector.AnimationUI.Options.AnimationHeight );
763 line.setAttribute("y2", WebInspector.AnimationUI.Options.AnimationHeight );
764 line.style.stroke = this._color();
765 return line;
766 },
767
768 /**
769 * @param {number} iteration
770 * @param {!Element} parentElement
771 */
772 _drawAnimationLine: function(iteration, parentElement)
773 {
774 var cache = this._cachedElements[iteration];
775 if (!cache.animationLine)
776 cache.animationLine = this._createLine(parentElement, "animation-lin e");
777 cache.animationLine.setAttribute("x2", (this._duration() * this._timelin e.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin).toFixed(2)) ;
778 },
779
780 /**
781 * @param {!Element} parentElement
782 */
783 _drawDelayLine: function(parentElement)
784 {
785 if (!this._delayLine) {
786 this._delayLine = this._createLine(parentElement, "animation-delay-l ine");
787 this._endDelayLine = this._createLine(parentElement, "animation-dela y-line");
788 }
789 this._delayLine.setAttribute("x1", WebInspector.AnimationUI.Options.Anim ationMargin);
790 this._delayLine.setAttribute("x2", (this._delay() * this._timeline.pixel MsRatio() + WebInspector.AnimationUI.Options.AnimationMargin).toFixed(2));
791 var leftMargin = (this._delay() + this._duration() * this._animation.sou rce().iterations()) * this._timeline.pixelMsRatio();
792 this._endDelayLine.style.transform = "translateX(" + Math.min(leftMargin , this._timeline.width()).toFixed(2) + "px)";
793 this._endDelayLine.setAttribute("x1", WebInspector.AnimationUI.Options.A nimationMargin);
794 this._endDelayLine.setAttribute("x2", (this._animation.source().endDelay () * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationM argin).toFixed(2));
795 },
796
797 /**
798 * @param {number} iteration
799 * @param {!Element} parentElement
800 * @param {number} x
801 * @param {number} keyframeIndex
802 * @param {boolean} attachEvents
803 */
804 _drawPoint: function(iteration, parentElement, x, keyframeIndex, attachEvent s)
805 {
806 if (this._cachedElements[iteration].keyframePoints[keyframeIndex]) {
807 this._cachedElements[iteration].keyframePoints[keyframeIndex].setAtt ribute("cx", x.toFixed(2));
808 return;
809 }
810
811 var circle = parentElement.createSVGChild("circle", keyframeIndex <= 0 ? "animation-endpoint" : "animation-keyframe-point");
812 circle.setAttribute("cx", x.toFixed(2));
813 circle.setAttribute("cy", WebInspector.AnimationUI.Options.AnimationHeig ht);
814 circle.style.stroke = this._color();
815 circle.setAttribute("r", WebInspector.AnimationUI.Options.AnimationMargi n / 2);
816
817 if (keyframeIndex <= 0)
818 circle.style.fill = this._color();
819
820 this._cachedElements[iteration].keyframePoints[keyframeIndex] = circle;
821
822 if (!attachEvents)
823 return;
824
825 if (keyframeIndex === 0) {
826 circle.addEventListener("mousedown", this._mouseDown.bind(this, WebI nspector.AnimationUI.MouseEvents.StartEndpointMove, keyframeIndex));
827 } else if (keyframeIndex === -1) {
828 circle.addEventListener("mousedown", this._mouseDown.bind(this, WebI nspector.AnimationUI.MouseEvents.FinishEndpointMove, keyframeIndex));
829 } else {
830 circle.addEventListener("mousedown", this._mouseDown.bind(this, WebI nspector.AnimationUI.MouseEvents.KeyframeMove, keyframeIndex));
831 }
832 },
833
834 /**
835 * @param {number} iteration
836 * @param {number} keyframeIndex
837 * @param {!Element} parentElement
838 * @param {number} leftDistance
839 * @param {number} width
840 * @param {string} easing
841 */
842 _renderKeyframe: function(iteration, keyframeIndex, parentElement, leftDista nce, width, easing)
843 {
844 /**
845 * @param {!Element} parentElement
846 * @param {number} x
847 * @param {string} strokeColor
848 */
849 function createStepLine(parentElement, x, strokeColor)
850 {
851 var line = parentElement.createSVGChild("line");
852 line.setAttribute("x1", x);
853 line.setAttribute("x2", x);
854 line.setAttribute("y1", WebInspector.AnimationUI.Options.AnimationMa rgin);
855 line.setAttribute("y2", WebInspector.AnimationUI.Options.AnimationHe ight);
856 line.style.stroke = strokeColor;
857 }
858
859 var bezier = WebInspector.Geometry.CubicBezier.parse(easing);
860 var cache = this._cachedElements[iteration].keyframeRender;
861 if (!cache[keyframeIndex])
862 cache[keyframeIndex] = bezier ? parentElement.createSVGChild("path", "animation-keyframe") : parentElement.createSVGChild("g", "animation-keyframe-s tep");
863 var group = cache[keyframeIndex];
864 group.style.transform = "translateX(" + leftDistance.toFixed(2) + "px)";
865
866 if (bezier) {
867 group.style.fill = this._color();
868 WebInspector.BezierUI.drawVelocityChart(bezier, group, width);
869 } else {
870 var stepFunction = WebInspector.AnimationTimeline.StepTimingFunction .parse(easing);
871 group.removeChildren();
872 const offsetMap = {"start": 0, "middle": 0.5, "end": 1};
873 const offsetWeight = offsetMap[stepFunction.stepAtPosition];
874 for (var i = 0; i < stepFunction.steps; i++)
875 createStepLine(group, (i + offsetWeight) * width / stepFunction. steps, this._color());
876 }
877 },
878
879 redraw: function()
880 {
881 var durationWithDelay = this._delay() + this._duration() * this._animati on.source().iterations() + this._animation.source().endDelay();
882 var leftMargin = ((this._animation.startTime() - this._timeline.startTim e()) * this._timeline.pixelMsRatio());
883 var maxWidth = this._timeline.width() - WebInspector.AnimationUI.Options .AnimationMargin - leftMargin;
884 var svgWidth = Math.min(maxWidth, durationWithDelay * this._timeline.pix elMsRatio());
885
886 this._svg.classList.toggle("animation-ui-canceled", this._animation.play State() === "idle");
887 this._svg.setAttribute("width", (svgWidth + 2 * WebInspector.AnimationUI .Options.AnimationMargin).toFixed(2));
888 this._svg.style.transform = "translateX(" + leftMargin.toFixed(2) + "px )";
889 this._activeIntervalGroup.style.transform = "translateX(" + (this._delay () * this._timeline.pixelMsRatio()).toFixed(2) + "px)";
890
891 this._nameElement.style.transform = "translateX(" + (leftMargin + this._ delay() * this._timeline.pixelMsRatio() + WebInspector.AnimationUI.Options.Anima tionMargin).toFixed(2) + "px)";
892 this._nameElement.style.width = (this._duration() * this._timeline.pixel MsRatio().toFixed(2)) + "px";
893 this._drawDelayLine(this._svg);
894
895 if (this._animation.type() === "CSSTransition") {
896 this._renderTransition();
897 return;
898 }
899
900 this._renderIteration(this._activeIntervalGroup, 0);
901 if (!this._tailGroup)
902 this._tailGroup = this._activeIntervalGroup.createSVGChild("g", "ani mation-tail-iterations");
903 var iterationWidth = this._duration() * this._timeline.pixelMsRatio();
904 for (var iteration = 1; iteration < this._animation.source().iterations( ) && iterationWidth * (iteration - 1) < this._timeline.width(); iteration++)
905 this._renderIteration(this._tailGroup, iteration);
906 while (iteration < this._cachedElements.length)
907 this._cachedElements.pop().group.remove();
908 },
909
910
911 _renderTransition: function()
912 {
913 if (!this._cachedElements[0])
914 this._cachedElements[0] = { animationLine: null, keyframePoints: {}, keyframeRender: {}, group: null };
915 this._drawAnimationLine(0, this._activeIntervalGroup);
916 this._renderKeyframe(0, 0, this._activeIntervalGroup, WebInspector.Anima tionUI.Options.AnimationMargin, this._duration() * this._timeline.pixelMsRatio() , this._animation.source().easing());
917 this._drawPoint(0, this._activeIntervalGroup, WebInspector.AnimationUI.O ptions.AnimationMargin, 0, true);
918 this._drawPoint(0, this._activeIntervalGroup, this._duration() * this._t imeline.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin, -1, t rue);
919 },
920
921 /**
922 * @param {!Element} parentElement
923 * @param {number} iteration
924 */
925 _renderIteration: function(parentElement, iteration)
926 {
927 if (!this._cachedElements[iteration])
928 this._cachedElements[iteration] = { animationLine: null, keyframePoi nts: {}, keyframeRender: {}, group: parentElement.createSVGChild("g") };
929 var group = this._cachedElements[iteration].group;
930 group.style.transform = "translateX(" + (iteration * this._duration() * this._timeline.pixelMsRatio()).toFixed(2) + "px)";
931 this._drawAnimationLine(iteration, group);
932 console.assert(this._keyframes.length > 1);
933 for (var i = 0; i < this._keyframes.length - 1; i++) {
934 var leftDistance = this._offset(i) * this._duration() * this._timeli ne.pixelMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin;
935 var width = this._duration() * (this._offset(i + 1) - this._offset(i )) * this._timeline.pixelMsRatio();
936 this._renderKeyframe(iteration, i, group, leftDistance, width, this. _keyframes[i].easing());
937 if (i || (!i && iteration === 0))
938 this._drawPoint(iteration, group, leftDistance, i, iteration === 0);
939 }
940 this._drawPoint(iteration, group, this._duration() * this._timeline.pixe lMsRatio() + WebInspector.AnimationUI.Options.AnimationMargin, -1, iteration === 0);
941 },
942
943 /**
944 * @return {number}
945 */
946 _delay: function()
947 {
948 var delay = this._animation.source().delay();
949 if (this._mouseEventType === WebInspector.AnimationUI.MouseEvents.Animat ionDrag || this._mouseEventType === WebInspector.AnimationUI.MouseEvents.StartEn dpointMove)
950 delay += this._movementInMs;
951 // FIXME: add support for negative start delay
952 return Math.max(0, delay);
953 },
954
955 /**
956 * @return {number}
957 */
958 _duration: function()
959 {
960 var duration = this._animation.source().duration();
961 if (this._mouseEventType === WebInspector.AnimationUI.MouseEvents.Finish EndpointMove)
962 duration += this._movementInMs;
963 else if (this._mouseEventType === WebInspector.AnimationUI.MouseEvents.S tartEndpointMove)
964 duration -= Math.max(this._movementInMs, -this._animation.source().d elay()); // Cannot have negative delay
965 return Math.max(0, duration);
966 },
967
968 /**
969 * @param {number} i
970 * @return {number} offset
971 */
972 _offset: function(i)
973 {
974 var offset = this._keyframes[i].offsetAsNumber();
975 if (this._mouseEventType === WebInspector.AnimationUI.MouseEvents.Keyfra meMove && i === this._keyframeMoved) {
976 console.assert(i > 0 && i < this._keyframes.length - 1, "First and l ast keyframe cannot be moved");
977 offset += this._movementInMs / this._animation.source().duration();
978 offset = Math.max(offset, this._keyframes[i - 1].offsetAsNumber());
979 offset = Math.min(offset, this._keyframes[i + 1].offsetAsNumber());
980 }
981 return offset;
982 },
983
984 /**
985 * @param {!WebInspector.AnimationUI.MouseEvents} mouseEventType
986 * @param {?number} keyframeIndex
987 * @param {!Event} event
988 */
989 _mouseDown: function(mouseEventType, keyframeIndex, event)
990 {
991 if (this._animation.playState() === "idle")
992 return;
993 this._mouseEventType = mouseEventType;
994 this._keyframeMoved = keyframeIndex;
995 this._downMouseX = event.clientX;
996 this._mouseMoveHandler = this._mouseMove.bind(this);
997 this._mouseUpHandler = this._mouseUp.bind(this);
998 this._parentElement.ownerDocument.addEventListener("mousemove", this._mo useMoveHandler);
999 this._parentElement.ownerDocument.addEventListener("mouseup", this._mous eUpHandler);
1000 event.preventDefault();
1001 event.stopPropagation();
1002
1003 if (this._node)
1004 WebInspector.Revealer.reveal(this._node);
1005 },
1006
1007 /**
1008 * @param {!Event} event
1009 */
1010 _mouseMove: function (event)
1011 {
1012 this._movementInMs = (event.clientX - this._downMouseX) / this._timeline .pixelMsRatio();
1013 if (this._animation.startTime() + this._delay() + this._duration() - thi s._timeline.startTime() > this._timeline.duration() * 0.8)
1014 this._timeline.setDuration(this._timeline.duration() * 1.2);
1015 this.redraw();
1016 },
1017
1018 /**
1019 * @param {!Event} event
1020 */
1021 _mouseUp: function(event)
1022 {
1023 this._movementInMs = (event.clientX - this._downMouseX) / this._timeline .pixelMsRatio();
1024
1025 // Commit changes
1026 if (this._mouseEventType === WebInspector.AnimationUI.MouseEvents.Keyfra meMove) {
1027 this._keyframes[this._keyframeMoved].setOffset(this._offset(this._ke yframeMoved));
1028 } else {
1029 var delay = this._delay();
1030 var duration = this._duration();
1031 this._setDelay(delay);
1032 this._setDuration(duration);
1033 if (this._animation.type() !== "CSSAnimation") {
1034 var target = WebInspector.targetManager.mainTarget();
1035 if (target)
1036 target.animationAgent().setTiming(this._animation.id(), dura tion, delay);
1037 }
1038 }
1039
1040 this._movementInMs = 0;
1041 this.redraw();
1042
1043 this._parentElement.ownerDocument.removeEventListener("mousemove", this. _mouseMoveHandler);
1044 this._parentElement.ownerDocument.removeEventListener("mouseup", this._m ouseUpHandler);
1045 delete this._mouseMoveHandler;
1046 delete this._mouseUpHandler;
1047 delete this._mouseEventType;
1048 delete this._downMouseX;
1049 delete this._keyframeMoved;
1050 },
1051
1052 /**
1053 * @param {number} value
1054 */
1055 _setDelay: function(value)
1056 {
1057 if (!this._node || this._animation.source().delay() == this._delay())
1058 return;
1059
1060 this._animation.source().setDelay(this._delay());
1061 var propertyName;
1062 if (this._animation.type() == "CSSTransition")
1063 propertyName = "transition-delay";
1064 else if (this._animation.type() == "CSSAnimation")
1065 propertyName = "animation-delay";
1066 else
1067 return;
1068 this._setNodeStyle(propertyName, Math.round(value) + "ms");
1069 },
1070
1071 /**
1072 * @param {number} value
1073 */
1074 _setDuration: function(value)
1075 {
1076 if (!this._node || this._animation.source().duration() == value)
1077 return;
1078
1079 this._animation.source().setDuration(value);
1080 var propertyName;
1081 if (this._animation.type() == "CSSTransition")
1082 propertyName = "transition-duration";
1083 else if (this._animation.type() == "CSSAnimation")
1084 propertyName = "animation-duration";
1085 else
1086 return;
1087 this._setNodeStyle(propertyName, Math.round(value) + "ms");
1088 },
1089
1090 /**
1091 * @param {string} name
1092 * @param {string} value
1093 */
1094 _setNodeStyle: function(name, value)
1095 {
1096 var style = this._node.getAttribute("style") || "";
1097 if (style)
1098 style = style.replace(new RegExp("\\s*(-webkit-)?" + name + ":[^;]*; ?\\s*", "g"), "");
1099 var valueString = name + ": " + value;
1100 this._node.setAttributeValue("style", style + " " + valueString + "; -we bkit-" + valueString + ";");
1101 },
1102
1103 /**
1104 * @return {string}
1105 */
1106 _color: function()
1107 {
1108 /**
1109 * @param {string} string
1110 * @return {number}
1111 */
1112 function hash(string)
1113 {
1114 var hash = 0;
1115 for (var i = 0; i < string.length; i++)
1116 hash = (hash << 5) + hash + string.charCodeAt(i);
1117 return Math.abs(hash);
1118 }
1119
1120 if (!this._selectedColor) {
1121 var names = Object.keys(WebInspector.AnimationUI.Colors);
1122 var color = WebInspector.AnimationUI.Colors[names[hash(this._animati on.name() || this._animation.id()) % names.length]];
1123 this._selectedColor = color.asString(WebInspector.Color.Format.RGB);
1124 }
1125 return this._selectedColor;
1126 }
1127 }
1128
1129 WebInspector.AnimationUI.Options = {
1130 AnimationHeight: 32,
1131 AnimationSVGHeight: 80,
1132 AnimationMargin: 7,
1133 EndpointsClickRegionSize: 10,
1134 GridCanvasHeight: 40
1135 }
1136
1137 WebInspector.AnimationUI.Colors = {
1138 "Purple": WebInspector.Color.parse("#9C27B0"),
1139 "Light Blue": WebInspector.Color.parse("#03A9F4"),
1140 "Deep Orange": WebInspector.Color.parse("#FF5722"),
1141 "Blue": WebInspector.Color.parse("#5677FC"),
1142 "Lime": WebInspector.Color.parse("#CDDC39"),
1143 "Blue Grey": WebInspector.Color.parse("#607D8B"),
1144 "Pink": WebInspector.Color.parse("#E91E63"),
1145 "Green": WebInspector.Color.parse("#0F9D58"),
1146 "Brown": WebInspector.Color.parse("#795548"),
1147 "Cyan": WebInspector.Color.parse("#00BCD4")
1148 }
OLDNEW
« no previous file with comments | « Source/devtools/front_end/elements/AnimationControlPane.js ('k') | Source/devtools/front_end/elements/BezierUI.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698