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:
1 parent
8d07b8c6bb
commit
c9a0b80656
48 files changed
+7210
-1241
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user