You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/PLAN-text-effects.md

56 lines
11 KiB

feat(packs+text): incorporate the animation collection — Ambient pack + text-effects family + docs Two streams, split by what the animation touches: STREAM A — decorative backgrounds → the pack tier - arts/scene: a consolidated scene runtime ($scene) that owns, once, the citizenship every ad-hoc background reinvented or skipped (frame loop, off-view pause, DPR cap, mandatory reduced-motion, WebGL context loss/restore, scene budget, teardown). SceneDom port (adom satisfies it), webgl/webgl2/canvas2d drivers + a custom-pipeline extension (vertexShader + draw + glContext.depth/dprCap) for real geometry (beam, particles, dither, grid, eter, pixel-blast, hyperspeed). 32 effects as shared resources. - src/packs/ambient: the first pack — <Ambient effect="…"> mounts a registered effect; the P contract (P-1..P-6) guarded by scripts/packs-check.ts; colors are token-aware (P-4). One-way dependency, removable-by-construction. - resolveToken extended to semantic color slots (--color-{role}-{slot}) so consumers resolve theme tokens to concrete colors (the P-4 half). STREAM B — animations over real text → canon - Six components (count-up + text-{gradient,circular,blur,focus,scramble}): each a morfo + eidos recipe (where there's styling) + demo. CountUp is a service component (counts through uix.format.numbers). The five Text* are passive decoratives. Upgrades over the seeds: SR hardening (real text visually-hidden + aria-hidden decoration), a11y fix (no fake role=button), measurement discipline (cached rects via dom.measure, no reflow storm), reduced-motion, ecosystem citizenship (eidos.dom/timers, no raw platform). - MorfoElement gains 'p'. DOCS - docs/architecture/packs.md (pack tier, admission rule, P contract, Aura promotion path); docs/decisions/design-text-effects.md (the family design record) + indexed in decisions.md / README.md; glossary entries (scene/Ambient/Aura/text effects); scene README custom-pipeline + authoring bridge; motion-guide content-effects note; strata tables acknowledge packs. Gates: component:audit 141/0/0 · docs:check 0/0 · scene tests 9/9 · packs:check 0/36 · check 0 own errors. Verified in browser (32 effects mount+compile; 6 text components SSR+hydrate, CountUp re-formats by locale live, TextGradient resolves token stops to OKLCH via var()). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
# PLAN — Efectos de texto (F5 del plan Scene/Ambient, desgajado por D9)
> **Tipo**: plan de ejecución por fases (process — efímero, no fuente de verdad).
> **Fecha**: 2026-07-12 · **Estado**: **PLAN COMPLETO — D-T1…D-T4 resueltas · FT0–FT7 EJECUTADAS (2026-07-12)**.
> FT7: **6 demos** en `web/routes/uix/components/{kebab}/` (frame de 6 pestañas de los pasivos shipped — patrón box/format-number; cada prop pública = control vivo; snippet con paridad; sema empty-state justificado; CountUp con `demoLocale` propio por chips — convención service-demos). Matriz **141/0/0 con Demo ✓ y 0 warns en los 6** · check 0 propios · docs:check 0/0 · contracts 38/38. **Verificado en navegador**: 6 rutas SSR 200 + hidratadas; CountUp formatea por locale del ecosistema EN VIVO (`0,00` es-ES → chip en-US → `0.00`); TextGradient resuelve tokens→OKLCH del tema vía `var()` con `background-clip:text` + animación activa; TextCircular `role=img`+33 chars posicionados; TextFocus SIN botones falsos dentro y el auto-avance de `timers.interval` CORRE (palabra activa cambia — los timers no son rAF); TextBlur segmentos `aria-hidden` + SR completo + estado inicial sin flash; TextScramble SR surface correcta. Cero errores de consola. Los caminos rAF/IO-dependientes (count visual, flicker, marco) quedan bajo el pendiente visual de pestaña visible ya conocido (pane oculto suspende rAF/IO/measure). Ajuste de tipos post-demo: los 5 visuales aceptan `style` (fijar tokens por instancia); text-gradient fusiona su style interno con el del consumidor.
> FT0–FT6: **los 6 componentes construidos y verdes en toda la maquinaria** — matriz component-audit **141/0/0** (los 6 PASS con README F-1 completo: Baseline/Comparativa/Gaps/Passive justification) · contracts 38/38 (enumeración morfo + scope) · check 0 errores propios. `CountUp` (servicio patrón FormatNumber: morfo 1-part, `uix.format.numbers` reactivo cada frame con fallback Intl, spring DHO analítico — el de `$motion` es element/CSS-bound, anotado —, IO/rAF vía `eidos.dom`, timers vía `eidos.timers`; `separator` del seed NO portado: el locale posee los separadores) · `TextGradient` (receta CSS + tokens públicos, stops `var()`-interpolados + `preset` → `--gradient-{name}` S9, borde interior `#000`→`--color-surface-default` token, keyframes R-4.5 anotados, `margin:0 auto` del seed fuera) · `TextCircular` (rAF vía dom, reduce-motion AÑADIDO — el seed no paraba nunca —, `currentColor` default, nota Aura en morfo/README) · `TextBlur` (**SR hardening**: texto real visually-hidden + segmentos aria-hidden; WAAPI directa = primitivo del driver waapi de $motion, migrará al dominio content cuando el RFC aterrice; IO vía dom) · `TextFocus` (**a11y fix**: fuera los `role="button"`+tabindex falsos del seed; `timers.interval` + `dom.measure` + `observeResize`; tokens border/glow; reduce = marco quieto; 1 `!important` anotado R-4.7) · `TextScramble` (**rediseño de medición**: cache de centros por carácter vía `dom.measure` invalidado por RO — el seed medía N rects POR pointermove; 1 lectura por move; SR hardening; `text: string` en vez de children — el split solo es honesto con texto plano, declarado). Extensión de vocabulario: `MorfoElement` + `'p'` (primer párrafo del catálogo).
> **Origen**: `docs/process/PLAN-scene-ambient-pack.md` D9 — los 6 efectos de texto de
> `web/routes/demos/animations/text/` NO van al pack Ambient: **envuelven contenido
> real → superficie a11y = superficie de contrato → canon**, con destinos
> heterogéneos. La colección seed queda intacta como referencia comparativa (D5/D7).
## Los 6 seeds, analizados
| Seed | Sustrato | Qué hace | Interacción | A11y del seed | Ciudadanía a corregir |
| --- | --- | --- | --- | --- | --- |
| `count` (Count Up) | rAF spring analítico | Cuenta from→to al entrar en viewport | — | ✓ texto real (números) | `Intl.NumberFormat('en-US')` **hardcoded** → debe componer `uix.format.numbers` (locale-aware); `setTimeout`→`uix.timers`; IO→`$adom`; reduce ✓ ya salta al final |
| `gradient` (Gradient Text) | CSS puro (`background-clip:text` + keyframes) | Gradiente animado sobre el texto (+ borde opcional) | pause-on-hover | ✓ texto real intacto | Colores crudos→tokens; `#000` del borde interior = phantom→token de superficie; `@keyframes`→R-4.5 (anotación functional / registro motion); reduce ✓ ya |
| `blur` (Blur Text) | WAAPI por segmento | Entrada staggered blur→nítido por palabras/letras al entrar en viewport | — | ⚠️ trocea en spans (por letras fragmenta lectura SR) | **SR hardening**: `aria-label` en contenedor + segmentos `aria-hidden`; WAAPI/stagger vía dominio content de `$motion`; IO→`$adom`; reduce ✓ ya |
| `circular` (Circular Text) | rAF + transforms | Caracteres en círculo girando; hover cambia velocidad | hover (slowDown/speedUp/pause/goBonkers) | ✓ `role="img"` + `aria-label` (buena base) | rAF→`dom.requestFrame`; escrituras transform→`dom.apply`; colores/tamaños crudos→tokens. **Nota Aura**: texto orbital alrededor de un shape redondo = pieza candidata de la periferia del indicador de agente |
| `focus` (True Focus) | CSS transitions + JS | Palabras difuminadas salvo la activa; marco de esquinas viaja (auto o hover) | hover en modo manual | ⚠️ `role="button"` + `tabindex=0` en CADA palabra sin acción real = anti-patrón | Quitar role=button (o darle semántica real); `setInterval`→`uix.timers`; `getBoundingClientRect`→**`dom.measure`** (regla dura de layout reads); RO→`dom.observe`; border/glow crudos→tokens; reduce ✓ ya |
| `scrambled` (Scrambled Text) | rAF + DOM textContent | Scramble de caracteres cerca del puntero | pointermove | ⚠️ SR puede leer los caracteres scrambled; split destruye el HTML original | **SR hardening** (aria-label + chars aria-hidden); **reflow storm**: mide `getBoundingClientRect` POR CARÁCTER en CADA pointermove → rediseño obligado: cache de rects vía `dom.measure` invalidado por RO; rAF→`dom.requestFrame`; pointer→`dom.listen` |
Precedente real del canon para servicios: `formatNumberMorfo` — morfo de **1 part,
`scope: ['eidos']`, sin CSS/eventos** («grep-friendly typed wrapper»,
`src/uix/morfo/components/format-number.ts`). Confirma que *morfo-first* aplica
incluso a componentes de servicio, con coste mínimo. Los efectos con interacción
(circular/focus/scrambled) añaden data-attrs de estado y, si procede, eventos sema.
## Decisiones (gate del usuario)
| ID | Cuestión | Opciones analizadas | Recomendación |
| --- | --- | --- | --- |
| **D-T1** | Arquitectura por efecto | (a) canon completo ×6 (ruta 9 fases con soma+sema cada uno) · (b) familia "text effects" = wrappers eidos con morfo mínimo · (c) **mixto**: cada uno a su hogar natural — count=service component (patrón FormatNumber); gradient=eidos CSS con morfo mínimo; blur/circular/focus/scrambled=eidos decorativos con morfo (parts+data-attrs de estado) sin soma provider (no hay máquina de estados accesible que justifique soma; la interacción es decorativa) | **(c)** — (a) infla 4 componentes decorativos con capas vacías; (b) niega a count su hogar de servicio |
| **D-T2** | count: ¿componente nuevo o prop de FormatNumber? | (a) `<CountUp>` hermano de FormatNumber que compone el MISMO formatter (`uix.format.numbers`) · (b) prop `animate` en FormatNumber | **(a)** — FormatNumber es puro/inmediato; animar es otra responsabilidad. Ambos comparten formatter (sin duplicar lógica de locale) |
| **D-T3** | gradient: ¿API de colores cruda o sistema de gradientes del theming (S9)? | (a) `colors: string[]` con tokens resolubles (API del seed, theming vía tokens) · (b) integrar el vocabulario de gradientes S9 (presets canónicos) además de colores libres | **(b) con (a) dentro**: acepta stops libres (tokens o hex) Y presets del sistema de gradientes — canon presentado como opción, sin recortar la API del seed |
| **D-T4** | Naming de la familia | (a) prefijo `Text*`: `TextBlur` / `TextCircular` / `TextFocus` / `TextGradient` / `TextScramble` + `CountUp` aparte (servicio) · (b) nombres del seed (BlurText, TrueFocus…) · (c) compound `Text.Blur` | **(a)** — agrupa en el catálogo junto al primitivo `Text`, evita el genérico "True/Up" del seed; count no es de la familia visual |
| **D-T5** | Dónde vive la animación de cada uno | blur = **dominio content de motion** (entrada de contenido: consume `uix.motion` / WAAPI vía runtime, no keyframes CSS) · gradient = keyframes CSS de receta (loop ambiental, R-4.5) · circular/scrambled = rAF vía `dom.requestFrame` (estado continuo JS) · focus = CSS transitions + `uix.timers` · count = spring JS (¿reutilizar el spring de `$motion` si su API lo expone para valores numéricos? — se verifica en FT1; si no, spring local como el seed) | Se deriva de la doctrina motion (perceptual anchoring; animation = channel); no exige gate salvo desacuerdo |
## Fases (tras el gate)
- **FT0 — Doctrina**: asentar D-T\* en este plan; verificar precedentes (spring de `$motion`, sistema de gradientes S9, wrappers eidos-only) leyendo el código real; actualizar `docs/README.md` si la familia merece fila propia.
- **FT1 — `CountUp`** (servicio): morfo 1-part patrón FormatNumber · compone `uix.format.numbers` (locale reactivo del ecosistema, NO en-US fijo; el `separator` del seed se reevalúa contra la API real del formatter) · IO por `$adom` · `uix.timers` · spring (D-T5) · reduce = salto al final.
- **FT2 — `TextGradient`** (eidos): receta CSS con tokens + R-4.5 · stops libres + presets S9 (D-T3) · borde con token de superficie · reduce ya en CSS.
- **FT3 — `TextBlur`** (eidos + motion content): split words/letters con **SR hardening** (contenedor `aria-label`, segmentos `aria-hidden`) · stagger/WAAPI vía runtime motion · IO por `$adom`.
- **FT4 — `TextCircular`** (eidos): rAF vía `dom.requestFrame` · hover states como data-attrs del morfo · tokens de tipografía/tamaño · conserva `role="img"`. Anotar la conexión Aura (texto orbital del indicador).
- **FT5 — `TextFocus`** (eidos): `uix.timers` + `dom.measure` + `dom.observe` · fix a11y del role=button · border/glow por tokens.
- **FT6 — `TextScramble`** (eidos): **cache de rects** (dom.measure + invalidación por RO — mata el reflow storm) · SR hardening · pointer por `dom.listen` · charset/duración/radio como props.
- **FT7 — Cierre**: demos con profundidad DEMO_AUTHORING (cada prop = control vivo) · checklist de componente por cada uno · `npm run check` + vitest + eidos-lint + navegador · memoria + este plan con bloques EJECUTADA.
- **Verify por fase**: check 0 errores propios · component:audit verde para los nuevos · demo montada en navegador · SR hardening revisado (árbol de accesibilidad en devtools).
## Fuera de alcance
La fase Aura (el plan madre §F6.3) sigue sin construirse aquí; `TextCircular`
solo deja anotada la pieza orbital. Los 4 efectos de imagen (D8) siguen fuera.

Powered by TurnKey Linux.