Primitivas y contenido del rediseño (escala tipográfica, Revelar, MarcaSeccion, Seccion, Antetitulo, SimboloMarca, Resplandor, ClaridadHero, sección Proceso de ocho etapas, lib/content.ts reestructurado en seis secciones con alias para las viejas). Mecanismo y FormaTrabajar se retiran. Encima, lo de esta sesión: - recorrido: creeer vuelve a responder cada situación en vez de repetirla (el alias apuntaba respuesta a situacion); respuestas en plural. Créditos de las seis fotos de México. - contacto: WhatsApp, email (con mailto:) y LinkedIn reales; iconos de las redes en el cierre y el footer (IconoRed, trazos de Simple Icons, CC0). - enlaces: lo externo abre en otra pestaña con noopener y aviso para lectores de pantalla (lib/enlaces.ts); mailto y anclas se quedan. - Proceso: las ocho tarjetas no aparecían nunca en móvil (amount 0.3 sobre una lista de ~3000px); ahora cada una se revela sola. El ≡ de la 08 ya no tapa un entregable, título equilibrado, etiqueta a 12px, sin elevar al hover lo que no es clicable. - Disciplinas: la frase «No son la promesa — son los medios» gana peso y se agrupa con título e ilustración; sin cortes huérfanos en móvil. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
13 KiB
«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, renameCLAUDE.md). Al commitear usa rutas explícitas (git add <archivo>), nuncagit add -A/git add .. - Commits de esta implementación aprobados por el usuario en la rama
animacion-tres-lineas. prefers-reduced-motiondebe montar la pose final directamente (patrónestaticoexistente).
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:
usePrefersReducedMotionde@/lib/motion(ya usada),Canvas/useFrame/useThreede@react-three/fiber. -
Produces:
export function EscenaClaridad({ className = '' }: { className?: string })— firma idéntica a la actual.ClaridadHeroy 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:
'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>
);
}
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
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)
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 deCamara; reduced-motion →estaticoenCamara/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 porClaridadHero.tsx:54y por los tests.Camara/TresLineascomparten la propestatico: boolean. ✔