fix(ui): background no longer moves — static grid, three glow spots fading in and out on their own slow cycles (owner 2026-08-25)
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# Phase 25 — Background: No Motion, Only Fading Light
|
||||
|
||||
**Owner report (2026-08-25, chat, verbatim):** "It should be smooth,
|
||||
fluxuating, dimming and brightening, but not moving. Different bright
|
||||
spots should slowly fade in and out." **Story:**
|
||||
`.agent/user_stories/background-no-motion.md`.
|
||||
|
||||
## Root causes (phase-22 measurements as context)
|
||||
|
||||
1. **"Jitters down and to the right every second" = the grid drift.**
|
||||
`bg-grid-drift` translated the 44px grid 44px per 60s (≈0.73px/s)
|
||||
diagonally down-right — exactly the reported direction. A 1px line
|
||||
moved sub-pixel by sub-pixel rasterizes with per-frame stepping, not
|
||||
smooth motion, so it reads as a once-per-second jitter. Phase 22
|
||||
had made that drift *visible* (60% line alpha + widened mask;
|
||||
measured 1.82/765 mean pixel change over 5s in the grid zone vs
|
||||
0.48 pre-phase-22) — that is precisely why it now reads as jitter.
|
||||
Confirmed in the `before.png`/`before_4s.png` pair below: a
|
||||
grid-only region far from every glow changes by 2.98/765 mean over
|
||||
4s (4.7% of its pixels) — the grid itself is moving.
|
||||
2. **"Slowly blinks brighter and darker" = the whole-layer breathe.**
|
||||
`bg-glow-breathe` swung the entire glow layer's opacity 0.85↔1 over
|
||||
14s (alternate) plus `scale(1)↔scale(1.05)` (a faint zoom). One
|
||||
synchronized pulse of the whole background reads as a blink; the
|
||||
before pair's uniform change across both glow corners (mean
|
||||
2.19/765 over the full frame) is that single pulse.
|
||||
|
||||
## Design change (styles.css — pure CSS, zero JS, no blur, palette
|
||||
untouched; task 01)
|
||||
|
||||
| property | phase 22 | phase 25 |
|
||||
|---|---|---|
|
||||
| grid (`body::before`) | `bg-grid-drift 60s linear infinite` (0→44px, 0.73px/s) | **static** — animation removed, `bg-grid-drift` deleted (44px cells, 60% `--line` 1px lines, widened mask kept) |
|
||||
| glow | one whole-layer `bg-glow-breathe` (14s, opacity 0.85↔1 + scale 1↔1.05) | **three independent spot layers, opacity-only fades** |
|
||||
| spot A `body::after` | indigo + cyan on one layer | indigo `rgb(109 120 242 / 0.14)` 56rem @ 12%/8% — `bg-glow-a` **26s** ease-in-out infinite, low 0.25 |
|
||||
| spot B `html::before` | — | cyan `rgb(34 211 238 / 0.10)` 60rem @ 88%/92% — `bg-glow-b` **34s** ease-in-out **−12s** infinite, low 0.20 |
|
||||
| spot C `html::after` | — | indigo `rgb(109 120 242 / 0.09)` 52rem @ 14%/86% — `bg-glow-c` **42s** ease-in-out **−23s** infinite, low 0.15 |
|
||||
| keyframes | `bg-grid-drift` (background-position), `bg-glow-breathe` (opacity + transform) | `bg-glow-a/b/c` — **opacity only** (0%/100% low → 50% 1) |
|
||||
| reduced motion | stills the two body layers | stills **all four** layers |
|
||||
|
||||
`html::before`/`html::after` join as background layers: `<html>` is
|
||||
the root stacking context, so their `z-index: -1` pseudo-elements
|
||||
paint above the `var(--bg)` canvas and below the transparent,
|
||||
non-stacking `<body>`'s content (verified live — E2E test 6). 26/34/42s
|
||||
with negative delays (LCM 4641s) keep the cycles out of phase, so the
|
||||
composite pattern effectively never repeats within a viewing session.
|
||||
No LOCKED anchor changed (A11 pure CSS / zero JS / no CDN / no new
|
||||
assets; no `filter: blur`).
|
||||
|
||||
## Before / after evidence (1280×800 headless Chromium, shots ~4s
|
||||
apart; `.agent/screenshots/25_background_no_motion/`)
|
||||
|
||||
- `before.png` / `before_4s.png` (captured **pre-change**, task 01):
|
||||
down-right grid jitter + the uniform whole-layer pulse.
|
||||
- `after.png` / `after_4s.png` (captured post-change, task 02):
|
||||
brightness changes; the grid is bit-identical.
|
||||
|
||||
Region diffs (pure-stdlib PNG decode; "changed" = pixels with
|
||||
per-channel Δ sum > 12/765):
|
||||
|
||||
| region | before pair: changed / mean\|d\| | after pair: changed / mean\|d\| |
|
||||
|---|---|---|
|
||||
| full frame | 42,316 (4.1%) / 2.190 | 8,357 (0.8%) / 1.754 |
|
||||
| grid-only patch (900,30)–(1250,120) | 1,484 (4.7%) / **2.979 — the grid moves** | 0 (0.0%) / **0.000 — bit-identical** |
|
||||
| spot A center (154,64) patch | 406 (4.1%) / 3.198 | 4,018 (40.2%) / 6.595 — mid-fade |
|
||||
| spot C clip (0,500)–(500,800) | 6,469 (4.3%) / 1.274 | 0 / 3.247 — gentle fade, no >12/765 steps |
|
||||
| spot C center (179,688) patch | 63 (0.6%) / 0.789 | 0 / 6.003 — visibly brightening |
|
||||
| spot B clip (780,500)–(1280,800) | 5,784 (3.9%) / 2.037 | 0 / 1.211 — slow fade at this phase |
|
||||
|
||||
Read: after the change, the only region that moves pixel-by-pixel is
|
||||
the light itself (spot A's 26s cycle passes through its steepest
|
||||
mid-fade in the 4s window); every region that the grid drift used to
|
||||
scrub through is now byte-identical — no positional shift anywhere.
|
||||
|
||||
## E2E story gate (task 02) — `tests/e2e/test_background_no_motion.py`
|
||||
|
||||
8 tests, green in isolation (see "Results"). The deterministic
|
||||
no-movement proof is test 3: a live Chromium walk of
|
||||
`document.styleSheets` shows the set of properties declared across
|
||||
every frame of every `bg-*` @keyframes rule is exactly `{opacity}`.
|
||||
Regression suites adapted to the new contract:
|
||||
`tests/e2e/test_background_animation.py` (grid static + `bg-glow-a`
|
||||
running + the three spot timelines advance + 4-layer contracts + 360px
|
||||
pin) and `tests/e2e/test_dark_tech_theme.py`
|
||||
(`test_animated_background` → static grid + 26/34/42s spots;
|
||||
`test_reduced_motion_honored` → all four layers stilled).
|
||||
|
||||
## UI Structure Check (AGENTS.md rule 5)
|
||||
- **Layers behind content:** E2E test 6 pins `position: fixed`,
|
||||
`z-index: -1`, `pointer-events: none`, `inset: 0` on all four
|
||||
pseudo-layers live, plus the html-canvas / body-transparent
|
||||
no-occlusion pair (`rgb(10, 14, 23)` / `rgba(0, 0, 0, 0)`).
|
||||
- **No text/contrast impact:** the change touches only background
|
||||
layers (no palette token, no text on the layers); the phase-08
|
||||
contrast suite stays green.
|
||||
- **No 360px overflow:** E2E test 8 — `scrollWidth <= clientWidth` at
|
||||
360×740 with all four `fixed; inset: 0` layers live.
|
||||
Reference in New Issue
Block a user