Projetos sobem sobre o palco do hero e o celular tem hero de uma tela sem mergulho; passagens com animation-timeline em CSS, sem GSAP nem motion; troca da vitrine sem texto duplicado; moldura de celular só com pôster e scrub reencodado em 960 px (9,4 para 5,2 MB); 3D monta na primeira interação e para quando parado; transição entre páginas do mesmo segmento; foco em anel duplo; texto de 15 px no celular; código morto removido.
66 lines
1.9 KiB
TypeScript
66 lines
1.9 KiB
TypeScript
"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;
|
|
}
|