From 5fb7d850193fb5586caade8918c23b131ed8b1f1 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Tue, 16 Jun 2026 12:47:44 -0400 Subject: [PATCH] docs(phase-10-reframe): fold Wave R2 eval into lava spec + PLAN (7th control, flat coalescing fluid, up-and-out collision) --- PLAN.md | 10 +- .../phase-10-mix-visualizer-lava-reframe.md | 286 ++++++++++++------ 2 files changed, 205 insertions(+), 91 deletions(-) diff --git a/PLAN.md b/PLAN.md index 0ab0b34..7d8f0aa 100644 --- a/PLAN.md +++ b/PLAN.md @@ -207,15 +207,17 @@ A **major reframe of the Mix visualizer's effects, controls, and color model**, **This supersedes the original Phase 10 (Waves 1–4) 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 ~16–32 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 ~16–32 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 `` 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 R1–R4 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. --- diff --git a/product-notes/phase-10-mix-visualizer-lava-reframe.md b/product-notes/phase-10-mix-visualizer-lava-reframe.md index 5adb7fe..df10432 100644 --- a/product-notes/phase-10-mix-visualizer-lava-reframe.md +++ b/product-notes/phase-10-mix-visualizer-lava-reframe.md @@ -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 16–32 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 16–32 / 20–100px 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 ~20–100px 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 ~20–100px 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 1–4.** 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 (16–32 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 R1–R4 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 R1–R4 builds it.