feat(mix-visualizer): Phase 10 tuning — smooth waveform, bouncy collision, 8 knobs

Smooth the loudness contour (~50 ms envelope at preprocessing + decode-time, plus
smootherstep render reconstruction); retune wax↔waveform collision to bouncy/sub-unity
(no explosion/stuck/jitter); split the bubbles knob into fluid-amount + fluid-viscosity
(cohesion via uniform-only smin/wobble); retune scroll/gravity/heat/width ranges; make
the colour rotation visible and boost OKLab chroma; the controls bar now holds its
layout and hides only its knobs via a Visible parameter.
This commit is contained in:
daniel-c-harvey
2026-06-17 05:12:15 -04:00
parent ba1a1cd8ec
commit 4e34696719
9 changed files with 500 additions and 183 deletions
@@ -2,12 +2,17 @@
@using DeepDrftPublic.Client.Services
@inject MixVisualizerControlState ControlState
@* The Mix visualizer controls. SEVEN continuous RadialKnobs — scroll speed, gradient rotation speed,
lava gravity, lava heat, blob density, collision strength, waveform width — each its own dedicated
control with a Material-icon caption. Visibility is controlled by Blazor, not CSS: the host page
renders this component only while the lava-lamp toggle is on (@if-guarded), so when off it is not in
the DOM at all. There is no collapse/expand animation and no glass surface — the knobs simply appear
in their own transparent band and disappear when un-rendered.
@* The Mix visualizer controls. EIGHT continuous RadialKnobs — scroll speed, gradient rotation speed,
lava gravity, lava heat, fluid amount, fluid viscosity, collision strength, waveform width — each its
own dedicated control with a Material-icon caption. The single "bubbles" knob is split into
fluid-amount + fluid-viscosity (Phase 10 §5).
Visibility (Phase 10 §4): the host ALWAYS renders this component now and feeds the lava-lamp toggle
into the @Visible parameter. THIS component decides knob visibility — it @if-gates the knobs but keeps
the container's reserved size, so the content below the controls bar never pops when the lamp toggles.
The gating is Blazor @if (matching the established "@if-gated knob band, no CSS hide/glass/animation"
convention) — the knobs are simply not rendered when hidden, while a min-height container holds the
layout. No collapse animation, no glass surface, no CSS visibility-hiding of populated knobs.
It owns NO JS interop: it mutates the shared, session-scoped MixVisualizerControlState and raises its
Changed event. The backdrop bridge (MixWaveformVisualizer) subscribes to that event and pushes the
@@ -21,72 +26,92 @@
<div class="mix-visualizer-controls-bar">
<div class="mix-visualizer-control" role="group" aria-label="Waveform scroll speed">
<RadialKnob Value="@ControlState.ScrollSpeed"
ValueChanged="@OnScrollSpeedChanged"
Min="0" Max="1" Step="0.001"
Size="64"
Color="Color.Primary" />
<MudIcon Icon="@Icons.Material.Filled.Speed" Size="Size.Small" Class="mix-visualizer-control-icon" />
</div>
@if (Visible)
{
<div class="mix-visualizer-control" role="group" aria-label="Waveform scroll speed">
<RadialKnob Value="@ControlState.ScrollSpeed"
ValueChanged="@OnScrollSpeedChanged"
Min="0" Max="1" Step="0.001"
Size="64"
Color="Color.Primary" />
<MudIcon Icon="@Icons.Material.Filled.Speed" Size="Size.Small" Class="mix-visualizer-control-icon" />
</div>
<div class="mix-visualizer-control" role="group" aria-label="Color gradient rotation speed">
<RadialKnob Value="@ControlState.GradientRotationSpeed"
ValueChanged="@OnGradientRotationSpeedChanged"
Min="0" Max="1" Step="0.001"
Size="64"
Color="Color.Primary" />
<MudIcon Icon="@Icons.Material.Filled.Palette" Size="Size.Small" Class="mix-visualizer-control-icon" />
</div>
<div class="mix-visualizer-control" role="group" aria-label="Color gradient rotation speed">
<RadialKnob Value="@ControlState.GradientRotationSpeed"
ValueChanged="@OnGradientRotationSpeedChanged"
Min="0" Max="1" Step="0.001"
Size="64"
Color="Color.Primary" />
<MudIcon Icon="@Icons.Material.Filled.Palette" Size="Size.Small" Class="mix-visualizer-control-icon" />
</div>
<div class="mix-visualizer-control" role="group" aria-label="Lava gravity">
<RadialKnob Value="@ControlState.LavaGravity"
ValueChanged="@OnLavaGravityChanged"
Min="0" Max="1" Step="0.001"
Size="64"
Color="Color.Primary" />
<MudIcon Icon="@Icons.Material.Filled.ArrowDownward" Size="Size.Small" Class="mix-visualizer-control-icon" />
</div>
<div class="mix-visualizer-control" role="group" aria-label="Lava gravity">
<RadialKnob Value="@ControlState.LavaGravity"
ValueChanged="@OnLavaGravityChanged"
Min="0" Max="1" Step="0.001"
Size="64"
Color="Color.Primary" />
<MudIcon Icon="@Icons.Material.Filled.ArrowDownward" Size="Size.Small" Class="mix-visualizer-control-icon" />
</div>
<div class="mix-visualizer-control" role="group" aria-label="Lava heat">
<RadialKnob Value="@ControlState.LavaHeat"
ValueChanged="@OnLavaHeatChanged"
Min="0" Max="1" Step="0.001"
Size="64"
Color="Color.Primary" />
<MudIcon Icon="@Icons.Material.Filled.LocalFireDepartment" Size="Size.Small" Class="mix-visualizer-control-icon" />
</div>
<div class="mix-visualizer-control" role="group" aria-label="Lava heat">
<RadialKnob Value="@ControlState.LavaHeat"
ValueChanged="@OnLavaHeatChanged"
Min="0" Max="1" Step="0.001"
Size="64"
Color="Color.Primary" />
<MudIcon Icon="@Icons.Material.Filled.LocalFireDepartment" Size="Size.Small" Class="mix-visualizer-control-icon" />
</div>
<div class="mix-visualizer-control" role="group" aria-label="Blob density and size">
<RadialKnob Value="@ControlState.BlobDensity"
ValueChanged="@OnBlobDensityChanged"
Min="0" Max="1" Step="0.001"
Size="64"
Color="Color.Primary" />
<MudIcon Icon="@Icons.Material.Filled.BubbleChart" Size="Size.Small" Class="mix-visualizer-control-icon" />
</div>
<div class="mix-visualizer-control" role="group" aria-label="Fluid amount">
<RadialKnob Value="@ControlState.FluidAmount"
ValueChanged="@OnFluidAmountChanged"
Min="0" Max="1" Step="0.001"
Size="64"
Color="Color.Primary" />
<MudIcon Icon="@Icons.Material.Filled.BubbleChart" Size="Size.Small" Class="mix-visualizer-control-icon" />
</div>
<div class="mix-visualizer-control" role="group" aria-label="Collision strength">
<RadialKnob Value="@ControlState.CollisionStrength"
ValueChanged="@OnCollisionStrengthChanged"
Min="0" Max="1" Step="0.001"
Size="64"
Color="Color.Primary" />
<MudIcon Icon="@Icons.Material.Filled.Compress" Size="Size.Small" Class="mix-visualizer-control-icon" />
</div>
<div class="mix-visualizer-control" role="group" aria-label="Fluid viscosity">
<RadialKnob Value="@ControlState.FluidViscosity"
ValueChanged="@OnFluidViscosityChanged"
Min="0" Max="1" Step="0.001"
Size="64"
Color="Color.Primary" />
<MudIcon Icon="@Icons.Material.Filled.Opacity" Size="Size.Small" Class="mix-visualizer-control-icon" />
</div>
<div class="mix-visualizer-control" role="group" aria-label="Waveform width">
<RadialKnob Value="@ControlState.WaveformWidth"
ValueChanged="@OnWaveformWidthChanged"
Min="0" Max="1" Step="0.001"
Size="64"
Color="Color.Primary" />
<MudIcon Icon="@Icons.Material.Filled.SettingsEthernet" Size="Size.Small" Class="mix-visualizer-control-icon" />
</div>
<div class="mix-visualizer-control" role="group" aria-label="Collision strength">
<RadialKnob Value="@ControlState.CollisionStrength"
ValueChanged="@OnCollisionStrengthChanged"
Min="0" Max="1" Step="0.001"
Size="64"
Color="Color.Primary" />
<MudIcon Icon="@Icons.Material.Filled.Compress" Size="Size.Small" Class="mix-visualizer-control-icon" />
</div>
<div class="mix-visualizer-control" role="group" aria-label="Waveform width">
<RadialKnob Value="@ControlState.WaveformWidth"
ValueChanged="@OnWaveformWidthChanged"
Min="0" Max="1" Step="0.001"
Size="64"
Color="Color.Primary" />
<MudIcon Icon="@Icons.Material.Filled.SettingsEthernet" Size="Size.Small" Class="mix-visualizer-control-icon" />
</div>
}
</div>
@code {
/// <summary>
/// Whether the knob band is shown. The host wires its lava-lamp toggle straight into this — the host
/// always renders this component, and THIS component decides knob visibility (Phase 10 §4). When
/// false the knobs are @if-gated out but the container holds its reserved height (CSS min-height), so
/// content below the bar never pops as the lamp toggles.
/// </summary>
[Parameter] public bool Visible { get; set; }
// Each handler mutates its own dedicated property then raises Changed — the bridge re-reads and
// pushes the affected dial. All values are already normalized [0,1]; the bridge maps scroll speed
// to a visible time-span and routes the rest straight to the lava/colour dials.
@@ -115,9 +140,15 @@
ControlState.NotifyChanged();
}
private void OnBlobDensityChanged(double value)
private void OnFluidAmountChanged(double value)
{
ControlState.BlobDensity = value;
ControlState.FluidAmount = value;
ControlState.NotifyChanged();
}
private void OnFluidViscosityChanged(double value)
{
ControlState.FluidViscosity = value;
ControlState.NotifyChanged();
}