PageSourceSearch

https://aesthetic.computer/aesthetic.computer/lib/loop.mjs

js aesthetic.computer collected 2026-09-28 12:08:09 UTC 5,268 bytes, 179 lines download raw bytes

1// 🔁 Loop
2
3// These numbers define the budgeted frame-time (max) for CPU and Rendering.
4// The updates will repeat multiple times per frame, but rendering will only
5// ever happen once per display refresh.
6
7const updateFps = /*120*/120; // This is constant and should be used for interpolation.
8let renderFps = 165; // This is a maximum and will vary across environments.
9const updateRate = 1000 / updateFps;
10let renderRate = 1000 / renderFps;
11let updateTime = 0;
12let renderTime = 0;
13let lastNow;
14let input;
15let updateAndRender;
16let paused = false;
17
18// ⏱️ Background Timer Worker
19// When the page is hidden and a piece opts into `background: true`, we use a
20// Web Worker setInterval to keep pumping sim-only frames. Worker timers are
21// NOT throttled by the browser, unlike rAF (paused) or main-thread timers (1/sec).
22let timerWorker = null;
23let backgroundMode = false; // true when hidden AND piece wants background ticks
24let backgroundEnabled = false; // set by bios when piece exports `background: true`
25
26function ensureTimerWorker() {
27  if (timerWorker) return timerWorker;
28  try {
29    let workerUrl;
30    try {
31      workerUrl = new URL("./timer-worker.mjs", import.meta.url);
32    } catch {
33      // In pack mode (blob: URLs), import.meta.url can't resolve relative paths.
34      return null;
35    }
36    timerWorker = new Worker(workerUrl, { type: "module" });
37    timerWorker.onmessage = onTimerWorkerTick;
38  } catch (e) {
39    console.warn("⏱️ Timer worker failed to start:", e);
40  }
41  return timerWorker;
42}
43
44function onTimerWorkerTick(e) {
45  if (e.data.type !== "tick" || paused || !backgroundMode) return;
46  // Drive a sim-only frame (no render).
47  const now = performance.now();
48  const rawDelta = now - lastNow;
49  // Clamp delta: the main thread only wakes ~1/sec when hidden.
50  // Cap to 50ms so we produce at most ~6 sim ticks per wake-up,
51  // avoiding massive bursts that confuse piece timing heuristics.
52  const delta = Math.min(rawDelta, 50);
53  updateTime += delta;
54  lastNow = now;
55
56  let updateTimes = 0;
57  while (updateTime >= updateRate) {
58    updateTimes += 1;
59    updateTime -= updateRate;
60  }
61
62  if (updateTimes > 0) {
63    // needsRender = false → sim only, no paint
64    updateAndRender(false, updateTimes, now);
65  }
66}
67
68function startBackgroundTicks() {
69  if (backgroundMode) return;
70  backgroundMode = true;
71  // Reset accumulators so the first background tick doesn't catch up
72  updateTime = 0;
73  lastNow = performance.now();
74  const w = ensureTimerWorker();
75  if (w) w.postMessage({ type: "start", rate: 8 }); // ~120fps
76}
77
78function stopBackgroundTicks() {
79  if (!backgroundMode) return;
80  backgroundMode = false;
81  if (timerWorker) timerWorker.postMessage({ type: "stop" });
82}
83
84// Input runs once per loop.
85// Update runs multiple times.
86// Render runs once if enough time has passed.
87function loop(now, XR = false) {
88  if (paused) {
89    lastNow = now;
90    if (!XR) window.requestAnimationFrame(loop);
91    return;
92  }
93
94  input(now);
95
96  const delta = now - lastNow;
97
98  updateTime += delta;
99  renderTime += delta;
100  lastNow = now;
101
102  let updateTimes = 0;
103
104  while (updateTime >= updateRate) {
105    updateTimes += 1;
106    updateTime -= updateRate;
107  }
108
109  let needsRender = false;
110
111  if (renderTime >= renderRate) {
112    needsRender = true;
113    renderTime -= renderRate;
114  }
115
116  updateAndRender(needsRender, updateTimes, now);
117
118  // We don't use requestAnimationframe when
119  // running the main loop in an VR/AR/XR environment.
120  if (!XR) window.requestAnimationFrame(loop);
121}
122
123// Handle visibility changes:
124// - When hidden + backgroundEnabled → start timer worker for sim-only frames
125// - When visible → stop timer worker, reset accumulators to prevent catch-up
126document.addEventListener("visibilitychange", function () {
127  if (document.hidden) {
128    if (backgroundEnabled) {
129      startBackgroundTicks();
130    }
131  } else {
132    stopBackgroundTicks();
133    updateTime = 0;
134    renderTime = 0; // Also reset render time to prevent catch-up rendering
135    lastNow = performance.now();
136  }
137});
138
139// Allow bios to enable/disable background mode for the current piece.
140function setBackgroundEnabled(enabled) {
141  backgroundEnabled = !!enabled;
142  // If we're already hidden and background just got enabled, start ticking.
143  if (backgroundEnabled && document.hidden) {
144    startBackgroundTicks();
145  } else if (!backgroundEnabled) {
146    stopBackgroundTicks();
147  }
148}
149
150// Start the loop.
151function start(inputFun, updateAndRenderFun) {
152  input = inputFun;
153  updateAndRender = updateAndRenderFun;
154  lastNow = performance.now();
155  paused = false;
156  window.requestAnimationFrame(loop);
157}
158
159function pause() {
160  paused = true;
161}
162
163function resume() {
164  paused = false;
165  lastNow = performance.now();
166  updateTime = 0;
167}
168
169// Update the frame rate.
170function frameRate(n = 165) {
171  if (renderFps === n) return; // Skip if framerate hasn't changed
172  renderFps = n;
173  if (renderFps !== 165) console.log("🎞️ FPS:", renderFps);
174  renderRate = 1000 / renderFps;
175  renderTime = 0;
176}
177
178export const mainLoop = loop;
179export { start, frameRate, pause, resume, setBackgroundEnabled };

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.