- AlbumReactivePanel: drop the eight 100ms Behaviors on 30 Hz data (a retriggered Behavior animates at display rate and every animation frame is a full-window present), and read the settle — not the publishing bridge list — in the energy formulas (the .length guard re-evaluated the visual bindings on every FFT publish). - LyricsSyncEngine: the fast cadence rides the pulse; the 250 ms idle re-check keeps its own timer. - EqualizerBars: the phase accumulates on the pulse and the 'active' gate is host-provided (invisible subtrees still dirty the window). Measured on the owner's machine, immersive reactive panel open: 132 -> ~33 presents/s, 83% -> ~25% GPU. km
111 lines
4.1 KiB
QML
111 lines
4.1 KiB
QML
// EqualizerBars — the 4-bar "now playing" equalizer (§6.7 of the
|
|
// 2026-08-02 immersive-port contract), port of the EqualizerBars component
|
|
// in crates/qbz-ui/ui/immersive/ImmersiveTrackInfo.slint:23-74.
|
|
//
|
|
// 18x14 box, four 3px bars at 2px spacing (manual x: 0/5/10/15 — a Row
|
|
// positioner would own the children's y and the bars must bottom-align by
|
|
// their OWN animated height). Base heights 60/100/40/80% of the 14px box,
|
|
// each sweeping scaleY 0.3..1.0 on the shared 800ms loop with phase offsets
|
|
// 0 / 0.25 / 0.125 / 0.375 (the Svelte animation-delays 0.0/0.2/0.1/0.3 of
|
|
// the 0.8s period, as fractions).
|
|
//
|
|
// The Slint loop is pull-based off animation-tick(); the Qt twin accumulates
|
|
// `t` on the shared shell pulse (see the note at the property), so the whole
|
|
// equalizer ticks at the shell's 30 Hz instead of the display rate. The wave
|
|
// is the Slint formula verbatim:
|
|
// 0.3 + 0.7 * (0.5 - 0.5*cos((t+phase mod 1) * 2π))
|
|
// (Svelte keyframes 0/100% -> 0.3, 50% -> 1.0). The loop runs only while the
|
|
// item is visible AND the immersive is open — every mount site gates
|
|
// visibility on npPlaying (ImmersiveTrackInfo.slint:85,:100), so a paused
|
|
// player animates nothing.
|
|
// Reduce-motion: the seam EXISTS now — `QbzShell.reduceMotion`, built by the
|
|
// cortinilla-parity contract (§5.g). This surface is simply not a consumer
|
|
// yet; wiring it is a one-liner (throttle the tick when it is true) and its
|
|
// own change, not a side effect of someone else's commit.
|
|
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
|
|
Item {
|
|
id: root
|
|
|
|
/// Bar color — every immersive mount passes #7c3aed.
|
|
property color tint: "#7c3aed"
|
|
|
|
width: 18
|
|
height: 14
|
|
implicitWidth: 18
|
|
implicitHeight: 14
|
|
|
|
// Loop phase 0..1, 800ms (ImmersiveTrackInfo.slint:32). Advanced on the
|
|
// SHELL PULSE (QbzShell.pulseMs), not a NumberAnimation: an 800ms
|
|
// infinite animation is serviced by the animation driver at DISPLAY rate
|
|
// (180 Hz on the owner's panel) and every update is a whole-window
|
|
// present. The wave's period is 800 ms, so the 30 Hz pulse samples it
|
|
// ~24 times per loop — indistinguishable. Riding the pulse also means
|
|
// ZERO extra presents: the bars move in the same frame as everything
|
|
// else the pulse moves.
|
|
//
|
|
// The `active` gate belongs to the HOST, because this component mounts
|
|
// in always-alive-but-invisible trees (the immersive overlay) where a
|
|
// local `visible` check alone keeps ticking with the overlay closed —
|
|
// and invisible geometry changes still dirty the whole window (the
|
|
// 2026-08-13 SpectrumPanel finding). ImmersiveTrackMeta passes
|
|
// `QbzImmersive.open`; the track row passes its playing-row predicate.
|
|
// `_tickMs` must match settings_qt::shell_pulse_ms (default 33) — a
|
|
// mismatch only mis-speeds the loop, never the present rate.
|
|
property bool active: true
|
|
property real t: 0
|
|
readonly property int _tickMs: 33
|
|
Connections {
|
|
target: QbzShell
|
|
function onPulseMsChanged() {
|
|
if (root.active && root.visible)
|
|
root.t = (root.t + root._tickMs / 800.0) % 1.0
|
|
}
|
|
}
|
|
|
|
// Slint wave() verbatim (:34-36).
|
|
function wave(phase) {
|
|
var u = (root.t + phase) % 1.0
|
|
return 0.3 + 0.7 * (0.5 - 0.5 * Math.cos(u * 6.283185))
|
|
}
|
|
|
|
// bar 1: delay 0.0, base 60% (:42-48)
|
|
Rectangle {
|
|
x: 0
|
|
width: 3
|
|
y: root.height - height
|
|
height: root.height * 0.6 * root.wave(0.0)
|
|
radius: 1
|
|
color: root.tint
|
|
}
|
|
// bar 2: delay 0.2, base 100% (:50-56)
|
|
Rectangle {
|
|
x: 5
|
|
width: 3
|
|
y: root.height - height
|
|
height: root.height * 1.0 * root.wave(0.25)
|
|
radius: 1
|
|
color: root.tint
|
|
}
|
|
// bar 3: delay 0.1, base 40% (:58-64)
|
|
Rectangle {
|
|
x: 10
|
|
width: 3
|
|
y: root.height - height
|
|
height: root.height * 0.4 * root.wave(0.125)
|
|
radius: 1
|
|
color: root.tint
|
|
}
|
|
// bar 4: delay 0.3, base 80% (:66-72)
|
|
Rectangle {
|
|
x: 15
|
|
width: 3
|
|
y: root.height - height
|
|
height: root.height * 0.8 * root.wave(0.375)
|
|
radius: 1
|
|
color: root.tint
|
|
}
|
|
}
|