feat(hero): camadas reconheciveis (tela, integracoes, tabela de dados), etiquetas e legendas do mergulho
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]>
This commit is contained in:
1 parent
a3a206fb7a
commit
92c602c0ad
4 files changed
+383
-86
No files matched your search
@@ -4,6 +4,13 @@ import dynamic from "next/dynamic";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
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,
|
||||
});
|
||||
@@ -26,6 +33,7 @@ function supportsWebGL2() {
|
||||
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);
|
||||
@@ -95,11 +103,26 @@ 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>
|
||||
);
|
||||
}
|
||||
@@ -13,54 +13,158 @@ 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;
|
||||
};
|
||||
|
||||
/*
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const RADIUS = 2.35;
|
||||
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 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 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++) {
|
||||
@@ -72,10 +176,14 @@ function buildField(dense: boolean) {
|
||||
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("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.setAttribute("aKind", new THREE.Float32BufferAttribute(kind, 1));
|
||||
geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(), 12);
|
||||
return geometry;
|
||||
}
|
||||
@@ -90,9 +198,13 @@ const vertexShader = /* glsl */ `
|
||||
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;
|
||||
attribute float aKind;
|
||||
varying vec3 vColor;
|
||||
varying float vAlpha;
|
||||
varying float vSoft;
|
||||
@@ -101,24 +213,19 @@ const vertexShader = /* glsl */ `
|
||||
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);
|
||||
// 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;
|
||||
|
||||
// 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);
|
||||
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;
|
||||
|
||||
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;
|
||||
// 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);
|
||||
@@ -129,24 +236,18 @@ const vertexShader = /* glsl */ `
|
||||
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);
|
||||
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);
|
||||
|
||||
// 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);
|
||||
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(isStream + isNode + (lift * 1.6 + rim * 0.55) * isPlane, 0.0, 1.0));
|
||||
vColor = mix(PAPER, LIME, clamp(aTone + lift * 1.6, 0.0, 1.0));
|
||||
}
|
||||
`
|
||||
.replaceAll("RADIUS_IN", (RADIUS * 0.62).toFixed(2))
|
||||
.replaceAll("RADIUS_OUT", RADIUS.toFixed(2));
|
||||
`;
|
||||
|
||||
const fragmentShader = /* glsl */ `
|
||||
varying vec3 vColor;
|
||||
@@ -165,7 +266,48 @@ const smooth = (a: number, b: number, x: number) => {
|
||||
return t * t * (3 - 2 * t);
|
||||
};
|
||||
|
||||
function Field({ progressRef, onReady, onError }: HeroFieldSceneProps) {
|
||||
/** 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(
|
||||
@@ -208,6 +350,9 @@ function Field({ progressRef, onReady, onError }: HeroFieldSceneProps) {
|
||||
ray: new THREE.Raycaster(),
|
||||
plane: new THREE.Plane(),
|
||||
hit: new THREE.Vector3(),
|
||||
anchor: new THREE.Vector3(),
|
||||
zero: new THREE.Vector2(),
|
||||
target: new THREE.Vector2(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
@@ -252,27 +397,31 @@ function Field({ progressRef, onReady, onError }: HeroFieldSceneProps) {
|
||||
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);
|
||||
// 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 malha
|
||||
// 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 : new THREE.Vector2(), 1 - Math.exp(-dt * 3));
|
||||
s.tilt.lerp(mouseActive ? s.pointer : helpers.zero, 1 - Math.exp(-dt * 3));
|
||||
|
||||
const face = smooth(0.04, 0.6, p);
|
||||
// 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,
|
||||
time * 0.045 + s.tilt.x * 0.18,
|
||||
(-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.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);
|
||||
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) {
|
||||
@@ -282,18 +431,31 @@ function Field({ progressRef, onReady, onError }: HeroFieldSceneProps) {
|
||||
.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));
|
||||
s.mouse.lerp(helpers.target.set(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),
|
||||
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 ? 1 : 0.55) * (1 - smooth(0.05, 0.3, p));
|
||||
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();
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user