memlnaut-nisps/docs/redesign/playground-2026.md
w1n5t0n 01f6522fd5 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.
2026-04-29 13:32:39 +03:00

340 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 |