docs(phase-10): respec Mix visualizer controls as in-flow container between back link and lava-lamp

This commit is contained in:
daniel-c-harvey
2026-06-16 20:05:59 -04:00
parent f7366b167c
commit b7a60f24c5
2 changed files with 129 additions and 49 deletions
+1 -1
View File
@@ -209,7 +209,7 @@ A **major reframe of the Mix visualizer's effects, controls, and color model**,
**The three reframes:**
- **Lava → CPU-physics wax blobs.** Keep the single-pass WebGL2 fragment renderer; add a small CPU-side per-frame physics step modeling ~1632 Lagrangian "wax blobs" (position/velocity/temperature/radius) uploaded as uniforms and blended with `smin` SDF metaballs. The waveform and lava share **the same plane WITH real 2D elastic collision** (blob↔waveform-boundary + blob↔blob) — the waveform pushes the fluid out of its way (read-only authority preserved; the fluid never deforms the waveform). **Refined by Daniel's Wave R2 eval:** the fluid must read **flat** (an evenly-lit unified body, NOT blobs with bright pointed centers / cone-like radial gradients) and **melt into one fluid** (low viscosity / strong coalescence — "behave more like a fluid," not stiff globs in contact; the slow wax-like *motion* damping and the freely-coalescing *surface* are independent). **Energy-coupled dynamics:** higher heat/energy → **smaller bubbles + more turbulence** (many small lively turbulent bubbles at high heat; fewer, larger, calmer wax at low heat) — the lamp should feel dynamic and fun. At heat 0 the wax rests on the floor and only collision moves it (collision always on, independent of heat); at heat max many small turbulent bubbles rise/morph per second. **Tuning anchor: Daniel's sweet spot is ~20% gravity / ~100% heat** — calibrate defaults/ranges so that combination lands lively. **Rejected: a full ping-pong FBO Navier-Stokes fluid sim** — a lava lamp is high-viscosity/low-turbulence, the opposite regime; large rewrite for unwanted realism. Deliberate later upgrade only.
- **Color → three-color OKLab gradient with three motions.** One source of truth (`DeepDrftPalettes`), no hardcoded hexes. Always A→B linear from the center line outward. Three combined motions: (1) anchors A/B **rotate among three theme colors X/Y/Z** at the rotation-speed control's rate — **OKLab interpolation, never through the rainbow** (the cyan fix is structural, not a tuning dial); (2) per-bar sinusoidal variation **baked at segment entry and fixed as the segment scrolls** — realized by **keying the sinusoid to mix-time** so it travels with the segment by construction (decided 2026-06-16; the explicit ring-buffer alternative is rejected for maintainability); (3) per-bar gradient curve shifts with scroll height (mostly A at bottom → mostly B at top). The static noise/frost texture is **removed** (Daniel: makes the screen look dirty).
- **Controls → seven knobs in an inline collapse/expand knob-bar.** Replaces the four: (1) waveform scroll speed [replaces resolution/zoom as a standalone control], (2) gradient rotation speed, (3) lava gravity, (4) lava heat, (5) blob density/size, (6) collision strength (soft mush→hard up-and-out throw), (7) **waveform width** [added in the Wave R2 eval — narrows the waveform band so the lava fluid has more room to move on loud/busy songs; scales the waveform's amplitude→width mapping and its collision boundary]. **NOT a popover or drawer — an inline collapse/expand knob-bar** (decided 2026-06-16): an `@if`-guarded **animated flex row of the seven `RadialKnob`s** living inline in the controls area, expanding/collapsing **in place** (CSS width/opacity/transform transition) so it reads as the controls collapsing/expanding, not a floating surface. The lava-lamp icon button toggles a bound bool — no MudPopover/MudDrawer. Styled to **match the NowPlaying hero aesthetic** (the session-detail hero overlay — translucent dark glass, overlay-label typography, `Color.Secondary`). Also: overflow-clip the visualizer to the **dynamic footer height** (the player bar changes height minimized/expanded) so visuals stop cleanly above it; the clip line is also the lava rest line.
- **Controls → seven knobs in an inline collapse/expand knob-bar.** Replaces the four: (1) waveform scroll speed [replaces resolution/zoom as a standalone control], (2) gradient rotation speed, (3) lava gravity, (4) lava heat, (5) blob density/size, (6) collision strength (soft mush→hard up-and-out throw), (7) **waveform width** [added in the Wave R2 eval — narrows the waveform band so the lava fluid has more room to move on loud/busy songs; scales the waveform's amplitude→width mapping and its collision boundary]. **NOT a popover or drawer — an in-flow controls container BETWEEN the back link (left) and the lava-lamp toggle (right)** on the detail top row (redesigned 2026-06-16, superseding the first realization). The first implementation read "inline collapse/expand" as an `position:absolute` bar floating under the lamp (`.mix-visualizer-controls-anchor`) — it clipped to a vertical sliver and read as a detached popover; **Daniel rejected it.** The redesign: `ReleaseDetailScaffold` gains a new optional **`TopRowCenter`** slot so the top row is `back | center-controls | lamp` (Track/Cut/Session supply neither → back-link-alone, reusability preserved); the seven `RadialKnob`s live in that center slot and **expand/collapse in the layout flow** (CSS width/opacity transition, container grows horizontally between back and lamp — **no `position:absolute`, no float, no overlay, no clipping, never overlaps the masthead/hero**). On narrow widths the container wraps in-flow to a second line / drops to the scaffold's `TopContent` band — never edge-clips. The lava-lamp icon button toggles a bound bool — no MudPopover/MudDrawer. Styled to **match the NowPlaying hero aesthetic** (the session-detail hero overlay — translucent dark glass, overlay-label typography, `Color.Secondary`). Also: overflow-clip the visualizer to the **dynamic footer height** (the player bar changes height minimized/expanded) so visuals stop cleanly above it; the clip line is also the lava rest line.
**Plus: redraw the lava-lamp glyph.** The current `DDIcons.LavaLamp` is rejected (Daniel: "form is shit, colors are shit"). Redraw to the classic 1970s silhouette — a wide truncated-cone metal **base**, a bulbous→roundedly-pointed teardrop **glass body**, a small cone **cap** ("offset cones") — with **navy fluid + moss blobs** (the theme's blue+green, faithful to the reference and on-theme) and a neutral/metallic base+cap. Authored in `DeepDrftShared.Client/Common/DDIcons.cs` as inner SVG markup (no `<svg>` wrapper; 24×24 viewBox); body silhouette `currentColor`, the two accent fills are commented literals traced to their `DeepDrftPalettes` source (SVG cannot resolve `var()`).
@@ -66,8 +66,12 @@ Cross-references (read these before implementing):
- `DeepDrftPublic.Client/Controls/MixZoomMapping.cs` — reused unchanged for the scroll-speed control.
- `DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor[.cs/.css]` — the bridge. Extend the handle
with the new control setters; the `.css` gains the overflow-clip work (§2).
- `DeepDrftPublic.Client/Pages/MixDetail.razor[.css]` — the page; the controls area gains the inline
collapse/expand knob-bar (§7).
- `DeepDrftPublic.Client/Pages/MixDetail.razor[.css]` — the page; the controls move into an **in-flow
container between the back link and the lava-lamp** on the detail top row (§7b). The old
`.mix-visualizer-controls-anchor` + `position:absolute` floating structure is **removed**.
- `DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor[.cs]` — the detail chrome; gains a new
optional **`TopRowCenter`** slot so the top row hosts `back | center-controls | action` (§7b). The slot
stays null for Track/Cut/Session, which render the back link alone — reusability preserved.
- `DeepDrftPublic.Client/Pages/SessionDetail.razor[.css]` — the **NowPlaying / hero aesthetic** the
knob-bar must match (§7e).
- `DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor[.css]` — the footer/player bar
@@ -85,8 +89,9 @@ Cross-references (read these before implementing):
metaballs in the existing fragment shader, sharing the *same plane* as the waveform **with real 2D
collision** — the waveform pushes the fluid out of its way. Replace the navy↔moss treatment with a
**three-color, OKLab-interpolated, per-segment-baked gradient** that animates along three combined
motions. Replace the four-knob popover with a **seven-knob inline collapse/expand knob-bar** styled to
match the hero NowPlaying aesthetic. Remove the static noise texture that makes the screen look dirty.
motions. Replace the four-knob popover with a **seven-knob in-flow controls container that sits between
the back link and the lava-lamp toggle** on the detail top row (reflowing the layout in place — not a
floating/absolutely-positioned bar), styled to match the hero NowPlaying aesthetic. Remove the static noise texture that makes the screen look dirty.
Redraw the lava-lamp trigger glyph to the classic 1970s silhouette (§7f).
**In scope.**
@@ -100,8 +105,8 @@ Redraw the lava-lamp trigger glyph to the classic 1970s silhouette (§7f).
height (§6).
- **Seven controls** replacing the four: scroll speed, gradient rotation speed, lava gravity, lava heat,
blob density/size, collision strength, waveform width (§7).
- The **inline collapse/expand knob-bar** (replacing the popover) styled to the NowPlaying hero
aesthetic (§7).
- The **in-flow controls container between the back link and the lava-lamp** (replacing the popover, and
superseding the rejected `position:absolute` floating bar) styled to the NowPlaying hero aesthetic (§7b).
- A **redrawn `DDIcons.LavaLamp`** glyph — classic 1970s lava-lamp silhouette (§7f).
- **Overflow clipping** to the dynamic footer height (§2).
- **Removing the static noise texture** (§3).
@@ -133,7 +138,7 @@ Redraw the lava-lamp trigger glyph to the classic 1970s silhouette (§7f).
| Color model (HSL navy↔moss) | — | OKLab three-color gradient, 3 motions (§6) |
| Glass treatment | — | folded into the blob shading (§4f); no separate glass dials |
| Static noise/frost texture | — | **removed** (§3) |
| Controls (4 knobs, popover) | — | 7 knobs in an inline collapse/expand bar (§7) |
| Controls (4 knobs, popover) | — | 7 knobs in an in-flow container between back & lamp (§7) |
| Lava-lamp trigger glyph | trigger placement kept | glyph **redrawn** (§7f) |
---
@@ -534,41 +539,109 @@ live). Recommended starting points: scroll speed ~mid, rotation ~0.3, gravity ~0
~0.4, collision ~0.5, width ~0.6. The **~20% gravity / ~100% heat** anchor reflects Daniel's stated sweet
spot (§4c). These are feel-anchors, not commitments.
### 7b. The inline collapse/expand knob-bar (decided — NOT a popover or drawer)
### 7b. The in-flow controls container — between the back link and the lava-lamp (redesigned 2026-06-16)
**Decision (Daniel, 2026-06-16): the controls are an inline collapse/expand knob-bar, NOT a floating
popover or a drawer.** The seven RadialKnobs live **inline in the controls area** (where the controls sit
today) as an **`@if`-guarded flex row** that **animates open and closed in place**. It must read as
**part of the controls collapsing/expanding** — the knob row expanding out from its predecessor — **not**
a separate surface hanging off the icon.
**Decision (Daniel, 2026-06-16): the controls are an in-flow container that lives BETWEEN the back link
(left) and the lava-lamp toggle (right), on the detail top row. It is NOT a floating popover, NOT a
drawer, and NOT a `position:absolute` element anchored under the lamp.** Expanding it **reflows the
layout in place** — the container grows in the row's flow between back and lamp — it never overlays the
page, never clips, and never overlaps the masthead/hero.
**The mechanism (no MudPopover, no MudDrawer):**
> **This redesign supersedes the first realization of §7b.** The first implementation read the
> "inline collapse/expand" intent as an *absolutely-positioned floating bar anchored under the lamp*
> (`.mix-visualizer-controls-anchor` stacked the lamp over the bar; the bar was
> `position:absolute; top:calc(100% + 0.5rem); right:0; z-index:3`). That is a "floating-but-inline
> popover" — Daniel rejects it: it clipped to a vertical sliver (only ~4 of 7 knobs visible) and read as
> a detached window hanging off the icon. **Remove the `position:absolute` rule and the
> `.mix-visualizer-controls-anchor` floating structure entirely.** The container must take real layout
> space in the row, not float over it.
- A **bound `bool`** (e.g. `_controlsExpanded`) gates the knob row. The **lava-lamp icon button toggles
it** — the same `DDIcons.LavaLamp` trigger kept from Phase 10 §7c, now redrawn (§7f), now a
collapse/expand toggle instead of a popover anchor.
- When toggled open, the flex row of seven knobs **animates open** via **CSS transition** — width / opacity
/ transform — **expanding out from the icon / its predecessor element**, so it reads as the controls
growing in place. When toggled closed it collapses back the same way. (Seven knobs is wider than six —
see the wrap note below.)
- **Animation intent:** a smooth in-place expansion (expand-from-icon / slide-open flex row), reading as
one continuous collapse/expand of the controls area — not a panel popping into existence over the page.
Use the codebase's existing transition vocabulary (the existing controls/transition CSS) so the motion
feels native, not bolted on.
- **No floating surface.** There is no overlay, no anchored popover panel, no edge drawer. The knob row is
a real inline child of the controls area that occupies layout when open and collapses to nothing
(or to just the icon) when closed.
**Placement — restructure the scaffold's top row into three zones (recommended).**
**Why this over the Phase 10 popover.** The popover read as a detached panel hanging off the icon; Daniel
wants the controls to *be* the controls — collapsing and expanding in place. The inline animated flex row
keeps the seven knobs in the page's flow, makes the open/close a property of the controls themselves, and
avoids the popover/drawer overlay machinery entirely. (Prior-art touchstone: an inline "expand for
advanced settings" disclosure row — e.g. a toolbar that grows a secondary row of controls in place —
rather than a flyout/menu.)
`ReleaseDetailScaffold`'s top row is today a two-zone `MudStack Row Justify="SpaceBetween"`:
`back-link (left) | @TopRightAction (right)`. Restructure it into **three zones**:
**Layout note.** Seven knobs in a flex row may wrap on narrow viewports (e.g. 4×3 / 3×4 / 2-row) — a
layout call, but all seven must remain reachable, and the wrap must still read as part of the inline
collapse/expand (the whole block grows/shrinks), not as a separate surface.
```
back-link (left) | @TopRowCenter (the controls container) | @TopRightAction (the lamp)
```
- **Add a new optional `RenderFragment? TopRowCenter` slot** to the scaffold, rendered between the back
link and `@TopRightAction` on the same row. The row becomes a three-child flex row: back link pinned
left, lamp pinned right, the center slot occupying the space between (and growing/shrinking with the
container's expand/collapse — see motion below).
- **Mix** supplies the seven-knob `MixVisualizerControls` to `TopRowCenter` and keeps the lava-lamp
`MudIconButton` in `TopRightAction`. The lamp toggles the container; the container reveals in the
center zone.
- **Reusability holds.** Track / Cut / Session supply neither `TopRowCenter` nor `TopRightAction`, so the
row degrades to **back-link-alone**, exactly as today. The center slot is a generic "affordance between
back and action" seam — it follows the scaffold's existing "variance rides a slot, never a flag"
convention (Phase 9 §5.3), the same way `TopRightAction` and `TopContent` already do. With
`Justify="SpaceBetween"` an absent center slot collapses to nothing and back/action sit at the two
edges unchanged.
**Why a scaffold center slot over a MixDetail-composed row.** Composing the whole `back | controls | lamp`
row inside MixDetail (and suppressing the scaffold's own back row) would duplicate the back-link markup,
break the scaffold's "owns the back link" invariant, and fork the one place every medium's back
navigation lives. The center slot keeps the scaffold the single owner of the row and adds Mix's piece as
data, not as a structural fork. **Recommended: the scaffold gains the `TopRowCenter` slot.**
(`TopContent` — the existing below-the-row band — is the *responsive fallback target*, not the primary
home; see the responsive note in §7b-responsive below.)
**The expand/collapse mechanism — in-flow, no float, no overlay (no MudPopover, no MudDrawer):**
- A **bound `bool`** (`_controlsExpanded`) gates the container. The **lava-lamp icon button toggles it**
the same `DDIcons.LavaLamp` trigger kept from Phase 10 §7c, now redrawn (§7f), now an in-flow
expand/collapse toggle. The icon swaps to its FILLED variant while expanded (§7f / Part B); a knob drag
never collapses the container (the toggle flips only on the lamp's click).
- **Collapsed:** the container occupies **no or minimal space** in the center zone — `max-width: 0` (or
`width: 0`) + `opacity: 0` + `overflow: hidden`, and `visibility: hidden` + `pointer-events: none` so
the collapsed knobs are not focusable or hit-testable. Back and lamp sit at the row's two edges with the
center collapsed between them.
- **Expanded:** the container **grows horizontally in place** between back and lamp — a `max-width` (or
`width`) + `opacity` (+ optional slight `transform`) CSS transition — so the seven knobs reveal **in the
row's flow**, pushing nothing over the page. **No `position: absolute`, no `float`, no `z-index` stacking
hack, no anchored panel.** The row reflows to accommodate the container; when the container is wider than
the available center space it wraps in-flow (see responsive, below) — it never clips to a sliver.
- **Motion intent:** a smooth horizontal grow/shrink of a real in-flow element, reading as **the controls
area opening between the back link and the lamp** — not a panel appearing over the page. Reuse the
existing transition vocabulary (the `cubic-bezier(0.22, 0.61, 0.36, 1)` easing already in
`MixVisualizerControls.razor.css`) so the motion feels native. The `max-height` collapse can stay as a
secondary axis for the wrap case, but the **primary** animated axis is horizontal width in the row.
**Must not overlap the masthead/hero.** Because the container lives in the top row (above the masthead,
which the scaffold renders below this row) and reflows in-flow, an expanded container **pushes the row's
own height** — it must never paint over the masthead, the hero/cover, or the waveform backdrop. If the
expanded container is tall (wrapped to multiple knob rows on narrow widths), the top row grows and the
masthead moves down with normal document flow; no overlap, no clipping.
**Why this over the rejected floating bar.** The floating `position:absolute` bar read as a detached
window hanging off the lamp and clipped because its width was constrained by the lamp's narrow column.
An in-flow container between back and lamp *is* the controls sitting in the layout — the open/close is a
property of the row reflowing, the seven knobs get the row's full horizontal space, and there is zero
overlay machinery. (Prior-art touchstone: a toolbar that grows a secondary in-flow region of controls in
place between two pinned end-affordances — e.g. a browser address bar revealing inline controls between
the back button and the menu — not a flyout anchored to an icon.)
### 7b-responsive. Narrow-width behavior — stay in-flow, never clip
The seven knobs are wide; on a narrow row the center zone cannot hold them on one line. **Recommended:
the container wraps to a second in-flow line under the back/lamp row** rather than clipping or scrolling
off-edge. Two acceptable realizations (staff-engineer's call, both in-flow):
1. **Flex-wrap in place.** The seven-knob flex row `flex-wrap: wrap`s within the center zone; when the
center zone is too narrow the whole top row grows taller and the knobs reflow to 4×3 / 3×4 / two rows.
The row's height grows in-flow; the masthead moves down. All seven stay reachable.
2. **Drop to the `TopContent` band when narrow.** Below a breakpoint, the container renders in the
scaffold's existing **`TopContent` slot** (the full-width in-flow band below the back/lamp row, above
the masthead) instead of the center zone — giving it the full container width to lay the seven knobs
out without crowding the back/lamp. This is still fully in-flow (it is literally the `TopContent`
position) and never floats. Use the center zone on wide rows, the `TopContent` band on narrow rows.
**Either way: in-flow, no overlay, no edge-clipping, no horizontal-scroll-that-hides-knobs.** A
horizontally-scrolling inline strip is a *last-resort* fallback only if wrapping proves visually worse —
and even then it must be a real in-flow scroll region with a visible affordance, never a clipped sliver.
All seven knobs must always be reachable. The wrap/drop must still read as part of the controls
container opening, not as a separate surface.
### 7c. State — widen to seven properties
@@ -746,9 +819,13 @@ convention, and the theme-alignment discipline — not the exact coordinates.)
19. **Waveform width.** Dragging the waveform-width control narrows/widens the waveform band across its
range; narrowing it visibly clears horizontal space for the lava (and shrinks the collision boundary
the fluid parts around). The datum/scroll geometry is otherwise unchanged.
20. **Inline collapse/expand.** Clicking the lava-lamp icon expands the seven-knob flex row **in place**
(animated open/closed via CSS transition), reading as the controls collapsing/expanding — **not** a
floating popover or drawer. Clicking again collapses it; dragging a knob does not collapse it.
20. **In-flow container between back and lamp.** The controls container sits **inline on the detail top
row, between the back link (left) and the lava-lamp toggle (right)**. Clicking the lava-lamp icon
expands it **in the layout flow** (animated open/closed via CSS width/opacity transition), reflowing
the row — **not** a floating popover, drawer, or `position:absolute` bar, and **not** clipped to a
sliver. Collapsed it takes no/minimal space (back and lamp at the row's two edges). All seven knobs are
visible and horizontal when expanded; the expansion never overlaps the masthead/hero. Clicking again
collapses it; dragging a knob does not collapse it.
21. **NowPlaying aesthetic.** The knob-bar's surface color + structure match the session hero now-playing
overlay (translucent dark glass, overlay-label typography, `Color.Secondary` accents).
22. **Persistence + read-only.** All seven positions survive SPA nav within a session, reset on fresh
@@ -795,13 +872,15 @@ sourced from `DeepDrftPalettes` with no hardcoded duplicates. **Acceptance:** §
per-segment-bake requirement (§6b motion 2) uses the mix-time-keyed realization so it travels with the
segment by construction (decided — §6b).
### Wave R4 — Seven controls + the NowPlaying-styled inline knob-bar
### Wave R4 — Seven controls + the NowPlaying-styled in-flow controls container
Widen `MixVisualizerControlState` to **seven** properties (including the new **waveform width** control,
§7a); replace the four-knob popover with the seven-knob **inline collapse/expand knob-bar** (§7b —
`@if`-guarded animated flex row, lava-lamp icon toggles it, no popover/drawer); wire the waveform-width
control to scale the waveform-band extent and its collision boundary (§7a); style it to the session-hero
aesthetic; extend the bridge handle with the new setters (including `setWaveformWidth`). **Acceptance:**
§8 #18#22.
§7a); add the scaffold's **`TopRowCenter`** slot and move the seven-knob `MixVisualizerControls` into an
**in-flow container between the back link and the lava-lamp** (§7b — animated horizontal grow/shrink in
the row's flow, lava-lamp icon toggles it, **no popover/drawer, no `position:absolute`**; **remove** the
old `.mix-visualizer-controls-anchor` + `position:absolute` floating structure); handle the narrow-width
in-flow wrap/drop (§7b-responsive); wire the waveform-width control to scale the waveform-band extent and
its collision boundary (§7a); style it to the session-hero aesthetic; extend the bridge handle with the
new setters (including `setWaveformWidth`). **Acceptance:** §8 #18#22.
**Dependency shape:** `Wave R1 → Wave R2 → (Wave R3 ‖ Wave R4)`. Wave R1 is a quick unblock (and folds in
the independent icon redraw). Wave R2 is the prerequisite for everything visual. Waves R3 (color) and R4
@@ -814,8 +893,9 @@ ranges/transfer-functions by hand once on screen throughout (his standing prefer
## 10. Open items
Tuning knobs, one undecided behavior call, and one deferred future idea — the controls-UI fork and the
per-segment-storage fork are both now **decided** (§7b inline knob-bar; §6b mix-time-keyed). None of the
items below block starting Wave R1.
per-segment-storage fork are both now **decided** (§7b in-flow controls container between back & lamp —
redesigned 2026-06-16, superseding the rejected `position:absolute` floating bar; §6b mix-time-keyed).
None of the items below block starting Wave R1.
- **§4c, §5d — transfer functions:** heat 0..1 → rise/morph intensity; collision 0..1 → soft↔hard blend
shape; restitution coefficients; penetration-penalty curve. All staff-engineer tuning tasks with the