wifi-densepose/api-docs/adr/ADR-169-adam-mode-light-the...

12 KiB
Raw Blame History

ADR-169: adam-mode — light theme toggle for the three.js realtime demo

Field Value
Status Proposed
Date 2026-06-02
Deciders ruv
Codename adam-mode
Scope examples/three.js/demos/05-skinned-realtime.html (primary), demos 0104 (follow-on)
Relates to ADR-019 (sensing-only UI), ADR-035 (live sensing UI accuracy)
Tracking issue none yet

1. Context

examples/three.js/demos/05-skinned-realtime.html (build stamp 2026-05-15-fps-tune) is the live MediaPipe → Mixamo retargeting + ESP32 CSI overlay demo. It currently ships a single, opinionated dark theme:

  • Body --bg: #050507 (near-black), --text: #d8c69a (warm beige).
  • Amber accents (--amber: #ffb840, --amber-hot: #ffe09f) on panels and controls.
  • Two full-screen overlays: a radial-vignette .overlay-frame and a 50%-opacity CRT-style .scanlines layer.
  • Three.js scene matches: scene.background = new THREE.Color(0x050507) and scene.fog = new THREE.FogExp2(0x050507, 0.06) (lines 269270).

The dark/amber CRT aesthetic is intentional for screen-recording and "command-centre" feel, but it has real failure modes:

  1. Daylight visibility — Demoing the live capture on a laptop in a sunlit room is unreadable; the dark background absorbs ambient glare and the amber-on-dark contrast disappears.
  2. Recording for embedded/print contexts — When the demo's screen is captured for documentation, blog posts, or HA blueprints, the dark theme bleeds into surrounding white content and looks heavy.
  3. Accessibility — A subset of users with light-sensitive retinas (the inverse of typical photophobia) report the high amber-on-near-black combination strains them; high-contrast light themes are easier.
  4. Operator pairing with a light-mode IDE — Many operators run a light-mode browser alongside a dark-mode IDE and want the demo to match the browser, not the IDE.

A toggle is the right answer because none of these reasons are universal — some sessions and some users want each mode.

1.1 What this ADR is not

  • Not a redesign. The amber accent stays; only the surface colours and overlays swap. The information density, panel layout, and three.js scene geometry are unchanged.
  • Not a multi-theme system. We add exactly two themes: the existing dark (default, unnamed) and adam-mode (light). Future themes would need a new ADR.
  • Not a backend / data-model change. Pure presentation.
  • Not yet propagated to demos 0104. Those follow-on after adam-mode lands on demo 05 and is validated.

2. Decision

Add a client-side theme toggle to 05-skinned-realtime.html that switches between the existing dark theme and a new light theme called adam-mode, driven by a data-theme="adam" attribute on <body> plus a sibling :root[data-theme="adam"] CSS block that re-defines the existing custom properties. A new toggle button in the existing #helpers panel switches between modes and persists the choice in localStorage under the key ruview.theme.

2.1 CSS — the colour swap

Add immediately after the existing :root { ... } block in <style>:

:root[data-theme="adam"] {
    --bg: #f6f2ea;
    --bg-panel: rgba(252, 250, 246, 0.92);
    --amber: #b8741a;        /* deeper amber, readable on cream */
    --amber-hot: #8a5612;    /* deepest amber for emphasis text */
    --cyan: #1a6f8a;         /* slate cyan */
    --magenta: #a8348a;      /* slate magenta */
    --text: #2a241c;         /* near-black warm */
    --text-mute: #7a6f5d;    /* warm grey */
    --green: #1f7a32;        /* forest green */
    --red: #b03a1a;          /* burnt sienna */
    --border: rgba(184, 116, 26, 0.28);
}

Every existing element already reads from these custom properties, so the swap is automatic for panels, text, borders, and bar fills. No per-element CSS rewrites required.

2.2 Overlay handling

The vignette and scanlines are dark-theme aesthetics. In adam-mode they would muddy the cream background. Two new rules:

:root[data-theme="adam"] .overlay-frame {
    background:
        radial-gradient(ellipse at center, transparent 70%, rgba(184,116,26,0.10) 100%),
        linear-gradient(180deg, rgba(184,116,26,0.06) 0%, transparent 18%, transparent 82%, rgba(184,116,26,0.08) 100%);
}
:root[data-theme="adam"] .scanlines {
    opacity: 0.15;
    mix-blend-mode: multiply;
}

The vignette is preserved but inverted in colour and lightened; scanlines drop to 15 % opacity and switch from overlay to multiply blend so they read as faint paper texture rather than CRT lines.

2.3 Three.js scene reactivity

Two scene colours are hard-coded at construction (lines 269270). Replace them with a function call that reads the current theme:

function themeSceneColors(theme) {
    return theme === 'adam'
        ? { bg: 0xf6f2ea, fogDensity: 0.025 }
        : { bg: 0x050507, fogDensity: 0.06 };
}
function applySceneTheme(theme) {
    const c = themeSceneColors(theme);
    scene.background = new THREE.Color(c.bg);
    scene.fog = new THREE.FogExp2(c.bg, c.fogDensity);
    renderer.setClearColor(c.bg, 1.0);
}

Called once after renderer is constructed, then again from the toggle handler.

scene.fog density drops in adam-mode because exponential fog on a light background reads as "haze" much more strongly than on dark — 0.06 → 0.025 keeps the falloff visible without losing the figure into the background.

2.4 UI toggle

Add to the #helpers panel (top of its labels list):

<label class="theme-toggle">
    <input type="checkbox" id="adam-mode-toggle">
    <span>adam-mode (light)</span>
    <span class="swatch" style="background: var(--amber)"></span>
</label>

Handler:

const THEME_KEY = 'ruview.theme';
const root = document.documentElement;
const toggle = document.getElementById('adam-mode-toggle');

function applyTheme(theme) {
    if (theme === 'adam') {
        root.setAttribute('data-theme', 'adam');
        toggle.checked = true;
    } else {
        root.removeAttribute('data-theme');
        toggle.checked = false;
    }
    applySceneTheme(theme);
    try { localStorage.setItem(THEME_KEY, theme); } catch (_) {}
}

const initialTheme = (() => {
    try { return localStorage.getItem(THEME_KEY) || 'dark'; }
    catch (_) { return 'dark'; }
})();
applyTheme(initialTheme);

toggle.addEventListener('change', e => {
    applyTheme(e.target.checked ? 'adam' : 'dark');
});

2.5 Why "adam-mode" as the codename

The user picked the name. It is a project-specific brand — distinct from the generic "light mode" terminology that other modes (--theme=high-contrast, --theme=print) may eventually need. Keeping a codename makes the toggle searchable in the codebase, the localStorage key portable across the demo set, and avoids ambiguity if dark itself is later renamed.

The string "adam" is the only literal value the data-theme attribute and the localStorage key ever take. "dark" is the implicit default (no attribute, no stored value).

2.6 Rejected alternatives

Alternative Rejected because
Use prefers-color-scheme: light only, no toggle Operators frequently want the opposite of their OS preference for screen-recording or daylight desk use. Auto-only frustrates the actual use case.
Ship two separate HTML files (05-…-dark.html, 05-…-light.html) Doubles maintenance for every future demo edit. No path to per-session toggle.
Build a full multi-theme system with a runtime registry Premature. Two themes don't need a registry; the data-theme="adam" attribute is the registry.
Use Tailwind / DaisyUI / a CSS framework Demos are intentionally stand-alone single-file HTML for portability. No build step exists; adding one for theming is wrong shape.
Adopt the cognitum-v0 / HOMECORE design tokens (--hc-* from examples/frontend/) That design system is dark-only by intent (ADR-131). adam-mode is the light counterpart needed in demo contexts, not HA dashboard contexts.
Make adam-mode the default Breaks the dark-aesthetic recording context this demo was originally built for. Default stays dark; toggle stays opt-in.

3. Consequences

3.1 Positive

  • Demo is usable in daylight, in printed documentation, on light-mode browsers, and by users who find the dark-amber combination fatiguing.
  • Toggle persists across reloads via localStorage — set once, sticks.
  • No structural change to information density, panel layout, or three.js scene geometry. Operators familiar with the dark theme can switch and still find every readout in the same place.
  • Implementation is contained — a single <style> block addition, a single button, a ~25-line JS handler, and a swap of two scene-construction lines.

3.2 Negative

  • Two themes to maintain. Any future colour change requires updating both :root blocks. Mitigated by keeping the existing custom-property names — adam-mode's values are the only edits.
  • The vignette + scanlines lose some of the CRT charm in adam-mode. Tradeoff accepted by design.
  • One additional localStorage slot consumed per origin (ruview.theme).
  • The amber accent in adam-mode (#b8741a) is visibly different from the dark-mode amber (#ffb840) — they share the same CSS variable name but a screenshot from each mode is not pixel-comparable. This is the correct call for accessibility (the bright amber is unreadable on cream) but does mean side-by-side comparisons need both screenshots labelled.

3.3 Risks

Risk Likelihood Mitigation
Future demo edits update one :root block and forget the other Medium A lint script in scripts/ could grep both blocks for matching key sets; documented as P2 follow-up.
localStorage blocked by privacy settings Low All accesses are wrapped in try/catch; falls back to dark.
Three.js fog density of 0.025 still washes out the model on adam-mode Low Empirically tuned during implementation; if it does, drop to 0.015 or remove fog entirely in adam-mode.
User on a high-DPI display sees scanlines as visible paper texture even at 15 % opacity Low If reported, drop to 8 % or hide scanlines entirely in adam-mode.

4. Implementation plan

Tiny scope — single file. No swarm needed.

  1. Add :root[data-theme="adam"] CSS block and the two overlay overrides.
  2. Refactor scene background + fog into the two helper functions themeSceneColors() and applySceneTheme().
  3. Add <label> markup and handler script.
  4. Verify in a browser at http://127.0.0.1:8765/examples/three.js/demos/05-skinned-realtime.html — toggle on, reload, confirm adam-mode persists; toggle off, reload, confirm dark persists.
  5. Smoke-screenshot both modes; commit.

Acceptance criteria:

  • Toggle checkbox visible in #helpers panel.
  • Clicking the toggle swaps colours within one frame.
  • Reload preserves last choice.
  • Three.js scene background follows the toggle (no dark frame visible behind a light HUD or vice-versa).
  • Existing dark-theme appearance is byte-identical when toggle is off.

5. Test plan

  • Manual visual check in two themes (no automated visual regression — demos aren't in the CI test loop today).
  • view-source confirms the new CSS block, the toggle markup, and the handler are present.
  • DevTools localStorage shows ruview.theme after a toggle.
  • Three.js inspector (or a console.log(scene.background.getHexString())) confirms scene colour swap.

6. Follow-on work (out of scope for this ADR)

  • Roll adam-mode into demos 0104. Each demo has its own <style> block; the same data-theme="adam" selector and the same JS handler can be copied.
  • Honor prefers-color-scheme: light on first load if localStorage has no stored choice. Trivial three-line addition.
  • Add a high-contrast theme for accessibility (separate ADR).
  • Lint script that asserts both :root blocks declare the same custom-property names.
  • ADR-019 — sensing-only UI mode (Gaussian splats viewer)
  • ADR-035 — live sensing UI accuracy norms (which this demo follows)
  • ADR-131 — HOMECORE / cognitum-v0 design tokens (dark-only, separate context)