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/decisions/design-text-effects.md

146 lines
8.4 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
---
title: Text effects — family design
type: reference
audience: human + agent
authority: E3 design record — why the text-effects family exists, where each member sits, and the doctrine every member obeys
status: current
related:
packs: docs/architecture/packs.md (the sibling stream — decorative backgrounds went to the pack tier)
format: src/uix/eidos/components/format-number/README.md (CountUp's service-component sibling)
---
# Text effects — family design
The text-effects family is one of the two outcomes of incorporating a large
animation collection into the ecosystem. The collection split along a single
axis — **what the animation is applied to**:
- **Backgrounds** paint behind content and touch nothing semantic → they are
decorative leaves → they went to the **pack tier**
([`architecture/packs.md`](../architecture/packs.md), the `Ambient` pack).
- **Text effects** treat REAL text content the reader consumes → the
accessibility surface *is* the contract surface → they are **canon**
components (this document).
Both streams share one provenance rule: the seed collection
(`web/routes/demos/animations`) is kept intact as comparative reference and is
NOT the shipped code; every component is a clean re-implementation against the
ecosystem's contracts.
## The members
Six components, in two roles (decision D-T1):
| Component | Role | What it does |
| --- | --- | --- |
| [`CountUp`](../../src/uix/eidos/components/count-up/README.md) | **service** | Spring-counts a number to a target on viewport entry, formatting every frame through `uix.format.numbers`. |
feat(text-gradient): `colors` discriminates canonical name vs stop list (folds `preset`) Per request: one `colors` prop that discriminates by shape instead of two rival props. A `string[]` is an explicit stop list (unchanged); a bare `string` names a canonical gradient from the theming's named set, resolved as `var(--gradient-{name})` — exactly what the removed `preset` prop did. The set is an open cage (`EidosConfig.gradients` / `applyGradients`), so the type stays `string | readonly string[]`, not a closed union. Removed `preset` outright (no back-compat shim, per project rule); the canonical S9 vocabulary and raw stops are now the SAME input. Docs shell: register a small canonical named set on `uix/+layout@` via `applyGradients` (`brand`, `spectrum`) — role-derived, so they re-tint with the seed and flip light/dark. Demo gains a live stops|named toggle so both branches are a real testbed. Finding worth recording (types + README): a named gradient painted through `background-clip: text` must be a single `<image>` (linear/radial/conic). A role-derived MESH serializes to radials + a base COLOR, and a bare color is not a valid `background-image` (only the `background` shorthand accepts it) — so a mesh name computes to `none` and never paints. The docs set is linear-only for this reason. Verified in real Chrome: stops branch paints the token gradient; `colors="spectrum"` paints `var(--gradient-spectrum)` through the text. svelte-check 0 own errors, component:audit 141/0/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
| [`TextGradient`](../../src/uix/eidos/components/text-gradient/README.md) | decorative (CSS) | Animated gradient painted through the text (`background-clip`); `colors` takes a free stop list or a canonical gradient name. |
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
| [`TextCircular`](../../src/uix/eidos/components/text-circular/README.md) | decorative (JS) | Characters on a spinning circle; hover retunes the spin. |
| [`TextBlur`](../../src/uix/eidos/components/text-blur/README.md) | decorative (WAAPI) | Staggered blur→sharp entrance per word/letter on viewport entry. |
| [`TextFocus`](../../src/uix/eidos/components/text-focus/README.md) | decorative (CSS+JS) | Every word blurred except the active one; a corner frame travels to it. |
| [`TextScramble`](../../src/uix/eidos/components/text-scramble/README.md) | decorative (JS) | Characters near the pointer flicker through a charset and settle back. |
**`CountUp` is a service component**, not a text effect (D-T2): it is the
animated sibling of `<FormatNumber>` — formatting is FormatNumber's job,
counting is CountUp's, and both route through the same locale-aware runtime
formatter. It carries no CSS recipe (the E-2.2 exception), like the rest of
the service family (`Trans` / `FormatNumber` / `FormatDate` / `RelativeTime`).
The five `Text*` decoratives (D-T4 naming: the `Text*` prefix groups them in
the catalog beside the typographic primitives) are **eidos-native passive
components** — a morfo (parts + data-attrs, no events), an eidos recipe where
there is styling to own, and NO soma provider: none owns an accessible state
machine, so the 2-of-3 rule leaves them at eidos-only. Their interaction
(hover retune, pointer proximity) is decorative modulation of an ambient
loop, not a semantic event — hence a **0-event morfo** with a passive
justification, the same class as `banner` / `box`.
## Why canon and not the pack
A background is invisible to the a11y tree (`aria-hidden`, pointer-transparent)
— nothing in the system selects against it, so it earns the pack's exemption
from the acceptance matrix. A text effect wraps content a screen reader must
read and a keyboard user may reach. That is contract surface by definition:
the moment an artifact owns an accessibility obligation, it enters the canon
through the normal route and takes its row in `component:audit`. The
[admission rule](../architecture/packs.md#the-admission-rule-canon-vs-pack)
is the same rule read from the other side.
## The doctrine every member obeys
The seeds were self-contained demos; bringing them into the ecosystem meant
correcting the same citizenship gaps in each. These corrections ARE the design
— they are what makes a "text animation" a UIX component rather than a pasted
snippet:
1. **The content is the accessibility surface, always.** Effects that split
text into per-character/word spans (`TextBlur`, `TextScramble`) keep the
real string in a visually-hidden node and mark every animated fragment
`aria-hidden` — a screen reader reads one continuous string, never
letter-by-letter debris. `TextCircular` uses `role="img"` + `aria-label`.
`TextGradient` leaves the text untouched (paint-only). This is an **upgrade
over the seeds**, which shipped no SR surface.
2. **No fake interactivity.** `TextFocus`'s seed stamped `role="button"` +
`tabindex="0"` on every word with no activatable action — a keyboard trap
of phantom buttons. The component drops them: the words are presentational,
the sentence reads as text. An effect only claims an interactive role when
it owns a real interactive contract (which would move it up a class — see
each component's Gaps).
3. **Measurement discipline.** Layout reads run coalesced post-layout via
`dom.measure`, never sync-after-write. `TextScramble`'s seed measured every
character's rect on every `pointermove` (a reflow storm); the component
caches character centers once per layout (invalidated by `ResizeObserver`)
and does a single container read per move. `TextFocus` reads the active
word's rect the same way.
4. **Reduced motion is honored.** Where a seed ignored it (`TextCircular`
spun forever), the component renders a static, legible state under
`prefers-reduced-motion`; the others jump to the final frame or freeze.
5. **Platform access through the ecosystem.** `IntersectionObserver` and rAF
arrive via `eidos.dom` (`observeIntersection` / `requestFrame`); delay and
cadence timers via `eidos.timers`; the reduced-motion query via
`eidos.dom.prefersReducedMotion`. No raw `setTimeout` / `setInterval` /
`requestAnimationFrame` / `matchMedia`.
6. **Theme-aware color.** Color params accept eidos token names — CSS-native
`var()` interpolation for the gradient stops (`TextGradient` re-tints on a
mode switch with zero JS), or `var()`-wrapped custom properties for the
feat(text-gradient): `colors` discriminates canonical name vs stop list (folds `preset`) Per request: one `colors` prop that discriminates by shape instead of two rival props. A `string[]` is an explicit stop list (unchanged); a bare `string` names a canonical gradient from the theming's named set, resolved as `var(--gradient-{name})` — exactly what the removed `preset` prop did. The set is an open cage (`EidosConfig.gradients` / `applyGradients`), so the type stays `string | readonly string[]`, not a closed union. Removed `preset` outright (no back-compat shim, per project rule); the canonical S9 vocabulary and raw stops are now the SAME input. Docs shell: register a small canonical named set on `uix/+layout@` via `applyGradients` (`brand`, `spectrum`) — role-derived, so they re-tint with the seed and flip light/dark. Demo gains a live stops|named toggle so both branches are a real testbed. Finding worth recording (types + README): a named gradient painted through `background-clip: text` must be a single `<image>` (linear/radial/conic). A role-derived MESH serializes to radials + a base COLOR, and a bare color is not a valid `background-image` (only the `background` shorthand accepts it) — so a mesh name computes to `none` and never paints. The docs set is linear-only for this reason. Verified in real Chrome: stops branch paints the token gradient; `colors="spectrum"` paints `var(--gradient-spectrum)` through the text. svelte-check 0 own errors, component:audit 141/0/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
focus frame. `TextGradient`'s `colors` prop is discriminated by shape: a
`string[]` is a free stop list, a bare `string` names a canonical gradient
(`--gradient-{name}`, decision D-T3) — so the S9 gradient vocabulary and raw
stops are the SAME input, discriminated by shape, not two rival props.
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
## Where the animation lives (per member, D-T5)
Animation home follows the motion doctrine (perceptual anchoring; animation =
a channel), not a single mechanism:
- **CountUp** — an analytic damped-harmonic-oscillator spring evaluated in JS
per frame. `$motion`'s `spring` driver animates element CSS properties, not
numeric callbacks, so it does not apply; the spring is local.
- **TextGradient** — pure CSS keyframes in the recipe (an ambient
background-position pan), annotated `/* functional: */` per R-4.5.
- **TextBlur** — WAAPI stagger from the wrapper. This is a content ENTRANCE;
when the motion service's content domain lands
(`src/uix/eidos/MOTION_SERVICE_RFC.md`) the stagger migrates behind it.
- **TextCircular / TextScramble** — continuous JS state stepped by
`eidos.dom.requestFrame`.
- **TextFocus** — CSS transitions gated by `eidos.timers`.
## The Aura connection
`TextCircular` is orbital text around a round shape — a candidate piece of the
periphery of the future agent-presence component (reserved name `Aura`; the
promotion path is in [`architecture/packs.md`](../architecture/packs.md#the-promotion-path-the-agentic-direction)).
It stays presentation-only so `Aura` can compose it; the agent-lifecycle
events would be declared by `Aura`'s morfo (`delegate` / `sustain`), never by
`TextCircular`.
## Verification
Each member self-documents (per-component README with design · usage · API ·
Baseline · Comparativa · Gaps · Passive justification) and ships an
interactive demo at `web/routes/uix/components/{kebab}/`. The family passes the
acceptance matrix (`npm run component:audit`) as six passive components. The
per-effect user-facing decisions (D-T1…D-T5) were made in the session and are
summarized above; their working record is the (ephemeral) execution plan
`docs/process/PLAN-text-effects.md`.

Powered by TurnKey Linux.