|
- const spectrumCanvas = document.getElementById('spectrum');
- const waterfallCanvas = document.getElementById('waterfall');
- const statusEl = document.getElementById('status');
- const metaEl = document.getElementById('meta');
-
- let latest = null;
- let zoom = 1.0;
- let pan = 0.0;
- let isDragging = false;
- let dragStartX = 0;
- let dragStartPan = 0;
-
- function resize() {
- const dpr = window.devicePixelRatio || 1;
- const rect1 = spectrumCanvas.getBoundingClientRect();
- spectrumCanvas.width = rect1.width * dpr;
- spectrumCanvas.height = rect1.height * dpr;
- const rect2 = waterfallCanvas.getBoundingClientRect();
- waterfallCanvas.width = rect2.width * dpr;
- waterfallCanvas.height = rect2.height * dpr;
- }
-
- window.addEventListener('resize', resize);
- resize();
-
- function colorMap(v) {
- // v in [0..1]
- const r = Math.min(255, Math.max(0, Math.floor(255 * Math.pow(v, 0.6))));
- const g = Math.min(255, Math.max(0, Math.floor(255 * Math.pow(v, 1.1))));
- const b = Math.min(255, Math.max(0, Math.floor(180 * Math.pow(1 - v, 1.2))));
- return [r, g, b];
- }
-
- function renderSpectrum() {
- if (!latest) return;
- const ctx = spectrumCanvas.getContext('2d');
- const w = spectrumCanvas.width;
- const h = spectrumCanvas.height;
- ctx.clearRect(0, 0, w, h);
-
- // Grid
- ctx.strokeStyle = '#13263b';
- ctx.lineWidth = 1;
- for (let i = 1; i < 10; i++) {
- const y = (h / 10) * i;
- ctx.beginPath();
- ctx.moveTo(0, y);
- ctx.lineTo(w, y);
- ctx.stroke();
- }
-
- const { spectrum_db, sample_rate, center_hz } = latest;
- const n = spectrum_db.length;
- const span = sample_rate / zoom;
- const startHz = center_hz - span / 2 + pan * span;
- const endHz = center_hz + span / 2 + pan * span;
-
- const minDb = -120;
- const maxDb = 0;
-
- ctx.strokeStyle = '#48d1b8';
- ctx.lineWidth = 2;
- ctx.beginPath();
- for (let i = 0; i < n; i++) {
- const freq = center_hz + (i - n / 2) * (sample_rate / n);
- if (freq < startHz || freq > endHz) continue;
- const x = ((freq - startHz) / (endHz - startHz)) * w;
- const v = spectrum_db[i];
- const y = h - ((v - minDb) / (maxDb - minDb)) * h;
- if (i === 0) ctx.moveTo(x, y);
- else ctx.lineTo(x, y);
- }
- ctx.stroke();
-
- // Signals overlay
- ctx.strokeStyle = '#ffb454';
- ctx.lineWidth = 2;
- if (latest.signals) {
- for (const s of latest.signals) {
- const left = s.center_hz - s.bw_hz / 2;
- const right = s.center_hz + s.bw_hz / 2;
- if (right < startHz || left > endHz) continue;
- const x1 = ((left - startHz) / (endHz - startHz)) * w;
- const x2 = ((right - startHz) / (endHz - startHz)) * w;
- ctx.beginPath();
- ctx.moveTo(x1, h - 4);
- ctx.lineTo(x2, h - 4);
- ctx.stroke();
- }
- }
-
- metaEl.textContent = `Center ${(center_hz/1e6).toFixed(3)} MHz | Span ${(span/1e6).toFixed(3)} MHz`;
- }
-
- function renderWaterfall() {
- if (!latest) return;
- const ctx = waterfallCanvas.getContext('2d');
- const w = waterfallCanvas.width;
- const h = waterfallCanvas.height;
-
- // Scroll down
- const image = ctx.getImageData(0, 0, w, h);
- ctx.putImageData(image, 0, 1);
-
- const { spectrum_db, sample_rate, center_hz } = latest;
- const n = spectrum_db.length;
- const span = sample_rate / zoom;
- const startHz = center_hz - span / 2 + pan * span;
- const endHz = center_hz + span / 2 + pan * span;
- const minDb = -120;
- const maxDb = 0;
-
- const row = ctx.createImageData(w, 1);
- for (let x = 0; x < w; x++) {
- const freq = startHz + (x / (w - 1)) * (endHz - startHz);
- const bin = Math.floor((freq - (center_hz - sample_rate / 2)) / (sample_rate / n));
- if (bin >= 0 && bin < n) {
- const v = spectrum_db[bin];
- const norm = Math.max(0, Math.min(1, (v - minDb) / (maxDb - minDb)));
- const [r, g, b] = colorMap(norm);
- row.data[x * 4 + 0] = r;
- row.data[x * 4 + 1] = g;
- row.data[x * 4 + 2] = b;
- row.data[x * 4 + 3] = 255;
- } else {
- row.data[x * 4 + 3] = 255;
- }
- }
- ctx.putImageData(row, 0, 0);
- }
-
- function tick() {
- renderSpectrum();
- renderWaterfall();
- requestAnimationFrame(tick);
- }
-
- function connect() {
- const proto = location.protocol === 'https:' ? 'wss' : 'ws';
- const ws = new WebSocket(`${proto}://${location.host}/ws`);
- ws.onopen = () => {
- statusEl.textContent = 'Connected';
- };
- ws.onmessage = (ev) => {
- latest = JSON.parse(ev.data);
- };
- ws.onclose = () => {
- statusEl.textContent = 'Disconnected - retrying...';
- setTimeout(connect, 1000);
- };
- ws.onerror = () => {
- ws.close();
- };
- }
-
- spectrumCanvas.addEventListener('wheel', (ev) => {
- ev.preventDefault();
- const delta = Math.sign(ev.deltaY);
- zoom = Math.max(0.5, Math.min(10, zoom * (delta > 0 ? 1.1 : 0.9)));
- });
-
- spectrumCanvas.addEventListener('mousedown', (ev) => {
- isDragging = true;
- dragStartX = ev.clientX;
- dragStartPan = pan;
- });
-
- window.addEventListener('mouseup', () => { isDragging = false; });
- window.addEventListener('mousemove', (ev) => {
- if (!isDragging) return;
- const dx = ev.clientX - dragStartX;
- pan = dragStartPan - dx / spectrumCanvas.clientWidth;
- pan = Math.max(-0.5, Math.min(0.5, pan));
- });
-
- connect();
- requestAnimationFrame(tick);
|