129 lines
6.5 KiB
Markdown
129 lines
6.5 KiB
Markdown
# Story: Animated Background That Actually Animates
|
||
|
||
> **SUPERSEDED (owner direction 2026-08-25):** the phase-22 motion
|
||
> design (grid drift + whole-layer breathe) is superseded by the
|
||
> owner's "no movement, only fading light" direction — see
|
||
> `.agent/user_stories/background-no-motion.md` (phase
|
||
> `25_background_no_motion`). The phase-22 history below is preserved
|
||
> as-is; the old E2E suite now pins the phase-25 contract as a
|
||
> regression.
|
||
|
||
**Phase:** `22_background_animation` · **E2E:** `tests/e2e/test_background_animation.py`
|
||
|
||
## Narrative
|
||
|
||
As **the owner**, I reported (2026-08-24) that the Brain of Reese
|
||
background "just blinks". I want the background the phase-08 design
|
||
promised — a slow, seamless grid drift plus a gentle glow breathe — to
|
||
be *perceived* as smooth motion in a real browser, so the app feels
|
||
alive but calm, not like a broken flicker.
|
||
|
||
- **Given** the phase-08 pure-CSS background (a 60s grid drift masked
|
||
into a small top band + a 14s glow opacity swing)
|
||
- **When** the page is observed in a real Chromium viewport
|
||
- **Then** both layers visibly and smoothly animate — the grid drift
|
||
reads as continuous motion and the glow reads as breathing — with no
|
||
blink, no static frame, no jank, no new overflow at 360px, and no
|
||
impact on text contrast or interactivity.
|
||
|
||
## Bug report (verbatim, TODO.md L5)
|
||
|
||
> Fix background animation not working, just blinking
|
||
|
||
## Owner-confirmed (2026-08-24, roadmap A3)
|
||
|
||
1. **Intended effect = the phase-08 design comments:** a seamless 60s
|
||
grid drift (one cell per loop) + a 14s eased glow breathing. The fix
|
||
serves that design — it is not a redesign.
|
||
2. **Pure CSS, zero JS** (phase-08 anchor) — no animation JS, no new
|
||
assets, no `filter: blur`.
|
||
|
||
## Root cause (found in task 01 — `.agent/reports/22_background_animation/`)
|
||
|
||
Both layers *were* animating and nothing occluded them (occlusion
|
||
disproven with a red-line probe) — but:
|
||
|
||
1. **The grid drift was invisible.** 1px lines at 35% `--line` alpha
|
||
blend to a ≈10/12/18-level delta over the page bg, and the radial
|
||
mask faded the layer to ~zero past the top ~25% of the viewport. A
|
||
0.73px/s drift of that faint texture sits below the perception
|
||
threshold (measured ≈1.4/765 mean pixel change over 5s in the grid
|
||
zone; ≈0 in the bottom half — no grid there at all).
|
||
2. **The glow swing was the only visible motion.** The 0.65↔1.0 opacity
|
||
swing (35% amplitude, frame-diff concentrated exactly on the two
|
||
glow centers) was 100% of the perceived animation → it read as a
|
||
blink/pulse, not breathing.
|
||
|
||
## Fix (styles.css — pure CSS, zero JS, no blur, palette untouched)
|
||
|
||
| property | phase 08 | phase 22 |
|
||
|---|---|---|
|
||
| grid line alpha | 35% `--line` ×2 | **60% `--line` ×2** |
|
||
| grid mask | `120% 90% at 50% 0%, black 25%, transparent 78%` | **`140% 110% at 50% 0%, black 40%, transparent 90%`** |
|
||
| grid drift | 60s, 0→44px | **unchanged** (one-cell seamless loop) |
|
||
| glow opacity swing | 0.65↔1 | **0.85↔1** |
|
||
| glow period/scale | 14s ease-in-out alternate, 1↔1.05 | **unchanged** |
|
||
|
||
Both layers keep `position: fixed; inset: 0; z-index: -1;
|
||
pointer-events: none`; `<html>` keeps the `var(--bg)` canvas and
|
||
`<body>` stays transparent (the no-occlusion contract); the
|
||
`prefers-reduced-motion` stilling block is untouched.
|
||
|
||
## Acceptance criteria
|
||
1. In a real Chromium viewport, both background layers run their
|
||
animations (grid drift + glow breathe) and the animation timelines
|
||
advance — the background is not a static or paused frame.
|
||
2. No blink: the glow reads as breathing (0.85↔1), not a pulse
|
||
(0.65↔1); the grid drift reads as smooth continuous motion.
|
||
3. Layers stay behind content: `position: fixed`, `z-index: -1`,
|
||
`pointer-events: none`, full-viewport `inset: 0`; `<html>` keeps
|
||
the page background and `<body>` stays transparent (no occlusion).
|
||
4. No new horizontal overflow at 360px (the phase-07 pin).
|
||
5. Pure CSS, zero JS, no `filter: blur`, no new assets (phase-08
|
||
anchor); WCAG AA palette untouched (the layers carry no text).
|
||
6. Regressions green in isolation: `test_dark_tech_theme.py` (pins the
|
||
live 60s/14s durations + reduced-motion stilling) and
|
||
`test_responsive_polish.py`.
|
||
7. Unit + integration green, `app/` coverage ≥90%, story E2E green in
|
||
isolation, ruff + pyright clean.
|
||
|
||
## UI Visualization & Structure
|
||
- **Grid layer (`body::before`):** 44px cells, 1px lines at 60% of
|
||
`--line` (`rgb(38 48 74 / 0.6)`), radial mask fully visible to 40% of
|
||
the radius and faded out by 90%, 60s linear infinite drift of
|
||
exactly one cell (0→44px) — seamless loop.
|
||
- **Glow layer (`body::after`):** indigo `rgb(109 120 242 / 0.14)`
|
||
56rem circle at 12%/8% + cyan `rgb(34 211 238 / 0.10)` 60rem circle
|
||
at 88%/92%; 14s ease-in-out infinite alternate, opacity 0.85↔1 +
|
||
scale 1↔1.05.
|
||
- **Stacking:** the `<html>` canvas (`var(--bg)` = `#0a0e17`) sits
|
||
under both `z-index: -1` layers; `<body>` stays transparent and
|
||
non-stacking, so nothing can paint over the layers.
|
||
- **Motion:** compositor-friendly `background-position` / `opacity` /
|
||
`transform` only; no blur filter; `prefers-reduced-motion` stills
|
||
both layers (phase-08 guard, unchanged).
|
||
|
||
## Playwright Mapping Rule
|
||
**Test Scenario → `tests/e2e/test_background_animation.py`** (the
|
||
layers are CSS pseudo-elements — asserted via computed style + the Web
|
||
Animations API; Chromium enumerates pseudo-element CSS animations in
|
||
`document.getAnimations()`, not `document.body.getAnimations()`):
|
||
1. `test_grid_layer_animation_running` — computed style of
|
||
`body::before`: `animationName` = `bg-grid-drift`, timing function
|
||
`linear`, iteration count `infinite`; a matching entry in the
|
||
document animation list with `playState === "running"`.
|
||
2. `test_glow_layer_animation_running` — same for `body::after` with
|
||
`bg-glow-breathe`; `playState === "running"`.
|
||
3. `test_animations_advance` — sample `currentTime` of both layers,
|
||
wait ~500ms, assert both advanced — truly running, not paused
|
||
(the headless document animation timeline starts ~1s after load, so
|
||
the sample polls until the timeline is alive first).
|
||
4. `test_background_layers_contracts` — both pseudo-elements:
|
||
`position: fixed`, `z-index: -1`, `pointer-events: none`,
|
||
`inset: 0`; `document.documentElement` computed background is the
|
||
palette bg (canvas stays on `html`); `document.body` computed
|
||
background is `rgba(0, 0, 0, 0)` (no occlusion).
|
||
5. `test_no_horizontal_overflow_with_layers` — 360px viewport:
|
||
`documentElement.scrollWidth <= clientWidth` (the phase-07 pin,
|
||
replicated locally).
|