mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 21:54:47 +08:00
Move and rename files. NOAUTOREVERT=true NOPRESUBMIT=true NOTREECHECKS=true Bug: 768828 TBR=darin@chromium.org NOTRY=true Change-Id: I66d3b155808bc5bdbf237b80208e1e552bcf7f28 Reviewed-on: https://chromium-review.googlesource.com/1001153 Reviewed-by: Blink Reformat <blink-reformat@chromium.org> Commit-Queue: Blink Reformat <blink-reformat@chromium.org> Cr-Original-Commit-Position: refs/heads/master@{#549061} Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src Cr-Mirrored-Commit: 0aee4434a4dba42a42abaea9bfbc0cd196a63bc1
738 lines
24 KiB
JavaScript
738 lines
24 KiB
JavaScript
// 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
|
|
*/
|
|
Animation.AnimationTimeline = class 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));
|
|
}
|
|
};
|
|
|
|
Animation.AnimationTimeline.GlobalPlaybackRates = [1, 0.25, 0.1];
|
|
|
|
/** @enum {string} */
|
|
Animation.AnimationTimeline._ControlState = {
|
|
Play: 'play-outline',
|
|
Replay: 'replay-outline',
|
|
Pause: 'pause-outline'
|
|
};
|
|
|
|
/**
|
|
* @unrestricted
|
|
*/
|
|
Animation.AnimationTimeline.NodeUI = class {
|
|
/**
|
|
* @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
|
|
*/
|
|
async 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
|
|
*/
|
|
Animation.AnimationTimeline.StepTimingFunction = class {
|
|
/**
|
|
* @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;
|
|
}
|
|
};
|