← back to Dead Agentabrams
docs(TK-11101): perf+a11y audit of room/index.html — findings + reviewed patch (not applied; for dead-orb)
062e20eccd43d12b7a71c31217e809834e28b46b · 2026-09-02 09:06:02 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbuCGne4EgqBfU2KzJGhXL
Files touched
A docs/TK-11101-perf-a11y-proposal.md
Diff
commit 062e20eccd43d12b7a71c31217e809834e28b46b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 2 09:06:02 2026 -0700
docs(TK-11101): perf+a11y audit of room/index.html — findings + reviewed patch (not applied; for dead-orb)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbuCGne4EgqBfU2KzJGhXL
---
docs/TK-11101-perf-a11y-proposal.md | 74 +++++++++++++++++++++++++++++++++++++
1 file changed, 74 insertions(+)
diff --git a/docs/TK-11101-perf-a11y-proposal.md b/docs/TK-11101-perf-a11y-proposal.md
new file mode 100644
index 0000000..313e5be
--- /dev/null
+++ b/docs/TK-11101-perf-a11y-proposal.md
@@ -0,0 +1,74 @@
+# TK-11101 — `room/index.html` Performance + Accessibility Proposal
+
+**Author:** dead-t3 · **For:** dead-orb (owner of `room/index.html`) · **Date:** 2026-09-02
+**Status:** PROPOSAL — a reviewed unified diff is attached; **nothing has been applied to `room/index.html`.**
+**Cost:** $0 (all local — python static server + local headless Chrome via Playwright; no paid API except one codex sanity-pass ~$0.02).
+
+---
+
+## How this was measured
+
+- Served the repo locally (`python3 -m http.server`, `127.0.0.1:8790`) — **never deployed**.
+- Drove the page in headless Chrome (Playwright) against `http://127.0.0.1:8790/room/index.html`.
+- Ran the **`a11y-check`** skill checklist over the dock/controls + canvas layer.
+- Built the fix on a **copy**, re-served it, and smoke-tested the patched build (0 JS errors).
+- Ran the findings past **codex (gpt-5.3-codex)** — confirmed valid, no false positives.
+
+The three animated layers are three stacked full-screen `<canvas>`: `#sky` + `#scene` (2D) and `#orb` (Three.js/WebGL). There are **three independent `requestAnimationFrame` loops** running: the 2D `frame()`, the WebGL orb `loop()`, and the audio `sampleLevel()`.
+
+### Empirical results
+| Probe | Result |
+|---|---|
+| Idle FPS (headless, this machine) | ~72 fps, worst frame **14.9 ms** — headroom *on a fast Mac*; the concern is low-end/battery + the correctness bugs below |
+| Canvas count / sizing | 3 × full-screen canvases, `devicePixelRatio` capped at **2** → on a Retina display that is ~3× (2400×2216) buffers |
+| 2D `frame()` while **paused AND reduced-motion ON** | **Still redrawing** — `scene` pixels changed across 450 ms (full pipeline runs every frame) |
+| WebGL orb vs reduced-motion | orb `loop()` contains **zero** reference to `S.reduced`/`S.playing` → renders WebGL every frame regardless |
+| `sampleLevel()` audio loop | `_looping` is set `true` once and **never cleared** → keeps sampling every frame after "React to Music" is turned off / audio paused |
+
+---
+
+## Findings & fixes (all in the attached patch unless noted)
+
+### P1 — WebGL orb ignores Reduced Motion *(WCAG 2.3.3 · perf/battery)* — **HIGH**
+The orb's own rAF loop spins + re-renders WebGL every frame even when the visitor has Reduced Motion on. The orb IIFE is a separate closure and can't see the visual `S` state.
+**Fix (EDIT B + C):** the 2D loop publishes `window.SRM.reduced` / `window.SRM.playing` (SRM is already the cross-script bridge); the orb loop renders **one calm static frame then early-returns** while reduced. Stops the most prominent 3D motion *and* the WebGL draw cost for reduced-motion users.
+
+### P2 — Audio analysis loop never stops *(perf)* — **MEDIUM**
+`sampleLevel()` recurses via rAF forever once React-to-Music is first enabled — it keeps calling `getByteFrequencyData()` every frame after the feature is toggled **off** or audio is paused.
+**Fix (EDIT D1 + D2):** early-return + `_looping=false` when `!SRM.react || audio.paused`; the react button and the audio `play` event restart it. Net: the analyser loop only runs while it is actually analyzing.
+
+### P3 — 2D `frame()` never idles *(perf)* — **MEDIUM — recommendation only, NOT patched**
+Confirmed empirically: the full draw pipeline (3 canyon layers + river + boat + venue + motifs) redraws every frame even while paused + reduced, purely to animate the one intended idle "backflip bear."
+**Deferred on purpose** (codex agreed): a safe fix (dirty-flag or a reduced frame-rate path under reduced-motion) interacts with the idle-bear intent and wants dead-orb's judgment. Recommended follow-up: when `S.reduced`, throttle the 2D loop to ~30 fps, or gate the heavy background layers behind a `dirty` flag so a static scene isn't repainted at 60 fps.
+
+### A1 — Broken `listbox` / `tablist` ARIA patterns *(WCAG 4.1.2 / 2.1.1)* — **HIGH**
+`#mrMap` is `role="listbox"` with child `<button role="option">`, and `#mrSrc` is `role="tablist"` with `role="tab"` buttons — but there is **no roving tabindex, no arrow-key navigation, no `aria-activedescendant`, and no tabpanels**. Screen readers announce a widget contract the page doesn't honor.
+**Fix (EDIT E1–E5):** downgrade both to `role="group"` and make the source buttons proper `aria-pressed` toggle buttons; drop the phantom `role="option"` on the year tiles. They are already real focusable `<button>`s, so **Tab access and Enter/Space activation are unchanged** — only the misleading roles are removed. (Full listbox/tablist keyboard could be implemented instead, but "no-pattern listbox" is worse than plain grouped buttons — codex concurred.)
+
+### A2 — `prefers-reduced-motion` CSS leaks *(WCAG 2.3.3)* — **MEDIUM**
+The `@media (prefers-reduced-motion: reduce)` block only killed `transition` on 4 selectors. The infinite `@keyframes pulse` (playing live-dot) and `mrPulse` (loading year tiles) keep animating.
+**Fix (EDIT A):** add the standard reduced-motion animation reset + explicit `animation:none` on the two known infinite loops.
+*Codex caveat:* a global `animation-duration:.001ms` can surprise external animation libraries — this file has none (only Three.js via rAF), so it's safe here, but dead-orb may prefer to keep only the two targeted `animation:none` rules if that feels safer.
+
+### A3 — Reduced-motion not re-checked at runtime *(nicety)* — **LOW**
+OS Reduced-Motion is read once at load. **Fix (EDIT F):** a `matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', …)` keeps `S.reduced` + the button's `aria-pressed` in sync if the visitor flips the OS setting mid-session.
+
+### Notes (no patch — dead-orb's call)
+- **Duplicate `<h1>`:** one `.sr` `<h1>` (line 259) + one `aria-hidden` visual `<h1>` (line 269). The a11y tree sees one, so it's benign, but it's a validation smell.
+- **Contrast:** `--ink-dim` (#c9a98c) on the near-black panels estimates ~7–8:1 (passes AA); worth a quick axe confirm on the 10 px uppercase labels.
+- **`#mrResults` / `#mrTracks` `role="list"` with `<button>` children:** lesser issue (buttons aren't `listitem`); left alone to keep the patch tight.
+- The console errors seen in testing are all blocked `archive.org` fetches (the sandbox has no external network) — **not code defects.**
+
+---
+
+## Apply / verify (for dead-orb)
+
+```bash
+cd ~/Projects/dead-agentabrams
+git apply --check docs/TK-11101-perf-a11y.patch # dry-run — verified clean by dead-t3
+git apply docs/TK-11101-perf-a11y.patch # apply when ready
+```
+
+**Patch:** `docs/TK-11101-perf-a11y.patch` — 11 hunks, applies cleanly, `room/index.html` currently untouched.
+**Smoke-tested patched build:** 0 JS errors; all controls wired; SRM reduced/playing bridge live; roles corrected; `m` keyboard shortcut still toggles reduced motion.
+Local-only. Any deploy / DNS / publish stays Steve-gated.
← 306d63a auto-data-snapshot: 2026-09-02T09:04:08 (28 data files) — .p
·
back to Dead Agentabrams
·
room(TK-11101): apply perf+a11y fixes (Steve-approved deploy ebd5538 →