theatre/packages/dataverse/src/Ticker.ts

171 lines
4.5 KiB
TypeScript
Raw Normal View History

2021-06-18 13:05:06 +02:00
type ICallback = (t: number) => void
function createRafTicker() {
const ticker = new Ticker()
if (typeof window !== 'undefined') {
/**
* @remarks
* TODO users should also be able to define their own ticker.
*/
const onAnimationFrame = (t: number) => {
ticker.tick(t)
window.requestAnimationFrame(onAnimationFrame)
}
window.requestAnimationFrame(onAnimationFrame)
} else {
ticker.tick(0)
setTimeout(() => ticker.tick(1), 0)
console.log(
`@theatre/dataverse is running in a server rather than in a browser. We haven't gotten around to testing server-side rendering, so if something is working in the browser but not on the server, please file a bug: https://github.com/theatre-js/theatre/issues/new`,
)
}
return ticker
}
let rafTicker: undefined | Ticker
2022-01-19 13:06:13 +01:00
/**
* The Ticker class helps schedule callbacks. Scheduled callbacks are executed per tick. Ticks can be triggered by an
* external scheduling strategy, e.g. a raf.
*/
2021-06-18 13:05:06 +02:00
export default class Ticker {
2022-07-16 15:47:31 +02:00
/** Get a shared `requestAnimationFrame` ticker. */
static get raf(): Ticker {
if (!rafTicker) {
rafTicker = createRafTicker()
}
return rafTicker
}
2021-06-18 13:05:06 +02:00
private _scheduledForThisOrNextTick: Set<ICallback>
private _scheduledForNextTick: Set<ICallback>
private _timeAtCurrentTick: number
private _ticking: boolean = false
/**
* Counts up for every tick executed.
* Internally, this is used to measure ticks per second.
*/
public __ticks = 0
2021-06-18 13:05:06 +02:00
constructor() {
this._scheduledForThisOrNextTick = new Set()
this._scheduledForNextTick = new Set()
this._timeAtCurrentTick = 0
}
/**
* Registers for fn to be called either on this tick or the next tick.
*
2022-01-19 13:06:13 +01:00
* If `onThisOrNextTick()` is called while `Ticker.tick()` is running, the
2021-06-18 13:05:06 +02:00
* side effect _will_ be called within the running tick. If you don't want this
2022-01-19 13:06:13 +01:00
* behavior, you can use `onNextTick()`.
2021-06-18 13:05:06 +02:00
*
2022-01-19 13:06:13 +01:00
* Note that `fn` will be added to a `Set()`. Which means, if you call `onThisOrNextTick(fn)`
2021-06-18 13:05:06 +02:00
* with the same fn twice in a single tick, it'll only run once.
2022-01-19 13:06:13 +01:00
*
2022-02-23 22:53:39 +01:00
* @param fn - The function to be registered.
2022-01-19 13:06:13 +01:00
*
* @see offThisOrNextTick
2021-06-18 13:05:06 +02:00
*/
onThisOrNextTick(fn: ICallback) {
this._scheduledForThisOrNextTick.add(fn)
}
/**
* Registers a side effect to be called on the next tick.
*
2022-02-23 22:53:39 +01:00
* @param fn - The function to be registered.
2022-01-19 13:06:13 +01:00
*
* @see onThisOrNextTick
* @see offNextTick
2021-06-18 13:05:06 +02:00
*/
onNextTick(fn: ICallback) {
this._scheduledForNextTick.add(fn)
}
2022-01-19 13:06:13 +01:00
/**
* De-registers a fn to be called either on this tick or the next tick.
*
2022-02-23 22:53:39 +01:00
* @param fn - The function to be de-registered.
2022-01-19 13:06:13 +01:00
*
* @see onThisOrNextTick
*/
2021-06-18 13:05:06 +02:00
offThisOrNextTick(fn: ICallback) {
this._scheduledForThisOrNextTick.delete(fn)
}
2022-01-19 13:06:13 +01:00
/**
* De-registers a fn to be called on the next tick.
*
2022-02-23 22:53:39 +01:00
* @param fn - The function to be de-registered.
2022-01-19 13:06:13 +01:00
*
* @see onNextTick
*/
2021-06-18 13:05:06 +02:00
offNextTick(fn: ICallback) {
this._scheduledForNextTick.delete(fn)
}
2022-01-19 13:06:13 +01:00
/**
* The time at the start of the current tick if there is a tick in progress, otherwise defaults to
* `performance.now()`.
*/
2021-06-18 13:05:06 +02:00
get time() {
if (this._ticking) {
return this._timeAtCurrentTick
} else return performance.now()
}
2022-01-19 13:06:13 +01:00
/**
* Triggers a tick which starts executing the callbacks scheduled for this tick.
*
2022-02-23 22:53:39 +01:00
* @param t - The time at the tick.
2022-01-19 13:06:13 +01:00
*
* @see onThisOrNextTick
* @see onNextTick
*/
2021-06-18 13:05:06 +02:00
tick(t: number = performance.now()) {
if (process.env.NODE_ENV === 'development') {
if (!(this instanceof Ticker)) {
throw new Error(
'ticker.tick must be called while bound to the ticker. As in, "ticker.tick(time)" or "requestAnimationFrame((t) => ticker.tick(t))" for performance.',
)
}
}
this.__ticks++
2021-06-18 13:05:06 +02:00
this._ticking = true
this._timeAtCurrentTick = t
for (const v of this._scheduledForNextTick) {
this._scheduledForThisOrNextTick.add(v)
}
2021-06-18 13:05:06 +02:00
this._scheduledForNextTick.clear()
this._tick(0)
this._ticking = false
}
private _tick(iterationNumber: number): void {
const time = this.time
if (iterationNumber > 10) {
console.warn('_tick() recursing for 10 times')
}
if (iterationNumber > 100) {
throw new Error(`Maximum recursion limit for _tick()`)
}
const oldSet = this._scheduledForThisOrNextTick
this._scheduledForThisOrNextTick = new Set()
for (const fn of oldSet) {
2021-06-18 13:05:06 +02:00
fn(time)
}
2021-06-18 13:05:06 +02:00
if (this._scheduledForThisOrNextTick.size > 0) {
return this._tick(iterationNumber + 1)
}
}
}