From 6783e32324aa5d9d14ea2b7cfd9b01e509b0f376 Mon Sep 17 00:00:00 2001 From: erikgh Date: Fri, 31 Jul 2026 11:04:21 -0600 Subject: [PATCH] =?UTF-8?q?feat(grafico):=20SecuenciaClaridad=20=E2=80=94?= =?UTF-8?q?=20la=20met=C3=A1fora=20del=20manual,=20dibujada?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/primitives/SecuenciaClaridad.tsx | 134 ++++++++++++++++++ .../__tests__/SecuenciaClaridad.test.tsx | 33 +++++ 2 files changed, 167 insertions(+) create mode 100644 components/primitives/SecuenciaClaridad.tsx create mode 100644 components/primitives/__tests__/SecuenciaClaridad.test.tsx diff --git a/components/primitives/SecuenciaClaridad.tsx b/components/primitives/SecuenciaClaridad.tsx new file mode 100644 index 0000000..0e1e4ae --- /dev/null +++ b/components/primitives/SecuenciaClaridad.tsx @@ -0,0 +1,134 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { useEnVista } from '@/lib/motion'; +import { VIEWBOX_SIMBOLO, LINEAS_SIMBOLO } from '@/lib/simbolo'; + +// The manual's core metaphor, finally drawn: Complejidad → Enfoque → Claridad. +// Eighteen crossing lines converge in three groups; in the "completa" variant +// each group collapses onto one of the three bars of the ≡, so the mark is +// literally produced by the resolution of the tangle rather than placed next +// to it. The "hero" variant stops at convergence and leaves the tension open — +// FraseAncla pays it off further down the page. + +type Variant = 'completa' | 'hero'; + +// Scattered start positions. Hand-placed rather than random so the tangle is +// identical on every render and between server and client. +const DISPERSAS = [ + { x1: 2, y1: 8, x2: 40, y2: 3, barra: 0 }, + { x1: 20, y1: 2, x2: 61, y2: 24, barra: 0 }, + { x1: 6, y1: 22, x2: 52, y2: 6, barra: 0 }, + { x1: 34, y1: 5, x2: 12, y2: 27, barra: 0 }, + { x1: 48, y1: 2, x2: 58, y2: 30, barra: 0 }, + { x1: 3, y1: 15, x2: 46, y2: 19, barra: 0 }, + { x1: 5, y1: 30, x2: 60, y2: 26, barra: 1 }, + { x1: 14, y1: 40, x2: 55, y2: 20, barra: 1 }, + { x1: 2, y1: 26, x2: 38, y2: 44, barra: 1 }, + { x1: 44, y1: 22, x2: 18, y2: 41, barra: 1 }, + { x1: 30, y1: 18, x2: 62, y2: 40, barra: 1 }, + { x1: 8, y1: 36, x2: 50, y2: 33, barra: 1 }, + { x1: 4, y1: 44, x2: 47, y2: 58, barra: 2 }, + { x1: 24, y1: 62, x2: 60, y2: 42, barra: 2 }, + { x1: 10, y1: 56, x2: 56, y2: 50, barra: 2 }, + { x1: 40, y1: 61, x2: 14, y2: 46, barra: 2 }, + { x1: 2, y1: 50, x2: 44, y2: 62, barra: 2 }, + { x1: 33, y1: 44, x2: 62, y2: 57, barra: 2 }, +]; + +// One line per group survives to become a bar of the ≡; the rest fade as they +// converge — many/disordered becomes few/aligned, per the manual. +const SOBREVIVE = [0, 6, 12]; + +const EASE = [0.22, 1, 0.36, 1] as const; + +export function SecuenciaClaridad({ + variant = 'completa', + className = '', +}: { + variant?: Variant; + className?: string; +}) { + const { ref, enVista, reduced } = useEnVista(0.35); + + const completa = variant === 'completa'; + + // Where each line ends up. In "completa" the survivors land exactly on the + // symbol's own coordinates; in "hero" they merely tighten toward their + // group's axis without ever aligning. + const destino = (i: number) => { + const { barra } = DISPERSAS[i]; + const linea = LINEAS_SIMBOLO[barra]; + const sobrevive = SOBREVIVE.includes(i); + if (!completa) { + const eje = linea.y1; + const d = DISPERSAS[i]; + return { + x1: d.x1 + (26 - d.x1) * 0.35, + y1: d.y1 + (eje - d.y1) * 0.55, + x2: d.x2 + (38 - d.x2) * 0.35, + y2: d.y2 + (eje - d.y2) * 0.55, + opacity: sobrevive ? 0.95 : 0.3, + }; + } + return { + x1: linea.x1, + y1: linea.y1, + x2: linea.x2, + y2: linea.y2, + opacity: sobrevive ? 1 : 0, + }; + }; + + const grosor = (i: number) => (completa && SOBREVIVE.includes(i) ? LINEAS_SIMBOLO[0].grosor : 1.5); + + // Reduced motion renders plain elements: motion.line would apply its + // own dash scaffolding and leave the drawing invisible (see the same trap in + // IlustracionInflexion). + if (reduced) { + return ( + + ); + } + + return ( + + ); +} diff --git a/components/primitives/__tests__/SecuenciaClaridad.test.tsx b/components/primitives/__tests__/SecuenciaClaridad.test.tsx new file mode 100644 index 0000000..946ac8d --- /dev/null +++ b/components/primitives/__tests__/SecuenciaClaridad.test.tsx @@ -0,0 +1,33 @@ +import { render } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { SecuenciaClaridad } from '../SecuenciaClaridad'; + +describe('', () => { + it('is decorative: hidden from assistive tech', () => { + const { container } = render(); + const svg = container.querySelector('svg'); + expect(svg).toHaveAttribute('aria-hidden', 'true'); + expect(svg).toHaveAttribute('focusable', 'false'); + }); + + it('draws the tangle of lines that stands for complexity', () => { + const { container } = render(); + expect(container.querySelectorAll('line').length).toBeGreaterThanOrEqual(18); + }); + + it('resolves into the mark itself, not a lookalike', () => { + const { container } = render(); + const resolucion = container.querySelectorAll('[data-resolucion]'); + expect(resolucion).toHaveLength(3); + }); + + it('leaves the tension unresolved in the hero variant', () => { + const { container } = render(); + expect(container.querySelectorAll('[data-resolucion]')).toHaveLength(0); + }); + + it('shares the viewBox with the symbol so both scale alike', () => { + const { container } = render(); + expect(container.querySelector('svg')).toHaveAttribute('viewBox', '0 0 64 64'); + }); +});