@hypernym/frame
Version:
Universal Frame Manager.
146 lines (144 loc) • 4.04 kB
JavaScript
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(state2) {
if (isRunning) {
flushNextFrame = true;
return;
}
isRunning = true;
[thisFrame, nextFrame] = [nextFrame, thisFrame];
thisFrame.forEach(runProcess);
thisFrame.clear();
isRunning = false;
if (flushNextFrame) {
flushNextFrame = false;
phase.add(state2);
}
},
delete(process) {
nextFrame.delete(process);
if (loops.has(process)) activeLoops--;
loops.delete(process);
}
};
return phase;
};
const runFrame = () => {
if (isStopped) return;
const now = performance.now();
const time = 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;
}
};
}
export { createFrame };