/* ================================================================== */ /* Wheel-Builder */ /* viewBox = quadratisch (z. B. -110 -110 220 220) */ /* 24 Segmente à 15°, 12B oben (12 Uhr), clockwise */ /* ================================================================== */ const keys = [ ['12B', 'F', '#2EC4B6'], ['1B', 'C', '#D62828'], ['2B', 'G', '#E77D11'], ['3B', 'D', '#E0B423'], ['4B', 'A', '#4F9D69'], ['5B', 'E', '#1B998B'], ['6B', 'B', '#6B4E9C'], ['7B', 'F#', '#3B7DD8'], ['8B', 'Db', '#3461A8'], ['9B', 'Ab', '#5B3A8C'], ['10B', 'Eb', '#B07ECC'], ['11B', 'Bb', '#D64966'], ['12A', 'Dm', '#2EC4B6'], ['1A', 'Am', '#D62828'], ['2A', 'Em', '#E77D11'], ['3A', 'Bm', '#E0B423'], ['4A', 'F#m', '#4F9D69'], ['5A', 'C#m', '#1B998B'], ['6A', 'G#m', '#6B4E9C'], ['7A', 'D#m', '#3B7DD8'], ['8A', 'Bbm', '#3461A8'], ['9A', 'Fm', '#5B3A8C'], ['10A', 'Cm', '#B07ECC'], ['11A', 'Gm', '#D64966'], ]; const hexRgba = (h, a) => { const r = parseInt(h.slice(1,3),16); const g = parseInt(h.slice(3,5),16); const b = parseInt(h.slice(5,7),16); return `rgba(${r},${g},${b},${a})`; }; function getThemeColors() { const isLight = document.documentElement.getAttribute('data-theme') === 'light'; return { textBright: isLight ? '#000000' : '#FFFFFF', textDim: isLight ? '#7A7A88' : '#9090B0', bg: isLight ? '#F4F4F8' : '#0E0E14', border: isLight ? '#DCDCE4' : '#2A2A36' }; } function buildWheel(svgId, viewBoxSize, currentKey = null, compatIdxs = { primary: new Set(), secondary: new Set() }) { const svg = document.getElementById(svgId); if (!svg) return; svg.innerHTML = ''; const NS = 'http://www.w3.org/2000/svg'; const currentIdx = keys.findIndex(k => k[0] === currentKey); const rOuter = viewBoxSize / 2 - 4; const rMid = rOuter * 0.62; const rInner = rOuter * 0.30; const theme = getThemeColors(); const p = (r, a) => { const rad = a * Math.PI / 180; return [r * Math.sin(rad), -r * Math.cos(rad)]; }; keys.forEach(([c, m, color], i) => { const isMajor = c.endsWith('B'); const isCurrent = i === currentIdx; const isPrimary = compatIdxs.primary.has(i); const isSecondary = compatIdxs.secondary.has(i); const rOut = isMajor ? rOuter : rMid; const rIn = isMajor ? rMid : rInner; // -15 to align 12B/12A so the center of the segment is at 0 degrees (top) const a1 = window.codeToAngle[c] - 15; const a2 = a1 + 30; const aMid = (a1 + a2) / 2; const [x1,y1] = p(rOut, a1); const [x2,y2] = p(rOut, a2); const [x3,y3] = p(rIn, a2); const [x4,y4] = p(rIn, a1); const poly = document.createElementNS(NS, 'polygon'); poly.setAttribute('points', `${x1.toFixed(2)},${y1.toFixed(2)} ` + `${x2.toFixed(2)},${y2.toFixed(2)} ` + `${x3.toFixed(2)},${y3.toFixed(2)} ` + `${x4.toFixed(2)},${y4.toFixed(2)}`); // Better stroke rendering per user request const baseStroke = '#000000'; // According to the chart reference, borders are black or dark if (isCurrent) { poly.setAttribute('fill', window.colorMatrix[c]); poly.setAttribute('stroke', baseStroke); poly.setAttribute('stroke-width', '1.5'); } else if (isPrimary) { poly.setAttribute('fill', hexRgba(window.colorMatrix[c], 0.70)); poly.setAttribute('stroke', baseStroke); poly.setAttribute('stroke-width', '1'); } else if (isSecondary) { poly.setAttribute('fill', hexRgba(window.colorMatrix[c], 0.40)); poly.setAttribute('stroke', baseStroke); poly.setAttribute('stroke-width', '1'); } else { poly.setAttribute('fill', hexRgba(window.colorMatrix[c], 0.15)); poly.setAttribute('stroke', baseStroke); poly.setAttribute('stroke-width', '0.5'); } poly.setAttribute('stroke-linejoin', 'miter'); svg.appendChild(poly); const [lx, ly] = p((rOut + rIn) / 2, aMid); const t1 = document.createElementNS(NS, 'text'); t1.setAttribute('x', lx); t1.setAttribute('y', ly - viewBoxSize/60); t1.setAttribute('text-anchor', 'middle'); t1.setAttribute('dominant-baseline', 'middle'); t1.setAttribute('font-family', 'JetBrains Mono, monospace'); t1.setAttribute('font-size', isMajor ? viewBoxSize/15 : viewBoxSize/22); t1.setAttribute('font-weight', '700'); t1.setAttribute('fill', (isCurrent || isPrimary) ? '#FFFFFF' : theme.textBright); t1.textContent = c; svg.appendChild(t1); const t2 = document.createElementNS(NS, 'text'); t2.setAttribute('x', lx); t2.setAttribute('y', ly + viewBoxSize/26); t2.setAttribute('text-anchor', 'middle'); t2.setAttribute('dominant-baseline', 'middle'); t2.setAttribute('font-family', 'JetBrains Mono, monospace'); t2.setAttribute('font-size', isMajor ? viewBoxSize/26 : viewBoxSize/32); t2.setAttribute('fill', (isCurrent || isPrimary) ? '#EEEEEE' : theme.textDim); t2.textContent = m; svg.appendChild(t2); }); // Outer circle border const outerCircle = document.createElementNS(NS, 'circle'); outerCircle.setAttribute('cx', 0); outerCircle.setAttribute('cy', 0); outerCircle.setAttribute('r', rOuter); outerCircle.setAttribute('fill', 'none'); outerCircle.setAttribute('stroke', '#000000'); outerCircle.setAttribute('stroke-width', '2'); svg.appendChild(outerCircle); // Inner circle border const innerCircle = document.createElementNS(NS, 'circle'); innerCircle.setAttribute('cx', 0); innerCircle.setAttribute('cy', 0); innerCircle.setAttribute('r', rInner); innerCircle.setAttribute('fill', 'none'); innerCircle.setAttribute('stroke', '#000000'); innerCircle.setAttribute('stroke-width', '2'); svg.appendChild(innerCircle); // Center hole background const centerHole = document.createElementNS(NS, 'circle'); centerHole.setAttribute('cx', 0); centerHole.setAttribute('cy', 0); centerHole.setAttribute('r', rInner - 1); // slightly smaller to not overlap border centerHole.setAttribute('fill', theme.bg); svg.appendChild(centerHole); if (currentIdx !== -1) { const cur = keys[currentIdx]; const cy1 = -viewBoxSize/25; // center a bit const c1 = document.createElementNS(NS, 'text'); c1.setAttribute('x', 0); c1.setAttribute('y', cy1); c1.setAttribute('text-anchor', 'middle'); c1.setAttribute('dominant-baseline', 'middle'); c1.setAttribute('font-family', 'JetBrains Mono, monospace'); c1.setAttribute('font-size', viewBoxSize/9); c1.setAttribute('font-weight', '700'); c1.setAttribute('fill', window.colorMatrix[cur[0]]); c1.textContent = cur[0]; svg.appendChild(c1); const c2 = document.createElementNS(NS, 'text'); c2.setAttribute('x', 0); c2.setAttribute('y', cy1 + viewBoxSize/8); c2.setAttribute('text-anchor', 'middle'); c2.setAttribute('dominant-baseline', 'middle'); c2.setAttribute('font-family', 'JetBrains Mono, monospace'); c2.setAttribute('font-size', viewBoxSize/20); c2.setAttribute('fill', theme.textDim); c2.textContent = cur[1]; svg.appendChild(c2); } else { // Empty state const c1 = document.createElementNS(NS, 'text'); c1.setAttribute('x', 0); c1.setAttribute('y', 0); c1.setAttribute('text-anchor', 'middle'); c1.setAttribute('dominant-baseline', 'middle'); c1.setAttribute('font-family', 'JetBrains Mono, monospace'); c1.setAttribute('font-size', viewBoxSize/15); c1.setAttribute('fill', theme.textDim); c1.textContent = "N/A"; svg.appendChild(c1); } } // Global update function for wheel window.updateWheel = function(currentKey) { let compatIdxs = { primary: new Set(), secondary: new Set() }; if (currentKey && window.mixingRules[currentKey]) { window.mixingRules[currentKey].slice(1).forEach((targetKey, i) => { const idx = keys.findIndex(k => k[0] === targetKey); if (idx < 0) return; const compatLevel = i + 1; if (compatLevel === 1 || compatLevel === 2 || compatLevel === 4) { compatIdxs.primary.add(idx); } else { compatIdxs.secondary.add(idx); } }); } buildWheel('wheel-small', 220, currentKey, compatIdxs); };