summaryrefslogtreecommitdiff
path: root/tools/mq_editor/viewer.js
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-18 17:59:49 +0100
committerskal <pascal.massimino@gmail.com>2026-02-18 17:59:49 +0100
commit49e4e374bc51517d8119e4c82d46e3a94ea0b75b (patch)
treee0ac136257faba9dc322978a5cd1df52012541c5 /tools/mq_editor/viewer.js
parentb9f6429b6cc741c5240c1fea8bbf4c6244e4e5d1 (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.js74
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';