From 34c7264b702856af2beb905bb97d27c1569cbb46 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 15 Dec 2025 21:53:32 +0000 Subject: [PATCH] Add zoom on scroll-wheel. Tidy up index.js --- main/index.js | 269 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 172 insertions(+), 97 deletions(-) diff --git a/main/index.js b/main/index.js index abeacd3..e367b3a 100644 --- a/main/index.js +++ b/main/index.js @@ -2,7 +2,6 @@ const canvas = document.getElementById('adcChart'); const ctx = canvas.getContext('2d'); const statusEl = document.getElementById('status'); const deltaPanel = document.getElementById('deltaPanel'); -const infoEl = document.getElementById('info'); const triggerLevel = document.getElementById('triggerLevel'); // Current active configuration for display scaling @@ -19,7 +18,6 @@ let activeConfig = { let lowRateState = { accMin: 4096, accMax: 0, - accMax: 0, progress: 0.0, targetCount: 1.0 }; @@ -28,56 +26,121 @@ let lowRateState = { // 0dB: ~950mV, 2.5dB: ~1250mV, 6dB: ~1750mV, 11dB: ~3100mV+ (use 3.3V) const ATTEN_TO_MAX_V = [0.95, 1.25, 1.75, 3.3]; -// Resize canvas +// Data buffer +let countPoints = 0; +/** @type Array */ +let dataBuffer = []; + +// Resize canvas & data function resize() { canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; + countPoints = parseInt(canvas.offsetWidth); + newBuffer = new Array(countPoints); + for (let i = 0; i < newBuffer.length; i++) + newBuffer[i] = i < dataBuffer.length ? dataBuffer[i] : 0; + dataBuffer = newBuffer; } + window.addEventListener('resize', resize); resize(); + let lastMousePosition = { x: null, y: null }; let isFrozen = false; // Track freeze state +canvas.addEventListener('click', () => isFrozen = !isFrozen); -function toggleFreeze() { - isFrozen = !isFrozen; -} +let viewTransform = { + scale: 1, + offsetX: 0, + offsetY: 0 +}; -canvas.addEventListener('click', toggleFreeze); - -let referencePosition = null; // Store the reference position for deltas - -// Helper function to calculate voltage -function calculateVoltage(offsetY) { - const maxV = ATTEN_TO_MAX_V[activeConfig.atten] || 3.3; - return ((canvas.height - offsetY) / canvas.height * maxV).toFixed(2); -} - -// Helper function to calculate effective sample rate -function getEffectiveSampleRate() { - return activeConfig.desiredRate < 1000 +// Helper to get total time (width of buffer in ms) +function getTotalTimeMs() { + const effectiveSampleRate = activeConfig.desiredRate < 1000 ? activeConfig.desiredRate / lowRateState.targetCount : activeConfig.desiredRate; + + const effectivePoints = (activeConfig.desiredRate < 1000) + ? countPoints / (lowRateState.targetCount * 2) + : countPoints; + + return (effectivePoints / effectiveSampleRate) * 1000; } -// Helper function to calculate time offset -function calculateTimeOffset(offsetX, effectiveSampleRate) { - let effectivePoints = maxPoints; - if (activeConfig.desiredRate < 1000) { - effectivePoints = maxPoints / (lowRateState.targetCount * 2); +// Helper to get max voltage +function getMaxVoltage() { + return ATTEN_TO_MAX_V[activeConfig.atten] || 3.3; +} + +// Coordinate Transforms +function XtoTime(px) { + const totalTime = getTotalTimeMs(); + // px = (t / totalTime * width) * scale + offsetX + // t = ((px - offsetX) / scale) * (totalTime / width) + if (canvas.width === 0) return 0; + return ((px - viewTransform.offsetX) / viewTransform.scale) * (totalTime / canvas.width); +} + +function TimeToX(t) { + const totalTime = getTotalTimeMs(); + if (totalTime === 0) return 0; + const xp = (t / totalTime) * canvas.width; + return xp * viewTransform.scale + viewTransform.offsetX; +} + +function YtoVolts(py) { + const maxV = getMaxVoltage(); + // sy = yp * scale + offsetY + // yp = (sy - offsetY) / scale + // yp = h * (1 - v/maxV) + // v = maxV * (1 - yp/h) + if (canvas.height === 0) return 0; + const yp = (py - viewTransform.offsetY) / viewTransform.scale; + return maxV * (1 - yp / canvas.height); +} + +function VoltsToY(v) { + const maxV = getMaxVoltage(); + const yp = canvas.height * (1 - v / maxV); + return yp * viewTransform.scale + viewTransform.offsetY; +} + +canvas.addEventListener('wheel', function (e) { + e.preventDefault(); + const zoomFactor = 1.1; + const direction = e.deltaY < 0 ? 1 : -1; + const factor = direction > 0 ? zoomFactor : 1 / zoomFactor; + + let newScale = viewTransform.scale * factor; + + if (newScale < 1.001) { + // Snap to 100% and reset position + newScale = 1.0; + viewTransform.offsetX = 0; + viewTransform.offsetY = 0; + } else if (newScale > 50) { + return; // Max limit + } else { + // Zoom centered on mouse + const mx = e.offsetX; + const my = e.offsetY; + + viewTransform.offsetX = mx - (mx - viewTransform.offsetX) * factor; + viewTransform.offsetY = my - (my - viewTransform.offsetY) * factor; } - const totalTimeMs = (effectivePoints / effectiveSampleRate) * 1000; - return ((offsetX / canvas.width) * totalTimeMs).toFixed(2); -} + viewTransform.scale = newScale; -// Helper function to reset lowRateState -function resetLowRateState() { - lowRateState.accMin = 4096; - lowRateState.accMax = 0; - lowRateState.accMax = 0; - // NOTE: We do NOT reset progress to maintain fractional phase alignment across packets -} + draw(); + // Update info if frozen to show correct values under cursor + if (isFrozen) { + updateInfo({ offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY }); + } +}); + +let referencePosition = null; // Store the reference position for deltas // Helper function to schedule WebSocket reconnection function scheduleReconnect() { @@ -109,10 +172,10 @@ function drawCrosshairs(x, y, color) { // Refactor duplicated code to use helper functions function updateInfo(event) { - const voltage = calculateVoltage(event.offsetY); - const effectiveSampleRate = getEffectiveSampleRate(); - const timeOffset = calculateTimeOffset(event.offsetX, effectiveSampleRate); - infoEl.textContent = `Voltage: ${voltage}V, Time: ${timeOffset}ms`; + // Use raw coordinates or event helpers + const voltage = YtoVolts(event.offsetY); + const timeOffset = XtoTime(event.offsetX); + let info = `
${voltage.toFixed(3)}V, ${timeOffset.toFixed(2)}ms
`; // Store the last mouse position lastMousePosition.x = event.offsetX; @@ -120,47 +183,36 @@ function updateInfo(event) { // Update delta panel position and content if frozen if (isFrozen && referencePosition) { - const maxV = ATTEN_TO_MAX_V[activeConfig.atten] || 3.3; - const deltaVoltage = (referencePosition.voltage - ((canvas.height - lastMousePosition.y) / canvas.height * maxV)).toFixed(2); - const deltaTime = (referencePosition.time - ((lastMousePosition.x / canvas.width) * maxPoints / effectiveSampleRate * 1000)).toFixed(2); + // Delta uses World Coordinates now + const deltaVoltage = (referencePosition.v - voltage); + const deltaTime = (referencePosition.t - timeOffset); - deltaPanel.style.left = `${event.pageX + 10}px`; - deltaPanel.style.top = `${event.pageY + 10}px`; - deltaPanel.style.display = 'block'; - deltaPanel.innerHTML = `
ΔVoltage: ${deltaVoltage}V
ΔTime: ${deltaTime}ms
`; - } else { - deltaPanel.style.display = 'none'; + info += `
ΔV ${deltaVoltage.toFixed(3)}V, ΔT ${deltaTime.toFixed(2)}ms
`; } + deltaPanel.style.left = `${event.pageX + 10}px`; + deltaPanel.style.top = `${event.pageY + 10}px`; + deltaPanel.innerHTML = info; + // Force redraw when frozen if (isFrozen) { draw(); } } canvas.addEventListener('mousemove', updateInfo); +canvas.addEventListener('mouseenter', () => deltaPanel.style.display = 'block'); +canvas.addEventListener('mouseleave', () => deltaPanel.style.display = 'none'); canvas.addEventListener('click', (event) => { if (isFrozen) { - const voltage = calculateVoltage(event.offsetY); - const effectiveSampleRate = getEffectiveSampleRate(); - const timeOffset = calculateTimeOffset(event.offsetX, effectiveSampleRate); - - // Set reference position for deltas + // Set reference position in World Coordinates referencePosition = { - x: event.offsetX, - y: event.offsetY, - voltage, - time: timeOffset + t: XtoTime(event.offsetX), + v: YtoVolts(event.offsetY) }; } }); -// Data buffer -const maxPoints = 4000; -/** @type Array */ -let dataBuffer = new Array(maxPoints).fill(0); - -// WebSocket // WebSocket let ws; let reconnectTimeout; @@ -194,7 +246,6 @@ function connect() { const arr = new Uint16Array(event.data); if (arr?.length) { processData(arr); - // draw(); // Removed: Drawing is now handled by animation loop } } catch (e) { console.error('Parse error:', e); @@ -263,8 +314,8 @@ function processData(/** @type Uint16Array */newData) { } function pushToBuffer(/** @type Uint16Array */ newItems) { - if (newItems.length >= maxPoints) { - dataBuffer = Array.from(newItems.slice(-maxPoints)); + if (newItems.length >= countPoints) { + dataBuffer = Array.from(newItems.slice(-countPoints)); } else { dataBuffer.splice(0, newItems.length); dataBuffer.push(...newItems); @@ -308,47 +359,47 @@ function drawGrid(w, h) { ctx.fillStyle = '#fff'; ctx.font = '15px monospace'; - // Y-Axis: Voltage (Nice Ticks) + // Determine Visible Voltage Range + const minV = YtoVolts(h); // Bottom of screen (normally 0 if unzoomed, or higher/lower if zoomed/panned) + const maxV = YtoVolts(0); // Top of screen + + // Calculate handy ticks in the visible range + const ticks = calculateNiceTicks(minV, maxV, 12); + ctx.textAlign = 'left'; - const maxV = ATTEN_TO_MAX_V[activeConfig.atten] || 3.3; - - const ticks = calculateNiceTicks(0, maxV, 6); // Aim for ~6 ticks - for (let val of ticks) { - if (val > maxV) continue; // Don't draw above max + const y = VoltsToY(val); - const y = h - (val / maxV * h); + // Skip if out of bounds (with a bit of margin) + if (y < -20 || y > h + 20) continue; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); - // Don't draw label at 0 (overlaps time) - if (val > 0.01) ctx.fillText(val.toFixed(2) + 'V', 5, y + 3); + ctx.fillText(val.toFixed(2) + 'V', 5, y + 3); } - // X-Axis: Time - // For peak detect mode, we push 2 points per 1 virtual sample. - // So the buffer effectively holds (maxPoints / 2) * timePerSample - let effectivePoints = maxPoints; - const effectiveSampleRate = getEffectiveSampleRate(); + // Determine Visible Time Range + const minT = XtoTime(0); + const maxT = XtoTime(w); - if (activeConfig.desiredRate < 1000) { - effectivePoints = maxPoints / (lowRateState.targetCount * 2); - } + // Create ticks for time + // Re-use logic or simple logic + const tTicks = calculateNiceTicks(minT, maxT, 12); - const totalTimeMs = (effectivePoints / effectiveSampleRate) * 1000; - const xSteps = 5; ctx.textAlign = 'center'; - for (let i = 0; i <= xSteps; i++) { - const x = (i / xSteps * w); - const tMs = (i / xSteps * totalTimeMs); + for (let t of tTicks) { + const x = TimeToX(t); + + if (x < -50 || x > w + 50) continue; + let timeStr; - if (tMs >= 1000) { - timeStr = (tMs / 1000).toFixed(2) + 's'; + if (Math.abs(t) >= 1000) { + timeStr = (t / 1000).toFixed(2) + 's'; } else { - timeStr = tMs.toFixed(1) + 'ms'; + timeStr = t.toFixed(1) + 'ms'; } ctx.beginPath(); @@ -384,13 +435,32 @@ function draw() { break; } } + // Optimize: only draw what's on screen? + // Simply iterating all is fine for now (< 2000 points usually) + // But we use transforms now. - let step = w / (maxPoints - 1); + // NOTE: dataBuffer index 'i' maps to x pixel in initial scale. + // sx = i * scale + offsetX + // sy : VoltsToY(val_in_volts) Or simpler: + // yp = h - (val / maxAdcVal * h) + // sy = yp * scale + offsetY + + ctx.beginPath(); + // Using loop for continuous line + // To avoid performance hit with huge offsets, we could window the loop, but let's stick to simple first drawData.forEach((val, i) => { - const x = i * step; - const y = h - (val / maxAdcVal * h); - if (i === 0) ctx.moveTo(x, y); - else ctx.lineTo(x, y); + // Original X pixel position (before zoom) was just 'i' (because buffer size ~ canvas width) + // Actually, update buffer size is 'countPoints'. + // Let's assume 'i' is the initial X coordinate. + const sx = i * viewTransform.scale + viewTransform.offsetX; + + // Normalized y calculation from original draw + // val is 0..4096. + const yp = h - (val / maxAdcVal * h); + const sy = yp * viewTransform.scale + viewTransform.offsetY; + + if (i === 0) ctx.moveTo(sx, sy); + else ctx.lineTo(sx, sy); }); ctx.stroke(); @@ -402,7 +472,10 @@ function draw() { // Draw reference crosshairs and deltas if frozen if (isFrozen && referencePosition) { - drawCrosshairs(referencePosition.x, referencePosition.y, '#eab308'); + // Convert World Reference to Screen + const refX = TimeToX(referencePosition.t); + const refY = VoltsToY(referencePosition.v); + drawCrosshairs(refX, refY, '#eab308'); } } @@ -423,7 +496,9 @@ function setParams() { body: JSON.stringify(payload) }).then(res => { if (res.ok) { - resetLowRateState(); + lowRateState.accMin = 4096; + lowRateState.accMax = 0; + lowRateState.accMax = 0; // Update active config activeConfig = { ...payload, desiredRate, trigger: parseInt(triggerLevel.value) || 2048 };