Files

224 lines
8.5 KiB
JavaScript

/* ================================================================== */
/* 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);
};