Files
KeySelector/Idea/KeySelector_Mockup_Wheel.html
T

648 lines
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de" data-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>KeySelector</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* ------------------------------------------------------------------ */
/* THEME */
/* ------------------------------------------------------------------ */
:root, [data-theme="dark"] {
--bg: #0E0E14;
--surface: #16161E;
--surface-2: #1E1E28;
--border: #2A2A36;
--text: #ECECF2;
--text-dim: #7878A0;
--text-bright: #FFFFFF;
--key-current: #4F9D69; /* 4B = A-Dur green */
--bpm: #E0B423;
--match: #4F9D69;
--mic: #D62828;
}
[data-theme="light"] {
--bg: #F4F4F8;
--surface: #FFFFFF;
--surface-2: #ECECF1;
--border: #DCDCE4;
--text: #1A1A22;
--text-dim: #6A6A78;
--text-bright: #000000;
--key-current: #2E7A48;
--bpm: #B88A1A;
--match: #2E7A48;
--mic: #C4202A;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
background: var(--bg);
color: var(--text);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
-webkit-font-smoothing: antialiased;
transition: background-color .18s, color .18s;
}
body { padding: 24px 32px; min-height: 100vh; }
.container { max-width: 1600px; margin: 0 auto; }
/* ------------------------------------------------------------------ */
/* TOPBAR */
/* ------------------------------------------------------------------ */
.topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.brand { font-family: 'JetBrains Mono', monospace; font-size: 12px; font-weight: 700; letter-spacing: 0.3em; }
.topbar-right { display: flex; align-items: center; gap: 18px; }
.status { display: flex; align-items: center; gap: 8px; font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text-dim); letter-spacing: 0.12em; text-transform: uppercase; }
.mic { width: 14px; height: 14px; fill: var(--mic); }
.theme-toggle {
background: none; border: 1px solid var(--border); border-radius: 4px;
padding: 5px 12px; color: var(--text-dim);
font-family: 'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 0.15em; text-transform: uppercase;
cursor: pointer; transition: all .12s;
}
.theme-toggle:hover { color: var(--text); border-color: var(--text-dim); }
/* ------------------------------------------------------------------ */
/* HERO */
/* ------------------------------------------------------------------ */
.hero {
display: grid;
grid-template-columns: 240px 1fr;
gap: 24px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
position: relative;
overflow: hidden;
transition: grid-template-columns .3s ease, padding .3s ease, min-height .3s ease;
min-height: 280px;
}
.hero::before {
content: ''; position: absolute; left: 0; top: 0; bottom: 0;
width: 3px; background: var(--key-current);
transition: background-color .18s;
}
/* WHEEL expanded mode */
.hero.wheel-mode {
grid-template-columns: 1fr;
padding: 48px;
min-height: 600px;
}
.hero.wheel-mode .cover,
.hero.wheel-mode .info-top,
.hero.wheel-mode .info-mid { display: none; }
.hero.wheel-mode .info-bot { padding-top: 0; justify-content: center; }
.hero.wheel-mode .wheel-container { width: 100%; max-width: 520px; height: 520px; }
/* COVER expanded mode */
.hero.cover-mode { grid-template-columns: 1fr; min-height: 540px; }
.hero.cover-mode .info { display: none; }
.hero.cover-mode .cover {
width: 100%; max-width: 480px; margin: 0 auto;
aspect-ratio: 1; max-height: 460px;
}
@media (max-width: 760px) {
.hero { grid-template-columns: 1fr; }
}
.cover {
width: 100%; aspect-ratio: 1; max-height: 240px;
border-radius: 8px; background: var(--surface-2);
overflow: hidden; position: relative; cursor: pointer;
transition: transform .15s;
}
.cover:hover { transform: scale(1.01); }
.cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cover::after {
content: ''; position: absolute; inset: 0;
background: linear-gradient(135deg, rgba(79, 157, 105, 0.20), transparent 60%);
pointer-events: none;
}
.cover-placeholder {
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 8px;
font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--text-dim);
letter-spacing: 0.2em; text-transform: uppercase;
}
.cover-placeholder .note { font-size: 32px; opacity: 0.3; }
.info { display: flex; flex-direction: column; min-width: 0; }
.info-top { display: flex; flex-direction: column; }
.info-mid { display: flex; flex-direction: column; gap: 14px; margin-top: 16px; }
.info-bot { margin-top: auto; padding-top: 20px; display: flex; justify-content: flex-end; align-items: flex-end; min-height: 160px; }
.track-title { font-size: 30px; font-weight: 600; line-height: 1.1; letter-spacing: -0.01em; margin-bottom: 4px; }
.track-artist { font-size: 15px; color: var(--text-dim); }
.stats-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
.stat { background: var(--surface); padding: 12px 14px; }
.stat-label { font-family: 'JetBrains Mono', monospace; font-size: 9px; letter-spacing: 0.22em; text-transform: uppercase; color: var(--text-dim); margin-bottom: 6px; }
.stat-value { font-family: 'JetBrains Mono', monospace; font-size: 22px; font-weight: 700; font-feature-settings: 'tnum'; line-height: 1; }
.stat-value.k { color: var(--key-current); }
.stat-value.b { color: var(--bpm); }
.stat-value.m { color: var(--match); }
.stat-sub { font-size: 10px; color: var(--text-dim); margin-top: 4px; font-family: 'JetBrains Mono', monospace; }
.tags-row { display: flex; flex-wrap: wrap; gap: 6px; }
.tag { padding: 4px 10px; border-radius: 4px; font-size: 11px; font-family: 'JetBrains Mono', monospace; letter-spacing: 0.04em; }
.tag-key { background: var(--key-current); color: #fff; font-weight: 700; }
.tag-genre { background: rgba(90, 138, 229, 0.13); color: #8AA8D8; border: 1px solid rgba(90, 138, 229, 0.32); }
[data-theme="light"] .tag-genre { background: rgba(52, 90, 156, 0.10); color: #345A9C; border-color: rgba(52, 90, 156, 0.28); }
.tag-mood { background: rgba(214, 41, 102, 0.13); color: #D88096; border: 1px solid rgba(214, 41, 102, 0.32); }
[data-theme="light"] .tag-mood { background: rgba(168, 51, 106, 0.10); color: #A8336A; border-color: rgba(168, 51, 106, 0.28); }
/* ------------------------------------------------------------------ */
/* WHEEL */
/* ------------------------------------------------------------------ */
.wheel-container {
width: 140px; height: 140px;
position: relative; cursor: pointer;
transition: transform .15s, width .3s ease, height .3s ease;
}
.wheel-container:hover { transform: scale(1.04); }
.wheel-container svg { width: 100%; height: 100%; display: block; }
.close-btn {
position: absolute; top: 12px; right: 12px;
width: 32px; height: 32px;
background: var(--surface-2); border: 1px solid var(--border);
border-radius: 50%; color: var(--text-dim);
font-size: 18px; cursor: pointer;
display: none; align-items: center; justify-content: center;
line-height: 1; z-index: 5;
transition: color .12s, border-color .12s;
}
.close-btn:hover { color: var(--text); border-color: var(--text-dim); }
.hero.wheel-mode .close-btn,
.hero.cover-mode .close-btn { display: flex; }
/* ------------------------------------------------------------------ */
/* SUGGESTIONS */
/* ------------------------------------------------------------------ */
.section-title {
font-family: 'JetBrains Mono', monospace; font-size: 11px;
letter-spacing: 0.25em; text-transform: uppercase; color: var(--text-dim);
margin: 28px 0 12px;
}
.suggestions { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
.sug {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px; padding: 10px;
display: flex; flex-direction: column; gap: 8px;
cursor: pointer;
transition: border-color .12s, transform .12s;
}
.sug:hover { border-color: var(--text-dim); transform: translateY(-2px); }
.sug-cover { width: 100%; aspect-ratio: 1; border-radius: 4px; background: var(--surface-2); overflow: hidden; position: relative; }
.sug-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sug-cover-placeholder { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 20px; opacity: 0.25; }
.sug-meta { display: flex; justify-content: space-between; align-items: center; }
.sug-key { font-family: 'JetBrains Mono', monospace; font-size: 12px; font-weight: 700; padding: 4px 8px; border-radius: 3px; color: #fff; letter-spacing: 0.04em; }
.sug-bpm { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text-dim); }
.sug-title { font-size: 13px; font-weight: 500; line-height: 1.25; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.sug-artist { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--text-dim); }
.sug-compat {
font-family: 'JetBrains Mono', monospace; font-size: 9px; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--text-dim);
padding-top: 4px; border-top: 1px solid var(--border);
}
</style>
</head>
<body>
<div class="container">
<div class="topbar">
<div class="brand">KeySelector</div>
<div class="topbar-right">
<div class="status">
<svg class="mic" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5z M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
</svg>
<span>Listening</span>
</div>
<button class="theme-toggle" id="theme-btn">Light</button>
</div>
</div>
<div class="hero" id="hero">
<div class="cover" id="cover" onclick="toggleCover()" title="Click to expand">
<div class="cover-placeholder">
<span class="note">♪</span>
<span>No Cover</span>
</div>
</div>
<div class="info">
<div class="info-top">
<div class="track-title">Made in Japan</div>
<div class="track-artist">Alphaville</div>
</div>
<div class="info-mid">
<div class="stats-row">
<div class="stat">
<div class="stat-label">Key</div>
<div class="stat-value k">4B</div>
<div class="stat-sub">A-Dur</div>
</div>
<div class="stat">
<div class="stat-label">BPM</div>
<div class="stat-value b">118</div>
<div class="stat-sub">± 2</div>
</div>
<div class="stat">
<div class="stat-label">Match</div>
<div class="stat-value m">98%</div>
<div class="stat-sub">acoustid</div>
</div>
</div>
<div class="tags-row">
<span class="tag tag-key">4B</span>
<span class="tag tag-genre">Synth-Pop</span>
<span class="tag tag-genre">New Wave</span>
<span class="tag tag-genre">80s</span>
<span class="tag tag-mood">Energetic</span>
<span class="tag tag-mood">Retro</span>
<span class="tag tag-mood">Euphoric</span>
</div>
</div>
<div class="info-bot">
<div class="wheel-container" id="wheel-container" onclick="toggleWheel()" title="Click to expand">
<svg viewBox="-110 -110 220 220" id="wheel-small" preserveAspectRatio="xMidYMid meet"></svg>
</div>
</div>
</div>
<button class="close-btn" id="close-btn" onclick="resetHero()" title="Close">×</button>
</div>
<div class="section-title">Up Next</div>
<div class="suggestions" id="suggestions"></div>
</div>
<script>
/* ================================================================== */
/* Camelot Wheel: 24 keys, clockwise from 12B (top) */
/* Indices 0–11: Major (B), außen. Indices 12–23: Minor (A), innen. */
/* ================================================================== */
const keys = [
/* Major (außen) */ /* color */
['12B', 'F', '#2EC4B6'], /* 0 */
['1B', 'C', '#D62828'], /* 1 */
['2B', 'G', '#E77D11'], /* 2 */
['3B', 'D', '#E0B423'], /* 3 */
['4B', 'A', '#4F9D69'], /* 4 ← CURRENT (A-Dur) */
['5B', 'E', '#1B998B'], /* 5 */
['6B', 'B', '#6B4E9C'], /* 6 */
['7B', 'F#', '#3B7DD8'], /* 7 */
['8B', 'Db', '#3461A8'], /* 8 */
['9B', 'Ab', '#5B3A8C'], /* 9 */
['10B', 'Eb', '#B07ECC'], /* 10 */
['11B', 'Bb', '#D64966'], /* 11 */
/* Minor (innen) — direkt innen neben dem entsprechenden Major */
['12A', 'Dm', '#2EC4B6'], /* 12 */
['1A', 'Am', '#D62828'], /* 13 */
['2A', 'Em', '#E77D11'], /* 14 */
['3A', 'Bm', '#E0B423'], /* 15 */
['4A', 'F#m', '#4F9D69'], /* 16 ← kompatibel (relative Moll) */
['5A', 'C#m', '#1B998B'], /* 17 */
['6A', 'G#m', '#6B4E9C'], /* 18 */
['7A', 'D#m', '#3B7DD8'], /* 19 ← kompatibel (Quinte -1) */
['8A', 'Bbm', '#3461A8'], /* 20 */
['9A', 'Fm', '#5B3A8C'], /* 21 */
['10A', 'Cm', '#B07ECC'], /* 22 */
['11A', 'Gm', '#D64966'], /* 23 ← kompatibel (Quinte +1) */
];
const currentKey = '4B';
const currentIdx = keys.findIndex(k => k[0] === currentKey);
/* ------------------------------------------------------------------ */
/* Camelot-Code → Winkel-Position (12 Uhr = 0°, clockwise) */
/* Major und Minor am GLEICHEN Winkel (12B außen + 12A innen) */
/* ------------------------------------------------------------------ */
const codeToAngle = {
'12B': 0, '12A': 0,
'1B': 15, '1A': 15,
'2B': 30, '2A': 30,
'3B': 45, '3A': 45,
'4B': 60, '4A': 60,
'5B': 75, '5A': 75,
'6B': 90, '6A': 90,
'7B': 105, '7A': 105,
'8B': 120, '8A': 120,
'9B': 135, '9A': 135,
'10B': 150, '10A': 150,
'11B': 165, '11A': 165,
};
/* ------------------------------------------------------------------ */
/* Farb-Matrix: pro Camelot-Code ein Hex-Wert */
/* Ändern hier = globale Farbänderung für alle 24 Segmente */
/* ------------------------------------------------------------------ */
const colorMatrix = {
'12B': '#2EC4B6', '12A': '#2EC4B6',
'1B': '#D62828', '1A': '#D62828',
'2B': '#E77D11', '2A': '#E77D11',
'3B': '#E0B423', '3A': '#E0B423',
'4B': '#4F9D69', '4A': '#4F9D69',
'5B': '#1B998B', '5A': '#1B998B',
'6B': '#6B4E9C', '6A': '#6B4E9C',
'7B': '#3B7DD8', '7A': '#3B7DD8',
'8B': '#3461A8', '8A': '#3461A8',
'9B': '#5B3A8C', '9A': '#5B3A8C',
'10B': '#B07ECC', '10A': '#B07ECC',
'11B': '#D64966', '11A': '#D64966',
};
/* ------------------------------------------------------------------ */
/* Mixed In Key Harmonic Mixing Rules */
/* Pro Key: [Perfect, -1, +1, Energy, Scale, Diagonal, Jaw's, Mood] */
/* ------------------------------------------------------------------ */
const mixingRules = {
'1A': ['1A', '12A', '2A', '3A', '1B', '12B', '8A', '4B' ],
'2A': ['2A', '1A', '3A', '4A', '2B', '1B', '9A', '5B' ],
'3A': ['3A', '2A', '4A', '5A', '3B', '2B', '10A', '6B' ],
'4A': ['4A', '3A', '5A', '6A', '4B', '3B', '11A', '7B' ],
'5A': ['5A', '4A', '6A', '7A', '5B', '4B', '12A', '8B' ],
'6A': ['6A', '5A', '7A', '8A', '6B', '5B', '1A', '9B' ],
'7A': ['7A', '6A', '8A', '9A', '7B', '6B', '2A', '10B'],
'8A': ['8A', '7A', '9A', '10A', '8B', '7B', '3A', '11B'],
'9A': ['9A', '8A', '10A', '11A', '9B', '8B', '4A', '12B'],
'10A': ['10A', '9A', '11A', '12A', '10B', '9B', '5A', '1B' ],
'11A': ['11A', '10A', '12A', '1A', '11B', '10B', '6A', '2B' ],
'12A': ['12A', '11A', '1A', '2A', '12B', '11B', '7A', '3B' ],
'1B': ['1B', '12B', '2B', '3B', '1A', '2A', '8B', '10A'],
'2B': ['2B', '1B', '3B', '4B', '2A', '3A', '9B', '11A'],
'3B': ['3B', '2B', '4B', '5B', '3A', '4A', '10B', '12A'],
'4B': ['4B', '3B', '5B', '6B', '4A', '5A', '11B', '1A' ],
'5B': ['5B', '4B', '6B', '7B', '5A', '6A', '12B', '2A' ],
'6B': ['6B', '5B', '7B', '8B', '6A', '7A', '1B', '3A' ],
'7B': ['7B', '6B', '8B', '9B', '7A', '8A', '2B', '4A' ],
'8B': ['8B', '7B', '9B', '10B', '8A', '9A', '3B', '5A' ],
'9B': ['9B', '8B', '10B', '11B', '9A', '10A', '4B', '6A' ],
'10B': ['10B', '9B', '11B', '12B', '10A', '11A', '5B', '7A' ],
'11B': ['11B', '10B', '12B', '1B', '11A', '12A', '6B', '8A' ],
'12B': ['12B', '11B', '1B', '2B', '12A', '1A', '7B', '9A' ],
};
const compatLabels = ['Perfect', 'Quinte -1', 'Quinte +1', 'Energy', 'Scale', 'Diagonal', "Jaw's", 'Mood'];
/* Strong compat: Quinte ±1 + Scale (relative Moll) */
/* Light compat: Energy + Diagonal + Jaw's + Mood */
const compatIdxs = { primary: new Set(), secondary: new Set() };
mixingRules[currentKey].slice(1).forEach((targetKey, i) => {
const idx = keys.findIndex(k => k[0] === targetKey);
if (idx < 0) return;
const compatLevel = i + 1; // 1=Quinte-1, 2=Quinte+1, 3=Energy, 4=Scale, 5=Diagonal, 6=Jaw's, 7=Mood
if (compatLevel === 1 || compatLevel === 2 || compatLevel === 4) {
compatIdxs.primary.add(idx);
} else {
compatIdxs.secondary.add(idx);
}
});
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',
};
}
/* ================================================================== */
/* Wheel-Builder */
/* viewBox = quadratisch (z. B. -110 -110 220 220) */
/* 24 Segmente à 15°, 12B oben (12 Uhr), clockwise */
/* ================================================================== */
function buildWheel(svgId, viewBoxSize) {
const svg = document.getElementById(svgId);
if (!svg) return;
svg.innerHTML = '';
const NS = 'http://www.w3.org/2000/svg';
const segAngle = 360 / keys.length; // 15°
const rOuter = viewBoxSize / 2 - 4;
const rMid = rOuter * 0.62;
const rInner = rOuter * 0.30;
const theme = getThemeColors();
// Punkt bei Winkel a (von 12 Uhr, im Uhrzeigersinn) und Radius r
// SVG: a=0° = oben (-y), a=90° = rechts (+x)
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;
// Winkel kommt aus dem Camelot-Code (12B+12A am gleichen Winkel)
const a1 = codeToAngle[c];
const a2 = a1 + 15;
const aMid = (a1 + a2) / 2;
// 4 Eckpunkte des Annular-Sektors
const [x1,y1] = p(rOut, a1); // outer-start
const [x2,y2] = p(rOut, a2); // outer-end
const [x3,y3] = p(rIn, a2); // inner-end
const [x4,y4] = p(rIn, a1); // inner-start
// Polygon (annular sector)
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)}`);
if (isCurrent) {
poly.setAttribute('fill', color);
poly.setAttribute('stroke', color);
poly.setAttribute('stroke-width', '0.8');
} else if (isPrimary) {
poly.setAttribute('fill', hexRgba(color, 0.55));
poly.setAttribute('stroke', hexRgba(color, 0.95));
poly.setAttribute('stroke-width', '0.4');
} else if (isSecondary) {
poly.setAttribute('fill', hexRgba(color, 0.28));
poly.setAttribute('stroke', hexRgba(color, 0.55));
poly.setAttribute('stroke-width', '0.35');
} else {
poly.setAttribute('fill', hexRgba(color, 0.10));
poly.setAttribute('stroke', hexRgba(color, 0.30));
poly.setAttribute('stroke-width', '0.3');
}
poly.setAttribute('stroke-linejoin', 'round');
svg.appendChild(poly);
// Labels: Camelot-Code (groß) + Music-Key (klein darunter)
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 ? '#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 ? '#FFFFFF' : theme.textDim);
t2.textContent = m;
svg.appendChild(t2);
});
// Center: Current-Key + Music-Key
const cur = keys[currentIdx];
const [, cy1] = p(rInner * 0.55, 270);
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/11);
c1.setAttribute('font-weight', '700');
c1.setAttribute('fill', cur[2]);
c1.textContent = cur[0];
svg.appendChild(c1);
const c2 = document.createElementNS(NS, 'text');
c2.setAttribute('x', 0);
c2.setAttribute('y', cy1 + viewBoxSize/7);
c2.setAttribute('text-anchor', 'middle');
c2.setAttribute('dominant-baseline', 'middle');
c2.setAttribute('font-family', 'JetBrains Mono, monospace');
c2.setAttribute('font-size', viewBoxSize/22);
c2.setAttribute('fill', theme.textDim);
c2.textContent = cur[1];
svg.appendChild(c2);
}
buildWheel('wheel-small', 220);
/* ================================================================== */
/* Theme Toggle */
/* ================================================================== */
function toggleTheme() {
const html = document.documentElement;
const isLight = html.getAttribute('data-theme') === 'light';
const next = isLight ? 'dark' : 'light';
html.setAttribute('data-theme', next);
document.getElementById('theme-btn').textContent = isLight ? 'Light' : 'Dark';
buildWheel('wheel-small', 220);
}
/* ================================================================== */
/* Hero Mode Toggles (inline expand) */
/* ================================================================== */
function toggleWheel() {
const hero = document.getElementById('hero');
if (hero.classList.contains('wheel-mode')) {
resetHero();
} else {
hero.classList.remove('cover-mode');
hero.classList.add('wheel-mode');
}
}
function toggleCover() {
const hero = document.getElementById('hero');
if (hero.classList.contains('cover-mode')) {
resetHero();
} else {
hero.classList.remove('wheel-mode');
hero.classList.add('cover-mode');
}
}
function resetHero() {
document.getElementById('hero').classList.remove('wheel-mode', 'cover-mode');
}
/* ================================================================== */
/* Suggestions: 10 Synth-Pop / New Wave Tracks, sortiert nach */
/* Mixed-In-Key-Kompatibilitäts-Stufe zu 4B (A-Dur). */
/* Stufen: 0=Perfect, 1=-1, 2=+1, 3=Energy, 4=Scale, */
/* 5=Diagonal, 6=Jaw's, 7=Mood */
/* ================================================================== */
const suggestions = [
/* Stufe 0: Perfect (4B = A) */
{ c: '4B', m: 'A', color: '#4F9D69', title: 'Big in Japan', artist: 'Alphaville', bpm: 120, compat: 0 },
{ c: '4B', m: 'A', color: '#4F9D69', title: 'Forever Young', artist: 'Alphaville', bpm: 116, compat: 0 },
{ c: '4B', m: 'A', color: '#4F9D69', title: 'Enola Gay', artist: 'OMD', bpm: 108, compat: 0 },
/* Stufe 1: Quinte -1 (3B = D) */
{ c: '3B', m: 'D', color: '#E0B423', title: 'West End Girls', artist: 'Pet Shop Boys', bpm: 122, compat: 1 },
{ c: '3B', m: 'D', color: '#E0B423', title: 'Tainted Love', artist: 'Soft Cell', bpm: 82, compat: 1 },
/* Stufe 2: Quinte +1 (5B = E) */
{ c: '5B', m: 'E', color: '#1B998B', title: 'Blue Monday', artist: 'New Order', bpm: 130, compat: 2 },
{ c: '5B', m: 'E', color: '#1B998B', title: 'Everything Counts', artist: 'Depeche Mode', bpm: 113, compat: 2 },
/* Stufe 3: Energy (6B = B) */
{ c: '6B', m: 'B', color: '#6B4E9C', title: 'Enjoy the Silence', artist: 'Depeche Mode', bpm: 113, compat: 3 },
/* Stufe 4: Scale (4A = F#m, relative Moll) */
{ c: '4A', m: 'F#m', color: '#4F9D69', title: 'Shout', artist: 'Tears for Fears', bpm: 102, compat: 4 },
/* Stufe 7: Mood (1A = Am) */
{ c: '1A', m: 'Am', color: '#D62828', title: 'Sweet Dreams', artist: 'Eurythmics', bpm: 125, compat: 7 },
];
const sugContainer = document.getElementById('suggestions');
suggestions.forEach(s => {
const div = document.createElement('div');
div.className = 'sug';
const idSafe = `sug-cover-${s.c.replace(/\s/g,'_')}-${s.title.replace(/[^A-Za-z0-9]/g,'')}`;
div.innerHTML = `
<div class="sug-cover" id="${idSafe}">
<div class="sug-cover-placeholder">♪</div>
</div>
<div class="sug-meta">
<div class="sug-key" style="background:${s.color}">${s.c}</div>
<div class="sug-bpm">${s.bpm} BPM</div>
</div>
<div>
<div class="sug-title">${s.title}</div>
<div class="sug-artist">${s.artist} · ${s.m}</div>
</div>
<div class="sug-compat">${compatLabels[s.compat]}</div>
`;
sugContainer.appendChild(div);
});
/* ================================================================== */
/* iTunes Search API für Cover-Art (mit Fallback auf Platzhalter) */
/* Funktioniert nur über einen Server wegen CORS, nicht bei file:// */
/* ================================================================== */
async function fetchCover(query, targetEl) {
try {
const url = `https://itunes.apple.com/search?term=${encodeURIComponent(query)}&media=music&entity=song&limit=1`;
const r = await fetch(url);
if (!r.ok) throw new Error('itunes error');
const data = await r.json();
if (data.results && data.results[0] && data.results[0].artworkUrl100) {
const src = data.results[0].artworkUrl100.replace('100x100bb', '300x300bb');
targetEl.innerHTML = `<img src="${src}" alt="cover" loading="lazy">`;
}
} catch (e) { /* leave placeholder */ }
}
fetchCover('Alphaville Made in Japan', document.getElementById('cover'));
suggestions.forEach(s => {
const el = document.getElementById(`sug-cover-${s.c.replace(/\s/g,'_')}-${s.title.replace(/[^A-Za-z0-9]/g,'')}`);
if (el) fetchCover(`${s.artist} ${s.title}`, el);
});
</script>
</body>
</html>