AnimationUI.js 12 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
  1. export default class AnimationUI{constructor(animation,timeline,parentElement){this._animation=animation;this._timeline=timeline;this._parentElement=parentElement;if(this._animation.source().keyframesRule()){this._keyframes=this._animation.source().keyframesRule().keyframes();}
  2. this._nameElement=parentElement.createChild('div','animation-name');this._nameElement.textContent=this._animation.name();this._svg=parentElement.createSVGChild('svg','animation-ui');this._svg.setAttribute('height',Animation.AnimationUI.Options.AnimationSVGHeight);this._svg.style.marginLeft='-'+Animation.AnimationUI.Options.AnimationMargin+'px';this._svg.addEventListener('contextmenu',this._onContextMenu.bind(this));this._activeIntervalGroup=this._svg.createSVGChild('g');UI.installDragHandle(this._activeIntervalGroup,this._mouseDown.bind(this,Animation.AnimationUI.MouseEvents.AnimationDrag,null),this._mouseMove.bind(this),this._mouseUp.bind(this),'-webkit-grabbing','-webkit-grab');this._cachedElements=[];this._movementInMs=0;this._color=Animation.AnimationUI.Color(this._animation);}
  3. static Color(animation){const names=Object.keys(Animation.AnimationUI.Colors);const color=Animation.AnimationUI.Colors[names[String.hashCode(animation.name()||animation.id())%names.length]];return color.asString(Common.Color.Format.RGB);}
  4. animation(){return this._animation;}
  5. setNode(node){this._node=node;}
  6. _createLine(parentElement,className){const line=parentElement.createSVGChild('line',className);line.setAttribute('x1',Animation.AnimationUI.Options.AnimationMargin);line.setAttribute('y1',Animation.AnimationUI.Options.AnimationHeight);line.setAttribute('y2',Animation.AnimationUI.Options.AnimationHeight);line.style.stroke=this._color;return line;}
  7. _drawAnimationLine(iteration,parentElement){const cache=this._cachedElements[iteration];if(!cache.animationLine){cache.animationLine=this._createLine(parentElement,'animation-line');}
  8. cache.animationLine.setAttribute('x2',(this._duration()*this._timeline.pixelMsRatio()+Animation.AnimationUI.Options.AnimationMargin).toFixed(2));}
  9. _drawDelayLine(parentElement){if(!this._delayLine){this._delayLine=this._createLine(parentElement,'animation-delay-line');this._endDelayLine=this._createLine(parentElement,'animation-delay-line');}
  10. const fill=this._animation.source().fill();this._delayLine.classList.toggle('animation-fill',fill==='backwards'||fill==='both');const margin=Animation.AnimationUI.Options.AnimationMargin;this._delayLine.setAttribute('x1',margin);this._delayLine.setAttribute('x2',(this._delay()*this._timeline.pixelMsRatio()+margin).toFixed(2));const forwardsFill=fill==='forwards'||fill==='both';this._endDelayLine.classList.toggle('animation-fill',forwardsFill);const leftMargin=Math.min(this._timeline.width(),(this._delay()+this._duration()*this._animation.source().iterations())*this._timeline.pixelMsRatio());this._endDelayLine.style.transform='translateX('+leftMargin.toFixed(2)+'px)';this._endDelayLine.setAttribute('x1',margin);this._endDelayLine.setAttribute('x2',forwardsFill?(this._timeline.width()-leftMargin+margin).toFixed(2):(this._animation.source().endDelay()*this._timeline.pixelMsRatio()+margin).toFixed(2));}
  11. _drawPoint(iteration,parentElement,x,keyframeIndex,attachEvents){if(this._cachedElements[iteration].keyframePoints[keyframeIndex]){this._cachedElements[iteration].keyframePoints[keyframeIndex].setAttribute('cx',x.toFixed(2));return;}
  12. const circle=parentElement.createSVGChild('circle',keyframeIndex<=0?'animation-endpoint':'animation-keyframe-point');circle.setAttribute('cx',x.toFixed(2));circle.setAttribute('cy',Animation.AnimationUI.Options.AnimationHeight);circle.style.stroke=this._color;circle.setAttribute('r',Animation.AnimationUI.Options.AnimationMargin/2);if(keyframeIndex<=0){circle.style.fill=this._color;}
  13. this._cachedElements[iteration].keyframePoints[keyframeIndex]=circle;if(!attachEvents){return;}
  14. let eventType;if(keyframeIndex===0){eventType=Animation.AnimationUI.MouseEvents.StartEndpointMove;}else if(keyframeIndex===-1){eventType=Animation.AnimationUI.MouseEvents.FinishEndpointMove;}else{eventType=Animation.AnimationUI.MouseEvents.KeyframeMove;}
  15. UI.installDragHandle(circle,this._mouseDown.bind(this,eventType,keyframeIndex),this._mouseMove.bind(this),this._mouseUp.bind(this),'ew-resize');}
  16. _renderKeyframe(iteration,keyframeIndex,parentElement,leftDistance,width,easing){function createStepLine(parentElement,x,strokeColor){const line=parentElement.createSVGChild('line');line.setAttribute('x1',x);line.setAttribute('x2',x);line.setAttribute('y1',Animation.AnimationUI.Options.AnimationMargin);line.setAttribute('y2',Animation.AnimationUI.Options.AnimationHeight);line.style.stroke=strokeColor;}
  17. const bezier=UI.Geometry.CubicBezier.parse(easing);const cache=this._cachedElements[iteration].keyframeRender;if(!cache[keyframeIndex]){cache[keyframeIndex]=bezier?parentElement.createSVGChild('path','animation-keyframe'):parentElement.createSVGChild('g','animation-keyframe-step');}
  18. const group=cache[keyframeIndex];group.style.transform='translateX('+leftDistance.toFixed(2)+'px)';if(easing==='linear'){group.style.fill=this._color;const height=InlineEditor.BezierUI.Height;group.setAttribute('d',['M',0,height,'L',0,5,'L',width.toFixed(2),5,'L',width.toFixed(2),height,'Z'].join(' '));}else if(bezier){group.style.fill=this._color;InlineEditor.BezierUI.drawVelocityChart(bezier,group,width);}else{const stepFunction=Animation.AnimationTimeline.StepTimingFunction.parse(easing);group.removeChildren();const offsetMap={'start':0,'middle':0.5,'end':1};const offsetWeight=offsetMap[stepFunction.stepAtPosition];for(let i=0;i<stepFunction.steps;i++){createStepLine(group,(i+offsetWeight)*width/stepFunction.steps,this._color);}}}
  19. redraw(){const maxWidth=this._timeline.width()-Animation.AnimationUI.Options.AnimationMargin;this._svg.setAttribute('width',(maxWidth+2*Animation.AnimationUI.Options.AnimationMargin).toFixed(2));this._activeIntervalGroup.style.transform='translateX('+(this._delay()*this._timeline.pixelMsRatio()).toFixed(2)+'px)';this._nameElement.style.transform='translateX('+
  20. (this._delay()*this._timeline.pixelMsRatio()+Animation.AnimationUI.Options.AnimationMargin).toFixed(2)+'px)';this._nameElement.style.width=(this._duration()*this._timeline.pixelMsRatio()).toFixed(2)+'px';this._drawDelayLine(this._svg);if(this._animation.type()==='CSSTransition'){this._renderTransition();return;}
  21. this._renderIteration(this._activeIntervalGroup,0);if(!this._tailGroup){this._tailGroup=this._activeIntervalGroup.createSVGChild('g','animation-tail-iterations');}
  22. const iterationWidth=this._duration()*this._timeline.pixelMsRatio();let iteration;for(iteration=1;iteration<this._animation.source().iterations()&&iterationWidth*(iteration-1)<this._timeline.width();iteration++){this._renderIteration(this._tailGroup,iteration);}
  23. while(iteration<this._cachedElements.length){this._cachedElements.pop().group.remove();}}
  24. _renderTransition(){if(!this._cachedElements[0]){this._cachedElements[0]={animationLine:null,keyframePoints:{},keyframeRender:{},group:null};}
  25. this._drawAnimationLine(0,this._activeIntervalGroup);this._renderKeyframe(0,0,this._activeIntervalGroup,Animation.AnimationUI.Options.AnimationMargin,this._duration()*this._timeline.pixelMsRatio(),this._animation.source().easing());this._drawPoint(0,this._activeIntervalGroup,Animation.AnimationUI.Options.AnimationMargin,0,true);this._drawPoint(0,this._activeIntervalGroup,this._duration()*this._timeline.pixelMsRatio()+Animation.AnimationUI.Options.AnimationMargin,-1,true);}
  26. _renderIteration(parentElement,iteration){if(!this._cachedElements[iteration]){this._cachedElements[iteration]={animationLine:null,keyframePoints:{},keyframeRender:{},group:parentElement.createSVGChild('g')};}
  27. const group=this._cachedElements[iteration].group;group.style.transform='translateX('+(iteration*this._duration()*this._timeline.pixelMsRatio()).toFixed(2)+'px)';this._drawAnimationLine(iteration,group);console.assert(this._keyframes.length>1);for(let i=0;i<this._keyframes.length-1;i++){const leftDistance=this._offset(i)*this._duration()*this._timeline.pixelMsRatio()+
  28. Animation.AnimationUI.Options.AnimationMargin;const width=this._duration()*(this._offset(i+1)-this._offset(i))*this._timeline.pixelMsRatio();this._renderKeyframe(iteration,i,group,leftDistance,width,this._keyframes[i].easing());if(i||(!i&&iteration===0)){this._drawPoint(iteration,group,leftDistance,i,iteration===0);}}
  29. this._drawPoint(iteration,group,this._duration()*this._timeline.pixelMsRatio()+Animation.AnimationUI.Options.AnimationMargin,-1,iteration===0);}
  30. _delay(){let delay=this._animation.source().delay();if(this._mouseEventType===Animation.AnimationUI.MouseEvents.AnimationDrag||this._mouseEventType===Animation.AnimationUI.MouseEvents.StartEndpointMove){delay+=this._movementInMs;}
  31. return Math.max(0,delay);}
  32. _duration(){let duration=this._animation.source().duration();if(this._mouseEventType===Animation.AnimationUI.MouseEvents.FinishEndpointMove){duration+=this._movementInMs;}else if(this._mouseEventType===Animation.AnimationUI.MouseEvents.StartEndpointMove){duration-=Math.max(this._movementInMs,-this._animation.source().delay());}
  33. return Math.max(0,duration);}
  34. _offset(i){let offset=this._keyframes[i].offsetAsNumber();if(this._mouseEventType===Animation.AnimationUI.MouseEvents.KeyframeMove&&i===this._keyframeMoved){console.assert(i>0&&i<this._keyframes.length-1,'First and last keyframe cannot be moved');offset+=this._movementInMs/this._animation.source().duration();offset=Math.max(offset,this._keyframes[i-1].offsetAsNumber());offset=Math.min(offset,this._keyframes[i+1].offsetAsNumber());}
  35. return offset;}
  36. _mouseDown(mouseEventType,keyframeIndex,event){if(event.buttons===2){return false;}
  37. if(this._svg.enclosingNodeOrSelfWithClass('animation-node-removed')){return false;}
  38. this._mouseEventType=mouseEventType;this._keyframeMoved=keyframeIndex;this._downMouseX=event.clientX;event.consume(true);if(this._node){Common.Revealer.reveal(this._node);}
  39. return true;}
  40. _mouseMove(event){this._movementInMs=(event.clientX-this._downMouseX)/this._timeline.pixelMsRatio();if(this._delay()+this._duration()>this._timeline.duration()*0.8){this._timeline.setDuration(this._timeline.duration()*1.2);}
  41. this.redraw();}
  42. _mouseUp(event){this._movementInMs=(event.clientX-this._downMouseX)/this._timeline.pixelMsRatio();if(this._mouseEventType===Animation.AnimationUI.MouseEvents.KeyframeMove){this._keyframes[this._keyframeMoved].setOffset(this._offset(this._keyframeMoved));}else{this._animation.setTiming(this._duration(),this._delay());}
  43. this._movementInMs=0;this.redraw();delete this._mouseEventType;delete this._downMouseX;delete this._keyframeMoved;}
  44. _onContextMenu(event){function showContextMenu(remoteObject){if(!remoteObject){return;}
  45. const contextMenu=new UI.ContextMenu(event);contextMenu.appendApplicableItems(remoteObject);contextMenu.show();}
  46. this._animation.remoteObjectPromise().then(showContextMenu);event.consume(true);}}
  47. export const MouseEvents={AnimationDrag:'AnimationDrag',KeyframeMove:'KeyframeMove',StartEndpointMove:'StartEndpointMove',FinishEndpointMove:'FinishEndpointMove'};export const Options={AnimationHeight:26,AnimationSVGHeight:50,AnimationMargin:7,EndpointsClickRegionSize:10,GridCanvasHeight:40};export const Colors={'Purple':Common.Color.parse('#9C27B0'),'Light Blue':Common.Color.parse('#03A9F4'),'Deep Orange':Common.Color.parse('#FF5722'),'Blue':Common.Color.parse('#5677FC'),'Lime':Common.Color.parse('#CDDC39'),'Blue Grey':Common.Color.parse('#607D8B'),'Pink':Common.Color.parse('#E91E63'),'Green':Common.Color.parse('#0F9D58'),'Brown':Common.Color.parse('#795548'),'Cyan':Common.Color.parse('#00BCD4')};self.Animation=self.Animation||{};Animation=Animation||{};Animation.AnimationUI=AnimationUI;Animation.AnimationUI.MouseEvents=MouseEvents;Animation.AnimationUI.Options=Options;Animation.AnimationUI.Colors=Colors;