perf(movimento): uma transição, 3D leve e vídeo certo por aparelho

Passagens com fade e 32 px, cena anterior termina em opacidade 0, sem visibility nem clip-path. 3D do hero como fundo ambiente, sob demanda e desligado em toque, aparelho fraco, saveData e movimento reduzido. Lenis fora do toque e salto direto em âncoras distantes. Vitrine só de clientes, até 3 capítulos, fixa só no desktop e carrossel no celular; vídeo só do projeto em cena com preload metadata. Rótulos mono a partir de 12 px e limão só no contato.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 03:32:23 -03:00
1 parent 8d07b8c6bb
commit c9a0b80656
48 files changed
+7210 -1241

No files matched your search

+16 -26
View File
@@ -2,19 +2,18 @@
import dynamic from "next/dynamic";
import { useCallback, useEffect, useRef, useState } from "react";
import { isLowPowerDevice } from "@/lib/device";
import type { HeroFieldSceneProps } from "./HeroFieldScene";
/** Etiquetas presas às camadas da cena (a cena calcula a posição de cada uma a cada quadro). */
const TAGS = [
["A tela", "o que sua equipe e seus clientes usam"],
["As integrações", "seus sistemas conversando entre si"],
["Os dados", "suas informações organizadas"],
] as const;
const Scene = dynamic<HeroFieldSceneProps>(() => import("./HeroFieldScene"), {
ssr: false,
});
const smooth = (a: number, b: number, x: number) => {
const t = Math.min(1, Math.max(0, (x - a) / (b - a)));
return t * t * (3 - 2 * t);
};
function supportsWebGL2() {
if (typeof window.WebGL2RenderingContext === "undefined") return false;
try {
@@ -27,13 +26,15 @@ function supportsWebGL2() {
}
/**
* Fundo do hero: liga a rolagem do hero à variável CSS --hero-p (0 no topo, 1 no fim do mergulho)
* e monta a cena WebGL só quando a página fica ociosa, com WebGL2 e sem preferência por movimento reduzido.
* Fundo ambiente do hero. Liga a rolagem do hero às variáveis CSS --hero-p (0 no topo, 1 no fim do mergulho)
* e --hero-open (0 em repouso, 1 quando a máscara do globals.css já se abriu para o mergulho), e monta a cena
* WebGL só quando a página fica ociosa e o aparelho aguenta: WebGL2, sem toque (pointer: coarse), com mais de
* 4 núcleos e mais de 4 GB, sem "economizar dados" e sem movimento reduzido. Nos outros casos fica o fundo
* estático leve do CSS (.hero-glow). Sem legendas sobre o 3D: o espaço da direita é das molduras.
*/
export default function HeroField() {
const rootRef = useRef<HTMLDivElement>(null);
const progressRef = useRef(0);
const labelsRef = useRef<(HTMLElement | null)[]>([]);
const [mount, setMount] = useState(false);
const [visible, setVisible] = useState(false);
const [ready, setReady] = useState(false);
@@ -53,8 +54,12 @@ export default function HeroField() {
window.innerHeight * 0.8,
);
const p = Math.min(1, Math.max(0, -rect.top / span));
// depois do mergulho (ou antes dele) a rolagem não muda mais nada: sem reescrever as variáveis CSS
if (p === progressRef.current) return;
progressRef.current = p;
section.style.setProperty("--hero-p", p.toFixed(4));
// a máscara só se abre quando o texto e a coluna da direita já quase saíram (o conteúdo some até p ≈ 0,24)
section.style.setProperty("--hero-open", smooth(0.16, 0.3, p).toFixed(4));
setDone(p >= 0.999);
};
const queue = () => {
@@ -82,7 +87,7 @@ export default function HeroField() {
}, []);
useEffect(() => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
if (isLowPowerDevice()) return;
if (!supportsWebGL2()) return;
// Título, botões e hidratação vêm primeiro; o Three.js entra quando o navegador fica ocioso.
if (typeof window.requestIdleCallback === "function") {
@@ -108,26 +113,11 @@ export default function HeroField() {
{mount && !failed && (
<Scene
progressRef={progressRef}
labelsRef={labelsRef}
visible={visible && !done}
onReady={handleReady}
onError={handleError}
/>
)}
{mount &&
!failed &&
TAGS.map(([title, text], index) => (
<span
key={title}
ref={(element) => {
labelsRef.current[index] = element;
}}
className="hero-tag"
>
<span className="hero-tag-title">{title}</span>
{text}
</span>
))}
</div>
);
}