feat(hero): campo de particulas WebGL com mergulho guiado pela rolagem e transicao de secoes em CSS
Tres camadas de pontos ligadas por colunas de dados, animadas no vertex shader; o hero fica fixo em telas grandes enquanto a camera atravessa as camadas. Secoes sobem como cartoes via animation-timeline: view() (sem JS). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3d47fe9cc7
commit
0ec0194441
7 files changed
+694
-1001
No files matched your search
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { HeroFieldSceneProps } from "./HeroFieldScene";
|
||||
|
||||
const Scene = dynamic<HeroFieldSceneProps>(() => import("./HeroFieldScene"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
function supportsWebGL2() {
|
||||
if (typeof window.WebGL2RenderingContext === "undefined") return false;
|
||||
try {
|
||||
const context = document.createElement("canvas").getContext("webgl2");
|
||||
context?.getExtension("WEBGL_lose_context")?.loseContext();
|
||||
return context !== null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export default function HeroField() {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const progressRef = useRef(0);
|
||||
const [mount, setMount] = useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [ready, setReady] = useState(false);
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const section = rootRef.current?.closest<HTMLElement>(".hero");
|
||||
if (!section) return;
|
||||
let frame = 0;
|
||||
const update = () => {
|
||||
frame = 0;
|
||||
const rect = section.getBoundingClientRect();
|
||||
const span = Math.max(rect.height - window.innerHeight, window.innerHeight * 0.8);
|
||||
const p = Math.min(1, Math.max(0, -rect.top / span));
|
||||
progressRef.current = p;
|
||||
section.style.setProperty("--hero-p", p.toFixed(4));
|
||||
};
|
||||
const queue = () => {
|
||||
if (!frame) frame = requestAnimationFrame(update);
|
||||
};
|
||||
update();
|
||||
window.addEventListener("scroll", queue, { passive: true });
|
||||
window.addEventListener("resize", queue);
|
||||
|
||||
let intersecting = false;
|
||||
const sync = () => setVisible(intersecting && !document.hidden);
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
intersecting = entry.isIntersecting;
|
||||
sync();
|
||||
});
|
||||
observer.observe(section);
|
||||
document.addEventListener("visibilitychange", sync);
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
window.removeEventListener("scroll", queue);
|
||||
window.removeEventListener("resize", queue);
|
||||
observer.disconnect();
|
||||
document.removeEventListener("visibilitychange", sync);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) 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") {
|
||||
const id = window.requestIdleCallback(() => setMount(true), { timeout: 2500 });
|
||||
return () => window.cancelIdleCallback(id);
|
||||
}
|
||||
const id = setTimeout(() => setMount(true), 1200);
|
||||
return () => clearTimeout(id);
|
||||
}, []);
|
||||
|
||||
const handleReady = useCallback(() => setReady(true), []);
|
||||
const handleError = useCallback(() => setFailed(true), []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
aria-hidden="true"
|
||||
className="hero-field"
|
||||
data-ready={ready && !failed ? "" : undefined}
|
||||
>
|
||||
{mount && !failed && (
|
||||
<Scene
|
||||
progressRef={progressRef}
|
||||
visible={visible}
|
||||
onReady={handleReady}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user