/* ================================================================== */ /* Wheel-Builder */ /* viewBox = quadratisch (z. B. -110 -110 220 220) */ /* 24 Segmente à 15°, 12B oben (12 Uhr), clockwise */ /* ================================================================== */ const keys = [ ['12B', 'E', '#00E5FF'], ['1B', 'B', '#5BE8B5'], ['2B', 'F#', '#8AEB86'], ['3B', 'Db', '#B9F062'], ['4B', 'Ab', '#E4DF6A'], ['5B', 'Eb', '#FFA07A'], ['6B', 'Bb', '#FF7582'], ['7B', 'F', '#E85D9E'], ['8B', 'C', '#C86DD7'], ['9B', 'G', '#9F72EE'], ['10B', 'D', '#8B94F8'], ['11B', 'A', '#68B6F9'], ['12A', 'Dbm', '#00E5FF'], ['1A', 'Abm', '#5BE8B5'], ['2A', 'Ebm', '#8AEB86'], ['3A', 'Bbm', '#B9F062'], ['4A', 'Fm', '#E4DF6A'], ['5A', 'Cm', '#FFA07A'], ['6A', 'Gm', '#FF7582'], ['7A', 'Dm', '#E85D9E'], ['8A', 'Am', '#C86DD7'], ['9A', 'Em', '#9F72EE'], ['10A', 'Bm', '#8B94F8'], ['11A', 'F#m', '#68B6F9'], ]; 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'); 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; 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; const segmentG = document.createElementNS(NS, 'g'); segmentG.setAttribute('class', 'wheel-segment'); segmentG.style.cursor = 'pointer'; segmentG.addEventListener('click', (e) => { e.stopPropagation(); if (window.selectKeyFromWheel) { window.selectKeyFromWheel(c, m); } }); segmentG.appendChild(poly); segmentG.appendChild(t1); segmentG.appendChild(t2); svg.appendChild(segmentG); }); // 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); };