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);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,31 +48,25 @@ else
|
||||
BackLabel="All mixes"
|
||||
ShowMeta="@(hasGenre || hasDate)">
|
||||
<TopRightAction>
|
||||
@* Lava-lamp button top-right, across from the back link. Toggles a popover holding the
|
||||
four visualizer knobs (spec §7c/§7d). The controls themselves are unchanged — they
|
||||
mutate the shared MixVisualizerControlState; the backdrop bridge pushes the uniforms.
|
||||
The popover only progressively-discloses them off the always-visible row. *@
|
||||
<MudTooltip Text="Visualizer settings">
|
||||
<MudIconButton Icon="@DDIcons.LavaLamp"
|
||||
Size="Size.Large"
|
||||
Color="Color.Secondary"
|
||||
Disabled="@(!RendererInfo.IsInteractive)"
|
||||
OnClick="@ToggleSettings"
|
||||
aria-label="Visualizer settings" />
|
||||
</MudTooltip>
|
||||
@* Lava-lamp button top-right, across from the back link. Toggles the INLINE seven-knob
|
||||
control bar that animates open/closed in place below it (lava reframe §7b) — not a
|
||||
popover or drawer. The icon swaps to its FILLED variant while the bar is expanded
|
||||
(§7f / Part B). The controls bar mutates the shared MixVisualizerControlState; the
|
||||
backdrop bridge pushes the dials. A knob drag does not collapse the bar — the toggle
|
||||
only flips on this button's click, never on a drag landing in the bar. *@
|
||||
<div class="mix-visualizer-controls-anchor">
|
||||
<MudTooltip Text="Visualizer settings">
|
||||
<MudIconButton Icon="@(_controlsExpanded ? DDIcons.LavaLampFilled : DDIcons.LavaLamp)"
|
||||
Size="Size.Large"
|
||||
Color="Color.Secondary"
|
||||
Disabled="@(!RendererInfo.IsInteractive)"
|
||||
OnClick="@ToggleSettings"
|
||||
aria-label="Visualizer settings"
|
||||
aria-expanded="@_controlsExpanded" />
|
||||
</MudTooltip>
|
||||
|
||||
@* Outside-click close via MudOverlay (the SharePopover idiom). A knob drag never lands
|
||||
on this overlay — the knob's own global capture overlay is a child of the popover
|
||||
content above it — so dragging a knob does not dismiss the popover. *@
|
||||
<MudOverlay Visible="@_settingsOpen" OnClick="@CloseSettings" AutoClose="true" />
|
||||
|
||||
<MudPopover Open="@_settingsOpen"
|
||||
Fixed="false"
|
||||
AnchorOrigin="Origin.BottomRight"
|
||||
TransformOrigin="Origin.TopRight"
|
||||
Class="mix-visualizer-popover">
|
||||
<MixVisualizerControls />
|
||||
</MudPopover>
|
||||
<MixVisualizerControls Expanded="@_controlsExpanded" />
|
||||
</div>
|
||||
</TopRightAction>
|
||||
<Hero>
|
||||
<div class="mix-detail-cover">
|
||||
@@ -114,11 +108,10 @@ else
|
||||
@code {
|
||||
protected override string PersistKey => "mix-detail";
|
||||
|
||||
// Lava-lamp settings popover open state. Pure presentation over MixVisualizerControlState — the
|
||||
// popover discloses the four knobs; toggling it touches no control value or bridge push.
|
||||
private bool _settingsOpen;
|
||||
// Lava-lamp inline knob-bar expanded state. Pure presentation over MixVisualizerControlState — the
|
||||
// bar discloses the seven knobs and animates open/closed; toggling it touches no control value or
|
||||
// bridge push. The lava-lamp button's filled/outline glyph is driven off this same flag.
|
||||
private bool _controlsExpanded;
|
||||
|
||||
private void ToggleSettings() => _settingsOpen = !_settingsOpen;
|
||||
|
||||
private void CloseSettings() => _settingsOpen = false;
|
||||
private void ToggleSettings() => _controlsExpanded = !_controlsExpanded;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,28 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* The lava-lamp toggle + its inline knob-bar. The anchor stacks the button over the bar and lets the
|
||||
bar grow downward/leftward in place when expanded, without shoving the masthead. The bar itself is
|
||||
absolutely positioned under the button (top-right of the detail body), so its open/close animation
|
||||
reads as the controls growing out from the icon rather than reflowing the page (lava reframe §7b). */
|
||||
.mix-visualizer-controls-anchor {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
/* MixVisualizerControls renders the .mix-visualizer-controls-bar as its single root. It is a child
|
||||
Razor component, so its scope attribute is not stamped here — reach the bar with ::deep to position
|
||||
it as a floating-but-inline element anchored to the toggle's bottom-right. */
|
||||
.mix-visualizer-controls-anchor ::deep .mix-visualizer-controls-bar {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.5rem);
|
||||
right: 0;
|
||||
z-index: 3;
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
/* Medium square cover — deliberately smaller than the 360px cut cover so the
|
||||
waveform backdrop keeps room. The placeholder/art MudPaper fills this frame. */
|
||||
.mix-detail-cover {
|
||||
|
||||
@@ -1,89 +1,103 @@
|
||||
namespace DeepDrftPublic.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the Mix visualizer's four continuous-control positions for the lifetime of the WASM app
|
||||
/// Holds the Mix visualizer's seven continuous-control positions for the lifetime of the WASM app
|
||||
/// instance. Scoped in DI, so it lives across SPA navigations within one listening session — open a
|
||||
/// second mix and the sliders keep where you left them — but a fresh page load (F5) constructs a new
|
||||
/// second mix and the knobs keep where you left them — but a fresh page load (F5) constructs a new
|
||||
/// instance, resetting to defaults. That matches the spec's "persist within session, reset on fresh
|
||||
/// load" without any cookie/localStorage round-trip (see mix-visualizer-webgl-renderer §3c).
|
||||
/// load" without any cookie/localStorage round-trip (lava reframe §7c).
|
||||
///
|
||||
/// One state object, four properties — not four sibling holders (Daniel's decided shape, spec §3c).
|
||||
/// Each C#-side default mirrors a TS-side tuning anchor in MixVisualizer.ts; keep the two in sync, as
|
||||
/// the existing <c>DefaultVisibleSeconds</c> / <c>DEFAULT_VISIBLE_SECONDS</c> pair does.
|
||||
/// One state object, seven properties — not seven sibling holders, and (deliberately) NO constructor
|
||||
/// parameters: this is a plain scoped value holder, so widening it from four to seven properties adds
|
||||
/// fields + defaults only and never forces a consumer constructor to grow. Each C#-side default mirrors
|
||||
/// a TS-side tuning anchor in MixVisualizer.ts; keep the two in sync, as the existing
|
||||
/// <c>DefaultVisibleSeconds</c> / <c>DEFAULT_VISIBLE_SECONDS</c> pair does.
|
||||
///
|
||||
/// <para>
|
||||
/// <see cref="Changed"/> is the decoupling seam between the controls row and the visualizer bridge.
|
||||
/// <see cref="Changed"/> is the decoupling seam between the controls bar and the visualizer bridge.
|
||||
/// The controls component (a sibling of the backdrop in the page tree) only mutates this shared state
|
||||
/// and raises <see cref="Changed"/>; the bridge component (<c>MixWaveformVisualizer</c>) subscribes
|
||||
/// and pushes the affected uniform to the JS module. This keeps the JS module handle single-owned by
|
||||
/// the bridge — no handle sharing, no service-locator, no cross-component interop.
|
||||
/// and pushes the affected uniform/dial to the JS module. This keeps the JS module handle single-owned
|
||||
/// by the bridge — no handle sharing, no service-locator, no cross-component interop.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class MixVisualizerControlState
|
||||
{
|
||||
/// <summary>
|
||||
/// Default opening window. Mirrors <c>DEFAULT_VISIBLE_SECONDS</c> in MixVisualizer.ts; keep the
|
||||
/// two in sync (the TS owns the rendering anchors, this owns the C#-side session default).
|
||||
/// </summary>
|
||||
public const double DefaultVisibleSeconds = 10.0;
|
||||
|
||||
// R2 TEMP (Phase 10 reframe Wave R2): the FOUR controls below are re-routed to the new
|
||||
// lava physics for Daniel's in-browser test — the JS handle setters map them as:
|
||||
// Bubblyness → lava GRAVITY, Detach → lava HEAT, ColorShiftSpeed → COLLISION STRENGTH,
|
||||
// Resolution (VisibleSeconds knob) → WAVEFORM WIDTH (see MixVisualizerControls.razor).
|
||||
// The defaults are tuned to Daniel's sweet spot (~20% gravity, ~100% heat). Wave R4
|
||||
// replaces this with the proper seven-knob set + its own typed properties. Keep these
|
||||
// mirrored to the DEFAULT_* anchors in MixVisualizer.ts, as the existing sync discipline.
|
||||
// ── The seven control defaults (lava reframe §7a). Each mirrors a DEFAULT_* anchor in
|
||||
// MixVisualizer.ts; keep the two in sync, as the existing default-sync discipline requires.
|
||||
// Feel-anchors only — Daniel tunes on screen; the ~20% gravity / ~100% heat pair is his stated
|
||||
// sweet spot (§4c).
|
||||
|
||||
/// <summary>
|
||||
/// Default GRAVITY dial (R2 temp; was bulge). Mirrors <c>DEFAULT_BUBBLYNESS</c> in MixVisualizer.ts.
|
||||
/// Normalized [0,1]; 0 = near-weightless float, 1 = wax falls + settles fast. Tuned to Daniel's
|
||||
/// ~20% sweet spot so the wax is buoyant-dominated and flows.
|
||||
/// Default scroll-speed dial. Mirrors <c>DEFAULT_SCROLL_SPEED</c> in MixVisualizer.ts. Normalized
|
||||
/// [0,1] → mapped to the visible time-span via <see cref="MixZoomMapping"/> (0 = slow/wide window,
|
||||
/// 1 = fast/tight window). Opens mid-range.
|
||||
/// </summary>
|
||||
public const double DefaultBubblyness = 0.2;
|
||||
public const double DefaultScrollSpeed = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// Default HEAT dial (R2 temp; was detach). Mirrors <c>DEFAULT_DETACH</c> in MixVisualizer.ts.
|
||||
/// Normalized [0,1]; 0 = wax rests at the bottom (collision-only), 1 = lots of small turbulent
|
||||
/// bubbles. Tuned to Daniel's ~100% sweet spot.
|
||||
/// Default gradient-rotation-speed dial. Mirrors <c>DEFAULT_GRADIENT_ROTATION_SPEED</c> in
|
||||
/// MixVisualizer.ts. Normalized [0,1] → slow→fast anchor-rotation. INERT until Wave R3 builds the
|
||||
/// OKLab three-colour gradient that consumes it.
|
||||
/// </summary>
|
||||
public const double DefaultDetach = 1.0;
|
||||
public const double DefaultGradientRotationSpeed = 0.3;
|
||||
|
||||
/// <summary>
|
||||
/// Default COLLISION-STRENGTH dial (R2 temp; was color-shift). Mirrors
|
||||
/// <c>DEFAULT_COLOR_SHIFT_SPEED</c> in MixVisualizer.ts. Normalized [0,1]; 0 = soft mush,
|
||||
/// 1 = hard elastic throw.
|
||||
/// Default lava-gravity dial. Mirrors <c>DEFAULT_LAVA_GRAVITY</c> in MixVisualizer.ts. Normalized
|
||||
/// [0,1]; 0 = near-weightless float, 1 = wax falls + settles fast. Tuned to Daniel's ~20% sweet spot.
|
||||
/// </summary>
|
||||
public const double DefaultColorShiftSpeed = 0.5;
|
||||
public const double DefaultLavaGravity = 0.2;
|
||||
|
||||
/// <summary>
|
||||
/// Default WAVEFORM-WIDTH dial (R2 temp; routed to the resolution/zoom knob this wave). Mirrors
|
||||
/// <c>DEFAULT_WAVEFORM_WIDTH</c> in MixVisualizer.ts. Normalized [0,1]; 1 = full ribbon width
|
||||
/// (prior look), lower narrows the band so the lava gets more room. Opens at full width.
|
||||
/// Default lava-heat dial. Mirrors <c>DEFAULT_LAVA_HEAT</c> in MixVisualizer.ts. Normalized [0,1];
|
||||
/// 0 = wax rests at the bottom (collision-only), 1 = many small turbulent rising bubbles. Tuned to
|
||||
/// Daniel's ~100% sweet spot.
|
||||
/// </summary>
|
||||
public const double DefaultWaveformWidth = 1.0;
|
||||
|
||||
/// <summary>Visible time-span in seconds (the resolution/zoom control). Reused as-is from 8.K.</summary>
|
||||
public double VisibleSeconds { get; set; } = DefaultVisibleSeconds;
|
||||
|
||||
/// <summary>Bulge amount, normalized [0,1]. Inert until Wave 3 consumes the uniform.</summary>
|
||||
public double Bubblyness { get; set; } = DefaultBubblyness;
|
||||
|
||||
/// <summary>Lava-lamp detachment, normalized [0,1]. Inert until Wave 3 consumes the uniform.</summary>
|
||||
public double Detach { get; set; } = DefaultDetach;
|
||||
|
||||
/// <summary>Gradient-morph rate, normalized [0,1]. Inert until Wave 3 consumes the uniform.</summary>
|
||||
public double ColorShiftSpeed { get; set; } = DefaultColorShiftSpeed;
|
||||
public const double DefaultLavaHeat = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Waveform width, normalized [0,1]. R2 TEMP: routed to the resolution/zoom knob for in-browser
|
||||
/// testing (Wave R4 gives it its own knob and restores the resolution knob to VisibleSeconds).
|
||||
/// Default blob-density dial. Mirrors <c>DEFAULT_BLOB_DENSITY</c> in MixVisualizer.ts. Normalized
|
||||
/// [0,1]; 0 = a few large lazy blobs, 1 = many smaller active blobs.
|
||||
/// </summary>
|
||||
public const double DefaultBlobDensity = 0.4;
|
||||
|
||||
/// <summary>
|
||||
/// Default collision-strength dial. Mirrors <c>DEFAULT_COLLISION_STRENGTH</c> in MixVisualizer.ts.
|
||||
/// Normalized [0,1]; 0 = soft mush, 1 = hard elastic up-and-out throw.
|
||||
/// </summary>
|
||||
public const double DefaultCollisionStrength = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// Default waveform-width dial. Mirrors <c>DEFAULT_WAVEFORM_WIDTH</c> in MixVisualizer.ts.
|
||||
/// Normalized [0,1]; 1 = full ribbon width, lower narrows the band so the lava gets more room.
|
||||
/// </summary>
|
||||
public const double DefaultWaveformWidth = 0.6;
|
||||
|
||||
/// <summary>Apparent bottom-to-top scroll rate, normalized [0,1]. Bridge maps it to a visible
|
||||
/// time-span via <see cref="MixZoomMapping"/>; the standalone resolution/zoom control is gone.</summary>
|
||||
public double ScrollSpeed { get; set; } = DefaultScrollSpeed;
|
||||
|
||||
/// <summary>Gradient anchor-rotation rate, normalized [0,1]. Inert until Wave R3 consumes it.</summary>
|
||||
public double GradientRotationSpeed { get; set; } = DefaultGradientRotationSpeed;
|
||||
|
||||
/// <summary>Downward force on the wax, normalized [0,1].</summary>
|
||||
public double LavaGravity { get; set; } = DefaultLavaGravity;
|
||||
|
||||
/// <summary>Energy into the lava system, normalized [0,1]. 0 = rest-at-bottom, 1 = roiling.</summary>
|
||||
public double LavaHeat { get; set; } = DefaultLavaHeat;
|
||||
|
||||
/// <summary>Amount of wax (blob count/size), normalized [0,1].</summary>
|
||||
public double BlobDensity { get; set; } = DefaultBlobDensity;
|
||||
|
||||
/// <summary>Collision hardness, normalized [0,1]. 0 = soft mush, 1 = hard up-and-out throw.</summary>
|
||||
public double CollisionStrength { get; set; } = DefaultCollisionStrength;
|
||||
|
||||
/// <summary>Waveform-band horizontal extent, normalized [0,1]. Narrowing clears room for the lava.</summary>
|
||||
public double WaveformWidth { get; set; } = DefaultWaveformWidth;
|
||||
|
||||
/// <summary>
|
||||
/// Raised whenever any control value changes. The visualizer bridge subscribes to push the
|
||||
/// affected uniform(s). Mutators set the property then raise this; subscribers re-read the values.
|
||||
/// affected dial(s). Mutators set the property then raise this; subscribers re-read the values.
|
||||
/// </summary>
|
||||
public event Action? Changed;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user