1f695d3932
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3.6 KiB
3.6 KiB
ANIMATION_SPEC.md
Technical realisation of the Phase 1 motion system (MOTION_GUIDELINES + ANIMATION_CONCEPTS A1–A10). Library decision: none (TECH_SPEC §4) — CSS-first + the shared observer module.
1. Token implementation
Motion tokens are CSS custom properties (--dur-instant: 80ms … --dur-signature: 700ms, --ease-out, --ease-in-out) defined once in motion.css; every transition/keyframe references tokens. A single @media (prefers-reduced-motion: reduce) layer overrides: durations → 1ms (not 0 — ensures transitionend semantics still fire), translations → none, trace/pulse keyframes → final frame. No per-component reduced-motion logic anywhere.
2. Concept → mechanism map
| Concept (Phase 1) | Mechanism | Trigger |
|---|---|---|
| A1 Trace draw | Inline SVG, stroke-dasharray/dashoffset keyframe, dot opacity blink ×1 |
CSS animation on load; sessionStorage flag adds data-seen on repeat visits → static |
| A2 Section arrive | [data-reveal] class toggle → opacity + translateY(12px) transition; children staggered via --reveal-index custom property delay |
observer.ts, threshold 0.2, once |
| A3 Card focus | Pure CSS :hover/:focus-visible (border token, transform 4px, underline scale, arrow translate) |
none (CSS) |
| A4 Chip pulse | CSS keyframe ×2 iterations on .status-active .dot, started when [data-reveal] fires (animation-play-state pattern) |
observer |
| A5 Diagram flow | SVG with class-annotated nodes/edges; :hover on node → CSS sibling/descendant rules highlight edges, dim others via opacity |
CSS only, @media (hover: hover) gate |
| A6 Theme morph | Two-path SVG icon crossfade/morph via CSS; global colour transition on data-theme swap limited to a curated property list (avoid transitioning everything) |
theme.ts |
| A7 Language cross-fade | View Transitions API (same-document for the switch control; cross-document for navigation) with CSS-only fallback = normal navigation | browser-native, progressive |
| A8 Timeline grow | Spine is a scaleY-transformed pseudo-element; dots opacity-staggered | observer |
| A9 Form confirm | Panel swap with height-locked container; check icon stroke-dashoffset draw |
form.ts on 2xx |
| A10 404 fray | Static SVG with a short CSS keyframe (dash gap growth); reduced-motion static | load |
| Header compress | data-scrolled boolean flag at >120px (observer sentinel element) → CSS height/blur transition. Deviation from Phase 1: linear scroll-mapping downgraded to a threshold toggle with smooth transition — visually near-identical, avoids scroll-linked JS entirely; scroll-driven CSS animations may restore true mapping as progressive enhancement where supported |
observer |
3. Performance rules (enforceable)
- Animatable properties allow-list:
transform,opacity,stroke-dashoffset, plus theme-swap colour transitions. Anything else fails review. - No
will-changeexcept trace + lightbox transitions (added/removed around use). - Observer instances: exactly one shared IntersectionObserver for reveals/spy + one sentinel for header. No scroll event listeners anywhere.
- Lighthouse TBT budget (<100 ms) is the regression tripwire for animation JS.
4. Accessibility rules
- Reduced-motion layer per §1 — verified in Playwright with emulated
prefers-reduced-motion. - Nothing flashes >3×/s (chip pulse is 2 iterations, ~1 Hz). No parallax beyond 4px.
- Focus is never moved by animation; reveals never contain focusable elements while hidden (visibility handled so tab order is stable pre-reveal).
- View-transition language swap must not discard focus position: switch control retains focus after navigation (tested).