212 lines
8.1 KiB
JavaScript
212 lines
8.1 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');
|
|
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);
|
|
};
|