| // Copyright (c) 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| /** |
| * @implements {SDK.SDKModelObserver<!Animation.AnimationModel>} |
| * @unrestricted |
| */ |
| export default class AnimationTimeline extends UI.VBox { |
| constructor() { |
| super(true); |
| this.registerRequiredCSS('animation/animationTimeline.css'); |
| this.element.classList.add('animations-timeline'); |
| |
| this._grid = this.contentElement.createSVGChild('svg', 'animation-timeline-grid'); |
| |
| this._playbackRate = 1; |
| this._allPaused = false; |
| this._createHeader(); |
| this._animationsContainer = this.contentElement.createChild('div', 'animation-timeline-rows'); |
| const timelineHint = this.contentElement.createChild('div', 'animation-timeline-rows-hint'); |
| timelineHint.textContent = ls`Select an effect above to inspect and modify.`; |
| |
| /** @const */ this._defaultDuration = 100; |
| this._duration = this._defaultDuration; |
| /** @const */ this._timelineControlsWidth = 150; |
| /** @type {!Map.<!Protocol.DOM.BackendNodeId, !Animation.AnimationTimeline.NodeUI>} */ |
| this._nodesMap = new Map(); |
| this._uiAnimations = []; |
| this._groupBuffer = []; |
| /** @type {!Map.<!Animation.AnimationModel.AnimationGroup, !Animation.AnimationGroupPreviewUI>} */ |
| this._previewMap = new Map(); |
| this._symbol = Symbol('animationTimeline'); |
| /** @type {!Map.<string, !Animation.AnimationModel.Animation>} */ |
| this._animationsMap = new Map(); |
| SDK.targetManager.addModelListener(SDK.DOMModel, SDK.DOMModel.Events.NodeRemoved, this._nodeRemoved, this); |
| SDK.targetManager.observeModels(Animation.AnimationModel, this); |
| UI.context.addFlavorChangeListener(SDK.DOMNode, this._nodeChanged, this); |
| } |
| |
| /** |
| * @override |
| */ |
| wasShown() { |
| for (const animationModel of SDK.targetManager.models(Animation.AnimationModel)) { |
| this._addEventListeners(animationModel); |
| } |
| } |
| |
| /** |
| * @override |
| */ |
| willHide() { |
| for (const animationModel of SDK.targetManager.models(Animation.AnimationModel)) { |
| this._removeEventListeners(animationModel); |
| } |
| this._popoverHelper.hidePopover(); |
| } |
| |
| /** |
| * @override |
| * @param {!Animation.AnimationModel} animationModel |
| */ |
| modelAdded(animationModel) { |
| if (this.isShowing()) { |
| this._addEventListeners(animationModel); |
| } |
| } |
| |
| /** |
| * @override |
| * @param {!Animation.AnimationModel} animationModel |
| */ |
| modelRemoved(animationModel) { |
| this._removeEventListeners(animationModel); |
| } |
| |
| /** |
| * @param {!Animation.AnimationModel} animationModel |
| */ |
| _addEventListeners(animationModel) { |
| animationModel.ensureEnabled(); |
| animationModel.addEventListener( |
| Animation.AnimationModel.Events.AnimationGroupStarted, this._animationGroupStarted, this); |
| animationModel.addEventListener(Animation.AnimationModel.Events.ModelReset, this._reset, this); |
| } |
| |
| /** |
| * @param {!Animation.AnimationModel} animationModel |
| */ |
| _removeEventListeners(animationModel) { |
| animationModel.removeEventListener( |
| Animation.AnimationModel.Events.AnimationGroupStarted, this._animationGroupStarted, this); |
| animationModel.removeEventListener(Animation.AnimationModel.Events.ModelReset, this._reset, this); |
| } |
| |
| _nodeChanged() { |
| for (const nodeUI of this._nodesMap.values()) { |
| nodeUI._nodeChanged(); |
| } |
| } |
| |
| /** |
| * @return {!Element} element |
| */ |
| _createScrubber() { |
| this._timelineScrubber = createElementWithClass('div', 'animation-scrubber hidden'); |
| this._timelineScrubberLine = this._timelineScrubber.createChild('div', 'animation-scrubber-line'); |
| this._timelineScrubberLine.createChild('div', 'animation-scrubber-head'); |
| this._timelineScrubber.createChild('div', 'animation-time-overlay'); |
| return this._timelineScrubber; |
| } |
| |
| _createHeader() { |
| const toolbarContainer = this.contentElement.createChild('div', 'animation-timeline-toolbar-container'); |
| const topToolbar = new UI.Toolbar('animation-timeline-toolbar', toolbarContainer); |
| const clearButton = new UI.ToolbarButton(ls`Clear all`, 'largeicon-clear'); |
| clearButton.addEventListener(UI.ToolbarButton.Events.Click, this._reset.bind(this)); |
| topToolbar.appendToolbarItem(clearButton); |
| topToolbar.appendSeparator(); |
| |
| this._pauseButton = new UI.ToolbarToggle(ls`Pause all`, 'largeicon-pause', 'largeicon-resume'); |
| this._pauseButton.addEventListener(UI.ToolbarButton.Events.Click, this._togglePauseAll.bind(this)); |
| topToolbar.appendToolbarItem(this._pauseButton); |
| |
| const playbackRateControl = toolbarContainer.createChild('div', 'animation-playback-rate-control'); |
| this._playbackRateButtons = []; |
| for (const playbackRate of Animation.AnimationTimeline.GlobalPlaybackRates) { |
| const button = playbackRateControl.createChild('div', 'animation-playback-rate-button'); |
| button.textContent = playbackRate ? ls`${playbackRate * 100}%` : ls`Pause`; |
| button.playbackRate = playbackRate; |
| button.addEventListener('click', this._setPlaybackRate.bind(this, playbackRate)); |
| button.title = ls`Set speed to ${button.textContent}`; |
| this._playbackRateButtons.push(button); |
| } |
| this._updatePlaybackControls(); |
| |
| this._previewContainer = this.contentElement.createChild('div', 'animation-timeline-buffer'); |
| this._popoverHelper = new UI.PopoverHelper(this._previewContainer, this._getPopoverRequest.bind(this)); |
| this._popoverHelper.setDisableOnClick(true); |
| this._popoverHelper.setTimeout(0); |
| const emptyBufferHint = this.contentElement.createChild('div', 'animation-timeline-buffer-hint'); |
| emptyBufferHint.textContent = ls`Listening for animations...`; |
| const container = this.contentElement.createChild('div', 'animation-timeline-header'); |
| const controls = container.createChild('div', 'animation-controls'); |
| this._currentTime = controls.createChild('div', 'animation-timeline-current-time monospace'); |
| |
| const toolbar = new UI.Toolbar('animation-controls-toolbar', controls); |
| this._controlButton = new UI.ToolbarToggle(ls`Replay timeline`, 'largeicon-replay-animation'); |
| this._controlState = Animation.AnimationTimeline._ControlState.Replay; |
| this._controlButton.setToggled(true); |
| this._controlButton.addEventListener(UI.ToolbarButton.Events.Click, this._controlButtonToggle.bind(this)); |
| toolbar.appendToolbarItem(this._controlButton); |
| |
| const gridHeader = container.createChild('div', 'animation-grid-header'); |
| UI.installDragHandle( |
| gridHeader, this._repositionScrubber.bind(this), this._scrubberDragMove.bind(this), |
| this._scrubberDragEnd.bind(this), 'text'); |
| container.appendChild(this._createScrubber()); |
| UI.installDragHandle( |
| this._timelineScrubberLine, this._scrubberDragStart.bind(this), this._scrubberDragMove.bind(this), |
| this._scrubberDragEnd.bind(this), 'col-resize'); |
| this._currentTime.textContent = ''; |
| |
| return container; |
| } |
| |
| /** |
| * @param {!Event} event |
| * @return {?UI.PopoverRequest} |
| */ |
| _getPopoverRequest(event) { |
| const element = event.target; |
| if (!element.isDescendant(this._previewContainer)) { |
| return null; |
| } |
| |
| return { |
| box: event.target.boxInWindow(), |
| show: popover => { |
| let animGroup; |
| for (const group of this._previewMap.keysArray()) { |
| if (this._previewMap.get(group).element === element.parentElement) { |
| animGroup = group; |
| } |
| } |
| console.assert(animGroup); |
| const screenshots = animGroup.screenshots(); |
| if (!screenshots.length) { |
| return Promise.resolve(false); |
| } |
| |
| let fulfill; |
| const promise = new Promise(x => fulfill = x); |
| if (!screenshots[0].complete) { |
| screenshots[0].onload = onFirstScreenshotLoaded.bind(null, screenshots); |
| } else { |
| onFirstScreenshotLoaded(screenshots); |
| } |
| return promise; |
| |
| /** |
| * @param {!Array.<!Image>} screenshots |
| */ |
| function onFirstScreenshotLoaded(screenshots) { |
| new Animation.AnimationScreenshotPopover(screenshots).show(popover.contentElement); |
| fulfill(true); |
| } |
| } |
| }; |
| } |
| |
| _togglePauseAll() { |
| this._allPaused = !this._allPaused; |
| this._pauseButton.setToggled(this._allPaused); |
| this._setPlaybackRate(this._playbackRate); |
| this._pauseButton.setTitle(this._allPaused ? ls`Resume all` : ls`Pause all`); |
| } |
| |
| /** |
| * @param {number} playbackRate |
| */ |
| _setPlaybackRate(playbackRate) { |
| this._playbackRate = playbackRate; |
| for (const animationModel of SDK.targetManager.models(Animation.AnimationModel)) { |
| animationModel.setPlaybackRate(this._allPaused ? 0 : this._playbackRate); |
| } |
| Host.userMetrics.actionTaken(Host.UserMetrics.Action.AnimationsPlaybackRateChanged); |
| if (this._scrubberPlayer) { |
| this._scrubberPlayer.playbackRate = this._effectivePlaybackRate(); |
| } |
| |
| this._updatePlaybackControls(); |
| } |
| |
| _updatePlaybackControls() { |
| for (const button of this._playbackRateButtons) { |
| const selected = this._playbackRate === button.playbackRate; |
| button.classList.toggle('selected', selected); |
| } |
| } |
| |
| _controlButtonToggle() { |
| if (this._controlState === Animation.AnimationTimeline._ControlState.Play) { |
| this._togglePause(false); |
| } else if (this._controlState === Animation.AnimationTimeline._ControlState.Replay) { |
| this._replay(); |
| } else { |
| this._togglePause(true); |
| } |
| } |
| |
| _updateControlButton() { |
| this._controlButton.setEnabled(!!this._selectedGroup); |
| if (this._selectedGroup && this._selectedGroup.paused()) { |
| this._controlState = Animation.AnimationTimeline._ControlState.Play; |
| this._controlButton.setToggled(true); |
| this._controlButton.setTitle(ls`Play timeline`); |
| this._controlButton.setGlyph('largeicon-play-animation'); |
| } else if (!this._scrubberPlayer || this._scrubberPlayer.currentTime >= this.duration()) { |
| this._controlState = Animation.AnimationTimeline._ControlState.Replay; |
| this._controlButton.setToggled(true); |
| this._controlButton.setTitle(ls`Replay timeline`); |
| this._controlButton.setGlyph('largeicon-replay-animation'); |
| } else { |
| this._controlState = Animation.AnimationTimeline._ControlState.Pause; |
| this._controlButton.setToggled(false); |
| this._controlButton.setTitle(ls`Pause timeline`); |
| this._controlButton.setGlyph('largeicon-pause-animation'); |
| } |
| } |
| |
| /** |
| * @return {number} |
| */ |
| _effectivePlaybackRate() { |
| return (this._allPaused || (this._selectedGroup && this._selectedGroup.paused())) ? 0 : this._playbackRate; |
| } |
| |
| /** |
| * @param {boolean} pause |
| */ |
| _togglePause(pause) { |
| this._selectedGroup.togglePause(pause); |
| if (this._scrubberPlayer) { |
| this._scrubberPlayer.playbackRate = this._effectivePlaybackRate(); |
| } |
| this._previewMap.get(this._selectedGroup).element.classList.toggle('paused', pause); |
| this._updateControlButton(); |
| } |
| |
| _replay() { |
| if (!this._selectedGroup) { |
| return; |
| } |
| this._selectedGroup.seekTo(0); |
| this._animateTime(0); |
| this._updateControlButton(); |
| } |
| |
| /** |
| * @return {number} |
| */ |
| duration() { |
| return this._duration; |
| } |
| |
| /** |
| * @param {number} duration |
| */ |
| setDuration(duration) { |
| this._duration = duration; |
| this.scheduleRedraw(); |
| } |
| |
| _clearTimeline() { |
| this._uiAnimations = []; |
| this._nodesMap.clear(); |
| this._animationsMap.clear(); |
| this._animationsContainer.removeChildren(); |
| this._duration = this._defaultDuration; |
| this._timelineScrubber.classList.add('hidden'); |
| delete this._selectedGroup; |
| if (this._scrubberPlayer) { |
| this._scrubberPlayer.cancel(); |
| } |
| delete this._scrubberPlayer; |
| this._currentTime.textContent = ''; |
| this._updateControlButton(); |
| } |
| |
| _reset() { |
| this._clearTimeline(); |
| if (this._allPaused) { |
| this._togglePauseAll(); |
| } else { |
| this._setPlaybackRate(this._playbackRate); |
| } |
| |
| for (const group of this._groupBuffer) { |
| group.release(); |
| } |
| this._groupBuffer = []; |
| this._previewMap.clear(); |
| this._previewContainer.removeChildren(); |
| this._popoverHelper.hidePopover(); |
| this._renderGrid(); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _animationGroupStarted(event) { |
| this._addAnimationGroup(/** @type {!Animation.AnimationModel.AnimationGroup} */ (event.data)); |
| } |
| |
| /** |
| * @param {!Animation.AnimationModel.AnimationGroup} group |
| */ |
| _addAnimationGroup(group) { |
| /** |
| * @param {!Animation.AnimationModel.AnimationGroup} left |
| * @param {!Animation.AnimationModel.AnimationGroup} right |
| */ |
| function startTimeComparator(left, right) { |
| return left.startTime() > right.startTime(); |
| } |
| |
| if (this._previewMap.get(group)) { |
| if (this._selectedGroup === group) { |
| this._syncScrubber(); |
| } else { |
| this._previewMap.get(group).replay(); |
| } |
| return; |
| } |
| this._groupBuffer.sort(startTimeComparator); |
| // Discard oldest groups from buffer if necessary |
| const groupsToDiscard = []; |
| const bufferSize = this.width() / 50; |
| while (this._groupBuffer.length > bufferSize) { |
| const toDiscard = this._groupBuffer.splice(this._groupBuffer[0] === this._selectedGroup ? 1 : 0, 1); |
| groupsToDiscard.push(toDiscard[0]); |
| } |
| for (const g of groupsToDiscard) { |
| this._previewMap.get(g).element.remove(); |
| this._previewMap.delete(g); |
| g.release(); |
| } |
| // Generate preview |
| const preview = new Animation.AnimationGroupPreviewUI(group); |
| this._groupBuffer.push(group); |
| this._previewMap.set(group, preview); |
| this._previewContainer.appendChild(preview.element); |
| preview.removeButton().addEventListener('click', this._removeAnimationGroup.bind(this, group)); |
| preview.element.addEventListener('click', this._selectAnimationGroup.bind(this, group)); |
| } |
| |
| /** |
| * @param {!Animation.AnimationModel.AnimationGroup} group |
| * @param {!Event} event |
| */ |
| _removeAnimationGroup(group, event) { |
| this._groupBuffer.remove(group); |
| this._previewMap.get(group).element.remove(); |
| this._previewMap.delete(group); |
| group.release(); |
| event.consume(true); |
| |
| if (this._selectedGroup === group) { |
| this._clearTimeline(); |
| this._renderGrid(); |
| } |
| } |
| |
| /** |
| * @param {!Animation.AnimationModel.AnimationGroup} group |
| */ |
| _selectAnimationGroup(group) { |
| /** |
| * @param {!Animation.AnimationGroupPreviewUI} ui |
| * @param {!Animation.AnimationModel.AnimationGroup} group |
| * @this {!Animation.AnimationTimeline} |
| */ |
| function applySelectionClass(ui, group) { |
| ui.element.classList.toggle('selected', this._selectedGroup === group); |
| } |
| |
| if (this._selectedGroup === group) { |
| this._togglePause(false); |
| this._replay(); |
| return; |
| } |
| this._clearTimeline(); |
| this._selectedGroup = group; |
| this._previewMap.forEach(applySelectionClass, this); |
| this.setDuration(Math.max(500, group.finiteDuration() + 100)); |
| for (const anim of group.animations()) { |
| this._addAnimation(anim); |
| } |
| this.scheduleRedraw(); |
| this._timelineScrubber.classList.remove('hidden'); |
| this._togglePause(false); |
| this._replay(); |
| } |
| |
| /** |
| * @param {!Animation.AnimationModel.Animation} animation |
| */ |
| _addAnimation(animation) { |
| /** |
| * @param {?SDK.DOMNode} node |
| * @this {Animation.AnimationTimeline} |
| */ |
| function nodeResolved(node) { |
| nodeUI.nodeResolved(node); |
| uiAnimation.setNode(node); |
| if (node) { |
| node[this._symbol] = nodeUI; |
| } |
| } |
| |
| let nodeUI = this._nodesMap.get(animation.source().backendNodeId()); |
| if (!nodeUI) { |
| nodeUI = new Animation.AnimationTimeline.NodeUI(animation.source()); |
| this._animationsContainer.appendChild(nodeUI.element); |
| this._nodesMap.set(animation.source().backendNodeId(), nodeUI); |
| } |
| const nodeRow = nodeUI.createNewRow(); |
| const uiAnimation = new Animation.AnimationUI(animation, this, nodeRow); |
| animation.source().deferredNode().resolve(nodeResolved.bind(this)); |
| this._uiAnimations.push(uiAnimation); |
| this._animationsMap.set(animation.id(), animation); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _nodeRemoved(event) { |
| const node = event.data.node; |
| if (node[this._symbol]) { |
| node[this._symbol].nodeRemoved(); |
| } |
| } |
| |
| _renderGrid() { |
| /** @const */ const gridSize = 250; |
| this._grid.setAttribute('width', this.width() + 10); |
| this._grid.setAttribute('height', this._cachedTimelineHeight + 30); |
| this._grid.setAttribute('shape-rendering', 'crispEdges'); |
| this._grid.removeChildren(); |
| let lastDraw = undefined; |
| for (let time = 0; time < this.duration(); time += gridSize) { |
| const line = this._grid.createSVGChild('rect', 'animation-timeline-grid-line'); |
| line.setAttribute('x', time * this.pixelMsRatio() + 10); |
| line.setAttribute('y', 23); |
| line.setAttribute('height', '100%'); |
| line.setAttribute('width', 1); |
| } |
| for (let time = 0; time < this.duration(); time += gridSize) { |
| const gridWidth = time * this.pixelMsRatio(); |
| if (lastDraw === undefined || gridWidth - lastDraw > 50) { |
| lastDraw = gridWidth; |
| const label = this._grid.createSVGChild('text', 'animation-timeline-grid-label'); |
| label.textContent = Number.millisToString(time); |
| label.setAttribute('x', gridWidth + 10); |
| label.setAttribute('y', 16); |
| } |
| } |
| } |
| |
| scheduleRedraw() { |
| this._renderQueue = []; |
| for (const ui of this._uiAnimations) { |
| this._renderQueue.push(ui); |
| } |
| if (this._redrawing) { |
| return; |
| } |
| this._redrawing = true; |
| this._renderGrid(); |
| this._animationsContainer.window().requestAnimationFrame(this._render.bind(this)); |
| } |
| |
| /** |
| * @param {number=} timestamp |
| */ |
| _render(timestamp) { |
| while (this._renderQueue.length && (!timestamp || window.performance.now() - timestamp < 50)) { |
| this._renderQueue.shift().redraw(); |
| } |
| if (this._renderQueue.length) { |
| this._animationsContainer.window().requestAnimationFrame(this._render.bind(this)); |
| } else { |
| delete this._redrawing; |
| } |
| } |
| |
| /** |
| * @override |
| */ |
| onResize() { |
| this._cachedTimelineWidth = Math.max(0, this._animationsContainer.offsetWidth - this._timelineControlsWidth) || 0; |
| this._cachedTimelineHeight = this._animationsContainer.offsetHeight; |
| this.scheduleRedraw(); |
| if (this._scrubberPlayer) { |
| this._syncScrubber(); |
| } |
| delete this._gridOffsetLeft; |
| } |
| |
| /** |
| * @return {number} |
| */ |
| width() { |
| return this._cachedTimelineWidth || 0; |
| } |
| |
| /** |
| * @param {!Animation.AnimationModel.Animation} animation |
| * @return {boolean} |
| */ |
| _resizeWindow(animation) { |
| let resized = false; |
| |
| // This shows at most 3 iterations |
| const duration = animation.source().duration() * Math.min(2, animation.source().iterations()); |
| const requiredDuration = animation.source().delay() + duration + animation.source().endDelay(); |
| if (requiredDuration > this._duration) { |
| resized = true; |
| this._duration = requiredDuration + 200; |
| } |
| return resized; |
| } |
| |
| _syncScrubber() { |
| if (!this._selectedGroup) { |
| return; |
| } |
| this._selectedGroup.currentTimePromise() |
| .then(this._animateTime.bind(this)) |
| .then(this._updateControlButton.bind(this)); |
| } |
| |
| /** |
| * @param {number} currentTime |
| */ |
| _animateTime(currentTime) { |
| if (this._scrubberPlayer) { |
| this._scrubberPlayer.cancel(); |
| } |
| |
| this._scrubberPlayer = this._timelineScrubber.animate( |
| [{transform: 'translateX(0px)'}, {transform: 'translateX(' + this.width() + 'px)'}], |
| {duration: this.duration(), fill: 'forwards'}); |
| this._scrubberPlayer.playbackRate = this._effectivePlaybackRate(); |
| this._scrubberPlayer.onfinish = this._updateControlButton.bind(this); |
| this._scrubberPlayer.currentTime = currentTime; |
| this.element.window().requestAnimationFrame(this._updateScrubber.bind(this)); |
| } |
| |
| /** |
| * @return {number} |
| */ |
| pixelMsRatio() { |
| return this.width() / this.duration() || 0; |
| } |
| |
| /** |
| * @param {number} timestamp |
| */ |
| _updateScrubber(timestamp) { |
| if (!this._scrubberPlayer) { |
| return; |
| } |
| this._currentTime.textContent = Number.millisToString(this._scrubberPlayer.currentTime); |
| if (this._scrubberPlayer.playState === 'pending' || this._scrubberPlayer.playState === 'running') { |
| this.element.window().requestAnimationFrame(this._updateScrubber.bind(this)); |
| } else if (this._scrubberPlayer.playState === 'finished') { |
| this._currentTime.textContent = ''; |
| } |
| } |
| |
| /** |
| * @param {!Event} event |
| * @return {boolean} |
| */ |
| _repositionScrubber(event) { |
| if (!this._selectedGroup) { |
| return false; |
| } |
| |
| // Seek to current mouse position. |
| if (!this._gridOffsetLeft) { |
| this._gridOffsetLeft = this._grid.totalOffsetLeft() + 10; |
| } |
| const seekTime = Math.max(0, event.x - this._gridOffsetLeft) / this.pixelMsRatio(); |
| this._selectedGroup.seekTo(seekTime); |
| this._togglePause(true); |
| this._animateTime(seekTime); |
| |
| // Interface with scrubber drag. |
| this._originalScrubberTime = seekTime; |
| this._originalMousePosition = event.x; |
| return true; |
| } |
| |
| /** |
| * @param {!Event} event |
| * @return {boolean} |
| */ |
| _scrubberDragStart(event) { |
| if (!this._scrubberPlayer || !this._selectedGroup) { |
| return false; |
| } |
| |
| this._originalScrubberTime = this._scrubberPlayer.currentTime; |
| this._timelineScrubber.classList.remove('animation-timeline-end'); |
| this._scrubberPlayer.pause(); |
| this._originalMousePosition = event.x; |
| |
| this._togglePause(true); |
| return true; |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _scrubberDragMove(event) { |
| const delta = event.x - this._originalMousePosition; |
| const currentTime = |
| Math.max(0, Math.min(this._originalScrubberTime + delta / this.pixelMsRatio(), this.duration())); |
| this._scrubberPlayer.currentTime = currentTime; |
| this._currentTime.textContent = Number.millisToString(Math.round(currentTime)); |
| this._selectedGroup.seekTo(currentTime); |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _scrubberDragEnd(event) { |
| const currentTime = Math.max(0, this._scrubberPlayer.currentTime); |
| this._scrubberPlayer.play(); |
| this._scrubberPlayer.currentTime = currentTime; |
| this._currentTime.window().requestAnimationFrame(this._updateScrubber.bind(this)); |
| } |
| } |
| |
| export const GlobalPlaybackRates = [1, 0.25, 0.1]; |
| |
| /** @enum {string} */ |
| export const _ControlState = { |
| Play: 'play-outline', |
| Replay: 'replay-outline', |
| Pause: 'pause-outline' |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| export class NodeUI { |
| /** |
| * @param {!Animation.AnimationModel.AnimationEffect} animationEffect |
| */ |
| constructor(animationEffect) { |
| this.element = createElementWithClass('div', 'animation-node-row'); |
| this._description = this.element.createChild('div', 'animation-node-description'); |
| this._timelineElement = this.element.createChild('div', 'animation-node-timeline'); |
| } |
| |
| /** |
| * @param {?SDK.DOMNode} node |
| */ |
| nodeResolved(node) { |
| if (!node) { |
| this._description.createTextChild('<node>'); |
| return; |
| } |
| this._node = node; |
| this._nodeChanged(); |
| Common.Linkifier.linkify(node).then(link => this._description.appendChild(link)); |
| if (!node.ownerDocument) { |
| this.nodeRemoved(); |
| } |
| } |
| |
| /** |
| * @return {!Element} |
| */ |
| createNewRow() { |
| return this._timelineElement.createChild('div', 'animation-timeline-row'); |
| } |
| |
| nodeRemoved() { |
| this.element.classList.add('animation-node-removed'); |
| this._node = null; |
| } |
| |
| _nodeChanged() { |
| this.element.classList.toggle( |
| 'animation-node-selected', this._node && this._node === UI.context.flavor(SDK.DOMNode)); |
| } |
| } |
| |
| /** |
| * @unrestricted |
| */ |
| export class StepTimingFunction { |
| /** |
| * @param {number} steps |
| * @param {string} stepAtPosition |
| */ |
| constructor(steps, stepAtPosition) { |
| this.steps = steps; |
| this.stepAtPosition = stepAtPosition; |
| } |
| |
| /** |
| * @param {string} text |
| * @return {?Animation.AnimationTimeline.StepTimingFunction} |
| */ |
| static parse(text) { |
| let match = text.match(/^steps\((\d+), (start|middle)\)$/); |
| if (match) { |
| return new Animation.AnimationTimeline.StepTimingFunction(parseInt(match[1], 10), match[2]); |
| } |
| match = text.match(/^steps\((\d+)\)$/); |
| if (match) { |
| return new Animation.AnimationTimeline.StepTimingFunction(parseInt(match[1], 10), 'end'); |
| } |
| return null; |
| } |
| } |
| |
| /* Legacy exported object */ |
| self.Animation = self.Animation || {}; |
| |
| /* Legacy exported object */ |
| Animation = Animation || {}; |
| |
| /** |
| * @implements {SDK.SDKModelObserver<!Animation.AnimationModel>} |
| * @constructor |
| * @unrestricted |
| */ |
| Animation.AnimationTimeline = AnimationTimeline; |
| |
| Animation.AnimationTimeline.GlobalPlaybackRates = GlobalPlaybackRates; |
| |
| /** @enum {string} */ |
| Animation.AnimationTimeline._ControlState = _ControlState; |
| |
| /** |
| * @unrestricted |
| * @constructor |
| */ |
| Animation.AnimationTimeline.NodeUI = NodeUI; |
| |
| /** |
| * @unrestricted |
| * @constructor |
| */ |
| Animation.AnimationTimeline.StepTimingFunction = StepTimingFunction; |