(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 (75−4=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 4–96, y 4–71 (width=92, height=67 in panel units). // Arc centre in panel SVG = (50, 50) → in face-div %: // cx = (50−4)/92×100 = 50 % // cy = (50−4)/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 (cx−r, 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 }; }; }());