A cena passa de discos abstratos para formas que um leigo reconhece. A camera enquadra cada camada enquanto a legenda em linguagem simples explica o que e, e so entao atravessa para a proxima. Co-Authored-By: Claude Opus 5.5 <[email protected]>
501 lines
19 KiB
TypeScript
501 lines
19 KiB
TypeScript
"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<number>;
|
||
/** etiquetas em HTML presas às camadas (tela, integrações, dados), posicionadas a cada quadro */
|
||
labelsRef: RefObject<(HTMLElement | null)[]>;
|
||
visible: boolean;
|
||
onReady: () => void;
|
||
onError: () => void;
|
||
};
|
||
|
||
/*
|
||
* Um sistema desenhado com pontos, em três camadas que um leigo reconhece:
|
||
* em cima, a TELA de um aplicativo (janela, título, botão, cards); no meio, as INTEGRAÇÕES (sistemas
|
||
* ligados a uma central, com mensagens correndo pelas linhas); embaixo, os DADOS (uma tabela).
|
||
* Colunas de partículas sobem da tabela até a tela: a informação chegando a quem usa.
|
||
* 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 uma a uma (as legendas de HeroField explicam cada camada nessa hora).
|
||
* Tudo se move no vertex shader; o JavaScript só atualiza alguns uniforms por quadro.
|
||
*/
|
||
|
||
type Layer = 1 | 0 | -1; // 1 = tela, 0 = integrações, -1 = dados
|
||
type PointStyle = { tone?: number; size?: number; alpha?: number };
|
||
|
||
class FieldBuilder {
|
||
base: number[] = [];
|
||
to: number[] = [];
|
||
move: number[] = [];
|
||
tone: number[] = [];
|
||
size: number[] = [];
|
||
alpha: number[] = [];
|
||
constructor(private density: number) {}
|
||
|
||
point(x: number, y: number, z: number, style: PointStyle = {}, to?: [number, number, number]) {
|
||
this.base.push(x, y, z);
|
||
this.to.push(...(to ?? [x, y, z]));
|
||
this.move.push(to ? 1 : 0);
|
||
this.tone.push(style.tone ?? 0);
|
||
this.size.push(style.size ?? 0.02);
|
||
this.alpha.push(style.alpha ?? 0.7);
|
||
}
|
||
line(y: Layer, x0: number, z0: number, x1: number, z1: number, step: number, style?: PointStyle) {
|
||
const s = step * this.density;
|
||
const n = Math.max(1, Math.round(Math.hypot(x1 - x0, z1 - z0) / s));
|
||
for (let i = 0; i <= n; i++) this.point(x0 + ((x1 - x0) * i) / n, y, z0 + ((z1 - z0) * i) / n, style);
|
||
}
|
||
rect(y: Layer, x0: number, z0: number, x1: number, z1: number, step: number, style?: PointStyle) {
|
||
this.line(y, x0, z0, x1, z0, step, style);
|
||
this.line(y, x1, z0, x1, z1, step, style);
|
||
this.line(y, x1, z1, x0, z1, step, style);
|
||
this.line(y, x0, z1, x0, z0, step, style);
|
||
}
|
||
fill(y: Layer, x0: number, z0: number, x1: number, z1: number, step: number, style?: PointStyle) {
|
||
const s = step * this.density;
|
||
for (let x = x0; x <= x1 + 1e-6; x += s)
|
||
for (let z = z0; z <= z1 + 1e-6; z += s)
|
||
this.point(x + (Math.random() - 0.5) * s * 0.3, y, z + (Math.random() - 0.5) * s * 0.3, style);
|
||
}
|
||
ring(y: Layer, cx: number, cz: number, r: number, step: number, style?: PointStyle) {
|
||
const n = Math.max(8, Math.round((Math.PI * 2 * r) / (step * this.density)));
|
||
for (let i = 0; i < n; i++) {
|
||
const a = (i / n) * Math.PI * 2;
|
||
this.point(cx + Math.cos(a) * r, y, cz + Math.sin(a) * r, style);
|
||
}
|
||
}
|
||
disc(y: Layer, cx: number, cz: number, r: number, step: number, style?: PointStyle) {
|
||
for (let rr = r; rr > 0.005; rr -= step * this.density) this.ring(y, cx, cz, rr, step, style);
|
||
this.point(cx, y, cz, style);
|
||
}
|
||
/** partículas que correm de a até b (a posição é calculada no shader) */
|
||
flow(a: [number, number, number], b: [number, number, number], count: number, style?: PointStyle) {
|
||
const n = Math.max(2, Math.round(count / this.density));
|
||
for (let i = 0; i < n; i++) this.point(...a, style, b);
|
||
}
|
||
}
|
||
|
||
// z negativo = topo da tela quando a camada fica de frente para a câmera
|
||
function drawScreen(f: FieldBuilder) {
|
||
const y: Layer = 1;
|
||
f.fill(y, -2.1, -1.4, 2.1, 1.4, 0.1, { alpha: 0.1 }); // superfície da tela
|
||
f.rect(y, -2.1, -1.4, 2.1, 1.4, 0.026, { alpha: 0.95 });
|
||
f.line(y, -2.1, -1.05, 2.1, -1.05, 0.03, { alpha: 0.6 }); // barra do navegador
|
||
for (const x of [-1.9, -1.72, -1.54]) f.disc(y, x, -1.23, 0.05, 0.025, { alpha: 0.8 });
|
||
f.rect(y, -1.1, -1.32, 1.3, -1.14, 0.035, { alpha: 0.45 }); // endereço
|
||
f.fill(y, -1.8, -0.82, 0.25, -0.58, 0.03, { alpha: 0.85 }); // título
|
||
f.line(y, -1.8, -0.38, 0.75, -0.38, 0.04, { alpha: 0.5 }); // texto
|
||
f.line(y, -1.8, -0.24, 0.35, -0.24, 0.04, { alpha: 0.5 });
|
||
f.fill(y, -1.8, -0.02, -0.75, 0.2, 0.028, { tone: 1, alpha: 0.95, size: 0.022 }); // botão
|
||
f.rect(y, 0.6, -0.82, 1.8, 0.2, 0.035, { alpha: 0.6 }); // imagem
|
||
f.fill(y, 0.6, -0.82, 1.8, 0.2, 0.08, { alpha: 0.25 });
|
||
for (const [x0, x1] of [
|
||
[-1.8, -0.7],
|
||
[-0.55, 0.55],
|
||
[0.7, 1.8],
|
||
]) {
|
||
f.rect(y, x0, 0.45, x1, 1.2, 0.035, { alpha: 0.55 }); // cards
|
||
f.line(y, x0 + 0.12, 0.62, x1 - 0.3, 0.62, 0.045, { alpha: 0.5 });
|
||
f.line(y, x0 + 0.12, 0.76, x1 - 0.5, 0.76, 0.045, { alpha: 0.35 });
|
||
}
|
||
}
|
||
|
||
/** posições dos sistemas ligados à central; as colunas de dados sobem por eles */
|
||
const NODES: [number, number][] = Array.from({ length: 6 }, (_, k) => {
|
||
const a = (k / 6) * Math.PI * 2 + Math.PI / 6;
|
||
return [Math.cos(a) * 1.7, Math.sin(a) * 1.05];
|
||
});
|
||
|
||
function drawIntegrations(f: FieldBuilder) {
|
||
const y: Layer = 0;
|
||
f.ring(y, 0, 0, 0.34, 0.02, { tone: 1, alpha: 0.95, size: 0.024 }); // central
|
||
f.disc(y, 0, 0, 0.2, 0.035, { tone: 1, alpha: 0.5 });
|
||
NODES.forEach(([x, z], k) => {
|
||
f.ring(y, x, z, 0.2, 0.02, { tone: 1, alpha: 0.9, size: 0.022 });
|
||
f.disc(y, x, z, 0.1, 0.03, { alpha: 0.6 });
|
||
const len = Math.hypot(x, z);
|
||
const ux = x / len;
|
||
const uz = z / len;
|
||
// linha da central até o sistema, e mensagens correndo nos dois sentidos
|
||
f.line(y, ux * 0.38, uz * 0.38, x - ux * 0.24, z - uz * 0.24, 0.06, { alpha: 0.35 });
|
||
f.flow([ux * 0.38, y, uz * 0.38], [x - ux * 0.24, y, z - uz * 0.24], 10, { tone: 1, alpha: 0.95, size: 0.026 });
|
||
f.flow([x - ux * 0.24, y, z - uz * 0.24], [ux * 0.38, y, uz * 0.38], 6, { alpha: 0.9, size: 0.024 });
|
||
const [nx, nz] = NODES[(k + 1) % NODES.length];
|
||
f.line(y, x, z, nx, nz, 0.08, { alpha: 0.15 });
|
||
});
|
||
}
|
||
|
||
function drawData(f: FieldBuilder) {
|
||
const y: Layer = -1;
|
||
const x0 = -2;
|
||
const x1 = 2;
|
||
const z0 = -1.3;
|
||
const z1 = 1.3;
|
||
const cols = [x0, -0.9, 0.2, 1.1, x1];
|
||
f.rect(y, x0, z0, x1, z1, 0.03, { alpha: 0.8 });
|
||
f.fill(y, x0, z0, x1, z0 + 0.32, 0.045, { tone: 0.6, alpha: 0.45 }); // cabeçalho da tabela
|
||
for (let z = z0 + 0.32; z < z1; z += 0.33) f.line(y, x0, z, x1, z, 0.04, { alpha: 0.4 });
|
||
for (const x of cols.slice(1, -1)) f.line(y, x, z0, x, z1, 0.04, { alpha: 0.4 });
|
||
for (let z = z0 + 0.48; z < z1; z += 0.33) {
|
||
for (let c = 0; c < cols.length - 1; c++) {
|
||
const start = cols[c] + 0.12;
|
||
const width = (cols[c + 1] - cols[c] - 0.24) * (0.35 + Math.random() * 0.6);
|
||
f.line(y, start, z, start + width, z, 0.045, { alpha: 0.55 }); // valores nas células
|
||
}
|
||
}
|
||
}
|
||
|
||
function buildField(dense: boolean) {
|
||
const f = new FieldBuilder(dense ? 1 : 1.5);
|
||
drawScreen(f);
|
||
drawIntegrations(f);
|
||
drawData(f);
|
||
// colunas: a informação sobe dos dados, passa pelos sistemas e chega à tela
|
||
for (const [x, z] of [[0, 0] as [number, number], ...NODES])
|
||
f.flow([x, -1, z], [x, 1, z], 40, { tone: 1, alpha: 0.6, size: 0.022 });
|
||
|
||
const count = f.move.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(f.base, 3));
|
||
geometry.setAttribute("aTo", new THREE.Float32BufferAttribute(f.to, 3));
|
||
geometry.setAttribute("aMove", new THREE.Float32BufferAttribute(f.move, 1));
|
||
geometry.setAttribute("aTone", new THREE.Float32BufferAttribute(f.tone, 1));
|
||
geometry.setAttribute("aSize", new THREE.Float32BufferAttribute(f.size, 1));
|
||
geometry.setAttribute("aAlpha", new THREE.Float32BufferAttribute(f.alpha, 1));
|
||
geometry.setAttribute("aScatter", new THREE.BufferAttribute(scatter, 3));
|
||
geometry.setAttribute("aRand", new THREE.BufferAttribute(rand, 4));
|
||
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 aTo;
|
||
attribute float aMove;
|
||
attribute float aTone;
|
||
attribute float aSize;
|
||
attribute float aAlpha;
|
||
attribute vec3 aScatter;
|
||
attribute vec4 aRand;
|
||
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() {
|
||
// partículas em movimento correm de position até aTo e recomeçam
|
||
float f = fract(uTime * (0.1 + aRand.x * 0.08) + aRand.y);
|
||
vec3 b = mix(position, aTo, aMove * f);
|
||
vec3 p = vec3(b.x, b.y * uSpread, b.z);
|
||
float still = 1.0 - aMove;
|
||
|
||
float wave = sin(b.x * 1.7 + uTime * 0.55 + b.y * 2.1) * cos(b.z * 1.3 - uTime * 0.4);
|
||
p.y += wave * (0.02 + uScroll * 0.2) * still;
|
||
|
||
// cursor: uma onda que levanta a camada e acende os pontos em verde
|
||
float d = length(b.xz - uMouse);
|
||
float lift = exp(-d * d * 2.2) * uMouseAmt * still;
|
||
p.y += lift * 0.3 + sin(d * 7.0 - uTime * 3.5) * lift * 0.05;
|
||
|
||
// 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 blur = clamp(abs(depth - uFocus) * 0.3, 0.0, 1.6);
|
||
float size = aSize * (0.8 + aRand.w * 0.4);
|
||
gl_PointSize = min(size * uScale / max(depth, 0.05) * (1.0 + blur * 1.3), 64.0);
|
||
|
||
float alpha = aAlpha * mix(1.0, sin(f * 3.14159), aMove);
|
||
alpha *= (0.25 + 0.75 * e) / (1.0 + blur * 1.5);
|
||
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(aTone + lift * 1.6, 0.0, 1.0));
|
||
}
|
||
`;
|
||
|
||
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);
|
||
};
|
||
|
||
/** Interpola entre quadros-chave [progresso, valor] com suavização em cada trecho. */
|
||
function keyframes(frames: readonly (readonly [number, number])[], p: number) {
|
||
if (p <= frames[0][0]) return frames[0][1];
|
||
for (let i = 1; i < frames.length; i++) {
|
||
const [p1, v1] = frames[i];
|
||
if (p <= p1) {
|
||
const [p0, v0] = frames[i - 1];
|
||
return v0 + (v1 - v0) * smooth(p0, p1, p);
|
||
}
|
||
}
|
||
return frames[frames.length - 1][1];
|
||
}
|
||
|
||
/** Distância entre as camadas no fim do mergulho: cada uma vira um "cômodo" que a câmera visita. */
|
||
const SPREAD = 5.5;
|
||
/*
|
||
* Coreografia da câmera (z): afasta para enquadrar a tela inteira, atravessa, enquadra as integrações,
|
||
* atravessa, enquadra os dados, atravessa. As legendas de Hero.tsx (--from/--to em globals.css) seguem
|
||
* estes mesmos intervalos: tela 0,26–0,46; integrações 0,52–0,71; dados 0,76–0,92.
|
||
*/
|
||
const CAMERA_Z = [
|
||
[0, 9],
|
||
[0.28, SPREAD + 5.2],
|
||
[0.44, SPREAD + 4.5],
|
||
[0.5, SPREAD - 0.3],
|
||
[0.55, 5.2],
|
||
[0.69, 4.5],
|
||
[0.74, -0.3],
|
||
[0.78, -SPREAD + 5.2],
|
||
[0.89, -SPREAD + 4.5],
|
||
[0.95, -SPREAD - 0.3],
|
||
[1, -SPREAD - 2],
|
||
] as const;
|
||
|
||
// ponto de cada camada onde a etiqueta se prende (canto direito da frente), na ordem tela, integrações, dados
|
||
const LABEL_ANCHORS: [number, Layer, number][] = [
|
||
[2.1, 1, 1.4],
|
||
[1.95, 0, 0.9],
|
||
[2, -1, 1.3],
|
||
];
|
||
|
||
function Field({ progressRef, labelsRef, onReady, onError }: HeroFieldSceneProps) {
|
||
const { gl, camera, size } = useThree();
|
||
const group = useRef<THREE.Group>(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(),
|
||
anchor: new THREE.Vector3(),
|
||
zero: new THREE.Vector2(),
|
||
target: new THREE.Vector2(),
|
||
}),
|
||
[],
|
||
);
|
||
|
||
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;
|
||
// as camadas só se afastam enquanto ficam de frente: inclinadas e afastadas sairiam do quadro
|
||
const face = smooth(0.02, 0.28, p);
|
||
u.uSpread.value = 1.05 + (SPREAD - 1.05) * face * face;
|
||
u.uFade.value = 1 - smooth(0.93, 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 tela
|
||
const mouseActive = time - s.lastMove < 2.5 && s.lastMove > 0 ? 1 : 0;
|
||
s.tilt.lerp(mouseActive ? s.pointer : helpers.zero, 1 - Math.exp(-dt * 3));
|
||
|
||
// em repouso, vista de três quartos que balança devagar; ao mergulhar, as camadas ficam de frente e retas
|
||
g.rotation.set(
|
||
0.42 + (Math.PI / 2 - 0.42) * face - s.tilt.y * 0.08,
|
||
(-0.45 + Math.sin(time * 0.2) * 0.08) * (1 - face) + s.tilt.x * 0.15 * (1 - face),
|
||
0,
|
||
);
|
||
const settle = 1 - smooth(0, 0.28, p);
|
||
g.position.set(wide ? 1.7 * settle : 0, wide ? -0.1 * settle : -1.25 * settle, 0);
|
||
camera.position.z = keyframes(CAMERA_Z, p);
|
||
// foco na próxima camada à frente da câmera (as de trás desfocam como em uma lente)
|
||
const spread = u.uSpread.value;
|
||
const ahead = [spread, 0, -spread].find((z) => z < camera.position.z - 0.2) ?? -spread;
|
||
u.uFocus.value = Math.max(1.5, camera.position.z - ahead);
|
||
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(helpers.target.set(helpers.hit.x, helpers.hit.z), 1 - Math.exp(-dt * 10));
|
||
}
|
||
} else {
|
||
s.mouse.lerp(
|
||
helpers.target.set(Math.sin(time * 0.31) * 1.5, Math.sin(time * 0.23 + 1.3) * 1.1),
|
||
1 - Math.exp(-dt * 1.5),
|
||
);
|
||
}
|
||
u.uMouse.value.copy(s.mouse);
|
||
const targetAmt = (mouseActive ? 0.8 : 0.4) * (1 - smooth(0.05, 0.3, p));
|
||
u.uMouseAmt.value += (targetAmt - u.uMouseAmt.value) * (1 - Math.exp(-dt * 4));
|
||
|
||
// etiquetas: projeta o canto de cada camada na tela e as esconde quando o mergulho começa
|
||
const labels = labelsRef.current ?? [];
|
||
const shown = smooth(0.75, 1, u.uIntro.value) * (1 - smooth(0.04, 0.16, p));
|
||
LABEL_ANCHORS.forEach(([x, layer, z], index) => {
|
||
const label = labels[index];
|
||
if (!label) return;
|
||
helpers.anchor.set(x, layer * u.uSpread.value, z).applyMatrix4(g.matrixWorld).project(camera);
|
||
const px = ((helpers.anchor.x + 1) / 2) * size.width;
|
||
const py = ((1 - helpers.anchor.y) / 2) * size.height;
|
||
label.style.transform = `translate3d(${px.toFixed(1)}px, ${py.toFixed(1)}px, 0)`;
|
||
label.style.opacity = shown.toFixed(3);
|
||
});
|
||
|
||
if (++s.frames === 2) onReady();
|
||
});
|
||
|
||
return (
|
||
<group ref={group}>
|
||
<points geometry={geometry} material={material} frustumCulled={false} />
|
||
</group>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<SceneBoundary onError={props.onError}>
|
||
<Canvas
|
||
aria-hidden="true"
|
||
style={{ width: "100%", height: "100%" }}
|
||
camera={{ fov: 35, position: [0, 0, 9], near: 0.05, far: 40 }}
|
||
dpr={[1, 1.75]}
|
||
frameloop={props.visible ? "always" : "never"}
|
||
gl={{ alpha: true, antialias: false, powerPreference: "high-performance" }}
|
||
>
|
||
<Field {...props} />
|
||
</Canvas>
|
||
</SceneBoundary>
|
||
);
|
||
}
|