From 85f5ac147421718a16147ed395e5f0fd4799a787 Mon Sep 17 00:00:00 2001 From: Reece Como Date: Thu, 26 Dec 2024 16:05:56 +1100 Subject: [PATCH] add event hooks and auto limits --- README.md | 70 ++++++--- dist/index.cjs | 2 +- dist/index.cjs.map | 2 +- dist/index.d.ts | 55 ++++++- dist/index.mjs | 2 +- dist/index.mjs.map | 2 +- package.json | 2 +- src/InterpolatedTicker.ts | 182 +++++++++++++++++++---- src/__tests__/InterpolatedTicker.test.ts | 8 +- 9 files changed, 268 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 16235b7..765c6b4 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ 💪 Configurable, with sensible defaults -🤏 <2Kb minzipped +🤏 Tiny (<2kB) 🍃 No dependencies, tree-shakeable @@ -42,14 +42,14 @@ ```ts // define an update loop (default: 60Hz) -const myLoop = new InterpolatedTicker({ app }) +const mainLoop = new InterpolatedTicker({ app }) -myLoop.update = () => { +mainLoop.update = () => { // changes made here will be rendered at the // current refresh rate is (e.g. 30Hz, 144Hz) } -myLoop.start() +mainLoop.start() ``` ## Getting Started @@ -82,7 +82,7 @@ During an **update** frame, the InterpolatedTicker hydrates its internal buffer *Configuring your interpolation ticker.* ```ts -const ticker = new InterpolationTicker({ +const mainLoop = new InterpolationTicker({ app: myApplication, // how often to trigger update loop (default: 1000/60) @@ -93,28 +93,62 @@ const ticker = new InterpolationTicker({ }) // set the target frequency of the update loop -ticker.updateIntervalMs = 8.3334 +mainLoop.updateIntervalMs = 1000 / 30; // modify the frequency of the update loop (relative to updateIntervalMs) -ticker.speed = 1.5 +mainLoop.speed = 1.5 -// listen to render frames -ticker.onRender = ( deltaTimeMs ) => { - // called during rendering -} +// limit the render frequency, -1 is unlimited (default: -1) +mainLoop.maxRenderFPS = 60 + +// limit render skips - if rendering is interrupted for any +// reason - e.g. the window loses focus - then this will +// limit the maximum number of "catch-up" frames. +mainLoop.maxUpdatesPerRender = 10; -// limit the render frequency (default: -1) -ticker.maxRenderFPS = 60 +// enable/disable interpolation overall +mainLoop.interpolation = false; -// limit render skips - if rendering is interuppted for any -// reason (e.g. window loses focus) then settings this will -// limit the number of "catch-up" frames. -ticker.maxUpdatesPerRender = 10; +// set upper limits for interpolation. +// any changes between update frames larger than these are discarded +// and values are snapped. +mainLoop.autoLimitAlpha = 0.1; // default: 0.5 +mainLoop.autoLimitPosition = 250; // default: 100 +mainLoop.autoLimitRotation = Math.PI; // default: Math.PI / 4 (45°) +mainLoop.autoLimitScale = 2; // default: 1.0 // set the default logic for opt-in/opt-out containers -ticker.getDefaultInterpolation = ( container ): boolean => { +mainLoop.getDefaultInterpolation = ( container ): boolean => { return !(container instanceof ParticleContainer); } + +// +// lifecycle hooks: +// + +mainLoop.preRender = ( deltaTimeMs ) => { + // triggered at the start of a render frame, immediately + // after any update frames have been processed. + // container values are their true values. +} +mainLoop.onRender = ( deltaTimeMs ) => { + // triggered during a render frame, prior to writing the framebuffer. + // container values are their interpolated values. + // changes to values made here will affect the current render. +} +mainLoop.postRender = ( deltaTimeMs ) => { + // triggered at the end of a render frame. + // container values are their true values. +} + +mainLoop.evalStart = ( startTime ) => { + // triggered at the start of each evaluation cycle, prior to + // any update or render frames being processed. +} +mainLoop.evalEnd = ( startTime ) => { + // triggered at the end of each evaluation cycle, after all + // update and render frames have been processed. +} ``` > [!TIP] diff --git a/dist/index.cjs b/dist/index.cjs index c92d271..89c97bf 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -1,2 +1,2 @@ -"use strict";exports.InterpolatedTicker=class InterpolatedTicker{constructor({app:t,updateIntervalMs:e=1e3/60,initialCapacity:i=500}){this.maxUpdatesPerRender=3,this._previousTime=0,this._accumulator=0,this._isRunning=!1,this._speed=1,this._maxRenderFPS=-1,this._maxRenderIntervalMs=-1,this._idxContainersCount=0,this._prevIdxContainersCount=0,this._maxIdx=0,this._releasedIdx=[],this._app=t,this._targetUpdateIntervalMs=e,this._updateIntervalMs=e;const s=i;this._capacity=s,this._idxContainers=new Array(this._capacity);const a=Float32Array.BYTES_PER_ELEMENT*s,r=12*a;this._buffer=new ArrayBuffer(r);let n=0;const allocate=()=>new Float32Array(this._buffer,a*n++,s);this._prevX=allocate(),this._prevY=allocate(),this._prevRotation=allocate(),this._prevScaleX=allocate(),this._prevScaleY=allocate(),this._prevAlpha=allocate(),this._shadowX=allocate(),this._shadowY=allocate(),this._shadowRotation=allocate(),this._shadowScaleX=allocate(),this._shadowScaleY=allocate(),this._shadowAlpha=allocate()}set speed(t){this._speed=t,this._updateIntervalMs=this._targetUpdateIntervalMs/t}get speed(){return this._speed}get updateIntervalMs(){return this._targetUpdateIntervalMs}set updateIntervalMs(t){this._targetUpdateIntervalMs=t,this._updateIntervalMs=t/this._speed}get maxRenderFPS(){return this._maxRenderFPS}set maxRenderFPS(t){this._maxRenderFPS=t<=0?-1:t,this._maxRenderIntervalMs=t<=0?-1:1e3/t}start(){if(this._isRunning)return;const loop=()=>{var t,e;const i=performance.now(),s=i-this._previousTime;if(s=this._updateIntervalMs;)this._captureContainers(),null===(t=this.update)||void 0===t||t.call(this,this._updateIntervalMs),this._accumulator-=this._updateIntervalMs;this._interpolateContainers(this._accumulator),null===(e=this.onRender)||void 0===e||e.call(this,s),this._app.renderer.render(this._app.stage),this._restoreContainers(),this._isRunning&&requestAnimationFrame(loop)}};this._isRunning=!0,this._previousTime=performance.now(),requestAnimationFrame(loop)}stop(){this._isRunning=!1}getDefaultInterpolation(t){return!0}_resizeBuffer(t){const e=Float32Array.BYTES_PER_ELEMENT*t,i=new ArrayBuffer(12*e);let s=0;const allocateAndCopy=a=>{const r=new Float32Array(i,e*s++,t);return r.set(a),r};this._prevX=allocateAndCopy(this._prevX),this._prevY=allocateAndCopy(this._prevY),this._prevRotation=allocateAndCopy(this._prevRotation),this._prevScaleX=allocateAndCopy(this._prevScaleX),this._prevScaleY=allocateAndCopy(this._prevScaleY),this._prevAlpha=allocateAndCopy(this._prevAlpha),this._shadowX=allocateAndCopy(this._shadowX),this._shadowY=allocateAndCopy(this._shadowY),this._shadowRotation=allocateAndCopy(this._shadowRotation),this._shadowScaleX=allocateAndCopy(this._shadowScaleX),this._shadowScaleY=allocateAndCopy(this._shadowScaleY),this._shadowAlpha=allocateAndCopy(this._shadowAlpha),this._buffer=i,this._capacity=t}_captureContainers(){this._idxContainersCount=0,this._captureContainersTraverseSubtree(this._app.stage);for(let t=this._maxIdx;t=this._capacity&&this._resizeBuffer(2*this._capacity);const a=null!==(i=null!==(e=t._interpIdx)&&void 0!==e?e:this._releasedIdx.pop())&&void 0!==i?i:this._maxIdx++;void 0===t._interpIdx&&(t._interpIdx=a),this._prevX[a]=t.position._x,this._prevY[a]=t.position._y,this._prevScaleX[a]=t.scale._x,this._prevScaleY[a]=t.scale._y,this._prevRotation[a]=t.rotation,this._prevAlpha[a]=t.alpha,this._idxContainers[this._idxContainersCount++]=t;const r=null!==(s=t.interpolatedChildren)&&void 0!==s?s:t.children;for(let t=0;t1?1:e<0?0:e;for(let t=0;tMath.PI?h-=2*Math.PI:h<-Math.PI&&(h+=2*Math.PI),e.rotation=this._prevRotation[s]+i*h,e.alpha=this._prevAlpha[s]+i*((this._shadowAlpha[s]=e.alpha)-this._prevAlpha[s])}}_restoreContainers(){for(let t=0;tnew Float32Array(this._buffer,v*m++,c);this._prevX=allocate(),this._prevY=allocate(),this._prevRotation=allocate(),this._prevScaleX=allocate(),this._prevScaleY=allocate(),this._prevAlpha=allocate(),this._shadowX=allocate(),this._shadowY=allocate(),this._shadowRotation=allocate(),this._shadowScaleX=allocate(),this._shadowScaleY=allocate(),this._shadowAlpha=allocate()}set speed(t){this._speed=t,this._updateIntervalMs=this._targetUpdateIntervalMs/t}get speed(){return this._speed}get updateIntervalMs(){return this._targetUpdateIntervalMs}set updateIntervalMs(t){this._targetUpdateIntervalMs=t,this._updateIntervalMs=t/this._speed}get maxRenderFPS(){return this._maxRenderFPS}set maxRenderFPS(t){this._maxRenderFPS=t<=0?-1:t,this._maxRenderIntervalMs=t<=0?-1:1e3/t}get started(){return this._started}start(){if(this._started)return;const loop=()=>{var t,i,e,s,a,r;const h=performance.now(),o=h-this._previousTime;if(o=this._updateIntervalMs;)this._captureContainers(),null===(i=this.update)||void 0===i||i.call(this,this._updateIntervalMs),this._accumulator-=this._updateIntervalMs;null===(e=this.beforeRender)||void 0===e||e.call(this,o),this._interpolateContainers(this._accumulator),null===(s=this.onRender)||void 0===s||s.call(this,o),this._app.renderer.render(this._app.stage),this._restoreContainers(),null===(a=this.afterRender)||void 0===a||a.call(this,o),null===(r=this.evalEnd)||void 0===r||r.call(this,h),this._started&&requestAnimationFrame(loop)}};this._started=!0,this._previousTime=performance.now(),requestAnimationFrame(loop)}stop(){this._started=!1}getDefaultInterpolation(t){return!0}_resizeBuffer(t){const i=Float32Array.BYTES_PER_ELEMENT*t,e=new ArrayBuffer(12*i);let s=0;const allocateAndCopy=a=>{const r=new Float32Array(e,i*s++,t);return r.set(a),r};this._prevX=allocateAndCopy(this._prevX),this._prevY=allocateAndCopy(this._prevY),this._prevRotation=allocateAndCopy(this._prevRotation),this._prevScaleX=allocateAndCopy(this._prevScaleX),this._prevScaleY=allocateAndCopy(this._prevScaleY),this._prevAlpha=allocateAndCopy(this._prevAlpha),this._shadowX=allocateAndCopy(this._shadowX),this._shadowY=allocateAndCopy(this._shadowY),this._shadowRotation=allocateAndCopy(this._shadowRotation),this._shadowScaleX=allocateAndCopy(this._shadowScaleX),this._shadowScaleY=allocateAndCopy(this._shadowScaleY),this._shadowAlpha=allocateAndCopy(this._shadowAlpha),this._buffer=e,this._capacity=t}_captureContainers(){this._idxContainersCount=0,this._captureContainersTraverseSubtree(this._app.stage);for(let t=this._maxIdx;t=this._capacity&&this._resizeBuffer(2*this._capacity);const a=null!==(e=null!==(i=t._interpIdx)&&void 0!==i?i:this._releasedIdx.pop())&&void 0!==e?e:this._maxIdx++;void 0===t._interpIdx&&(t._interpIdx=a),this._prevX[a]=t.position._x,this._prevY[a]=t.position._y,this._prevScaleX[a]=t.scale._x,this._prevScaleY[a]=t.scale._y,this._prevRotation[a]=t.rotation,this._prevAlpha[a]=t.alpha,this._idxContainers[this._idxContainersCount++]=t;const r=null!==(s=t.interpolatedChildren)&&void 0!==s?s:t.children;for(let t=0;t1?1:i<0?0:i;for(let t=0;tMath.PI?_-=2*Math.PI:_<-Math.PI&&(_+=2*Math.PI),Math.abs(_)<=this.autoLimitRotation&&(i.rotation=this._prevRotation[s]+e*_);const d=(this._shadowAlpha[s]=i.alpha)-this._prevAlpha[s];Math.abs(d)<=this.autoLimitAlpha&&(i.alpha=this._prevAlpha[s]+e*d)}}_restoreContainers(){if(this.interpolation)for(let t=0;t void; /** - * Triggered on each render frame during frame interpolation. - * Container values will be their temporary interpolated values. + * Triggered at the start of each cycle, prior to + * any update or render frames being processed. + */ + evalStart?: (start: number) => void; + /** + * Triggered at the end of each cycle, after any + * update or render frames have been processed. + */ + evalEnd?: (start: number) => void; + /** + * Triggered before a render frame. + * + * Container values are their true values. + */ + beforeRender?: (dt: number) => void; + /** + * Triggered during a render frame (prior to writing + * the framebuffer). + * + * Container values are their interpolated values. */ onRender?: (dt: number) => void; + /** + * Triggered after a render frame (after writing the + * framebuffer). + * + * Container values are their true values. + */ + afterRender?: (dt: number) => void; /** Limit maximum number of update() per render (i.e. rendering is slow). */ maxUpdatesPerRender: number; + /** Whether interpolation is currently enabled. */ + interpolation: boolean; + /** The maximum change in position values to interpolate (default: 100). */ + autoLimitPosition: number; + /** The maximum change in scale values to interpolate (default: 1). */ + autoLimitScale: number; + /** The maximum change in rotation values to interpolate (default: 45°). */ + autoLimitRotation: number; + /** The maximum change in alpha values to interpolate (default: 0.5). */ + autoLimitAlpha: number; protected _app: Application; protected _targetUpdateIntervalMs: number; protected _updateIntervalMs: number; protected _previousTime: number; protected _accumulator: number; - protected _isRunning: boolean; + protected _started: boolean; protected _speed: number; protected _maxRenderFPS: number; protected _maxRenderIntervalMs: number; @@ -88,10 +123,21 @@ export declare class InterpolatedTicker { protected _shadowScaleY: Float32Array; protected _shadowAlpha: Float32Array; protected _buffer: ArrayBuffer; - constructor({ app, updateIntervalMs, initialCapacity, }: { + constructor({ app, update, evalStart, evalEnd, beforeRender, onRender, afterRender, autoLimitAlpha, autoLimitPosition, autoLimitRotation, autoLimitScale, interpolation, updateIntervalMs, initialCapacity, }: { app: Application; + interpolation?: boolean; updateIntervalMs?: number; initialCapacity?: number; + update?: (ft: number) => void; + evalStart?: (start: number) => void; + evalEnd?: (start: number) => void; + beforeRender?: (dt: number) => void; + onRender?: (dt: number) => void; + afterRender?: (dt: number) => void; + autoLimitAlpha?: number; + autoLimitPosition?: number; + autoLimitRotation?: number; + autoLimitScale?: number; }); set speed(value: number); get speed(): number; @@ -99,6 +145,7 @@ export declare class InterpolatedTicker { set updateIntervalMs(value: number); get maxRenderFPS(): number; set maxRenderFPS(value: number); + get started(): boolean; start(): void; stop(): void; /** diff --git a/dist/index.mjs b/dist/index.mjs index 4ff184e..12f20a3 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -1,2 +1,2 @@ -class InterpolatedTicker{constructor({app:t,updateIntervalMs:e=1e3/60,initialCapacity:i=500}){this.maxUpdatesPerRender=3,this._previousTime=0,this._accumulator=0,this._isRunning=!1,this._speed=1,this._maxRenderFPS=-1,this._maxRenderIntervalMs=-1,this._idxContainersCount=0,this._prevIdxContainersCount=0,this._maxIdx=0,this._releasedIdx=[],this._app=t,this._targetUpdateIntervalMs=e,this._updateIntervalMs=e;const s=i;this._capacity=s,this._idxContainers=new Array(this._capacity);const a=Float32Array.BYTES_PER_ELEMENT*s,r=12*a;this._buffer=new ArrayBuffer(r);let n=0;const allocate=()=>new Float32Array(this._buffer,a*n++,s);this._prevX=allocate(),this._prevY=allocate(),this._prevRotation=allocate(),this._prevScaleX=allocate(),this._prevScaleY=allocate(),this._prevAlpha=allocate(),this._shadowX=allocate(),this._shadowY=allocate(),this._shadowRotation=allocate(),this._shadowScaleX=allocate(),this._shadowScaleY=allocate(),this._shadowAlpha=allocate()}set speed(t){this._speed=t,this._updateIntervalMs=this._targetUpdateIntervalMs/t}get speed(){return this._speed}get updateIntervalMs(){return this._targetUpdateIntervalMs}set updateIntervalMs(t){this._targetUpdateIntervalMs=t,this._updateIntervalMs=t/this._speed}get maxRenderFPS(){return this._maxRenderFPS}set maxRenderFPS(t){this._maxRenderFPS=t<=0?-1:t,this._maxRenderIntervalMs=t<=0?-1:1e3/t}start(){if(this._isRunning)return;const loop=()=>{var t,e;const i=performance.now(),s=i-this._previousTime;if(s=this._updateIntervalMs;)this._captureContainers(),null===(t=this.update)||void 0===t||t.call(this,this._updateIntervalMs),this._accumulator-=this._updateIntervalMs;this._interpolateContainers(this._accumulator),null===(e=this.onRender)||void 0===e||e.call(this,s),this._app.renderer.render(this._app.stage),this._restoreContainers(),this._isRunning&&requestAnimationFrame(loop)}};this._isRunning=!0,this._previousTime=performance.now(),requestAnimationFrame(loop)}stop(){this._isRunning=!1}getDefaultInterpolation(t){return!0}_resizeBuffer(t){const e=Float32Array.BYTES_PER_ELEMENT*t,i=new ArrayBuffer(12*e);let s=0;const allocateAndCopy=a=>{const r=new Float32Array(i,e*s++,t);return r.set(a),r};this._prevX=allocateAndCopy(this._prevX),this._prevY=allocateAndCopy(this._prevY),this._prevRotation=allocateAndCopy(this._prevRotation),this._prevScaleX=allocateAndCopy(this._prevScaleX),this._prevScaleY=allocateAndCopy(this._prevScaleY),this._prevAlpha=allocateAndCopy(this._prevAlpha),this._shadowX=allocateAndCopy(this._shadowX),this._shadowY=allocateAndCopy(this._shadowY),this._shadowRotation=allocateAndCopy(this._shadowRotation),this._shadowScaleX=allocateAndCopy(this._shadowScaleX),this._shadowScaleY=allocateAndCopy(this._shadowScaleY),this._shadowAlpha=allocateAndCopy(this._shadowAlpha),this._buffer=i,this._capacity=t}_captureContainers(){this._idxContainersCount=0,this._captureContainersTraverseSubtree(this._app.stage);for(let t=this._maxIdx;t=this._capacity&&this._resizeBuffer(2*this._capacity);const a=null!==(i=null!==(e=t._interpIdx)&&void 0!==e?e:this._releasedIdx.pop())&&void 0!==i?i:this._maxIdx++;void 0===t._interpIdx&&(t._interpIdx=a),this._prevX[a]=t.position._x,this._prevY[a]=t.position._y,this._prevScaleX[a]=t.scale._x,this._prevScaleY[a]=t.scale._y,this._prevRotation[a]=t.rotation,this._prevAlpha[a]=t.alpha,this._idxContainers[this._idxContainersCount++]=t;const r=null!==(s=t.interpolatedChildren)&&void 0!==s?s:t.children;for(let t=0;t1?1:e<0?0:e;for(let t=0;tMath.PI?h-=2*Math.PI:h<-Math.PI&&(h+=2*Math.PI),e.rotation=this._prevRotation[s]+i*h,e.alpha=this._prevAlpha[s]+i*((this._shadowAlpha[s]=e.alpha)-this._prevAlpha[s])}}_restoreContainers(){for(let t=0;tnew Float32Array(this._buffer,v*m++,c);this._prevX=allocate(),this._prevY=allocate(),this._prevRotation=allocate(),this._prevScaleX=allocate(),this._prevScaleY=allocate(),this._prevAlpha=allocate(),this._shadowX=allocate(),this._shadowY=allocate(),this._shadowRotation=allocate(),this._shadowScaleX=allocate(),this._shadowScaleY=allocate(),this._shadowAlpha=allocate()}set speed(t){this._speed=t,this._updateIntervalMs=this._targetUpdateIntervalMs/t}get speed(){return this._speed}get updateIntervalMs(){return this._targetUpdateIntervalMs}set updateIntervalMs(t){this._targetUpdateIntervalMs=t,this._updateIntervalMs=t/this._speed}get maxRenderFPS(){return this._maxRenderFPS}set maxRenderFPS(t){this._maxRenderFPS=t<=0?-1:t,this._maxRenderIntervalMs=t<=0?-1:1e3/t}get started(){return this._started}start(){if(this._started)return;const loop=()=>{var t,i,e,s,a,r;const h=performance.now(),o=h-this._previousTime;if(o=this._updateIntervalMs;)this._captureContainers(),null===(i=this.update)||void 0===i||i.call(this,this._updateIntervalMs),this._accumulator-=this._updateIntervalMs;null===(e=this.beforeRender)||void 0===e||e.call(this,o),this._interpolateContainers(this._accumulator),null===(s=this.onRender)||void 0===s||s.call(this,o),this._app.renderer.render(this._app.stage),this._restoreContainers(),null===(a=this.afterRender)||void 0===a||a.call(this,o),null===(r=this.evalEnd)||void 0===r||r.call(this,h),this._started&&requestAnimationFrame(loop)}};this._started=!0,this._previousTime=performance.now(),requestAnimationFrame(loop)}stop(){this._started=!1}getDefaultInterpolation(t){return!0}_resizeBuffer(t){const i=Float32Array.BYTES_PER_ELEMENT*t,e=new ArrayBuffer(12*i);let s=0;const allocateAndCopy=a=>{const r=new Float32Array(e,i*s++,t);return r.set(a),r};this._prevX=allocateAndCopy(this._prevX),this._prevY=allocateAndCopy(this._prevY),this._prevRotation=allocateAndCopy(this._prevRotation),this._prevScaleX=allocateAndCopy(this._prevScaleX),this._prevScaleY=allocateAndCopy(this._prevScaleY),this._prevAlpha=allocateAndCopy(this._prevAlpha),this._shadowX=allocateAndCopy(this._shadowX),this._shadowY=allocateAndCopy(this._shadowY),this._shadowRotation=allocateAndCopy(this._shadowRotation),this._shadowScaleX=allocateAndCopy(this._shadowScaleX),this._shadowScaleY=allocateAndCopy(this._shadowScaleY),this._shadowAlpha=allocateAndCopy(this._shadowAlpha),this._buffer=e,this._capacity=t}_captureContainers(){this._idxContainersCount=0,this._captureContainersTraverseSubtree(this._app.stage);for(let t=this._maxIdx;t=this._capacity&&this._resizeBuffer(2*this._capacity);const a=null!==(e=null!==(i=t._interpIdx)&&void 0!==i?i:this._releasedIdx.pop())&&void 0!==e?e:this._maxIdx++;void 0===t._interpIdx&&(t._interpIdx=a),this._prevX[a]=t.position._x,this._prevY[a]=t.position._y,this._prevScaleX[a]=t.scale._x,this._prevScaleY[a]=t.scale._y,this._prevRotation[a]=t.rotation,this._prevAlpha[a]=t.alpha,this._idxContainers[this._idxContainersCount++]=t;const r=null!==(s=t.interpolatedChildren)&&void 0!==s?s:t.children;for(let t=0;t1?1:i<0?0:i;for(let t=0;tMath.PI?_-=2*Math.PI:_<-Math.PI&&(_+=2*Math.PI),Math.abs(_)<=this.autoLimitRotation&&(i.rotation=this._prevRotation[s]+e*_);const d=(this._shadowAlpha[s]=i.alpha)-this._prevAlpha[s];Math.abs(d)<=this.autoLimitAlpha&&(i.alpha=this._prevAlpha[s]+e*d)}}_restoreContainers(){if(this.interpolation)for(let t=0;t", "authors": [ "Reece Como " diff --git a/src/InterpolatedTicker.ts b/src/InterpolatedTicker.ts index ac58de5..a7c5a64 100644 --- a/src/InterpolatedTicker.ts +++ b/src/InterpolatedTicker.ts @@ -49,6 +49,8 @@ export type InterpolatedContainer = Container & { */ export class InterpolatedTicker { + // ----- Event hooks: ----- + /** * The update loop to trigger on each fixed timestep. * Container values set here are interpolated on render frames. @@ -56,16 +58,62 @@ export class InterpolatedTicker public update?: ( ft: number ) => void; /** - * Triggered on each render frame during frame interpolation. - * Container values will be their temporary interpolated values. + * Triggered at the start of each cycle, prior to + * any update or render frames being processed. + */ + public evalStart?: ( start: number ) => void; + + /** + * Triggered at the end of each cycle, after any + * update or render frames have been processed. + */ + public evalEnd?: ( start: number ) => void; + + /** + * Triggered before a render frame. + * + * Container values are their true values. + */ + public beforeRender?: ( dt: number ) => void; + + /** + * Triggered during a render frame (prior to writing + * the framebuffer). + * + * Container values are their interpolated values. */ public onRender?: ( dt: number ) => void; - /** Limit maximum number of update() per render (i.e. rendering is slow). */ - public maxUpdatesPerRender = 3; + /** + * Triggered after a render frame (after writing the + * framebuffer). + * + * Container values are their true values. + */ + public afterRender?: ( dt: number ) => void; // ----- Properties: ----- + /** Limit maximum number of update() per render (i.e. rendering is slow). */ + public maxUpdatesPerRender = 10; + + /** Whether interpolation is currently enabled. */ + public interpolation = true; + + /** The maximum change in position values to interpolate (default: 100). */ + public autoLimitPosition = 100; + + /** The maximum change in scale values to interpolate (default: 1). */ + public autoLimitScale = 1; + + /** The maximum change in rotation values to interpolate (default: 45°). */ + public autoLimitRotation = Math.PI / 4; + + /** The maximum change in alpha values to interpolate (default: 0.5). */ + public autoLimitAlpha = 0.5; + + // ----- Internal: ----- + protected _app: Application; // ticker @@ -73,7 +121,7 @@ export class InterpolatedTicker protected _updateIntervalMs: number; protected _previousTime: number = 0; protected _accumulator: number = 0; - protected _isRunning: boolean = false; + protected _started: boolean = false; protected _speed: number = 1.0; protected _maxRenderFPS: number = -1; protected _maxRenderIntervalMs: number = -1; @@ -116,20 +164,56 @@ export class InterpolatedTicker public constructor( { app, + update, + evalStart, + evalEnd, + beforeRender, + onRender, + afterRender, + autoLimitAlpha, + autoLimitPosition, + autoLimitRotation, + autoLimitScale, + interpolation = true, updateIntervalMs = 1_000 / 60, initialCapacity = 500, }: { app: Application; + // optional config: + interpolation?: boolean; updateIntervalMs?: number; initialCapacity?: number; + update?: ( ft: number ) => void; + evalStart?: ( start: number ) => void; + evalEnd?: ( start: number ) => void; + beforeRender?: ( dt: number ) => void; + onRender?: ( dt: number ) => void; + afterRender?: ( dt: number ) => void; + autoLimitAlpha?: number; + autoLimitPosition?: number; + autoLimitRotation?: number; + autoLimitScale?: number; } ) { this._app = app; + this.update = update; + this.evalStart = evalStart; + this.evalEnd = evalEnd; + this.onRender = onRender; + this.beforeRender = beforeRender; + this.afterRender = afterRender; + this._targetUpdateIntervalMs = updateIntervalMs; this._updateIntervalMs = updateIntervalMs; + this.interpolation = interpolation; + this.autoLimitAlpha = autoLimitAlpha; + this.autoLimitPosition = autoLimitPosition; + this.autoLimitRotation = autoLimitRotation; + this.autoLimitScale = autoLimitScale; + const capacity = initialCapacity; this._capacity = capacity; this._idxContainers = new Array( this._capacity ); @@ -192,19 +276,24 @@ export class InterpolatedTicker this._maxRenderIntervalMs = value <= 0 ? -1 : 1000 / value; } + public get started(): boolean + { + return this._started; + } + public start(): void { - if ( this._isRunning ) return; + if ( this._started ) return; const loop = (): void => { - const now = performance.now(); - const renderDelta = now - this._previousTime; + const start = performance.now(); + const renderDelta = start - this._previousTime; // limit renders if needed if ( renderDelta < this._maxRenderIntervalMs ) { - if ( this._isRunning ) + if ( this._started ) { requestAnimationFrame( loop ); } @@ -212,7 +301,9 @@ export class InterpolatedTicker return; } - this._previousTime = now; + this.evalStart?.( start ); + + this._previousTime = start; this._accumulator = Math.min( this._accumulator + renderDelta, @@ -236,26 +327,31 @@ export class InterpolatedTicker // ------------------------------------- // + this.beforeRender?.( renderDelta ); this._interpolateContainers( this._accumulator ); this.onRender?.( renderDelta ); this._app.renderer.render( this._app.stage ); this._restoreContainers(); + this.afterRender?.( renderDelta ); + + // end the loop + this.evalEnd?.( start ); - if ( this._isRunning ) + if ( this._started ) { requestAnimationFrame( loop ); } }; // kick off - this._isRunning = true; + this._started = true; this._previousTime = performance.now(); requestAnimationFrame( loop ); } public stop(): void { - this._isRunning = false; + this._started = false; } /** @@ -372,6 +468,8 @@ export class InterpolatedTicker protected _interpolateContainers( accumulated: number ): void { + if ( !this.interpolation ) return; + const rawFactor = accumulated / this._updateIntervalMs; const factor = rawFactor > 1 ? 1 : rawFactor < 0 ? 0 : rawFactor; @@ -416,22 +514,37 @@ export class InterpolatedTicker dy = ( ( dy + yrange / 2 ) % yrange + yrange ) % yrange - yrange / 2; } - container.position.set( - this._prevX[index]! + factor * dx, - this._prevY[index]! + factor * dy - ); + if ( + Math.abs( dx ) <= this.autoLimitPosition + && Math.abs( dy ) <= this.autoLimitPosition + ) + { + container.position.set( + this._prevX[index]! + factor * dx, + this._prevY[index]! + factor * dy + ); + } // scale - container.scale.set( - this._prevScaleX[index]! + factor * ( - ( this._shadowScaleX[index] = container.scale._x ) // 🔬 NOTE: assignment - - this._prevScaleX[index]! - ), - this._prevScaleY[index]! + factor * ( - ( this._shadowScaleY[index] = container.scale._y ) // 🔬 NOTE: assignment - - this._prevScaleY[index]! - ) + const scaleDx = ( + ( this._shadowScaleX[index] = container.scale._x ) // 🔬 NOTE: assignment + - this._prevScaleX[index]! ); + const scaleDy = ( + ( this._shadowScaleY[index] = container.scale._y ) // 🔬 NOTE: assignment + - this._prevScaleY[index]! + ); + + if ( + Math.abs( scaleDx ) <= this.autoLimitScale + && Math.abs( scaleDy ) <= this.autoLimitScale + ) + { + container.scale.set( + this._prevScaleX[index]! + factor * scaleDx, + this._prevScaleY[index]! + factor * scaleDy + ); + } // rotation (wrap-around) let rotationDelta = @@ -440,18 +553,29 @@ export class InterpolatedTicker if ( rotationDelta > Math.PI ) rotationDelta -= 2 * Math.PI; else if ( rotationDelta < -Math.PI ) rotationDelta += 2 * Math.PI; - container.rotation = this._prevRotation[index]! + factor * rotationDelta; + + if ( Math.abs( rotationDelta ) <= this.autoLimitRotation ) + { + container.rotation = this._prevRotation[index]! + factor * rotationDelta; + } // alpha - container.alpha = this._prevAlpha[index]! + factor * ( + const alphaDelta = ( ( this._shadowAlpha[index] = container.alpha ) // 🔬 NOTE: assignment - - this._prevAlpha[index]! + - this._prevAlpha[index]! ); + + if ( Math.abs( alphaDelta ) <= this.autoLimitAlpha ) + { + container.alpha = this._prevAlpha[index]! + factor * alphaDelta; + } } } protected _restoreContainers(): void { + if ( !this.interpolation ) return; + for ( let i = 0; i < this._idxContainersCount; i++ ) { if ( this._idxContainers[i] === undefined ) continue; diff --git a/src/__tests__/InterpolatedTicker.test.ts b/src/__tests__/InterpolatedTicker.test.ts index 63f5f7b..1ca19c9 100644 --- a/src/__tests__/InterpolatedTicker.test.ts +++ b/src/__tests__/InterpolatedTicker.test.ts @@ -20,7 +20,13 @@ describe("InterpolatedTicker", () => beforeEach(() => { app = mockApp(); - ticker = new InterpolatedTicker({ app }); + ticker = new InterpolatedTicker({ + app, + autoLimitPosition: 1000, + autoLimitScale: 10, + autoLimitAlpha: 100, + autoLimitRotation: Math.PI * 2, + }); }); it("should initialize with default properties", () =>