"use client"; import { Canvas, useFrame, useThree } from "@react-three/fiber"; import { Component, type ReactNode, type RefObject, useEffect, useMemo, useRef, } from "react"; import * as THREE from "three"; export type HeroFieldSceneProps = { progressRef: RefObject; visible: boolean; onReady: () => void; onError: () => void; }; /* * Três camadas de pontos (interface, integrações, dados) ligadas por colunas de partículas que sobem * como dados. Na entrada, os pontos saem de uma nuvem e se organizam; ao rolar, as camadas se afastam, * ficam de frente e a câmera atravessa as três até o escuro da próxima seção. * Tudo acontece no vertex shader: o JavaScript só atualiza meia dúzia de uniforms por quadro. */ const RADIUS = 2.35; function buildField(dense: boolean) { const grid = dense ? 76 : 46; const columns = dense ? 18 : 10; const perColumn = dense ? 64 : 40; const base: number[] = []; const kind: number[] = []; const step = (RADIUS * 2) / (grid - 1); for (const layer of [-1, 0, 1]) { for (let i = 0; i < grid; i++) { for (let j = 0; j < grid; j++) { // leve desalinhamento: quebra o moiré da grade perfeita const x = -RADIUS + (i + (Math.random() - 0.5) * 0.35) * step; const z = -RADIUS + (j + (Math.random() - 0.5) * 0.35) * step; if (Math.hypot(x, z) > RADIUS) continue; base.push(x, layer, z); kind.push(0); } } } for (let c = 0; c < columns; c++) { const angle = c * 2.39996 + 0.4; // ângulo áureo: colunas espalhadas sem se amontoar const r = Math.sqrt((c + 0.5) / columns) * RADIUS * 0.78; const x = Math.round((Math.cos(angle) * r) / step) * step; const z = Math.round((Math.sin(angle) * r) / step) * step; for (let k = 0; k < perColumn; k++) { base.push(x, 0, z); kind.push(1); } for (const layer of [-1, 0, 1]) { base.push(x, layer, z); kind.push(2); } } const count = kind.length; const scatter = new Float32Array(count * 3); const rand = new Float32Array(count * 4); for (let n = 0; n < count; n++) { const u = Math.random() * 2 - 1; const t = Math.random() * Math.PI * 2; const s = Math.sqrt(1 - u * u); const d = 4 + Math.random() * 7; scatter.set([Math.cos(t) * s * d, u * d, Math.sin(t) * s * d], n * 3); rand.set([Math.random(), Math.random(), Math.random(), Math.random()], n * 4); } const geometry = new THREE.BufferGeometry(); geometry.setAttribute("position", new THREE.Float32BufferAttribute(base, 3)); geometry.setAttribute("aScatter", new THREE.BufferAttribute(scatter, 3)); geometry.setAttribute("aRand", new THREE.BufferAttribute(rand, 4)); geometry.setAttribute("aKind", new THREE.Float32BufferAttribute(kind, 1)); geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(), 12); return geometry; } const vertexShader = /* glsl */ ` uniform float uTime; uniform float uIntro; uniform float uSpread; uniform float uScroll; uniform float uScale; uniform float uFocus; uniform float uFade; uniform vec2 uMouse; uniform float uMouseAmt; attribute vec3 aScatter; attribute vec4 aRand; attribute float aKind; varying vec3 vColor; varying float vAlpha; varying float vSoft; const vec3 PAPER = vec3(0.957, 0.949, 0.929); const vec3 LIME = vec3(0.8, 1.0, 0.0); void main() { float layer = position.y; float isPlane = 1.0 - step(0.5, aKind); float isStream = step(0.5, aKind) * (1.0 - step(1.5, aKind)); float isNode = step(1.5, aKind); // colunas: cada partícula sobe da camada de dados até a interface e recomeça float flow = fract(uTime * (0.08 + aRand.x * 0.07) + aRand.y); layer = mix(layer, flow * 2.0 - 1.0, isStream); vec3 p = vec3(position.x, layer * uSpread, position.z); float wave = sin(position.x * 1.7 + uTime * 0.55 + layer * 2.1) * cos(position.z * 1.3 - uTime * 0.4); p.y += wave * (0.045 + uScroll * 0.25) * isPlane; p.xz += (aRand.zw - 0.5) * 0.05 * isStream; // cursor: uma onda que levanta a malha e acende os pontos em verde float d = length(position.xz - uMouse); float lift = exp(-d * d * 2.2) * uMouseAmt; p.y += (lift * 0.32 + sin(d * 7.0 - uTime * 3.5) * lift * 0.05) * isPlane; // entrada: cada ponto sai da nuvem com atraso próprio float t = clamp((uIntro - aRand.z * 0.4) / 0.6, 0.0, 1.0); float e = 1.0 - pow(1.0 - t, 4.0); p = mix(aScatter, p, e); vec4 mv = modelViewMatrix * vec4(p, 1.0); gl_Position = projectionMatrix * mv; float depth = -mv.z; float size = 0.021 * isPlane * (0.7 + aRand.w * 0.6) + 0.024 * isStream + 0.07 * isNode; float blur = clamp(abs(depth - uFocus) * 0.32, 0.0, 1.6); gl_PointSize = min(size * uScale / max(depth, 0.05) * (1.0 + blur * 1.4), 64.0); // cada camada é um disco: miolo que esmaece para a borda e um aro fino que desenha o contorno float radial = length(position.xz); float edge = 1.0 - smoothstep(RADIUS_IN, RADIUS_OUT, radial); float rim = smoothstep(RADIUS_OUT - 0.16, RADIUS_OUT - 0.04, radial); float alpha = isPlane * ((0.5 + 0.2 * step(0.5, position.y)) * edge + rim * 0.75) + isStream * 0.55 + isNode; alpha *= (0.25 + 0.75 * e) / (1.0 + blur * 1.6); alpha *= smoothstep(0.25, 1.6, depth) * uFade; vAlpha = alpha; vSoft = clamp(0.35 + blur * 0.45, 0.35, 1.0); vColor = mix(PAPER, LIME, clamp(isStream + isNode + (lift * 1.6 + rim * 0.55) * isPlane, 0.0, 1.0)); } ` .replaceAll("RADIUS_IN", (RADIUS * 0.62).toFixed(2)) .replaceAll("RADIUS_OUT", RADIUS.toFixed(2)); const fragmentShader = /* glsl */ ` varying vec3 vColor; varying float vAlpha; varying float vSoft; void main() { float r = length(gl_PointCoord - 0.5) * 2.0; float a = 1.0 - smoothstep(1.0 - vSoft, 1.0, r); if (a <= 0.0) discard; gl_FragColor = vec4(vColor, a * vAlpha); } `; 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 Field({ progressRef, onReady, onError }: HeroFieldSceneProps) { const { gl, camera, size } = useThree(); const group = useRef(null); const dense = useMemo( () => window.innerWidth >= 900 && (navigator.hardwareConcurrency ?? 4) >= 4, [], ); const geometry = useMemo(() => buildField(dense), [dense]); const material = useMemo( () => new THREE.ShaderMaterial({ vertexShader, fragmentShader, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, uniforms: { uTime: { value: 0 }, uIntro: { value: 0 }, uSpread: { value: 1.05 }, uScroll: { value: 0 }, uScale: { value: 1 }, uFocus: { value: 9 }, uFade: { value: 1 }, uMouse: { value: new THREE.Vector2(9, 9) }, uMouseAmt: { value: 0 }, }, }), [], ); const state = useRef({ progress: 0, pointer: new THREE.Vector2(), lastMove: -10, mouse: new THREE.Vector2(), tilt: new THREE.Vector2(), frames: 0, }); const helpers = useMemo( () => ({ ray: new THREE.Raycaster(), plane: new THREE.Plane(), hit: new THREE.Vector3(), }), [], ); useEffect(() => { const onMove = (event: PointerEvent) => { if (event.pointerType !== "mouse") return; const s = state.current; s.pointer.set( (event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1, ); s.lastMove = performance.now() / 1000; }; const onLost = (event: Event) => { event.preventDefault(); onError(); }; window.addEventListener("pointermove", onMove, { passive: true }); gl.domElement.addEventListener("webglcontextlost", onLost); return () => { window.removeEventListener("pointermove", onMove); gl.domElement.removeEventListener("webglcontextlost", onLost); geometry.dispose(); material.dispose(); }; }, [gl, geometry, material, onError]); useFrame((frame, delta) => { const g = group.current; if (!g) return; const dt = Math.min(delta, 1 / 20); const s = state.current; const u = material.uniforms; const time = frame.clock.elapsedTime; // rolagem amortecida: o mergulho segue o dedo com um leve atraso de câmera s.progress += ((progressRef.current ?? 0) - s.progress) * (1 - Math.exp(-dt * 5)); const p = s.progress; const wide = size.width / size.height > 1.1; u.uTime.value = time; u.uIntro.value = Math.min(1, u.uIntro.value + dt / 2.6); u.uScroll.value = p; u.uSpread.value = 1.05 + 1.3 * smooth(0, 0.55, p); u.uFade.value = 1 - smooth(0.94, 1, p); u.uScale.value = (gl.getPixelRatio() * size.height * 0.5) / Math.tan(THREE.MathUtils.degToRad((camera as THREE.PerspectiveCamera).fov / 2)); // sem mouse (toque, ou mouse parado), um cursor fantasma percorre a malha const mouseActive = time - s.lastMove < 2.5 && s.lastMove > 0 ? 1 : 0; s.tilt.lerp(mouseActive ? s.pointer : new THREE.Vector2(), 1 - Math.exp(-dt * 3)); const face = smooth(0.04, 0.6, p); g.rotation.set( 0.42 + (Math.PI / 2 - 0.42) * face - s.tilt.y * 0.08, time * 0.045 + s.tilt.x * 0.18, 0, ); const settle = 1 - smooth(0, 0.4, p); g.position.set(wide ? 1.85 * settle : 0, wide ? -0.15 * settle : -1.25 * settle, 0); const dive = smooth(0.22, 1, p); camera.position.z = 9 - 12.5 * dive * dive; u.uFocus.value = Math.max(1.5, camera.position.z - g.position.z); g.updateMatrixWorld(); if (mouseActive) { helpers.ray.setFromCamera(s.pointer, camera); helpers.plane .set(new THREE.Vector3(0, 1, 0), -u.uSpread.value) .applyMatrix4(g.matrixWorld); if (helpers.ray.ray.intersectPlane(helpers.plane, helpers.hit)) { g.worldToLocal(helpers.hit); s.mouse.lerp(new THREE.Vector2(helpers.hit.x, helpers.hit.z), 1 - Math.exp(-dt * 10)); } } else { s.mouse.lerp( new THREE.Vector2(Math.sin(time * 0.31) * 1.5, Math.sin(time * 0.23 + 1.3) * 1.3), 1 - Math.exp(-dt * 1.5), ); } u.uMouse.value.copy(s.mouse); const targetAmt = (mouseActive ? 1 : 0.55) * (1 - smooth(0.05, 0.3, p)); u.uMouseAmt.value += (targetAmt - u.uMouseAmt.value) * (1 - Math.exp(-dt * 4)); if (++s.frames === 2) onReady(); }); return ( ); } class SceneBoundary extends Component< { children: ReactNode; onError: () => void }, { failed: boolean } > { state = { failed: false }; static getDerivedStateFromError() { return { failed: true }; } componentDidCatch() { this.props.onError(); } render() { return this.state.failed ? null : this.props.children; } } export default function HeroFieldScene(props: HeroFieldSceneProps) { return ( ); }