summaryrefslogtreecommitdiff
path: root/tools/mq_editor/editor.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/mq_editor/editor.js')
-rw-r--r--tools/mq_editor/editor.js122
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;
}
}