blob: 7afa652ff5740a2161e32dbcffb51b682b4d0589 [file] [log] [blame]
// 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.
import * as Common from '../common/common.js';
import * as Host from '../host/host.js';
import * as Platform from '../platform/platform.js';
import * as SDK from '../sdk/sdk.js';
import * as UI from '../ui/ui.js';
import {AnimationGroupPreviewUI} from './AnimationGroupPreviewUI.js';
import {AnimationEffect, AnimationGroup, AnimationImpl, AnimationModel, Events} from './AnimationModel.js'; // eslint-disable-line no-unused-vars
import {AnimationScreenshotPopover} from './AnimationScreenshotPopover.js';
import {AnimationUI} from './AnimationUI.js';
/** @type {!WeakMap<!SDK.DOMModel.DOMNode, !NodeUI>} */
const nodeUIsByNode = new WeakMap();
/** @type {!WeakMap<!HTMLElement, number>} */
const playbackRates = new WeakMap();
/**
* @implements {SDK.SDKModel.SDKModelObserver<!AnimationModel>}
* @unrestricted
*/
export class AnimationTimeline extends UI.Widget.VBox {
constructor() {
super(true);
this.registerRequiredCSS('animation/animationTimeline.css', {enableLegacyPatching: true});
this.element.classList.add('animations-timeline');
this._grid = UI.UIUtils.createSVGChild(this.contentElement, '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.`;
/** @type {!Array<!HTMLElement>} */
this._playbackRateButtons;
/** @type {!HTMLElement} */
this._previewContainer;
/** @type {!HTMLElement} */
this._timelineScrubber;
/** @type {!HTMLElement} */
this._currentTime;
/** @type {!UI.PopoverHelper.PopoverHelper} */
this._popoverHelper;
/** @type {!UI.Toolbar.ToolbarButton} */
this._clearButton;
/** @type {?AnimationGroup} */
this._selectedGroup;
/** @type {!Array<!AnimationUI>} */
this._renderQueue;
/** @const */ this._defaultDuration = 100;
this._duration = this._defaultDuration;
/** @const */ this._timelineControlsWidth = 150;
/** @type {!Map.<!Protocol.DOM.BackendNodeId, !NodeUI>} */
this._nodesMap = new Map();
/** @type {!Array<!AnimationUI>} */
this._uiAnimations = [];
/** @type {!Array<!AnimationGroup>} */
this._groupBuffer = [];
/** @type {!Map.<!AnimationGroup, !AnimationGroupPreviewUI>} */
this._previewMap = new Map();
this._symbol = Symbol('animationTimeline');
/** @type {!Map.<string, !AnimationImpl>} */
this._animationsMap = new Map();
SDK.SDKModel.TargetManager.instance().addModelListener(
SDK.DOMModel.DOMModel, SDK.DOMModel.Events.NodeRemoved, this._nodeRemoved, this);
SDK.SDKModel.TargetManager.instance().observeModels(AnimationModel, this);
UI.Context.Context.instance().addFlavorChangeListener(SDK.DOMModel.DOMNode, this._nodeChanged, this);
}
/**
* @override
*/
wasShown() {
for (const animationModel of SDK.SDKModel.TargetManager.instance().models(AnimationModel)) {
this._addEventListeners(animationModel);
}
}
/**
* @override
*/
willHide() {
for (const animationModel of SDK.SDKModel.TargetManager.instance().models(AnimationModel)) {
this._removeEventListeners(animationModel);
}
if (this._popoverHelper) {
this._popoverHelper.hidePopover();
}
}
/**
* @override
* @param {!AnimationModel} animationModel
*/
modelAdded(animationModel) {
if (this.isShowing()) {
this._addEventListeners(animationModel);
}
}
/**
* @override
* @param {!AnimationModel} animationModel
*/
modelRemoved(animationModel) {
this._removeEventListeners(animationModel);
}
/**
* @param {!AnimationModel} animationModel
*/
_addEventListeners(animationModel) {
animationModel.ensureEnabled();
animationModel.addEventListener(Events.AnimationGroupStarted, this._animationGroupStarted, this);
animationModel.addEventListener(Events.ModelReset, this._reset, this);
}
/**
* @param {!AnimationModel} animationModel
*/
_removeEventListeners(animationModel) {
animationModel.removeEventListener(Events.AnimationGroupStarted, this._animationGroupStarted, this);
animationModel.removeEventListener(Events.ModelReset, this._reset, this);
}
_nodeChanged() {
for (const nodeUI of this._nodesMap.values()) {
nodeUI._nodeChanged();
}
}
/**
* @return {!HTMLElement} element
*/
_createScrubber() {
this._timelineScrubber = /** @type {!HTMLElement} */ (document.createElement('div'));
this._timelineScrubber.classList.add('animation-scrubber');
this._timelineScrubber.classList.add('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.Toolbar('animation-timeline-toolbar', toolbarContainer);
this._clearButton = new UI.Toolbar.ToolbarButton(ls`Clear all`, 'largeicon-clear');
this._clearButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._reset.bind(this));
topToolbar.appendToolbarItem(this._clearButton);
topToolbar.appendSeparator();
this._pauseButton = new UI.Toolbar.ToolbarToggle(ls`Pause all`, 'largeicon-pause', 'largeicon-resume');
this._pauseButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._togglePauseAll.bind(this));
topToolbar.appendToolbarItem(this._pauseButton);
const playbackRateControl = toolbarContainer.createChild('div', 'animation-playback-rate-control');
playbackRateControl.addEventListener('keydown', this._handlePlaybackRateControlKeyDown.bind(this));
UI.ARIAUtils.markAsListBox(playbackRateControl);
UI.ARIAUtils.setAccessibleName(playbackRateControl, ls`Playback rates`);
/** @type {!Array<!HTMLElement>} */
this._playbackRateButtons = [];
for (const playbackRate of GlobalPlaybackRates) {
const button =
/** @type {!HTMLElement} */ (playbackRateControl.createChild('button', 'animation-playback-rate-button'));
button.textContent = playbackRate ? ls`${playbackRate * 100}%` : ls`Pause`;
playbackRates.set(button, playbackRate);
button.addEventListener('click', this._setPlaybackRate.bind(this, playbackRate));
UI.ARIAUtils.markAsOption(button);
button.title = ls`Set speed to ${button.textContent}`;
button.tabIndex = -1;
this._playbackRateButtons.push(button);
}
this._updatePlaybackControls();
this._previewContainer =
/** @type {!HTMLElement} */ (this.contentElement.createChild('div', 'animation-timeline-buffer'));
UI.ARIAUtils.markAsListBox(this._previewContainer);
UI.ARIAUtils.setAccessibleName(this._previewContainer, ls`Animation previews`);
this._popoverHelper =
new UI.PopoverHelper.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 =
/** @type {!HTMLElement} */ (controls.createChild('div', 'animation-timeline-current-time monospace'));
const toolbar = new UI.Toolbar.Toolbar('animation-controls-toolbar', controls);
this._controlButton = new UI.Toolbar.ToolbarToggle(ls`Replay timeline`, 'largeicon-replay-animation');
this._controlState = _ControlState.Replay;
this._controlButton.setToggled(true);
this._controlButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._controlButtonToggle.bind(this));
toolbar.appendToolbarItem(this._controlButton);
const gridHeader = container.createChild('div', 'animation-grid-header');
UI.UIUtils.installDragHandle(
gridHeader, this._repositionScrubber.bind(this), this._scrubberDragMove.bind(this),
this._scrubberDragEnd.bind(this), 'text');
container.appendChild(this._createScrubber());
if (this._timelineScrubberLine) {
UI.UIUtils.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
*/
_handlePlaybackRateControlKeyDown(event) {
const keyboardEvent = /** @type {!KeyboardEvent} */ (event);
switch (keyboardEvent.key) {
case 'ArrowLeft':
case 'ArrowUp':
this._focusNextPlaybackRateButton(event.target, /* focusPrevious */ true);
break;
case 'ArrowRight':
case 'ArrowDown':
this._focusNextPlaybackRateButton(event.target);
break;
}
}
/**
* @param {!EventTarget|null} target
* @param {boolean=} focusPrevious
*/
_focusNextPlaybackRateButton(target, focusPrevious) {
const button = /** @type {!HTMLElement} */ (target);
const currentIndex = this._playbackRateButtons.indexOf(button);
const nextIndex = focusPrevious ? currentIndex - 1 : currentIndex + 1;
if (nextIndex < 0 || nextIndex >= this._playbackRateButtons.length) {
return;
}
const nextButton = this._playbackRateButtons[nextIndex];
nextButton.tabIndex = 0;
nextButton.focus();
if (target) {
/** @type {!HTMLElement} */ (target).tabIndex = -1;
}
}
/**
* @param {!Event} event
* @return {?UI.PopoverHelper.PopoverRequest}
*/
_getPopoverRequest(event) {
const element = /** @type {!HTMLElement} */ (event.target);
if (!element || !element.isDescendant(this._previewContainer)) {
return null;
}
return {
box: element.boxInWindow(),
show: popover => {
let animGroup;
for (const [group, previewUI] of this._previewMap) {
if (previewUI.element === element.parentElement) {
animGroup = group;
}
}
console.assert(typeof animGroup !== 'undefined');
if (!animGroup) {
return Promise.resolve(false);
}
const screenshots = animGroup.screenshots();
if (!screenshots.length) {
return Promise.resolve(false);
}
/** @type {function(*):void} */
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.<!HTMLImageElement>} screenshots
*/
function onFirstScreenshotLoaded(screenshots) {
new AnimationScreenshotPopover(screenshots).show(popover.contentElement);
fulfill(true);
}
},
hide: undefined
};
}
_togglePauseAll() {
this._allPaused = !this._allPaused;
if (this._pauseButton) {
this._pauseButton.setToggled(this._allPaused);
}
this._setPlaybackRate(this._playbackRate);
if (this._pauseButton) {
this._pauseButton.setTitle(this._allPaused ? ls`Resume all` : ls`Pause all`);
}
}
/**
* @param {number} playbackRate
*/
_setPlaybackRate(playbackRate) {
this._playbackRate = playbackRate;
for (const animationModel of SDK.SDKModel.TargetManager.instance().models(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 === playbackRates.get(button);
button.classList.toggle('selected', selected);
button.tabIndex = selected ? 0 : -1;
}
}
_controlButtonToggle() {
if (this._controlState === _ControlState.Play) {
this._togglePause(false);
} else if (this._controlState === _ControlState.Replay) {
this._replay();
} else {
this._togglePause(true);
}
}
_updateControlButton() {
if (!this._controlButton) {
return;
}
this._controlButton.setEnabled(!!this._selectedGroup);
if (this._selectedGroup && this._selectedGroup.paused()) {
this._controlState = _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._scrubberPlayer.currentTime >= this.duration()) {
this._controlState = _ControlState.Replay;
this._controlButton.setToggled(true);
this._controlButton.setTitle(ls`Replay timeline`);
this._controlButton.setGlyph('largeicon-replay-animation');
} else {
this._controlState = _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) {
if (this._scrubberPlayer) {
this._scrubberPlayer.playbackRate = this._effectivePlaybackRate();
}
if (this._selectedGroup) {
this._selectedGroup.togglePause(pause);
const preview = this._previewMap.get(this._selectedGroup);
if (preview) {
preview.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');
this._selectedGroup = null;
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.EventTarget.EventTargetEvent} event
*/
_animationGroupStarted(event) {
this._addAnimationGroup(/** @type {!AnimationGroup} */ (event.data));
}
/**
* @param {!AnimationGroup} group
*/
_addAnimationGroup(group) {
/**
* @param {!AnimationGroup} left
* @param {!AnimationGroup} right
*/
function startTimeComparator(left, right) {
if (left.startTime() === right.startTime()) {
return 0;
}
return left.startTime() > right.startTime() ? 1 : -1;
}
const previewGroup = this._previewMap.get(group);
if (previewGroup) {
if (this._selectedGroup === group) {
this._syncScrubber();
} else {
previewGroup.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) {
const discardGroup = this._previewMap.get(g);
if (!discardGroup) {
continue;
}
discardGroup.element.remove();
this._previewMap.delete(g);
g.release();
}
// Generate preview
const preview = new 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));
preview.element.addEventListener('keydown', this._handleAnimationGroupKeyDown.bind(this, group));
UI.ARIAUtils.setAccessibleName(preview.element, ls`Animation Preview ${this._groupBuffer.indexOf(group) + 1}`);
UI.ARIAUtils.markAsOption(preview.element);
if (this._previewMap.size === 1) {
const preview = this._previewMap.get(this._groupBuffer[0]);
if (preview) {
preview.element.tabIndex = 0;
}
}
}
/**
* @param {!AnimationGroup} group
* @param {!KeyboardEvent} event
*/
_handleAnimationGroupKeyDown(group, event) {
switch (event.key) {
case ' ':
case 'Enter':
this._selectAnimationGroup(group);
break;
case 'Backspace':
case 'Delete':
this._removeAnimationGroup(group, event);
break;
case 'ArrowLeft':
case 'ArrowUp':
this._focusNextGroup(group, /* target */ event.target, /* focusPrevious */ true);
break;
case 'ArrowRight':
case 'ArrowDown':
this._focusNextGroup(group, /* target */ event.target);
}
}
/**
* @param {!AnimationGroup} group
* @param {!EventTarget|null} target
* @param {boolean=} focusPrevious
*/
_focusNextGroup(group, target, focusPrevious) {
const currentGroupIndex = this._groupBuffer.indexOf(group);
const nextIndex = focusPrevious ? currentGroupIndex - 1 : currentGroupIndex + 1;
if (nextIndex < 0 || nextIndex >= this._groupBuffer.length) {
return;
}
const preview = this._previewMap.get(this._groupBuffer[nextIndex]);
if (preview) {
preview.element.tabIndex = 0;
preview.element.focus();
}
if (target) {
/** @type {!HTMLElement} */ (target).tabIndex = -1;
}
}
/**
* @param {!AnimationGroup} group
* @param {!Event} event
*/
_removeAnimationGroup(group, event) {
const currentGroupIndex = this._groupBuffer.indexOf(group);
Platform.ArrayUtilities.removeElement(this._groupBuffer, group);
const previewGroup = this._previewMap.get(group);
if (previewGroup) {
previewGroup.element.remove();
}
this._previewMap.delete(group);
group.release();
event.consume(true);
if (this._selectedGroup === group) {
this._clearTimeline();
this._renderGrid();
}
const groupLength = this._groupBuffer.length;
if (groupLength === 0) {
/** @type {!HTMLElement} */ (this._clearButton.element).focus();
return;
}
const nextGroup = currentGroupIndex >= this._groupBuffer.length ?
this._previewMap.get(this._groupBuffer[this._groupBuffer.length - 1]) :
this._previewMap.get(this._groupBuffer[currentGroupIndex]);
if (nextGroup) {
nextGroup.element.tabIndex = 0;
nextGroup.element.focus();
}
}
/**
* @param {!AnimationGroup} group
*/
_selectAnimationGroup(group) {
/**
* @param {!AnimationGroupPreviewUI} ui
* @param {!AnimationGroup} group
* @this {!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 {!AnimationImpl} animation
*/
_addAnimation(animation) {
/**
* @param {?SDK.DOMModel.DOMNode} node
* @this {AnimationTimeline}
*/
function nodeResolved(node) {
uiAnimation.setNode(node);
if (node && nodeUI) {
nodeUI.nodeResolved(node);
nodeUIsByNode.set(node, nodeUI);
}
}
let nodeUI = this._nodesMap.get(animation.source().backendNodeId());
if (!nodeUI) {
nodeUI = new NodeUI(animation.source());
this._animationsContainer.appendChild(nodeUI.element);
this._nodesMap.set(animation.source().backendNodeId(), nodeUI);
}
const nodeRow = nodeUI.createNewRow();
const uiAnimation = new AnimationUI(animation, this, nodeRow);
animation.source().deferredNode().resolve(nodeResolved.bind(this));
this._uiAnimations.push(uiAnimation);
this._animationsMap.set(animation.id(), animation);
}
/**
* @param {!Common.EventTarget.EventTargetEvent} event
*/
_nodeRemoved(event) {
const node = event.data.node;
const nodeUI = nodeUIsByNode.get(node);
if (nodeUI) {
nodeUI.nodeRemoved();
}
}
_renderGrid() {
/** @const */ const gridSize = 250;
this._grid.setAttribute('width', (this.width() + 10).toString());
this._grid.setAttribute('height', ((this._cachedTimelineHeight || 0) + 30).toString());
this._grid.setAttribute('shape-rendering', 'crispEdges');
this._grid.removeChildren();
let lastDraw = undefined;
for (let time = 0; time < this.duration(); time += gridSize) {
const line = UI.UIUtils.createSVGChild(this._grid, 'rect', 'animation-timeline-grid-line');
line.setAttribute('x', (time * this.pixelMsRatio() + 10).toString());
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 = UI.UIUtils.createSVGChild(this._grid, 'text', 'animation-timeline-grid-label');
label.textContent = Number.millisToString(time);
label.setAttribute('x', (gridWidth + 10).toString());
label.setAttribute('y', '16');
}
}
}
scheduleRedraw() {
/** @type {!Array<!AnimationUI>} */
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)) {
const animationUI = this._renderQueue.shift();
if (animationUI) {
animationUI.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 {!AnimationImpl} 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 || 0);
if (this._scrubberPlayer.playState.toString() === '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 {x} = /** @type {*} */ (event);
const seekTime = Math.max(0, x - this._gridOffsetLeft) / this.pixelMsRatio();
this._selectedGroup.seekTo(seekTime);
this._togglePause(true);
this._animateTime(seekTime);
// Interface with scrubber drag.
this._originalScrubberTime = seekTime;
this._originalMousePosition = 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();
const {x} = /** @type {*} */ (event);
this._originalMousePosition = x;
this._togglePause(true);
return true;
}
/**
* @param {!Event} event
*/
_scrubberDragMove(event) {
const {x} = /** @type {*} */ (event);
const delta = x - this._originalMousePosition;
const currentTime =
Math.max(0, Math.min((this._originalScrubberTime || 0) + delta / this.pixelMsRatio(), this.duration()));
if (this._scrubberPlayer) {
this._scrubberPlayer.currentTime = currentTime;
}
this._currentTime.textContent = Number.millisToString(Math.round(currentTime));
if (this._selectedGroup) {
this._selectedGroup.seekTo(currentTime);
}
}
/**
* @param {!Event} event
*/
_scrubberDragEnd(event) {
if (this._scrubberPlayer) {
const currentTime = Math.max(0, this._scrubberPlayer.currentTime || 0);
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 {!AnimationEffect} animationEffect
*/
constructor(animationEffect) {
this.element = document.createElement('div');
this.element.classList.add('animation-node-row');
this._description = this.element.createChild('div', 'animation-node-description');
this._description.tabIndex = 0;
this._timelineElement = this.element.createChild('div', 'animation-node-timeline');
UI.ARIAUtils.markAsApplication(this._timelineElement);
}
/**
* @param {?SDK.DOMModel.DOMNode} node
*/
nodeResolved(node) {
if (!node) {
UI.UIUtils.createTextChild(this._description, '<node>');
return;
}
this._node = node;
this._nodeChanged();
Common.Linkifier.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() {
let animationNodeSelected = false;
if (this._node) {
animationNodeSelected = (this._node === UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode));
}
this.element.classList.toggle('animation-node-selected', animationNodeSelected);
}
}
/**
* @unrestricted
*/
export class StepTimingFunction {
/**
* @param {number} steps
* @param {string} stepAtPosition
*/
constructor(steps, stepAtPosition) {
this.steps = steps;
this.stepAtPosition = stepAtPosition;
}
/**
* @param {string} text
* @return {?StepTimingFunction}
*/
static parse(text) {
let match = text.match(/^steps\((\d+), (start|middle)\)$/);
if (match) {
return new StepTimingFunction(parseInt(match[1], 10), match[2]);
}
match = text.match(/^steps\((\d+)\)$/);
if (match) {
return new StepTimingFunction(parseInt(match[1], 10), 'end');
}
return null;
}
}