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 };
};
})();
@@ -0,0 +1,302 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.0"
viewBox="0 0 1024 1024"
id="svg128"
width="1024"
height="1024"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs128" />
<g
id="g132"
style="display:inline"
>
<path
style="display:inline;fill:#7cb8b7;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 0,877.33008 107.64,775.73995 1024,775.85547 V 817.12 L 107.64,816.90184 0,941.53906 Z"
id="path132"
/>
<path
style="display:inline;fill:#8acfca;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 0,877.33008 107.64,775.73995 1024,775.85547 V 0 H 0 Z"
id="path133"
/>
<path
style="fill:#dbdbdb;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 0,941.53906 107.64,816.90184 1024,817.12 V 1024 H 0 Z"
id="path134"
/>
<path
style="fill:none;stroke:#407473;stroke-width:5.9;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 0,877.33008 107.64,775.73995 1024,775.85547"
id="path129"
/>
<path
style="fill:none;stroke:#407473;stroke-width:5.9;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 0,941.53906 107.64,816.90184 1024,817.12"
id="path130"
/>
<path
style="fill:none;stroke:#407473;stroke-width:5.9;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 107.64,816.90184 107.20507,0"
id="path131"
/>
</g>
<path
fill="#303332"
d="m 880.44,100.72 12.55,1.47 a 0.16,0.16 0 0 1 0,0.31 l -11.7,1.2 -130.6,-0.05 a 0.55,0.54 0 0 0 -0.55,0.54 v 54.92 a 0.52,0.52 0 0 0 0.52,0.52 h 197.01 a 0.53,0.54 0 0 0 0.53,-0.54 l 0.03,-45.02 c 1.72,-1.01 2.57,1.12 2.56,2.41 q -0.1,17.63 0.16,34.73 l -0.03,213.46 a 1.51,1.51 0 0 1 -1.51,1.51 l -194.95,0.01 -6.01,-0.06 a 1.09,1.09 0 0 1 -1.08,-1.09 V 102.03 a 1.27,1.27 0 0 1 1.27,-1.27 h 79.46 a 0.51,0.5 86.9 0 0 0.5,-0.56 c -1.27,-12.04 8.01,-24.14 20.26,-24.1 13.18,0.04 21.86,11.09 21.07,24.08 a 0.59,0.59 0 0 0 0.59,0.63 z"
id="path56" />
<path
fill="#cad5d9"
d="m 849.27,79 q 3.65,0 6.27,1.16 12.72,5.61 11.62,19.95 a 0.72,0.72 0 0 1 -0.67,0.66 l -2.63,0.15 a 0.62,0.63 1.9 0 1 -0.65,-0.7 q 1.19,-9.27 -6.39,-14.39 a 2.36,2.28 6.1 0 1 -0.83,-0.99 q -2.14,-4.88 -6.72,-4.88 -4.58,0 -6.71,4.89 a 2.28,2.36 83.8 0 1 -0.82,1 q -7.57,5.13 -6.36,14.4 a 0.63,0.62 87.9 0 1 -0.65,0.7 l -2.63,-0.15 a 0.72,0.72 0 0 1 -0.67,-0.65 q -1.13,-14.34 11.58,-19.98 2.61,-1.16 6.26,-1.17 z"
id="path57" />
<circle
fill="#cad5d9"
cx="849.33002"
cy="86.360001"
r="3.6600001"
id="circle57" />
<path
fill="#8acfca"
d="m 849.32,92.65 c 2.98,0 4.99,-1.25 6.08,-4.01 a 0.35,0.36 29.3 0 1 0.54,-0.16 q 5.47,4.14 4.59,11.41 a 1.08,1.07 2.4 0 1 -1.03,0.94 q -0.66,0.03 -10.17,0.04 -9.5,0.01 -10.17,-0.01 a 1.07,1.08 87.4 0 1 -1.03,-0.94 q -0.9,-7.27 4.56,-11.42 a 0.36,0.35 60.5 0 1 0.55,0.15 c 1.09,2.76 3.11,4.01 6.08,4 z"
id="path58" />
<path
fill="#2c4e56"
d="m 951.09,108.53 -0.14,42.68 q -0.26,-17.1 -0.16,-34.73 c 0.01,-1.29 -0.84,-3.42 -2.56,-2.41 l 0.02,-9.66 a 0.71,0.72 0 0 0 -0.71,-0.72 l -66.25,0.01 11.7,-1.2 a 0.16,0.16 0 0 0 0,-0.31 l -12.55,-1.47 69.01,0.05 a 1.62,1.62 0 0 1 1.62,1.61 z"
id="path59" />
<path
fill="#b0d5e7"
d="m 881.29,103.7 66.25,-0.01 a 0.71,0.72 0 0 1 0.71,0.72 l -0.02,9.66 -0.03,45.02 a 0.53,0.54 0 0 1 -0.53,0.54 H 750.66 a 0.52,0.52 0 0 1 -0.52,-0.52 v -54.92 a 0.55,0.54 0 0 1 0.55,-0.54 z"
id="path60" />
<path
fill="#7cb8b7"
d="m 754.46,366.19 194.95,-0.01 a 1.51,1.51 0 0 0 1.51,-1.51 l 0.03,-213.46 0.14,-42.68 4.88,0.07 a 1.25,1.25 0 0 1 1.23,1.25 v 261.69 a 1.08,1.08 0 0 1 -1.07,1.08 c -25.42,0.16 -47.71,-0.41 -71.04,-0.13 -22.61,0.26 -53.51,-0.33 -75.5,-0.15 -26.8,0.22 -41.06,0.95 -52.82,-0.59 a 1.35,1.34 86.4 0 1 -1.12,-0.99 z"
id="path61" />
<path
fill="#e8f7fc"
d="m 948.3,362.83 a 0.58,0.58 0 0 1 -0.58,0.58 H 750.74 a 0.58,0.58 0 0 1 -0.58,-0.58 V 162.89 a 0.58,0.58 0 0 1 0.58,-0.58 h 196.98 a 0.58,0.58 0 0 1 0.58,0.58 z"
id="path62" />
<path
fill="#303332"
d="m 622.02,774.15 -2.38,3.3 q -11.53,15.67 -26.1,26.61 -2.9,2.18 -8.95,5.9 -4.48,2.75 -9.05,5.74 -0.1,1.96 0.32,2.78 6.41,40.45 13.06,80.7 c 1,6.03 1.12,10.17 -2.83,14.46 -4.92,5.35 -13.13,8.68 -19.89,10.83 -23.4,7.43 -50.48,9.44 -75.17,6.74 q -20.39,-2.23 -35.43,-7.53 c -6.84,-2.41 -13.32,-5.22 -17.94,-10.58 q -3.76,-4.34 -2.81,-10.39 6.59,-42.14 13.46,-84.39 0.7,-0.48 0.32,-2.34 -3.89,-2.78 -7.82,-5.13 c -14.33,-8.62 -26.7,-20.54 -36.72,-33.81 q -0.81,-1.73 -1.53,-2.37 c -8.31,-12.93 -14.1,-24.79 -18.18,-38.76 q -0.73,-2.51 -1.83,-5.92 a 3.06,3.07 11.7 0 0 -0.9,-1.37 c -8.37,-7.39 -10.1,-15.14 -9.98,-26.63 q 0.14,-13.39 0.21,-25.87 c 0.1,-19.59 24.1,-31.14 38.65,-38.38 a 0.67,0.68 4 0 0 0.26,-0.23 l 12.23,-17.49 a 0.38,0.39 17.8 0 0 -0.31,-0.61 q -6.87,0 -20.35,0.16 c -8.7,0.1 -16.14,-1.08 -21.66,-6.72 q -5.16,-5.25 -5.65,-12.86 -1.5,-23.19 -9.77,-145 a 0.86,0.87 1.6 0 0 -0.75,-0.8 c -3.53,-0.47 -7.9,-0.45 -11.98,-1.4 -3.82,-0.89 -6.26,-4.35 -5.62,-8.09 0.76,-4.37 3.71,-6.03 8.13,-6.34 q 3.12,-0.23 8.53,-0.45 a 0.52,0.52 0 0 0 0.5,-0.55 l -1.07,-18.74 a 0.73,0.74 4.9 0 0 -0.55,-0.66 q -0.11,-0.03 -4.44,-0.04 -5.05,-0.02 -6.35,-4.47 a 3.22,3.3 35.4 0 1 -0.13,-0.84 q -0.04,-3.89 0.26,-8.86 0.98,-15.76 16.95,-15.97 38.17,-0.49 65.62,-1.06 a 1.07,1.11 20.9 0 0 0.79,-0.37 c 25.71,-29.81 63.98,-45.4 102.12,-34.09 21.22,6.29 37.54,17.95 51.73,34.08 a 1.14,1.12 69.4 0 0 0.82,0.38 q 40.39,0.89 62.15,1 8.65,0.04 11.79,1.66 c 8.82,4.53 8.93,13.98 8.63,22.86 -0.22,6.47 -6.29,5.63 -10.73,5.61 a 0.77,0.77 0 0 0 -0.77,0.8 q 0.12,2.84 -0.17,7.16 -2.02,30.7 -11.51,172.46 -1,14.96 -13.98,19.75 -4.42,1.62 -12.64,1.46 -10.85,-0.22 -21.04,0 A 0.34,0.34 0 0 0 601.3,620 l 11.37,16.77 a 4.33,4.42 85 0 0 1.92,1.58 q 12.17,5.15 21.57,12.41 c 9.33,7.2 16.08,14.46 16.1,26.23 q 0.02,12.16 0.25,23.25 c 0.23,10.62 -1.03,20.82 -9.58,27.75 a 5.19,5.16 77.4 0 0 -1.79,2.84 q -5.27,22.2 -19.12,43.32 z"
id="path83" />
<path
fill="#cad5d9"
d="m 512.21,351.41 c 28.39,0.02 54.98,14.39 73.45,35.52 q 27.41,31.37 37,72.79 c 11.84,51.2 4.91,106.12 -26.26,149.15 -3.89,5.38 -8.94,9.62 -15.67,9.54 q -2.62,-0.03 -7.39,0.12 a 0.86,0.87 80.2 0 1 -0.85,-0.59 q -1.77,-5.55 -9.24,-5.54 -25.9,0.05 -51.23,0.03 -25.33,-0.02 -51.24,-0.1 -7.47,-0.02 -9.25,5.52 a 0.87,0.86 9.9 0 1 -0.85,0.59 q -4.77,-0.16 -7.39,-0.13 c -6.73,0.07 -11.77,-4.18 -15.65,-9.56 -31.11,-43.08 -37.96,-98.01 -26.05,-149.19 q 9.65,-41.41 37.11,-72.73 c 18.5,-21.11 45.11,-35.44 73.51,-35.42 z"
id="path84" />
<path
fill="#e8f7fc"
d="m 592.82,389.8 q 5.7,-0.2 7.72,-0.15 34.39,0.86 51.54,1.03 8.42,0.07 11.14,1.62 c 6.73,3.82 6.36,11.76 6.35,19.22 q -0.01,3.56 -2.77,3.56 -12.82,0.07 -57.52,-0.09 a 0.62,0.65 75.9 0 1 -0.56,-0.32 q -6.92,-12.3 -16.18,-24.29 a 0.36,0.36 0 0 1 0.28,-0.58 z"
id="path85" />
<path
fill="#e8f7fc"
d="m 431.78,390.35 q -9.3,12.03 -16.25,24.36 a 0.65,0.63 14.1 0 1 -0.56,0.32 q -44.85,0.13 -57.71,0.06 -2.77,-0.01 -2.78,-3.58 c 0,-7.48 -0.37,-15.45 6.39,-19.28 q 2.73,-1.55 11.17,-1.62 17.21,-0.15 51.72,-0.99 2.02,-0.05 7.74,0.15 a 0.36,0.36 0 0 1 0.28,0.58 z"
id="path86" />
<path
fill="#e8f7fc"
d="m 405.22,437.95 a 0.46,0.46 0 0 1 -0.82,0.11 q -3.26,-5.17 -8.43,-6.74 c -6.7,-2.03 -11.58,0.49 -16.09,5.13 a 1.86,1.88 20.9 0 1 -1.23,0.56 l -11.01,0.63 A 0.61,0.61 0 0 1 367,437.07 l -1.07,-18.67 a 0.51,0.51 0 0 1 0.51,-0.54 h 46.99 a 0.3,0.3 0 0 1 0.26,0.45 q -5.25,9.24 -8.47,19.64 z"
id="path87" />
<path
fill="#e8f7fc"
d="m 593.59,616.55 a 0.29,0.28 27 0 1 -0.17,-0.51 q 1.68,-1.23 3.16,-2.94 4.6,-5.32 9.71,-13.8 c 18.31,-30.42 25.79,-65.67 24.42,-101.05 q -1.65,-42.75 -19.94,-79.91 a 0.35,0.36 76.7 0 1 0.32,-0.51 h 46.89 a 0.36,0.35 2.2 0 1 0.36,0.38 q -4.03,61.42 -11.7,174.44 -0.75,11.15 -2.42,14.19 -2.98,5.43 -9.07,8.21 -3.28,1.5 -12.11,1.53 -18.31,0.08 -29.45,-0.03 z"
id="path88" />
<path
fill="#cad5d9"
d="m 402.86,441.04 a 2.95,2.96 39.2 0 1 0.19,1.93 q -1.02,4.32 -2.85,10.47 -0.6,2.01 -2.84,3 -8.96,3.96 -15.35,-3.49 a 2.9,2.89 70.2 0 0 -2.16,-1.02 q -13.91,-0.29 -25.05,-1.56 c -6.22,-0.7 -7.05,-8.44 -0.22,-9.07 q 10.77,-1 25.21,-1.29 a 2.8,2.8 0 0 0 2.29,-1.27 c 5.37,-8.18 16.91,-6.04 20.78,2.3 z"
id="path89" />
<path
fill="#e8f7fc"
d="m 398.43,459.9 c -11.09,48.51 -5.11,101.36 22.19,143.75 q 5.4,8.38 10.17,12.52 a 0.24,0.24 0 0 1 -0.16,0.42 q -28.55,0.12 -33.83,-0.09 -12.1,-0.5 -16.98,-9.94 -1.73,-3.35 -2.5,-14.84 -8.61,-128.96 -9.12,-137.11 a 0.39,0.39 0 0 1 0.41,-0.42 l 10.16,0.59 a 1.8,1.75 67.7 0 1 1.1,0.48 c 0.82,0.79 2.1,2.14 3.16,2.85 q 6.85,4.58 14.9,1.37 a 0.37,0.37 0 0 1 0.5,0.42 z"
id="path90" />
<rect
fill="#4987a9"
x="119.94"
y="566.21002"
width="240.60001"
height="2.78"
rx="1.37"
id="rect90" />
<rect
fill="#4987a9"
x="666.07001"
y="566.23999"
width="345.48001"
height="2.74"
rx="1.34"
id="rect91" />
<rect
fill="#4987a9"
x="242.7493"
y="573.5766"
transform="rotate(-0.1)"
width="2.3199999"
height="45.82"
rx="1.14"
id="rect92" />
<rect
fill="#4987a9"
x="770.96826"
y="574.59918"
transform="rotate(-0.1)"
width="3.04"
height="45.860001"
rx="1.5"
id="rect93" />
<rect
fill="#4987a9"
x="904.82001"
y="573.03998"
width="2.4200001"
height="46.16"
rx="1.1900001"
id="rect94" />
<path
fill="#cad5d9"
d="m 467.38,628.28 q -0.03,0.03 -0.07,0.06 a 0.04,0.22 44.2 0 1 -0.14,0.1 l -12.2,-0.05 a 0.7,0.7 0 0 1 -0.7,-0.7 c -0.03,-3.17 -0.59,-9.32 1.44,-11.31 2.03,-1.99 8.16,-1.31 11.34,-1.22 a 0.7,0.7 0 0 1 0.68,0.72 l -0.19,12.2 a 0.04,0.22 46.9 0 1 -0.1,0.14 q -0.03,0.03 -0.06,0.06 z"
id="path94" />
<rect
fill="#cad5d9"
x="470.64999"
y="615.21002"
width="82.580002"
height="13.16"
rx="0.75999999"
id="rect95" />
<path
fill="#cad5d9"
d="m 568.98,628.3 h -12.09 a 0.59,0.58 0 0 1 -0.59,-0.58 V 615.7 a 0.36,0.37 89.2 0 1 0.36,-0.36 q 4.87,-0.06 8.65,0.05 c 3.24,0.09 4.64,2.61 4.56,5.99 q -0.11,4.66 -0.12,6.16 a 0.77,0.77 0 0 1 -0.77,0.76 z"
id="path95" />
<path
fill="#b0d5e7"
d="m 416.02,635.53 a 0.14,0.14 0 0 1 -0.16,-0.21 l 10.46,-15.29 a 0.96,0.96 0 0 1 0.66,-0.42 q 5.8,-0.75 8.06,-0.04 7.7,2.43 15.38,1.74 a 0.76,0.76 0 0 1 0.84,0.76 l -0.03,5.63 a 0.84,0.84 0 0 1 -0.88,0.83 q -9.94,-0.53 -13.56,0.32 -8.89,2.1 -20.77,6.68 z"
id="path96" />
<path
fill="#b0d5e7"
d="m 608.32,635.71 q -11.93,-4.66 -20.87,-6.81 -3.63,-0.87 -13.64,-0.38 a 0.84,0.84 0 0 1 -0.88,-0.84 v -5.66 a 0.77,0.76 2.7 0 1 0.84,-0.76 q 7.73,0.73 15.49,-1.69 2.27,-0.7 8.1,0.08 a 0.96,0.97 77.2 0 1 0.67,0.42 l 10.45,15.43 a 0.14,0.14 0 0 1 -0.16,0.21 z"
id="path97" />
<rect
fill="#4987a9"
x="162.21001"
y="623.14001"
width="205.72"
height="2.9000001"
rx="1.4299999"
id="rect97" />
<rect
fill="#4987a9"
x="680.59003"
y="623.15997"
width="292.20001"
height="2.9200001"
rx="1.4400001"
id="rect98" />
<rect
fill="#4987a9"
x="772.07001"
y="629.89001"
width="2.96"
height="60.18"
rx="1.46"
id="rect99" />
<rect
fill="#4987a9"
x="903.69696"
y="631.39026"
transform="rotate(-0.1)"
width="2.3199999"
height="60.060001"
rx="1.14"
id="rect100" />
<rect
fill="#4987a9"
x="245.01134"
y="629.40143"
transform="rotate(0.1)"
width="2.26"
height="60.099998"
rx="1.11"
id="rect101" />
<path
fill="#dbdbdb"
d="m 512.09,631.19 q 33.75,0 68.91,0.11 c 5.66,0.02 8.48,0.93 14.26,2.74 17.22,5.39 62.33,23.34 52.76,48.95 -3.26,8.71 -13.54,16.51 -21.82,20.95 -19.43,10.4 -41.29,16.23 -63.43,19.59 q -24.09,3.65 -50.69,3.65 -26.6,-0.01 -50.69,-3.66 c -22.14,-3.37 -44,-9.2 -63.43,-19.61 -8.28,-4.44 -18.56,-12.24 -21.81,-20.95 -9.57,-25.61 35.55,-43.55 52.77,-48.94 5.78,-1.81 8.6,-2.72 14.26,-2.74 q 35.16,-0.1 68.91,-0.09 z"
id="path101" />
<path
fill="#303332"
d="m 412.72,673.72 a 99.35,31.12 0 0 1 99.35,-31.12 99.35,31.12 0 0 1 99.35,31.12 99.35,31.12 0 0 1 -99.35,31.12 99.35,31.12 0 0 1 -99.35,-31.12 z"
id="path102" />
<path
fill="#b0d5e7"
d="m 512.07,645.44 q 27.67,0 53.32,4.94 c 9.84,1.89 43.24,10.11 43.24,23.37 -0.01,13.27 -33.41,21.47 -43.24,23.36 q -25.66,4.93 -53.32,4.93 -27.67,-0.01 -53.32,-4.94 c -9.84,-1.9 -43.24,-10.11 -43.24,-23.38 0.01,-13.26 33.41,-21.47 43.25,-23.36 q 25.65,-4.93 53.31,-4.92 z"
id="path103" />
<path
fill="#e8f7fc"
d="m 512.06,758.97 c -36.86,0 -71.27,-4.36 -104.51,-18.45 Q 393,734.35 382.36,725.24 c -5.64,-4.83 -7.35,-11.15 -7.68,-19.29 q -0.41,-9.84 -0.13,-18.43 a 0.31,0.3 30.7 0 1 0.57,-0.14 c 7.23,12.16 22.54,20.74 35.51,25.76 31.91,12.35 66.39,16.74 101.44,16.75 35.04,0 69.53,-4.38 101.44,-16.72 12.97,-5.02 28.28,-13.6 35.52,-25.76 a 0.3,0.31 59.3 0 1 0.57,0.14 q 0.27,8.59 -0.14,18.43 c -0.33,8.14 -2.04,14.46 -7.68,19.29 q -10.64,9.11 -25.2,15.27 c -33.24,14.09 -67.65,18.44 -104.52,18.43 z"
id="path104" />
<rect
fill="#4987a9"
x="117.78"
y="694.95001"
width="238.72"
height="2.1600001"
rx="1.0700001"
id="rect104" />
<rect
fill="#4987a9"
x="666.19"
y="694.90997"
width="332.78"
height="2.26"
rx="1.11"
id="rect105" />
<rect
fill="#4987a9"
x="243.99001"
y="701.87"
width="1.9"
height="64.720001"
rx="0.93000001"
id="rect106" />
<rect
fill="#4987a9"
x="904.91998"
y="701.76001"
width="2.2"
height="65.419998"
rx="1.08"
id="rect107" />
<path
fill="#303332"
d="m 785.23,815.55 -0.39,3.26 c -1.63333,12.71333 -3.43333,26.27 -5.4,40.67 -0.56,4.16 -4.24,5.81 -8.06,7.14 -11.28,3.91 -22.66,5.01 -35.43,5.48 -15.36,0.56667 -29.91333,-0.79667 -43.66,-4.09 -5.01,-1.2 -12.08,-3.01 -12.84,-8.49 -1.93333,-13.78 -3.83333,-27.52667 -5.7,-41.24 l -0.38,-2.25 -5.39,-39.21 c -0.46,-3.57 0.0743,0.0726 -0.46,-3.57 -2.7,-19.28 -4.52,-32.20333 -5.46,-38.77 -0.6,-4.18 1.14,-5.69 4.91,-7.74 2.94667,-1.6 7.67333,-3.04 14.18,-4.32 6.21333,-1.22 12.25,-2.10667 18.11,-2.66 24.17333,-2.28667 47.66333,-1.86667 70.47,1.26 8.44,1.16 16.84,2.7 23.88,6.84 2.47333,1.44667 3.51333,3.55667 3.12,6.33 -0.3566,1.66195 -4.93784,37.60422 -11.5,81.36 z"
id="path107"
/>
<path
fill="#a6876e"
d="m 729.37,721.24 c 14.87,0.01 28.95,0.8 42.34,3.06 3.8,0.64 22.36,4.26 22.36,8.47 0,4.21 -18.57,7.8 -22.37,8.43 -13.39,2.24 -27.48,3 -42.34,2.99 -14.87,-0.01 -28.96,-0.79 -42.34,-3.05 -3.8,-0.64 -22.37,-4.26 -22.36,-8.47 0,-4.21 18.57,-7.8 22.37,-8.44 13.39,-2.24 27.47,-3 42.34,-2.99 z"
id="path108" />
<path
fill="#e8f7fc"
d="m 512.05,761.72 c 40.54,0.01 80.55,-5.15 116.53,-23.82 q 4.84,-2.51 8.71,-5.54 a 0.21,0.21 0 0 1 0.33,0.21 q -3.48,14.17 -10.39,26.9 -27,49.78 -81.58,64.66 -15.67,4.27 -33.62,4.27 -17.95,-0.01 -33.61,-4.29 -54.58,-14.91 -81.55,-64.71 -6.9,-12.73 -10.37,-26.91 a 0.21,0.21 0 0 1 0.33,-0.21 q 3.87,3.04 8.71,5.55 c 35.96,18.69 75.97,23.88 116.51,23.89 z"
id="path109" />
<path
fill="#d1ae92"
d="m 729.35,747.04 c 17.81,0 35.43,-1.25 51.93,-4.85 q 6.12,-1.34 11.44,-4.19 a 0.22,0.22 0 0 1 0.32,0.22 q -8.25,61.43 -16.49,119.8 -0.34,2.43 -1.29,3.45 -1.2,1.3 -6.51,3.02 c -12.09,3.91 -26.05,4.73 -39.36,4.73 -13.31,0 -27.28,-0.8 -39.37,-4.71 q -5.31,-1.71 -6.51,-3.01 -0.95,-1.02 -1.29,-3.45 -8.28,-58.37 -16.56,-119.79 a 0.22,0.22 0 0 1 0.32,-0.22 q 5.32,2.84 11.45,4.18 c 16.49,3.59 34.11,4.83 51.92,4.82 z"
id="path110" />
<path
fill="#e8f7fc"
d="m 512.06,831.28 c 20.96,0 41.44,-4.81 60.17,-13.97 a 0.39,0.39 0 0 1 0.56,0.28 q 9.13,56.81 13.3,82.32 0.82,5.02 0.47,6.64 c -2.41,11.21 -25.79,17.01 -35.32,18.95 q -19.93,4.04 -39.18,4.04 -19.25,0 -39.18,-4.05 c -9.53,-1.93 -32.91,-7.73 -35.32,-18.94 q -0.35,-1.62 0.47,-6.64 4.17,-25.51 13.31,-82.32 a 0.39,0.39 0 0 1 0.55,-0.29 c 18.73,9.16 39.21,13.98 60.17,13.98 z"
id="path124" />
<path
fill="#a0a7a5"
d="m 288.52,1024 h -0.75 q 0.08,-0.76 -0.39,-0.95 a 1.46,1.5 21.1 0 1 -0.82,-1.91 q 8.4,-22.47 18.01,-47.76 a 0.55,0.55 0 0 0 -0.51,-0.75 q -93.61,-1.16 -188.79,0.19 a 2.29,2.29 0 0 0 -1.81,0.94 L 80.4,1019.6 a 1.38,1.39 37.5 0 1 -1.99,0.27 l -0.34,-0.28 a 1.14,1.13 37.5 0 1 -0.21,-1.55 l 32.38,-44.9 a 0.4,0.4 0 0 0 -0.32,-0.63 H 52.97 a 0.92,0.93 88.7 0 1 -0.93,-0.88 q -0.05,-1.26 0.67,-1.41 1.82,-0.39 2.59,-0.38 33.31,0.27 56.92,-0.16 a 0.98,0.99 17.6 0 0 0.78,-0.41 l 58.12,-80.59 a 0.51,0.52 16.6 0 0 -0.44,-0.82 q -6.93,0.4 -9.43,0.39 -56.58,-0.28 -98.96,-0.32 a 1.37,1.37 0 0 1 -1.37,-1.45 l 0.01,-0.1 a 1.58,1.58 0 0 1 1.58,-1.47 c 33.36,0.07 64.66,0.31 94.44,-0.21 q 6.97,-0.12 14.93,0.48 a 2.78,2.77 20 0 0 2.47,-1.15 l 40.68,-56.44 a 1.33,1.33 0 0 1 1.9,-0.27 l 0.06,0.04 a 1.48,1.48 0 0 1 0.29,2.02 l -39.49,55.01 a 0.36,0.36 0 0 0 0.3,0.57 h 158.9 a 0.86,0.85 10.4 0 0 0.8,-0.55 l 21.86,-57.87 a 1.87,1.85 8.3 0 1 1.62,-1.2 h 0.05 a 1.23,1.23 0 0 1 1.1,1.66 l -21.22,57.4 a 0.46,0.46 0 0 0 0.43,0.61 h 78.19 a 1.29,1.28 2.8 0 1 1.28,1.41 l -0.06,0.55 a 1.16,1.15 2.7 0 1 -1.15,1.04 H 340.9 a 1.92,1.93 10.3 0 0 -1.8,1.25 l -29.82,79.88 a 0.73,0.72 10.3 0 0 0.68,0.98 h 200.42 a 0.66,0.65 0 0 0 0.66,-0.65 v -27.39 a 0.75,0.76 0 0 1 0.75,-0.76 h 0.46 a 0.87,0.87 0 0 1 0.87,0.87 v 26.93 a 1.03,1.03 0 0 0 1.03,1.03 h 183.36 a 0.63,0.62 79.6 0 0 0.58,-0.85 l -31.05,-80.92 a 0.94,0.95 78.6 0 0 -0.91,-0.6 c -3.21,0.09 -5.82,0.42 -8.56,0.39 q -28.86,-0.24 -57.4,-0.18 a 1.42,1.42 0 0 1 -1.43,-1.5 l 0.01,-0.1 a 1.55,1.54 1.5 0 1 1.56,-1.47 q 29.9,0.36 58.94,-0.16 2.36,-0.04 5.94,0.36 a 0.45,0.45 0 0 0 0.47,-0.62 c -1.44,-3.4 -3.19,-7.04 -4.3,-10.16 q -8.1,-22.75 -17.58,-46.41 a 0.97,0.97 0 0 1 0.97,-1.33 l 0.47,0.05 a 1.95,1.96 82.4 0 1 1.63,1.24 l 21.74,56.66 a 0.88,0.87 80.2 0 0 0.78,0.56 q 4.96,0.18 7.37,0.02 10.7,-0.68 21.34,-0.43 8.4,0.2 18.84,0.2 61.72,-0.01 123.58,0.18 c 14.6,0.05 29.71,-0.57 43.98,0.11 a 0.25,0.26 72.9 0 0 0.22,-0.41 l -42.62,-56.8 a 1.37,1.37 0 0 1 0.32,-1.95 l 0.07,-0.05 a 1.44,1.44 0 0 1 1.97,0.32 l 43.65,58.09 a 1.6,1.6 0 0 0 1.28,0.64 h 109.77 a 1.54,1.54 0 0 1 1.52,1.29 l 0.08,0.51 A 1,1 0 0 1 999.75,888 H 891.54 a 0.42,0.42 0 0 0 -0.34,0.68 l 60.39,80.31 a 2.37,2.37 0 0 0 1.89,0.94 h 64.38 a 0.9,0.9 0 0 1 0.84,0.58 l 0.27,0.7 a 0.9,0.9 0 0 1 -0.84,1.22 h -63.32 a 0.33,0.33 0 0 0 -0.27,0.53 l 29.25,38.89 a 0.97,0.96 56 0 1 -0.27,1.4 l -0.37,0.23 a 1.59,1.58 55.6 0 1 -2.09,-0.4 l -30.04,-39.81 a 1.5,1.5 0 0 0 -1.2,-0.6 q -14.08,0.01 -33.04,-0.23 c -37.13,-0.47 -73.55,-0.31 -144.9,-0.33 -24.38,-0.01 -46.16,0.49 -68.75,0.55 a 0.36,0.35 79.7 0 0 -0.33,0.48 l 18.56,48.36 a 1.08,1.09 69 0 1 -0.63,1.4 l -0.62,0.24 a 1.22,1.21 68.8 0 1 -1.57,-0.7 l -18.67,-48.67 a 1.82,1.81 79.4 0 0 -1.69,-1.16 c -14.35,0.1 -27.87,-0.42 -42.29,-0.45 q -72.81,-0.14 -141.78,0.05 a 0.96,0.95 0 0 0 -0.96,0.95 v 49.01 a 0.97,0.96 86.7 0 1 -0.85,0.96 l -0.39,0.04 a 0.79,0.78 86.5 0 1 -0.88,-0.78 v -49.35 a 0.73,0.73 0 0 0 -0.69,-0.73 q -5.78,-0.3 -11.09,-0.29 -32.95,0.1 -126.89,0.13 c -18.31,0 -42.15,0.52 -63.44,0.48 a 1.44,1.44 0 0 0 -1.35,0.94 z"
id="path126" />
<path
fill="#dbdbdb"
d="m 116.67,969.67 a 0.13,0.13 0 0 1 -0.1,-0.21 l 58.24,-80.75 a 1.62,1.64 18.1 0 1 1.32,-0.68 h 160.02 a 0.49,0.49 0 0 1 0.46,0.65 c -1.79,5.21 -5.33,12.79 -7.86,19.63 q -8.33,22.49 -22.68,60.79 a 1.11,1.09 9.6 0 1 -1.02,0.71 q -91.98,0.77 -188.38,-0.14 z"
id="path127" />
<path
fill="#dbdbdb"
d="m 948.1,969.31 a 0.32,0.32 0 0 1 -0.26,0.51 c -82.94,0.37 -169.69,0.59 -245.75,-0.02 a 0.99,0.99 0 0 1 -0.91,-0.63 l -30.96,-80.7 a 0.42,0.42 0 0 1 0.32,-0.57 q 1.29,-0.23 4.21,-0.2 c 4.6,0.04 12.97,0.84 19.02,0.7 32.16,-0.71 66.97,-0.15 98.56,-0.34 q 36.78,-0.22 81.48,0.08 c 3.4,0.03 7.79,-0.25 12.19,-0.45 a 1.36,1.33 69.9 0 1 1.13,0.54 z"
id="path128" />
</svg>

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="29.814716mm"
height="69.541504mm"
viewBox="0 0 29.814716 69.541504"
version="1.1"
id="svg1"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-118.16809,-81.184232)">
<path
id="rect134"
style="fill:#ffc986;fill-opacity:1;stroke:#303332;stroke-width:0.661458;stroke-dasharray:none;stroke-opacity:1"
d="m 133.0757,81.514962 c -1.97967,-1e-4 -2.87969,1.093515 -2.77916,1.760617 l -0.009,54.876751 h 5.57641 l -0.009,-54.876751 c 0.10049,-0.666985 -0.79916,-1.760498 -2.77864,-1.760617 z"
sodipodi:nodetypes="sccccss" />
<path
id="path135"
style="fill:#ff5339;fill-opacity:1;stroke:#303332;stroke-width:0.661458;stroke-dasharray:none;stroke-opacity:1"
d="m 133.0757,137.17203 c -7.21636,-5e-5 -13.06641,5.75899 -13.06638,10.25209 -0.83439,1.3e-4 -1.51068,0.66531 -1.51051,1.4857 1.2e-4,0.82018 0.67632,1.48505 1.51051,1.48518 h 26.13225 c 0.83418,-1.3e-4 1.51039,-0.665 1.5105,-1.48518 1.8e-4,-0.82039 -0.67611,-1.48557 -1.5105,-1.4857 3e-5,-4.49298 -5.84971,-10.25197 -13.06587,-10.25209 z"
sodipodi:nodetypes="cccccccc"
inkscape:export-filename="Plunger.svg"
inkscape:export-xdpi="150"
inkscape:export-ydpi="150" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="74.313499mm"
height="68.430984mm"
viewBox="0 0 74.313499 68.430984"
version="1.1"
id="svg1"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-64.250391,-129.42155)">
<path
id="path83"
style="fill:#303332;fill-opacity:1;stroke-width:0.264583"
d="m 64.359379,129.42156 c -0.0227,0.20997 -0.05057,0.41824 -0.05168,0.63562 -0.01235,2.20133 -0.03112,4.4827 -0.05581,6.84454 -0.03175,3.04006 0.426107,5.09081 2.640666,7.04608 a 0.80962499,0.81227082 11.7 0 1 0.238228,0.36225 c 0.194028,0.60149 0.355445,1.12358 0.484208,1.56632 1.079498,3.69621 2.61136,6.83412 4.810043,10.25519 0.126998,0.11288 0.261751,0.32219 0.404626,0.62735 2.651119,3.511 5.924215,6.66448 9.715686,8.94519 0.693206,0.41451 1.382973,0.86718 2.069124,1.35754 0.06703,0.32808 0.03872,0.53441 -0.08475,0.61908 -1.21179,7.45241 -2.399139,14.89534 -3.56154,22.32835 -0.167568,1.06715 0.0804,1.98314 0.743624,2.74867 1.222373,1.41816 2.936733,2.16167 4.746481,2.79932 2.652883,0.93484 5.777542,1.59929 9.374104,1.99264 6.532551,0.71438 13.697481,0.18248 19.888711,-1.78335 1.78858,-0.56886 3.96098,-1.44994 5.26273,-2.86546 1.04511,-1.13506 1.01338,-2.2307 0.74879,-3.82613 -1.17298,-7.09963 -2.32495,-14.21675 -3.4556,-21.35166 -0.0741,-0.14464 -0.10242,-0.38964 -0.0847,-0.73536 0.80609,-0.5274 1.60447,-1.0337 2.39468,-1.51877 1.06716,-0.65617 1.85629,-1.17662 2.36782,-1.56115 2.56998,-1.92968 4.87228,-4.27639 6.90604,-7.04039 l 0.62942,-0.87333 c 2.44297,-3.72533 4.12955,-7.54601 5.05912,-11.46184 a 1.3731875,1.36525 77.4 0 1 0.47336,-0.75137 c 2.26216,-1.83357 2.59562,-4.53232 2.53472,-7.34219 -0.0407,-1.95615 -0.0628,-4.00668 -0.0661,-6.15156 -5.2e-4,-0.30242 -0.0315,-0.58459 -0.0646,-0.86558 h -11.48043 c 1e-5,0.003 0.001,0.005 0.001,0.008 -0.003,3.511 -8.83928,5.68095 -11.44013,6.18101 -4.52612,0.8696 -9.22876,1.30432 -14.10766,1.30432 -4.880682,-0.002 -9.583308,-0.43806 -14.107675,-1.30742 -2.603494,-0.50271 -11.440645,-2.67465 -11.440645,-6.18567 v -5.1e-4 z" />
<path
id="path101"
style="fill:#dfdfdf;fill-opacity:1;stroke-width:0.264583"
d="m 65.12419,129.42156 c -0.09341,0.78171 -0.0036,1.59756 0.313159,2.44533 0.859895,2.3045 3.579445,4.36806 5.770191,5.54281 5.140844,2.75431 10.925092,4.29668 16.782955,5.18831 4.2492,0.64358 8.719648,0.96642 13.411585,0.96842 4.69193,0 9.16239,-0.3215 13.41158,-0.96532 5.85787,-0.88898 11.6416,-2.432 16.78244,-5.18366 2.19075,-1.17475 4.91074,-3.23829 5.77329,-5.54281 0.31785,-0.85057 0.40752,-1.66895 0.31265,-2.45308 h -10.73527 c 1e-5,0.003 5.2e-4,0.005 5.2e-4,0.008 -0.003,3.511 -8.83928,5.68095 -11.44013,6.18101 -4.52612,0.8696 -9.22876,1.30432 -14.10766,1.30432 -4.880678,-0.002 -9.583304,-0.43806 -14.107671,-1.30742 -2.603494,-0.50271 -11.440645,-2.67465 -11.440645,-6.18567 v -5.1e-4 z" />
<path
id="path102"
style="display:inline;fill:#303332;fill-opacity:1;stroke-width:0.264583"
d="m 75.113245,129.42156 a 26.286354,8.2338332 0 0 0 0,5.1e-4 26.286354,8.2338332 0 0 0 26.286255,8.23361 26.286354,8.2338332 0 0 0 26.28625,-8.23361 26.286354,8.2338332 0 0 0 0,-5.1e-4 h -0.73898 c 1e-5,0.003 5.2e-4,0.005 5.2e-4,0.008 -0.003,3.511 -8.83928,5.68095 -11.44013,6.18101 -4.52612,0.8696 -9.22876,1.30432 -14.10766,1.30432 -4.880678,-0.002 -9.583304,-0.43806 -14.107671,-1.30742 -2.603494,-0.50271 -11.440645,-2.67465 -11.440645,-6.18567 v -5.1e-4 z" />
<path
fill="#e8f7fc"
d="m 101.39673,151.97772 c -9.752545,0 -18.856857,-1.15359 -27.651607,-4.88156 q -3.849687,-1.63248 -6.664854,-4.04284 c -1.49225,-1.27794 -1.944687,-2.9501 -2.032,-5.10381 q -0.108479,-2.6035 -0.0344,-4.87627 a 0.08202083,0.079375 30.7 0 1 0.150813,-0.037 c 1.912937,3.21733 5.963708,5.48746 9.395354,6.81566 8.442854,3.26761 17.565687,4.42913 26.839334,4.43177 9.271,0 18.39648,-1.15887 26.83934,-4.42383 3.43164,-1.32821 7.48241,-3.59833 9.398,-6.81567 a 0.079375,0.08202083 59.3 0 1 0.15081,0.037 q 0.0714,2.27277 -0.037,4.87627 c -0.0873,2.15371 -0.53975,3.82587 -2.032,5.10381 q -2.81517,2.41035 -6.6675,4.04019 c -8.79475,3.72798 -17.89907,4.87891 -27.65425,4.87627 z"
id="path104"
style="stroke-width:0.264583" />
<path
fill="#e8f7fc"
d="m 101.39408,152.70532 c 10.72621,0.003 21.31219,-1.3626 30.8319,-6.30237 q 1.28058,-0.66411 2.30452,-1.46579 a 0.0555625,0.0555625 0 0 1 0.0873,0.0556 q -0.92075,3.74914 -2.74902,7.11729 -7.14375,13.17096 -21.58471,17.10796 -4.14602,1.12977 -8.89529,1.12977 -4.749271,-0.003 -8.892646,-1.13506 -14.440958,-3.94494 -21.576771,-17.12119 -1.825625,-3.36815 -2.743729,-7.11994 a 0.0555625,0.0555625 0 0 1 0.08731,-0.0556 q 1.023937,0.80433 2.30452,1.46844 c 9.514417,4.94506 20.100396,6.31825 30.826606,6.32089 z"
id="path109"
style="stroke-width:0.264583" />
<path
fill="#e8f7fc"
d="m 101.39673,171.10974 c 5.54566,0 10.96433,-1.27265 15.91998,-3.69623 a 0.1031875,0.1031875 0 0 1 0.14816,0.0741 q 2.41565,15.03098 3.51896,21.7805 0.21696,1.32821 0.12436,1.75684 c -0.63765,2.96597 -6.82361,4.50056 -9.34509,5.01385 q -5.27314,1.06892 -10.36637,1.06892 -5.093232,0 -10.366378,-1.07157 c -2.521479,-0.51064 -8.707437,-2.04523 -9.345083,-5.0112 q -0.0926,-0.42863 0.124354,-1.75684 1.103313,-6.74952 3.521604,-21.7805 a 0.1031875,0.1031875 0 0 1 0.145521,-0.0767 c 4.955646,2.42359 10.374312,3.69888 15.919982,3.69888 z"
id="path124"
style="stroke-width:0.264583" />
<rect
style="display:none;fill:#303332;fill-opacity:1;stroke-width:1.56104"
id="rect5"
width="121.91432"
height="92.455063"
x="45.702045"
y="36.966751" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

