diff options
Diffstat (limited to 'tools/spectral_editor/script.js')
| -rw-r--r-- | tools/spectral_editor/script.js | 71 |
1 files changed, 66 insertions, 5 deletions
diff --git a/tools/spectral_editor/script.js b/tools/spectral_editor/script.js index 6c6dd49..7c424f9 100644 --- a/tools/spectral_editor/script.js +++ b/tools/spectral_editor/script.js @@ -30,6 +30,8 @@ const state = { canvasHeight: 0, pixelsPerFrame: 2.0, // Zoom level (pixels per frame) pixelsPerBin: 1.0, // Vertical scale (pixels per frequency bin) + viewportOffsetX: 0, // Horizontal pan offset (pixels) + viewportOffsetY: 0, // Vertical pan offset (pixels) // Audio playback audioContext: null, @@ -94,6 +96,9 @@ function initCanvas() { // Mouse hover handlers (for crosshair) canvas.addEventListener('mousemove', onCanvasHover); canvas.addEventListener('mouseleave', onCanvasLeave); + + // Mouse wheel: zoom (with Ctrl/Cmd) or pan + canvas.addEventListener('wheel', onCanvasWheel, { passive: false }); } function initUI() { @@ -404,6 +409,9 @@ function onReferenceLoaded(fileName) { // Adjust zoom to fit state.pixelsPerFrame = Math.max(1.0, state.canvasWidth / state.referenceNumFrames); + state.pixelsPerBin = 1.0; // Reset vertical scale + state.viewportOffsetX = 0; // Reset pan + state.viewportOffsetY = 0; updateCurveUI(); updateUndoRedoButtons(); @@ -849,12 +857,62 @@ function onCanvasLeave(e) { render(); } +function onCanvasWheel(e) { + e.preventDefault(); + + const canvas = e.target; + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + // Zoom mode: Ctrl/Cmd + wheel + if (e.ctrlKey || e.metaKey) { + // Calculate frame under cursor BEFORE zoom + const frameUnderCursor = (mouseX + state.viewportOffsetX) / state.pixelsPerFrame; + + // Calculate new zoom level (horizontal only - logarithmic frequency axis doesn't zoom) + const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; // Wheel down = zoom out, wheel up = zoom in + state.pixelsPerFrame = Math.max(0.5, Math.min(20.0, state.pixelsPerFrame * zoomFactor)); + + // Adjust viewport offset so frame under cursor stays in same screen position + // After zoom: new_offset = frame * newPixelsPerFrame - mouseX + state.viewportOffsetX = frameUnderCursor * state.pixelsPerFrame - mouseX; + + // Clamp viewport offset to valid range + const maxOffsetX = Math.max(0, state.referenceNumFrames * state.pixelsPerFrame - state.canvasWidth); + state.viewportOffsetX = Math.max(0, Math.min(maxOffsetX, state.viewportOffsetX)); + + render(); + return; + } + + // Pan mode: Shift + wheel (horizontal/vertical pan) + if (e.shiftKey) { + state.viewportOffsetX += e.deltaY; + const maxOffsetX = Math.max(0, state.referenceNumFrames * state.pixelsPerFrame - state.canvasWidth); + state.viewportOffsetX = Math.max(0, Math.min(maxOffsetX, state.viewportOffsetX)); + render(); + return; + } + + // Normal mode: pan vertically (disabled for logarithmic frequency axis) + // Note: With logarithmic frequency scale, vertical pan doesn't make sense + // because the frequency range (FREQ_MIN to FREQ_MAX) is always scaled to fit canvas height. + // Vertical pan only works in linear frequency mode. + if (!USE_LOG_SCALE) { + state.viewportOffsetY += e.deltaY; + const maxOffsetY = Math.max(0, DCT_SIZE * state.pixelsPerBin - state.canvasHeight); + state.viewportOffsetY = Math.max(0, Math.min(maxOffsetY, state.viewportOffsetY)); + render(); + } +} + // ============================================================================ // Coordinate Conversion // ============================================================================ function screenToSpectrogram(screenX, screenY) { - const frame = Math.round(screenX / state.pixelsPerFrame); + const frame = Math.round((screenX + state.viewportOffsetX) / state.pixelsPerFrame); let freqHz; if (USE_LOG_SCALE) { @@ -881,7 +939,7 @@ function screenToSpectrogram(screenX, screenY) { } function spectrogramToScreen(frame, freqHz) { - const x = frame * state.pixelsPerFrame; + const x = frame * state.pixelsPerFrame - state.viewportOffsetX; let y; if (USE_LOG_SCALE) { @@ -891,11 +949,11 @@ function spectrogramToScreen(frame, freqHz) { const clampedFreq = Math.max(FREQ_MIN, Math.min(FREQ_MAX, freqHz)); const logFreq = Math.log10(clampedFreq); const normalizedY = (logFreq - logMin) / (logMax - logMin); - y = state.canvasHeight * (1.0 - normalizedY); // Flip Y back to screen coords + y = state.canvasHeight * (1.0 - normalizedY) - state.viewportOffsetY; // Flip Y back to screen coords } else { // Linear frequency mapping (old behavior) const bin = (freqHz / (SAMPLE_RATE / 2)) * state.referenceDctSize; - y = state.canvasHeight - (bin * state.pixelsPerBin); + y = state.canvasHeight - (bin * state.pixelsPerBin) - state.viewportOffsetY; } return {x, y}; @@ -943,7 +1001,10 @@ function render() { function drawPlayhead(ctx) { if (!state.isPlaying || state.playbackCurrentFrame < 0) return; - const x = state.playbackCurrentFrame * state.pixelsPerFrame; + const x = state.playbackCurrentFrame * state.pixelsPerFrame - state.viewportOffsetX; + + // Only draw if playhead is visible in viewport + if (x < 0 || x > state.canvasWidth) return; // Draw vertical line ctx.strokeStyle = '#ff3333'; // Bright red |
