11 KiB
RFC — Motor de forma (shape) de Eidos
Hermano de
COLOR_ENGINE_RFC.md,TYPOGRAPHY_ENGINE_RFC.mdyDEPTH_ENGINE_RFC.md. Lleva el canal de forma (shapedel libro Diseñando lo que ocurre) a reference-grade rompiendo el modelo de los referentes — con la jaula abierta. Es el 8º y último canal de expresión del libro sin elevar (tiempo · movimiento · presencia · profundidad · forma · color · sonido · háptica).
0. Tesis
La forma no es un número de redondeo que un elemento tiene; es una cualidad perceptual — continuidad, familia, tensión — que el sistema compone y que, en su momento, ocurre.
Todos reducen la forma a UN número: border-radius. Pero la esquina tiene más ejes que su
magnitud: cuán continua es (arco circular vs superelipse), a qué familia pertenece
(redondeada / continua / cortada / píldora), cómo se relaciona con las formas anidadas, y
qué le ocurre cuando el elemento es presionado o confirma. Eidos trata la forma como un
canal de primera clase: unificado (magnitud + continuidad + familia + armonía), y
eventful (la forma puede tensarse/relajarse), bajo la jaula abierta.
1. El estudio — cómo lo hacen los referentes y dónde topan
| Framework | Modelo | Límite estructural |
|---|---|---|
| Tailwind / Bootstrap | escala rounded-sm…full, arco circular |
sin continuidad, sin armonía anidada, sin familia, estática; la esquina es un arco y punto |
| Material 3 | shape scale + mapping por componente + (M3 Expressive) morph + familia cut |
arco/cut; el morph es reciente y acotado; sistema cerrado (eliges de la escala) |
| Apple / SwiftUI | esquinas continuas (superelipse) .continuous |
atado a plataforma, no es token portable; no eventful; anidado manual |
| CSS hoy | border-radius = arcos elípticos |
sin superelipse nativa universal (corner-shape emergente); squircles vía SVG/clip-path hacks |
Límite común: forma = un número de radio de arco, propiedad estática que asignas, desligada de los elementos vecinos/anidados, sin continuidad real, y desconectada de lo que ocurre. SwiftUI es el único con continuidad — pero cerrada y por plataforma. Material es el único con familia + morph — pero arco y cerrado. Nadie los unifica ni los hace abiertos.
2. Dónde está Eidos hoy (coherente, pero no novedoso)
- Escala de radius en
--radius-{none·sm·md·lg·xl·xxl·full}(4/6/10/16/20px + pill). ✓ - Asignada por componente vía el size primitive (
radius: 'md'). ✓ - PERO el modelo es exactamente "un número de arco circular": sin continuidad (squircle), sin armonía entre radios anidados, sin familia perceptual, y estática (nunca ocurre). Estás ~a la par de Tailwind; por debajo de SwiftUI (continuidad) y de M3 (familia + morph).
3. El modelo novel — 4 piezas
- Continuidad (superelipse) — un eje nuevo
--shape-smoothing(0 = arco circular … 1 = superelipse tipo iOS). Output progresivo (estrategia idéntica al oklch del color): se emiteborder-radius(universal, el fallback) ycorner-shape: superellipse(…)donde se soporta. Resuelve de raíz la esquina-de-arco que todos arrastran, sin romper a nadie. - Armonía de radios anidados — radio interior derivado del exterior:
interior = max(0, exterior − gap)(concéntrico), computado encalc(). Una convención--shape-nestpara que tarjeta-dentro-de-tarjeta (o input-dentro-de-panel) nunca desencaje. - Familia de forma —
rounded · continuous · cut · pill(+sharp). Eje perceptual, no de magnitud. Canon del eidos (como roles/variants): valores themeables, set canon. - Forma eventful (lo genuinamente nuevo) — la forma se tensa/relaja al ocurrir algo:
contact→aprieta esquinas (squeeze),commit→las redondea, vía la misma firma que ya dirige motion/depth. Forma-como-suceso, coordinada desde un solo evento sema.
3.bis Los dos momentos de la forma
La forma respeta el modelo de dos momentos del framework (motion F1) — no lo reinventa:
| Momento | Atributo (eje) | Para shape | Fase |
|---|---|---|---|
| estado | data-state (persistente) |
la forma en reposo — magnitud (--radius-*) + familia + continuidad: cómo es el elemento |
1 |
| evento | data-event-* (transitorio, durante el hold) |
el morph — apretar/redondear al contact/commit: lo que ocurre. Ordenado por sequence, junto a las firmas de motion/depth |
3 |
Un control tiene una forma (estado) y la tensa al pulsarse (evento) — los dos momentos, nunca colapsados. Mismo rigor del libro (evento ≠ estado) llevado a la forma.
4. Doctrina — default fuerte, jaula abierta
| Pieza | Default fuerte | Puerta abierta |
|---|---|---|
| escala | --radius-* canónica |
config-driven (EidosConfig size/radius); el border-radius crudo siempre a un paso |
| continuidad | --shape-smoothing por defecto (sutil) |
override global/por-componente/por-elemento; 0 vuelve al arco puro |
| familia | set canónico (rounded/continuous/cut/pill/sharp) | el set es canon pero themeable; un wrapper compone una propia sin inventar familia |
| anidado | convención --shape-nest (concéntrico) |
opt-in; el cálculo es transparente (calc()), anulable |
| eventful | contact↓apretar / commit↑redondear |
opt-in/opt-out (como sound/haptic/depth); registro abierto de sema; degrada con prefers-reduced-motion; la forma estática funciona sin el canal |
| sistema entero | tema canónico | applyShape(seed) runtime (hermano de applyColorScheme/applyTypeScale/applyDepth) |
Es exactamente cómo ya operan color/tipografía/profundidad: canon retintable + builders runtime + primitivos siempre accesibles. Forma hereda el mismo contrato de apertura.
5. Contrato de tokens (añadidos, congelados)
Bare-prefixed por sistema (--shape-…). La magnitud sigue en --radius-* (no se renombra
nada → cero rotura):
--radius-{key} magnitud (escala existente — se mantiene)
--shape-smoothing factor de superelipse 0..1 (Fase 1) → corner-shape: superellipse(k)
--shape-nest-gap gap por defecto del radio anidado (Fase 2)
- Continuidad:
[style] { border-radius: var(--radius-md) }(universal) + donde se soportecorner-shape: superellipse(var(--shape-smoothing-k))(progresivo, degrada al arco). - Familia: atributo
data-shape='cut'|'continuous'|…o recipe del componente (no un token global) — selecciona elcorner-shape/ clip apropiado. - Anidado:
border-radius: max(0px, calc(var(--_outer-radius) - var(--shape-nest-gap)))— convención encalc(), sin token de salida nuevo.
El anidado de 4 esquinas requiere radios FINITOS — a
fulles solo el top. Afull(--radius-full= 9999px) el radio se recorta a la mitad de la dimensión menor de cada elemento. Un hijo ancho (p. ej. 16:10) no puede ser concéntrico en sus 4 esquinas: su altura capea las cuatro a ~½-altura, distinto del padre. Pero sus esquinas superiores SÍ pueden valerradio_card − gapsi las inferiores se quedan rectas (la restricción de altura deja de morder). Esa es la geometría del reproductor iOS: carátula con top redondeado concéntrico + base recta que encuentra los metadatos.La regla CSS
[data-shape-nest]no sirve aquí porque el valor computado del radio del padre sigue siendo9999px(el cap es un valor usado, no legible en CSS). El demo/temas/formalo mide (min(w,h)/2 − gapvíaResizeObserver) y lo aplica a las esquinas superiores de la carátula; las inferiores quedan a0. El toggle "radios concéntricos" se inhabilita afull(«auto: solo top») porque ahí el anidado es automático y parcial por geometría, no opcional.
6. Fases (la flexibilidad va horneada en cada una)
- ✅ Continuidad + escala + familias —
--shape-smoothing+data-shape→corner-shapecon las 4 familias (rounded/continuous/cut/scoop). Emisión progresiva:border-radiusuniversal (la magnitud,--radius-*intactos) +corner-shapedonde se soporte (degrada al arco). Config-driven + validación + test + regen. (Las familias se adelantaron aquí — son la misma mecánicacorner-shape.) - ✅ Armonía anidada —
--shape-nest-gap+ regla[data-shape-nest]:border-radius: max(0, var(--shape-outer-radius) − gap)(concéntrico). - ✅ Forma eventful (momento-evento) — el morph de esquina plegado en la firma
press-squeeze(cross-modal: un press = escala + sombra + esquina) +@property --shape-smoothingpara que interpole. Sobre el sistema designaturesexistente; degrada con reduced-motion. No un sistema paralelo. - ✅ Builder runtime
applyShape(seed)— dial desmoothing/nestGap+ override de familias, bloque gestionado (hermano deapplyColorScheme/applyTypeScale/applyDepth). - ✅ Showcase + docs —
/temas/forma(continuidad con dial + familias + anidado + eventful en vivo) + THEMING §30.
7. Composición con lo existente
- Escala
--radius-*→ la magnitud de cada forma (se reusa, no se duplica). - Sistema de
signatures(motion/depth) → el morph eventful (Fase 3), no un motor nuevo. - Dos momentos de motion → el morph es un movimiento de la forma, no un salto.
- Bus de eventos sema → el disparo eventful (Fase 3), como sound/haptic/depth.
- Espaciado → el gap del radio anidado (Fase 2).
No se reinventa nada: se unifica + se hace continuo, armónico y eventful lo que hoy es un número suelto, bajo la doctrina del libro.
8. Doctrina (paralela a color / tipografía / profundidad)
- Escala + familias = canon del eidos (como roles/variants y planos): valores themeables, pero el set es canon.
- Continuidad = cualidad por defecto, retuneable;
0= arco puro (jaula abierta). - Forma eventful = capacidad del motor, opt-in con degradación (como sound/haptic/depth).
- Theme = retintar/retunear lo perceptualmente fijo: cambia CUÁNTO redondea
mdo cuánta continuidad lleva, no QUÉ significacut. - Jaula abierta: opinión fuerte que nunca atrapa —
border-radiuscrudo siempre accesible.
9. Fuera de alcance
- Blobs / formas orgánicas aleatorias y morphing de
patharbitrario (la forma es perceptual, no un motor de geometría libre). - Recortes complejos por geometría (clip-path a medida por componente queda en cada recipe).
- Esquinas por-lado asimétricas como sistema (el
border-radiuspor-esquina crudo ya las cubre).