Skip to content

Commit

Permalink
add event hooks and auto limits
Browse files Browse the repository at this point in the history
  • Loading branch information
reececomo committed Dec 26, 2024
1 parent 8335498 commit 85f5ac1
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 57 deletions.
70 changes: 52 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<td>💪 Configurable, with sensible defaults</td>
</tr>
<tr>
<td>🤏 &lt;2Kb minzipped</td>
<td>🤏 Tiny (&lt;2kB)</td>
<td>🍃 No dependencies, tree-shakeable</td>
</tr>
</tbody>
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion dist/index.cjs
Original file line number Diff line number Diff line change
@@ -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._maxRenderIntervalMs)this._isRunning&&requestAnimationFrame(loop);else{for(this._previousTime=i,this._accumulator=Math.min(this._accumulator+s,this._updateIntervalMs*this.maxUpdatesPerRender);this._accumulator>=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._prevIdxContainersCount;t++)this._markReleased(this._idxContainers[t]),this._idxContainers[t]=void 0;this._prevIdxContainersCount=this._maxIdx}_captureContainersTraverseSubtree(t){var e,i,s;if(t.destroyed)return;if(!1===t.interpolation)return;if(void 0===t.interpolation){if(!this.getDefaultInterpolation(t))return void(t.interpolation=!1);t.interpolation=!0}this._maxIdx+1>=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;t<r.length;t++)this._captureContainersTraverseSubtree(r[t])}_interpolateContainers(t){const e=t/this._updateIntervalMs,i=e>1?1:e<0?0:e;for(let t=0;t<this._idxContainersCount;t++){if(void 0===this._idxContainers[t])continue;if(this._idxContainers[t].destroyed){this._markReleased(this._idxContainers[t]);continue}const e=this._idxContainers[t],s=e._interpIdx,a=e.interpolationWraparound;let r=(this._shadowX[s]=e.position._x)-this._prevX[s],n=(this._shadowY[s]=e.position._y)-this._prevY[s];if(void 0!==a){const t=a.xRange,e=a.yRange;r=((r+t/2)%t+t)%t-t/2,n=((n+e/2)%e+e)%e-e/2}e.position.set(this._prevX[s]+i*r,this._prevY[s]+i*n),e.scale.set(this._prevScaleX[s]+i*((this._shadowScaleX[s]=e.scale._x)-this._prevScaleX[s]),this._prevScaleY[s]+i*((this._shadowScaleY[s]=e.scale._y)-this._prevScaleY[s]));let h=(this._shadowRotation[s]=e.rotation)-this._prevRotation[s];h>Math.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;t<this._idxContainersCount;t++){if(void 0===this._idxContainers[t])continue;if(this._idxContainers[t].destroyed){this._markReleased(this._idxContainers[t]);continue}const e=this._idxContainers[t],i=e._interpIdx;e.position.set(this._shadowX[i],this._shadowY[i]),e.scale.set(this._shadowScaleX[i],this._shadowScaleY[i]),e.rotation=this._shadowRotation[i],e.alpha=this._shadowAlpha[i]}}_markReleased(t){void 0!==(null==t?void 0:t._interpIdx)&&(this._releasedIdx.push(t._interpIdx),t._interpIdx=void 0)}};
"use strict";exports.InterpolatedTicker=class InterpolatedTicker{constructor({app:t,update:i,evalStart:e,evalEnd:s,beforeRender:a,onRender:r,afterRender:h,autoLimitAlpha:o,autoLimitPosition:n,autoLimitRotation:_,autoLimitScale:d,interpolation:l=!0,updateIntervalMs:p=1e3/60,initialCapacity:u=500}){this.maxUpdatesPerRender=10,this.interpolation=!0,this.autoLimitPosition=100,this.autoLimitScale=1,this.autoLimitRotation=Math.PI/4,this.autoLimitAlpha=.5,this._previousTime=0,this._accumulator=0,this._started=!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.update=i,this.evalStart=e,this.evalEnd=s,this.onRender=r,this.beforeRender=a,this.afterRender=h,this._targetUpdateIntervalMs=p,this._updateIntervalMs=p,this.interpolation=l,this.autoLimitAlpha=o,this.autoLimitPosition=n,this.autoLimitRotation=_,this.autoLimitScale=d;const c=u;this._capacity=c,this._idxContainers=new Array(this._capacity);const v=Float32Array.BYTES_PER_ELEMENT*c,x=12*v;this._buffer=new ArrayBuffer(x);let m=0;const allocate=()=>new 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._maxRenderIntervalMs)this._started&&requestAnimationFrame(loop);else{for(null===(t=this.evalStart)||void 0===t||t.call(this,h),this._previousTime=h,this._accumulator=Math.min(this._accumulator+o,this._updateIntervalMs*this.maxUpdatesPerRender);this._accumulator>=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._prevIdxContainersCount;t++)this._markReleased(this._idxContainers[t]),this._idxContainers[t]=void 0;this._prevIdxContainersCount=this._maxIdx}_captureContainersTraverseSubtree(t){var i,e,s;if(t.destroyed)return;if(!1===t.interpolation)return;if(void 0===t.interpolation){if(!this.getDefaultInterpolation(t))return void(t.interpolation=!1);t.interpolation=!0}this._maxIdx+1>=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;t<r.length;t++)this._captureContainersTraverseSubtree(r[t])}_interpolateContainers(t){if(!this.interpolation)return;const i=t/this._updateIntervalMs,e=i>1?1:i<0?0:i;for(let t=0;t<this._idxContainersCount;t++){if(void 0===this._idxContainers[t])continue;if(this._idxContainers[t].destroyed){this._markReleased(this._idxContainers[t]);continue}const i=this._idxContainers[t],s=i._interpIdx,a=i.interpolationWraparound;let r=(this._shadowX[s]=i.position._x)-this._prevX[s],h=(this._shadowY[s]=i.position._y)-this._prevY[s];if(void 0!==a){const t=a.xRange,i=a.yRange;r=((r+t/2)%t+t)%t-t/2,h=((h+i/2)%i+i)%i-i/2}Math.abs(r)<=this.autoLimitPosition&&Math.abs(h)<=this.autoLimitPosition&&i.position.set(this._prevX[s]+e*r,this._prevY[s]+e*h);const o=(this._shadowScaleX[s]=i.scale._x)-this._prevScaleX[s],n=(this._shadowScaleY[s]=i.scale._y)-this._prevScaleY[s];Math.abs(o)<=this.autoLimitScale&&Math.abs(n)<=this.autoLimitScale&&i.scale.set(this._prevScaleX[s]+e*o,this._prevScaleY[s]+e*n);let _=(this._shadowRotation[s]=i.rotation)-this._prevRotation[s];_>Math.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<this._idxContainersCount;t++){if(void 0===this._idxContainers[t])continue;if(this._idxContainers[t].destroyed){this._markReleased(this._idxContainers[t]);continue}const i=this._idxContainers[t],e=i._interpIdx;i.position.set(this._shadowX[e],this._shadowY[e]),i.scale.set(this._shadowScaleX[e],this._shadowScaleY[e]),i.rotation=this._shadowRotation[e],i.alpha=this._shadowAlpha[e]}}_markReleased(t){void 0!==(null==t?void 0:t._interpIdx)&&(this._releasedIdx.push(t._interpIdx),t._interpIdx=void 0)}};
//# sourceMappingURL=index.cjs.map
Loading

0 comments on commit 85f5ac1

Please sign in to comment.