"use client"; import dynamic from "next/dynamic"; import { useCallback, useEffect, useRef, useState } from "react"; import { isLowPowerDevice } from "@/lib/device"; import type { HeroFieldSceneProps } from "./HeroFieldScene"; const Scene = dynamic(() => 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 { const context = document.createElement("canvas").getContext("webgl2"); context?.getExtension("WEBGL_lose_context")?.loseContext(); return context !== null; } catch { return false; } } /** * 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(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); // fim do mergulho: a cena já está no escuro e o hero se dissolve; parar o desenho poupa a GPU const [done, setDone] = useState(false); useEffect(() => { const section = rootRef.current?.closest(".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)); // 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 = () => { 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 (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") { 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 ( ); }