import { Initializable } from '@pilotlab/lux-initializable'; import is from '@pilotlab/lux-is'; import Result from '@pilotlab/lux-result'; import { Signal} from '@pilotlab/lux-signals'; import { Identifier } from '@pilotlab/lux-ids'; import { IPromise } from '@pilotlab/lux-result'; import IAnimation from './interfaces/iAnimation'; import { MapList } from '@pilotlab/lux-collections'; import { HiResTimer, HiResTimerCounterBrowser } from '@pilotlab/lux-timer'; import { SpeedType } from './animationEnums'; import IAnimationEventArgs from './interfaces/iAnimationEventArgs'; import IAnimationValue from './interfaces/iAnimationValue'; import IAnimationEaseFunction from './interfaces/iAnimationEaseFunction'; import AnimationEase from './animationEase'; import AnimationManager from './animationManager'; import AnimationEventArgs from './animationEventArgs'; import AnimationValue from './animationValue'; import AnimationEaseLinear from './easing/linear'; import IAnimationBatch from './interfaces/iAnimationBatch'; import AnimationBatch from './animationBatch'; import ISpeed from './interfaces/iSpeed'; import Speed from './speed'; export class Animation extends Initializable implements IAnimation { constructor( values:IAnimationValue[], duration:number = 0.0, ease:IAnimationEaseFunction = AnimationEase.defaultEase, repeatCount:number = 0, key?:string ) { super(); this._values = values; this.p_result = new Result(); this.p_duration = duration; if (is.notEmpty(ease)) this.p_ease = ease; this.p_repeatCount = repeatCount; if (is.notEmpty(key)) this.p_key = key; this.ticked = new Signal(); this.completed = new Signal(true); } /*====================================================================* START: Properties and Fields *====================================================================*/ get values():IAnimationValue[] { return this._values; } private _values:IAnimationValue[]; get result():IPromise { return this.p_result; } protected p_result:IPromise; then(onDone:(value:IAnimation) => void, onError?:(error:Error) => void):IPromise { return this.p_result.then(onDone, onError); } get isTimed():boolean { return this.p_isTimed; } set isTimed(value:boolean) { this.p_isTimed = value; } protected p_isTimed:boolean = true; get isCompleted():boolean { return this.p_isCompleted; } protected p_isCompleted:boolean = false; get isInterrupted():boolean { return this.p_isInterrupted; } protected p_isInterrupted:boolean = false; get animationManager():AnimationManager { return this.internalAnimationManager; } internalAnimationManager:AnimationManager; get progress():number { return this.p_progress; } protected p_progress:number = 0; protected p_ease:IAnimationEaseFunction; protected p_duration:number = 0.0; protected p_startElapsedMilliseconds:number = -1; protected p_elapsedMilliseconds:number = 0; protected p_isInitialized:boolean = false; /// Set to -1 for infinite repeat. /// Set to 1 to reverse the animation when the target is reached and return to start. protected p_repeatCount:number = 0; get key():string { if (is.empty(this.p_key)) { this.p_key = `animation_internal_${Identifier.getSessionUniqueInteger('animation_internal')}`; } return this.p_key; } protected p_key:string; /*====================================================================* START: Signals *====================================================================*/ ticked:Signal; completed:Signal; /*====================================================================* START: Methods *====================================================================*/ internalTick(elapsedMilliseconds:number):void { if (this.p_isCompleted) return; if (!this.isInitialized) { if (!this.p_isTimed) { this.initialize(); } else if (is.empty(this.p_startElapsedMilliseconds) || this.p_startElapsedMilliseconds < 0) { /// Timer hasn't been started, so initialize this.initialize(); this.p_startElapsedMilliseconds = elapsedMilliseconds; } } if (this.p_isTimed && is.notEmpty(this.p_startElapsedMilliseconds) && this.p_startElapsedMilliseconds > -1) { let quotient:number = 0.0; let quotientInt:number = 0.0; let remainder:number = 0.0; let progress:number = 0.0; let halfCycles:number = 0.0; if (this.p_duration > 0) { quotient = ((elapsedMilliseconds - this.p_startElapsedMilliseconds) / 1000) / this.p_duration; quotientInt = Math.floor(quotient); remainder = quotient - quotientInt; progress = remainder; halfCycles = quotientInt; } else { this._completeAnimation(1.0, elapsedMilliseconds - this.p_startElapsedMilliseconds); return; } if (halfCycles >= 1.0) { if (this.p_repeatCount === 0) { // Don't repeat; just run the animation once and wrap it up. this._completeAnimation(1.0, elapsedMilliseconds - this.p_startElapsedMilliseconds); return; } else if (this.p_repeatCount === 1) { // Run the animation once, then return to the start and stop. if (halfCycles >= 2.0) { this._completeAnimation(0.0, elapsedMilliseconds - this.p_startElapsedMilliseconds); return; } } else if (this.p_repeatCount > 1) { // Repeat the animation cycle the specified number of times. if ((halfCycles / 2) >= this.p_repeatCount) { this._completeAnimation(this.p_repeatCount % 1, elapsedMilliseconds - this.p_startElapsedMilliseconds); return; } } else if (this.p_repeatCount < 0) { /* Just keep animating indefinitely. */ } } this.p_elapsedMilliseconds = elapsedMilliseconds - this.p_startElapsedMilliseconds; this.p_progress = progress; this.p_onTicked(new AnimationEventArgs(this, progress, this.p_elapsedMilliseconds)); } } protected p_onTicked(args:IAnimationEventArgs):void { let isAllTargetsReached:boolean = true; if (is.notEmpty(this.p_ease)) { this._values.forEach((value:IAnimationValue) => { value.current = this.p_ease(args.elapsedMilliseconds / 1000, value.start, value.target - value.start, this.p_duration); if (!value.isTargetReached) isAllTargetsReached = false; return true; }); } else { this._values.forEach((value:IAnimationValue) => { value.current = (value.start + (args.progress * (value.target - value.start))); if (!value.isTargetReached) isAllTargetsReached = false; return true; }); } /// Make sure to fire ticked, even if the animation is completing on this tick. this.ticked.dispatch(new AnimationEventArgs(this, args.progress, args.elapsedMilliseconds, this._values)); if (this._values.length > 0 && isAllTargetsReached) this.completeAnimation(); } private _completeAnimation(progress:number, elapsedMilliseconds:number, isInterrupted:boolean = false):void { let args:AnimationEventArgs = new AnimationEventArgs(this, progress, elapsedMilliseconds); if (isInterrupted) { this.p_isInterrupted = true; } this.p_isCompleted = true; this.p_onCompleted(args); this.completed.dispatch(args); this.p_result.resolve(this); this.ticked.deleteAll(); this.completed.deleteAll(); } /** * Fire event from an external class. */ completeAnimation(isInterrupted:boolean = false):void { this._completeAnimation(1, this.p_elapsedMilliseconds, isInterrupted); } protected p_onCompleted(args:IAnimationEventArgs):void { if (this.isInterrupted) return; this._values.forEach((value:IAnimationValue) => { value.current = (value.start + (args.progress * (value.target - value.start))); return true; }); } interrupt() { this.completeAnimation(true); } /*====================================================================* START: Static *====================================================================*/ static get animate():AnimationManager { return this._animate; } private static _animate:AnimationManager = new AnimationManager(HiResTimer.timerDefault); static get animations():MapList { return this._animations; } private static _animations:MapList = new MapList(); static speed:Speed = new Speed(); //===== FPS private static _timerFPS:HiResTimer; private static _timerFPSActual:HiResTimer; static fps:number = 30; static get fpsActual():number { return this._fpsActual; } private static _fpsActual:number = 0.0; private static _frameCount:number = 0; private static _fpsAccum:number = 0; private static _frameAverageCount:number = 30; private static _isRunningInWindow:boolean = true; private static _intervalReference:any; private static _isInitialized:boolean = false; static initialize():void { if (this._isInitialized) return; this._timerFPS = new HiResTimer(new HiResTimerCounterBrowser()); this._timerFPSActual = new HiResTimer(new HiResTimerCounterBrowser()); try { window['requestAnimationFrame'](() => this._onTimerTick()); } catch(e) { this._intervalReference = setInterval(() => this._onTimerTick(), 20); this._isRunningInWindow = false; } this._timerFPS.start(); this._isInitialized = true; } static reset():void { if (!this._isRunningInWindow && this._intervalReference) { clearInterval(this._intervalReference); this._intervalReference = null; } } private static _onTimerTick():void { this._tick(); if (this._isRunningInWindow) window.requestAnimationFrame(() => this._onTimerTick()); } private static _tick():void { if (is.empty(this.animate.timer)) return; this._timerFPSActual.start(); /// Throttle tick speed to desired FPS. if (this._timerFPS.elapsedMilliseconds >= 1000 / this.fps) { this._timerFPS.start(); this.animate.tick(); } if (this._frameCount === this._frameAverageCount) { this._fpsActual = Math.round(1000 / (this._fpsAccum / this._frameAverageCount)); this._fpsAccum = 0; this._frameCount = 0; } else { this._frameCount++; this._fpsAccum += this._timerFPSActual.elapsedMilliseconds } } static validateSpeed(durationSpeed?:(number | ISpeed)):ISpeed { let speed:ISpeed = Speed.validate(durationSpeed); if (is.empty(speed) || speed.isDefault) if (is.notEmpty(this.speed)) speed = this.speed; return speed; } static getDuration(durationSpeed?:(number | ISpeed), startValue?:number, targetValue?:number, isNormalizedValue:boolean = false, fps:number = 24):number { let duration:number = 0; let speed:ISpeed; if (typeof durationSpeed === 'number' && durationSpeed >= 0) return durationSpeed; else { speed = this.validateSpeed(durationSpeed); if ( speed.type === SpeedType.DURATION || (speed.type === SpeedType.UNITS_PER_SECOND && (is.empty(startValue) || is.empty(targetValue))) ) { duration = speed.duration >= 0 ? speed.duration : this.speed.duration; } else if (speed.type === SpeedType.UNITS_PER_SECOND) { let diff:number = Math.abs(targetValue - startValue); let unitValue:number = isNormalizedValue ? speed.normalizedUnitValue : speed.unitValue; duration = (diff) / (speed.unitsPerSecond * unitValue); } else if (speed.type === SpeedType.DIFFERENCE_FACTOR) { if (speed.differenceFactor === 0) duration = 0; else { let ticks:number = 1 / speed.differenceFactor; duration = ticks / fps; } } } return duration; } static go( startValue:number, targetValue:number, durationSpeed?:(number | ISpeed), ease?:IAnimationEaseFunction, repeatCount:number = 0, animationKey?:string ):Animation { let duration:number = this.getDuration(durationSpeed, startValue, targetValue); let animation:Animation = new Animation( [new AnimationValue(startValue, targetValue)], duration, ease, repeatCount, animationKey ); this.goAnimation(animation); return animation; } static goAnimation(animationIn:IAnimation):void { if (is.empty(animationIn)) return null; let animationCurrent:IAnimation = null; if (is.notEmpty(animationIn.key)) animationCurrent = this._animations.get(animationIn.key); if (is.notEmpty(animationCurrent)) { animationCurrent.interrupt(); this._animations.delete(animationCurrent.key); animationCurrent = null; } this._animations.set(animationIn.key, animationIn); animationIn.completed.listenOnce(() => { let animationOld:IAnimation = this.animations.get(animationIn.key); if (is.notEmpty(animationOld) && animationOld == animationIn && animationOld.isCompleted) this._animations.delete(animationIn.key); animationIn = null; }); this.initialize(); this.animate.run(animationIn); } static goAnimations(animationsIn:IAnimation[]):IAnimationBatch { const animationBatch:IAnimationBatch = new AnimationBatch(this.animate, [], false); if (is.empty(animationsIn) || animationsIn.length === 0) return animationBatch; for (let i = 0; i < animationsIn.length; i++) { let animationIn = animationsIn[i]; let animationCurrent: IAnimation = null; if (is.notEmpty(animationIn.key)) animationCurrent = this._animations.get(animationIn.key); if (is.notEmpty(animationCurrent)) { animationCurrent.interrupt(); this._animations.delete(animationCurrent.key); animationCurrent = null; } this._animations.set(animationIn.key, animationIn); animationIn.completed.listenOnce(() => { let animationOld: IAnimation = this.animations.get(animationIn.key); if (is.notEmpty(animationOld) && animationOld == animationIn && animationOld.isCompleted) this._animations.delete(animationIn.key); animationIn = null; }); animationBatch.animations.add(animationIn); } this.initialize(); animationBatch.initialize(); return animationBatch; } //===== Pause static pause( durationSpeed?:(number | ISpeed), ease:IAnimationEaseFunction = AnimationEaseLinear.none ):Animation { let duration:number = this.getDuration(durationSpeed, 0, 1); let animation:Animation = new Animation( [new AnimationValue(0, 1)], duration, ease, 0 ); this.initialize(); this.goAnimation(animation); return animation; } } // End class export default Animation;