UNPKG

@hypernym/frame

Version:
170 lines (169 loc) 4.23 kB
//#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 };