"use client"; import { useThree } from "@react-three/fiber"; import { useEffect } from "react"; /** 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; /** * Batida das cenas WebGL em `frameloop="demand"`: enquanto `active`, pede um quadro a no máximo `fps` por * segundo (fundo ambiente, sem gastar 60 quadros por segundo); com `active` falso, nenhum quadro é desenhado. * Depois de `idleMs` sem cursor, rolagem nem teclado, a batida para de vez (nenhum quadro, nenhum * requestAnimationFrame) e o último quadro fica na tela; qualquer movimento a religa. * Quem precisa de mais (entrada, rolagem, cursor) pede o próximo quadro com `invalidate()` dentro do useFrame. */ export default function FramePulse({ active, fps = 30, idleMs = 5000, }: { active: boolean; fps?: number; idleMs?: number; }) { const invalidate = useThree((state) => state.invalidate); useEffect(() => { if (!active) return; const gap = 1000 / fps - 2; let id = 0; let last = 0; let lastActivity = performance.now(); const beat = (now: number) => { // parada por inatividade: sem laço até o próximo movimento if (now - lastActivity > idleMs) { id = 0; return; } id = requestAnimationFrame(beat); if (now - last < gap) return; last = now; invalidate(); }; const wake = () => { lastActivity = performance.now(); if (!id) id = requestAnimationFrame(beat); }; for (const type of ACTIVITY) window.addEventListener(type, wake, { passive: true }); id = requestAnimationFrame(beat); return () => { for (const type of ACTIVITY) window.removeEventListener(type, wake); cancelAnimationFrame(id); }; }, [active, fps, idleMs, invalidate]); return null; }