blob: 45b29149449653dd439a72074dfbf39189735e5e [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:371// 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.
Jack Franklin899893a2020-07-30 14:01:394// @ts-nocheck
5// TODO(crbug.com/1011811): Enable TypeScript compiler checks
Paul Lewis752031f2020-01-09 14:14:236
Tim van der Lippe91d32562020-02-13 15:08:477import * as Common from '../common/common.js';
8import * as Host from '../host/host.js';
Simon Zünd684ebae2020-03-09 11:44:169import * as Platform from '../platform/platform.js';
Tim van der Lippe91d32562020-02-13 15:08:4710import * as SDK from '../sdk/sdk.js';
11import * as UI from '../ui/ui.js';
12
Paul Lewis752031f2020-01-09 14:14:2313import {AnimationGroupPreviewUI} from './AnimationGroupPreviewUI.js';
14import {AnimationEffect, AnimationGroup, AnimationImpl, AnimationModel, Events} from './AnimationModel.js'; // eslint-disable-line no-unused-vars
15import {AnimationScreenshotPopover} from './AnimationScreenshotPopover.js';
16import {AnimationUI} from './AnimationUI.js';
17
Blink Reformat4c46d092018-04-07 15:32:3718/**
Tim van der Lippe91d32562020-02-13 15:08:4719 * @implements {SDK.SDKModel.SDKModelObserver<!AnimationModel>}
Blink Reformat4c46d092018-04-07 15:32:3720 * @unrestricted
21 */
Tim van der Lippe91d32562020-02-13 15:08:4722export class AnimationTimeline extends UI.Widget.VBox {
Blink Reformat4c46d092018-04-07 15:32:3723 constructor() {
24 super(true);
25 this.registerRequiredCSS('animation/animationTimeline.css');
26 this.element.classList.add('animations-timeline');
27
28 this._grid = this.contentElement.createSVGChild('svg', 'animation-timeline-grid');
29
30 this._playbackRate = 1;
31 this._allPaused = false;
32 this._createHeader();
33 this._animationsContainer = this.contentElement.createChild('div', 'animation-timeline-rows');
34 const timelineHint = this.contentElement.createChild('div', 'animation-timeline-rows-hint');
35 timelineHint.textContent = ls`Select an effect above to inspect and modify.`;
36
37 /** @const */ this._defaultDuration = 100;
38 this._duration = this._defaultDuration;
39 /** @const */ this._timelineControlsWidth = 150;
Paul Lewis752031f2020-01-09 14:14:2340 /** @type {!Map.<!Protocol.DOM.BackendNodeId, !NodeUI>} */
Blink Reformat4c46d092018-04-07 15:32:3741 this._nodesMap = new Map();
42 this._uiAnimations = [];
43 this._groupBuffer = [];
Paul Lewis752031f2020-01-09 14:14:2344 /** @type {!Map.<!AnimationGroup, !AnimationGroupPreviewUI>} */
Blink Reformat4c46d092018-04-07 15:32:3745 this._previewMap = new Map();
46 this._symbol = Symbol('animationTimeline');
Paul Lewis752031f2020-01-09 14:14:2347 /** @type {!Map.<string, !AnimationImpl>} */
Blink Reformat4c46d092018-04-07 15:32:3748 this._animationsMap = new Map();
Paul Lewisdaac1062020-03-05 14:37:1049 SDK.SDKModel.TargetManager.instance().addModelListener(
Tim van der Lippe91d32562020-02-13 15:08:4750 SDK.DOMModel.DOMModel, SDK.DOMModel.Events.NodeRemoved, this._nodeRemoved, this);
Paul Lewisdaac1062020-03-05 14:37:1051 SDK.SDKModel.TargetManager.instance().observeModels(AnimationModel, this);
Tim van der Lipped1a00aa2020-08-19 16:03:5652 UI.Context.Context.instance().addFlavorChangeListener(SDK.DOMModel.DOMNode, this._nodeChanged, this);
Blink Reformat4c46d092018-04-07 15:32:3753 }
54
55 /**
56 * @override
57 */
58 wasShown() {
Paul Lewisdaac1062020-03-05 14:37:1059 for (const animationModel of SDK.SDKModel.TargetManager.instance().models(AnimationModel)) {
Blink Reformat4c46d092018-04-07 15:32:3760 this._addEventListeners(animationModel);
Tim van der Lippe1d6e57a2019-09-30 11:55:3461 }
Blink Reformat4c46d092018-04-07 15:32:3762 }
63
64 /**
65 * @override
66 */
67 willHide() {
Paul Lewisdaac1062020-03-05 14:37:1068 for (const animationModel of SDK.SDKModel.TargetManager.instance().models(AnimationModel)) {
Blink Reformat4c46d092018-04-07 15:32:3769 this._removeEventListeners(animationModel);
Tim van der Lippe1d6e57a2019-09-30 11:55:3470 }
Blink Reformat4c46d092018-04-07 15:32:3771 this._popoverHelper.hidePopover();
72 }
73
74 /**
75 * @override
Paul Lewis752031f2020-01-09 14:14:2376 * @param {!AnimationModel} animationModel
Blink Reformat4c46d092018-04-07 15:32:3777 */
78 modelAdded(animationModel) {
Tim van der Lippe1d6e57a2019-09-30 11:55:3479 if (this.isShowing()) {
Blink Reformat4c46d092018-04-07 15:32:3780 this._addEventListeners(animationModel);
Tim van der Lippe1d6e57a2019-09-30 11:55:3481 }
Blink Reformat4c46d092018-04-07 15:32:3782 }
83
84 /**
85 * @override
Paul Lewis752031f2020-01-09 14:14:2386 * @param {!AnimationModel} animationModel
Blink Reformat4c46d092018-04-07 15:32:3787 */
88 modelRemoved(animationModel) {
89 this._removeEventListeners(animationModel);
90 }
91
92 /**
Paul Lewis752031f2020-01-09 14:14:2393 * @param {!AnimationModel} animationModel
Blink Reformat4c46d092018-04-07 15:32:3794 */
95 _addEventListeners(animationModel) {
96 animationModel.ensureEnabled();
Paul Lewis752031f2020-01-09 14:14:2397 animationModel.addEventListener(Events.AnimationGroupStarted, this._animationGroupStarted, this);
98 animationModel.addEventListener(Events.ModelReset, this._reset, this);
Blink Reformat4c46d092018-04-07 15:32:3799 }
100
101 /**
Paul Lewis752031f2020-01-09 14:14:23102 * @param {!AnimationModel} animationModel
Blink Reformat4c46d092018-04-07 15:32:37103 */
104 _removeEventListeners(animationModel) {
Paul Lewis752031f2020-01-09 14:14:23105 animationModel.removeEventListener(Events.AnimationGroupStarted, this._animationGroupStarted, this);
106 animationModel.removeEventListener(Events.ModelReset, this._reset, this);
Blink Reformat4c46d092018-04-07 15:32:37107 }
108
109 _nodeChanged() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34110 for (const nodeUI of this._nodesMap.values()) {
Blink Reformat4c46d092018-04-07 15:32:37111 nodeUI._nodeChanged();
Tim van der Lippe1d6e57a2019-09-30 11:55:34112 }
Blink Reformat4c46d092018-04-07 15:32:37113 }
114
115 /**
116 * @return {!Element} element
117 */
118 _createScrubber() {
Tim van der Lippee7f27052020-05-01 15:15:28119 this._timelineScrubber = document.createElement('div');
120 this._timelineScrubber.classList.add('animation-scrubber');
121 this._timelineScrubber.classList.add('hidden');
Blink Reformat4c46d092018-04-07 15:32:37122 this._timelineScrubberLine = this._timelineScrubber.createChild('div', 'animation-scrubber-line');
123 this._timelineScrubberLine.createChild('div', 'animation-scrubber-head');
124 this._timelineScrubber.createChild('div', 'animation-time-overlay');
125 return this._timelineScrubber;
126 }
127
128 _createHeader() {
129 const toolbarContainer = this.contentElement.createChild('div', 'animation-timeline-toolbar-container');
Tim van der Lippe91d32562020-02-13 15:08:47130 const topToolbar = new UI.Toolbar.Toolbar('animation-timeline-toolbar', toolbarContainer);
131 this._clearButton = new UI.Toolbar.ToolbarButton(ls`Clear all`, 'largeicon-clear');
132 this._clearButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._reset.bind(this));
Kalon Hinds0fae2e72020-01-08 20:02:02133 topToolbar.appendToolbarItem(this._clearButton);
Blink Reformat4c46d092018-04-07 15:32:37134 topToolbar.appendSeparator();
135
Tim van der Lippe91d32562020-02-13 15:08:47136 this._pauseButton = new UI.Toolbar.ToolbarToggle(ls`Pause all`, 'largeicon-pause', 'largeicon-resume');
137 this._pauseButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._togglePauseAll.bind(this));
Blink Reformat4c46d092018-04-07 15:32:37138 topToolbar.appendToolbarItem(this._pauseButton);
139
140 const playbackRateControl = toolbarContainer.createChild('div', 'animation-playback-rate-control');
Kalon Hinds0fae2e72020-01-08 20:02:02141 playbackRateControl.addEventListener('keydown', this._handlePlaybackRateControlKeyDown.bind(this));
Kalon Hinds08dd3682020-01-10 23:51:25142 UI.ARIAUtils.markAsListBox(playbackRateControl);
143 UI.ARIAUtils.setAccessibleName(playbackRateControl, ls`Playback rates`);
144
Blink Reformat4c46d092018-04-07 15:32:37145 this._playbackRateButtons = [];
Paul Lewis752031f2020-01-09 14:14:23146 for (const playbackRate of GlobalPlaybackRates) {
Kalon Hinds0fae2e72020-01-08 20:02:02147 const button = playbackRateControl.createChild('button', 'animation-playback-rate-button');
Blink Reformat4c46d092018-04-07 15:32:37148 button.textContent = playbackRate ? ls`${playbackRate * 100}%` : ls`Pause`;
149 button.playbackRate = playbackRate;
150 button.addEventListener('click', this._setPlaybackRate.bind(this, playbackRate));
Kalon Hinds08dd3682020-01-10 23:51:25151 UI.ARIAUtils.markAsOption(button);
Blink Reformat4c46d092018-04-07 15:32:37152 button.title = ls`Set speed to ${button.textContent}`;
Kalon Hinds0fae2e72020-01-08 20:02:02153 button.tabIndex = -1;
Blink Reformat4c46d092018-04-07 15:32:37154 this._playbackRateButtons.push(button);
155 }
156 this._updatePlaybackControls();
157
158 this._previewContainer = this.contentElement.createChild('div', 'animation-timeline-buffer');
Kalon Hinds08dd3682020-01-10 23:51:25159 UI.ARIAUtils.markAsListBox(this._previewContainer);
160 UI.ARIAUtils.setAccessibleName(this._previewContainer, ls`Animation previews`);
Tim van der Lippe91d32562020-02-13 15:08:47161 this._popoverHelper =
162 new UI.PopoverHelper.PopoverHelper(this._previewContainer, this._getPopoverRequest.bind(this));
Blink Reformat4c46d092018-04-07 15:32:37163 this._popoverHelper.setDisableOnClick(true);
164 this._popoverHelper.setTimeout(0);
165 const emptyBufferHint = this.contentElement.createChild('div', 'animation-timeline-buffer-hint');
166 emptyBufferHint.textContent = ls`Listening for animations...`;
167 const container = this.contentElement.createChild('div', 'animation-timeline-header');
168 const controls = container.createChild('div', 'animation-controls');
169 this._currentTime = controls.createChild('div', 'animation-timeline-current-time monospace');
170
Tim van der Lippe91d32562020-02-13 15:08:47171 const toolbar = new UI.Toolbar.Toolbar('animation-controls-toolbar', controls);
172 this._controlButton = new UI.Toolbar.ToolbarToggle(ls`Replay timeline`, 'largeicon-replay-animation');
Paul Lewis752031f2020-01-09 14:14:23173 this._controlState = _ControlState.Replay;
Blink Reformat4c46d092018-04-07 15:32:37174 this._controlButton.setToggled(true);
Tim van der Lippe91d32562020-02-13 15:08:47175 this._controlButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._controlButtonToggle.bind(this));
Blink Reformat4c46d092018-04-07 15:32:37176 toolbar.appendToolbarItem(this._controlButton);
177
178 const gridHeader = container.createChild('div', 'animation-grid-header');
Tim van der Lippe91d32562020-02-13 15:08:47179 UI.UIUtils.installDragHandle(
Blink Reformat4c46d092018-04-07 15:32:37180 gridHeader, this._repositionScrubber.bind(this), this._scrubberDragMove.bind(this),
181 this._scrubberDragEnd.bind(this), 'text');
182 container.appendChild(this._createScrubber());
Tim van der Lippe91d32562020-02-13 15:08:47183 UI.UIUtils.installDragHandle(
Blink Reformat4c46d092018-04-07 15:32:37184 this._timelineScrubberLine, this._scrubberDragStart.bind(this), this._scrubberDragMove.bind(this),
185 this._scrubberDragEnd.bind(this), 'col-resize');
186 this._currentTime.textContent = '';
187
188 return container;
189 }
190
191 /**
192 * @param {!Event} event
Kalon Hinds0fae2e72020-01-08 20:02:02193 */
194 _handlePlaybackRateControlKeyDown(event) {
195 switch (event.key) {
196 case 'ArrowLeft':
197 case 'ArrowUp':
198 this._focusNextPlaybackRateButton(event.target, /* focusPrevious */ true);
199 break;
200 case 'ArrowRight':
201 case 'ArrowDown':
202 this._focusNextPlaybackRateButton(event.target);
203 break;
204 }
205 }
206
207 /**
208 * @param {!EventTarget|null} target
209 * @param {boolean=} focusPrevious
210 */
211 _focusNextPlaybackRateButton(target, focusPrevious) {
212 const currentIndex = this._playbackRateButtons.indexOf(target);
213 const nextIndex = focusPrevious ? currentIndex - 1 : currentIndex + 1;
214 if (nextIndex < 0 || nextIndex >= this._playbackRateButtons.length) {
215 return;
216 }
217 const nextButton = this._playbackRateButtons[nextIndex];
218 nextButton.tabIndex = 0;
219 nextButton.focus();
220 target.tabIndex = -1;
221 }
222
223 /**
224 * @param {!Event} event
Tim van der Lippe49ff36b2020-10-08 12:11:00225 * @return {?UI.PopoverHelper.PopoverRequest}
Blink Reformat4c46d092018-04-07 15:32:37226 */
227 _getPopoverRequest(event) {
228 const element = event.target;
Tim van der Lippe1d6e57a2019-09-30 11:55:34229 if (!element.isDescendant(this._previewContainer)) {
Blink Reformat4c46d092018-04-07 15:32:37230 return null;
Tim van der Lippe1d6e57a2019-09-30 11:55:34231 }
Blink Reformat4c46d092018-04-07 15:32:37232
233 return {
234 box: event.target.boxInWindow(),
235 show: popover => {
236 let animGroup;
Simon Zündf27be3d2020-02-11 14:46:27237 for (const [group, previewUI] of this._previewMap) {
238 if (previewUI.element === element.parentElement) {
Blink Reformat4c46d092018-04-07 15:32:37239 animGroup = group;
Tim van der Lippe1d6e57a2019-09-30 11:55:34240 }
Blink Reformat4c46d092018-04-07 15:32:37241 }
242 console.assert(animGroup);
Michael Liao64b9ee32020-05-01 17:18:29243 if (!animGroup) {
244 return Promise.resolve(false);
245 }
Blink Reformat4c46d092018-04-07 15:32:37246 const screenshots = animGroup.screenshots();
Tim van der Lippe1d6e57a2019-09-30 11:55:34247 if (!screenshots.length) {
Blink Reformat4c46d092018-04-07 15:32:37248 return Promise.resolve(false);
Tim van der Lippe1d6e57a2019-09-30 11:55:34249 }
Blink Reformat4c46d092018-04-07 15:32:37250
251 let fulfill;
Patrick Brossete65aaac2020-06-22 08:04:40252 const promise = new Promise(x => {
253 fulfill = x;
254 });
Tim van der Lippe1d6e57a2019-09-30 11:55:34255 if (!screenshots[0].complete) {
Blink Reformat4c46d092018-04-07 15:32:37256 screenshots[0].onload = onFirstScreenshotLoaded.bind(null, screenshots);
Tim van der Lippe1d6e57a2019-09-30 11:55:34257 } else {
Blink Reformat4c46d092018-04-07 15:32:37258 onFirstScreenshotLoaded(screenshots);
Tim van der Lippe1d6e57a2019-09-30 11:55:34259 }
Blink Reformat4c46d092018-04-07 15:32:37260 return promise;
261
262 /**
263 * @param {!Array.<!Image>} screenshots
264 */
265 function onFirstScreenshotLoaded(screenshots) {
Paul Lewis752031f2020-01-09 14:14:23266 new AnimationScreenshotPopover(screenshots).show(popover.contentElement);
Blink Reformat4c46d092018-04-07 15:32:37267 fulfill(true);
268 }
269 }
270 };
271 }
272
273 _togglePauseAll() {
274 this._allPaused = !this._allPaused;
275 this._pauseButton.setToggled(this._allPaused);
276 this._setPlaybackRate(this._playbackRate);
277 this._pauseButton.setTitle(this._allPaused ? ls`Resume all` : ls`Pause all`);
278 }
279
280 /**
281 * @param {number} playbackRate
282 */
283 _setPlaybackRate(playbackRate) {
284 this._playbackRate = playbackRate;
Paul Lewisdaac1062020-03-05 14:37:10285 for (const animationModel of SDK.SDKModel.TargetManager.instance().models(AnimationModel)) {
Blink Reformat4c46d092018-04-07 15:32:37286 animationModel.setPlaybackRate(this._allPaused ? 0 : this._playbackRate);
Tim van der Lippe1d6e57a2019-09-30 11:55:34287 }
Blink Reformat4c46d092018-04-07 15:32:37288 Host.userMetrics.actionTaken(Host.UserMetrics.Action.AnimationsPlaybackRateChanged);
Tim van der Lippe1d6e57a2019-09-30 11:55:34289 if (this._scrubberPlayer) {
Blink Reformat4c46d092018-04-07 15:32:37290 this._scrubberPlayer.playbackRate = this._effectivePlaybackRate();
Tim van der Lippe1d6e57a2019-09-30 11:55:34291 }
Blink Reformat4c46d092018-04-07 15:32:37292
293 this._updatePlaybackControls();
294 }
295
296 _updatePlaybackControls() {
297 for (const button of this._playbackRateButtons) {
298 const selected = this._playbackRate === button.playbackRate;
299 button.classList.toggle('selected', selected);
Kalon Hinds0fae2e72020-01-08 20:02:02300 button.tabIndex = selected ? 0 : -1;
Blink Reformat4c46d092018-04-07 15:32:37301 }
302 }
303
304 _controlButtonToggle() {
Paul Lewis752031f2020-01-09 14:14:23305 if (this._controlState === _ControlState.Play) {
Blink Reformat4c46d092018-04-07 15:32:37306 this._togglePause(false);
Paul Lewis752031f2020-01-09 14:14:23307 } else if (this._controlState === _ControlState.Replay) {
Blink Reformat4c46d092018-04-07 15:32:37308 this._replay();
Tim van der Lippe1d6e57a2019-09-30 11:55:34309 } else {
Blink Reformat4c46d092018-04-07 15:32:37310 this._togglePause(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34311 }
Blink Reformat4c46d092018-04-07 15:32:37312 }
313
314 _updateControlButton() {
315 this._controlButton.setEnabled(!!this._selectedGroup);
316 if (this._selectedGroup && this._selectedGroup.paused()) {
Paul Lewis752031f2020-01-09 14:14:23317 this._controlState = _ControlState.Play;
Blink Reformat4c46d092018-04-07 15:32:37318 this._controlButton.setToggled(true);
319 this._controlButton.setTitle(ls`Play timeline`);
320 this._controlButton.setGlyph('largeicon-play-animation');
321 } else if (!this._scrubberPlayer || this._scrubberPlayer.currentTime >= this.duration()) {
Paul Lewis752031f2020-01-09 14:14:23322 this._controlState = _ControlState.Replay;
Blink Reformat4c46d092018-04-07 15:32:37323 this._controlButton.setToggled(true);
324 this._controlButton.setTitle(ls`Replay timeline`);
325 this._controlButton.setGlyph('largeicon-replay-animation');
326 } else {
Paul Lewis752031f2020-01-09 14:14:23327 this._controlState = _ControlState.Pause;
Blink Reformat4c46d092018-04-07 15:32:37328 this._controlButton.setToggled(false);
329 this._controlButton.setTitle(ls`Pause timeline`);
330 this._controlButton.setGlyph('largeicon-pause-animation');
331 }
332 }
333
334 /**
335 * @return {number}
336 */
337 _effectivePlaybackRate() {
338 return (this._allPaused || (this._selectedGroup && this._selectedGroup.paused())) ? 0 : this._playbackRate;
339 }
340
341 /**
342 * @param {boolean} pause
343 */
344 _togglePause(pause) {
345 this._selectedGroup.togglePause(pause);
Tim van der Lippe1d6e57a2019-09-30 11:55:34346 if (this._scrubberPlayer) {
Blink Reformat4c46d092018-04-07 15:32:37347 this._scrubberPlayer.playbackRate = this._effectivePlaybackRate();
Tim van der Lippe1d6e57a2019-09-30 11:55:34348 }
Blink Reformat4c46d092018-04-07 15:32:37349 this._previewMap.get(this._selectedGroup).element.classList.toggle('paused', pause);
350 this._updateControlButton();
351 }
352
353 _replay() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34354 if (!this._selectedGroup) {
Blink Reformat4c46d092018-04-07 15:32:37355 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34356 }
Blink Reformat4c46d092018-04-07 15:32:37357 this._selectedGroup.seekTo(0);
358 this._animateTime(0);
359 this._updateControlButton();
360 }
361
362 /**
363 * @return {number}
364 */
365 duration() {
366 return this._duration;
367 }
368
369 /**
370 * @param {number} duration
371 */
372 setDuration(duration) {
373 this._duration = duration;
374 this.scheduleRedraw();
375 }
376
377 _clearTimeline() {
378 this._uiAnimations = [];
379 this._nodesMap.clear();
380 this._animationsMap.clear();
381 this._animationsContainer.removeChildren();
382 this._duration = this._defaultDuration;
383 this._timelineScrubber.classList.add('hidden');
384 delete this._selectedGroup;
Tim van der Lippe1d6e57a2019-09-30 11:55:34385 if (this._scrubberPlayer) {
Blink Reformat4c46d092018-04-07 15:32:37386 this._scrubberPlayer.cancel();
Tim van der Lippe1d6e57a2019-09-30 11:55:34387 }
Blink Reformat4c46d092018-04-07 15:32:37388 delete this._scrubberPlayer;
389 this._currentTime.textContent = '';
390 this._updateControlButton();
391 }
392
393 _reset() {
394 this._clearTimeline();
Tim van der Lippe1d6e57a2019-09-30 11:55:34395 if (this._allPaused) {
Blink Reformat4c46d092018-04-07 15:32:37396 this._togglePauseAll();
Tim van der Lippe1d6e57a2019-09-30 11:55:34397 } else {
Blink Reformat4c46d092018-04-07 15:32:37398 this._setPlaybackRate(this._playbackRate);
Tim van der Lippe1d6e57a2019-09-30 11:55:34399 }
Blink Reformat4c46d092018-04-07 15:32:37400
Tim van der Lippe1d6e57a2019-09-30 11:55:34401 for (const group of this._groupBuffer) {
Blink Reformat4c46d092018-04-07 15:32:37402 group.release();
Tim van der Lippe1d6e57a2019-09-30 11:55:34403 }
Blink Reformat4c46d092018-04-07 15:32:37404 this._groupBuffer = [];
405 this._previewMap.clear();
406 this._previewContainer.removeChildren();
407 this._popoverHelper.hidePopover();
408 this._renderGrid();
409 }
410
411 /**
Tim van der Lippec02a97c2020-02-14 14:39:27412 * @param {!Common.EventTarget.EventTargetEvent} event
Blink Reformat4c46d092018-04-07 15:32:37413 */
414 _animationGroupStarted(event) {
Paul Lewis752031f2020-01-09 14:14:23415 this._addAnimationGroup(/** @type {!AnimationGroup} */ (event.data));
Blink Reformat4c46d092018-04-07 15:32:37416 }
417
418 /**
Paul Lewis752031f2020-01-09 14:14:23419 * @param {!AnimationGroup} group
Blink Reformat4c46d092018-04-07 15:32:37420 */
421 _addAnimationGroup(group) {
422 /**
Paul Lewis752031f2020-01-09 14:14:23423 * @param {!AnimationGroup} left
Blink Reformat4c46d092018-04-07 15:32:37424 * @param {!Animation.AnimationModel.AnimationGroup} right
425 */
426 function startTimeComparator(left, right) {
427 return left.startTime() > right.startTime();
428 }
429
430 if (this._previewMap.get(group)) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34431 if (this._selectedGroup === group) {
Blink Reformat4c46d092018-04-07 15:32:37432 this._syncScrubber();
Tim van der Lippe1d6e57a2019-09-30 11:55:34433 } else {
Blink Reformat4c46d092018-04-07 15:32:37434 this._previewMap.get(group).replay();
Tim van der Lippe1d6e57a2019-09-30 11:55:34435 }
Blink Reformat4c46d092018-04-07 15:32:37436 return;
437 }
438 this._groupBuffer.sort(startTimeComparator);
439 // Discard oldest groups from buffer if necessary
440 const groupsToDiscard = [];
441 const bufferSize = this.width() / 50;
442 while (this._groupBuffer.length > bufferSize) {
443 const toDiscard = this._groupBuffer.splice(this._groupBuffer[0] === this._selectedGroup ? 1 : 0, 1);
444 groupsToDiscard.push(toDiscard[0]);
445 }
446 for (const g of groupsToDiscard) {
447 this._previewMap.get(g).element.remove();
448 this._previewMap.delete(g);
449 g.release();
450 }
451 // Generate preview
Paul Lewis752031f2020-01-09 14:14:23452 const preview = new AnimationGroupPreviewUI(group);
Blink Reformat4c46d092018-04-07 15:32:37453 this._groupBuffer.push(group);
454 this._previewMap.set(group, preview);
455 this._previewContainer.appendChild(preview.element);
456 preview.removeButton().addEventListener('click', this._removeAnimationGroup.bind(this, group));
457 preview.element.addEventListener('click', this._selectAnimationGroup.bind(this, group));
Kalon Hinds0fae2e72020-01-08 20:02:02458 preview.element.addEventListener('keydown', this._handleAnimationGroupKeyDown.bind(this, group));
Kalon Hinds08dd3682020-01-10 23:51:25459 UI.ARIAUtils.setAccessibleName(preview.element, ls`Animation Preview ${this._groupBuffer.indexOf(group) + 1}`);
460 UI.ARIAUtils.markAsOption(preview.element);
461
Kalon Hinds0fae2e72020-01-08 20:02:02462 if (this._previewMap.size === 1) {
463 this._previewMap.get(this._groupBuffer[0]).element.tabIndex = 0;
464 }
465 }
466
467 /**
Paul Lewis752031f2020-01-09 14:14:23468 * @param {!AnimationGroup} group
Kalon Hinds0fae2e72020-01-08 20:02:02469 * @param {!Event} event
470 */
471 _handleAnimationGroupKeyDown(group, event) {
472 switch (event.key) {
473 case ' ':
474 case 'Enter':
475 this._selectAnimationGroup(group);
476 break;
477 case 'Backspace':
478 case 'Delete':
479 this._removeAnimationGroup(group, event);
480 break;
481 case 'ArrowLeft':
482 case 'ArrowUp':
483 this._focusNextGroup(group, /* target */ event.target, /* focusPrevious */ true);
484 break;
485 case 'ArrowRight':
486 case 'ArrowDown':
487 this._focusNextGroup(group, /* target */ event.target);
488 }
489 }
490
491 /**
Paul Lewis752031f2020-01-09 14:14:23492 * @param {!AnimationGroup} group
Kalon Hinds0fae2e72020-01-08 20:02:02493 * @param {!EventTarget|null} target
494 * @param {boolean=} focusPrevious
495 */
496 _focusNextGroup(group, target, focusPrevious) {
497 const currentGroupIndex = this._groupBuffer.indexOf(group);
498 const nextIndex = focusPrevious ? currentGroupIndex - 1 : currentGroupIndex + 1;
499 if (nextIndex < 0 || nextIndex >= this._groupBuffer.length) {
500 return;
501 }
502 const preview = this._previewMap.get(this._groupBuffer[nextIndex]);
503 preview.element.tabIndex = 0;
504 preview.element.focus();
505 target.tabIndex = -1;
Blink Reformat4c46d092018-04-07 15:32:37506 }
507
508 /**
Paul Lewis752031f2020-01-09 14:14:23509 * @param {!AnimationGroup} group
Blink Reformat4c46d092018-04-07 15:32:37510 * @param {!Event} event
511 */
512 _removeAnimationGroup(group, event) {
Kalon Hinds0fae2e72020-01-08 20:02:02513 const currentGroupIndex = this._groupBuffer.indexOf(group);
514
Simon Zünd684ebae2020-03-09 11:44:16515 Platform.ArrayUtilities.removeElement(this._groupBuffer, group);
Blink Reformat4c46d092018-04-07 15:32:37516 this._previewMap.get(group).element.remove();
517 this._previewMap.delete(group);
518 group.release();
519 event.consume(true);
520
521 if (this._selectedGroup === group) {
522 this._clearTimeline();
523 this._renderGrid();
524 }
Kalon Hinds0fae2e72020-01-08 20:02:02525
526 const groupLength = this._groupBuffer.length;
527 if (groupLength === 0) {
528 this._clearButton.element.focus();
529 return;
530 }
531 const nextGroup = currentGroupIndex >= this._groupBuffer.length ?
532 this._previewMap.get(this._groupBuffer[this._groupBuffer.length - 1]) :
533 this._previewMap.get(this._groupBuffer[currentGroupIndex]);
534 nextGroup.element.tabIndex = 0;
535 nextGroup.element.focus();
Blink Reformat4c46d092018-04-07 15:32:37536 }
537
538 /**
Paul Lewis752031f2020-01-09 14:14:23539 * @param {!AnimationGroup} group
Blink Reformat4c46d092018-04-07 15:32:37540 */
541 _selectAnimationGroup(group) {
542 /**
Paul Lewis752031f2020-01-09 14:14:23543 * @param {!AnimationGroupPreviewUI} ui
544 * @param {!AnimationGroup} group
545 * @this {!AnimationTimeline}
Blink Reformat4c46d092018-04-07 15:32:37546 */
547 function applySelectionClass(ui, group) {
548 ui.element.classList.toggle('selected', this._selectedGroup === group);
549 }
550
551 if (this._selectedGroup === group) {
552 this._togglePause(false);
553 this._replay();
554 return;
555 }
556 this._clearTimeline();
557 this._selectedGroup = group;
558 this._previewMap.forEach(applySelectionClass, this);
559 this.setDuration(Math.max(500, group.finiteDuration() + 100));
Tim van der Lippe1d6e57a2019-09-30 11:55:34560 for (const anim of group.animations()) {
Blink Reformat4c46d092018-04-07 15:32:37561 this._addAnimation(anim);
Tim van der Lippe1d6e57a2019-09-30 11:55:34562 }
Blink Reformat4c46d092018-04-07 15:32:37563 this.scheduleRedraw();
564 this._timelineScrubber.classList.remove('hidden');
565 this._togglePause(false);
566 this._replay();
567 }
568
569 /**
Paul Lewis752031f2020-01-09 14:14:23570 * @param {!AnimationImpl} animation
Blink Reformat4c46d092018-04-07 15:32:37571 */
572 _addAnimation(animation) {
573 /**
Tim van der Lippe91d32562020-02-13 15:08:47574 * @param {?SDK.DOMModel.DOMNode} node
Paul Lewis752031f2020-01-09 14:14:23575 * @this {AnimationTimeline}
Blink Reformat4c46d092018-04-07 15:32:37576 */
577 function nodeResolved(node) {
578 nodeUI.nodeResolved(node);
579 uiAnimation.setNode(node);
Tim van der Lippe1d6e57a2019-09-30 11:55:34580 if (node) {
Blink Reformat4c46d092018-04-07 15:32:37581 node[this._symbol] = nodeUI;
Tim van der Lippe1d6e57a2019-09-30 11:55:34582 }
Blink Reformat4c46d092018-04-07 15:32:37583 }
584
585 let nodeUI = this._nodesMap.get(animation.source().backendNodeId());
586 if (!nodeUI) {
Paul Lewis752031f2020-01-09 14:14:23587 nodeUI = new NodeUI(animation.source());
Blink Reformat4c46d092018-04-07 15:32:37588 this._animationsContainer.appendChild(nodeUI.element);
589 this._nodesMap.set(animation.source().backendNodeId(), nodeUI);
590 }
591 const nodeRow = nodeUI.createNewRow();
Paul Lewis752031f2020-01-09 14:14:23592 const uiAnimation = new AnimationUI(animation, this, nodeRow);
Blink Reformat4c46d092018-04-07 15:32:37593 animation.source().deferredNode().resolve(nodeResolved.bind(this));
594 this._uiAnimations.push(uiAnimation);
595 this._animationsMap.set(animation.id(), animation);
596 }
597
598 /**
Tim van der Lippec02a97c2020-02-14 14:39:27599 * @param {!Common.EventTarget.EventTargetEvent} event
Blink Reformat4c46d092018-04-07 15:32:37600 */
601 _nodeRemoved(event) {
602 const node = event.data.node;
Tim van der Lippe1d6e57a2019-09-30 11:55:34603 if (node[this._symbol]) {
Blink Reformat4c46d092018-04-07 15:32:37604 node[this._symbol].nodeRemoved();
Tim van der Lippe1d6e57a2019-09-30 11:55:34605 }
Blink Reformat4c46d092018-04-07 15:32:37606 }
607
608 _renderGrid() {
609 /** @const */ const gridSize = 250;
610 this._grid.setAttribute('width', this.width() + 10);
611 this._grid.setAttribute('height', this._cachedTimelineHeight + 30);
612 this._grid.setAttribute('shape-rendering', 'crispEdges');
613 this._grid.removeChildren();
614 let lastDraw = undefined;
615 for (let time = 0; time < this.duration(); time += gridSize) {
616 const line = this._grid.createSVGChild('rect', 'animation-timeline-grid-line');
617 line.setAttribute('x', time * this.pixelMsRatio() + 10);
618 line.setAttribute('y', 23);
619 line.setAttribute('height', '100%');
620 line.setAttribute('width', 1);
621 }
622 for (let time = 0; time < this.duration(); time += gridSize) {
623 const gridWidth = time * this.pixelMsRatio();
624 if (lastDraw === undefined || gridWidth - lastDraw > 50) {
625 lastDraw = gridWidth;
626 const label = this._grid.createSVGChild('text', 'animation-timeline-grid-label');
627 label.textContent = Number.millisToString(time);
628 label.setAttribute('x', gridWidth + 10);
629 label.setAttribute('y', 16);
630 }
631 }
632 }
633
634 scheduleRedraw() {
635 this._renderQueue = [];
Tim van der Lippe1d6e57a2019-09-30 11:55:34636 for (const ui of this._uiAnimations) {
Blink Reformat4c46d092018-04-07 15:32:37637 this._renderQueue.push(ui);
Tim van der Lippe1d6e57a2019-09-30 11:55:34638 }
639 if (this._redrawing) {
Blink Reformat4c46d092018-04-07 15:32:37640 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34641 }
Blink Reformat4c46d092018-04-07 15:32:37642 this._redrawing = true;
643 this._renderGrid();
644 this._animationsContainer.window().requestAnimationFrame(this._render.bind(this));
645 }
646
647 /**
648 * @param {number=} timestamp
649 */
650 _render(timestamp) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34651 while (this._renderQueue.length && (!timestamp || window.performance.now() - timestamp < 50)) {
Blink Reformat4c46d092018-04-07 15:32:37652 this._renderQueue.shift().redraw();
Tim van der Lippe1d6e57a2019-09-30 11:55:34653 }
654 if (this._renderQueue.length) {
Blink Reformat4c46d092018-04-07 15:32:37655 this._animationsContainer.window().requestAnimationFrame(this._render.bind(this));
Tim van der Lippe1d6e57a2019-09-30 11:55:34656 } else {
Blink Reformat4c46d092018-04-07 15:32:37657 delete this._redrawing;
Tim van der Lippe1d6e57a2019-09-30 11:55:34658 }
Blink Reformat4c46d092018-04-07 15:32:37659 }
660
661 /**
662 * @override
663 */
664 onResize() {
665 this._cachedTimelineWidth = Math.max(0, this._animationsContainer.offsetWidth - this._timelineControlsWidth) || 0;
666 this._cachedTimelineHeight = this._animationsContainer.offsetHeight;
667 this.scheduleRedraw();
Tim van der Lippe1d6e57a2019-09-30 11:55:34668 if (this._scrubberPlayer) {
Blink Reformat4c46d092018-04-07 15:32:37669 this._syncScrubber();
Tim van der Lippe1d6e57a2019-09-30 11:55:34670 }
Blink Reformat4c46d092018-04-07 15:32:37671 delete this._gridOffsetLeft;
672 }
673
674 /**
675 * @return {number}
676 */
677 width() {
678 return this._cachedTimelineWidth || 0;
679 }
680
681 /**
Paul Lewis752031f2020-01-09 14:14:23682 * @param {!AnimationImpl} animation
Blink Reformat4c46d092018-04-07 15:32:37683 * @return {boolean}
684 */
685 _resizeWindow(animation) {
686 let resized = false;
687
688 // This shows at most 3 iterations
689 const duration = animation.source().duration() * Math.min(2, animation.source().iterations());
690 const requiredDuration = animation.source().delay() + duration + animation.source().endDelay();
691 if (requiredDuration > this._duration) {
692 resized = true;
693 this._duration = requiredDuration + 200;
694 }
695 return resized;
696 }
697
698 _syncScrubber() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34699 if (!this._selectedGroup) {
Blink Reformat4c46d092018-04-07 15:32:37700 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34701 }
Blink Reformat4c46d092018-04-07 15:32:37702 this._selectedGroup.currentTimePromise()
703 .then(this._animateTime.bind(this))
704 .then(this._updateControlButton.bind(this));
705 }
706
707 /**
708 * @param {number} currentTime
709 */
710 _animateTime(currentTime) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34711 if (this._scrubberPlayer) {
Blink Reformat4c46d092018-04-07 15:32:37712 this._scrubberPlayer.cancel();
Tim van der Lippe1d6e57a2019-09-30 11:55:34713 }
Blink Reformat4c46d092018-04-07 15:32:37714
715 this._scrubberPlayer = this._timelineScrubber.animate(
716 [{transform: 'translateX(0px)'}, {transform: 'translateX(' + this.width() + 'px)'}],
717 {duration: this.duration(), fill: 'forwards'});
718 this._scrubberPlayer.playbackRate = this._effectivePlaybackRate();
719 this._scrubberPlayer.onfinish = this._updateControlButton.bind(this);
720 this._scrubberPlayer.currentTime = currentTime;
721 this.element.window().requestAnimationFrame(this._updateScrubber.bind(this));
722 }
723
724 /**
725 * @return {number}
726 */
727 pixelMsRatio() {
728 return this.width() / this.duration() || 0;
729 }
730
731 /**
732 * @param {number} timestamp
733 */
734 _updateScrubber(timestamp) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34735 if (!this._scrubberPlayer) {
Blink Reformat4c46d092018-04-07 15:32:37736 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34737 }
Blink Reformat4c46d092018-04-07 15:32:37738 this._currentTime.textContent = Number.millisToString(this._scrubberPlayer.currentTime);
Tim van der Lippe1d6e57a2019-09-30 11:55:34739 if (this._scrubberPlayer.playState === 'pending' || this._scrubberPlayer.playState === 'running') {
Blink Reformat4c46d092018-04-07 15:32:37740 this.element.window().requestAnimationFrame(this._updateScrubber.bind(this));
Tim van der Lippe1d6e57a2019-09-30 11:55:34741 } else if (this._scrubberPlayer.playState === 'finished') {
Blink Reformat4c46d092018-04-07 15:32:37742 this._currentTime.textContent = '';
Tim van der Lippe1d6e57a2019-09-30 11:55:34743 }
Blink Reformat4c46d092018-04-07 15:32:37744 }
745
746 /**
747 * @param {!Event} event
748 * @return {boolean}
749 */
750 _repositionScrubber(event) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34751 if (!this._selectedGroup) {
Blink Reformat4c46d092018-04-07 15:32:37752 return false;
Tim van der Lippe1d6e57a2019-09-30 11:55:34753 }
Blink Reformat4c46d092018-04-07 15:32:37754
755 // Seek to current mouse position.
Tim van der Lippe1d6e57a2019-09-30 11:55:34756 if (!this._gridOffsetLeft) {
Blink Reformat4c46d092018-04-07 15:32:37757 this._gridOffsetLeft = this._grid.totalOffsetLeft() + 10;
Tim van der Lippe1d6e57a2019-09-30 11:55:34758 }
Blink Reformat4c46d092018-04-07 15:32:37759 const seekTime = Math.max(0, event.x - this._gridOffsetLeft) / this.pixelMsRatio();
760 this._selectedGroup.seekTo(seekTime);
761 this._togglePause(true);
762 this._animateTime(seekTime);
763
764 // Interface with scrubber drag.
765 this._originalScrubberTime = seekTime;
766 this._originalMousePosition = event.x;
767 return true;
768 }
769
770 /**
771 * @param {!Event} event
772 * @return {boolean}
773 */
774 _scrubberDragStart(event) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34775 if (!this._scrubberPlayer || !this._selectedGroup) {
Blink Reformat4c46d092018-04-07 15:32:37776 return false;
Tim van der Lippe1d6e57a2019-09-30 11:55:34777 }
Blink Reformat4c46d092018-04-07 15:32:37778
779 this._originalScrubberTime = this._scrubberPlayer.currentTime;
780 this._timelineScrubber.classList.remove('animation-timeline-end');
781 this._scrubberPlayer.pause();
782 this._originalMousePosition = event.x;
783
784 this._togglePause(true);
785 return true;
786 }
787
788 /**
789 * @param {!Event} event
790 */
791 _scrubberDragMove(event) {
792 const delta = event.x - this._originalMousePosition;
793 const currentTime =
794 Math.max(0, Math.min(this._originalScrubberTime + delta / this.pixelMsRatio(), this.duration()));
795 this._scrubberPlayer.currentTime = currentTime;
796 this._currentTime.textContent = Number.millisToString(Math.round(currentTime));
797 this._selectedGroup.seekTo(currentTime);
798 }
799
800 /**
801 * @param {!Event} event
802 */
803 _scrubberDragEnd(event) {
804 const currentTime = Math.max(0, this._scrubberPlayer.currentTime);
805 this._scrubberPlayer.play();
806 this._scrubberPlayer.currentTime = currentTime;
807 this._currentTime.window().requestAnimationFrame(this._updateScrubber.bind(this));
808 }
Paul Lewis4962e2a2019-11-19 14:20:16809}
Blink Reformat4c46d092018-04-07 15:32:37810
Paul Lewis4962e2a2019-11-19 14:20:16811export const GlobalPlaybackRates = [1, 0.25, 0.1];
Blink Reformat4c46d092018-04-07 15:32:37812
813/** @enum {string} */
Paul Lewis4962e2a2019-11-19 14:20:16814export const _ControlState = {
Blink Reformat4c46d092018-04-07 15:32:37815 Play: 'play-outline',
816 Replay: 'replay-outline',
817 Pause: 'pause-outline'
818};
819
820/**
821 * @unrestricted
822 */
Paul Lewis4962e2a2019-11-19 14:20:16823export class NodeUI {
Blink Reformat4c46d092018-04-07 15:32:37824 /**
Paul Lewis752031f2020-01-09 14:14:23825 * @param {!AnimationEffect} animationEffect
Blink Reformat4c46d092018-04-07 15:32:37826 */
827 constructor(animationEffect) {
Tim van der Lippef49e2322020-05-01 15:03:09828 this.element = document.createElement('div');
829 this.element.classList.add('animation-node-row');
Blink Reformat4c46d092018-04-07 15:32:37830 this._description = this.element.createChild('div', 'animation-node-description');
Brandon Goddard99420bb2019-12-17 23:30:29831 this._description.tabIndex = 0;
Blink Reformat4c46d092018-04-07 15:32:37832 this._timelineElement = this.element.createChild('div', 'animation-node-timeline');
Kalon Hinds08dd3682020-01-10 23:51:25833 UI.ARIAUtils.markAsApplication(this._timelineElement);
Blink Reformat4c46d092018-04-07 15:32:37834 }
835
836 /**
Tim van der Lippe91d32562020-02-13 15:08:47837 * @param {?SDK.DOMModel.DOMNode} node
Blink Reformat4c46d092018-04-07 15:32:37838 */
Jeff Fisher3f5f19c2019-08-28 19:10:02839 nodeResolved(node) {
Blink Reformat4c46d092018-04-07 15:32:37840 if (!node) {
Sigurd Schneider23c52972020-10-13 09:31:14841 UI.UIUtils.createTextChild(this._description, '<node>');
Blink Reformat4c46d092018-04-07 15:32:37842 return;
843 }
844 this._node = node;
845 this._nodeChanged();
Tim van der Lippe91d32562020-02-13 15:08:47846 Common.Linkifier.Linkifier.linkify(node, {preventKeyboardFocus: true}).then(link => {
Brandon Goddard99420bb2019-12-17 23:30:29847 this._description.appendChild(link);
848 this._description.addEventListener('keydown', event => {
849 if (isEnterOrSpaceKey(event) && this._node) {
850 Common.Revealer.reveal(node, false);
851 event.consume(true);
852 }
853 });
854 });
Tim van der Lippe1d6e57a2019-09-30 11:55:34855 if (!node.ownerDocument) {
Blink Reformat4c46d092018-04-07 15:32:37856 this.nodeRemoved();
Tim van der Lippe1d6e57a2019-09-30 11:55:34857 }
Blink Reformat4c46d092018-04-07 15:32:37858 }
859
860 /**
861 * @return {!Element}
862 */
863 createNewRow() {
864 return this._timelineElement.createChild('div', 'animation-timeline-row');
865 }
866
867 nodeRemoved() {
868 this.element.classList.add('animation-node-removed');
869 this._node = null;
870 }
871
872 _nodeChanged() {
873 this.element.classList.toggle(
Tim van der Lipped1a00aa2020-08-19 16:03:56874 'animation-node-selected',
875 this._node && this._node === UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode));
Blink Reformat4c46d092018-04-07 15:32:37876 }
Paul Lewis4962e2a2019-11-19 14:20:16877}
Blink Reformat4c46d092018-04-07 15:32:37878
879/**
880 * @unrestricted
881 */
Paul Lewis4962e2a2019-11-19 14:20:16882export class StepTimingFunction {
Blink Reformat4c46d092018-04-07 15:32:37883 /**
884 * @param {number} steps
885 * @param {string} stepAtPosition
886 */
887 constructor(steps, stepAtPosition) {
888 this.steps = steps;
889 this.stepAtPosition = stepAtPosition;
890 }
891
892 /**
893 * @param {string} text
Paul Lewis752031f2020-01-09 14:14:23894 * @return {?StepTimingFunction}
Blink Reformat4c46d092018-04-07 15:32:37895 */
896 static parse(text) {
897 let match = text.match(/^steps\((\d+), (start|middle)\)$/);
Tim van der Lippe1d6e57a2019-09-30 11:55:34898 if (match) {
Paul Lewis752031f2020-01-09 14:14:23899 return new StepTimingFunction(parseInt(match[1], 10), match[2]);
Tim van der Lippe1d6e57a2019-09-30 11:55:34900 }
Blink Reformat4c46d092018-04-07 15:32:37901 match = text.match(/^steps\((\d+)\)$/);
Tim van der Lippe1d6e57a2019-09-30 11:55:34902 if (match) {
Paul Lewis752031f2020-01-09 14:14:23903 return new StepTimingFunction(parseInt(match[1], 10), 'end');
Tim van der Lippe1d6e57a2019-09-30 11:55:34904 }
Blink Reformat4c46d092018-04-07 15:32:37905 return null;
906 }
Paul Lewis4962e2a2019-11-19 14:20:16907}