diff --git a/components/primitives/EscenaClaridad.tsx b/components/primitives/EscenaClaridad.tsx new file mode 100644 index 0000000..add4fdb --- /dev/null +++ b/components/primitives/EscenaClaridad.tsx @@ -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(null); + const m2 = useRef(null); + const m3 = useRef(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 ( + + {meshes.map((ref, i) => ( + + ))} + + ); +} + +// ── 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 ( + <> + + + + + ); +} + +// ── 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(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 ( + + ); +}