Phase 10 reframe R4: seven-knob inline visualizer controls, always-on lava loop, filled lava-lamp icon
This commit is contained in:
@@ -2,96 +2,144 @@
|
||||
@using DeepDrftPublic.Client.Services
|
||||
@inject MixVisualizerControlState ControlState
|
||||
|
||||
@* The Mix visualizer controls (Phase 10, Wave 4). Four continuous RadialKnobs — resolution,
|
||||
bubblyness, detach, color-shift speed — laid out in a row. This component lives inside the
|
||||
lava-lamp popover on the Mix detail page (Wave 4 moved it out of the always-visible TopContent row).
|
||||
@* The Mix visualizer controls (lava reframe Wave R4). 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 (no more R2 temp-remapping: no knob
|
||||
caption misrepresents its function). The bar lives INLINE in the mix-detail controls area and
|
||||
ANIMATES open/closed in place via CSS transition off the @Expanded flag — it reads as the controls
|
||||
collapsing/expanding, NOT a floating popover/drawer (§7b).
|
||||
|
||||
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
|
||||
affected uniform to the WebGL module. That keeps the JS module handle single-owned by the bridge and
|
||||
this component purely presentational. None of these is a seek surface (spec §D).
|
||||
affected dial to the WebGL module. That keeps the JS module handle single-owned by the bridge and
|
||||
this component purely presentational. None of these is a seek surface (read-only contract §D).
|
||||
|
||||
RadialKnob has no icon slot (its Label renders as SVG text), so each control's Material icon rides
|
||||
beside its knob as an adjacent MudIcon caption (spec §7e). HoldValue stays false so the knobs are
|
||||
live — ValueChanged fires continuously during drag, exactly as the sliders fired before, preserving
|
||||
the "visibly and continuously affects its target" feel and the Changed/NotifyChanged seam. *@
|
||||
RadialKnob has no icon slot (its Label renders as SVG text) and no aria attribute-capture, so each
|
||||
control's Material icon rides beside its knob as an adjacent MudIcon caption and the accessible name
|
||||
rides on the wrapping group div (§7d). HoldValue stays false so the knobs are live — ValueChanged
|
||||
fires continuously during drag, preserving the Changed/NotifyChanged seam.
|
||||
|
||||
<div class="mix-visualizer-controls">
|
||||
Aesthetic: the bar matches the session-hero NowPlaying overlay (§7e) — a translucent dark glass
|
||||
surface with overlay-label captions and Color.Secondary accents, so it reads as of-a-piece with the
|
||||
hero rather than a generic MudBlazor panel. *@
|
||||
|
||||
@* RadialKnob exposes no aria-label/attribute-capture and is out of scope to modify, so the
|
||||
accessible name rides on the wrapping group div instead (a plain element accepts it).
|
||||
R2 TEMP: this knob is repurposed from resolution/zoom to WAVEFORM WIDTH for in-browser lava
|
||||
testing (scroll speed isn't critical for evaluating the lava). The on-screen icon still reads
|
||||
ZoomIn; R4 redraws the controls and restores the resolution mapping. *@
|
||||
<div class="mix-visualizer-control" role="group" aria-label="Waveform width (R2 temp: on the resolution knob)">
|
||||
<RadialKnob Value="@ControlState.WaveformWidth"
|
||||
ValueChanged="@OnWaveformWidthChanged"
|
||||
Min="0"
|
||||
Max="1"
|
||||
Step="0.001"
|
||||
Size="72"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.ZoomIn" Size="Size.Small" Class="mix-visualizer-control-icon" />
|
||||
<div class="mix-visualizer-controls-bar @(Expanded ? "is-expanded" : "")" aria-hidden="@(!Expanded)">
|
||||
|
||||
<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.Secondary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Speed" Size="Size.Small" Class="mix-visualizer-control-icon" />
|
||||
</div>
|
||||
|
||||
<div class="mix-visualizer-control" role="group" aria-label="Bubblyness">
|
||||
<RadialKnob Value="@ControlState.Bubblyness"
|
||||
ValueChanged="@OnBubblynessChanged"
|
||||
Min="0"
|
||||
Max="1"
|
||||
Step="0.001"
|
||||
Size="72"
|
||||
Color="Color.Primary" />
|
||||
<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.Secondary" />
|
||||
<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.Secondary" />
|
||||
<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.Secondary" />
|
||||
<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.Secondary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.BubbleChart" Size="Size.Small" Class="mix-visualizer-control-icon" />
|
||||
</div>
|
||||
|
||||
<div class="mix-visualizer-control" role="group" aria-label="Detach (unleash the lava lamp)">
|
||||
<RadialKnob Value="@ControlState.Detach"
|
||||
ValueChanged="@OnDetachChanged"
|
||||
Min="0"
|
||||
Max="1"
|
||||
Step="0.001"
|
||||
Size="72"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Air" Size="Size.Small" Class="mix-visualizer-control-icon" />
|
||||
<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.Secondary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Compress" Size="Size.Small" Class="mix-visualizer-control-icon" />
|
||||
</div>
|
||||
|
||||
<div class="mix-visualizer-control" role="group" aria-label="Color-shift speed">
|
||||
<RadialKnob Value="@ControlState.ColorShiftSpeed"
|
||||
ValueChanged="@OnColorShiftSpeedChanged"
|
||||
Min="0"
|
||||
Max="1"
|
||||
Step="0.001"
|
||||
Size="72"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Palette" Size="Size.Small" Class="mix-visualizer-control-icon" />
|
||||
<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.Secondary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.SettingsEthernet" Size="Size.Small" Class="mix-visualizer-control-icon" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
// R2 TEMP: the resolution knob is repurposed to WAVEFORM WIDTH (already normalized [0,1], binds
|
||||
// directly). R4 restores the log zoom mapping (MixZoomMapping) and gives width its own knob.
|
||||
/// <summary>
|
||||
/// Whether the knob bar is expanded. Owned by the host page (the lava-lamp toggle button flips it);
|
||||
/// drives the CSS open/close transition. When false the bar collapses to zero size in place.
|
||||
/// </summary>
|
||||
[Parameter] public bool Expanded { 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.
|
||||
|
||||
private void OnScrollSpeedChanged(double value)
|
||||
{
|
||||
ControlState.ScrollSpeed = value;
|
||||
ControlState.NotifyChanged();
|
||||
}
|
||||
|
||||
private void OnGradientRotationSpeedChanged(double value)
|
||||
{
|
||||
ControlState.GradientRotationSpeed = value;
|
||||
ControlState.NotifyChanged();
|
||||
}
|
||||
|
||||
private void OnLavaGravityChanged(double value)
|
||||
{
|
||||
ControlState.LavaGravity = value;
|
||||
ControlState.NotifyChanged();
|
||||
}
|
||||
|
||||
private void OnLavaHeatChanged(double value)
|
||||
{
|
||||
ControlState.LavaHeat = value;
|
||||
ControlState.NotifyChanged();
|
||||
}
|
||||
|
||||
private void OnBlobDensityChanged(double value)
|
||||
{
|
||||
ControlState.BlobDensity = value;
|
||||
ControlState.NotifyChanged();
|
||||
}
|
||||
|
||||
private void OnCollisionStrengthChanged(double value)
|
||||
{
|
||||
ControlState.CollisionStrength = value;
|
||||
ControlState.NotifyChanged();
|
||||
}
|
||||
|
||||
private void OnWaveformWidthChanged(double value)
|
||||
{
|
||||
ControlState.WaveformWidth = value;
|
||||
ControlState.NotifyChanged();
|
||||
}
|
||||
|
||||
private void OnBubblynessChanged(double value)
|
||||
{
|
||||
ControlState.Bubblyness = value;
|
||||
ControlState.NotifyChanged();
|
||||
}
|
||||
|
||||
private void OnDetachChanged(double value)
|
||||
{
|
||||
ControlState.Detach = value;
|
||||
ControlState.NotifyChanged();
|
||||
}
|
||||
|
||||
private void OnColorShiftSpeedChanged(double value)
|
||||
{
|
||||
ControlState.ColorShiftSpeed = value;
|
||||
ControlState.NotifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,65 @@
|
||||
/* The controls live inside the lava-lamp popover (Wave 4). Four knob+icon stacks laid out in a row.
|
||||
On a narrow viewport the row wraps to 2×2 so all four stay reachable (spec §7d: none may drop). */
|
||||
.mix-visualizer-controls {
|
||||
/* The seven-knob bar lives INLINE in the mix-detail controls area and animates open/closed in place
|
||||
(lava reframe §7b) — NOT a popover or drawer. Collapsed, it has zero size and is fully transparent;
|
||||
the @Expanded flag (mirrored to the .is-expanded class) transitions it open. We animate max-width +
|
||||
max-height + opacity + transform together so the bar reads as the controls growing in place rather
|
||||
than a panel popping in. Closed state is pointer-events:none + visibility:hidden so collapsed knobs
|
||||
are not focusable or hit-testable. */
|
||||
.mix-visualizer-controls-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 1rem 1.25rem;
|
||||
padding: 0.25rem;
|
||||
justify-content: flex-end;
|
||||
gap: 0.85rem 1rem;
|
||||
|
||||
/* Collapsed: zero footprint, slid up toward the toggle, transparent. */
|
||||
max-width: 0;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
/* NowPlaying glass surface (§7e): translucent dark shim, soft blur, rounded, secondary-tinted
|
||||
hairline — matches the session-hero overlay family. Padding animates in with the size. */
|
||||
padding: 0;
|
||||
border-radius: 10px;
|
||||
background: rgba(13, 27, 42, 0.55);
|
||||
border: 1px solid color-mix(in srgb, var(--mud-palette-secondary) 22%, transparent);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
||||
|
||||
transition:
|
||||
max-width 0.32s cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
max-height 0.32s cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
opacity 0.24s ease,
|
||||
padding 0.32s cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
transform 0.32s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
|
||||
/* One control: a RadialKnob with its Material icon as a caption underneath. RadialKnob has no icon
|
||||
slot, so the icon rides adjacent (spec §7e). Center the pair so the four read as a tidy row. */
|
||||
.mix-visualizer-controls-bar.is-expanded {
|
||||
max-width: 640px;
|
||||
max-height: 420px;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
padding: 0.85rem 1rem;
|
||||
}
|
||||
|
||||
/* One control: a RadialKnob with its Material icon caption underneath. RadialKnob has no icon slot, so
|
||||
the icon rides adjacent (§7d). Center the pair so the seven read as a tidy bar. */
|
||||
.mix-visualizer-control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
/* The caption icon is a MudIcon (a Razor component), so Blazor CSS isolation does not stamp the scope
|
||||
attribute onto its element — reach it with ::deep. */
|
||||
attribute onto its element — reach it with ::deep. Tinted to the secondary accent and the
|
||||
overlay-label opacity so it matches the session-hero NowPlaying captions (§7e). */
|
||||
.mix-visualizer-control ::deep .mix-visualizer-control-icon {
|
||||
opacity: 0.7;
|
||||
color: var(--mud-palette-secondary);
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
@@ -58,9 +58,9 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
// datum-fetch / subscription / playback-coupling seams log to the browser console (prefixed
|
||||
// `[MixVisualizer]`, same as the JS logs so the two interleave into one timeline). These pinpoint
|
||||
// which upstream link is broken when the ribbon stays blank — set false once confirmed healthy.
|
||||
// ON for the Phase 10 reframe Wave R2 lava test (matches the JS-side DEBUG in
|
||||
// MixVisualizer.ts). Daniel evaluates the physics in-browser; flip back to false at
|
||||
// reframe close along with the JS flag.
|
||||
// ON for the Phase 10 reframe Wave R4 controls test (matches the JS-side DEBUG in
|
||||
// MixVisualizer.ts). Daniel evaluates the seven-knob bar + pause behavior in-browser; flip back to
|
||||
// false at reframe close along with the JS flag.
|
||||
private static readonly bool Debug = true;
|
||||
private const string Tag = "[MixVisualizer]";
|
||||
|
||||
@@ -176,9 +176,9 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
// Seed the module with the current state now that it exists. All four control values
|
||||
// Seed the module with the current state now that it exists. All seven control values
|
||||
// come from the shared (session-persisted) state, so a mix opened mid-session seeds the
|
||||
// module with the slider positions the listener left them at.
|
||||
// module with the knob positions the listener left them at.
|
||||
await PushControlsAsync();
|
||||
await PushDatumAsync();
|
||||
await PushPlaybackAsync();
|
||||
@@ -190,10 +190,10 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
await PushThemeIfChangedAsync();
|
||||
}
|
||||
|
||||
// The controls row mutated a slider on the shared state and raised Changed. Push all four control
|
||||
// values (cheap scalar interop). In the Phase 10 reframe Wave R2, three of them are re-routed to
|
||||
// the lava physics inside the JS handle (setBubblyness→gravity, setDetach→heat,
|
||||
// setColorShiftSpeed→collision) — see MixVisualizer.ts; the bridge contract is unchanged.
|
||||
// The controls bar mutated a knob on the shared state and raised Changed. Push all seven control
|
||||
// values (cheap scalar interop). Each control now drives its own dedicated dial in the JS handle
|
||||
// (lava reframe Wave R4) — scroll speed → visible-time-span, plus the six lava/colour dials; see
|
||||
// PushControlsAsync. The bridge stays the sole owner of the JS module handle.
|
||||
private void OnControlStateChanged() => InvokeAsync(async () =>
|
||||
{
|
||||
await PushControlsAsync();
|
||||
@@ -202,20 +202,29 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
// ── Bridge pushes. Each is a no-op until the module handle exists. ───────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Push the control values to the module from the shared state. Used to seed on first render and
|
||||
/// to re-push when the controls row signals a change. In the Phase 10 reframe Wave R2 the four
|
||||
/// live controls are routed to the lava physics by the JS handle (see MixVisualizer.ts):
|
||||
/// Bubblyness→gravity, Detach→heat, ColorShiftSpeed→collision, and the repurposed resolution knob
|
||||
/// (WaveformWidth)→waveform width. VisibleSeconds is still seeded once via setZoom so the window
|
||||
/// holds at its default; the controls row no longer mutates it this wave. Bridge contract unchanged.
|
||||
/// Push the seven control values to the module from the shared state. Used to seed on first render
|
||||
/// and to re-push when the controls bar signals a change (lava reframe Wave R4). Each value is its
|
||||
/// own dedicated dial now — no more R2 temp-remapping:
|
||||
/// <list type="bullet">
|
||||
/// <item>scroll speed [0,1] is mapped to a visible time-span via <see cref="MixZoomMapping"/> and
|
||||
/// pushed through <c>setScrollSpeed</c> (higher speed → tighter window → faster scroll);</item>
|
||||
/// <item>gradient rotation speed → <c>setGradientRotationSpeed</c> (inert until Wave R3);</item>
|
||||
/// <item>gravity / heat / blob density / collision strength → their dedicated lava-physics dials;</item>
|
||||
/// <item>waveform width → the ribbon-extent uniform.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
private async Task PushControlsAsync()
|
||||
{
|
||||
if (_handle is null) return;
|
||||
await _handle.InvokeVoidAsync("setZoom", ControlState.VisibleSeconds);
|
||||
await _handle.InvokeVoidAsync("setBubblyness", ControlState.Bubblyness);
|
||||
await _handle.InvokeVoidAsync("setDetach", ControlState.Detach);
|
||||
await _handle.InvokeVoidAsync("setColorShiftSpeed", ControlState.ColorShiftSpeed);
|
||||
// Scroll speed is a normalized [0,1] axis; map it to the visible time-span the renderer scrolls
|
||||
// through. The log map keeps the even-to-the-hand feel the old zoom slider had.
|
||||
var visibleSeconds = MixZoomMapping.FractionToSeconds(ControlState.ScrollSpeed);
|
||||
await _handle.InvokeVoidAsync("setScrollSpeed", visibleSeconds);
|
||||
await _handle.InvokeVoidAsync("setGradientRotationSpeed", ControlState.GradientRotationSpeed);
|
||||
await _handle.InvokeVoidAsync("setLavaGravity", ControlState.LavaGravity);
|
||||
await _handle.InvokeVoidAsync("setLavaHeat", ControlState.LavaHeat);
|
||||
await _handle.InvokeVoidAsync("setBlobDensity", ControlState.BlobDensity);
|
||||
await _handle.InvokeVoidAsync("setCollisionStrength", ControlState.CollisionStrength);
|
||||
await _handle.InvokeVoidAsync("setWaveformWidth", ControlState.WaveformWidth);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user