docs(phase-10-reframe): fold Wave R2 eval into lava spec + PLAN (7th control, flat coalescing fluid, up-and-out collision)

This commit is contained in:
daniel-c-harvey
2026-06-16 12:47:44 -04:00
parent 09309630cb
commit 5fb7d85019
2 changed files with 205 additions and 91 deletions
+6 -4
View File
@@ -207,15 +207,17 @@ A **major reframe of the Mix visualizer's effects, controls, and color model**,
**This supersedes the original Phase 10 (Waves 14) effects/controls/color design**`product-notes/mix-visualizer-webgl-renderer.md` §4 (effects) and §7 (popover-controls) are marked superseded with a pointer to the reframe spec. The renderer *infrastructure* carries forward unchanged.
**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). At heat 0 the wax rests at the bottom and only collision moves it (collision always on, independent of heat); at heat max many bubbles rise/morph per second. **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.
- **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 → six 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→hard). **NOT a popover or drawer — an inline collapse/expand knob-bar** (decided 2026-06-16): an `@if`-guarded **animated flex row of the six `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 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.
**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()`).
Heat→intensity and collision soft↔hard transfer functions are **staff-engineer tuning tasks** (endpoints fixed in the spec, formulas not). Full design, the wax-blob model, the collision model, the three-motion color model, the inline knob-bar, the icon redraw, observable acceptance criteria, and phasing: `product-notes/phase-10-mix-visualizer-lava-reframe.md`.
Heat→intensity and collision soft↔hard transfer functions are **staff-engineer tuning tasks** (endpoints fixed in the spec — heat 0 = wax rests on floor / heat max = many small turbulent rising bubbles; collision soft = gentle mush / hard = high-elasticity up-and-out throw, wide range, smooth/no jitter — formulas not). Full design, the wax-blob model, the collision model, the three-motion color model, the inline knob-bar, the icon redraw, observable acceptance criteria, and phasing: `product-notes/phase-10-mix-visualizer-lava-reframe.md`.
**Sequenced as four reframe waves.** `Wave R1 → Wave R2 → (Wave R3 ‖ Wave R4)`. Wave R1 (de-noise + dynamic footer clip + icon redraw) is a cheap unblock for a clean substrate. Wave R2 (wax-blob physics + 2D collision) is the load-bearing prerequisite — prove the lava before the color and the UI. Wave R3 (OKLab three-color gradient, the three motions) and Wave R4 (six controls + NowPlaying-styled inline knob-bar + widened state to six properties + extended bridge handle) both depend on Wave R2 but are independent of each other. **Both prior open Daniel calls are now decided:** controls-UI is an inline collapse/expand knob-bar (not popover/drawer); per-segment color is mix-time-keyed.
**Open / undecided (spec §10):** (a) **pause behavior** — whether the lava keeps convecting while audio is *paused* (lamp "always on") vs. the current freeze-on-pause (rAF gated on `isPlaying`) is an **undecided Daniel call**; default to freeze-on-pause until decided, switching is a localized gating change, not a model change. (b) **Future enhancement (deferred, NOT in R1R4 scope):** a per-control **"auto-modulate" checkbox** that slowly oscillates each knob's value via a low-frequency oscillator so the visualizer drifts on its own — Daniel flagged it as a cool future idea; the knob-bar layout leaves room but nothing builds it now.
**Sequenced as four reframe waves.** `Wave R1 → Wave R2 → (Wave R3 ‖ Wave R4)`. Wave R1 (de-noise + dynamic footer clip + icon redraw) is a cheap unblock for a clean substrate. Wave R2 (wax-blob physics + 2D collision) is the load-bearing prerequisite — prove the lava before the color and the UI. Wave R3 (OKLab three-color gradient, the three motions) and Wave R4 (seven controls — including the new waveform-width knob — + NowPlaying-styled inline knob-bar + widened state to seven properties + extended bridge handle) both depend on Wave R2 but are independent of each other. **Both prior open Daniel calls are now decided:** controls-UI is an inline collapse/expand knob-bar (not popover/drawer); per-segment color is mix-time-keyed.
---
@@ -31,7 +31,7 @@ This spec **supersedes** the original Phase 10 effects/controls/color design:
- `product-notes/mix-visualizer-webgl-renderer.md` **§4 (the four visual effects)** — superseded by
§3–§6 here.
- `product-notes/mix-visualizer-webgl-renderer.md` **§7 (Wave 4 popover-controls rework)** — superseded
by §7 here (the trigger and the widened body are kept; the four-knob popover becomes a six-knob
by §7 here (the trigger and the widened body are kept; the four-knob popover becomes a seven-knob
inline collapse/expand knob-bar — see §7).
What carries forward unchanged from Phase 10's landed waves (do **not** re-derive — reference it):
@@ -60,9 +60,9 @@ Cross-references (read these before implementing):
geometry, the datum texture, and the playhead machinery are reused; the §4-effect GLSL (the bubble
SDF, the detach blobs, the HSL `mixHsl`/`vivify` color, the glass) is the part being replaced.
- `DeepDrftPublic.Client/Controls/MixVisualizerControls.razor[.cs]` — the four-knob control component;
becomes six knobs in an inline collapse/expand bar (§7).
becomes seven knobs in an inline collapse/expand bar (§7).
- `DeepDrftPublic.Client/Services/MixVisualizerControlState.cs` — the scoped four-property state holder;
widens to six properties (§7c).
widens to seven properties (§7c).
- `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).
@@ -85,7 +85,7 @@ 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 **six-knob inline collapse/expand knob-bar** styled to
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.
Redraw the lava-lamp trigger glyph to the classic 1970s silhouette (§7f).
@@ -98,8 +98,8 @@ Redraw the lava-lamp trigger glyph to the classic 1970s silhouette (§7f).
- The **three-color OKLab gradient model** with three combined motions: anchor rotation among X/Y/Z,
per-segment sinusoidal variation baked at segment entry, and the per-bar curve shift with scroll
height (§6).
- **Six controls** replacing the four: scroll speed, gradient rotation speed, lava gravity, lava heat,
blob density/size, collision strength (§7).
- **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).
- A **redrawn `DDIcons.LavaLamp`** glyph — classic 1970s lava-lamp silhouette (§7f).
@@ -133,7 +133,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) | — | 6 knobs in an inline collapse/expand bar (§7) |
| Controls (4 knobs, popover) | — | 7 knobs in an inline collapse/expand bar (§7) |
| Lava-lamp trigger glyph | trigger placement kept | glyph **redrawn** (§7f) |
---
@@ -236,6 +236,23 @@ This fixes "giant disconnected circles": the prior approach had too few scripted
= 6`) with hash-driven pseudo-motion and no physics, so they read as detached discs. Real physics on
1632 blobs with `smin` merging reads as continuous wax that splits and recombines.
**The fluid must read FLAT, and the blobs must melt into one unified fluid (from Daniel's Wave R2 eval).**
Two refinements to the rendered result, both load-bearing:
- **Flat fluid surface, not bright pointed centers.** The metaball/fluid surface must read **flat**
an evenly-lit fluid body. It must **NOT** read as blobs with bright pointed centers / cone-like radial
gradients (a per-blob radial falloff peaking at each center reads as a field of glowing cones, which
Daniel rejected). Shade the *composited* surface, not each blob's center: the brightness should be a
property of the unified fluid's surface (normal/Fresnel from the SDF, §4f), flat across the body, not a
per-blob hotspot.
- **Melt into one fluid — low viscosity, strong coalescence.** The blobs must **coalesce into a single
unified fluid** rather than reading as distinct stiff globs that merely touch. Bias the `smin` blend
radius wide and the inter-blob cohesion high so overlapping/near blobs **melt together** into one
continuous body — "behave more like a fluid," not like rigid beads in contact. This trades against the
earlier "high-viscosity wax, not water" damping note (§4a integration): keep the *motion* viscous and
slow, but make the *surface* coalesce freely. The damping is on velocity (slow, wax-like movement); the
coalescence is on the surface field (fluid, unified). They are independent.
### 4b. Blob shape — varied, organic, not always circular
Blobs range **~20px to ~100px** across and are **not always circular** — they should read as varied,
@@ -253,10 +270,31 @@ wax," not "N identical circles."
**always on, independent of heat** (§5).
- At **heat = max**, **many bubbles rise and morph per second** — a busy, actively roiling lamp.
The mapping from the 0..1 heat scalar to effect intensity (rise rate, how many blobs are buoyant at
once, churn frequency) is a **well-tuned transfer function that staff-engineer owns** — this spec does
not fix a formula. Note it as a tuning task: the requirement is the *endpoints* (0 = rest-at-bottom,
collision-only; max = many rising/morphing per second) and a smooth, good-feeling sweep between them.
**Energy-coupled dynamics (from Daniel's Wave R2 eval) — heat changes the *character* of the wax, not
just the rise rate.** Higher heat/energy → **smaller bubbles + more turbulence**; lower heat → **fewer,
larger, calmer wax**. Concretely:
- **High heat:** lots of **small, lively, turbulent bubbles** — the wax fragments into many small active
blobs with visible churn/turbulence. Energetic and busy.
- **Low heat:** **fewer, larger, calmer** masses — big lazy wax that mostly rests and drifts.
So heat couples to *both* buoyancy (rise) *and* the effective blob size/count distribution and the
turbulence in the velocity field. (This interacts with the blob density/size control, §4e — heat shifts
the wax toward the small-and-many end of whatever the density control sets.) **The visualizer should feel
dynamic and fun** — heat is the "liveliness" axis, and the whole point is that turning it up makes the
lamp visibly come alive, not merely rise faster.
The mapping from the 0..1 heat scalar to effect intensity (rise rate, bubble size/count split, churn
frequency, turbulence) is a **well-tuned transfer function that staff-engineer owns** — this spec does
not fix a formula. Note it as a tuning task: the requirement is the *endpoints* (0 = wax rests on the
floor, collision-only; max = many small turbulent rising bubbles) and a smooth, good-feeling, dynamic
sweep between them.
**Tuning reference — Daniel's sweet spot is ~20% gravity / ~100% heat.** Calibrate the defaults and the
transfer-function ranges so that this combination lands in a great-feeling, lively state (it is the
target the small-turbulent-bubble behavior above is described against). Treat ~20% gravity / ~100% heat
as the calibration anchor for where "dynamic and fun" should sit; the ranges should make that reachable
and good, not extreme.
### 4d. Gravity control
@@ -320,6 +358,16 @@ is a separate phase, not a tuning step.
Mass can be derived from radius (bigger blob = more mass) so large blobs shove small ones convincingly —
staff-engineer's call; the requirement is *elastic 2D collision on both pairs.*
**High elasticity, and the throw is UP AND OUT, not just sideways (from Daniel's Wave R2 eval).** The
collision is **highly elastic** at the hard end. Critically, when the waveform collides with the wax at
high collision strength it must **throw the bubbles UPWARD AND OUTWARD** — the waveform surface impulse
has a vertical (lift) component, not merely horizontal displacement. The earlier "pushed out along the
waveform's surface normal" framing is too flat: the desired read is the waveform *kicking* the fluid up
and away, like a paddle slapping liquid, so the lava springs off it. Staff-engineer composes the impulse
so a hard collision imparts both outward (normal) and upward (lift) velocity to the struck blobs.
**Collision must be smooth — no jitter** (no per-frame popping/vibration at the boundary; the response
must integrate stably even at high elasticity).
### 5b. Collision always on, independent of heat
Per §4c: the waveform↔lava collision runs **at all heat levels, including heat = 0.** At rest the
@@ -328,18 +376,24 @@ lava feeling like one physical system rather than two layers.
### 5c. Collision strength — soft → hard (the sixth control)
A **collision strength** control sweeps the interaction from **soft → 100% hard.** It blends between two
behaviors:
A **collision strength** control sweeps the interaction from **genuinely soft → 100% hard** across a
**wide soft↔hard range** (from Daniel's Wave R2 eval — the soft end must be genuinely soft, not a faint
version of hard, and the spread between the two ends should be large and expressive). It blends between
two behaviors:
- **(a) Hard obstacle:** the waveform is a rigid wall the fluid flows around — full elastic reflection,
blobs cannot enter the silhouette at all.
- **(b) Soft displacement/shove:** the waveform gently pushes the fluid aside — a soft penalty force
proportional to penetration depth, blobs squish against and partially into the boundary before being
eased out.
- **(a) Hard / high-elasticity throw:** the waveform is a near-rigid surface that **throws the wax UP AND
OUT** — high restitution, the struck blobs spring off the boundary with both outward (normal) and
upward (lift) velocity (§5a). At max it reads as the waveform energetically kicking the lava off it.
- **(b) Soft mush:** the waveform gently **mushes** the lava around — a soft penalty force proportional to
penetration depth, the wax squishes against and partially into the boundary and slowly eases back, with
little or no springy throw. At/near min it must be **genuinely soft** — the fluid yields and gently
deforms, no kick.
The knob blends (b) → (a) as it sweeps 0 → 1. At 0 it is a gentle shove (the fluid yields and slowly
recovers); at 1 it is a hard wall (crisp reflection, no penetration). The same blend factor can scale the
blob↔blob restitution for consistency (softer overall world at low strength), staff-engineer's call.
The knob blends (b) → (a) as it sweeps 0 → 1: at 0, gentle mushing of the lava (soft, yielding, slow
recovery); at 1, a high-elasticity up-and-out throw. The range must be **wide** so the difference between
ends is dramatic, and the sweep must stay **smooth with no jitter** at any setting (§5a). The same blend
factor can scale the blob↔blob restitution for consistency (softer overall world at low strength),
staff-engineer's call.
> **Dropped:** the earlier Phase 10 "bubbles spawn from peaks" idea is **not relevant** to this model —
> blobs are a persistent physical population, not spawned-from-the-waveform particles. Do not carry it
@@ -347,9 +401,13 @@ blob↔blob restitution for consistency (softer overall world at low strength),
### 5d. Transfer functions left to staff-engineer
As with heat (§4c), the exact penetration-penalty curve, restitution coefficients, and the soft↔hard
blend shape are **staff-engineer tuning tasks.** This spec fixes the *model* (two elastic collision
pairs, a soft↔hard blend on a knob, collision always on) and the *endpoints*, not the constants.
As with heat (§4c), the exact penetration-penalty curve, restitution coefficients, the lift/normal
impulse split, and the soft↔hard blend shape are **staff-engineer tuning tasks.** This spec fixes the
*model* (two elastic collision pairs, a wide soft↔hard blend on a knob, collision always on, smooth/no
jitter) and the *endpoints* (soft = gentle mush; hard = high-elasticity up-and-out throw), not the
constants. The heat transfer-function endpoints are likewise fixed (heat 0 = wax rests on the floor;
heat max = many small turbulent rising bubbles) with the ~20% gravity / ~100% heat sweet spot as the
calibration anchor (§4c).
---
@@ -442,18 +500,19 @@ OKLab. Drop the `mixHsl` / `vivify` / `VIVID_*` machinery entirely.
---
## 7. The six controls + the inline collapse/expand knob-bar
## 7. The seven controls + the inline collapse/expand knob-bar
### 7a. The six controls (replacing the four)
### 7a. The seven controls (replacing the four)
| # | Control | What it drives | Range | Replaces |
|---|---------|----------------|-------|----------|
| 1 | **Waveform scroll speed** | Apparent bottom-to-top scroll rate (decouples scroll from zoom) | normalized, mapped via `MixZoomMapping` or a new scroll-rate map | the old **resolution/zoom** control — *resolution as a standalone control is gone* |
| 2 | **Color gradient rotation speed** | Motion 1 anchor-rotation rate among X/Y/Z (§6b) | normalized 0→1 → slow→fast cycle | the old **color-shift speed** |
| 3 | **Lava gravity** | Downward force on the wax (§4d) | normalized 0→1 | new |
| 4 | **Lava heat** | Energy into the system; 0 = rest-at-bottom, max = many rising/morphing (§4c) | normalized 0→1 | the old **detach**, re-modeled |
| 4 | **Lava heat** | Energy into the system; 0 = rest-at-bottom, max = many small turbulent rising bubbles (§4c) | normalized 0→1 | the old **detach**, re-modeled |
| 5 | **Blob density/size** | Amount of wax — count/size within the 1632 / 20100px bands (§4e) | normalized 0→1 | new (the old **bubblyness** is gone; bulge is now physical) |
| 6 | **Collision strength** | Soft → 100% hard waveform/blob collision (§5c) | normalized 0→1 → soft→hard | new |
| 6 | **Collision strength** | Soft mush → 100% hard up-and-out throw, waveform/blob collision (§5c) | normalized 0→1 → soft→hard | new |
| 7 | **Waveform width** | Width of the waveform portion of the visualizer — narrows the waveform band so the lava fluid has more room to move (§7a, below) | normalized 0→1 → narrow→wide | new (added in the Wave R2 eval) |
Note the swap: **resolution/zoom as a standalone control is removed — scroll speed replaces it.** The
`MIN_VISIBLE_SECONDS` anchor still exists internally for the datum-density framing, but the user-facing
@@ -461,14 +520,24 @@ control is "how fast does it scroll," not "how much do I see." Staff-engineer de
speed reuses `MixZoomMapping` (treating it as a scroll-rate map) or gets a fresh linear map; either way
keep the log-feel continuity that made the zoom slider feel good.
**Control 7 — waveform width (added in the Wave R2 eval).** Adjusts the **width of the waveform portion**
of the visualizer — i.e. how much horizontal band the symmetric ±amplitude waveform silhouette occupies.
**The point of the control:** on loud/busy songs the waveform is wide and crowds the canvas; narrowing it
gives the **lava fluid more room to move**. Low = a narrow waveform band (more room for lava); high = a
wide waveform band. Mechanically this scales the waveform's amplitude→screen-width mapping (the half-width
the silhouette extends from the center line for a given loudness), which also resizes the collision
boundary the fluid parts around (§5) — narrowing the waveform literally clears space for the wax. The
datum and scroll geometry are unchanged; only the horizontal extent of the waveform band scales.
Defaults are Daniel's to tune on screen (his standing preference — he tunes ranges by hand once it is
live). Recommended starting points: scroll speed ~mid, rotation ~0.3, gravity ~0.5, heat ~0.3, density
~0.4, collision ~0.5. These are feel-anchors, not commitments.
live). Recommended starting points: scroll speed ~mid, rotation ~0.3, gravity ~0.2, heat ~1.0, density
~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)
**Decision (Daniel, 2026-06-16): the controls are an inline collapse/expand knob-bar, NOT a floating
popover or a drawer.** The six RadialKnobs live **inline in the controls area** (where the controls sit
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.
@@ -478,9 +547,10 @@ a separate surface hanging off the icon.
- 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 six knobs **animates open** via **CSS transition** — width / opacity
- 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.
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
@@ -491,36 +561,37 @@ a separate surface hanging off the icon.
**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 six knobs in the page's flow, makes the open/close a property of the controls themselves, and
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.)
**Layout note.** Six knobs in a flex row may wrap on narrow viewports (2×3 / 3×2) — a layout call, but
all six 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.
**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.
### 7c. State — widen to six properties
### 7c. State — widen to seven properties
Widen `MixVisualizerControlState` from four properties to **six**: `ScrollSpeed` (replacing
Widen `MixVisualizerControlState` from four properties to **seven**: `ScrollSpeed` (replacing
`VisibleSeconds` as the user-facing axis, or keep `VisibleSeconds` internally and add a `ScrollSpeed`
that maps to it — staff-engineer's call), `GradientRotationSpeed` (rename of `ColorShiftSpeed`),
`LavaGravity`, `LavaHeat` (re-modeled from `Detach`), `BlobDensity`, `CollisionStrength`. Each with a
`const` default mirrored to the TS tuning anchors (keep the C#↔TS default-sync discipline the existing
`Default*` consts have). Same scoped-DI persistence model: survives SPA nav within a session, resets on
fresh load. Same `Changed` event seam — the bridge subscribes and pushes the affected uniform; the
knob-bar component only mutates state and raises `Changed`. **This is the same architecture as today, just
six properties instead of four.**
`LavaGravity`, `LavaHeat` (re-modeled from `Detach`), `BlobDensity`, `CollisionStrength`, and
`WaveformWidth` (the seventh, added in the Wave R2 eval — drives the waveform-band horizontal extent,
§7a). Each with a `const` default mirrored to the TS tuning anchors (keep the C#↔TS default-sync
discipline the existing `Default*` consts have). Same scoped-DI persistence model: survives SPA nav
within a session, resets on fresh load. Same `Changed` event seam — the bridge subscribes and pushes the
affected uniform; the knob-bar component only mutates state and raises `Changed`. **This is the same
architecture as today, just seven properties instead of four.**
The bridge handle gains setters for the new controls (`setScrollSpeed`, `setGradientRotationSpeed`,
`setLavaGravity`, `setLavaHeat`, `setBlobDensity`, `setCollisionStrength`) — extend, don't redesign,
mirroring how Phase 10 §2d extended the handle.
`setLavaGravity`, `setLavaHeat`, `setBlobDensity`, `setCollisionStrength`, `setWaveformWidth`) — extend,
don't redesign, mirroring how Phase 10 §2d extended the handle.
### 7d. RadialKnob — consumed unchanged, six instead of four
### 7d. RadialKnob — consumed unchanged, seven instead of four
`RadialKnob` (`DeepDrftShared.Client/Components/RadialKnob.razor`) is consumed as-is (its API is fixed —
`Value`/`ValueChanged`/`Min`/`Max`/`Step`/`Label`/`Size`/`Color`/`HoldValue`; no icon slot; `Label` is
SVG text). Six knobs in the inline bar, each with an adjacent `MudIcon` caption (the no-icon-slot
SVG text). Seven knobs in the inline bar, each with an adjacent `MudIcon` caption (the no-icon-slot
constraint from Phase 10 §7e still holds). `HoldValue=false` so they are live. `Step="0.001"` for
continuous feel. Suggested Material icons per control (staff-engineer picks final glyphs):
@@ -532,9 +603,10 @@ continuous feel. Suggested Material icons per control (staff-engineer picks fina
| 4. Lava heat | `LocalFireDepartment` / `Whatshot` |
| 5. Blob density/size | `BubbleChart` / `Grain` |
| 6. Collision strength | `Adjust` / `Compress` |
| 7. Waveform width | `WidthNormal` / `SwapHoriz` / `SettingsEthernet` (a horizontal-extent glyph) |
Six knobs in a row may wrap on narrow viewports (2×3 / 3×2) — a layout call, but all six must remain
reachable.
Seven knobs in a row may wrap on narrow viewports (4×3 / 3×4 / two rows) — a layout call, but all seven
must remain reachable.
### 7e. Aesthetic target — match the NowPlaying hero
@@ -629,58 +701,69 @@ convention, and the theme-alignment discipline — not the exact coordinates.)
**Lava behavior**
5. **Heat 0 = rest + collision-only.** At heat 0 the wax pools at the rest line and does not rise on its
own; the waveform pushing through it still displaces it (collision always on).
6. **Heat max = active.** At max heat, many bubbles rise and morph per second — an actively roiling lamp.
7. **Blobs.** Blobs read as varied organic wax shapes (not N identical circles), in the ~20100px range,
5. **Heat 0 = rest + collision-only.** At heat 0 the wax rests on the floor (pooled at the rest line) and
does not rise on its own; the waveform pushing through it still displaces it (collision always on).
6. **Heat max = small turbulent bubbles.** At max heat, the wax breaks into **many small, lively,
turbulent bubbles** that rise and morph per second — an actively roiling, fragmented lamp (not a few
big masses rising faster). Energy couples to bubble size/count and turbulence, not just rise rate.
7. **Flat, unified fluid.** The fluid reads **flat** — an evenly-lit fluid body, **not** blobs with bright
pointed centers / cone-like radial gradients. Blobs **melt into one unified fluid** (strong
coalescence) rather than distinct stiff globs in contact.
8. **Blobs.** Blobs read as varied organic wax shapes (not N identical circles), in the ~20100px range,
merging and splitting via `smin` — no "giant disconnected circles."
8. **Gravity** changes settling/rise behavior independently of heat.
9. **Blob density/size** changes how much wax is in the system across its range.
9. **Gravity** changes settling/rise behavior independently of heat. At the **~20% gravity / ~100% heat**
sweet spot the lamp reads dynamic, lively, and fun (the calibration anchor, §4c).
10. **Blob density/size** changes how much wax is in the system across its range.
**Collision**
10. **Two elastic pairs.** Blob↔waveform and blob↔blob both collide elastically.
11. **Collision strength** sweeps soft (fluid yields and recovers) → hard (rigid wall, no penetration)
across its range.
11. **Two elastic pairs.** Blob↔waveform and blob↔blob both collide elastically.
12. **Collision strength — wide soft↔hard sweep.** Sweeps **genuinely soft mush** (the waveform gently
pushes the lava around; fluid yields and slowly recovers, no springy kick) → **hard up-and-out throw**
(high elasticity; the waveform throws the bubbles **upward and outward**, not just sideways). The
range is wide/expressive and the response is **smooth — no jitter** at any setting.
**Color**
12. **Three-color OKLab gradient.** A→B linear from center outward; no cyan, no rainbow excursion — the
13. **Three-color OKLab gradient.** A→B linear from center outward; no cyan, no rainbow excursion — the
blend stays faithful to the theme colors at all rotation phases.
13. **Anchor rotation.** A and B rotate among X/Y/Z over time at the gradient-rotation-speed rate;
14. **Anchor rotation.** A and B rotate among X/Y/Z over time at the gradient-rotation-speed rate;
dragging the control visibly changes the rotation rate, never frozen at the slow end.
14. **Per-segment bake (mix-time-keyed).** A segment's colors are fixed when it enters at the bottom and
15. **Per-segment bake (mix-time-keyed).** A segment's colors are fixed when it enters at the bottom and
travel with it unchanged as it scrolls up and out — colors do not recompute under a stationary
segment. (Realized via the mix-time-keyed sinusoid, §6b motion 2.)
15. **Per-bar curve shift.** A bar is mostly A at the bottom and mostly B by the top — color appears to
16. **Per-bar curve shift.** A bar is mostly A at the bottom and mostly B by the top — color appears to
move outward as the bar climbs.
16. **One source of truth.** No hardcoded theme hexes in the visualizer; a `DeepDrftPalettes` edit or a
17. **One source of truth.** No hardcoded theme hexes in the visualizer; a `DeepDrftPalettes` edit or a
dark-mode toggle re-themes the field live with no duplicate to maintain. (The icon's two accent
literals are the documented, commented exception — §7f.)
**Controls / knob-bar**
17. **Six controls.** Exactly six RadialKnobs — scroll speed, gradient rotation speed, gravity, heat,
density/size, collision strength — each captioned with an icon; resolution/zoom as a standalone
control is gone.
18. **Inline collapse/expand.** Clicking the lava-lamp icon expands the six-knob flex row **in place**
18. **Seven controls.** Exactly seven RadialKnobs — scroll speed, gradient rotation speed, gravity, heat,
density/size, collision strength, **waveform width** — each captioned with an icon; resolution/zoom as
a standalone control is gone.
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.
19. **NowPlaying aesthetic.** The knob-bar's surface color + structure match the session hero now-playing
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).
20. **Persistence + read-only.** All six positions survive SPA nav within a session, reset on fresh
22. **Persistence + read-only.** All seven positions survive SPA nav within a session, reset on fresh
load; no control and no knob-bar element is a seek/playback surface; the bridge and read-only contract
are intact.
**Icon**
21. **Redrawn lava-lamp glyph.** The trigger shows the classic 1970s silhouette — wide truncated-cone
23. **Redrawn lava-lamp glyph.** The trigger shows the classic 1970s silhouette — wide truncated-cone
base, bulbous→roundedly-pointed teardrop glass body, small cone cap — with navy fluid + moss blobs
and a neutral/metallic base+cap; body tints via `currentColor`; renders cleanly at ~24px.
**Performance**
22. **60 FPS** on a mid-range desktop with heat, density, and collision at non-trivial values
24. **60 FPS** on a mid-range desktop with heat, density, and collision at non-trivial values
simultaneously; graceful degrade (drop internal resolution before frames) on weaker/mobile devices.
---
@@ -695,27 +778,30 @@ under the open Phase 10 and distinct from its landed Waves 14.**
Remove the static noise/frost layer (§3), implement the dynamic footer-height clip + lava-rest line
(§2c), and **redraw the `DDIcons.LavaLamp` glyph** (§7f — independent, can land anytime, grouped here as
cheap polish). Small, independent, and gives a clean substrate to build the lava on. **Acceptance:** §8
#3, #4, #21.
#3, #4, #23.
### Wave R2 — The wax-blob physics + collision (the load-bearing step)
Stand up the CPU physics step (1632 blobs: position/velocity/temperature/radius), the per-frame uniform
upload, the `smin` metaball render, the heat/gravity/density mapping, and the 2D collision model (both
pairs, the soft↔hard blend). This is where the architecture is proven; it replaces the §4-effect GLSL.
**Acceptance:** §8 #1, #5#11, #22 (at this wave's workload). Controls can be temporary sliders/debug
knobs here — the real inline knob-bar is Wave R4.
upload, the `smin` metaball render (**flat, coalescing fluid** — §4a refinement), the heat/gravity/density
mapping (**energy-coupled: high heat → small turbulent bubbles** — §4c), and the 2D collision model (both
pairs, the wide soft-mush ↔ hard up-and-out-throw blend, smooth/no jitter — §5a/§5c). This is where the
architecture is proven; it replaces the §4-effect GLSL. **Acceptance:** §8 #1, #5#12, #24 (at this wave's
workload). Controls can be temporary sliders/debug knobs here — the real inline knob-bar is Wave R4.
### Wave R3 — The OKLab three-color gradient (the three motions)
Replace the HSL `mixHsl`/`vivify` color with OKLab interpolation; implement the three motions (anchor
rotation among X/Y/Z, per-segment baked sinusoidal variation **keyed to mix-time**, per-bar curve shift),
sourced from `DeepDrftPalettes` with no hardcoded duplicates. **Acceptance:** §8 #12#16. The
sourced from `DeepDrftPalettes` with no hardcoded duplicates. **Acceptance:** §8 #13#17. The
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 — Six controls + the NowPlaying-styled inline knob-bar
Widen `MixVisualizerControlState` to six properties; replace the four-knob popover with the six-knob
**inline collapse/expand knob-bar** (§7b — `@if`-guarded animated flex row, lava-lamp icon toggles it, no
popover/drawer); style it to the session-hero aesthetic; extend the bridge handle with the new setters.
**Acceptance:** §8 #17#20.
### Wave R4 — Seven controls + the NowPlaying-styled inline knob-bar
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.
**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
@@ -727,8 +813,9 @@ ranges/transfer-functions by hand once on screen throughout (his standing prefer
## 10. Open items
Tuning knobs only — the controls-UI fork and the per-segment-storage fork are both now **decided** (§7b
inline knob-bar; §6b mix-time-keyed). None block starting Wave R1.
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.
- **§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
@@ -741,4 +828,29 @@ inline knob-bar; §6b mix-time-keyed). None block starting Wave R1.
(strongest match in the tree) or point to a different component.
- **§7f — icon accent literals:** the two literal navy/moss stops the SVG forces (commented to their
`DeepDrftPalettes` source) — exact hexes are staff-engineer's to pull from the palette.
- **Defaults** for all six controls — Daniel tunes on screen.
- **§7d — waveform-width icon glyph:** `WidthNormal` / `SwapHoriz` / `SettingsEthernet` suggested;
staff-engineer picks the final horizontal-extent glyph.
- **Defaults** for all seven controls — Daniel tunes on screen (the ~20% gravity / ~100% heat sweet spot,
§4c, anchors the heat/gravity pair).
### Undecided — Daniel calls (not blocking, but unresolved)
- **Pause behavior — does the lava keep convecting while audio is paused?** Today the visualizer freezes
on pause (the rAF loop is gated on `isPlaying` — Phase 10 §2e, carried forward). The reframe's wax
physics raises the question of whether the lava should **keep convecting/bubbling while paused** (the
lamp is "always on," the way a real lava lamp keeps going regardless of the music) or **freeze with the
scroll** as it does now. **Undecided — Daniel's call.** It affects the rAF-gating rule and whether the
physics step runs decoupled from playback. Flagged here; not blocking Wave R1/R2 (default to the current
freeze-on-pause behavior until Daniel decides; switching to always-convect is a localized change to the
gating, not the model).
### Future enhancements (explicitly out of the current reframe scope)
- **Per-control slow LFO auto-modulation.** Each knob could carry an optional **"auto-modulate" checkbox**
that, when enabled, gently oscillates that control's value over time via a **low-frequency oscillator**
(a slow sinusoid around the knob's set point), so the visualizer drifts and breathes on its own without
the user touching it. Daniel flagged this as a "cool future enhancement" (Wave R2 eval). **Deferred — not
in the R1R4 scope.** When picked up it is additive: a per-control bool + amplitude/rate in
`MixVisualizerControlState`, an LFO applied at the bridge before the uniform push, and a small checkbox
affordance per knob in the inline knob-bar. Captured so the knob-bar's per-control layout (§7b) leaves
room for a future toggle, but nothing in R1R4 builds it.