Files
qbz/crates/qbz-ui/ui/immersive/ImmersiveSpectralOverlay.slint
T
vicrodh 226c2118ef feat(immersive): three new visualizer scenes (Wave Bed, Spectral Ribbon, Line Bed) + polish
Wave Bed (focus mode 6): centered vertical-mirror waveform. Spectral Ribbon (shader-mode 4): paint-as-you-play GPU spectrogram with green axes + header overlay, bass-at-bottom orientation aligned to the axes, and a real-time ceiling line tracking the highest active frequency. Line Bed (shader-mode 5): wgpu line-strip waterfall with a leveled camera, B-spline-smoothed continuous curves, a primary->accent height gradient, a bottom cutoff above the player, 2x supersampling, and a sampled-texture heights feed (no vertex storage buffer, so it runs on Slint's downlevel wgpu device).

Also: Plasma reworked to be far more reactive (4 ink emitters, beat splats, faster turbulence, quicker decay); immersive song card no longer truncates the album/artist line; player heart reflects favorite state; player bar narrowed; sample-rate and bit-depth wired through for the spectral header.

cc
2026-06-28 23:55:17 -06:00

96 lines
3.7 KiB
Plaintext

// Spectral Ribbon overlay — green axes + labels + the stream header, drawn over
// the GPU spectrogram (shader-mode 4). Everything is positioned as FRACTIONS of
// the viewport so the axes grow/shrink with the window. The plot rectangle MUST
// match spectral_ribbon.wgsl (PLOT_X0..Y1) so the labels line up with the
// heatmap. A plain Rectangle with no TouchArea → input passes through to the
// player bar below.
import { NowPlayingState } from "../state.slint";
export component ImmersiveSpectralOverlay inherits Rectangle {
in property <length> viewport-width: 1280px;
in property <length> viewport-height: 720px;
// Plot rect — SAME fractions as spectral_ribbon.wgsl.
property <length> plot-x0: root.viewport-width * 0.055;
property <length> plot-x1: root.viewport-width * 0.970;
property <length> plot-y0: root.viewport-height * 0.070;
property <length> plot-y1: root.viewport-height * 0.780;
property <length> plot-w: root.plot-x1 - root.plot-x0;
property <length> plot-h: root.plot-y1 - root.plot-y0;
property <color> axis: #5fb87a;
// Nyquist in kHz (sample-rate / 2). Falls back to 22 (44.1k) when unknown.
property <float> nyq-khz: NowPlayingState.sample-rate-hz > 0
? NowPlayingState.sample-rate-hz / 2000.0
: 22.05;
property <int> dur: NowPlayingState.duration-secs;
width: root.viewport-width;
height: root.viewport-height;
// --- Stream header (top center, green box) ---------------------------
Rectangle {
x: (root.viewport-width - self.width) / 2;
y: root.viewport-height * 0.045;
width: hdr.width + 28px;
height: 22px;
border-radius: 5px;
border-width: 1px;
border-color: root.axis;
background: #0a140de6;
hdr := Text {
x: 14px;
y: (parent.height - self.height) / 2;
text: "Stream 1/1: Audio Stream, \{NowPlayingState.sample-rate-hz} Hz, \{NowPlayingState.bit-depth} bits, FFT:4096, Bands:512";
color: root.axis;
font-size: 12px;
}
}
// --- Axis frame (L-shape) --------------------------------------------
Rectangle { // vertical (frequency)
x: root.plot-x0;
y: root.plot-y0;
width: 1px;
height: root.plot-h;
background: root.axis;
}
Rectangle { // horizontal (time)
x: root.plot-x0;
y: root.plot-y1;
width: root.plot-w;
height: 1px;
background: root.axis;
}
// --- Frequency axis labels (Y, left) — 0 .. Nyquist ------------------
for f in [0.0, 0.25, 0.5, 0.75, 1.0]: Text {
x: root.plot-x0 - 46px;
y: root.plot-y1 - f * root.plot-h - self.height / 2;
width: 40px;
horizontal-alignment: right;
text: "\{Math.round(root.nyq-khz * f)}k";
color: root.axis;
font-size: 11px;
}
// --- Time axis labels (X, bottom) — 30 s ticks -----------------------
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30]: Text {
visible: root.dur > 0 && (i * 30) <= root.dur;
x: root.plot-x0 + (root.dur > 0 ? root.plot-w * ((i * 30) / root.dur) : 0) - self.width / 2;
y: root.plot-y1 + 6px;
text: "\{Math.floor((i * 30) / 60)}:\{Math.mod(i * 30, 60) < 10 ? "0" : ""}\{Math.mod(i * 30, 60)}";
color: root.axis;
font-size: 10px;
}
// Final duration marker at the right edge.
if root.dur > 0: Text {
x: root.plot-x1 - self.width / 2;
y: root.plot-y1 + 6px;
text: "\{Math.floor(root.dur / 60)}:\{Math.mod(root.dur, 60) < 10 ? "0" : ""}\{Math.mod(root.dur, 60)}";
color: root.axis;
font-size: 10px;
}
}