docs: capture playground redesign intent from _rewound snapshot

Source: archive/playground-redesign-2026-snapshot branch (commit 287e3a1).
Reference-only — playground is being rewritten in SolidJS, so the redesign's
implementation will not be merged, but its design intent is preserved here
for the rewrite team.
This commit is contained in:
w1n5t0n 2026-04-29 13:32:39 +03:00
parent d3373a4e44
commit 01f6522fd5

View file

@ -0,0 +1,340 @@
# Playground Redesign — 2026 (Design Intent)
> **Purpose of this doc.** Captures design intent of an unfinished playground UI
> redesign that lived as uncommitted changes in a `MEMLNaut-NISPS_rewound`
> working copy. The implementation will not be merged into the current vanilla
> playground — the playground is being rewritten in SolidJS — but the *design*
> shouldn't be lost. Read this as a feature spec for the SolidJS rewrite, not as
> a description of current code.
>
> **Source snapshot.** `archive/playground-redesign-2026-snapshot` branch in
> this repo. Single commit `287e3a1` on top of `ddb6b77`. Three files:
> `playground/a-immersive.html`, `playground/css/a-immersive.css`,
> `playground/js/a-app.js`. ~1200 lines net of changes.
>
> **Status.** Reference-only. Do not merge into the vanilla playground — those
> files have moved on independently in `main` and a textual merge would be
> meaningless.
---
## Context
The pre-redesign playground UI was organised around a collapsible **bottom
sheet** containing a sticky **floating bar** (mode toggles, train/randomize
buttons, follow toggle, expand chevron). The sheet would expand upward to show
synth/visual controls. RL thumbs-up/down floating buttons sat just above the
collapsed sheet at `bottom: 92px`.
The redesign moves to a **right-side macOS-style dock + drawer system** and
makes the **top heatmap strip directly interactive**. It also adds **undo** for
RL actions and a **per-parameter override system** that unifies how visual-mode
and synth-mode parameters can be constrained.
There are five themes, summarised here in decreasing order of architectural
weight.
---
## Theme 1 — Right-side dock + drawer system
### What changed
Replaces the bottom-sheet pattern with a vertical dock pinned to the right edge
of the viewport, plus a stack of named drawers that open to the left of the
dock when their dock icon is activated.
- **Removed:** `<div class="bottom-sheet collapsed">`, `<div class="floating-bar">`,
the sheet expand/collapse chevron, sheet-internal sticky mode bar, sheet
status line.
- **Removed JS:** `wireBottomSheet()`, `$bottomSheet`, `$sheetContent`,
`$floatingBar`, `$chevronBtn`, `sheetExpanded` state.
- **Added:** `<div class="dock" id="dock">` with five icon buttons:
- `data-drawer="training"` — Training (Add Example, Train, Clear Ex, Clear
All, Randomize, plus preset chips: Calm/Chaos, Rainbow, Vortex, Spiral,
Embers; loss-canvas section)
- `data-drawer="mode"` — Input/Output mode toggles (was the floating-bar
pill toggles: Joystick/Hands and Visual/Synth)
- `data-drawer="synth"` — Synth controls
- `data-drawer="params"` — Engine parameters (see Theme 4)
- `data-drawer="help"` — Help
- **Added:** `<div class="drawer-stack" id="drawer-stack">` containing one
`<div class="drawer hidden" data-drawer="…">` per icon, each with a
`drawer-header` (title + close ×) and `drawer-body`.
- **Added JS:** `wireDock()` to hook up icon clicks and drawer close buttons.
### Inferred intent
- Reclaim canvas real estate. The bottom-sheet ate vertical space whenever
expanded; a side dock keeps controls flush to one edge and only one drawer
opens at a time.
- Surface mode controls (Visual/Synth, Joystick/Hands) without forcing the
user to expand the sheet. Each function lives behind one click on a labeled
icon.
- macOS-dock visual idiom is a familiar metaphor — users intuit
"click icon → panel appears."
- The action-row inside Training drawer (Add Example / Train / Clear Ex /
Clear All / Randomize) is grouped by purpose rather than by toolbar real
estate constraints, so destructive/dim variants (`.action-btn.dim`) read
as secondary.
### Key UX flow
1. Canvas is fullscreen by default, no chrome over it except top heatmap, RL
buttons, joystick, and the right dock.
2. User clicks dock icon → corresponding drawer slides in from the right
(between dock and canvas), other drawers close.
3. User clicks ✕ on drawer header *or* the same dock icon again → drawer
closes, canvas is unobstructed.
4. Mutually exclusive drawers (only one open at a time) keeps the visual
hierarchy simple.
### Implementation notes for SolidJS rewrite
- This is the largest theme and pulls HTML, CSS, and JS together. In SolidJS
it should naturally become a `<Dock>` component listing icon → drawer
pairs, plus a single `<DrawerStack>` rendering whichever is active from a
signal.
- CSS custom properties `--dock-width: 48px` and `--dock-gap: 8px` are
introduced; layout (joystick, hand-pip, dev-panel) is offset by these.
- Old positioning constants — joystick & hand-pip at `bottom: 136px`, RL
buttons at `bottom: 92px` — were tied to the collapsed bottom sheet's
88px height. With no sheet, they shift down (joystick/hand-pip to 100px,
RL buttons to 36px). The rewrite should drive these from layout, not
hardcode.
- `dev-panel` shifts right edge from `right: 16px` to
`right: calc(var(--dock-width) + 24px)` to clear the dock.
---
## Theme 2 — Interactive heatmap strip
### What changed
The top heatmap strip — previously read-only visualization of model outputs —
becomes the primary parameter input surface.
### Two interactions per cell
1. **Drag** (pointermove with motion threshold > 3px) horizontally across a
cell → directly sets that parameter's value to the normalized x-position
within the cell.
2. **Click** (pointerup with no motion) → toggles a per-parameter popup with
override controls (min, max, curve, freeze).
### Tooltip
Existing tooltip extended:
- During hover (no drag): shows `{name}: {value}` with a `▾` indicator hinting
the bar is clickable.
- During drag: stays visible and updates live as user scrubs.
- New CSS: `.heatmap-cell.dragging` with `cursor: ew-resize`,
`.heatmap-cell-bar { pointer-events: none }` so child bar doesn't intercept
parent pointer events.
### Popup contents
Per-parameter popup (`#param-popup` or similar) built on demand for the
clicked param:
- min slider (range 01)
- max slider (range 01)
- curve slider (range 01, see Theme 4)
- freeze toggle — when frozen, dragging the heatmap bar updates a *fixed
value* directly rather than going through the model
- close button / clicks-outside dismissal
- show/hide handled with `popupHideTimer` (300ms grace period to let user
move pointer from heatmap cell into the popup without dismiss-on-leave)
### New JS
- `setHeatmapValue(paramIndex, e, cell)` — converts pointer x to normalized
value, applies override semantics (writes to `ov.fixedValue` if frozen),
routes outputs, syncs UI.
- `showParamPopup(paramIndex, cell)` — positions popup near the cell.
- `hideParamPopup()` — clears active popup state.
- `wireParamPopup()` — wires popup controls to update underlying override.
- Tracking globals: `activePopupParam`, `popupHideTimer`.
### Inferred intent
- Make the heatmap a control surface, not just a readout. Saves a trip to
the bottom-sheet/drawer for common adjustments.
- Per-parameter freeze enables hybrid workflow: freeze a few params at fixed
values while letting the model drive the rest.
- Popup is in-context (anchored to the bar) rather than in a side panel,
reducing eye travel.
### Key UX flow
- **Quick adjust:** point at heatmap bar → drag horizontally → value changes
live, model output reroutes immediately.
- **Override setup:** click bar → popup opens → adjust min/max to constrain
range, or freeze at fixed value, or shape curve (see Theme 4) → click
outside or wait for cell pointer-leave to dismiss.
### Notes
- `.heatmap-cell-frozen` CSS class dims frozen cells visually so user knows
what's manually overridden.
- Drag detection uses 3px motion threshold to disambiguate click-vs-drag.
- Pointer capture (`setPointerCapture`) ensures drag continues even if
pointer leaves the cell.
---
## Theme 3 — Undo
### What changed
Adds an undo button below the RL thumbs-up/down buttons, with a 20-step
history of weight snapshots taken before each RL action.
### Implementation
- New HTML: `<button class="rl-undo-btn" id="btn-undo">` in the RL buttons
cluster, with an SVG curl-arrow icon.
- New CSS: `.rl-undo-btn` (28px circle, dim-by-default at opacity 0.4,
brightens to 0.8 on hover, 0.7 when undo is available),
`.rl-undo-btn.has-undo` and `.rl-undo-btn.flash` states. Animation
`rl-flash` (0.25s ease-out) for feedback after undo.
- Layout: `.rl-buttons` becomes `display: grid` with two columns and two
rows; the undo button spans both columns in row 2 (`grid-column: 1 / -1`)
and is centered.
- New JS:
- `const undoStack = []` and `const MAX_UNDO = 20`
- `pushUndoSnapshot()` — called before each RL action; clones current
weights into the stack; pops oldest if over MAX_UNDO.
- `onUndo()` — pops last snapshot, restores weights, flashes button.
- `updateUndoButton()` — toggles `.has-undo` class based on
`undoStack.length > 0`.
### Inferred intent
- RL actions in this playground are exploratory and easy to "go too far" —
e.g. a thumbs-down sequence that drives the model into uninteresting
territory. Without undo, user has to retrain or randomize, losing all
history.
- Visual subtlety (low default opacity, only fully visible when there's
something to undo) keeps it out of the way until needed.
### Key UX flow
User clicks 👎 → push snapshot, apply update. Result is bad. User clicks
↶ → restores previous weights. Button flashes briefly to confirm.
### Notes
- This is the cleanest standalone theme — minimal coupling to the rest of
the redesign. Consider implementing first in the SolidJS rewrite to
validate the snapshot model.
---
## Theme 4 — Per-parameter override generalization
### What changed
Generalizes the existing `groupOverrides` system (which let synth-mode params
be constrained per-group) to also work in visual mode, and unifies access
behind a single `getParamOverride(i)` accessor.
### New shapes
- `const visualOverrides = VISUAL_PARAM_NAMES.map(() => ({ min: 0, max: 1, curve: 0.5, frozen: false, fixedValue: 0.5 }))`
— one override entry per visual parameter, parallel to the existing
per-section/per-param structure on the synth side.
- `getParamOverride(paramIndex)` — given a heatmap index, returns an object
with the same shape regardless of mode:
- In `visual` mode: returns the corresponding `visualOverrides[i]` entry
directly.
- In `synth` mode: looks up `paramToSection[paramIndex]` and returns a
proxy onto `groupOverrides[si].params[li]`, with `muted` mapped to
`frozen` so the popup UI can be mode-agnostic.
### Renames (cosmetic but indicative)
- `buildRawParams()``buildEngineParams()`
- `$rawParams``$engineParams`
- HTML id `raw-params``engine-params`
### New JS helpers
- `syncEngineParams()` — pull current values into the drawer-rendered slider
list.
- `formatEngineVal(v, step)` — display formatting for slider readouts.
### Other change
- `let rlExplorationDecay = 0.97` (was `const`) — runtime-tunable, presumably
exposed via an Engine drawer slider.
### Inferred intent
- Visual mode previously had no way to clamp/freeze individual parameters.
Synth mode did. The asymmetry forced the heatmap popup to behave
differently per mode, which is poor UX.
- Unifying behind one accessor lets Theme 2's popup code be mode-agnostic.
- "Engine" terminology generalizes "raw params" to whichever output system
is active.
### Notes
- This theme is **coupled to Theme 2** — the popup is what consumes the
override accessor. They should ship together (or, in the SolidJS rewrite,
be designed together as one feature: "per-parameter constraints + the UI
to set them").
---
## Theme 5 — Polish (positioning, status line)
Small adjustments forced by the larger redesign or independent quality-of-life
fixes:
- **Status line** moves from inside the sheet (`<div class="sheet-status">`)
to a free-floating pill anchored at `bottom: 8px`, centered, low-opacity
(`#status-line`). Lives outside any drawer; visible always.
- **Joystick & hand-pip** move from `bottom: 136px` to `bottom: 100px`
(sheet no longer takes up vertical space).
- **RL buttons** move from `bottom: 92px` to `bottom: 36px`, gap from 32px
to 12px, layout from flex to 2-col grid (to accommodate the spanning undo
button below).
- **`rlExplorationDecay`** changes from `const` to `let` (see Theme 4).
- **Heatmap tooltip** gets `▾` suffix to hint clickability and stays visible
during drag.
- **Hand-pip video** loses `/* Hidden — only used as MediaPipe source */`
comment on the position-absolute hidden video element. Behavior unchanged.
### Notes
- These are positioning constants tied to the shape of the surrounding
layout. The SolidJS rewrite shouldn't carry them forward as literals —
derive from layout / CSS variables.
---
## What this redesign does NOT change
For completeness — these survived the redesign untouched:
- Joystick container and joystick interaction logic (`wireJoystick`).
- Hand tracker / MediaPipe pipeline (`createDevPanel(() => handTracker)`).
- Synth visualizer canvas (`#synth-vis-canvas`, `SYNTH_SECTIONS`).
- Loss canvas drawing.
- Help overlay (`wireHelp`).
- Group overrides for synth mode (extended in Theme 4, but base mechanism
unchanged).
- Gamepad input.
- Output mode routing logic (`routeOutputs`, `rawParamValues`).
---
## Summary table for the SolidJS rewrite
| Theme | Carry forward? | How it should land in SolidJS |
|---|---|---|
| 1. Dock + drawers | Yes | `<Dock>` + `<DrawerStack>` driven by signal for active drawer |
| 2. Interactive heatmap | Yes | Heatmap cells as components with pointer-event handlers; popup as a portal anchored to clicked cell |
| 3. Undo | Yes | Single store with `undo()` action + size cap; trivially reactive |
| 4. Param override generalization | Yes | One unified store keyed by param index; mode is just a derived accessor |
| 5. Polish | Re-derive | Don't carry positioning literals; recompute from layout |