@hypernym/frame
Version:
Universal Frame Manager.
170 lines (169 loc) • 4.23 kB
JavaScript
//#region src/index.ts
/**
* Creates a universal `frame` manager.
*
* @example
*
* ```ts
* import { createFrame } from '@hypernym/frame'
*
* const frame = createFrame()
*
* const process = frame.add((state) => console.log(state), { loop: true }) // Adds the process
*
* frame.delete(process) // Deletes a specific process
*
* frame.add((state) => console.log(state), { phase: 1 }) // Adds the process to a specific phase (default is 0)
*
* frame.delete() // Deletes all processes, phases and resets the frame state
* ```
*
* @see [Repository](https://github.com/hypernym-studio/frame)
*/
function createFrame(options = {}) {
let { scheduler = typeof window !== "undefined" ? requestAnimationFrame : () => {}, loop: allowLoop = true, fps = false } = options;
const phases = /* @__PURE__ */ new Map();
let order = [];
let loops = /* @__PURE__ */ new WeakSet();
let activeLoops = 0;
let shouldRun = false;
let isStopped = false;
const maxDeltaTime = 40;
let frameInterval = 1e3 / (fps || 60);
let lastTime = 0;
let lastPauseTime = 0;
let totalPausedTime = 0;
const defaultState = () => ({
delta: 0,
timestamp: 0,
isRunning: false
});
let state = defaultState();
const createPhase = () => {
let thisFrame = /* @__PURE__ */ new Set();
let nextFrame = /* @__PURE__ */ new Set();
let isRunning = false;
let flushNextFrame = false;
const runProcess = (process) => {
if (loops.has(process)) phase.schedule(process);
process(state);
};
const phase = {
schedule(process, { loop, schedule = true } = {}) {
const queue = isRunning && !schedule ? thisFrame : nextFrame;
if (loop && !loops.has(process)) {
loops.add(process);
activeLoops++;
}
queue.add(process);
return process;
},
add(state$1) {
if (isRunning) {
flushNextFrame = true;
return;
}
isRunning = true;
[thisFrame, nextFrame] = [nextFrame, thisFrame];
thisFrame.forEach(runProcess);
thisFrame.clear();
isRunning = false;
if (flushNextFrame) {
flushNextFrame = false;
phase.add(state$1);
}
},
delete(process) {
nextFrame.delete(process);
if (loops.has(process)) activeLoops--;
loops.delete(process);
}
};
return phase;
};
const runFrame = () => {
if (isStopped) return;
const time = performance.now() - totalPausedTime;
shouldRun = activeLoops > 0;
if (fps) {
const delta = time - lastTime;
if (delta < frameInterval) {
if (!isStopped) scheduler(runFrame);
return;
}
lastTime = time - delta % frameInterval;
state.delta = frameInterval;
} else {
state.delta = state.timestamp === 0 ? frameInterval : Math.min(Math.max(time - state.timestamp, 1), maxDeltaTime);
lastTime = time;
}
state.timestamp = time;
state.isRunning = true;
order.forEach((p) => phases.get(p)?.add(state));
state.isRunning = false;
if (shouldRun && allowLoop && !isStopped) scheduler(runFrame);
};
return {
add(process, { loop, phase = 0, schedule = true } = {}) {
let p = phases.get(phase);
if (!p) {
order.push(phase);
order.sort((a, b) => a - b);
p = createPhase();
phases.set(phase, p);
}
if (!shouldRun) {
shouldRun = true;
lastTime = performance.now();
scheduler(runFrame);
}
return p.schedule(process, {
loop,
phase,
schedule
});
},
delete(process) {
if (!process) {
state = defaultState();
phases.clear();
order = [];
loops = /* @__PURE__ */ new WeakSet();
activeLoops = lastTime = lastPauseTime = totalPausedTime = 0;
shouldRun = isStopped = false;
return;
}
phases.forEach((id) => id.delete(process));
},
start() {
if (isStopped && shouldRun) {
isStopped = false;
const now = performance.now();
if (lastPauseTime) {
totalPausedTime += now - lastPauseTime;
lastPauseTime = 0;
}
state.timestamp = lastTime = now - totalPausedTime;
scheduler(runFrame);
}
},
stop() {
if (!isStopped) {
isStopped = true;
lastPauseTime = performance.now();
}
},
get state() {
return state;
},
get fps() {
return fps;
},
set fps(v) {
frameInterval = 1e3 / (v || 60);
fps = v;
}
};
}
//#endregion
export { createFrame };