AnimationModel.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. export default class AnimationModel extends SDK.SDKModel{constructor(target){super(target);this._runtimeModel=(target.model(SDK.RuntimeModel));this._agent=target.animationAgent();target.registerAnimationDispatcher(new Animation.AnimationDispatcher(this));this._animationsById=new Map();this._animationGroups=new Map();this._pendingAnimations=[];this._playbackRate=1;const resourceTreeModel=(target.model(SDK.ResourceTreeModel));resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.MainFrameNavigated,this._reset,this);const screenCaptureModel=target.model(SDK.ScreenCaptureModel);if(screenCaptureModel){this._screenshotCapture=new ScreenshotCapture(this,screenCaptureModel);}}
  2. _reset(){this._animationsById.clear();this._animationGroups.clear();this._pendingAnimations=[];this.dispatchEventToListeners(Events.ModelReset);}
  3. animationCreated(id){this._pendingAnimations.push(id);}
  4. _animationCanceled(id){this._pendingAnimations.remove(id);this._flushPendingAnimationsIfNeeded();}
  5. animationStarted(payload){if(!payload.source||!payload.source.backendNodeId){return;}
  6. const animation=AnimationImpl.parsePayload(this,payload);if(animation.type()==='WebAnimation'&&animation.source().keyframesRule().keyframes().length===0){this._pendingAnimations.remove(animation.id());}else{this._animationsById.set(animation.id(),animation);if(this._pendingAnimations.indexOf(animation.id())===-1){this._pendingAnimations.push(animation.id());}}
  7. this._flushPendingAnimationsIfNeeded();}
  8. _flushPendingAnimationsIfNeeded(){for(const id of this._pendingAnimations){if(!this._animationsById.get(id)){return;}}
  9. while(this._pendingAnimations.length){this._matchExistingGroups(this._createGroupFromPendingAnimations());}}
  10. _matchExistingGroups(incomingGroup){let matchedGroup=null;for(const group of this._animationGroups.values()){if(group._matches(incomingGroup)){matchedGroup=group;group._update(incomingGroup);break;}}
  11. if(!matchedGroup){this._animationGroups.set(incomingGroup.id(),incomingGroup);if(this._screenshotCapture){this._screenshotCapture.captureScreenshots(incomingGroup.finiteDuration(),incomingGroup._screenshots);}}
  12. this.dispatchEventToListeners(Events.AnimationGroupStarted,matchedGroup||incomingGroup);return!!matchedGroup;}
  13. _createGroupFromPendingAnimations(){console.assert(this._pendingAnimations.length);const groupedAnimations=[this._animationsById.get(this._pendingAnimations.shift())];const remainingAnimations=[];for(const id of this._pendingAnimations){const anim=this._animationsById.get(id);if(anim.startTime()===groupedAnimations[0].startTime()){groupedAnimations.push(anim);}else{remainingAnimations.push(id);}}
  14. this._pendingAnimations=remainingAnimations;return new AnimationGroup(this,groupedAnimations[0].id(),groupedAnimations);}
  15. setPlaybackRate(playbackRate){this._playbackRate=playbackRate;this._agent.setPlaybackRate(playbackRate);}
  16. _releaseAnimations(animations){this._agent.releaseAnimations(animations);}
  17. suspendModel(){this._reset();return this._agent.disable();}
  18. resumeModel(){if(!this._enabled){return Promise.resolve();}
  19. return this._agent.enable();}
  20. ensureEnabled(){if(this._enabled){return;}
  21. this._agent.enable();this._enabled=true;}}
  22. export const Events={AnimationGroupStarted:Symbol('AnimationGroupStarted'),ModelReset:Symbol('ModelReset')};export class AnimationImpl{constructor(animationModel,payload){this._animationModel=animationModel;this._payload=payload;this._source=new AnimationEffect(animationModel,(this._payload.source));}
  23. static parsePayload(animationModel,payload){return new AnimationImpl(animationModel,payload);}
  24. payload(){return this._payload;}
  25. id(){return this._payload.id;}
  26. name(){return this._payload.name;}
  27. paused(){return this._payload.pausedState;}
  28. playState(){return this._playState||this._payload.playState;}
  29. setPlayState(playState){this._playState=playState;}
  30. playbackRate(){return this._payload.playbackRate;}
  31. startTime(){return this._payload.startTime;}
  32. endTime(){if(!this.source().iterations){return Infinity;}
  33. return this.startTime()+this.source().delay()+this.source().duration()*this.source().iterations()+
  34. this.source().endDelay();}
  35. _finiteDuration(){const iterations=Math.min(this.source().iterations(),3);return this.source().delay()+this.source().duration()*iterations;}
  36. currentTime(){return this._payload.currentTime;}
  37. source(){return this._source;}
  38. type(){return(this._payload.type);}
  39. overlaps(animation){if(!this.source().iterations()||!animation.source().iterations()){return true;}
  40. const firstAnimation=this.startTime()<animation.startTime()?this:animation;const secondAnimation=firstAnimation===this?animation:this;return firstAnimation.endTime()>=secondAnimation.startTime();}
  41. setTiming(duration,delay){this._source.node().then(this._updateNodeStyle.bind(this,duration,delay));this._source._duration=duration;this._source._delay=delay;this._animationModel._agent.setTiming(this.id(),duration,delay);}
  42. _updateNodeStyle(duration,delay,node){let animationPrefix;if(this.type()===Type.CSSTransition){animationPrefix='transition-';}else if(this.type()===Type.CSSAnimation){animationPrefix='animation-';}else{return;}
  43. const cssModel=node.domModel().cssModel();cssModel.setEffectivePropertyValueForNode(node.id,animationPrefix+'duration',duration+'ms');cssModel.setEffectivePropertyValueForNode(node.id,animationPrefix+'delay',delay+'ms');}
  44. remoteObjectPromise(){return this._animationModel._agent.resolveAnimation(this.id()).then(payload=>payload&&this._animationModel._runtimeModel.createRemoteObject(payload));}
  45. _cssId(){return this._payload.cssId||'';}}
  46. export const Type={CSSTransition:'CSSTransition',CSSAnimation:'CSSAnimation',WebAnimation:'WebAnimation'};export class AnimationEffect{constructor(animationModel,payload){this._animationModel=animationModel;this._payload=payload;if(payload.keyframesRule){this._keyframesRule=new KeyframesRule(payload.keyframesRule);}
  47. this._delay=this._payload.delay;this._duration=this._payload.duration;}
  48. delay(){return this._delay;}
  49. endDelay(){return this._payload.endDelay;}
  50. iterationStart(){return this._payload.iterationStart;}
  51. iterations(){if(!this.delay()&&!this.endDelay()&&!this.duration()){return 0;}
  52. return this._payload.iterations||Infinity;}
  53. duration(){return this._duration;}
  54. direction(){return this._payload.direction;}
  55. fill(){return this._payload.fill;}
  56. node(){if(!this._deferredNode){this._deferredNode=new SDK.DeferredDOMNode(this._animationModel.target(),this.backendNodeId());}
  57. return this._deferredNode.resolvePromise();}
  58. deferredNode(){return new SDK.DeferredDOMNode(this._animationModel.target(),this.backendNodeId());}
  59. backendNodeId(){return(this._payload.backendNodeId);}
  60. keyframesRule(){return this._keyframesRule;}
  61. easing(){return this._payload.easing;}}
  62. export class KeyframesRule{constructor(payload){this._payload=payload;this._keyframes=this._payload.keyframes.map(function(keyframeStyle){return new KeyframeStyle(keyframeStyle);});}
  63. _setKeyframesPayload(payload){this._keyframes=payload.map(function(keyframeStyle){return new KeyframeStyle(keyframeStyle);});}
  64. name(){return this._payload.name;}
  65. keyframes(){return this._keyframes;}}
  66. export class KeyframeStyle{constructor(payload){this._payload=payload;this._offset=this._payload.offset;}
  67. offset(){return this._offset;}
  68. setOffset(offset){this._offset=offset*100+'%';}
  69. offsetAsNumber(){return parseFloat(this._offset)/100;}
  70. easing(){return this._payload.easing;}}
  71. export class AnimationGroup{constructor(animationModel,id,animations){this._animationModel=animationModel;this._id=id;this._animations=animations;this._paused=false;this._screenshots=[];this._screenshotImages=[];}
  72. id(){return this._id;}
  73. animations(){return this._animations;}
  74. release(){this._animationModel._animationGroups.remove(this.id());this._animationModel._releaseAnimations(this._animationIds());}
  75. _animationIds(){function extractId(animation){return animation.id();}
  76. return this._animations.map(extractId);}
  77. startTime(){return this._animations[0].startTime();}
  78. finiteDuration(){let maxDuration=0;for(let i=0;i<this._animations.length;++i){maxDuration=Math.max(maxDuration,this._animations[i]._finiteDuration());}
  79. return maxDuration;}
  80. seekTo(currentTime){this._animationModel._agent.seekAnimations(this._animationIds(),currentTime);}
  81. paused(){return this._paused;}
  82. togglePause(paused){if(paused===this._paused){return;}
  83. this._paused=paused;this._animationModel._agent.setPaused(this._animationIds(),paused);}
  84. currentTimePromise(){let longestAnim=null;for(const anim of this._animations){if(!longestAnim||anim.endTime()>longestAnim.endTime()){longestAnim=anim;}}
  85. return this._animationModel._agent.getCurrentTime(longestAnim.id()).then(currentTime=>currentTime||0);}
  86. _matches(group){function extractId(anim){if(anim.type()===Type.WebAnimation){return anim.type()+anim.id();}else{return anim._cssId();}}
  87. if(this._animations.length!==group._animations.length){return false;}
  88. const left=this._animations.map(extractId).sort();const right=group._animations.map(extractId).sort();for(let i=0;i<left.length;i++){if(left[i]!==right[i]){return false;}}
  89. return true;}
  90. _update(group){this._animationModel._releaseAnimations(this._animationIds());this._animations=group._animations;}
  91. screenshots(){for(let i=0;i<this._screenshots.length;++i){const image=new Image();image.src='data:image/jpeg;base64,'+this._screenshots[i];this._screenshotImages.push(image);}
  92. this._screenshots=[];return this._screenshotImages;}}
  93. export class AnimationDispatcher{constructor(animationModel){this._animationModel=animationModel;}
  94. animationCreated(id){this._animationModel.animationCreated(id);}
  95. animationCanceled(id){this._animationModel._animationCanceled(id);}
  96. animationStarted(payload){this._animationModel.animationStarted(payload);}}
  97. export class ScreenshotCapture{constructor(animationModel,screenCaptureModel){this._requests=[];this._screenCaptureModel=screenCaptureModel;this._animationModel=animationModel;this._animationModel.addEventListener(Events.ModelReset,this._stopScreencast,this);}
  98. captureScreenshots(duration,screenshots){const screencastDuration=Math.min(duration/this._animationModel._playbackRate,3000);const endTime=screencastDuration+window.performance.now();this._requests.push({endTime:endTime,screenshots:screenshots});if(!this._endTime||endTime>this._endTime){clearTimeout(this._stopTimer);this._stopTimer=setTimeout(this._stopScreencast.bind(this),screencastDuration);this._endTime=endTime;}
  99. if(this._capturing){return;}
  100. this._capturing=true;this._screenCaptureModel.startScreencast('jpeg',80,undefined,300,2,this._screencastFrame.bind(this),visible=>{});}
  101. _screencastFrame(base64Data,metadata){function isAnimating(request){return request.endTime>=now;}
  102. if(!this._capturing){return;}
  103. const now=window.performance.now();this._requests=this._requests.filter(isAnimating);for(const request of this._requests){request.screenshots.push(base64Data);}}
  104. _stopScreencast(){if(!this._capturing){return;}
  105. delete this._stopTimer;delete this._endTime;this._requests=[];this._capturing=false;this._screenCaptureModel.stopScreencast();}}
  106. SDK.SDKModel.register(AnimationModel,SDK.Target.Capability.DOM,false);self.Animation=self.Animation||{};Animation=Animation||{};Animation.AnimationModel=AnimationModel;Animation.AnimationModel.Events=Events;Animation.AnimationModel.Animation=AnimationImpl;Animation.AnimationModel.Animation.Type=Type;Animation.AnimationModel.AnimationEffect=AnimationEffect;Animation.AnimationModel.KeyframesRule=KeyframesRule;Animation.AnimationModel.KeyframeStyle=KeyframeStyle;Animation.AnimationModel.AnimationGroup=AnimationGroup;Animation.AnimationModel.ScreenshotCapture=ScreenshotCapture;Animation.AnimationModel.ScreenshotCapture.Request;Animation.AnimationDispatcher=AnimationDispatcher;