[feat](trx-frontend-http): GPU-composited CSS overlay for instant freq/BW updates

Replace synchronous drawSignalOverlay() calls in freq/BW change handlers
with lightweight CSS div elements repositioned via transform: translateX().
This is GPU-composited with zero layout/paint cost, making frequency and
bandwidth changes appear instantaneous. The full WebGL overlay catches up
on the next requestAnimationFrame.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-22 08:09:36 +01:00
parent dc2c8b6eb1
commit 54df7cf0f9
3 changed files with 103 additions and 8 deletions
@@ -1668,6 +1668,71 @@ function resetWfmStereoIndicator() {
wfmStFlagEl.classList.add("wfm-st-flag-mono"); wfmStFlagEl.classList.add("wfm-st-flag-mono");
} }
// ── Fast CSS-based frequency/BW marker positioning ──────────────────────────
// These lightweight DOM elements reposition via `transform: translateX()`
// which is GPU-composited — zero layout/paint cost. The full WebGL overlay
// (drawSignalOverlay) catches up on the next rAF.
const _fastFreqMarker = document.getElementById("fast-freq-marker");
const _fastBwLeft = document.getElementById("fast-bw-left");
const _fastBwRight = document.getElementById("fast-bw-right");
function positionFastOverlay(freqHz, bwHz) {
if (!lastSpectrumData || !signalVisualBlockEl) {
if (_fastFreqMarker) _fastFreqMarker.style.display = "none";
if (_fastBwLeft) _fastBwLeft.style.display = "none";
if (_fastBwRight) _fastBwRight.style.display = "none";
return;
}
const cssW = signalVisualBlockEl.clientWidth;
if (cssW <= 0) return;
const range = spectrumVisibleRange(lastSpectrumData);
const hzToFrac = (hz) => (hz - range.visLoHz) / range.visSpanHz;
if (_fastFreqMarker && Number.isFinite(freqHz)) {
const frac = hzToFrac(freqHz);
if (frac >= 0 && frac <= 1) {
_fastFreqMarker.style.display = "";
_fastFreqMarker.style.transform = `translateX(${frac * cssW}px)`;
} else {
_fastFreqMarker.style.display = "none";
}
}
if (_fastBwLeft && _fastBwRight && Number.isFinite(freqHz) && Number.isFinite(bwHz) && bwHz > 0) {
const side = sidebandDirectionForMode(modeEl ? modeEl.value : "USB");
let loHz, hiHz;
if (side < 0) {
loHz = freqHz - bwHz; hiHz = freqHz;
} else if (side > 0) {
loHz = freqHz; hiHz = freqHz + bwHz;
} else {
loHz = freqHz - bwHz / 2; hiHz = freqHz + bwHz / 2;
}
const lFrac = hzToFrac(loHz);
const rFrac = hzToFrac(hiHz);
const cFrac = hzToFrac(freqHz);
// Left side of BW
if (lFrac < cFrac && cFrac >= 0 && lFrac <= 1) {
const x = Math.max(0, lFrac) * cssW;
const w = (Math.min(1, cFrac) - Math.max(0, lFrac)) * cssW;
_fastBwLeft.style.display = "";
_fastBwLeft.style.transform = `translateX(${x}px)`;
_fastBwLeft.style.width = `${w}px`;
} else {
_fastBwLeft.style.display = "none";
}
// Right side of BW
if (rFrac > cFrac && rFrac >= 0 && cFrac <= 1) {
const x = Math.max(0, cFrac) * cssW;
const w = (Math.min(1, rFrac) - Math.max(0, cFrac)) * cssW;
_fastBwRight.style.display = "";
_fastBwRight.style.transform = `translateX(${x}px)`;
_fastBwRight.style.width = `${w}px`;
} else {
_fastBwRight.style.display = "none";
}
}
}
function applyLocalTunedFrequency(hz, forceDisplay = false) { function applyLocalTunedFrequency(hz, forceDisplay = false) {
if (!Number.isFinite(hz)) return; if (!Number.isFinite(hz)) return;
const freqChanged = lastFreqHz !== hz; const freqChanged = lastFreqHz !== hz;
@@ -1693,11 +1758,9 @@ function applyLocalTunedFrequency(hz, forceDisplay = false) {
if (window.refreshCwTonePicker) { if (window.refreshCwTonePicker) {
window.refreshCwTonePicker(); window.refreshCwTonePicker();
} }
// Instant CSS marker repositioning (GPU-composited, no WebGL).
positionFastOverlay(lastFreqHz, currentBandwidthHz);
if (lastSpectrumData) { if (lastSpectrumData) {
// Redraw the signal/BW overlay immediately so the frequency marker and
// bandwidth picker move without waiting for the next spectrum frame or
// requestAnimationFrame callback.
drawSignalOverlay();
scheduleSpectrumDraw(); scheduleSpectrumDraw();
} }
positionRdsPsOverlay(); positionRdsPsOverlay();
@@ -3788,8 +3851,8 @@ async function applyBwDefaultForMode(mode, sendToServer) {
currentBandwidthHz = def; currentBandwidthHz = def;
window.currentBandwidthHz = currentBandwidthHz; window.currentBandwidthHz = currentBandwidthHz;
syncBandwidthInput(def); syncBandwidthInput(def);
positionFastOverlay(lastFreqHz, def);
if (lastSpectrumData) { if (lastSpectrumData) {
drawSignalOverlay();
scheduleSpectrumDraw(); scheduleSpectrumDraw();
} }
if (sendToServer) { if (sendToServer) {
@@ -3810,8 +3873,8 @@ async function applyBandwidthFromInput() {
currentBandwidthHz = clamped; currentBandwidthHz = clamped;
window.currentBandwidthHz = currentBandwidthHz; window.currentBandwidthHz = currentBandwidthHz;
syncBandwidthInput(clamped); syncBandwidthInput(clamped);
positionFastOverlay(lastFreqHz, clamped);
if (lastSpectrumData) { if (lastSpectrumData) {
drawSignalOverlay();
scheduleSpectrumDraw(); scheduleSpectrumDraw();
} }
try { try {
@@ -3888,8 +3951,8 @@ async function applyAutoBandwidth() {
currentBandwidthHz = estimated; currentBandwidthHz = estimated;
window.currentBandwidthHz = currentBandwidthHz; window.currentBandwidthHz = currentBandwidthHz;
syncBandwidthInput(estimated); syncBandwidthInput(estimated);
positionFastOverlay(lastFreqHz, estimated);
if (lastSpectrumData) { if (lastSpectrumData) {
drawSignalOverlay();
scheduleSpectrumDraw(); scheduleSpectrumDraw();
} }
try { try {
@@ -9725,7 +9788,7 @@ if (spectrumCanvas || overviewCanvas) {
currentBandwidthHz = newBw; currentBandwidthHz = newBw;
window.currentBandwidthHz = currentBandwidthHz; window.currentBandwidthHz = currentBandwidthHz;
syncBandwidthInput(newBw); syncBandwidthInput(newBw);
drawSignalOverlay(); positionFastOverlay(lastFreqHz, newBw);
scheduleSpectrumDraw(); scheduleSpectrumDraw();
scheduleOverviewDraw(); scheduleOverviewDraw();
return; return;
@@ -160,6 +160,9 @@
<span id="signal-split-value" aria-live="polite">50/50</span> <span id="signal-split-value" aria-live="polite">50/50</span>
</div> </div>
<canvas id="signal-overlay-canvas" aria-hidden="true"></canvas> <canvas id="signal-overlay-canvas" aria-hidden="true"></canvas>
<div id="fast-freq-marker" aria-hidden="true"></div>
<div id="fast-bw-left" aria-hidden="true"></div>
<div id="fast-bw-right" aria-hidden="true"></div>
</div> </div>
<div class="status"> <div class="status">
<div class="full-row freq-row"> <div class="full-row freq-row">
@@ -675,6 +675,35 @@ small { color: var(--text-muted); }
pointer-events: none; pointer-events: none;
z-index: 4; z-index: 4;
} }
#fast-freq-marker {
display: none;
position: absolute;
top: 0;
left: 0;
width: 2px;
height: 100%;
background: #ff1744;
opacity: 0.85;
pointer-events: none;
z-index: 5;
will-change: transform;
}
#fast-bw-left, #fast-bw-right {
display: none;
position: absolute;
top: 0;
left: 0;
height: 100%;
pointer-events: none;
z-index: 3;
will-change: transform, width;
}
#fast-bw-left {
background: linear-gradient(to right, transparent, rgba(255,23,68,0.10));
}
#fast-bw-right {
background: linear-gradient(to left, transparent, rgba(255,23,68,0.10));
}
#rds-ps-overlay { #rds-ps-overlay {
display: none; display: none;
position: absolute; position: absolute;