/** Gestos que mostram alguém olhando para a página: qualquer um deles religa a batida. */ const ACTIVITY = [ "pointermove", "pointerdown", "scroll", "wheel", "touchmove", "keydown", ] as const; export type FramePulseOptions = { /** Quadros por segundo pedidos (padrão 30). */ fps?: number; /** Tempo (ms) sem cursor, rolagem nem teclado depois do qual a batida para de vez (padrão 5000). */ idleMs?: number; }; /** * Batida das cenas WebGL em demanda: pede um quadro (`invalidate`) a no máximo `fps` por segundo, de um único * `setInterval` (fundo ambiente, sem gastar 60 quadros por segundo e sem um segundo laço de requestAnimationFrame ao * lado do da cena). Depois de `idleMs` sem cursor, rolagem nem teclado, a batida para de vez (nenhum intervalo, nenhum * quadro) e o último quadro fica na tela; qualquer movimento a religa. Quem precisa de mais (entrada, rolagem, cursor) * pede o próximo quadro por conta própria, dentro do quadro. Devolve a função que desliga tudo. */ export function startFramePulse( invalidate: () => void, { fps = 30, idleMs = 5000 }: FramePulseOptions = {}, ) { const period = Math.round(1000 / fps); let timer: ReturnType | undefined; let lastActivity = performance.now(); const stopTimer = () => { clearInterval(timer); timer = undefined; }; const beat = () => { // parada por inatividade: sem intervalo até o próximo movimento if (performance.now() - lastActivity > idleMs) stopTimer(); else invalidate(); }; const wake = () => { lastActivity = performance.now(); if (timer === undefined) timer = setInterval(beat, period); }; for (const type of ACTIVITY) window.addEventListener(type, wake, { passive: true }); wake(); invalidate(); return () => { for (const type of ACTIVITY) window.removeEventListener(type, wake); stopTimer(); }; }