Files
qbz/crates/qbz-qt/qml/immersive/EqualizerBars.qml
T
vicrodh a2a99a72cc perf(qt): bring the immersive onto the pulse (83% -> 25% GPU)
- 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
2026-08-13 13:02:10 -06:00

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
}
}