# «Las Tres Líneas» — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Reemplazar la escena Three.js del hero ("Astrolabio") por «Las Tres Líneas»: tres barras doradas que pasan de caos → ≡ → escalera → crecimiento, con reposo vivo. **Architecture:** Solo se reescribe el interior de `components/primitives/EscenaClaridad.tsx` (misma ruta, misma API `className`, mismo export). El wrapper (IntersectionObserver, `prefers-reduced-motion`, `SiHayWebGL`, props del `Canvas`) queda idéntico. `ClaridadHero.tsx`, `Hero.tsx` y las 4 pruebas existentes no se tocan. **Tech Stack:** Next.js 14 (App Router, `output: export`), `three@0.185`, `@react-three/fiber@8`, TypeScript, Vitest. **Spec:** `docs/superpowers/specs/2026-08-21-animacion-tres-lineas-design.md` ## Global Constraints - Gestor de paquetes: **pnpm** (nunca npm). Comandos: `pnpm test`, `pnpm lint`, `npx tsc --noEmit`, `pnpm build`. - **Comentarios de código en inglés**; copy/docs/commits en español. - Paleta: dorado `#C4942E` (la escena no usa azul — ya no hay anillos ni sombra). - **Staging del repo tiene cambios ajenos staged** (deletes de `FormaTrabajar`/`Mecanismo`, rename `CLAUDE.md`). Al commitear usa **rutas explícitas** (`git add `), nunca `git add -A` / `git add .`. - Commits de esta implementación **aprobados** por el usuario en la rama `animacion-tres-lineas`. - `prefers-reduced-motion` debe montar la pose final directamente (patrón `estatico` existente). ## Fuente de verdad de la coreografía Los parámetros aprobados son los del **preview del visual companion** (elección de Erik, concepto A). Nota: el geometry es **centrado** (`BoxGeometry(2.3, 0.3, 0.3)`) y crece simétricamente en X — así se ve en el preview aprobado; ignora la mención a "origen en extremo" heredada del código viejo en una frase del spec. --- ### Task 1: Reescribir `EscenaClaridad.tsx` con «Las Tres Líneas» **Files:** - Modify: `components/primitives/EscenaClaridad.tsx` (reescritura completa del interior) - Test (sin edits): `components/primitives/__tests__/EscenaClaridad.test.tsx` **Interfaces:** - Consumes: `usePrefersReducedMotion` de `@/lib/motion` (ya usada), `Canvas`/`useFrame`/`useThree` de `@react-three/fiber`. - Produces: `export function EscenaClaridad({ className = '' }: { className?: string })` — firma idéntica a la actual. `ClaridadHero` y los tests la consumen sin cambios. - [ ] **Step 1: Baseline — las 4 pruebas pasan hoy** Run: `pnpm test components/primitives/__tests__/EscenaClaridad.test.tsx` Expected: 4 passed (aria-hidden, tamaño del caller, frameloop always, frameloop demand). - [ ] **Step 2: Reemplazar el archivo completo por la nueva escena** Escribir en `components/primitives/EscenaClaridad.tsx`: ```tsx '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 ( ); } ``` Diferencias clave vs. el archivo viejo: fuera `Anillos`, `Simbolo`, `Suelo`/`texturaSombra` y la `pointLight` azul; el `fov` pasa de 40 a 38 y `far` de 60 a 100 (los del preview); `Camara` recibe `estatico` para congelar la deriva en reduced-motion. - [ ] **Step 3: Correr las pruebas del componente** Run: `pnpm test components/primitives/__tests__/EscenaClaridad.test.tsx` Expected: 4 passed — el wrapper no cambió de contrato. - [ ] **Step 4: Commit** ```bash git add components/primitives/EscenaClaridad.tsx git commit -m "feat(hero): el astrolabio deja paso a «Las Tres Líneas» — caos, ≡, escalera, crecimiento" ``` --- ### Task 2: Verificación completa y ajuste de luces **Files:** - Modify (solo si hace falta): `components/primitives/EscenaClaridad.tsx` **Interfaces:** - Consumes: Task 1 completo. - Produces: nada nuevo — verificación. - [ ] **Step 1: Typecheck y lint** Run: `npx tsc --noEmit && pnpm lint` Expected: sin errores. Si `three@0.185` tipa distinto algún prop de luces, ajusta el valor, no el tipo. - [ ] **Step 2: Suite completa** Run: `pnpm test` Expected: todos pasan (incl. content/contraste/escala, intactos). - [ ] **Step 3: Build de export estático** Run: `pnpm build` Expected: export exitoso a `out/` sin errores de prerender. - [ ] **Step 4: Humo visual** Run: `pnpm dev` → abrir `http://localhost:3000`, dejar correr el relato completo (~10 s) y verificar: (a) las tres barras forman el ≡, (b) se vuelven escalera ascendente, (c) crecen con brillo, (d) reposo con deriva sutil sin reiniciar, (e) con DevTools → rendering → emulate prefers-reduced-motion, la escena monta directo en la escalera, quieta. Nota de luces: el preview corrió en three 0.160; en 0.185 la caída física de la `pointLight` es igual pero si el dorado se ve apagado o quemado, ajusta `intensity` (rango sano 6–16) en `Luces` — es calibración, no cambio de concepto. - [ ] **Step 5: Commit (solo si hubo ajuste)** ```bash git add components/primitives/EscenaClaridad.tsx git commit -m "fix(hero): calibración de luces de «Las Tres Líneas» para three 0.185" ``` Si no hubo ajuste, este paso se omite. --- ## Self-Review - **Cobertura del spec:** concepto/tabla de fases → Task 1 Step 2 (`CAOS_DURA`, `CLARIDAD`, `DIRECCION`, `CRECIMIENTO`, `pulso`, deriva de cámara); reposo vivo → `pulso` + drift de `Camara`; reduced-motion → `estatico` en `Camara`/`TresLineas` + `frameloop: demand`; fuera de vista → IntersectionObserver intacto; eliminaciones (Anillos/Suelo/pointLight azul) → código nuevo no los contiene; pruebas intactas → Steps 1/3; commit en rama → aprobado. ✔ - **Placeholders:** ninguno — todo paso tiene código o comando exacto. ✔ - **Consistencia de tipos:** `EscenaClaridad({ className }: { className?: string })` idéntico al consumido por `ClaridadHero.tsx:54` y por los tests. `Camara`/`TresLineas` comparten la prop `estatico: boolean`. ✔