Files

57 lines
2.0 KiB
TypeScript

/** 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. É o único relógio do cursor (nenhum
* requestAnimationFrame encadeado a cada quadro da tela); quem precisa de mais (entrada, rolagem) 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<typeof setInterval> | 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();
};
}