feat(hero): el astrolabio deja paso a «Las Tres Líneas» — caos, ≡, escalera, crecimiento
This commit is contained in:
1 parent
4e2932292c
commit
97fd0a0224
1 file changed
+209
@@ -0,0 +1,209 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Component, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||||
|
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { usePrefersReducedMotion } from '@/lib/motion';
|
||||||
|
|
||||||
|
// VISUAL CONCEPT: "Las Tres Líneas" — the brand symbol tells the story.
|
||||||
|
// 1. Caos: three golden bars drift tangled, each on its own frequencies.
|
||||||
|
// 2. Claridad: they settle into the "≡" of cre≡er — level, exact, still.
|
||||||
|
// 3. Dirección: together they tilt ~9° and stagger into a staircase.
|
||||||
|
// 4. Crecimiento: each line extends forward (the top one the most) with a
|
||||||
|
// rising golden glow. The scene then rests, barely alive: a slow camera
|
||||||
|
// drift and an almost imperceptible emissive pulse. No loops, no resets.
|
||||||
|
|
||||||
|
const DORADO = new THREE.Color('#C4942E');
|
||||||
|
|
||||||
|
const suave = (t: number) => 1 - Math.pow(1 - t, 3);
|
||||||
|
const tramo = (t: number, desde: number, dura: number) =>
|
||||||
|
Math.min(1, Math.max(0, (t - desde) / dura));
|
||||||
|
|
||||||
|
// Phase timings (seconds), verbatim from the approved preview.
|
||||||
|
const CAOS_DURA = 3.0;
|
||||||
|
const CLARIDAD = { desde: 3.0, dura: 2.2 };
|
||||||
|
const DIRECCION = { desde: 5.2, dura: 2.0 };
|
||||||
|
const CRECIMIENTO = { desde: 7.2, dura: 2.4 };
|
||||||
|
|
||||||
|
// ── Las Tres Líneas ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function TresLineas({ estatico }: { estatico: boolean }) {
|
||||||
|
const time = useRef(estatico ? 10 : 0);
|
||||||
|
const m1 = useRef<THREE.Mesh>(null);
|
||||||
|
const m2 = useRef<THREE.Mesh>(null);
|
||||||
|
const m3 = useRef<THREE.Mesh>(null);
|
||||||
|
const meshes = [m1, m2, m3];
|
||||||
|
|
||||||
|
// Decoupled drift frequencies per bar, so the chaos never syncs.
|
||||||
|
const deriva = useMemo(
|
||||||
|
() => [
|
||||||
|
{ fx: 1.1, fy: 0.9, fz: 0.7, rx: 0.8, ry: 1.1 },
|
||||||
|
{ fx: 1.47, fy: 1.4, fz: 1.4, rx: 1.1, ry: 0.9 },
|
||||||
|
{ fx: 1.84, fy: 1.9, fz: 0.7, rx: 1.4, ry: 0.7 },
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const geometria = useMemo(() => new THREE.BoxGeometry(2.3, 0.3, 0.3), []);
|
||||||
|
const materiales = useMemo(
|
||||||
|
() =>
|
||||||
|
[0, 1, 2].map(
|
||||||
|
() =>
|
||||||
|
new THREE.MeshStandardMaterial({
|
||||||
|
color: DORADO,
|
||||||
|
metalness: 0.8,
|
||||||
|
roughness: 0.25,
|
||||||
|
emissive: DORADO,
|
||||||
|
emissiveIntensity: 0,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Final poses: the "≡" (identity rotation) and the tilted staircase.
|
||||||
|
const qSimbolo = useMemo(() => new THREE.Quaternion().identity(), []);
|
||||||
|
const qEscalera = useMemo(
|
||||||
|
() => new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, -0.16)),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useFrame((_, delta) => {
|
||||||
|
if (!estatico) time.current += delta;
|
||||||
|
const t = time.current;
|
||||||
|
|
||||||
|
const claridad = suave(tramo(t, CLARIDAD.desde, CLARIDAD.dura));
|
||||||
|
const direccion = suave(tramo(t, DIRECCION.desde, DIRECCION.dura));
|
||||||
|
const crecimiento = suave(tramo(t, CRECIMIENTO.desde, CRECIMIENTO.dura));
|
||||||
|
// "Reposo vivo": a slow emissive breathing once the story is told.
|
||||||
|
const pulso = Math.sin(t * 0.8) * 0.03 * crecimiento;
|
||||||
|
|
||||||
|
meshes.forEach((ref, i) => {
|
||||||
|
const mesh = ref.current;
|
||||||
|
if (!mesh) return;
|
||||||
|
const d = deriva[i];
|
||||||
|
|
||||||
|
// Caos: free drift around the frame.
|
||||||
|
const posCaos = new THREE.Vector3(
|
||||||
|
Math.sin(t * d.fx + i * 2.1) * 1.6,
|
||||||
|
Math.cos(t * d.fy + i) * 1.1 + 0.7,
|
||||||
|
Math.sin(t * d.fz + i * 3) * 0.9,
|
||||||
|
);
|
||||||
|
const rotCaos = new THREE.Quaternion().setFromEuler(
|
||||||
|
new THREE.Euler(t * d.rx, t * (d.ry - i * 0.2), t * 0.5),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Claridad: the "≡" of the wordmark.
|
||||||
|
const posSimbolo = new THREE.Vector3(0, 0.7 - i * 0.7, 0);
|
||||||
|
// Dirección: the ascending staircase.
|
||||||
|
const posEscalera = new THREE.Vector3(i * 1.15 - 1.15, 0.35 + i * 0.8, 0);
|
||||||
|
|
||||||
|
mesh.position.copy(
|
||||||
|
posCaos.lerp(posSimbolo, claridad).lerp(posEscalera, direccion),
|
||||||
|
);
|
||||||
|
mesh.quaternion.slerpQuaternions(rotCaos, qSimbolo, claridad);
|
||||||
|
mesh.quaternion.slerp(qEscalera, direccion);
|
||||||
|
|
||||||
|
// Crecimiento: each line extends, the top one the most.
|
||||||
|
mesh.scale.set(
|
||||||
|
1 + direccion * 0.1 + crecimiento * (0.35 + i * 0.35),
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
materiales[i].emissiveIntensity = crecimiento * (0.1 + i * 0.06) + pulso;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{meshes.map((ref, i) => (
|
||||||
|
<mesh key={i} ref={ref} geometry={geometria} material={materiales[i]} />
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cámara — frontal, with the preview's gentle drift ───────────────────────
|
||||||
|
|
||||||
|
const CAMARA: [number, number, number] = [0, 0.9, 8];
|
||||||
|
const MIRA: [number, number, number] = [0, 0.7, 0];
|
||||||
|
|
||||||
|
function Camara({ estatico }: { estatico: boolean }) {
|
||||||
|
const { camera, invalidate } = useThree();
|
||||||
|
const time = useRef(estatico ? 10 : 0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
camera.position.set(...CAMARA);
|
||||||
|
camera.lookAt(...MIRA);
|
||||||
|
invalidate();
|
||||||
|
}, [camera, invalidate]);
|
||||||
|
|
||||||
|
useFrame((_, delta) => {
|
||||||
|
if (estatico) return;
|
||||||
|
time.current += delta;
|
||||||
|
camera.position.set(
|
||||||
|
CAMARA[0] + Math.sin(time.current * 0.25) * 0.5,
|
||||||
|
CAMARA[1],
|
||||||
|
CAMARA[2],
|
||||||
|
);
|
||||||
|
camera.lookAt(...MIRA);
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Iluminación — the three lights of the approved preview ──────────────────
|
||||||
|
|
||||||
|
function Luces() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ambientLight intensity={1.4} color="#ffffff" />
|
||||||
|
<directionalLight position={[4, 8, 5]} intensity={2.2} color="#ffffff" />
|
||||||
|
<pointLight position={[0, 2, 4]} intensity={10} distance={25} color={DORADO} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WebGL Guard ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class SiHayWebGL extends Component<{ children: ReactNode }, { falla: boolean }> {
|
||||||
|
state = { falla: false };
|
||||||
|
static getDerivedStateFromError() { return { falla: true }; }
|
||||||
|
render() { return this.state.falla ? null : this.props.children; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── El Componente ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function EscenaClaridad({ className = '' }: { className?: string }) {
|
||||||
|
const reducido = usePrefersReducedMotion();
|
||||||
|
const contenedor = useRef<HTMLDivElement>(null);
|
||||||
|
const [enVista, setEnVista] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = contenedor.current;
|
||||||
|
if (!el || typeof IntersectionObserver === 'undefined') return;
|
||||||
|
const obs = new IntersectionObserver(([entrada]) => setEnVista(entrada.isIntersecting));
|
||||||
|
obs.observe(el);
|
||||||
|
return () => obs.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const animando = enVista && !reducido;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={contenedor} className={className} aria-hidden="true">
|
||||||
|
<SiHayWebGL>
|
||||||
|
<Canvas
|
||||||
|
dpr={[1, 2]}
|
||||||
|
frameloop={animando ? 'always' : 'demand'}
|
||||||
|
camera={{ position: CAMARA, fov: 38, near: 0.1, far: 100 }}
|
||||||
|
gl={{ antialias: true, alpha: true, powerPreference: 'low-power' }}
|
||||||
|
style={{ background: 'transparent' }}
|
||||||
|
>
|
||||||
|
<Camara estatico={reducido} />
|
||||||
|
<Luces />
|
||||||
|
<TresLineas estatico={reducido} />
|
||||||
|
</Canvas>
|
||||||
|
</SiHayWebGL>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user