Clean release snapshot

This commit is contained in:
byrongamatos
2026-06-16 18:47:13 +02:00
commit 6c110398b4
574 changed files with 162566 additions and 0 deletions
@@ -0,0 +1,388 @@
/**
* Analogue gauge tuner visualization for the Slopsmith tuner plugin.
*
* Contract: window['_tunerViz_analogue-gauge'](container) → { update(note, cents, freq), destroy() }
* - note: string | null (null = no signal)
* - cents: number (deviation from target, 50…+50)
* - freq: number (detected frequency in Hz)
*
* Layout (vintage analogue instrument panel):
* - Off-white panel face
* - Full-width black gauge section; frequency drum window centred inside it
* - Red SVG needle sweeps over the freq drum window
* - Note name drum + lightbulb below the gauge
*/
(function () {
'use strict';
// ── Constants ─────────────────────────────────────────────────────
var _TUNER_LABEL_H = 12; // px height of each drum label
var _TUNER_NEEDLE_HALF_SWEEP = 90; // degrees — ±50 cents = horizontal (180° apart)
var _TUNER_IN_TUNE_THRESHOLD = 2;
var _TUNER_STRIP_START_MIDI = 14; // ~18 Hz — covers 20 Hz minimum
var _TUNER_STRIP_END_MIDI = 84; // ~1047 Hz C6
var _TUNER_NOTE_NAMES = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
var _TUNER_NOTE_FLAT = ['C','Db','D','Eb', 'E','F','Gb','G','Ab','A','Bb', 'B'];
// SVG gauge geometry — viewBox 200 × 110, pivot at bottom-centre
// R=95 keeps arc endpoints ~5 SVG units from the viewBox edges to prevent clipping
var _SVG_CX = 100, _SVG_CY = 110, _SVG_R = 95, _SVG_NEEDLE_LEN = 88;
window['_tunerViz_analogue-gauge'] = function (container) {
'use strict';
var svgNS = 'http://www.w3.org/2000/svg';
function _midiToFreq(m) { return Math.pow(2, (m - 69) / 12) * 440; }
// ── Panel (off-white, vintage) ────────────────────────────────
var panel = document.createElement('div');
panel.className = 'w-full relative flex flex-col items-center gap-2 p-3 rounded-lg';
panel.style.backgroundColor = '#e8e0cc';
panel.style.border = '2px solid #b0a080';
// ── AUTO lamp (top-left; lit in free-tune mode) ──────────────
var autoWrap = document.createElement('div');
autoWrap.style.position = 'absolute';
autoWrap.style.top = '8px';
autoWrap.style.left = '10px';
autoWrap.style.zIndex = '20';
autoWrap.style.display = 'flex';
autoWrap.style.alignItems = 'center';
autoWrap.style.gap = '4px';
var autoLamp = document.createElement('div');
autoLamp.style.width = '8px';
autoLamp.style.height = '8px';
autoLamp.style.backgroundColor = '#2a0000';
autoLamp.style.border = '1px solid #5a2020';
autoLamp.style.flexShrink = '0';
var autoLabel = document.createElement('span');
autoLabel.style.fontSize = '9px';
autoLabel.style.fontFamily = 'monospace';
autoLabel.style.fontWeight = 'bold';
autoLabel.style.color = '#888';
autoLabel.textContent = 'AUTO';
autoWrap.appendChild(autoLamp);
autoWrap.appendChild(autoLabel);
panel.appendChild(autoWrap);
// ── A=440 label (top-right) ───────────────────────────────────
var refLabel = document.createElement('span');
refLabel.style.position = 'absolute';
refLabel.style.top = '8px';
refLabel.style.right = '10px';
refLabel.style.zIndex = '20';
refLabel.style.fontSize = '9px';
refLabel.style.fontFamily = 'monospace';
refLabel.style.fontWeight = 'bold';
refLabel.style.color = '#888';
refLabel.textContent = 'A=440';
panel.appendChild(refLabel);
// ── Gauge section (full-width, black face) ────────────────────
var gaugeFace = document.createElement('div');
gaugeFace.className = 'w-full relative';
gaugeFace.style.backgroundColor = '#e8e0cc';
gaugeFace.style.height = '95px'; // matches cropped viewBox height (110-15)
// Frequency drum window — centred inside the gauge, behind the needle
var freqWindow = document.createElement('div');
freqWindow.style.position = 'absolute';
freqWindow.style.overflow = 'hidden';
freqWindow.style.backgroundColor = '#fff';
freqWindow.style.border = '1px solid #bbb';
freqWindow.style.width = '104px';
freqWindow.style.height = (_TUNER_LABEL_H * 2) + 'px';
freqWindow.style.left = 'calc(50% - 52px)';
freqWindow.style.top = '39px'; // half needle from pivot: 95 - 88/2 - 24/2 = 39
freqWindow.style.zIndex = '1';
// Inset shadows top & bottom → suggests a curved drum surface receding at the edges.
// Top is shorter/lighter, bottom taller/darker — reads as a drum lit from above.
freqWindow.style.boxShadow = 'inset 0 4px 5px -4px rgba(0,0,0,0.35), inset 0 -7px 7px -4px rgba(0,0,0,0.55)';
var freqStrip = document.createElement('div');
freqStrip.style.position = 'absolute';
freqStrip.style.width = '100%';
// "---" is index 0; actual notes start at index 1
function _makeDrumLabel(text) {
var el = document.createElement('div');
el.style.height = _TUNER_LABEL_H + 'px';
el.style.display = 'flex';
el.style.alignItems = 'center';
el.style.justifyContent = 'center';
el.style.userSelect = 'none';
el.textContent = text;
return el;
}
var fIdleLabel = _makeDrumLabel('---');
fIdleLabel.style.fontSize = '11px';
fIdleLabel.style.fontFamily = 'monospace';
fIdleLabel.style.fontWeight = 'bold';
fIdleLabel.style.color = '#111';
freqStrip.appendChild(fIdleLabel);
freqStrip.appendChild(_makeDrumLabel('')); // separator: keeps real labels out of view at idle
for (var fm = _TUNER_STRIP_START_MIDI; fm <= _TUNER_STRIP_END_MIDI; fm++) {
var fLabel = _makeDrumLabel(_midiToFreq(fm).toFixed(1) + ' Hz');
fLabel.style.fontSize = '11px';
fLabel.style.fontFamily = 'monospace';
fLabel.style.fontWeight = 'bold';
fLabel.style.color = '#111';
freqStrip.appendChild(fLabel);
}
freqWindow.appendChild(freqStrip);
gaugeFace.appendChild(freqWindow);
// SVG — arc, tick marks, needle, pivot (z above freq window)
var svg = document.createElementNS(svgNS, 'svg');
svg.setAttribute('viewBox', '0 15 200 95'); // crop 15px dead space above arc top
svg.setAttribute('preserveAspectRatio', 'none');
svg.style.position = 'absolute';
svg.style.top = '0';
svg.style.left = '0';
svg.style.width = '100%';
svg.style.height = '100%';
svg.style.zIndex = '2';
svg.style.overflow = 'visible'; // prevent viewBox from clipping arc edges
// Arc: R=95 keeps endpoints ~5 SVG units from the viewBox edges
var arcPath = document.createElementNS(svgNS, 'path');
arcPath.setAttribute('d', 'M 5 110 A 95 95 0 0 1 195 110');
arcPath.setAttribute('fill', 'none');
arcPath.setAttribute('stroke', '#222');
arcPath.setAttribute('stroke-width', '1.5');
svg.appendChild(arcPath);
// Tick marks: long every 10 cents, 4 short between each (every 2 cents).
// 5 outermost marks on each side (|c| >= 42) in red.
for (var tc = -50; tc <= 50; tc += 2) {
var isLong = (tc % 10 === 0);
var isRed = Math.abs(tc) >= 42;
var tLen = isLong ? 10 : 5;
var tColor = isRed ? '#cc2200' : '#222';
var tWidth = isLong ? 1.5 : 1;
var tAngleRad = ((tc / 50) * _TUNER_NEEDLE_HALF_SWEEP - 90) * Math.PI / 180;
var ttick = document.createElementNS(svgNS, 'line');
ttick.setAttribute('x1', (_SVG_CX + (_SVG_R - tLen) * Math.cos(tAngleRad)).toFixed(1));
ttick.setAttribute('y1', (_SVG_CY + (_SVG_R - tLen) * Math.sin(tAngleRad)).toFixed(1));
ttick.setAttribute('x2', (_SVG_CX + _SVG_R * Math.cos(tAngleRad)).toFixed(1));
ttick.setAttribute('y2', (_SVG_CY + _SVG_R * Math.sin(tAngleRad)).toFixed(1));
ttick.setAttribute('stroke', tColor);
ttick.setAttribute('stroke-width', String(tWidth));
svg.appendChild(ttick);
}
// Inner labels — dominant-baseline="central" so y = vertical centre of text
[
{ c: -50, text: '-50', extreme: true },
{ c: -30, text: '-30', yOff: -1 },
{ c: 0, text: '0', yOff: -1 },
{ c: 30, text: '+30', yOff: -1 },
{ c: 50, text: '+50', extreme: true }
].forEach(function (m) {
var aRad = ((m.c / 50) * _TUNER_NEEDLE_HALF_SWEEP - 90) * Math.PI / 180;
var lx = (_SVG_CX + 76 * Math.cos(aRad)).toFixed(1);
// extreme labels: centre at arc baseline (y=_SVG_CY)
// others: on arc circle at r=76 plus per-label vertical nudge
var ly = m.extreme
? String(_SVG_CY)
: (_SVG_CY + 76 * Math.sin(aRad) + (m.yOff || 0)).toFixed(1);
var lbl = document.createElementNS(svgNS, 'text');
lbl.setAttribute('x', lx);
lbl.setAttribute('y', ly);
lbl.setAttribute('text-anchor', 'middle');
lbl.setAttribute('dominant-baseline', 'central');
lbl.setAttribute('font-size', '8');
lbl.setAttribute('font-family', 'monospace');
lbl.setAttribute('fill', m.extreme ? '#cc2200' : '#555');
lbl.textContent = m.text;
svg.appendChild(lbl);
});
// Needle line (pivot at SVG bottom-centre; x2/y2 updated in RAF)
var needleLine = document.createElementNS(svgNS, 'line');
needleLine.setAttribute('x1', '100');
needleLine.setAttribute('y1', '110');
needleLine.setAttribute('x2', '100');
needleLine.setAttribute('y2', String(110 - _SVG_NEEDLE_LEN)); // initial: 0 cents
needleLine.setAttribute('stroke', '#cc2200');
needleLine.setAttribute('stroke-width', '2');
needleLine.setAttribute('stroke-linecap', 'round');
svg.appendChild(needleLine);
// Pivot cap
var pivotCap = document.createElementNS(svgNS, 'circle');
pivotCap.setAttribute('cx', '100');
pivotCap.setAttribute('cy', '110');
pivotCap.setAttribute('r', '5');
pivotCap.setAttribute('fill', '#cc2200');
svg.appendChild(pivotCap);
gaugeFace.appendChild(svg);
panel.appendChild(gaugeFace);
// ── Note drum + lightbulb row (below gauge) ───────────────────
var noteRow = document.createElement('div');
noteRow.className = 'w-full relative flex justify-center items-center';
var noteWindow = document.createElement('div');
noteWindow.style.position = 'relative';
noteWindow.style.overflow = 'hidden';
noteWindow.style.backgroundColor = '#fff';
noteWindow.style.border = '1px solid #999';
noteWindow.style.width = '48px';
noteWindow.style.height = (_TUNER_LABEL_H * 2) + 'px';
// Inset shadows top & bottom → suggests a curved drum surface receding at the edges.
// Top is shorter/lighter, bottom taller/darker — reads as a drum lit from above.
noteWindow.style.boxShadow = 'inset 0 4px 5px -4px rgba(0,0,0,0.35), inset 0 -7px 7px -4px rgba(0,0,0,0.55)';
var noteStrip = document.createElement('div');
noteStrip.style.position = 'absolute';
noteStrip.style.width = '100%';
var nIdleLabel = _makeDrumLabel('---');
nIdleLabel.style.fontSize = '10px';
nIdleLabel.style.fontWeight = 'bold';
nIdleLabel.style.color = '#111';
noteStrip.appendChild(nIdleLabel);
noteStrip.appendChild(_makeDrumLabel('')); // separator
var _drumLabels = []; // {el, nm} — for flat/sharp relabeling
for (var nm = _TUNER_STRIP_START_MIDI; nm <= _TUNER_STRIP_END_MIDI; nm++) {
var nLabel = _makeDrumLabel(_TUNER_NOTE_NAMES[nm % 12]);
nLabel.style.fontSize = '10px';
nLabel.style.fontWeight = 'bold';
nLabel.style.color = '#111';
noteStrip.appendChild(nLabel);
_drumLabels.push({ el: nLabel, nm: nm });
}
noteWindow.appendChild(noteStrip);
// Lightbulb — absolutely offset from panel centre so note window stays centred
// noteWindow is 48px wide → bulb left edge = 50% + 24px (half window) + 6px gap
var bulbEl = document.createElement('div');
bulbEl.style.position = 'absolute';
bulbEl.style.left = 'calc(50% + 30px)';
bulbEl.style.top = '50%';
bulbEl.style.transform = 'translateY(-50%)';
bulbEl.style.width = '20px';
bulbEl.style.height = '20px';
bulbEl.style.borderRadius = '50%';
bulbEl.style.backgroundColor = '#2a1010';
bulbEl.style.border = '2px solid #4a2020';
noteRow.appendChild(noteWindow);
noteRow.appendChild(bulbEl);
panel.appendChild(noteRow);
container.appendChild(panel);
// ── State ─────────────────────────────────────────────────────
// Must be defined before currentDrumY initialisation (var hoisting trap)
var _IDLE_DRUM_Y = _TUNER_LABEL_H * 0.5; // centres --- label (index 0) in window
var currentDrumY = _IDLE_DRUM_Y, targetDrumY = _IDLE_DRUM_Y;
var _lastUseFlats = false;
var currentAngle = 0, targetAngle = 0;
var lastTime = performance.now();
var rafId = null;
// ── Needle SVG update ─────────────────────────────────────────
function _setNeedle(angleDeg) {
var rad = (angleDeg - 90) * Math.PI / 180;
needleLine.setAttribute('x2', (_SVG_CX + _SVG_NEEDLE_LEN * Math.cos(rad)).toFixed(1));
needleLine.setAttribute('y2', (_SVG_CY + _SVG_NEEDLE_LEN * Math.sin(rad)).toFixed(1));
}
// ── Drum position ─────────────────────────────────────────────
function _computeDrumY(freq, cents) {
if (!freq || freq <= 0) return _IDLE_DRUM_Y;
var midi = 69 + 12 * Math.log2(freq / 440);
var targetMidi = midi - cents / 100;
var clamped = Math.max(-50, Math.min(50, cents));
// +2: index 0 = ---, index 1 = separator, index 2+ = real notes
var idx = Math.max(2, Math.min(_TUNER_STRIP_END_MIDI - _TUNER_STRIP_START_MIDI + 2, Math.round(targetMidi) - _TUNER_STRIP_START_MIDI + 2));
return _TUNER_LABEL_H * (0.5 - idx) - (clamped / 50) * (_TUNER_LABEL_H / 2);
}
// ── Animation loop ────────────────────────────────────────────
function _animate() {
var now = performance.now();
var dt = Math.min((now - lastTime) / 1000, 0.1);
lastTime = now;
var lf = 1 - Math.exp(-10 * dt);
currentDrumY += (targetDrumY - currentDrumY) * lf;
freqStrip.style.transform = 'translateY(' + currentDrumY + 'px)';
noteStrip.style.transform = 'translateY(' + currentDrumY + 'px)';
currentAngle += (targetAngle - currentAngle) * lf;
_setNeedle(currentAngle);
rafId = requestAnimationFrame(_animate);
}
rafId = requestAnimationFrame(_animate);
// ── Public API ────────────────────────────────────────────────
function _setAutoLamp(lit) {
autoLamp.style.backgroundColor = lit ? '#cc2200' : '#2a0000';
autoLamp.style.border = lit ? '1px solid #ff4422' : '1px solid #5a2020';
autoLamp.style.boxShadow = lit ? '0 0 5px 2px rgba(200,50,0,0.7)' : 'none';
}
function update(note, cents, freq, mode, targetFreq, referencePitch, useFlats) {
if (typeof referencePitch === 'number' && referencePitch > 0) {
refLabel.textContent = 'A=' + Math.round(referencePitch);
}
var wantFlats = !!useFlats;
if (wantFlats !== _lastUseFlats) {
_lastUseFlats = wantFlats;
var names = wantFlats ? _TUNER_NOTE_FLAT : _TUNER_NOTE_NAMES;
for (var _di = 0; _di < _drumLabels.length; _di++) {
_drumLabels[_di].el.textContent = names[_drumLabels[_di].nm % 12];
}
}
// AUTO lamp: free → always lit; auto → lit on signal; manual/unknown → off
if (mode === 'free') {
_setAutoLamp(true);
} else if (mode === 'auto') {
_setAutoLamp(note !== null);
} else {
_setAutoLamp(false);
}
if (note === null) {
targetDrumY = _IDLE_DRUM_Y;
targetAngle = 0;
bulbEl.style.backgroundColor = '#2a1010';
bulbEl.style.border = '2px solid #4a2020';
bulbEl.style.boxShadow = 'none';
return;
}
targetDrumY = _computeDrumY(freq, cents);
targetAngle = (Math.max(-50, Math.min(50, cents)) / 50) * _TUNER_NEEDLE_HALF_SWEEP;
if (Math.abs(cents) <= _TUNER_IN_TUNE_THRESHOLD) {
bulbEl.style.backgroundColor = '#cc3300';
bulbEl.style.border = '2px solid #ff5522';
bulbEl.style.boxShadow = '0 0 10px 4px rgba(200,50,0,0.85)';
} else {
bulbEl.style.backgroundColor = '#2a1010';
bulbEl.style.border = '2px solid #4a2020';
bulbEl.style.boxShadow = 'none';
}
}
function destroy() {
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
panel.remove();
}
return { update: update, destroy: destroy };
};
})();