diff options
Diffstat (limited to 'tools/mq_editor/editor.js')
| -rw-r--r-- | tools/mq_editor/editor.js | 122 |
1 files changed, 53 insertions, 69 deletions
diff --git a/tools/mq_editor/editor.js b/tools/mq_editor/editor.js index 97d8a7a..a7d0879 100644 --- a/tools/mq_editor/editor.js +++ b/tools/mq_editor/editor.js @@ -20,6 +20,8 @@ class PartialEditor { // Callback: called after a partial is deleted so the host can update keepCount this.onPartialDeleted = null; + // Callback: called before any mutation (for undo/redo) + this.onBeforeChange = null; // Private state this._selectedIndex = -1; @@ -85,11 +87,11 @@ class PartialEditor { muteBtn.style.color = partial.muted ? '#fa4' : ''; this._buildCurveGrid(this._freqGrid, partial, 'freqCurve', 'f', index); - this._buildCurveGrid(this._ampGrid, partial, 'ampCurve', 'a', index); + this._buildCurveGrid(this._ampGrid, partial, 'freqCurve', 'a', index, 'a'); this._buildSynthGrid(partial, index); } - _buildCurveGrid(grid, partial, curveKey, valueLabel, partialIndex) { + _buildCurveGrid(grid, partial, curveKey, valueLabel, partialIndex, valueKey = 'v') { grid.innerHTML = ''; const curve = partial[curveKey]; if (!curve) { grid.style.color = '#444'; grid.textContent = 'none'; return; } @@ -107,10 +109,10 @@ class PartialEditor { const vInput = document.createElement('input'); vInput.type = 'number'; - vInput.value = curveKey === 'freqCurve' ? curve['v' + i].toFixed(2) : curve['v' + i].toFixed(4); - vInput.step = curveKey === 'freqCurve' ? '1' : '0.0001'; + vInput.value = valueKey === 'v' ? curve['v' + i].toFixed(2) : curve[valueKey + i].toFixed(4); + vInput.step = valueKey === 'v' ? '1' : '0.0001'; vInput.title = valueLabel + i; - vInput.addEventListener('change', this._makeCurveUpdater(partialIndex, curveKey, 'v', i)); + vInput.addEventListener('change', this._makeCurveUpdater(partialIndex, curveKey, valueKey, i)); grid.appendChild(lbl); grid.appendChild(tInput); @@ -181,7 +183,15 @@ class PartialEditor { }); sinInputs[p.key] = inp; - const jog = this._makeJogSlider(inp, partial, index, p, repDefaults); + const jog = this._makeJogSlider(inp, { + step: parseFloat(p.step), + onUpdate: (newVal) => { + if (!this.partials || !this.partials[index]) return; + if (!this.partials[index].replicas) this.partials[index].replicas = { ...repDefaults }; + this.partials[index].replicas[p.key] = newVal; + if (this.viewer) this.viewer.render(); + } + }); const wrap = document.createElement('div'); wrap.className = 'synth-field-wrap'; wrap.appendChild(inp); @@ -245,45 +255,15 @@ class PartialEditor { if (this.viewer) this.viewer.render(); }); - // Inline jog slider for resonator params - const step = parseFloat(p.step); - const sensitivity = step * 5; - const jog = document.createElement('div'); - jog.className = 'jog-slider'; - const thumb = document.createElement('div'); - thumb.className = 'jog-thumb'; - jog.appendChild(thumb); - let dragging = false, startX = 0, startVal = 0; - const onMove = (ev) => { - if (!dragging) return; - const dx = ev.clientX - startX; - const half = jog.offsetWidth / 2; - const clamped = Math.max(-half, Math.min(half, dx)); - thumb.style.transition = 'none'; - thumb.style.left = `calc(50% - 3px + ${clamped}px)`; - const newVal = Math.max(parseFloat(inp.min) || 0, - Math.min(parseFloat(inp.max) || 1e9, startVal + dx * sensitivity)); - inp.value = newVal.toFixed(4); - if (!this.partials || !this.partials[index]) return; - if (!this.partials[index].resonator) this.partials[index].resonator = { ...resDefaults }; - this.partials[index].resonator[p.key] = newVal; - if (this.viewer) this.viewer.render(); - }; - const onUp = () => { - if (!dragging) return; - dragging = false; - thumb.style.transition = ''; - thumb.style.left = 'calc(50% - 3px)'; - document.removeEventListener('mousemove', onMove); - document.removeEventListener('mouseup', onUp); - }; - jog.addEventListener('mousedown', (ev) => { - dragging = true; - startX = ev.clientX; - startVal = parseFloat(inp.value) || 0; - document.addEventListener('mousemove', onMove); - document.addEventListener('mouseup', onUp); - ev.preventDefault(); + const jog = this._makeJogSlider(inp, { + step: parseFloat(p.step), + decimals: 4, + onUpdate: (newVal) => { + if (!this.partials || !this.partials[index]) return; + if (!this.partials[index].resonator) this.partials[index].resonator = { ...resDefaults }; + this.partials[index].resonator[p.key] = newVal; + if (this.viewer) this.viewer.render(); + } }); const wrap = document.createElement('div'); @@ -322,14 +302,20 @@ class PartialEditor { }); } - _makeJogSlider(inp, partial, index, p, defaults) { + _makeJogSlider(inp, options) { + const {step, onUpdate, decimals = 3} = options; + const min = options.min != null ? options.min : + (inp.min !== '' && !isNaN(parseFloat(inp.min)) ? parseFloat(inp.min) : 0); + const max = options.max != null ? options.max : + (inp.max !== '' && !isNaN(parseFloat(inp.max)) ? parseFloat(inp.max) : Infinity); + const sensitivity = step * 5; + const slider = document.createElement('div'); slider.className = 'jog-slider'; const thumb = document.createElement('div'); thumb.className = 'jog-thumb'; slider.appendChild(thumb); - const sensitivity = parseFloat(p.step) * 5; let startX = 0, startVal = 0, dragging = false; const onMove = (e) => { @@ -339,12 +325,9 @@ class PartialEditor { const clamped = Math.max(-half, Math.min(half, dx)); thumb.style.transition = 'none'; thumb.style.left = `calc(50% - 3px + ${clamped}px)`; - const newVal = Math.max(0, startVal + dx * sensitivity); - inp.value = newVal.toFixed(3); - if (!this.partials || !this.partials[index]) return; - if (!this.partials[index].replicas) this.partials[index].replicas = { ...defaults }; - this.partials[index].replicas[p.key] = newVal; - if (this.viewer) this.viewer.render(); + const newVal = Math.max(min, Math.min(max, startVal + dx * sensitivity)); + inp.value = newVal.toFixed(decimals); + onUpdate(newVal); }; const onUp = () => { @@ -359,7 +342,7 @@ class PartialEditor { slider.addEventListener('mousedown', (e) => { dragging = true; startX = e.clientX; - startVal = Math.max(0, parseFloat(inp.value) || 0); + startVal = Math.max(min, parseFloat(inp.value) || 0); document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); e.preventDefault(); @@ -373,6 +356,7 @@ class PartialEditor { if (!this.partials) return; const val = parseFloat(e.target.value); if (isNaN(val)) return; + if (this.onBeforeChange) this.onBeforeChange(); this.partials[partialIndex][curveKey][field + pointIndex] = val; if (this.viewer) this.viewer.render(); }; @@ -381,6 +365,7 @@ class PartialEditor { _setupButtons() { document.getElementById('mutePartialBtn').addEventListener('click', () => { if (this._selectedIndex < 0 || !this.partials) return; + if (this.onBeforeChange) this.onBeforeChange(); const p = this.partials[this._selectedIndex]; p.muted = !p.muted; if (this.viewer) this.viewer.render(); @@ -389,6 +374,7 @@ class PartialEditor { document.getElementById('deletePartialBtn').addEventListener('click', () => { if (this._selectedIndex < 0 || !this.partials || !this.viewer) return; + if (this.onBeforeChange) this.onBeforeChange(); this.partials.splice(this._selectedIndex, 1); this.viewer.selectPartial(-1); if (this.onPartialDeleted) this.onPartialDeleted(); @@ -462,7 +448,7 @@ class PartialEditor { } // Bezier curve - const curve = partial.ampCurve; + const curve = partial.freqCurve; if (!curve) return; const color = this.viewer ? this.viewer.partialColor(this._selectedIndex) : '#f44'; @@ -474,7 +460,7 @@ class PartialEditor { const t = curve.t0 + (curve.t3 - curve.t0) * i / 120; const x = this._tToX(t); if (x < -1 || x > W + 1) { started = false; continue; } - const y = this._ampToY(evalBezier(curve, t)); + const y = this._ampToY(evalBezierAmp(curve, t)); if (!started) { ctx.moveTo(x, y); started = true; } else ctx.lineTo(x, y); } ctx.stroke(); @@ -482,7 +468,7 @@ class PartialEditor { // Control points for (let i = 0; i < 4; ++i) { const x = this._tToX(curve['t' + i]); - const y = this._ampToY(curve['v' + i]); + const y = this._ampToY(curve['a' + i]); ctx.fillStyle = color; ctx.beginPath(); ctx.arc(x, y, 6, 0, 2 * Math.PI); @@ -503,12 +489,12 @@ class PartialEditor { canvas.addEventListener('mousedown', (e) => { if (this._selectedIndex < 0 || !this.partials) return; const partial = this.partials[this._selectedIndex]; - if (!partial || !partial.ampCurve) return; - const rect = canvas.getBoundingClientRect(); - const x = e.clientX - rect.left, y = e.clientY - rect.top; - const curve = partial.ampCurve; + if (!partial || !partial.freqCurve) return; + const {x, y} = getCanvasCoords(e, canvas); + const curve = partial.freqCurve; for (let i = 0; i < 4; ++i) { - if (Math.hypot(this._tToX(curve['t' + i]) - x, this._ampToY(curve['v' + i]) - y) <= 8) { + if (Math.hypot(this._tToX(curve['t' + i]) - x, this._ampToY(curve['a' + i]) - y) <= 8) { + if (this.onBeforeChange) this.onBeforeChange(); this._dragPointIndex = i; canvas.style.cursor = 'grabbing'; e.preventDefault(); @@ -518,14 +504,12 @@ class PartialEditor { }); canvas.addEventListener('mousemove', (e) => { - const rect = canvas.getBoundingClientRect(); - const x = e.clientX - rect.left, y = e.clientY - rect.top; + const {x, y} = getCanvasCoords(e, canvas); if (this._dragPointIndex >= 0) { - const curve = this.partials[this._selectedIndex].ampCurve; + const curve = this.partials[this._selectedIndex].freqCurve; const i = this._dragPointIndex; - curve['t' + i] = Math.max(0, Math.min(this.viewer ? this.viewer.t_max : 1e6, this._xToT(x))); - curve['v' + i] = Math.max(0, this._yToAmp(y)); + curve['a' + i] = Math.max(0, this._yToAmp(y)); this._renderAmpEditor(); if (this.viewer) this.viewer.render(); e.preventDefault(); @@ -534,11 +518,11 @@ class PartialEditor { // Cursor hint if (this._selectedIndex >= 0 && this.partials) { - const curve = this.partials[this._selectedIndex]?.ampCurve; + const curve = this.partials[this._selectedIndex]?.freqCurve; if (curve) { let near = false; for (let i = 0; i < 4; ++i) { - if (Math.hypot(this._tToX(curve['t' + i]) - x, this._ampToY(curve['v' + i]) - y) <= 8) { + if (Math.hypot(this._tToX(curve['t' + i]) - x, this._ampToY(curve['a' + i]) - y) <= 8) { near = true; break; } } |
