mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
* Update GitHub repo references from feedback* to feedBack* * rename: slopsmith -> feedBack, byron -> got-feedBack Renames across the entire codebase: - slopsmith/Slopsmith/SLOPSMITH/SlopSmith -> feedBack/FeedBack/FEEDBACK/FeedBack - byron/Byron/Byrongamatos -> got-feedBack/got-feedBack/got-feedBack - /home/byron/ -> /opt/got-feedBack/ - byron@ougsoft.com -> hi@got-feedBack.org - github.com/byrongamatos/ -> github.com/got-feedback/ - com.byron. -> com.got-feedback. - SLOPSMITH_ env vars -> FEEDBACK_ with backward-compat fallback - Protocol/storage strings migrated with read-old/write-new pattern - window.slopsmith JS API -> window.feedBack (canonical) + backward-compat alias Refs: #rename-slopsmith * rename: complete regen against current main + fix backward-compat alias Regenerated the slopsmith->feedBack / byron->got-feedBack rename on top of current main (3 commits had landed since the branch: #572/#554/#574), resolving the four content conflicts in favour of main's newer content (autoplay/auto-exit, accuracy-badge, Virtuoso re-home, feedpak badge). Completion fixes on top of the mechanical rename: - Re-apply rename to post-branch content the original rename never saw: window.slopsmith(.Tour) consumers in lessons.js / notifications.js / onboarding-tour.js, and the matching JS + python tests (autoplay_exit, progression_*, test_feedpak_extension FEEDBACK_* env vars). The test env vars now match server.py (which reads FEEDBACK_SYNC_STARTUP / FEEDBACK_SKIP_STARTUP_TASKS), so the sync-startup test exercises the real path again. - Restore the window.slopsmith backward-compat alias dropped during conflict resolution, and move the bus aliases to AFTER the _feedBackExisting merge block so they reference the fully-assembled object (also fixes the loop_api.test.js API-surface regex, which the original PR latently broke). - Drop the stray empty data/web_library.db (runtime DB lives in CONFIG_DIR) and gitignore it. - Fix stale tone-source test: feed[dB]ack -> fee[dB]ack to match shipped source labels. Verified locally (org CI billing-blocked): JS 819/819 pass; pytest 1669 passed / 1683 collected with 0 import errors; zero residual slopsmith/byron except the two intentional window.slopsmith aliases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rename: implement advertised backward-compat + prune dead community plugins Address gaps where PR #537's "Backward compatibility" section was advertised but not implemented, and clean up the community plugin list. Env vars (FEEDBACK_* canonical, legacy SLOPSMITH_* honoured): - New lib/env_compat.py (getenv_compat / env_flag_compat) + tests. server.py (_env_flag + all FEEDBACK_* reads), diagnostics_hardware, gp2midi and tailwind_rebuild now resolve the legacy alias, so existing SLOPSMITH_UI / SLOPSMITH_PLUGINS_DIR / etc. deployments keep working. - Fix the rename collapsing plugins/__init__.py and minigames/routes.py from `FEEDBACK_PLUGINS_DIR or SLOPSMITH_PLUGINS_DIR` into a redundant `FEEDBACK_ or FEEDBACK_` (the fallback was silently lost). Storage (app.js update-channel): - Read feedBack-update-channel, fall back to legacy slopsmith-update-channel, and clear the legacy key on write — so a user's update-channel preference survives the rename instead of resetting to "stable". Community plugin list (README): the rename rewrote third-party repo URLs we don't own. Probed every one; their owners never renamed, so: - Restore the 13 live community plugins to their real slopsmith-* names. - Prune 6 that are 404 to the public (topkoa splitscreen/stems, OmikronApex tuner, Jafz2001 nam-rig-builder, DeathlySin song-preview, Erikcb91 shuffle). - Fix a pre-existing Guitar Theory clone-command typo (nam-tone -> guitar-theory). Verified: env_compat 7/7, JS 819/819, pytest 1690 collected / 0 import errors, rename-sensitive + startup suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: byrongamatos <xasiklas@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
389 lines
18 KiB
JavaScript
389 lines
18 KiB
JavaScript
/**
|
||
* Analogue gauge tuner visualization for the FeedBack 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 };
|
||
};
|
||
})();
|