AnimationTimeline.js 18 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. export default class AnimationTimeline extends UI.VBox{constructor(){super(true);this.registerRequiredCSS('animation/animationTimeline.css');this.element.classList.add('animations-timeline');this._grid=this.contentElement.createSVGChild('svg','animation-timeline-grid');this._playbackRate=1;this._allPaused=false;this._createHeader();this._animationsContainer=this.contentElement.createChild('div','animation-timeline-rows');const timelineHint=this.contentElement.createChild('div','animation-timeline-rows-hint');timelineHint.textContent=ls`Select an effect above to inspect and modify.`;this._defaultDuration=100;this._duration=this._defaultDuration;this._timelineControlsWidth=150;this._nodesMap=new Map();this._uiAnimations=[];this._groupBuffer=[];this._previewMap=new Map();this._symbol=Symbol('animationTimeline');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);}
  2. wasShown(){for(const animationModel of SDK.targetManager.models(Animation.AnimationModel)){this._addEventListeners(animationModel);}}
  3. willHide(){for(const animationModel of SDK.targetManager.models(Animation.AnimationModel)){this._removeEventListeners(animationModel);}
  4. this._popoverHelper.hidePopover();}
  5. modelAdded(animationModel){if(this.isShowing()){this._addEventListeners(animationModel);}}
  6. modelRemoved(animationModel){this._removeEventListeners(animationModel);}
  7. _addEventListeners(animationModel){animationModel.ensureEnabled();animationModel.addEventListener(Animation.AnimationModel.Events.AnimationGroupStarted,this._animationGroupStarted,this);animationModel.addEventListener(Animation.AnimationModel.Events.ModelReset,this._reset,this);}
  8. _removeEventListeners(animationModel){animationModel.removeEventListener(Animation.AnimationModel.Events.AnimationGroupStarted,this._animationGroupStarted,this);animationModel.removeEventListener(Animation.AnimationModel.Events.ModelReset,this._reset,this);}
  9. _nodeChanged(){for(const nodeUI of this._nodesMap.values()){nodeUI._nodeChanged();}}
  10. _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;}
  11. _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);}
  12. 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;}
  13. _getPopoverRequest(event){const element=event.target;if(!element.isDescendant(this._previewContainer)){return null;}
  14. 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;}}
  15. console.assert(animGroup);const screenshots=animGroup.screenshots();if(!screenshots.length){return Promise.resolve(false);}
  16. let fulfill;const promise=new Promise(x=>fulfill=x);if(!screenshots[0].complete){screenshots[0].onload=onFirstScreenshotLoaded.bind(null,screenshots);}else{onFirstScreenshotLoaded(screenshots);}
  17. return promise;function onFirstScreenshotLoaded(screenshots){new Animation.AnimationScreenshotPopover(screenshots).show(popover.contentElement);fulfill(true);}}};}
  18. _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`);}
  19. _setPlaybackRate(playbackRate){this._playbackRate=playbackRate;for(const animationModel of SDK.targetManager.models(Animation.AnimationModel)){animationModel.setPlaybackRate(this._allPaused?0:this._playbackRate);}
  20. Host.userMetrics.actionTaken(Host.UserMetrics.Action.AnimationsPlaybackRateChanged);if(this._scrubberPlayer){this._scrubberPlayer.playbackRate=this._effectivePlaybackRate();}
  21. this._updatePlaybackControls();}
  22. _updatePlaybackControls(){for(const button of this._playbackRateButtons){const selected=this._playbackRate===button.playbackRate;button.classList.toggle('selected',selected);}}
  23. _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);}}
  24. _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');}}
  25. _effectivePlaybackRate(){return(this._allPaused||(this._selectedGroup&&this._selectedGroup.paused()))?0:this._playbackRate;}
  26. _togglePause(pause){this._selectedGroup.togglePause(pause);if(this._scrubberPlayer){this._scrubberPlayer.playbackRate=this._effectivePlaybackRate();}
  27. this._previewMap.get(this._selectedGroup).element.classList.toggle('paused',pause);this._updateControlButton();}
  28. _replay(){if(!this._selectedGroup){return;}
  29. this._selectedGroup.seekTo(0);this._animateTime(0);this._updateControlButton();}
  30. duration(){return this._duration;}
  31. setDuration(duration){this._duration=duration;this.scheduleRedraw();}
  32. _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();}
  33. delete this._scrubberPlayer;this._currentTime.textContent='';this._updateControlButton();}
  34. _reset(){this._clearTimeline();if(this._allPaused){this._togglePauseAll();}else{this._setPlaybackRate(this._playbackRate);}
  35. for(const group of this._groupBuffer){group.release();}
  36. this._groupBuffer=[];this._previewMap.clear();this._previewContainer.removeChildren();this._popoverHelper.hidePopover();this._renderGrid();}
  37. _animationGroupStarted(event){this._addAnimationGroup((event.data));}
  38. _addAnimationGroup(group){function startTimeComparator(left,right){return left.startTime()>right.startTime();}
  39. if(this._previewMap.get(group)){if(this._selectedGroup===group){this._syncScrubber();}else{this._previewMap.get(group).replay();}
  40. return;}
  41. this._groupBuffer.sort(startTimeComparator);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]);}
  42. for(const g of groupsToDiscard){this._previewMap.get(g).element.remove();this._previewMap.delete(g);g.release();}
  43. 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));}
  44. _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();}}
  45. _selectAnimationGroup(group){function applySelectionClass(ui,group){ui.element.classList.toggle('selected',this._selectedGroup===group);}
  46. if(this._selectedGroup===group){this._togglePause(false);this._replay();return;}
  47. 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);}
  48. this.scheduleRedraw();this._timelineScrubber.classList.remove('hidden');this._togglePause(false);this._replay();}
  49. _addAnimation(animation){function nodeResolved(node){nodeUI.nodeResolved(node);uiAnimation.setNode(node);if(node){node[this._symbol]=nodeUI;}}
  50. 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);}
  51. 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);}
  52. _nodeRemoved(event){const node=event.data.node;if(node[this._symbol]){node[this._symbol].nodeRemoved();}}
  53. _renderGrid(){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);}
  54. 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);}}}
  55. scheduleRedraw(){this._renderQueue=[];for(const ui of this._uiAnimations){this._renderQueue.push(ui);}
  56. if(this._redrawing){return;}
  57. this._redrawing=true;this._renderGrid();this._animationsContainer.window().requestAnimationFrame(this._render.bind(this));}
  58. _render(timestamp){while(this._renderQueue.length&&(!timestamp||window.performance.now()-timestamp<50)){this._renderQueue.shift().redraw();}
  59. if(this._renderQueue.length){this._animationsContainer.window().requestAnimationFrame(this._render.bind(this));}else{delete this._redrawing;}}
  60. onResize(){this._cachedTimelineWidth=Math.max(0,this._animationsContainer.offsetWidth-this._timelineControlsWidth)||0;this._cachedTimelineHeight=this._animationsContainer.offsetHeight;this.scheduleRedraw();if(this._scrubberPlayer){this._syncScrubber();}
  61. delete this._gridOffsetLeft;}
  62. width(){return this._cachedTimelineWidth||0;}
  63. _resizeWindow(animation){let resized=false;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;}
  64. return resized;}
  65. _syncScrubber(){if(!this._selectedGroup){return;}
  66. this._selectedGroup.currentTimePromise().then(this._animateTime.bind(this)).then(this._updateControlButton.bind(this));}
  67. _animateTime(currentTime){if(this._scrubberPlayer){this._scrubberPlayer.cancel();}
  68. 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));}
  69. pixelMsRatio(){return this.width()/this.duration()||0;}
  70. _updateScrubber(timestamp){if(!this._scrubberPlayer){return;}
  71. 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='';}}
  72. _repositionScrubber(event){if(!this._selectedGroup){return false;}
  73. if(!this._gridOffsetLeft){this._gridOffsetLeft=this._grid.totalOffsetLeft()+10;}
  74. const seekTime=Math.max(0,event.x-this._gridOffsetLeft)/this.pixelMsRatio();this._selectedGroup.seekTo(seekTime);this._togglePause(true);this._animateTime(seekTime);this._originalScrubberTime=seekTime;this._originalMousePosition=event.x;return true;}
  75. _scrubberDragStart(event){if(!this._scrubberPlayer||!this._selectedGroup){return false;}
  76. this._originalScrubberTime=this._scrubberPlayer.currentTime;this._timelineScrubber.classList.remove('animation-timeline-end');this._scrubberPlayer.pause();this._originalMousePosition=event.x;this._togglePause(true);return true;}
  77. _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);}
  78. _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));}}
  79. export const GlobalPlaybackRates=[1,0.25,0.1];export const _ControlState={Play:'play-outline',Replay:'replay-outline',Pause:'pause-outline'};export class NodeUI{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');}
  80. nodeResolved(node){if(!node){this._description.createTextChild('<node>');return;}
  81. this._node=node;this._nodeChanged();Common.Linkifier.linkify(node).then(link=>this._description.appendChild(link));if(!node.ownerDocument){this.nodeRemoved();}}
  82. createNewRow(){return this._timelineElement.createChild('div','animation-timeline-row');}
  83. nodeRemoved(){this.element.classList.add('animation-node-removed');this._node=null;}
  84. _nodeChanged(){this.element.classList.toggle('animation-node-selected',this._node&&this._node===UI.context.flavor(SDK.DOMNode));}}
  85. export class StepTimingFunction{constructor(steps,stepAtPosition){this.steps=steps;this.stepAtPosition=stepAtPosition;}
  86. static parse(text){let match=text.match(/^steps\((\d+), (start|middle)\)$/);if(match){return new Animation.AnimationTimeline.StepTimingFunction(parseInt(match[1],10),match[2]);}
  87. match=text.match(/^steps\((\d+)\)$/);if(match){return new Animation.AnimationTimeline.StepTimingFunction(parseInt(match[1],10),'end');}
  88. return null;}}
  89. self.Animation=self.Animation||{};Animation=Animation||{};Animation.AnimationTimeline=AnimationTimeline;Animation.AnimationTimeline.GlobalPlaybackRates=GlobalPlaybackRates;Animation.AnimationTimeline._ControlState=_ControlState;Animation.AnimationTimeline.NodeUI=NodeUI;Animation.AnimationTimeline.StepTimingFunction=StepTimingFunction;