+690
View File
@@ -0,0 +1,690 @@
/**
* CHEF MT-3 tuner visualization for the Slopsmith tuner plugin.
*
* Inspired by classic chromatic pedal tuners:
* - Shiny black rectangular panel with chamfered edges and corner screws
* - 90° curved glass gauge arc spanning between the screw inner edges
* - 51 tick marks; glow fades in/out with audio signal presence
* - Red 7-segment display (bottom centre) with "#" symbol
* - Two rubber buttons flanked by panel labels: MODE (left), BRGHT. (right)
* - Standard mode: nearest tick cluster glows for current deviation
* - Strobe mode: groups of 3 bright ticks + lightspill drift with deviation
*
* Contract: window['_tunerViz_chef-mt3'](container) → { update(note, cents, freq, mode), destroy() }
*/
(function () {
'use strict';
// ── Module-level constants ─────────────────────────────────────────
var _TUNER_MT3_IN_TUNE_THR = 2;
var _TUNER_MT3_GAUGE_CENTS = 50;
var _TUNER_MT3_TICK_COUNT = 51; // 0¢ centre + 25×2¢ per side
var _TUNER_MT3_STROBE_GROUP_COUNT = 5;
// ── Colours ────────────────────────────────────────────────────────
var _MT3_COL_BG = '#0a0a0a';
var _MT3_COL_GAUGE_ARC = 'rgba(255,255,255,0.18)';
var _MT3_COL_TICK_DIM = 'rgba(255,255,255,0.45)';
// Tick glow colours are computed per-tick at factory construction (orange→yellow gradient)
var _MT3_COL_SEG_UNLIT = '#2a0000';
// Brightness levels: [brightScale for tick glow, SVG filter flood-opacity, lit segment fill, segment drop-shadow]
var _MT3_BRIGHTNESS = [
{ brightScale: 0.66, floodOpacity: '0.45', litFill: '#cc1500', glow: 'drop-shadow(0 0 2px #bb1100)' }, // low
{ brightScale: 1.0, floodOpacity: '0.65', litFill: '#ff2200', glow: 'drop-shadow(0 0 5px #ff2200)' }, // medium
{ brightScale: 1.0, floodOpacity: '1.0', litFill: '#ff5533', glow: 'drop-shadow(0 0 9px #ff4400)' }, // high
];
var _MT3_COL_BUTTON = '#1a1a1a';
var _MT3_COL_LABEL = '#c8c8c8';
// ── Gauge SVG geometry ─────────────────────────────────────────────
// viewBox "0 0 200 66": ratio 3.03 matches SVG element (width:100% height:auto on 5:3 panel)
// 90° arc: 225°(50¢) → 270°(apex) → 315°(+50¢); R=124, cy=138 → apex at y=14
var _MT3_cx = 100;
var _MT3_cy = 138;
var _MT3_ARC_R = 124;
var _MT3_ARC_START = 5 * Math.PI / 4;
var _MT3_ARC_SPAN = Math.PI / 2;
var _MT3_ARC_SX = _MT3_cx + _MT3_ARC_R * Math.cos(_MT3_ARC_START);
var _MT3_ARC_SY = _MT3_cy + _MT3_ARC_R * Math.sin(_MT3_ARC_START);
var _MT3_ARC_EX = _MT3_cx + _MT3_ARC_R * Math.cos(_MT3_ARC_START + _MT3_ARC_SPAN);
var _MT3_ARC_EY = _MT3_cy + _MT3_ARC_R * Math.sin(_MT3_ARC_START + _MT3_ARC_SPAN);
var _SVG_NS = 'http://www.w3.org/2000/svg';
// ── 8-segment lookup table ─────────────────────────────────────────
var _TUNER_MT3_SEGMENTS = {
// a b c d e f g1 g2
'A': [ true, true, true, false, true, true, true, true ],
'B': [ false, false, true, true, true, true, true, true ],
'C': [ true, false, false, true, true, true, false, false ],
'D': [ false, true, true, true, true, false, true, true ],
'E': [ true, false, false, true, true, true, true, false ],
'F': [ true, false, false, false, true, true, true, false ],
'G': [ true, false, true, true, true, true, false, true ],
' ': [ false, false, false, false, false, false, false, false ],
};
var _segKeys = ['a', 'b', 'c', 'd', 'e', 'f', 'g1', 'g2'];
window['_tunerViz_chef-mt3'] = function (container) {
'use strict';
// ── Root panel ────────────────────────────────────────────────
var panel = document.createElement('div');
panel.style.position = 'relative';
panel.style.overflow = 'hidden';
panel.style.aspectRatio = '5 / 3';
panel.style.minHeight = '120px';
panel.style.backgroundColor = _MT3_COL_BG;
panel.style.border = '2px solid #505050';
panel.style.borderRadius = '6px';
panel.style.userSelect = 'none';
panel.style.fontFamily = 'monospace';
// ── Corner screws ─────────────────────────────────────────────
[['top','left'],['top','right'],['bottom','left'],['bottom','right']].forEach(function (pos) {
var s = document.createElement('div');
s.style.position = 'absolute';
s.style[pos[0]] = '3%';
s.style[pos[1]] = '2%';
s.style.width = '4%';
s.style.height = '0';
s.style.paddingBottom = '4%';
s.style.borderRadius = '50%';
s.style.background = 'radial-gradient(circle at 35% 35%, #666, #222)';
s.style.boxShadow = '0 1px 3px rgba(0,0,0,0.9), inset 0 1px 1px rgba(255,255,255,0.12)';
s.style.zIndex = '5';
var slot = document.createElement('div');
slot.style.position = 'absolute';
slot.style.top = '45%';
slot.style.left = '15%';
slot.style.right = '15%';
slot.style.height = '10%';
slot.style.backgroundColor = '#111';
s.appendChild(slot);
panel.appendChild(s);
});
// ── Gauge SVG ─────────────────────────────────────────────────
var gaugeSvg = document.createElementNS(_SVG_NS, 'svg');
gaugeSvg.setAttribute('viewBox', '0 0 200 66');
gaugeSvg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
gaugeSvg.style.position = 'absolute';
gaugeSvg.style.top = '5%';
gaugeSvg.style.left = '0';
gaugeSvg.style.width = '100%';
gaugeSvg.style.height = 'auto';
gaugeSvg.style.overflow = 'visible';
// SVG glow filter — applied to the glow tick group as a whole
var _glowId = 'mt3-glow-' + Math.random().toString(36).slice(2, 7);
var _svgDefs = document.createElementNS(_SVG_NS, 'defs');
var _svgFilter = document.createElementNS(_SVG_NS, 'filter');
_svgFilter.setAttribute('id', _glowId);
_svgFilter.setAttribute('x', '-60%'); _svgFilter.setAttribute('y', '-60%');
_svgFilter.setAttribute('width', '220%'); _svgFilter.setAttribute('height', '220%');
var _sfBlur = document.createElementNS(_SVG_NS, 'feGaussianBlur');
_sfBlur.setAttribute('stdDeviation', '1.8'); _sfBlur.setAttribute('result', 'blur');
var _sfFlood = document.createElementNS(_SVG_NS, 'feFlood');
_sfFlood.setAttribute('flood-color', '#ff8800'); _sfFlood.setAttribute('flood-opacity', '0.65');
_sfFlood.setAttribute('result', 'col');
var _sfComp = document.createElementNS(_SVG_NS, 'feComposite');
_sfComp.setAttribute('in', 'col'); _sfComp.setAttribute('in2', 'blur');
_sfComp.setAttribute('operator', 'in'); _sfComp.setAttribute('result', 'glow');
var _sfMerge = document.createElementNS(_SVG_NS, 'feMerge');
var _sfMn1 = document.createElementNS(_SVG_NS, 'feMergeNode'); _sfMn1.setAttribute('in', 'glow');
var _sfMn2 = document.createElementNS(_SVG_NS, 'feMergeNode'); _sfMn2.setAttribute('in', 'SourceGraphic');
_sfMerge.appendChild(_sfMn1); _sfMerge.appendChild(_sfMn2);
_svgFilter.appendChild(_sfBlur); _svgFilter.appendChild(_sfFlood);
_svgFilter.appendChild(_sfComp); _svgFilter.appendChild(_sfMerge);
// Shared blur filter for highlight and shadow arcs
var _glassBlurId = 'mt3-gb-' + Math.random().toString(36).slice(2, 7);
var _glassBlur = document.createElementNS(_SVG_NS, 'filter');
_glassBlur.setAttribute('id', _glassBlurId);
_glassBlur.setAttribute('x', '-30%'); _glassBlur.setAttribute('y', '-30%');
_glassBlur.setAttribute('width', '160%'); _glassBlur.setAttribute('height', '160%');
var _gbFe = document.createElementNS(_SVG_NS, 'feGaussianBlur');
_gbFe.setAttribute('stdDeviation', '1.8');
_glassBlur.appendChild(_gbFe);
_svgDefs.appendChild(_glassBlur);
// Gradient for specular highlight: transparent at arc ends, white at centre
var _hlGradId = 'mt3-hl-' + Math.random().toString(36).slice(2, 7);
var _hlGrad = document.createElementNS(_SVG_NS, 'linearGradient');
_hlGrad.setAttribute('id', _hlGradId);
_hlGrad.setAttribute('gradientUnits', 'userSpaceOnUse');
_hlGrad.setAttribute('x1', _MT3_ARC_SX.toFixed(2)); _hlGrad.setAttribute('y1', '0');
_hlGrad.setAttribute('x2', _MT3_ARC_EX.toFixed(2)); _hlGrad.setAttribute('y2', '0');
[['0%','rgba(255,255,255,0.65)'],['4%','rgba(255,255,255,0)'],['20%','rgba(255,255,255,0)'],['35%','rgba(255,255,255,0.68)'],['65%','rgba(255,255,255,0.68)'],['80%','rgba(255,255,255,0)'],['96%','rgba(255,255,255,0)'],['100%','rgba(255,255,255,0.65)']]
.forEach(function(s){var st=document.createElementNS(_SVG_NS,'stop');st.setAttribute('offset',s[0]);st.setAttribute('stop-color',s[1]);_hlGrad.appendChild(st);});
_svgDefs.appendChild(_hlGrad);
// Gradient for outer shadow: transparent at arc ends, dark at centre
var _shGradId = 'mt3-sh-' + Math.random().toString(36).slice(2, 7);
var _shGrad = document.createElementNS(_SVG_NS, 'linearGradient');
_shGrad.setAttribute('id', _shGradId);
_shGrad.setAttribute('gradientUnits', 'userSpaceOnUse');
_shGrad.setAttribute('x1', _MT3_ARC_SX.toFixed(2)); _shGrad.setAttribute('y1', '0');
_shGrad.setAttribute('x2', _MT3_ARC_EX.toFixed(2)); _shGrad.setAttribute('y2', '0');
[['0%','rgba(0,0,0,0.65)'],['4%','rgba(0,0,0,0)'],['20%','rgba(0,0,0,0.68)'],['80%','rgba(0,0,0,0.68)'],['96%','rgba(0,0,0,0)'],['100%','rgba(0,0,0,0.65)']]
.forEach(function(s){var st=document.createElementNS(_SVG_NS,'stop');st.setAttribute('offset',s[0]);st.setAttribute('stop-color',s[1]);_shGrad.appendChild(st);});
_svgDefs.appendChild(_shGrad);
_svgDefs.appendChild(_svgFilter);
gaugeSvg.appendChild(_svgDefs);
var _arcD = 'M ' + _MT3_ARC_SX.toFixed(2) + ' ' + _MT3_ARC_SY.toFixed(2) +
' A ' + _MT3_ARC_R + ' ' + _MT3_ARC_R + ' 0 0 1 ' +
_MT3_ARC_EX.toFixed(2) + ' ' + _MT3_ARC_EY.toFixed(2);
// Glass arc body
var arcBody = document.createElementNS(_SVG_NS, 'path');
arcBody.setAttribute('d', _arcD);
arcBody.setAttribute('fill', 'none');
arcBody.setAttribute('stroke', _MT3_COL_GAUGE_ARC);
arcBody.setAttribute('stroke-width', '16');
arcBody.setAttribute('stroke-linecap', 'round');
gaugeSvg.appendChild(arcBody);
// Inner shadow — dark stroke on the inner-lower edge, simulates less light reaching far side
var _shadowR = _MT3_ARC_R - 6;
// Align shadow gradient vector to shadow arc's own endpoints (not the main arc's)
_shGrad.setAttribute('x1', (_MT3_cx + _shadowR * Math.cos(_MT3_ARC_START)).toFixed(2));
_shGrad.setAttribute('x2', (_MT3_cx + _shadowR * Math.cos(_MT3_ARC_START + _MT3_ARC_SPAN)).toFixed(2));
var arcShadow = document.createElementNS(_SVG_NS, 'path');
arcShadow.setAttribute('d',
'M ' + (_MT3_cx + _shadowR * Math.cos(_MT3_ARC_START)).toFixed(2) + ' ' +
(_MT3_cy + _shadowR * Math.sin(_MT3_ARC_START)).toFixed(2) +
' A ' + _shadowR + ' ' + _shadowR + ' 0 0 1 ' +
(_MT3_cx + _shadowR * Math.cos(_MT3_ARC_START + _MT3_ARC_SPAN)).toFixed(2) + ' ' +
(_MT3_cy + _shadowR * Math.sin(_MT3_ARC_START + _MT3_ARC_SPAN)).toFixed(2));
arcShadow.setAttribute('fill', 'none');
arcShadow.setAttribute('stroke', 'url(#' + _shGradId + ')');
arcShadow.setAttribute('stroke-width', '3.5');
arcShadow.setAttribute('stroke-linecap', 'round');
arcShadow.setAttribute('filter', 'url(#' + _glassBlurId + ')');
// Specular highlight — bright white arc fading to transparent at ends
var _hlR = _MT3_ARC_R + 2;
var arcHighlight = document.createElementNS(_SVG_NS, 'path');
arcHighlight.setAttribute('d',
'M ' + (_MT3_cx + _hlR * Math.cos(_MT3_ARC_START)).toFixed(2) + ' ' +
(_MT3_cy + _hlR * Math.sin(_MT3_ARC_START)).toFixed(2) +
' A ' + _hlR + ' ' + _hlR + ' 0 0 1 ' +
(_MT3_cx + _hlR * Math.cos(_MT3_ARC_START + _MT3_ARC_SPAN)).toFixed(2) + ' ' +
(_MT3_cy + _hlR * Math.sin(_MT3_ARC_START + _MT3_ARC_SPAN)).toFixed(2));
arcHighlight.setAttribute('fill', 'none');
arcHighlight.setAttribute('stroke', 'url(#' + _hlGradId + ')');
arcHighlight.setAttribute('stroke-width', '2.5');
arcHighlight.setAttribute('stroke-linecap', 'round');
arcHighlight.setAttribute('filter', 'url(#' + _glassBlurId + ')');
// dimGroup: base tick lines always shown at dim colour — constructed once, never updated
var _dimGroup = document.createElementNS(_SVG_NS, 'g');
// glowGroup: lit tick overlay with shared glow filter, opacity animated 0→1
var _glowGroup = document.createElementNS(_SVG_NS, 'g');
_glowGroup.setAttribute('filter', 'url(#' + _glowId + ')');
_glowGroup.setAttribute('opacity', '0');
// Z-order: arcBody → dimGroup (unlit ticks) → arcShadow → glowGroup (lit ticks) → arcHighlight
gaugeSvg.appendChild(_dimGroup);
gaugeSvg.appendChild(arcShadow);
gaugeSvg.appendChild(_glowGroup);
var _mt3GlowTickEls = [];
for (var i = 0; i < _TUNER_MT3_TICK_COUNT; i++) {
var isMajor = (i % 5 === 0);
var halfLen = isMajor ? 5 : 3;
var a = _MT3_ARC_START + (_MT3_ARC_SPAN / (_TUNER_MT3_TICK_COUNT - 1)) * i;
var cosA = Math.cos(a), sinA = Math.sin(a);
var x1 = _MT3_cx + (_MT3_ARC_R - halfLen) * cosA;
var y1 = _MT3_cy + (_MT3_ARC_R - halfLen) * sinA;
var x2 = _MT3_cx + (_MT3_ARC_R + halfLen) * cosA;
var y2 = _MT3_cy + (_MT3_ARC_R + halfLen) * sinA;
var dimTick = document.createElementNS(_SVG_NS, 'line');
dimTick.setAttribute('x1', String(x1)); dimTick.setAttribute('y1', String(y1));
dimTick.setAttribute('x2', String(x2)); dimTick.setAttribute('y2', String(y2));
dimTick.setAttribute('stroke', _MT3_COL_TICK_DIM);
dimTick.setAttribute('stroke-width', '1');
dimTick.setAttribute('stroke-linecap', 'round');
_dimGroup.appendChild(dimTick);
var glowTick = document.createElementNS(_SVG_NS, 'line');
glowTick.setAttribute('x1', String(x1)); glowTick.setAttribute('y1', String(y1));
glowTick.setAttribute('x2', String(x2)); glowTick.setAttribute('y2', String(y2));
glowTick.setAttribute('stroke', 'none');
glowTick.setAttribute('stroke-width', '1');
glowTick.setAttribute('stroke-linecap', 'round');
_glowGroup.appendChild(glowTick);
_mt3GlowTickEls.push(glowTick);
}
// Per-tick gradient colours: orange (#ff7700) at arc edges → yellow (#ffee00) at centre
// d = distance from centre (0 = centre tick, 1 = end ticks)
var _mt3TickColors = [];
var _mt3TickSpillColors = [];
for (var tc = 0; tc < _TUNER_MT3_TICK_COUNT; tc++) {
var d = Math.abs((tc / (_TUNER_MT3_TICK_COUNT - 1)) - 0.5) * 2;
var tg = Math.round(238 * (1 - d) + 119 * d); // G channel: 238 (yellow) → 119 (orange)
_mt3TickColors.push('rgb(255,' + tg + ',0)');
_mt3TickSpillColors.push('rgba(255,' + tg + ',0,0.52)');
}
// Arc-following labels — inside the arc at R18 (below the tube's inner edge with a gap)
// Tube inner edge at R8; labels at R18 give ~10 SVG-unit gap at apex, ~7 at ends
[
{ t: 0.0, text: '-50' },
{ t: 0.5, text: '0' },
{ t: 1.0, text: '+50' },
].forEach(function (lbl) {
var ang = _MT3_ARC_START + lbl.t * _MT3_ARC_SPAN;
var r = _MT3_ARC_R - 18;
var lx = _MT3_cx + r * Math.cos(ang);
var ly = _MT3_cy + r * Math.sin(ang);
var el = document.createElementNS(_SVG_NS, 'text');
el.setAttribute('x', String(lx));
el.setAttribute('y', String(ly));
el.setAttribute('text-anchor', 'middle');
el.setAttribute('dominant-baseline', 'middle');
el.setAttribute('font-size', '7');
el.setAttribute('fill', 'rgba(255,255,255,0.50)');
el.textContent = lbl.text;
gaugeSvg.appendChild(el);
});
// arcHighlight is topmost — appended last so it renders above ticks
gaugeSvg.appendChild(arcHighlight);
panel.appendChild(gaugeSvg);
// ── 7-segment display ─────────────────────────────────────────
var displayWrap = document.createElement('div');
displayWrap.style.position = 'absolute';
displayWrap.style.bottom = '5%';
displayWrap.style.left = '50%';
displayWrap.style.transform = 'translateX(-50%)';
displayWrap.style.width = '18%';
displayWrap.style.height = '45%';
displayWrap.style.background = '#0d0000';
displayWrap.style.borderRadius = '3px';
displayWrap.style.border = '1px solid #2a0000';
displayWrap.style.display = 'flex';
displayWrap.style.alignItems = 'center';
displayWrap.style.justifyContent = 'center';
displayWrap.style.padding = '4%';
displayWrap.style.boxSizing = 'border-box';
displayWrap.style.boxShadow = 'inset 0 0 8px #000';
panel.appendChild(displayWrap);
var segSvg = document.createElementNS(_SVG_NS, 'svg');
segSvg.setAttribute('viewBox', '0 0 100 200');
segSvg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
segSvg.style.width = '90%';
segSvg.style.aspectRatio = '1 / 2';
segSvg.style.flexShrink = '0';
segSvg.style.overflow = 'visible';
displayWrap.appendChild(segSvg);
var _mt3SegEls = {};
function _makeSegPoly(key, points) {
var el = document.createElementNS(_SVG_NS, 'polygon');
el.setAttribute('points', points);
el.setAttribute('fill', _MT3_COL_SEG_UNLIT);
el.setAttribute('shape-rendering', 'crispEdges');
segSvg.appendChild(el);
_mt3SegEls[key] = el;
}
_makeSegPoly('a', '11,5 89,5 95,13 89,21 11,21 5,13');
_makeSegPoly('b', '87,26 95,32 95,81 87,87 79,81 79,32');
_makeSegPoly('c', '87,113 95,119 95,168 87,174 79,168 79,119');
_makeSegPoly('d', '11,179 89,179 95,187 89,195 11,195 5,187');
_makeSegPoly('e', '13,113 21,119 21,168 13,174 5,168 5,119');
_makeSegPoly('f', '13,26 21,32 21,81 13,87 5,81 5,32');
_makeSegPoly('g1', '11,92 42.5,92 48.5,100 42.5,108 11,108 5,100');
_makeSegPoly('g2', '57.5,92 89,92 95,100 89,108 57.5,108 51.5,100');
var sharpSvg = document.createElementNS(_SVG_NS, 'svg');
sharpSvg.setAttribute('viewBox', '0 0 90 90');
sharpSvg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
sharpSvg.style.position = 'absolute';
sharpSvg.style.top = '57%';
sharpSvg.style.right = '3%';
sharpSvg.style.width = '23%';
sharpSvg.style.aspectRatio = '1 / 1';
sharpSvg.style.overflow = 'visible';
sharpSvg.style.pointerEvents = 'none';
displayWrap.appendChild(sharpSvg);
var _mt3SharpParts = [];
function _makeSharpPoly(pts) {
var el = document.createElementNS(_SVG_NS, 'polygon');
el.setAttribute('points', pts);
el.setAttribute('fill', _MT3_COL_SEG_UNLIT);
el.setAttribute('shape-rendering', 'crispEdges');
sharpSvg.appendChild(el);
_mt3SharpParts.push(el);
}
_makeSharpPoly('28.3,0 33.3,4 33.3,86 28.3,90 23.3,86 23.3,4');
_makeSharpPoly('61.7,0 66.7,4 66.7,86 61.7,90 56.7,86 56.7,4');
_makeSharpPoly('4,23.3 86,23.3 90,28.3 86,33.3 4,33.3 0,28.3');
_makeSharpPoly('4,56.7 86,56.7 90,61.7 86,66.7 4,66.7 0,61.7');
// "♭" symbol — same position as sharpSvg, shown in place of it for flat notes
var flatSvg = document.createElementNS(_SVG_NS, 'svg');
flatSvg.setAttribute('viewBox', '0 0 90 90');
flatSvg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
flatSvg.style.position = 'absolute';
flatSvg.style.top = '57%';
flatSvg.style.right = '3%';
flatSvg.style.width = '23%';
flatSvg.style.aspectRatio = '1 / 1';
flatSvg.style.overflow = 'visible';
flatSvg.style.pointerEvents = 'none';
flatSvg.style.display = 'none';
displayWrap.appendChild(flatSvg);
var _mt3FlatText = document.createElementNS(_SVG_NS, 'text');
_mt3FlatText.setAttribute('x', '45');
_mt3FlatText.setAttribute('y', '82');
_mt3FlatText.setAttribute('text-anchor', 'middle');
_mt3FlatText.setAttribute('font-size', '85');
_mt3FlatText.setAttribute('font-family', 'Georgia, serif');
_mt3FlatText.setAttribute('fill', _MT3_COL_SEG_UNLIT);
_mt3FlatText.textContent = '♭';
flatSvg.appendChild(_mt3FlatText);
// ── Buttons ───────────────────────────────────────────────────
// Strobe indicator LED — centred above the MODE button, lit when strobe mode is active
// Button: bottom:24%, height:8% → top edge at bottom:32%; LED sits at bottom:33.5%
// Button left:calc(50%-22%), width:10% → centre at calc(50%-17%); LED width:2.5% → left:calc(50%-18.25%)
var _mt3StrobeLed = document.createElement('div');
_mt3StrobeLed.style.position = 'absolute';
_mt3StrobeLed.style.bottom = '33.5%';
_mt3StrobeLed.style.left = 'calc(50% - 18.25%)';
_mt3StrobeLed.style.width = '2.5%';
_mt3StrobeLed.style.height = '0';
_mt3StrobeLed.style.paddingBottom = '2.5%';
_mt3StrobeLed.style.borderRadius = '50%';
_mt3StrobeLed.style.background = 'radial-gradient(circle at 35% 35%, #2a0000, #0a0000)';
_mt3StrobeLed.style.boxShadow = 'none';
_mt3StrobeLed.style.pointerEvents = 'none';
panel.appendChild(_mt3StrobeLed);
var _mt3ModeBtn = document.createElement('div');
_mt3ModeBtn.style.position = 'absolute';
_mt3ModeBtn.style.bottom = '24%';
_mt3ModeBtn.style.left = 'calc(50% - 22%)';
_mt3ModeBtn.style.width = '10%';
_mt3ModeBtn.style.height = '8%';
_mt3ModeBtn.style.backgroundColor = _MT3_COL_BUTTON;
_mt3ModeBtn.style.borderRadius = '3px';
_mt3ModeBtn.style.border = '1px solid #333';
_mt3ModeBtn.style.boxShadow = 'inset 0 1px 2px rgba(255,255,255,0.08), 0 2px 3px rgba(0,0,0,0.7)';
_mt3ModeBtn.style.cursor = 'pointer';
panel.appendChild(_mt3ModeBtn);
var brightBtn = document.createElement('div');
brightBtn.style.position = 'absolute';
brightBtn.style.bottom = '24%';
brightBtn.style.left = 'calc(50% + 12%)';
brightBtn.style.width = '10%';
brightBtn.style.height = '8%';
brightBtn.style.backgroundColor = _MT3_COL_BUTTON;
brightBtn.style.borderRadius = '3px';
brightBtn.style.border = '1px solid #333';
brightBtn.style.boxShadow = 'inset 0 1px 2px rgba(255,255,255,0.08), 0 2px 3px rgba(0,0,0,0.7)';
brightBtn.style.cursor = 'pointer';
panel.appendChild(brightBtn);
[{text: 'MODE', left: 'calc(50% - 22%)'}, {text: 'BRGHT', left: 'calc(50% + 12%)'}]
.forEach(function (lbl) {
var el = document.createElement('div');
el.style.position = 'absolute';
el.style.bottom = '14%';
el.style.left = lbl.left;
el.style.width = '10%';
el.style.textAlign = 'center';
el.style.color = _MT3_COL_LABEL;
el.style.fontSize = '0.75em';
el.style.letterSpacing = '0.05em';
el.style.pointerEvents = 'none';
el.textContent = lbl.text;
panel.appendChild(el);
});
var brandLbl = document.createElement('div');
brandLbl.style.position = 'absolute';
brandLbl.style.top = '2%';
brandLbl.style.right = '8%';
brandLbl.style.color = _MT3_COL_LABEL;
brandLbl.style.fontSize = '0.75em';
brandLbl.style.fontWeight = '600';
brandLbl.style.letterSpacing = '0.06em';
brandLbl.textContent = 'CHEF MT-3';
panel.appendChild(brandLbl);
container.appendChild(panel);
// ── Animation / glow state ────────────────────────────────────
var _mt3Mode = 'standard';
var _mt3CurrentCents = 0;
var _mt3SmoothedCents = 0;
var _mt3HasSignal = false;
var _mt3GlowOpacity = 0;
var _mt3RafId = null;
var _mt3BrightnessIdx = 1; // 0=low, 1=medium, 2=high
var _mt3LastLetter = ' '; // for re-render on brightness change
var _mt3LastSharp = false;
var _mt3LastFlat = false;
var _mt3LastTime = null;
var _mt3StrobeOffset = 0;
// Tick state: 0=dim, 1=spill, 2=bright (computed each frame, never persisted between calls)
var _mt3TickState = [];
var _mt3LastTickState = [];
for (var ti = 0; ti < _TUNER_MT3_TICK_COUNT; ti++) {
_mt3TickState.push(0);
_mt3LastTickState.push(-1); // -1 = unrendered, forces first paint
}
// ── Segment helpers ───────────────────────────────────────────
function _setSegment(el, lit) {
var brt = _MT3_BRIGHTNESS[_mt3BrightnessIdx];
el.setAttribute('fill', lit ? brt.litFill : _MT3_COL_SEG_UNLIT);
el.style.filter = lit ? brt.glow : 'drop-shadow(0 0 0px transparent)';
}
function _renderNote(letter) {
var map = _TUNER_MT3_SEGMENTS[letter] || _TUNER_MT3_SEGMENTS[' '];
for (var k = 0; k < _segKeys.length; k++) { _setSegment(_mt3SegEls[_segKeys[k]], map[k]); }
}
function _setSharp(lit) {
var brt = _MT3_BRIGHTNESS[_mt3BrightnessIdx];
for (var p = 0; p < _mt3SharpParts.length; p++) {
_mt3SharpParts[p].setAttribute('fill', lit ? brt.litFill : _MT3_COL_SEG_UNLIT);
}
sharpSvg.style.filter = lit ? brt.glow : 'none';
}
function _setFlat(lit) {
flatSvg.style.display = lit ? '' : 'none';
if (lit) {
var brt = _MT3_BRIGHTNESS[_mt3BrightnessIdx];
_mt3FlatText.setAttribute('fill', brt.litFill);
flatSvg.style.filter = brt.glow;
} else {
flatSvg.style.filter = 'none';
}
}
function _applyAccidental() {
if (_mt3LastFlat) {
sharpSvg.style.display = 'none';
_setFlat(true);
} else {
sharpSvg.style.display = '';
_setSharp(_mt3LastSharp);
_setFlat(false);
}
}
// ── Tick state helpers ────────────────────────────────────────
function _clearTickStates() {
for (var ti = 0; ti < _TUNER_MT3_TICK_COUNT; ti++) { _mt3TickState[ti] = 0; }
}
// Never downgrade: a bright centre tick won't be overwritten by a spill from another group
function _setTickState(idx, level) {
if (idx < 0 || idx >= _TUNER_MT3_TICK_COUNT) { return; }
if (level > _mt3TickState[idx]) { _mt3TickState[idx] = level; }
}
// Standard mode: 1 bright + ±1 spill
function _computeStandardStates(cents, hasSignal) {
_clearTickStates();
if (!hasSignal) { return; }
var clamped = Math.max(-_TUNER_MT3_GAUGE_CENTS, Math.min(_TUNER_MT3_GAUGE_CENTS, cents));
var targetIdx = Math.round((clamped + _TUNER_MT3_GAUGE_CENTS) /
(2 * _TUNER_MT3_GAUGE_CENTS / (_TUNER_MT3_TICK_COUNT - 1)));
targetIdx = Math.max(0, Math.min(_TUNER_MT3_TICK_COUNT - 1, targetIdx));
_setTickState(targetIdx, 2);
_setTickState(targetIdx - 1, 1);
_setTickState(targetIdx + 1, 1);
}
// Strobe mode: 3 bright ticks per group + ±1 spill on each outer edge
function _computeStrobeStates() {
_clearTickStates();
for (var g = 0; g < _TUNER_MT3_STROBE_GROUP_COUNT; g++) {
var baseAngle = _MT3_ARC_START + (g / _TUNER_MT3_STROBE_GROUP_COUNT) * _MT3_ARC_SPAN + _mt3StrobeOffset;
var relAngle = ((baseAngle - _MT3_ARC_START) % _MT3_ARC_SPAN + _MT3_ARC_SPAN) % _MT3_ARC_SPAN;
var nearIdx = Math.max(0, Math.min(_TUNER_MT3_TICK_COUNT - 1,
Math.round(relAngle / _MT3_ARC_SPAN * (_TUNER_MT3_TICK_COUNT - 1))));
// 3 bright ticks centred at nearIdx
_setTickState(nearIdx - 1, 2);
_setTickState(nearIdx, 2);
_setTickState(nearIdx + 1, 2);
// Lightspill beyond the cluster
_setTickState(nearIdx - 2, 1);
_setTickState(nearIdx + 2, 1);
}
}
// Apply current tick states to the glowGroup DOM — only write changed ticks
function _applyTickStates() {
for (var ti = 0; ti < _TUNER_MT3_TICK_COUNT; ti++) {
var state = _mt3TickState[ti];
if (state === _mt3LastTickState[ti]) { continue; }
_mt3LastTickState[ti] = state;
var el = _mt3GlowTickEls[ti];
if (state === 2) {
el.setAttribute('stroke', _mt3TickColors[ti]);
el.setAttribute('stroke-width', '3');
} else if (state === 1) {
el.setAttribute('stroke', _mt3TickSpillColors[ti]);
el.setAttribute('stroke-width', '2');
} else {
el.setAttribute('stroke', 'none');
el.setAttribute('stroke-width', '1');
}
}
}
// ── RAF loop ──────────────────────────────────────────────────
function _animateStrobe(now) {
if (_mt3LastTime === null) { _mt3LastTime = now; }
var dt = Math.min((now - _mt3LastTime) / 1000, 0.1);
_mt3LastTime = now;
// Smooth cents for strobe drift
var lerpFactor = 1 - Math.exp(-10 * dt);
_mt3SmoothedCents += (_mt3CurrentCents - _mt3SmoothedCents) * lerpFactor;
// Animate glow opacity: fast fade-in (~120ms), slow fade-out (~400ms)
var opacityTarget = _mt3HasSignal ? 1.0 : 0.0;
var opacityRate = _mt3HasSignal ? 8.0 : 2.5;
_mt3GlowOpacity += (opacityTarget - _mt3GlowOpacity) * (1 - Math.exp(-opacityRate * dt));
var _scaledOpacity = Math.min(1, _mt3GlowOpacity * _MT3_BRIGHTNESS[_mt3BrightnessIdx].brightScale);
_glowGroup.setAttribute('opacity', _scaledOpacity.toFixed(3));
// Advance strobe offset
if (_mt3Mode === 'strobe' && Math.abs(_mt3SmoothedCents) > 0.1) {
var absCents = Math.min(_TUNER_MT3_GAUGE_CENTS, Math.abs(_mt3SmoothedCents));
var normalized = Math.max(0, absCents - _TUNER_MT3_IN_TUNE_THR) / (_TUNER_MT3_GAUGE_CENTS - _TUNER_MT3_IN_TUNE_THR);
var speed = _MT3_ARC_SPAN * Math.pow(normalized, 0.9);
if (_mt3SmoothedCents < 0) { speed = -speed; }
_mt3StrobeOffset = ((_mt3StrobeOffset + speed * dt) % _MT3_ARC_SPAN + _MT3_ARC_SPAN) % _MT3_ARC_SPAN;
}
// Recompute and apply strobe tick states each frame
if (_mt3Mode === 'strobe') { _computeStrobeStates(); }
_applyTickStates();
_mt3RafId = requestAnimationFrame(_animateStrobe);
}
_mt3RafId = requestAnimationFrame(_animateStrobe);
// ── MODE button ───────────────────────────────────────────────
_mt3ModeBtn.addEventListener('click', function () {
_mt3ModeBtn.style.boxShadow = 'inset 0 2px 4px rgba(0,0,0,0.9)';
setTimeout(function () {
_mt3ModeBtn.style.boxShadow = 'inset 0 1px 2px rgba(255,255,255,0.08), 0 2px 3px rgba(0,0,0,0.7)';
}, 120);
if (_mt3Mode === 'standard') {
_mt3Mode = 'strobe';
_mt3StrobeOffset = 0;
_clearTickStates();
_mt3StrobeLed.style.background = 'radial-gradient(circle at 35% 35%, #ff4444, #cc0000)';
_mt3StrobeLed.style.boxShadow = '0 0 4px 2px #ff2200, 0 0 8px 3px #880000';
} else {
_mt3Mode = 'standard';
_computeStandardStates(_mt3CurrentCents, _mt3HasSignal);
_mt3StrobeLed.style.background = 'radial-gradient(circle at 35% 35%, #2a0000, #0a0000)';
_mt3StrobeLed.style.boxShadow = 'none';
}
});
// ── BRGHT button — cycle low/medium/high brightness ──────────
brightBtn.addEventListener('click', function () {
brightBtn.style.boxShadow = 'inset 0 2px 4px rgba(0,0,0,0.9)';
setTimeout(function () {
brightBtn.style.boxShadow = 'inset 0 1px 2px rgba(255,255,255,0.08), 0 2px 3px rgba(0,0,0,0.7)';
}, 120);
_mt3BrightnessIdx = (_mt3BrightnessIdx + 1) % _MT3_BRIGHTNESS.length;
// Update SVG tick glow filter intensity
_sfFlood.setAttribute('flood-opacity', _MT3_BRIGHTNESS[_mt3BrightnessIdx].floodOpacity);
// Re-render segment display with new brightness
_renderNote(_mt3LastLetter);
_applyAccidental();
});
// ── Public: update ────────────────────────────────────────────
function update(note, cents) {
var hasNote = (note !== null && note !== undefined);
_mt3HasSignal = hasNote;
_mt3CurrentCents = hasNote ? (cents || 0) : 0;
if (_mt3Mode === 'standard') { _computeStandardStates(_mt3CurrentCents, hasNote); }
if (hasNote) {
_mt3LastLetter = note.charAt(0);
_mt3LastSharp = note.charAt(1) === '#';
_mt3LastFlat = note.charAt(1) === 'b';
_renderNote(_mt3LastLetter);
_applyAccidental();
} else {
_mt3LastLetter = ' ';
_mt3LastSharp = false;
_mt3LastFlat = false;
_renderNote(' ');
_applyAccidental();
}
}
// ── Public: destroy ───────────────────────────────────────────
function destroy() {
if (_mt3RafId) { cancelAnimationFrame(_mt3RafId); _mt3RafId = null; }
panel.remove();
}
return { update: update, destroy: destroy };
};
})();
+81
View File
@@ -0,0 +1,81 @@
/**
* Default (gauge) tuner visualization for the Slopsmith tuner plugin.
*
* Contract: window._tunerViz_default(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)
*/
window._tunerViz_default = function (container) {
'use strict';
// ── DOM ───────────────────────────────────────────────────────────
const noteDisplay = document.createElement('div');
noteDisplay.className = 'my-2 h-16 flex items-center justify-center';
const noteText = document.createElement('div');
noteText.className = 'text-5xl font-black text-white';
noteText.textContent = '--';
noteDisplay.appendChild(noteText);
container.appendChild(noteDisplay);
const freqDisplay = document.createElement('div');
freqDisplay.className = 'text-xs text-gray-500 mb-3 font-mono text-center w-full';
freqDisplay.textContent = '0.0 Hz';
container.appendChild(freqDisplay);
const gaugeEl = document.createElement('div');
gaugeEl.className = 'w-full h-2.5 bg-dark-900 border border-gray-800 rounded-full relative overflow-hidden mb-1.5';
const centerMarker = document.createElement('div');
centerMarker.className = 'absolute left-1/2 top-0 bottom-0 w-0.5 bg-accent z-10';
gaugeEl.appendChild(centerMarker);
const gaugeNeedle = document.createElement('div');
gaugeNeedle.className = 'absolute left-1/2 top-0 bottom-0 w-1 bg-white transition-all duration-100 ease-out -translate-x-1/2 z-20 shadow-[0_0_8px_rgba(255,255,255,0.5)]';
gaugeEl.appendChild(gaugeNeedle);
container.appendChild(gaugeEl);
const centsDisplay = document.createElement('div');
centsDisplay.className = 'text-sm font-bold tracking-tight text-center w-full';
centsDisplay.textContent = '0 cents';
container.appendChild(centsDisplay);
// ── Public API ────────────────────────────────────────────────────
function update(note, cents, freq) {
if (note === null) {
noteText.textContent = '--';
noteText.className = 'text-5xl font-black text-white';
freqDisplay.textContent = '0.0 Hz';
centsDisplay.textContent = '0 cents';
gaugeNeedle.style.left = '50%';
gaugeNeedle.className = 'absolute left-1/2 top-0 bottom-0 w-1 bg-white transition-all duration-100 ease-out -translate-x-1/2 z-20 shadow-[0_0_8px_rgba(255,255,255,0.5)]';
return;
}
noteText.textContent = note;
noteText.className = 'text-5xl font-black ' + (Math.abs(cents) < 5 ? 'text-green-400' : 'text-white');
freqDisplay.textContent = freq.toFixed(1) + ' Hz';
centsDisplay.textContent = (cents > 0 ? '+' : '') + cents.toFixed(0) + ' cents';
const gaugeRange = 50;
const percent = Math.max(0, Math.min(100, 50 + (cents / gaugeRange) * 50));
gaugeNeedle.style.left = percent + '%';
if (Math.abs(cents) < 5) {
gaugeNeedle.className = 'absolute top-0 bottom-0 w-1 bg-green-400 transition-all duration-100 ease-out -translate-x-1/2 z-20 shadow-[0_0_8px_rgba(74,222,128,0.5)]';
} else {
gaugeNeedle.className = 'absolute top-0 bottom-0 w-1 bg-white transition-all duration-100 ease-out -translate-x-1/2 z-20 shadow-[0_0_8px_rgba(255,255,255,0.5)]';
}
}
function destroy() {
noteDisplay.remove();
freqDisplay.remove();
gaugeEl.remove();
centsDisplay.remove();
}
return { update, destroy };
};
+452
View File
@@ -0,0 +1,452 @@
/**
* Mace Fx III style tuner visualization for the Slopsmith tuner plugin.
*
* Inspired by hardware rack tuner displays:
* - Dark navy LCD background
* - Horizontal chromatic tick-mark gauge (top)
* - Inward-pointing directional arrows (▶ ◀) below gauge
* - Large note name (lower-left) and octave number (lower-right)
* - Orange dashed strobe circle (bottom centre)
* - Mode tabs Free / Auto / Manual (top-right)
*
* Contract: window['_tunerViz_mace-fx-iii'](container) → { update(note, cents, freq, mode), destroy() }
* - note: string | null (null = no signal)
* - cents: number (deviation from target, 50…+50)
* - freq: number (detected frequency in Hz)
* - mode: 'free' | 'auto' | 'manual' (tuning mode from screen.js)
*/
(function () {
'use strict';
// ── Constants ─────────────────────────────────────────────────────
var _TUNER_TICK_COUNT = 11;
var _TUNER_STROBE_N = 4; // segments fitting in 180° (plus one trailing gap)
var _TUNER_STROBE_R = 38; // radius in SVG units (full circle, fits in 120×120 viewBox)
var _TUNER_IN_TUNE_THR = 2; // cents threshold for in-tune state
var _TUNER_ARROW_THR = 3; // cents threshold for arrow direction
var _SVG_NS = 'http://www.w3.org/2000/svg';
// ── Colours (custom palette; no Tailwind token equivalents) ──────
var _COL_BG = '#0e0e0e'; // dark gray background
var _COL_TICK = '#7ad400'; // yellow-green gauge ticks
var _COL_MARKER = '#ffffff'; // white pitch-position marker
var _COL_NOTE = '#ffffff'; // white note/octave text
var _COL_ARROW_WH = '#e8e8e8'; // lit arrow colour
var _COL_ARROW_DIM = '#1e3030'; // dimmed arrow colour
var _COL_STROBE = '#e87020'; // orange strobe circle
var _COL_TAB_ACT_BG = '#505868'; // active tab background (slate-gray)
var _COL_TAB_ACT_FG = '#ffffff'; // active tab text
var _COL_TAB_DIM = '#506080'; // inactive tab text
window['_tunerViz_mace-fx-iii'] = function (container) {
'use strict';
// ── Root panel ────────────────────────────────────────────────
var panel = document.createElement('div');
panel.className = 'relative w-full overflow-hidden font-mono select-none';
panel.style.backgroundColor = _COL_BG;
panel.style.aspectRatio = '16 / 9';
panel.style.minHeight = '120px';
// ── Mode tabs (full-width bar, ~12.5% height) ────────────────
var tabsWrap = document.createElement('div');
tabsWrap.style.position = 'absolute';
tabsWrap.style.top = '0';
tabsWrap.style.left = '0';
tabsWrap.style.right = '0';
tabsWrap.style.height = '12.5%';
tabsWrap.style.display = 'flex';
tabsWrap.style.alignItems = 'flex-end';
tabsWrap.style.justifyContent = 'flex-end';
tabsWrap.style.borderBottom = '2px solid ' + _COL_TAB_ACT_BG;
tabsWrap.style.zIndex = '10';
var _tabNames = ['Free', 'Auto', 'Manual'];
var _tabEls = _tabNames.map(function (name) {
var tab = document.createElement('span');
tab.className = 'px-2 py-px text-xs leading-none';
tab.style.borderRadius = '2px 2px 0 0';
tab.style.cursor = 'default';
tab.textContent = name;
tabsWrap.appendChild(tab);
return tab;
});
panel.appendChild(tabsWrap);
// ── Gauge + arrows zone (25%→50% from top) ───────────────────
var gaugeZone = document.createElement('div');
gaugeZone.style.position = 'absolute';
gaugeZone.style.top = '25%';
gaugeZone.style.left = '0';
gaugeZone.style.right = '0';
gaugeZone.style.height = '25%';
gaugeZone.style.display = 'flex';
gaugeZone.style.flexDirection = 'column';
gaugeZone.style.justifyContent = 'center';
gaugeZone.style.zIndex = '5';
// Chromatic gauge — ends at 12.5% from each edge
var gaugeOuter = document.createElement('div');
gaugeOuter.style.position = 'relative';
gaugeOuter.style.marginLeft = '12.5%';
gaugeOuter.style.marginRight = '12.5%';
gaugeOuter.style.flexShrink = '0';
var gaugeBg = document.createElement('div');
gaugeBg.style.position = 'absolute';
gaugeBg.style.top = '22.5%'; // (100% - 55%) / 2, matches flex items-center
gaugeBg.style.left = '0';
gaugeBg.style.right = '0';
gaugeBg.style.height = '55%';
gaugeBg.style.backgroundColor = 'rgba(0,60,20,0.7)';
gaugeBg.style.borderRadius = '2px';
gaugeOuter.appendChild(gaugeBg);
var gaugeWrap = document.createElement('div');
gaugeWrap.className = 'relative flex items-center justify-between';
gaugeWrap.style.height = '1.4em';
var _tickEls = [];
for (var i = 0; i < _TUNER_TICK_COUNT; i++) {
var isCentre = (i === Math.floor(_TUNER_TICK_COUNT / 2));
var tick = document.createElement('div');
tick.style.width = '2px';
tick.style.height = isCentre ? '100%' : '55%';
tick.style.backgroundColor = _COL_TICK;
tick.style.borderRadius = '1px';
tick.style.flexShrink = '0';
tick.style.filter = 'drop-shadow(0 0 4px rgba(122,212,0,0.35))';
gaugeWrap.appendChild(tick);
_tickEls.push(tick);
}
var marker = document.createElement('div');
marker.style.position = 'absolute';
marker.style.top = '0';
marker.style.bottom = '0';
marker.style.width = '3px';
marker.style.backgroundColor = _COL_MARKER;
marker.style.left = '50%';
marker.style.transform = 'translateX(-50%)';
marker.style.display = 'none';
marker.style.zIndex = '6';
marker.style.boxShadow = '0 0 6px 1px rgba(255,255,255,0.6)';
gaugeWrap.appendChild(marker);
gaugeOuter.appendChild(gaugeWrap);
// Spacer: 1/3 of regular tick height (1/3 * 55% * 1.4em ≈ 0.257em)
var gaugeArrowGap = document.createElement('div');
gaugeArrowGap.style.height = '0.257em';
gaugeArrowGap.style.flexShrink = '0';
// Direction arrows SVG — outer edges at ±10¢, gap 15% (~3¢)
var arrowSvg = document.createElementNS(_SVG_NS, 'svg');
arrowSvg.setAttribute('viewBox', '0 0 100 10');
arrowSvg.setAttribute('preserveAspectRatio', 'none');
arrowSvg.style.alignSelf = 'center';
arrowSvg.style.width = '15%';
arrowSvg.style.height = '0.77rem';
arrowSvg.style.flexShrink = '0';
arrowSvg.style.overflow = 'visible';
// SVG glow filter — applied per-polygon so only lit arrows glow
var _arrowGlowId = 'arrow-glow-' + Math.random().toString(36).slice(2, 8);
var _arrowDefs = document.createElementNS(_SVG_NS, 'defs');
var _arrowFilter = document.createElementNS(_SVG_NS, 'filter');
_arrowFilter.setAttribute('id', _arrowGlowId);
_arrowFilter.setAttribute('x', '-80%'); _arrowFilter.setAttribute('y', '-80%');
_arrowFilter.setAttribute('width', '260%'); _arrowFilter.setAttribute('height', '260%');
var _fBlur = document.createElementNS(_SVG_NS, 'feGaussianBlur');
_fBlur.setAttribute('stdDeviation', '1.2'); _fBlur.setAttribute('result', 'blur');
var _fFlood = document.createElementNS(_SVG_NS, 'feFlood');
_fFlood.setAttribute('flood-color', 'white'); _fFlood.setAttribute('flood-opacity', '0.5'); _fFlood.setAttribute('result', 'col');
var _fComp = document.createElementNS(_SVG_NS, 'feComposite');
_fComp.setAttribute('in', 'col'); _fComp.setAttribute('in2', 'blur'); _fComp.setAttribute('operator', 'in'); _fComp.setAttribute('result', 'glow');
var _fMerge = document.createElementNS(_SVG_NS, 'feMerge');
[['glow'], ['SourceGraphic']].forEach(function (n) {
var mn = document.createElementNS(_SVG_NS, 'feMergeNode'); mn.setAttribute('in', n[0]); _fMerge.appendChild(mn);
});
_arrowFilter.appendChild(_fBlur); _arrowFilter.appendChild(_fFlood);
_arrowFilter.appendChild(_fComp); _arrowFilter.appendChild(_fMerge);
_arrowDefs.appendChild(_arrowFilter);
arrowSvg.appendChild(_arrowDefs);
var arrowLPoly = document.createElementNS(_SVG_NS, 'polygon');
arrowLPoly.setAttribute('points', '0,0 0,10 42.5,5');
arrowLPoly.setAttribute('fill', _COL_ARROW_DIM);
var arrowRPoly = document.createElementNS(_SVG_NS, 'polygon');
arrowRPoly.setAttribute('points', '100,0 100,10 57.5,5');
arrowRPoly.setAttribute('fill', _COL_ARROW_DIM);
arrowSvg.appendChild(arrowLPoly);
arrowSvg.appendChild(arrowRPoly);
// Order: arrows → spacer → gauge (arrows on top, gauge underneath)
gaugeZone.appendChild(arrowSvg);
gaugeZone.appendChild(gaugeArrowGap);
gaugeZone.appendChild(gaugeOuter);
panel.appendChild(gaugeZone);
var arrowL = arrowLPoly;
var arrowR = arrowRPoly;
var _arrowGlowUrl = 'url(#' + _arrowGlowId + ')';
// ── Note name display ─────────────────────────────────────────
// Horizontal: center of note letter at 12.5% from left.
// font-size set on wrapper so `ch` resolves to the note character width.
// noteLetter is width:1ch so the accidental never shifts the F position.
var noteWrap = document.createElement('div');
noteWrap.style.position = 'absolute';
noteWrap.style.left = 'calc(12.5% - 0.5ch)';
noteWrap.style.top = '67%';
noteWrap.style.transform = 'translateY(-50%)';
noteWrap.style.height = '25%';
noteWrap.style.display = 'flex';
noteWrap.style.alignItems = 'center';
noteWrap.style.fontSize = '3.2rem';
noteWrap.style.color = _COL_NOTE;
noteWrap.style.zIndex = '5';
noteWrap.style.overflow = 'visible';
noteWrap.style.textShadow = '0 0 5px rgba(255,255,255,0.4)';
var noteLetter = document.createElement('span');
noteLetter.style.display = 'inline-block';
noteLetter.style.width = '1ch';
noteLetter.style.flexShrink = '0';
noteLetter.style.fontWeight = '700';
noteLetter.style.lineHeight = '1';
noteLetter.textContent = '-';
var noteAccidental = document.createElement('span');
noteAccidental.style.fontSize = '1.7rem';
noteAccidental.style.fontWeight = '700';
noteAccidental.style.lineHeight = '1';
noteAccidental.style.alignSelf = 'flex-start';
noteAccidental.style.marginTop = '0.15em';
noteAccidental.textContent = '';
noteWrap.appendChild(noteLetter);
noteWrap.appendChild(noteAccidental);
panel.appendChild(noteWrap);
// ── Octave display ────────────────────────────────────────────
// Center of digit at 12.5% from right; width:1ch pins the element size.
var octaveEl = document.createElement('div');
octaveEl.style.position = 'absolute';
octaveEl.style.right = 'calc(12.5% - 0.5ch)';
octaveEl.style.top = '67%';
octaveEl.style.transform = 'translateY(-50%)';
octaveEl.style.height = '25%';
octaveEl.style.width = '1ch';
octaveEl.style.display = 'flex';
octaveEl.style.alignItems = 'center';
octaveEl.style.fontSize = '3.2rem';
octaveEl.style.fontWeight = '700';
octaveEl.style.lineHeight = '1';
octaveEl.style.color = _COL_NOTE;
octaveEl.style.zIndex = '5';
octaveEl.style.textShadow = '0 0 5px rgba(255,255,255,0.4)';
octaveEl.textContent = '-';
panel.appendChild(octaveEl);
// ── Strobe circle SVG (bottom-centre) ────────────────────────
// Full dashed circle, centered in the SVG viewBox so no part is clipped.
// gap = (2/3)*dash; using full circumference for dash calculation.
var _sVB_W = 120, _sVB_H = 120;
var _scx = 60, _scy = 60;
var _halfCirc = 2 * Math.PI * _TUNER_STROBE_R; // full circumference
var _dashLen = 3 * _halfCirc / 20;
var _gapLen = (2 / 3) * _dashLen;
var strobeSvg = document.createElementNS(_SVG_NS, 'svg');
strobeSvg.setAttribute('viewBox', '0 0 ' + _sVB_W + ' ' + _sVB_H);
strobeSvg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
strobeSvg.setAttribute('class', 'absolute');
strobeSvg.style.bottom = '4%';
strobeSvg.style.left = '50%';
strobeSvg.style.transform = 'translateX(-50%)';
strobeSvg.style.width = '22%';
strobeSvg.style.overflow = 'visible';
strobeSvg.style.zIndex = '4';
// SVG glow filter for arc — avoids CSS filter viewport clipping
var _strobeGlowId = 'strobe-glow-' + Math.random().toString(36).slice(2, 8);
var _strobeDefs = document.createElementNS(_SVG_NS, 'defs');
var _strobeFilter = document.createElementNS(_SVG_NS, 'filter');
_strobeFilter.setAttribute('id', _strobeGlowId);
_strobeFilter.setAttribute('x', '-30%'); _strobeFilter.setAttribute('y', '-30%');
_strobeFilter.setAttribute('width', '160%'); _strobeFilter.setAttribute('height', '160%');
var _sfBlur = document.createElementNS(_SVG_NS, 'feGaussianBlur');
_sfBlur.setAttribute('stdDeviation', '2'); _sfBlur.setAttribute('result', 'blur');
var _sfFlood = document.createElementNS(_SVG_NS, 'feFlood');
_sfFlood.setAttribute('flood-color', _COL_STROBE); _sfFlood.setAttribute('flood-opacity', '0.45'); _sfFlood.setAttribute('result', 'col');
var _sfComp = document.createElementNS(_SVG_NS, 'feComposite');
_sfComp.setAttribute('in', 'col'); _sfComp.setAttribute('in2', 'blur'); _sfComp.setAttribute('operator', 'in'); _sfComp.setAttribute('result', 'glow');
var _sfMerge = document.createElementNS(_SVG_NS, 'feMerge');
[['glow'], ['SourceGraphic']].forEach(function (n) {
var mn = document.createElementNS(_SVG_NS, 'feMergeNode'); mn.setAttribute('in', n[0]); _sfMerge.appendChild(mn);
});
_strobeFilter.appendChild(_sfBlur); _strobeFilter.appendChild(_sfFlood);
_strobeFilter.appendChild(_sfComp); _strobeFilter.appendChild(_sfMerge);
_strobeDefs.appendChild(_strobeFilter);
strobeSvg.appendChild(_strobeDefs);
// Full dashed circle — <circle> supports stroke-dashoffset identically to <path>
var arcPath = document.createElementNS(_SVG_NS, 'circle');
arcPath.setAttribute('cx', String(_scx));
arcPath.setAttribute('cy', String(_scy));
arcPath.setAttribute('r', String(_TUNER_STROBE_R));
arcPath.setAttribute('fill', 'none');
arcPath.setAttribute('stroke', _COL_STROBE);
arcPath.setAttribute('stroke-width', String(_dashLen));
arcPath.setAttribute('stroke-dasharray', _dashLen + ' ' + _gapLen);
arcPath.setAttribute('stroke-linecap', 'butt');
arcPath.setAttribute('filter', 'url(#' + _strobeGlowId + ')');
strobeSvg.appendChild(arcPath);
panel.appendChild(strobeSvg);
// ── LCD grid overlay ──────────────────────────────────────────
// Spans everything below the tab bar (top: 12.5%), 3px cell, bg colour lines.
var lcdGrid = document.createElement('div');
lcdGrid.style.position = 'absolute';
lcdGrid.style.top = '12.5%';
lcdGrid.style.left = '0';
lcdGrid.style.right = '0';
lcdGrid.style.bottom = '0';
lcdGrid.style.zIndex = '50';
lcdGrid.style.pointerEvents = 'none';
lcdGrid.style.backgroundImage = [
'repeating-linear-gradient(0deg, rgba(14,14,14,0.33) 0px, rgba(14,14,14,0.33) 1px, transparent 1px, transparent 2px)',
'repeating-linear-gradient(90deg, rgba(14,14,14,0.33) 0px, rgba(14,14,14,0.33) 1px, transparent 1px, transparent 2px)'
].join(',');
lcdGrid.style.backgroundPosition = '12.5% 0';
panel.appendChild(lcdGrid);
container.appendChild(panel);
// ── Internal state ────────────────────────────────────────────
var _rafId = null;
var _currentMode = 'free';
var _strobeOffset = 0; // stroke-dashoffset accumulator (SVG length units)
var _currentCents = 0; // target cents (0 when no signal)
var _smoothedCents = 0; // lerped cents — drives speed, decays to 0 on stop
var _lastTime = null;
var _totalDash = _dashLen + _gapLen; // one dash-cycle period
// ── Strobe RAF animation loop ─────────────────────────────────
// _smoothedCents lerps toward _currentCents every frame (mirrors strobe.js).
// When signal stops, _currentCents = 0 → _smoothedCents decays → speed → 0.
// The strobe always decelerates smoothly rather than snapping to a freeze.
function _animateStrobe(now) {
if (_lastTime === null) { _lastTime = now; }
var dt = Math.min((now - _lastTime) / 1000, 0.1);
_lastTime = now;
var lerpFactor = 1 - Math.exp(-10 * dt);
_smoothedCents += (_currentCents - _smoothedCents) * lerpFactor;
if (Math.abs(_smoothedCents) > 0.1) {
var absCents = Math.min(50, Math.abs(_smoothedCents));
var normalized = Math.max(0, absCents - _TUNER_IN_TUNE_THR) / (50 - _TUNER_IN_TUNE_THR);
var speed = _halfCirc * Math.pow(normalized, 0.9);
if (_smoothedCents > 0) { speed = -speed; }
_strobeOffset = ((_strobeOffset + speed * dt) % _totalDash + _totalDash) % _totalDash;
arcPath.setAttribute('stroke-dashoffset', String(_strobeOffset));
}
_rafId = requestAnimationFrame(_animateStrobe);
}
_rafId = requestAnimationFrame(_animateStrobe);
// ── Helper: derive octave number from frequency ───────────────
function _freqToOctave(freq) {
if (!freq || freq <= 0) return '-';
var midi = Math.round(69 + 12 * Math.log2(freq / 440));
return String(Math.floor(midi / 12) - 1);
}
// ── Helper: update mode tab highlights ────────────────────────
function _updateTabs(mode) {
var map = { free: 0, auto: 1, manual: 2 };
var active = (map[mode] !== undefined) ? map[mode] : 0;
_tabEls.forEach(function (tab, i) {
if (i === active) {
tab.style.backgroundColor = _COL_TAB_ACT_BG;
tab.style.color = _COL_TAB_ACT_FG;
} else {
tab.style.backgroundColor = 'transparent';
tab.style.color = _COL_TAB_DIM;
}
});
}
// Initialise tabs
_updateTabs('free');
// ── Public: update ────────────────────────────────────────────
function update(note, cents, freq, mode, targetFreq) {
var hasNote = (note !== null && note !== undefined);
// Mode tabs
if (mode !== undefined) { _currentMode = mode; }
_updateTabs(_currentMode);
// Gauge marker — clamp cents to [-50,50] so marker stays within gauge bounds
if (hasNote) {
marker.style.left = Math.max(0, Math.min(100, cents + 50)) + '%';
marker.style.display = 'block';
} else {
marker.style.display = 'none';
}
// Direction arrows — use setAttribute('filter','none') not removeAttribute
// so the filter is explicitly cleared on every dim transition
if (!hasNote) {
arrowL.setAttribute('fill', _COL_ARROW_DIM); arrowL.setAttribute('filter', 'none');
arrowR.setAttribute('fill', _COL_ARROW_DIM); arrowR.setAttribute('filter', 'none');
} else if (cents <= -_TUNER_ARROW_THR) {
arrowL.setAttribute('fill', _COL_ARROW_WH); arrowL.setAttribute('filter', _arrowGlowUrl);
arrowR.setAttribute('fill', _COL_ARROW_DIM); arrowR.setAttribute('filter', 'none');
} else if (cents >= _TUNER_ARROW_THR) {
arrowL.setAttribute('fill', _COL_ARROW_DIM); arrowL.setAttribute('filter', 'none');
arrowR.setAttribute('fill', _COL_ARROW_WH); arrowR.setAttribute('filter', _arrowGlowUrl);
} else {
arrowL.setAttribute('fill', _COL_ARROW_WH); arrowL.setAttribute('filter', _arrowGlowUrl);
arrowR.setAttribute('fill', _COL_ARROW_WH); arrowR.setAttribute('filter', _arrowGlowUrl);
}
// Note display
if (hasNote) {
noteLetter.textContent = note.charAt(0);
noteAccidental.textContent = note.slice(1);
} else {
noteLetter.textContent = '-';
noteAccidental.textContent = '';
}
// Octave display — show target octave in auto/manual, detected octave in free
if (hasNote) {
if ((_currentMode === 'auto' || _currentMode === 'manual') && targetFreq) {
octaveEl.textContent = _freqToOctave(targetFreq);
} else {
octaveEl.textContent = _freqToOctave(freq);
}
} else {
octaveEl.textContent = '-';
}
// Strobe state — smoothed animation decelerates naturally when _currentCents → 0
_currentCents = hasNote ? cents : 0;
}
// ── Public: destroy ───────────────────────────────────────────
function destroy() {
if (_rafId) { cancelAnimationFrame(_rafId); _rafId = null; }
panel.remove();
}
return { update: update, destroy: destroy };
};
})();
+496
View File
@@ -0,0 +1,496 @@
(function () {
'use strict';
// ── Constants ─────────────────────────────────────────────────────
var _TUNER_PT_IN_TUNE_THR = 2;
var _TUNER_PT_LED_COUNT = 11;
var _TUNER_PT_CENTS_RANGE = 50;
// Display colours
var _TUNER_PT_LIT = '#ff2200';
var _TUNER_PT_UNLIT = '#1a0000';
var _TUNER_PT_BG = '#0d0000';
// ── 8-segment map ─────────────────────────────────────────────────
// Segments indexed: [a, b, c, d, e, f, g1, g2]
var _TUNER_PT_SEGMENTS = {
// a b c d e f g1 g2
'A': [ true, true, true, false, true, true, true, true ],
'B': [ false, false, true, true, true, true, true, true ],
'C': [ true, false, false, true, true, true, false, false ],
'D': [ false, true, true, true, true, false, true, true ],
'E': [ true, false, false, true, true, true, true, false ],
'F': [ true, false, false, false, true, true, true, false ],
'G': [ true, false, true, true, true, true, false, true ],
' ': [ false, false, false, false, false, false, false, false ],
};
// Instance counter for unique SVG gradient IDs
var _ppTinyCount = 0;
window['_tunerViz_pp-tiny'] = function (container) {
'use strict';
// ── SVG frame ─────────────────────────────────────────────────
// Shape: semi-circle (r = W/2) + rectangle (h = W/4)
// Total height = W/2 + W/4 = 3W/4 → aspect-ratio 4:3
// viewBox "0 0 100 75" (75 = 3/4 × 100).
// Semi-circle arc: centre (50,50), r=50, from (0,50) to (100,50)
// Rectangle: y 50→75, full width, small rounded bottom corners
// Face inset 4 units on all sides:
// Semi-circle arc: centre (50,50), r=46, from (4,50) to (96,50)
// Rectangle: y 50→71 (754=71)
var _gradId = 'ppTinyFrameGrad' + (++_ppTinyCount);
var panel = document.createElement('div');
panel.style.cssText = 'position:relative;width:100%;aspect-ratio:4/3;user-select:none;';
// SVG draws the gray frame + dark face shape
var frameSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
frameSvg.setAttribute('viewBox', '0 0 100 75');
frameSvg.setAttribute('preserveAspectRatio', 'none');
frameSvg.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;display:block;';
var _brushId = 'ppTinyBrush' + _ppTinyCount;
var _bevelBrushId = 'ppTinyBevelBrush' + _ppTinyCount;
var defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
// Main face: metallic gradient with multiple highlight/shadow bands
var grad = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
grad.setAttribute('id', _gradId);
grad.setAttribute('x1', '0.2'); grad.setAttribute('y1', '0');
grad.setAttribute('x2', '0.8'); grad.setAttribute('y2', '1');
[['0%','#f2f2f2'],['14%','#d0d0d0'],['30%','#888888'],['46%','#bababa'],['62%','#7e7e7e'],['80%','#c2c2c2'],['100%','#6a6a6a']].forEach(function(s) {
var stop = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop.setAttribute('offset', s[0]); stop.setAttribute('stop-color', s[1]);
grad.appendChild(stop);
});
defs.appendChild(grad);
function _makeBrushFilter(id, freqX, freqY, seed, contrast, base) {
var c = contrast || 0.4, b = base !== undefined ? base : 0.25;
var v = c + ' 0 0 0 ' + b + ' ' + c + ' 0 0 0 ' + b + ' ' + c + ' 0 0 0 ' + b + ' 0 0 0 1 0';
var f = document.createElementNS('http://www.w3.org/2000/svg', 'filter');
f.setAttribute('id', id);
f.setAttribute('color-interpolation-filters', 'sRGB');
var t = document.createElementNS('http://www.w3.org/2000/svg', 'feTurbulence');
t.setAttribute('type', 'fractalNoise'); t.setAttribute('baseFrequency', freqX + ' ' + freqY);
t.setAttribute('numOctaves', '2'); t.setAttribute('seed', seed); t.setAttribute('result', 'noise');
var cm = document.createElementNS('http://www.w3.org/2000/svg', 'feColorMatrix');
cm.setAttribute('type', 'matrix'); cm.setAttribute('in', 'noise');
cm.setAttribute('values', v);
cm.setAttribute('result', 'grayNoise');
var bl = document.createElementNS('http://www.w3.org/2000/svg', 'feBlend');
bl.setAttribute('in', 'SourceGraphic'); bl.setAttribute('in2', 'grayNoise');
bl.setAttribute('mode', 'soft-light'); bl.setAttribute('result', 'blended');
var cp = document.createElementNS('http://www.w3.org/2000/svg', 'feComposite');
cp.setAttribute('in', 'blended'); cp.setAttribute('in2', 'SourceGraphic'); cp.setAttribute('operator', 'in');
f.appendChild(t); f.appendChild(cm); f.appendChild(bl); f.appendChild(cp);
return f;
}
defs.appendChild(_makeBrushFilter(_brushId, '0.65', '0.015', '3', 0.4, 0.25)); // horizontal grain — main arc face
defs.appendChild(_makeBrushFilter(_bevelBrushId, '0.45', '0.015', '7', 0.65, 0.08)); // horizontal grain, lower freq, higher contrast — bevel face
frameSvg.appendChild(defs);
// Main frame — restored with original rounded corners, unchanged
var framePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
framePath.setAttribute('d', 'M 0,50 A 50,50 0 0 1 100,50 L 100,73 Q 100,75 98,75 L 2,75 Q 0,75 0,73 Z');
framePath.setAttribute('fill', 'url(#' + _gradId + ')');
framePath.setAttribute('filter', 'url(#' + _brushId + ')');
frameSvg.appendChild(framePath);
// Bevel trapezoid — sits on top of the main frame, covers only the bottom strip.
// Sides meet the corner curves at their t=0.5 midpoints (de Casteljau):
// Right midpoint: (99.5, 74.5); lower-half bezier: Q 99,75 98,75
// Left midpoint: (0.5, 74.5); lower-half bezier: Q 1,75 0.5,74.5 (path direction reversed)
// 45° sides: Δx=Δy=5.5 each ✓ — top edge y=69, x=6 to x=94
var bevelPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
bevelPath.setAttribute('d', 'M 6,69 L 94,69 L 99.5,74.5 Q 99,75 98,75 L 2,75 Q 1,75 0.5,74.5 Z');
bevelPath.setAttribute('fill', '#c0c0c0');
bevelPath.setAttribute('filter', 'url(#' + _bevelBrushId + ')');
frameSvg.appendChild(bevelPath);
var faceBgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
faceBgPath.setAttribute('d', 'M 4,50 A 46,46 0 0 1 96,50 L 96,70 Q 96,71 95,71 L 5,71 Q 4,71 4,70 Z');
faceBgPath.setAttribute('fill', '#080808');
frameSvg.appendChild(faceBgPath);
panel.appendChild(frameSvg);
// ── Black panel face (content host) ───────────────────────────
// Face occupies inset 4 units in viewBox coords:
// left: 4/100 = 4%, top: 4/75 = 5.333%
// width: 92/100 = 92%, height: 67/75 = 89.333%
var face = document.createElement('div');
face.style.cssText = 'position:absolute;left:4%;top:5.333%;width:92%;height:89.333%;overflow:hidden;';
panel.appendChild(face);
// ── Arc geometry ──────────────────────────────────────────────
// Face SVG inset: x 496, y 471 (width=92, height=67 in panel units).
// Arc centre in panel SVG = (50, 50) → in face-div %:
// cx = (504)/92×100 = 50 %
// cy = (504)/67×100 = 68.657 %
// Face aspect A = 92/67 ≈ 1.3731.
// For a physical circle of radius r (% of face-width):
// x = cx + r·cos(θ) (face-width %)
// y = cy r·A·sin(θ) (face-height %; A corrects non-square face)
// Separator SVG arc (viewBox 0 0 100 100, preserveAspectRatio=none):
// rx = r (x-units ≡ face-width %), ry = r·A (y-units ≡ face-height %)
// Radii (r in % of face_width, max=46):
// LEDs r=40 → top at (50%, 14%)
// line r=35 → top at (50%, 21%)
// labels r=30 → top at (50%, 27%)
var _ARC_CX = 50;
var _ARC_CY = 68.657; // % of face height
var _ARC_ASPECT = 92 / 67; // face width / face height
var _ARC_R_LEDS = 40;
var _ARC_R_LINE = 35;
var _ARC_R_LABELS = 30;
var _ARC_CENTRE_IDX = Math.floor(_TUNER_PT_LED_COUNT / 2); // 5
function _arcPoint(i, r) {
var angleDeg = 180 - i * (180 / (_TUNER_PT_LED_COUNT - 1));
var rad = angleDeg * Math.PI / 180;
return {
x: _ARC_CX + r * Math.cos(rad),
y: _ARC_CY - r * _ARC_ASPECT * Math.sin(rad),
};
}
// ── 1. LED arc ────────────────────────────────────────────────
var leds = [];
for (var i = 0; i < _TUNER_PT_LED_COUNT; i++) {
var pt = _arcPoint(i, _ARC_R_LEDS);
var led = document.createElement('div');
led.style.position = 'absolute';
led.style.left = pt.x.toFixed(2) + '%';
led.style.top = pt.y.toFixed(2) + '%';
led.style.transform = 'translate(-50%, -50%)';
led.style.width = '5%';
led.style.aspectRatio = '1 / 1';
led.style.borderRadius = '50%';
var isCentre = (i === _ARC_CENTRE_IDX);
led.style.background = isCentre
? 'radial-gradient(circle at 35% 35%, #3a0000, #1a0000)'
: 'radial-gradient(circle at 35% 35%, #2a2000, #141000)';
led.style.border = '1px solid ' + (isCentre ? '#400' : '#420');
led.style.boxShadow = 'none';
face.appendChild(led);
leds.push(led);
}
// ── 2. White separator arc (SVG) ──────────────────────────────
// Semicircle from (cxr, cy) to (cx+r, cy) through the top.
// viewBox "0 0 100 100" fills the square face exactly; rx=ry gives a
// true circle. sweep=1 (clockwise in SVG y-down space) draws upward.
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 100 100');
svg.setAttribute('preserveAspectRatio', 'none');
svg.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;overflow:visible';
// SVG viewBox 0 0 100 100, preserveAspectRatio=none:
// x-unit = 1% face-width, y-unit = 1% face-height.
// For a physical circle: rx=r, ry=r×A (corrects non-square face).
var x0Line = _ARC_CX - _ARC_R_LINE;
var x1Line = _ARC_CX + _ARC_R_LINE;
var ryLine = (_ARC_R_LINE * _ARC_ASPECT).toFixed(3);
var arcPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
arcPath.setAttribute('d',
'M ' + x0Line + ',' + _ARC_CY.toFixed(3) +
' A ' + _ARC_R_LINE + ',' + ryLine + ' 0 0 1 ' +
x1Line + ',' + _ARC_CY.toFixed(3));
arcPath.setAttribute('stroke', 'rgba(255,255,255,0.65)');
arcPath.setAttribute('stroke-width', '0.8');
arcPath.setAttribute('fill', 'none');
svg.appendChild(arcPath);
face.appendChild(svg);
// ── 3. Range labels ───────────────────────────────────────────
var labelDefs = [
{ text: '-50', i: 0 },
{ text: '0', i: _ARC_CENTRE_IDX },
{ text: '+50', i: _TUNER_PT_LED_COUNT - 1 },
];
labelDefs.forEach(function (d) {
var pt = _arcPoint(d.i, _ARC_R_LABELS);
var el = document.createElement('div');
el.style.position = 'absolute';
el.style.left = pt.x.toFixed(2) + '%';
el.style.top = pt.y.toFixed(2) + '%';
el.style.transform = 'translate(-50%, -50%)';
el.style.color = '#cccccc';
el.style.fontSize = '50%';
el.style.fontWeight = 'bold';
el.style.fontFamily = 'sans-serif';
el.style.lineHeight = '1';
el.textContent = d.text;
face.appendChild(el);
});
// ── 4. LCD display (letter + # inside one box) ────────────────
// top=50%, height=35% → bottom=85%, centre=67.5%.
var displayWrap = document.createElement('div');
displayWrap.style.cssText = [
'position:absolute',
'left:50%',
'top:50%',
'transform:translateX(-50%)',
'width:23%',
'height:35%',
'background:' + _TUNER_PT_BG,
'border-radius:3px',
'border:1px solid #2a0000',
'display:flex',
'flex-direction:row',
'align-items:center',
'justify-content:center',
'padding:4%',
'box-sizing:border-box',
'box-shadow:inset 0 0 8px #000'
].join(';');
face.appendChild(displayWrap);
// Letter digit (8-segment SVG, viewBox 100×200)
// T=16, G=5, CH=6, mid-gap=3 — thicker segs, uniform 5-unit gaps
// horiz: (xL+CH,y0),(xR-CH,y0),(xR,y0+T/2),(xR-CH,y1),(xL+CH,y1),(xL,y0+T/2)
// vert: (x+T/2,y0),(x+T,y0+CH),(x+T,y1-CH),(x+T/2,y1),(x,y1-CH),(x,y0+CH)
var segSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
segSvg.setAttribute('viewBox', '0 0 100 200');
segSvg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
segSvg.style.cssText = 'width:55%;aspect-ratio:1/2;flex-shrink:0;overflow:visible;';
displayWrap.appendChild(segSvg);
var segmentEls = {};
function _makeSeg(key, points) {
var el = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
el.setAttribute('points', points);
el.setAttribute('fill', _TUNER_PT_UNLIT);
segSvg.appendChild(el);
segmentEls[key] = el;
}
_makeSeg('a', '11,5 89,5 95,13 89,21 11,21 5,13');
_makeSeg('b', '87,26 95,32 95,81 87,87 79,81 79,32');
_makeSeg('c', '87,113 95,119 95,168 87,174 79,168 79,119');
_makeSeg('d', '11,179 89,179 95,187 89,195 11,195 5,187');
_makeSeg('e', '13,113 21,119 21,168 13,174 5,168 5,119');
_makeSeg('f', '13,26 21,32 21,81 13,87 5,81 5,32');
_makeSeg('g1', '11,92 42.5,92 48.5,100 42.5,108 11,108 5,100');
_makeSeg('g2', '57.5,92 89,92 95,100 89,108 57.5,108 51.5,100');
// "#" symbol — absolute-positioned bottom-right, viewBox 90×90 (symbol fills it)
// T=10, s=(90-20)/3=23.3 → bars and gaps evenly distributed
var sharpSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
sharpSvg.setAttribute('viewBox', '0 0 90 90');
sharpSvg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
sharpSvg.style.cssText = 'position:absolute;top:54%;right:6%;width:22%;aspect-ratio:1/1;overflow:visible;pointer-events:none;';
displayWrap.appendChild(sharpSvg);
var sharpParts = [];
function _makeSharpPoly(points) {
var el = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
el.setAttribute('points', points);
el.setAttribute('fill', _TUNER_PT_UNLIT);
sharpSvg.appendChild(el);
sharpParts.push(el);
}
// left vert, right vert, top horiz, bottom horiz — all 90 units, T=10, CH=4, s=23.3
_makeSharpPoly('28.3,0 33.3,4 33.3,86 28.3,90 23.3,86 23.3,4');
_makeSharpPoly('61.7,0 66.7,4 66.7,86 61.7,90 56.7,86 56.7,4');
_makeSharpPoly('4,23.3 86,23.3 90,28.3 86,33.3 4,33.3 0,28.3');
_makeSharpPoly('4,56.7 86,56.7 90,61.7 86,66.7 4,66.7 0,61.7');
// "♭" symbol — same position as sharpSvg, shown in place of it for flat notes
var flatSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
flatSvg.setAttribute('viewBox', '0 0 90 90');
flatSvg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
flatSvg.style.cssText = 'position:absolute;top:54%;right:6%;width:22%;aspect-ratio:1/1;overflow:visible;pointer-events:none;display:none;';
displayWrap.appendChild(flatSvg);
var flatText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
flatText.setAttribute('x', '45');
flatText.setAttribute('y', '82');
flatText.setAttribute('text-anchor', 'middle');
flatText.setAttribute('font-size', '85');
flatText.setAttribute('font-family', 'Georgia, serif');
flatText.setAttribute('fill', _TUNER_PT_UNLIT);
flatText.textContent = '♭';
flatSvg.appendChild(flatText);
// ── 5. AUTO LED (lit when mode is 'free' or 'auto') ──────────
// Anchored to the display's right edge (≈69%) at the display's
// vertical midpoint (54% + 20% = 74%).
var autoWrap = document.createElement('div');
autoWrap.style.cssText = 'position:absolute;left:85%;top:92%;transform:translateY(-50%);display:flex;flex-direction:column;align-items:center;gap:4%;pointer-events:none';
var autoLed = document.createElement('div');
autoLed.style.cssText = [
'width:6%',
'aspect-ratio:1/1',
'border-radius:50%',
'background:radial-gradient(circle at 35% 35%, #3a0000, #1a0000)',
'box-shadow:none',
'border:1px solid #400',
'flex-shrink:0'
].join(';');
var autoLabel = document.createElement('span');
autoLabel.style.cssText = 'color:#cccccc;font-size:52%;font-weight:bold;letter-spacing:0.05em;font-family:sans-serif;';
autoLabel.textContent = 'AUTO';
autoWrap.appendChild(autoLed);
autoWrap.appendChild(autoLabel);
face.appendChild(autoWrap);
// ── Brand label ───────────────────────────────────────────────
var brandLabel = document.createElement('div');
brandLabel.style.cssText = [
'position:absolute',
'bottom:4%',
'left:50%',
'transform:translateX(-50%)',
'color:#cccccc',
'font-size:60%',
'font-weight:bold',
'letter-spacing:0.1em',
'font-family:sans-serif',
'pointer-events:none'
].join(';');
brandLabel.textContent = 'PP-Tiny';
face.appendChild(brandLabel);
container.appendChild(panel);
// ── Helpers ───────────────────────────────────────────────────
function _setLed(index, lit) {
var led = leds[index];
var isCentre = (index === _ARC_CENTRE_IDX);
if (lit) {
if (isCentre) {
led.style.background = 'radial-gradient(circle at 35% 35%, #ff6644, #cc1100)';
led.style.boxShadow = '0 0 5px 2px #ff3300, 0 0 10px 4px #aa1100';
led.style.border = '1px solid #ff4400';
} else {
led.style.background = 'radial-gradient(circle at 35% 35%, #ffee66, #cc9900)';
led.style.boxShadow = '0 0 5px 2px #ffcc00, 0 0 10px 4px #aa8800';
led.style.border = '1px solid #ffbb00';
}
} else {
if (isCentre) {
led.style.background = 'radial-gradient(circle at 35% 35%, #3a0000, #1a0000)';
led.style.boxShadow = 'none';
led.style.border = '1px solid #400';
} else {
led.style.background = 'radial-gradient(circle at 35% 35%, #2a2000, #141000)';
led.style.boxShadow = 'none';
led.style.border = '1px solid #420';
}
}
}
function _updateLeds(cents, hasSignal) {
if (!hasSignal) {
for (var i = 0; i < _TUNER_PT_LED_COUNT; i++) _setLed(i, false);
return;
}
var c = Math.max(-_TUNER_PT_CENTS_RANGE, Math.min(_TUNER_PT_CENTS_RANGE, cents));
var targetIdx = _ARC_CENTRE_IDX + Math.round(c / 10);
targetIdx = Math.max(0, Math.min(_TUNER_PT_LED_COUNT - 1, targetIdx));
for (var j = 0; j < _TUNER_PT_LED_COUNT; j++) {
var lit;
if (c >= 0) {
lit = (j >= _ARC_CENTRE_IDX && j <= targetIdx);
} else {
lit = (j <= _ARC_CENTRE_IDX && j >= targetIdx);
}
_setLed(j, lit);
}
}
var _segKeys = ['a', 'b', 'c', 'd', 'e', 'f', 'g1', 'g2'];
function _setSegment(segEl, lit) {
segEl.setAttribute('fill', lit ? _TUNER_PT_LIT : _TUNER_PT_UNLIT);
segEl.style.filter = lit ? 'drop-shadow(0 0 2px #ff4400) drop-shadow(0 0 5px #cc1100)' : 'none';
}
function _renderNote(letter) {
var map = _TUNER_PT_SEGMENTS[letter ? letter.toUpperCase() : ' '] || _TUNER_PT_SEGMENTS[' '];
for (var k = 0; k < _segKeys.length; k++) {
_setSegment(segmentEls[_segKeys[k]], map[k]);
}
}
function _setSharp(lit) {
var fill = lit ? _TUNER_PT_LIT : _TUNER_PT_UNLIT;
var filter = lit ? 'drop-shadow(0 0 2px #ff4400) drop-shadow(0 0 5px #cc1100)' : 'none';
for (var si = 0; si < sharpParts.length; si++) {
sharpParts[si].setAttribute('fill', fill);
sharpParts[si].style.filter = filter;
}
}
function _setFlat(lit) {
flatSvg.style.display = lit ? '' : 'none';
if (lit) {
flatText.setAttribute('fill', _TUNER_PT_LIT);
flatText.style.filter = 'drop-shadow(0 0 2px #ff4400) drop-shadow(0 0 5px #cc1100)';
}
}
function _setAuto(mode) {
var lit = (mode === 'free' || mode === 'auto');
autoLed.style.background = lit
? 'radial-gradient(circle at 35% 35%, #ff6644, #cc1100)'
: 'radial-gradient(circle at 35% 35%, #3a0000, #1a0000)';
autoLed.style.boxShadow = lit ? '0 0 5px 1px #ff3300, 0 0 10px 2px #aa1100' : 'none';
autoLed.style.border = lit ? '1px solid #ff2200' : '1px solid #400';
}
// ── Public API ────────────────────────────────────────────────
function update(note, cents, freq, mode) {
_setAuto(mode);
if (note === null) {
_updateLeds(0, false);
_renderNote(' ');
sharpSvg.style.display = '';
_setSharp(false);
_setFlat(false);
return;
}
var letter = note[0];
var acc = note.length > 1 ? note[1] : '';
_updateLeds(cents, true);
_renderNote(letter);
if (acc === '#') {
sharpSvg.style.display = '';
_setSharp(true);
_setFlat(false);
} else if (acc === 'b') {
sharpSvg.style.display = 'none';
_setFlat(true);
} else {
sharpSvg.style.display = '';
_setSharp(false);
_setFlat(false);
}
}
function destroy() {
panel.remove();
}
return { update: update, destroy: destroy };
};
}());
+199
View File
@@ -0,0 +1,199 @@
/**
* Strobe tuner visualization for the Slopsmith tuner plugin.
*
* Contract: window._tunerViz_strobe(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)
*/
window._tunerViz_strobe = function (container) {
'use strict';
// ── LCD segment map ───────────────────────────────────────────────
const _SEGMENT_MAP = {
'A': [1,1,1,1,0,0,1,1,1,1,0,0,0,0,0,0],
'B': [1,1,1,1,1,1,0,0,0,1,1,1,0,0,0,0],
'C': [1,1,0,0,1,1,1,1,0,0,0,0,0,0,0,0],
'D': [1,1,1,1,1,1,0,0,0,0,1,1,0,0,0,0],
'E': [1,1,0,0,1,1,1,1,1,0,0,0,0,0,0,0],
'F': [1,1,0,0,0,0,1,1,1,0,0,0,0,0,0,0],
'G': [1,1,0,1,1,1,1,1,0,1,0,0,0,0,0,0],
'-': [0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0],
' ': [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
};
// Subtle glow for lit LCD elements; currentColor matches each element's own color
const _LIT_GLOW = 'drop-shadow(0 0 4px currentColor)';
function _createLCDDigit() {
const digit = document.createElement('div');
digit.className = 'segment-digit relative w-12 h-16 flex-shrink-0';
// Glow rides on the rendered segments; unlit ones (opacity 0.05) stay dark
digit.style.filter = _LIT_GLOW;
const seg = 'absolute bg-current transition-opacity duration-150 rounded-sm';
digit.innerHTML = `
<div class="${seg} top-0 left-0.5 w-[calc(50%-1px)] h-2 rounded-tl-md" data-seg="0"></div>
<div class="${seg} top-0 right-0.5 w-[calc(50%-1px)] h-2 rounded-tr-md" data-seg="1"></div>
<div class="${seg} bottom-0 right-0.5 w-[calc(50%-1px)] h-2 rounded-br-md" data-seg="4"></div>
<div class="${seg} bottom-0 left-0.5 w-[calc(50%-1px)] h-2 rounded-bl-md" data-seg="5"></div>
<div class="${seg} top-1 left-0 w-2 h-[calc(50%-1.5px)]" data-seg="7"></div>
<div class="${seg} bottom-1 left-0 w-2 h-[calc(50%-1.5px)]" data-seg="6"></div>
<div class="${seg} top-1 right-0 w-2 h-[calc(50%-1.5px)]" data-seg="2"></div>
<div class="${seg} bottom-1 right-0 w-2 h-[calc(50%-1.5px)]" data-seg="3"></div>
<div class="${seg} top-1/2 left-1.5 w-[calc(50%-2px)] h-2 -translate-y-1/2" data-seg="8"></div>
<div class="${seg} top-1/2 right-1.5 w-[calc(50%-2px)] h-2 -translate-y-1/2" data-seg="9"></div>
<div class="${seg} top-1.5 left-1/2 w-2 h-[calc(50%-2.5px)] -translate-x-1/2" data-seg="10"></div>
<div class="${seg} bottom-1.5 left-1/2 w-2 h-[calc(50%-2.5px)] -translate-x-1/2" data-seg="11"></div>
<svg class="absolute inset-0 w-full h-full pointer-events-none overflow-visible" viewBox="0 0 48 64">
<line x1="10" y1="10" x2="22" y2="30" stroke="currentColor" stroke-width="6" stroke-linecap="round" class="transition-opacity duration-150" style="opacity:0.05" data-seg="12"/>
<line x1="38" y1="10" x2="26" y2="30" stroke="currentColor" stroke-width="6" stroke-linecap="round" class="transition-opacity duration-150" style="opacity:0.05" data-seg="13"/>
<line x1="10" y1="54" x2="22" y2="34" stroke="currentColor" stroke-width="6" stroke-linecap="round" class="transition-opacity duration-150" style="opacity:0.05" data-seg="14"/>
<line x1="38" y1="54" x2="26" y2="34" stroke="currentColor" stroke-width="6" stroke-linecap="round" class="transition-opacity duration-150" style="opacity:0.05" data-seg="15"/>
</svg>
`;
return digit;
}
function _updateSegmentDigit(el, char) {
const active = _SEGMENT_MAP[char.toUpperCase()] || _SEGMENT_MAP[' '];
el.querySelectorAll('[data-seg]').forEach((s) => {
s.style.opacity = active[parseInt(s.dataset.seg)] ? '1' : '0.05';
});
}
// ── DOM ─────────────────────────────────────────────────────────
const wrap = document.createElement('div');
wrap.className = 'w-full h-32 bg-dark-900 border border-gray-800 rounded-lg relative overflow-hidden mb-3 flex flex-col items-center justify-end pb-4';
const noteSegmented = document.createElement('div');
noteSegmented.className = 'flex items-center justify-center gap-2 text-accent z-30';
const spacer = document.createElement('div');
spacer.className = 'w-8 flex-shrink-0';
noteSegmented.appendChild(spacer);
const digit = _createLCDDigit();
noteSegmented.appendChild(digit);
const sharp = document.createElement('div');
sharp.className = 'relative w-8 h-16 flex-shrink-0';
sharp.innerHTML = `
<div class="sharp-segments absolute inset-0 opacity-5 transition-opacity duration-150">
<div class="absolute top-[30%] left-0 right-0 h-2.5 bg-current -rotate-12 rounded-full"></div>
<div class="absolute bottom-[30%] left-0 right-0 h-2.5 bg-current -rotate-12 rounded-full"></div>
<div class="absolute top-0 bottom-0 left-[30%] w-2.5 bg-current rotate-12 rounded-full"></div>
<div class="absolute top-0 bottom-0 right-[30%] w-2.5 bg-current rotate-12 rounded-full"></div>
</div>
`;
const sharpSegsEl = sharp.querySelector('.sharp-segments');
const flatSegEl = document.createElement('div');
flatSegEl.style.cssText = 'position:absolute;inset:0;display:none;align-items:flex-end;justify-content:center;font-size:2.4rem;font-weight:900;line-height:1;color:currentColor;transition:opacity 0.15s;';
flatSegEl.textContent = '♭';
sharp.appendChild(flatSegEl);
noteSegmented.appendChild(sharp);
wrap.appendChild(noteSegmented);
const scanlines = document.createElement('div');
scanlines.className = 'absolute inset-0 z-40 pointer-events-none opacity-20';
scanlines.style.backgroundImage = 'repeating-linear-gradient(90deg,#000 0px,#000 1px,transparent 1px,transparent 3px),repeating-linear-gradient(0deg,#000 0px,#000 1px,transparent 1px,transparent 3px)';
wrap.appendChild(scanlines);
const stripeGrad = 'linear-gradient(90deg,currentColor 0%,currentColor 45%,transparent 45%,transparent 100%)';
const strobeEl = document.createElement('div');
strobeEl.className = 'absolute top-0 left-0 w-full h-full text-accent';
strobeEl.style.cssText = `transition:opacity 0.3s ease,filter 0.3s ease;background-image:${stripeGrad},${stripeGrad};background-size:40px 15%,80px 15%;background-position:0 5%,0 21%;background-repeat:repeat-x;opacity:0`;
// Strobe glow scales with its brightness state (set in update); none while hidden
const _STROBE_GLOW_IN_TUNE = 'drop-shadow(0 0 3px currentColor)';
const _STROBE_GLOW_OUT = 'drop-shadow(0 0 1px currentColor)';
wrap.appendChild(strobeEl);
container.appendChild(wrap);
// ── State ─────────────────────────────────────────────────────────
let strobePhase = 0;
let smoothedCents = 0;
let currentCents = 0;
let strobeActive = false;
let lastAnimateTime = performance.now();
let lastSignalTime = performance.now();
let rafId = null;
function _animate() {
const now = performance.now();
let dt = (now - lastAnimateTime) / 1000;
if (dt > 0.1) dt = 0.016;
lastAnimateTime = now;
const lerpFactor = 1 - Math.exp(-10 * dt);
smoothedCents = smoothedCents * (1 - lerpFactor) + currentCents * lerpFactor;
const signalTimeout = (now - lastSignalTime) > 1000;
if (strobeActive && !signalTimeout) {
const absCents = Math.min(100, Math.abs(smoothedCents));
const maxSpeed = 2500;
const base = 10;
let speed = maxSpeed * (Math.pow(base, absCents / 100) - 1) / (base - 1);
if (smoothedCents < 0) speed = -speed;
strobePhase = ((strobePhase + speed * dt) % 80 + 80) % 80;
strobeEl.style.backgroundPosition = `${strobePhase}px 5%,${strobePhase}px 21%`;
} else if (signalTimeout && strobeActive) {
strobeActive = false;
strobeEl.style.opacity = '0';
}
rafId = requestAnimationFrame(_animate);
}
rafId = requestAnimationFrame(_animate);
// ── Public API ────────────────────────────────────────────────────
function _setAccidental(acc) {
if (acc === '#') {
sharpSegsEl.style.opacity = '1';
sharpSegsEl.style.filter = _LIT_GLOW;
flatSegEl.style.display = 'none';
} else if (acc === 'b') {
sharpSegsEl.style.opacity = '0.05';
sharpSegsEl.style.filter = 'none';
flatSegEl.style.display = 'flex';
flatSegEl.style.filter = _LIT_GLOW;
} else {
sharpSegsEl.style.opacity = '0.05';
sharpSegsEl.style.filter = 'none';
flatSegEl.style.display = 'none';
}
}
function update(note, cents, freq) {
if (note === null) {
strobeActive = false;
strobeEl.style.opacity = '0';
_updateSegmentDigit(digit, '-');
_setAccidental('');
currentCents = 0;
return;
}
lastSignalTime = performance.now();
strobeActive = true;
currentCents = cents;
_updateSegmentDigit(digit, note[0]);
_setAccidental(note.length > 1 ? note[1] : '');
strobeEl.style.backgroundImage = `${stripeGrad},${stripeGrad}`;
strobeEl.style.backgroundSize = '40px 15%,80px 15%';
strobeEl.style.backgroundPosition = `${strobePhase}px 5%,${strobePhase}px 21%`;
const inTune = Math.abs(cents) < 5;
strobeEl.style.opacity = inTune ? '1' : '0.6';
strobeEl.style.filter = inTune ? _STROBE_GLOW_IN_TUNE : _STROBE_GLOW_OUT;
}
function destroy() {
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
wrap.remove();
}
return { update, destroy };
};
+145
View File
@@ -0,0 +1,145 @@
/**
* Toilet Tuner visualization for the Slopsmith tuner plugin.
*
* Bathroom scene background; plunger slides left/right over the bowl based on
* cents deviation; dips into bowl when in tune (±2 cents); wall calendar shows
* the detected note name.
*
* Contract: window['_tunerViz_toilet-tuner'](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)
*/
(function () {
'use strict';
// ── Constants ─────────────────────────────────────────────────────
var _TUNER_TT_IN_TUNE_THR = 2;
var _TUNER_TT_ASSET_BASE = '/api/plugins/tuner/viz-assets/';
// Positions derived from Bathroom.svg 0-1024 coordinate space.
// Bowl ellipse centre: x=512 (50%), y=673 (65.7%), semi-major=99 (9.7%).
// Plunger SVG is 29.8mm wide x 69.5mm tall (ratio 1:2.33).
// At width=8%, rendered height = 8% x 2.33 = 18.6%.
// Raised: cup bottom at ~62% (above bowl top) → top = 62 - 18.6 = 43%.
// Dipped: cup inside bowl → top = 52%.
var _TUNER_TT_LEFT_PCT = 15; // x at cents=-50
var _TUNER_TT_RIGHT_PCT = 85; // x at cents=+50
var _TUNER_TT_CENTRE_PCT = 50; // x at cents=0 (bowl centre)
var _TUNER_TT_RAISED_TOP = 41; // plunger top % when hovering above bowl
var _TUNER_TT_DIPPED_TOP = 52; // plunger top % when cup inside bowl
window['_tunerViz_toilet-tuner'] = function (container) {
'use strict';
// ── Root panel — 1:1 square, full width ───────────────────────
// padding-bottom: 100% trick: reliable square even with all-absolute children.
// Background loaded as CSS background-image: bypasses browser intrinsic-size
// limits that cause SVGs with huge explicit width/height to fail as <img>.
var panel = document.createElement('div');
panel.className = 'relative w-full overflow-hidden select-none';
panel.style.height = '0';
panel.style.paddingBottom = '100%';
panel.style.backgroundImage = "url('" + _TUNER_TT_ASSET_BASE + "Bathroom.svg')";
panel.style.backgroundSize = 'cover';
panel.style.backgroundPosition = 'center';
// ── Note label (over calendar on wall) ────────────────────────
var noteEl = document.createElement('div');
noteEl.className = 'absolute font-bold pointer-events-none';
noteEl.style.right = '17.25%';
noteEl.style.top = '17%';
noteEl.style.fontSize = '1.6rem';
noteEl.style.color = '#303332';
noteEl.style.textAlign = 'center';
noteEl.style.transform = 'translateX(50%)';
noteEl.textContent = '';
panel.appendChild(noteEl);
// ── Plunger ───────────────────────────────────────────────────
var plungerEl = document.createElement('img');
plungerEl.src = _TUNER_TT_ASSET_BASE + 'Plunger.svg';
plungerEl.className = 'absolute pointer-events-none';
plungerEl.style.width = '10%';
plungerEl.style.left = _TUNER_TT_CENTRE_PCT + '%';
plungerEl.style.top = _TUNER_TT_RAISED_TOP + '%';
plungerEl.style.transform = 'translateX(-50%)';
panel.appendChild(plungerEl);
// ── Toilet bowl overlay (hides plunger cup when dipped) ───────
var bowlEl = document.createElement('img');
bowlEl.src = _TUNER_TT_ASSET_BASE + 'Toiletbowl.svg';
bowlEl.className = 'absolute pointer-events-none';
// Bowl overlay aligned via shared path124 (lower bowl body) registration:
// Bathroom path102 ellipse centre: x=50%, y=65.8%; width=19.4% of panel.
// Toiletbowl path102 same ellipse: centre at y=0 (top of viewBox), rx=35.4% of viewBox.
// → width = 19.4% / 70.7% = 27.4%; left = 50% - 27.4%/2 = 36.3%; top = 65.8%.
// Verified: Toiletbowl path124 at y=60.93% × height(25.2%) + 65.8% = 81.2% = Bathroom path124 ✓
bowlEl.style.left = '36.3%';
bowlEl.style.top = '65.8%';
bowlEl.style.width = '27.4%';
panel.appendChild(bowlEl);
container.appendChild(panel);
// ── State ─────────────────────────────────────────────────────
var _rafId = null;
var _currentNote = null;
var _currentCents = 0;
var _plungerDipped = false;
var _lastTime = null;
var _leftPct = _TUNER_TT_CENTRE_PCT;
var _topPct = _TUNER_TT_RAISED_TOP;
// ── Animation loop ────────────────────────────────────────────
function _animate(now) {
var dt = Math.min(((now - (_lastTime || now)) / 1000), 0.1);
_lastTime = now;
var inTune = _currentNote !== null && Math.abs(_currentCents) <= _TUNER_TT_IN_TUNE_THR;
var targetLeft = _currentNote === null
? _TUNER_TT_CENTRE_PCT
: Math.min(_TUNER_TT_RIGHT_PCT, Math.max(_TUNER_TT_LEFT_PCT,
_TUNER_TT_CENTRE_PCT + (_currentCents / 50) * (_TUNER_TT_RIGHT_PCT - _TUNER_TT_CENTRE_PCT)));
if (inTune && !_plungerDipped) {
_leftPct = _TUNER_TT_CENTRE_PCT;
_topPct = _TUNER_TT_DIPPED_TOP;
_plungerDipped = true;
noteEl.textContent = '💩';
} else if (!inTune && _plungerDipped) {
_topPct = _TUNER_TT_RAISED_TOP;
_plungerDipped = false;
noteEl.textContent = _currentNote || '';
}
if (!_plungerDipped) {
_leftPct += (targetLeft - _leftPct) * 8 * dt;
}
plungerEl.style.left = _leftPct.toFixed(2) + '%';
plungerEl.style.top = _topPct.toFixed(2) + '%';
_rafId = requestAnimationFrame(_animate);
}
// ── Public API ────────────────────────────────────────────────
function update(note, cents, freq) {
_currentNote = note;
_currentCents = note === null ? 0 : cents;
if (!_plungerDipped) { noteEl.textContent = note || ''; }
}
function destroy() {
if (_rafId) { cancelAnimationFrame(_rafId); _rafId = null; }
panel.remove();
}
_rafId = requestAnimationFrame(_animate);
return { update: update, destroy: destroy };
};
})();