diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-18 17:59:49 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-18 17:59:49 +0100 |
| commit | 49e4e374bc51517d8119e4c82d46e3a94ea0b75b (patch) | |
| tree | e0ac136257faba9dc322978a5cd1df52012541c5 /tools/mq_editor/viewer.js | |
| parent | b9f6429b6cc741c5240c1fea8bbf4c6244e4e5d1 (diff) | |
feat(mq_editor): add explore mode for interactive partial tracking
Hover to preview a tracked partial from mouse position (peak-snapped,
forward+backward MQ tracking). Click to commit. Toggle with ⊕ Explore
button or X key. Escape exits explore mode.
handoff(Gemini): explore mode added in mq_extract.trackFromSeed + viewer.js/app.js
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'tools/mq_editor/viewer.js')
| -rw-r--r-- | tools/mq_editor/viewer.js | 74 |
1 files changed, 67 insertions, 7 deletions
diff --git a/tools/mq_editor/viewer.js b/tools/mq_editor/viewer.js index fea5cd0..8f97c3b 100644 --- a/tools/mq_editor/viewer.js +++ b/tools/mq_editor/viewer.js @@ -57,6 +57,12 @@ class SpectrogramViewer { this.onRender = null; // callback() called after each render (for synced panels) this.onBeforeChange = null; // callback() called before any mutation (for undo/redo) + // Explore mode + this.exploreMode = false; + this.previewPartial = null; + this.onExploreMove = null; // callback(time, freq) + this.onExploreCommit = null; // callback(partial) + // Setup event handlers this.setupMouseHandlers(); @@ -378,12 +384,51 @@ class SpectrogramViewer { const h = this.cursorCanvas.height; ctx.clearRect(0, 0, this.cursorCanvas.width, h); if (x < 0) return; - ctx.strokeStyle = 'rgba(255, 60, 60, 0.7)'; + ctx.strokeStyle = this.exploreMode ? 'rgba(255,160,0,0.8)' : 'rgba(255,60,60,0.7)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); + if (this.exploreMode && this.previewPartial) { + this._drawPreviewPartial(ctx, this.previewPartial); + } + } + + setExploreMode(enabled) { + this.exploreMode = enabled; + if (!enabled) this.previewPartial = null; + this.drawMouseCursor(this.mouseX); + this.canvas.style.cursor = enabled ? 'cell' : 'crosshair'; + } + + setPreviewPartial(partial) { + this.previewPartial = partial; + this.drawMouseCursor(this.mouseX); + } + + _drawPreviewPartial(ctx, partial) { + const curve = partial.freqCurve; + if (!curve) return; + ctx.save(); + ctx.strokeStyle = 'rgba(255,160,0,0.9)'; + ctx.lineWidth = 2; + ctx.setLineDash([6, 3]); + ctx.shadowColor = 'rgba(255,160,0,0.5)'; + ctx.shadowBlur = 6; + ctx.beginPath(); + let started = false; + for (let i = 0; i <= 80; ++i) { + const t = curve.t0 + (curve.t3 - curve.t0) * i / 80; + const freq = evalBezier(curve, t); + if (t < this.t_view_min || t > this.t_view_max) continue; + if (freq < this.freqStart || freq > this.freqEnd) continue; + const px = this.timeToX(t); + const py = this.freqToY(freq); + if (!started) { ctx.moveTo(px, py); started = true; } else ctx.lineTo(px, py); + } + if (started) ctx.stroke(); + ctx.restore(); } drawPlayhead() { @@ -566,6 +611,14 @@ class SpectrogramViewer { canvas.addEventListener('mousedown', (e) => { const {x, y} = getCanvasCoords(e, canvas); + // Explore mode: commit preview on click + if (this.exploreMode) { + if (this.previewPartial && this.onExploreCommit) { + this.onExploreCommit(this.previewPartial); + } + return; + } + // Check control point drag on selected partial if (this.selectedPartial >= 0 && this.selectedPartial < this.partials.length) { const ptIdx = this.hitTestControlPoint(x, y, this.partials[this.selectedPartial]); @@ -614,6 +667,11 @@ class SpectrogramViewer { const time = this.canvasToTime(x); const freq = this.canvasToFreq(y); + + if (this.exploreMode && this.onExploreMove) { + this.onExploreMove(time, freq); // may call setPreviewPartial → redraws cursor canvas + } + const intensity = this.getIntensityAt(time, freq); if (this.playheadTime < 0) { @@ -621,12 +679,14 @@ class SpectrogramViewer { this.renderSpectrum(); } - // Cursor hint for control points - if (this.selectedPartial >= 0 && this.selectedPartial < this.partials.length) { - const ptIdx = this.hitTestControlPoint(x, y, this.partials[this.selectedPartial]); - canvas.style.cursor = ptIdx >= 0 ? 'grab' : 'crosshair'; - } else { - canvas.style.cursor = 'crosshair'; + // Cursor hint for control points (skip in explore mode) + if (!this.exploreMode) { + if (this.selectedPartial >= 0 && this.selectedPartial < this.partials.length) { + const ptIdx = this.hitTestControlPoint(x, y, this.partials[this.selectedPartial]); + canvas.style.cursor = ptIdx >= 0 ? 'grab' : 'crosshair'; + } else { + canvas.style.cursor = 'crosshair'; + } } tooltip.style.left = (e.clientX + 10) + 'px'; |
