[fix](trx-frontend): unify signal markers and clamp axis labels
Render the BW and tuned-frequency markers on a shared overlay and keep spectrum axis labels bold and inside their box. Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -320,6 +320,8 @@ const ownerSubtitle = document.getElementById("owner-subtitle");
|
|||||||
const loadingTitle = document.getElementById("loading-title");
|
const loadingTitle = document.getElementById("loading-title");
|
||||||
const loadingSub = document.getElementById("loading-sub");
|
const loadingSub = document.getElementById("loading-sub");
|
||||||
const overviewCanvas = document.getElementById("overview-canvas");
|
const overviewCanvas = document.getElementById("overview-canvas");
|
||||||
|
const signalOverlayCanvas = document.getElementById("signal-overlay-canvas");
|
||||||
|
const signalVisualBlockEl = document.querySelector(".signal-visual-block");
|
||||||
const overviewPeakHoldEl = document.getElementById("overview-peak-hold");
|
const overviewPeakHoldEl = document.getElementById("overview-peak-hold");
|
||||||
const themeToggleBtn = document.getElementById("theme-toggle");
|
const themeToggleBtn = document.getElementById("theme-toggle");
|
||||||
const headerRigSwitchSelect = document.getElementById("header-rig-switch-select");
|
const headerRigSwitchSelect = document.getElementById("header-rig-switch-select");
|
||||||
@@ -690,6 +692,98 @@ function resizeHeaderSignalCanvas() {
|
|||||||
drawHeaderSignalGraph();
|
drawHeaderSignalGraph();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function signalOverlayHeight() {
|
||||||
|
if (!overviewCanvas) return 0;
|
||||||
|
let height = overviewCanvas.clientHeight || 0;
|
||||||
|
const spectrumCanvasEl = document.getElementById("spectrum-canvas");
|
||||||
|
const spectrumPanelEl = document.getElementById("spectrum-panel");
|
||||||
|
const spectrumVisible =
|
||||||
|
spectrumCanvasEl &&
|
||||||
|
spectrumCanvasEl.clientHeight > 0 &&
|
||||||
|
spectrumPanelEl &&
|
||||||
|
getComputedStyle(spectrumPanelEl).display !== "none";
|
||||||
|
if (spectrumVisible) {
|
||||||
|
height += spectrumCanvasEl.clientHeight || 0;
|
||||||
|
}
|
||||||
|
return Math.floor(height);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSignalOverlay() {
|
||||||
|
if (!signalOverlayCanvas || !signalVisualBlockEl) return;
|
||||||
|
const cssW = Math.floor(signalVisualBlockEl.clientWidth);
|
||||||
|
const cssH = signalOverlayHeight();
|
||||||
|
signalOverlayCanvas.style.height = cssH > 0 ? `${cssH}px` : "0";
|
||||||
|
if (cssW <= 0 || cssH <= 0) {
|
||||||
|
signalOverlayCanvas.width = 0;
|
||||||
|
signalOverlayCanvas.height = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const nextW = Math.floor(cssW * dpr);
|
||||||
|
const nextH = Math.floor(cssH * dpr);
|
||||||
|
if (signalOverlayCanvas.width !== nextW || signalOverlayCanvas.height !== nextH) {
|
||||||
|
signalOverlayCanvas.width = nextW;
|
||||||
|
signalOverlayCanvas.height = nextH;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = signalOverlayCanvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.scale(dpr, dpr);
|
||||||
|
ctx.clearRect(0, 0, cssW, cssH);
|
||||||
|
|
||||||
|
if (lastSpectrumData) {
|
||||||
|
const range = spectrumVisibleRange(lastSpectrumData);
|
||||||
|
const hzToX = (hz) => ((hz - range.visLoHz) / range.visSpanHz) * cssW;
|
||||||
|
|
||||||
|
if (lastFreqHz != null && currentBandwidthHz > 0) {
|
||||||
|
const halfBw = currentBandwidthHz / 2;
|
||||||
|
const xL = hzToX(lastFreqHz - halfBw);
|
||||||
|
const xR = hzToX(lastFreqHz + halfBw);
|
||||||
|
const stripW = xR - xL;
|
||||||
|
if (stripW > 1) {
|
||||||
|
const grd = ctx.createLinearGradient(xL, 0, xR, 0);
|
||||||
|
grd.addColorStop(0, "rgba(240,173,78,0.05)");
|
||||||
|
grd.addColorStop(0.2, "rgba(240,173,78,0.14)");
|
||||||
|
grd.addColorStop(0.5, "rgba(240,173,78,0.19)");
|
||||||
|
grd.addColorStop(0.8, "rgba(240,173,78,0.14)");
|
||||||
|
grd.addColorStop(1, "rgba(240,173,78,0.05)");
|
||||||
|
ctx.fillStyle = grd;
|
||||||
|
ctx.fillRect(xL, 0, stripW, cssH);
|
||||||
|
|
||||||
|
const edgeW = 5;
|
||||||
|
ctx.fillStyle = "rgba(240,173,78,0.30)";
|
||||||
|
ctx.fillRect(xL, 0, edgeW, cssH);
|
||||||
|
ctx.fillRect(xR - edgeW, 0, edgeW, cssH);
|
||||||
|
|
||||||
|
ctx.strokeStyle = "rgba(240,173,78,0.70)";
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.beginPath(); ctx.moveTo(xL, 0); ctx.lineTo(xL, cssH); ctx.stroke();
|
||||||
|
ctx.beginPath(); ctx.moveTo(xR, 0); ctx.lineTo(xR, cssH); ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastFreqHz != null) {
|
||||||
|
const xf = hzToX(lastFreqHz);
|
||||||
|
if (xf >= 0 && xf <= cssW) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.setLineDash([4, 4]);
|
||||||
|
ctx.strokeStyle = "#ff1744";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(xf, 0);
|
||||||
|
ctx.lineTo(xf, cssH);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
function scheduleOverviewDraw() {
|
function scheduleOverviewDraw() {
|
||||||
if (!overviewCanvas || overviewDrawPending) return;
|
if (!overviewCanvas || overviewDrawPending) return;
|
||||||
overviewDrawPending = true;
|
overviewDrawPending = true;
|
||||||
@@ -766,9 +860,9 @@ function drawHeaderSignalGraph() {
|
|||||||
} else {
|
} else {
|
||||||
drawOverviewSignalHistory(ctx, w, h, pal);
|
drawOverviewSignalHistory(ctx, w, h, pal);
|
||||||
}
|
}
|
||||||
drawOverviewTuningOverlay(ctx, w, h);
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
positionRdsPsOverlay();
|
positionRdsPsOverlay();
|
||||||
|
drawSignalOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _wfDrawRows(oct, rows, startRowIdx, endRowIdx, iW, iH, pal) {
|
function _wfDrawRows(oct, rows, startRowIdx, endRowIdx, iW, iH, pal) {
|
||||||
@@ -838,54 +932,6 @@ function drawOverviewWaterfall(ctx, w, h, pal) {
|
|||||||
ctx.drawImage(_wfOC, 0, 0, w, h);
|
ctx.drawImage(_wfOC, 0, 0, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawOverviewTuningOverlay(ctx, w, h) {
|
|
||||||
if (!lastSpectrumData) return;
|
|
||||||
const range = spectrumVisibleRange(lastSpectrumData);
|
|
||||||
const hzToX = (hz) => ((hz - range.visLoHz) / range.visSpanHz) * w;
|
|
||||||
|
|
||||||
if (lastFreqHz != null && currentBandwidthHz > 0) {
|
|
||||||
const halfBw = currentBandwidthHz / 2;
|
|
||||||
const xL = hzToX(lastFreqHz - halfBw);
|
|
||||||
const xR = hzToX(lastFreqHz + halfBw);
|
|
||||||
const stripW = xR - xL;
|
|
||||||
if (stripW > 1) {
|
|
||||||
const grd = ctx.createLinearGradient(xL, 0, xR, 0);
|
|
||||||
grd.addColorStop(0, "rgba(240,173,78,0.05)");
|
|
||||||
grd.addColorStop(0.2, "rgba(240,173,78,0.14)");
|
|
||||||
grd.addColorStop(0.5, "rgba(240,173,78,0.19)");
|
|
||||||
grd.addColorStop(0.8, "rgba(240,173,78,0.14)");
|
|
||||||
grd.addColorStop(1, "rgba(240,173,78,0.05)");
|
|
||||||
ctx.fillStyle = grd;
|
|
||||||
ctx.fillRect(xL, 0, stripW, h);
|
|
||||||
|
|
||||||
const edgeW = 5;
|
|
||||||
ctx.fillStyle = "rgba(240,173,78,0.30)";
|
|
||||||
ctx.fillRect(xL, 0, edgeW, h);
|
|
||||||
ctx.fillRect(xR - edgeW, 0, edgeW, h);
|
|
||||||
|
|
||||||
ctx.strokeStyle = "rgba(240,173,78,0.70)";
|
|
||||||
ctx.lineWidth = 1.5;
|
|
||||||
ctx.beginPath(); ctx.moveTo(xL, 0); ctx.lineTo(xL, h); ctx.stroke();
|
|
||||||
ctx.beginPath(); ctx.moveTo(xR, 0); ctx.lineTo(xR, h); ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastFreqHz != null) {
|
|
||||||
const xf = hzToX(lastFreqHz);
|
|
||||||
if (xf >= 0 && xf <= w) {
|
|
||||||
ctx.save();
|
|
||||||
ctx.setLineDash([4, 4]);
|
|
||||||
ctx.strokeStyle = "#ff1744";
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(xf, 0);
|
|
||||||
ctx.lineTo(xf, h);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawOverviewSignalHistory(ctx, w, h, pal) {
|
function drawOverviewSignalHistory(ctx, w, h, pal) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const samples = overviewSignalSamples.filter((sample) => now - sample.t <= HEADER_SIG_WINDOW_MS);
|
const samples = overviewSignalSamples.filter((sample) => now - sample.t <= HEADER_SIG_WINDOW_MS);
|
||||||
@@ -3753,37 +3799,9 @@ function drawSpectrum(data) {
|
|||||||
|
|
||||||
// ── BW strip (drawn before spectrum so traces appear on top) ──────────────
|
// ── BW strip (drawn before spectrum so traces appear on top) ──────────────
|
||||||
if (lastFreqHz != null && currentBandwidthHz > 0) {
|
if (lastFreqHz != null && currentBandwidthHz > 0) {
|
||||||
const halfBw = currentBandwidthHz / 2;
|
|
||||||
const xL = hzToX(lastFreqHz - halfBw);
|
|
||||||
const xR = hzToX(lastFreqHz + halfBw);
|
|
||||||
const stripW = xR - xL;
|
|
||||||
|
|
||||||
if (stripW > 1) {
|
|
||||||
// Warm amber gradient fill
|
|
||||||
const grd = ctx.createLinearGradient(xL, 0, xR, 0);
|
|
||||||
grd.addColorStop(0, "rgba(240,173,78,0.05)");
|
|
||||||
grd.addColorStop(0.2, "rgba(240,173,78,0.14)");
|
|
||||||
grd.addColorStop(0.5, "rgba(240,173,78,0.19)");
|
|
||||||
grd.addColorStop(0.8, "rgba(240,173,78,0.14)");
|
|
||||||
grd.addColorStop(1, "rgba(240,173,78,0.05)");
|
|
||||||
ctx.fillStyle = grd;
|
|
||||||
ctx.fillRect(xL, 0, stripW, H);
|
|
||||||
|
|
||||||
// Edge handle bars
|
|
||||||
const EDGE = 5 * dpr;
|
|
||||||
ctx.fillStyle = "rgba(240,173,78,0.30)";
|
|
||||||
ctx.fillRect(xL, 0, EDGE, H);
|
|
||||||
ctx.fillRect(xR - EDGE, 0, EDGE, H);
|
|
||||||
|
|
||||||
// Edge border lines
|
|
||||||
ctx.strokeStyle = "rgba(240,173,78,0.70)";
|
|
||||||
ctx.lineWidth = 1.5 * dpr;
|
|
||||||
ctx.beginPath(); ctx.moveTo(xL, 0); ctx.lineTo(xL, H); ctx.stroke();
|
|
||||||
ctx.beginPath(); ctx.moveTo(xR, 0); ctx.lineTo(xR, H); ctx.stroke();
|
|
||||||
|
|
||||||
if (_bwDragEdge) {
|
if (_bwDragEdge) {
|
||||||
// Bottom bookmark tab centered on the dial frequency, shown only while resizing BW
|
|
||||||
const xMid = hzToX(lastFreqHz);
|
const xMid = hzToX(lastFreqHz);
|
||||||
|
// Bottom bookmark tab centered on the dial frequency, shown only while resizing BW
|
||||||
const bwText = formatBwLabel(currentBandwidthHz);
|
const bwText = formatBwLabel(currentBandwidthHz);
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.font = `bold ${Math.round(10 * dpr)}px sans-serif`;
|
ctx.font = `bold ${Math.round(10 * dpr)}px sans-serif`;
|
||||||
@@ -3814,7 +3832,6 @@ function drawSpectrum(data) {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ── Spectrum fill ─────────────────────────────────────────────────────────
|
// ── Spectrum fill ─────────────────────────────────────────────────────────
|
||||||
ctx.save();
|
ctx.save();
|
||||||
@@ -3858,20 +3875,8 @@ function drawSpectrum(data) {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tuned-frequency marker ────────────────────────────────────────────────
|
|
||||||
if (lastFreqHz != null) {
|
|
||||||
const xf = hzToX(lastFreqHz);
|
|
||||||
if (xf >= 0 && xf <= W) {
|
|
||||||
ctx.save();
|
|
||||||
ctx.setLineDash([4 * dpr, 4 * dpr]);
|
|
||||||
ctx.strokeStyle = "#ff1744";
|
|
||||||
ctx.lineWidth = Math.max(1, dpr);
|
|
||||||
ctx.beginPath(); ctx.moveTo(xf, 0); ctx.lineTo(xf, H); ctx.stroke();
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSpectrumFreqAxis(range);
|
updateSpectrumFreqAxis(range);
|
||||||
|
drawSignalOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSpectrumFreqAxis(range) {
|
function updateSpectrumFreqAxis(range) {
|
||||||
@@ -3894,6 +3899,8 @@ function updateSpectrumFreqAxis(range) {
|
|||||||
|
|
||||||
const firstHz = Math.ceil(range.visLoHz / stepHz) * stepHz;
|
const firstHz = Math.ceil(range.visLoHz / stepHz) * stepHz;
|
||||||
spectrumFreqAxis.innerHTML = "";
|
spectrumFreqAxis.innerHTML = "";
|
||||||
|
const axisWidth = spectrumFreqAxis.clientWidth || 0;
|
||||||
|
const edgePad = 6;
|
||||||
for (let hz = firstHz; hz <= range.visHiHz + stepHz * 0.01; hz += stepHz) {
|
for (let hz = firstHz; hz <= range.visHiHz + stepHz * 0.01; hz += stepHz) {
|
||||||
const frac = (hz - range.visLoHz) / range.visSpanHz;
|
const frac = (hz - range.visLoHz) / range.visSpanHz;
|
||||||
if (frac < 0 || frac > 1) continue;
|
if (frac < 0 || frac > 1) continue;
|
||||||
@@ -3904,8 +3911,17 @@ function updateSpectrumFreqAxis(range) {
|
|||||||
: hz.toFixed(0);
|
: hz.toFixed(0);
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
span.textContent = label;
|
span.textContent = label;
|
||||||
span.style.left = (frac * 100).toFixed(2) + "%";
|
|
||||||
spectrumFreqAxis.appendChild(span);
|
spectrumFreqAxis.appendChild(span);
|
||||||
|
const labelWidth = span.offsetWidth || 0;
|
||||||
|
if (axisWidth > 0 && labelWidth > 0) {
|
||||||
|
const minCenter = edgePad + labelWidth / 2;
|
||||||
|
const maxCenter = axisWidth - edgePad - labelWidth / 2;
|
||||||
|
const desiredCenter = frac * axisWidth;
|
||||||
|
const clampedCenter = Math.max(minCenter, Math.min(maxCenter, desiredCenter));
|
||||||
|
span.style.left = `${clampedCenter}px`;
|
||||||
|
} else {
|
||||||
|
span.style.left = (frac * 100).toFixed(2) + "%";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,7 @@
|
|||||||
<div id="spectrum-hint" class="spectrum-hint-mouse">Scroll to zoom · Ctrl+Scroll to tune · Drag to pan · Drag BW edges to resize</div>
|
<div id="spectrum-hint" class="spectrum-hint-mouse">Scroll to zoom · Ctrl+Scroll to tune · Drag to pan · Drag BW edges to resize</div>
|
||||||
<div id="spectrum-hint-touch" class="spectrum-hint-touch">Pinch to zoom · Drag to pan · Drag BW edges to resize</div>
|
<div id="spectrum-hint-touch" class="spectrum-hint-touch">Pinch to zoom · Drag to pan · Drag BW edges to resize</div>
|
||||||
</div>
|
</div>
|
||||||
|
<canvas id="signal-overlay-canvas" aria-hidden="true"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<div class="full-row freq-row">
|
<div class="full-row freq-row">
|
||||||
|
|||||||
@@ -510,14 +510,23 @@ small { color: var(--text-muted); }
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 4;
|
|
||||||
}
|
}
|
||||||
.signal-visual-block {
|
.signal-visual-block {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
margin-bottom: 0.9rem;
|
margin-bottom: 0.9rem;
|
||||||
}
|
}
|
||||||
|
#signal-overlay-canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
#rds-ps-overlay {
|
#rds-ps-overlay {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -1041,6 +1050,7 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
|||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
#spectrum-tooltip {
|
#spectrum-tooltip {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user