@gblp/circle-of-fifths 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,274 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input, computed, Component, signal } from '@angular/core';
|
|
2
|
+
import { input, output, computed, Component, inject, signal, effect } from '@angular/core';
|
|
3
3
|
import { NgClass } from '@angular/common';
|
|
4
|
+
import { ChordService, ChordDiagram } from '@gblp/chord-finder';
|
|
5
|
+
|
|
6
|
+
const KEYS = [
|
|
7
|
+
{ index: 0, major: 'C', minor: 'Am', sharps: 0, flats: 0 },
|
|
8
|
+
{ index: 1, major: 'G', minor: 'Em', sharps: 1, flats: 0 },
|
|
9
|
+
{ index: 2, major: 'D', minor: 'Bm', sharps: 2, flats: 0 },
|
|
10
|
+
{ index: 3, major: 'A', minor: 'F♯m', sharps: 3, flats: 0 },
|
|
11
|
+
{ index: 4, major: 'E', minor: 'C♯m', sharps: 4, flats: 0 },
|
|
12
|
+
{ index: 5, major: 'B', minor: 'G♯m', sharps: 5, flats: 0 },
|
|
13
|
+
{ index: 6, major: 'F♯', minor: 'D♯m', sharps: 6, flats: 0 },
|
|
14
|
+
{ index: 7, major: 'D♭', minor: 'B♭m', sharps: 0, flats: 5 },
|
|
15
|
+
{ index: 8, major: 'A♭', minor: 'Fm', sharps: 0, flats: 4 },
|
|
16
|
+
{ index: 9, major: 'E♭', minor: 'Cm', sharps: 0, flats: 3 },
|
|
17
|
+
{ index: 10, major: 'B♭', minor: 'Gm', sharps: 0, flats: 2 },
|
|
18
|
+
{ index: 11, major: 'F', minor: 'Dm', sharps: 0, flats: 1 },
|
|
19
|
+
];
|
|
20
|
+
const COPY = {
|
|
21
|
+
en: {
|
|
22
|
+
title: 'Circle of Fifths',
|
|
23
|
+
subtitle: 'Click any major or minor key to highlight its diatonic chords',
|
|
24
|
+
ariaLabel: 'Circle of Fifths',
|
|
25
|
+
clickKey: 'Click a key',
|
|
26
|
+
seeChords: 'to see its chords',
|
|
27
|
+
deselect: '↩ click to deselect',
|
|
28
|
+
tonicLegend: 'Tonic (I / i)',
|
|
29
|
+
majorLegend: 'Major chords (IV, V)',
|
|
30
|
+
minorLegend: 'Minor chords (ii, iii, vi)',
|
|
31
|
+
diminishedLegend: 'Diminished (vii° / ii°)',
|
|
32
|
+
diatonicChords: 'Diatonic Chords',
|
|
33
|
+
commonProgressions: 'Common Progressions in',
|
|
34
|
+
generateProgressions: 'Shuffle',
|
|
35
|
+
chorus: 'Chorus',
|
|
36
|
+
bridge: 'Bridge',
|
|
37
|
+
outro: 'Outro',
|
|
38
|
+
verse: 'Verse',
|
|
39
|
+
copy: 'Copy',
|
|
40
|
+
expand: 'Expand',
|
|
41
|
+
collapse: 'Collapse',
|
|
42
|
+
transposeLabel: 'Transpose to:',
|
|
43
|
+
spiceLabels: ['🌶 Spice', '🌶🌶 Spicier', '🔥 Hot!', '↩ Plain'],
|
|
44
|
+
major: 'Major',
|
|
45
|
+
minor: 'Minor',
|
|
46
|
+
diminished: 'diminished',
|
|
47
|
+
relativeMinor: 'Relative minor',
|
|
48
|
+
relativeMajor: 'Relative major',
|
|
49
|
+
showChordDiagrams: 'Show chord diagrams',
|
|
50
|
+
hideChordDiagrams: 'Hide chord diagrams',
|
|
51
|
+
previousPosition: 'Previous position',
|
|
52
|
+
nextPosition: 'Next position',
|
|
53
|
+
previousPositionAll: 'Previous position (all diagrams)',
|
|
54
|
+
nextPositionAll: 'Next position (all diagrams)',
|
|
55
|
+
roles: {
|
|
56
|
+
tonic: 'Tonic',
|
|
57
|
+
supertonic: 'Supertonic',
|
|
58
|
+
mediant: 'Mediant',
|
|
59
|
+
subdominant: 'Subdominant',
|
|
60
|
+
dominant: 'Dominant',
|
|
61
|
+
submediant: 'Submediant',
|
|
62
|
+
leadingTone: 'Leading Tone',
|
|
63
|
+
subtonic: 'Subtonic',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
es: {
|
|
67
|
+
title: 'Círculo de quintas',
|
|
68
|
+
subtitle: 'Haz clic en una tonalidad mayor o menor para resaltar sus acordes diatónicos',
|
|
69
|
+
ariaLabel: 'Círculo de quintas',
|
|
70
|
+
clickKey: 'Haz clic en una tonalidad',
|
|
71
|
+
seeChords: 'para ver sus acordes',
|
|
72
|
+
deselect: '↩ haz clic para deseleccionar',
|
|
73
|
+
tonicLegend: 'Tónica (I / i)',
|
|
74
|
+
majorLegend: 'Acordes mayores (IV, V)',
|
|
75
|
+
minorLegend: 'Acordes menores (ii, iii, vi)',
|
|
76
|
+
diminishedLegend: 'Disminuido (vii° / ii°)',
|
|
77
|
+
diatonicChords: 'Acordes diatónicos',
|
|
78
|
+
commonProgressions: 'Progresiones comunes en',
|
|
79
|
+
generateProgressions: 'Generar nuevos',
|
|
80
|
+
chorus: 'Coro',
|
|
81
|
+
bridge: 'Puente',
|
|
82
|
+
outro: 'Final',
|
|
83
|
+
verse: 'Estrofa',
|
|
84
|
+
copy: 'Copiar',
|
|
85
|
+
expand: 'Expandir',
|
|
86
|
+
collapse: 'Colapsar',
|
|
87
|
+
transposeLabel: 'Transponer a:',
|
|
88
|
+
spiceLabels: ['🌶 Condimentar', '🌶🌶 Más sabor', '🔥 ¡Picante!', '↩ Simple'],
|
|
89
|
+
major: 'Mayor',
|
|
90
|
+
minor: 'Menor',
|
|
91
|
+
diminished: 'disminuido',
|
|
92
|
+
relativeMinor: 'Relativa menor',
|
|
93
|
+
relativeMajor: 'Relativa mayor',
|
|
94
|
+
showChordDiagrams: 'Mostrar acordes de diagramas',
|
|
95
|
+
hideChordDiagrams: 'Ocultar acordes de diagramas',
|
|
96
|
+
previousPosition: 'Posición anterior',
|
|
97
|
+
nextPosition: 'Siguiente posición',
|
|
98
|
+
previousPositionAll: 'Posición anterior (todos)',
|
|
99
|
+
nextPositionAll: 'Siguiente posición (todos)',
|
|
100
|
+
roles: {
|
|
101
|
+
tonic: 'Tónica',
|
|
102
|
+
supertonic: 'Supertónica',
|
|
103
|
+
mediant: 'Mediante',
|
|
104
|
+
subdominant: 'Subdominante',
|
|
105
|
+
dominant: 'Dominante',
|
|
106
|
+
submediant: 'Submediante',
|
|
107
|
+
leadingTone: 'Sensible',
|
|
108
|
+
subtonic: 'Subtónica',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
class CircleOfFifthsWheel {
|
|
114
|
+
language = input('en', /* @ts-ignore */
|
|
115
|
+
...(ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []));
|
|
116
|
+
selectedIndex = input(null, /* @ts-ignore */
|
|
117
|
+
...(ngDevMode ? [{ debugName: "selectedIndex" }] : /* istanbul ignore next */ []));
|
|
118
|
+
selectedType = input(null, /* @ts-ignore */
|
|
119
|
+
...(ngDevMode ? [{ debugName: "selectedType" }] : /* istanbul ignore next */ []));
|
|
120
|
+
interactive = input(true, /* @ts-ignore */
|
|
121
|
+
...(ngDevMode ? [{ debugName: "interactive" }] : /* istanbul ignore next */ []));
|
|
122
|
+
keySelected = output();
|
|
123
|
+
text = computed(() => COPY[this.language()], /* @ts-ignore */
|
|
124
|
+
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
125
|
+
keys = KEYS;
|
|
126
|
+
CX = 300;
|
|
127
|
+
CY = 300;
|
|
128
|
+
KEY_SIG = { inner: 250, outer: 288 };
|
|
129
|
+
MAJOR = { inner: 172, outer: 250 };
|
|
130
|
+
MINOR = { inner: 102, outer: 172 };
|
|
131
|
+
get slice() {
|
|
132
|
+
const idx = this.selectedIndex();
|
|
133
|
+
if (idx === null)
|
|
134
|
+
return null;
|
|
135
|
+
return {
|
|
136
|
+
prev: (idx - 1 + 12) % 12,
|
|
137
|
+
curr: idx,
|
|
138
|
+
next: (idx + 1) % 12,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
onKeyClick(index, type) {
|
|
142
|
+
if (!this.interactive())
|
|
143
|
+
return;
|
|
144
|
+
this.keySelected.emit({ index, type });
|
|
145
|
+
}
|
|
146
|
+
getMajorState(index) {
|
|
147
|
+
const s = this.slice;
|
|
148
|
+
if (!s)
|
|
149
|
+
return '';
|
|
150
|
+
const type = this.selectedType();
|
|
151
|
+
if (index === s.curr)
|
|
152
|
+
return type === 'major' ? 'tonic' : 'scale-major';
|
|
153
|
+
if (index === s.prev || index === s.next)
|
|
154
|
+
return 'scale-major';
|
|
155
|
+
return '';
|
|
156
|
+
}
|
|
157
|
+
getMinorState(index) {
|
|
158
|
+
const s = this.slice;
|
|
159
|
+
if (!s)
|
|
160
|
+
return '';
|
|
161
|
+
const type = this.selectedType();
|
|
162
|
+
const dimIdx = (s.curr + 2) % 12;
|
|
163
|
+
if (index === s.curr)
|
|
164
|
+
return type === 'minor' ? 'tonic' : 'scale-minor';
|
|
165
|
+
if (index === s.prev || index === s.next)
|
|
166
|
+
return 'scale-minor';
|
|
167
|
+
if (index === dimIdx)
|
|
168
|
+
return 'scale-diminished';
|
|
169
|
+
return '';
|
|
170
|
+
}
|
|
171
|
+
getMajorRole(index) {
|
|
172
|
+
const s = this.slice;
|
|
173
|
+
if (!s)
|
|
174
|
+
return '';
|
|
175
|
+
const type = this.selectedType();
|
|
176
|
+
if (type === 'major') {
|
|
177
|
+
if (index === s.curr)
|
|
178
|
+
return 'I';
|
|
179
|
+
if (index === s.prev)
|
|
180
|
+
return 'IV';
|
|
181
|
+
if (index === s.next)
|
|
182
|
+
return 'V';
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
if (index === s.curr)
|
|
186
|
+
return 'III';
|
|
187
|
+
if (index === s.prev)
|
|
188
|
+
return 'VI';
|
|
189
|
+
if (index === s.next)
|
|
190
|
+
return 'VII';
|
|
191
|
+
}
|
|
192
|
+
return '';
|
|
193
|
+
}
|
|
194
|
+
getMinorRole(index) {
|
|
195
|
+
const s = this.slice;
|
|
196
|
+
if (!s)
|
|
197
|
+
return '';
|
|
198
|
+
const type = this.selectedType();
|
|
199
|
+
const dimIdx = (s.curr + 2) % 12;
|
|
200
|
+
if (type === 'major') {
|
|
201
|
+
if (index === s.curr)
|
|
202
|
+
return 'vi';
|
|
203
|
+
if (index === s.prev)
|
|
204
|
+
return 'ii';
|
|
205
|
+
if (index === s.next)
|
|
206
|
+
return 'iii';
|
|
207
|
+
if (index === dimIdx)
|
|
208
|
+
return 'vii°';
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
if (index === s.curr)
|
|
212
|
+
return 'i';
|
|
213
|
+
if (index === s.prev)
|
|
214
|
+
return 'iv';
|
|
215
|
+
if (index === s.next)
|
|
216
|
+
return 'v';
|
|
217
|
+
if (index === dimIdx)
|
|
218
|
+
return 'ii°';
|
|
219
|
+
}
|
|
220
|
+
return '';
|
|
221
|
+
}
|
|
222
|
+
arcPath(innerR, outerR, index, gap = 1.5) {
|
|
223
|
+
const mid = index * 30 - 90;
|
|
224
|
+
const s = (mid - 15 + gap) * (Math.PI / 180);
|
|
225
|
+
const e = (mid + 15 - gap) * (Math.PI / 180);
|
|
226
|
+
const cx = this.CX, cy = this.CY;
|
|
227
|
+
const x1 = cx + outerR * Math.cos(s), y1 = cy + outerR * Math.sin(s);
|
|
228
|
+
const x2 = cx + outerR * Math.cos(e), y2 = cy + outerR * Math.sin(e);
|
|
229
|
+
const x3 = cx + innerR * Math.cos(e), y3 = cy + innerR * Math.sin(e);
|
|
230
|
+
const x4 = cx + innerR * Math.cos(s), y4 = cy + innerR * Math.sin(s);
|
|
231
|
+
const f = (n) => n.toFixed(2);
|
|
232
|
+
return `M${f(x1)} ${f(y1)} A${outerR} ${outerR} 0 0 1 ${f(x2)} ${f(y2)} L${f(x3)} ${f(y3)} A${innerR} ${innerR} 0 0 0 ${f(x4)} ${f(y4)}Z`;
|
|
233
|
+
}
|
|
234
|
+
textPos(midR, index) {
|
|
235
|
+
const a = (index * 30 - 90) * (Math.PI / 180);
|
|
236
|
+
return {
|
|
237
|
+
x: this.CX + midR * Math.cos(a),
|
|
238
|
+
y: this.CY + midR * Math.sin(a),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
getAccidentalText(key) {
|
|
242
|
+
if (key.sharps > 0)
|
|
243
|
+
return `${key.sharps}♯`;
|
|
244
|
+
if (key.flats > 0)
|
|
245
|
+
return `${key.flats}♭`;
|
|
246
|
+
return '—';
|
|
247
|
+
}
|
|
248
|
+
get selectedInfo() {
|
|
249
|
+
const idx = this.selectedIndex();
|
|
250
|
+
const type = this.selectedType();
|
|
251
|
+
if (idx === null || type === null)
|
|
252
|
+
return null;
|
|
253
|
+
const key = KEYS[idx];
|
|
254
|
+
const copy = this.text();
|
|
255
|
+
const scale = type === 'major' ? copy.major : copy.minor;
|
|
256
|
+
return {
|
|
257
|
+
name: type === 'major' ? key.major : key.minor,
|
|
258
|
+
scale,
|
|
259
|
+
fullName: `${type === 'major' ? key.major : key.minor} ${scale}`,
|
|
260
|
+
relativeKey: type === 'major'
|
|
261
|
+
? `${copy.relativeMinor}: ${key.minor}`
|
|
262
|
+
: `${copy.relativeMajor}: ${key.major}`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: CircleOfFifthsWheel, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
266
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: CircleOfFifthsWheel, isStandalone: true, selector: "circle-of-fifths-wheel", inputs: { language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null }, selectedIndex: { classPropertyName: "selectedIndex", publicName: "selectedIndex", isSignal: true, isRequired: false, transformFunction: null }, selectedType: { classPropertyName: "selectedType", publicName: "selectedType", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { keySelected: "keySelected" }, host: { properties: { "class.readonly": "!interactive()" } }, ngImport: i0, template: "<div class=\"circle-wrapper\">\n <svg\n viewBox=\"0 0 600 600\"\n xmlns=\"http://www.w3.org/2000/svg\"\n [attr.aria-label]=\"text().ariaLabel\"\n >\n <!-- \u2500\u2500 Key Signature Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <path [attr.d]=\"arcPath(KEY_SIG.inner, KEY_SIG.outer, key.index)\" class=\"keysig-segment\" />\n <text\n [attr.x]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).y\"\n class=\"keysig-label\"\n >\n {{ getAccidentalText(key) }}\n </text>\n }\n\n <!-- \u2500\u2500 Major Keys Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <g\n [ngClass]=\"['major-group', getMajorState(key.index)]\"\n (click)=\"onKeyClick(key.index, 'major')\"\n >\n <path [attr.d]=\"arcPath(MAJOR.inner, MAJOR.outer, key.index)\" />\n\n <text\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\n [attr.y]=\"\n textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y +\n (getMajorRole(key.index) ? -9 : 0)\n \"\n class=\"major-label\"\n >\n {{ key.major }}\n </text>\n\n @if (getMajorRole(key.index)) {\n <text\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y + 11\"\n class=\"role-label\"\n >\n {{ getMajorRole(key.index) }}\n </text>\n }\n </g>\n }\n\n <!-- \u2500\u2500 Minor Keys Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <g\n [ngClass]=\"['minor-group', getMinorState(key.index)]\"\n (click)=\"onKeyClick(key.index, 'minor')\"\n >\n <path [attr.d]=\"arcPath(MINOR.inner, MINOR.outer, key.index)\" />\n\n <text\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\n [attr.y]=\"\n textPos((MINOR.inner + MINOR.outer) / 2, key.index).y +\n (getMinorRole(key.index) ? -8 : 0)\n \"\n class=\"minor-label\"\n >\n {{ key.minor }}\n </text>\n\n @if (getMinorRole(key.index)) {\n <text\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).y + 9\"\n class=\"role-label\"\n >\n {{ getMinorRole(key.index) }}\n </text>\n }\n </g>\n }\n\n <!-- \u2500\u2500 Outer border \u2500\u2500 -->\n <circle cx=\"300\" cy=\"300\" r=\"289\" class=\"outer-border\" />\n\n <!-- \u2500\u2500 Center Circle \u2500\u2500 -->\n <circle cx=\"300\" cy=\"300\" r=\"99\" class=\"center-circle\" />\n\n @if (selectedInfo) {\n <text x=\"300\" y=\"283\" class=\"center-name\">{{ selectedInfo.name }}</text>\n <text x=\"300\" y=\"304\" class=\"center-scale\">{{ selectedInfo.scale }}</text>\n <text x=\"300\" y=\"322\" class=\"center-relative\">{{ selectedInfo.relativeKey }}</text>\n @if (interactive()) {\n <text x=\"300\" y=\"338\" class=\"center-deselect\">{{ text().deselect }}</text>\n }\n } @else if (interactive()) {\n <text x=\"300\" y=\"293\" class=\"center-hint\">{{ text().clickKey }}</text>\n <text x=\"300\" y=\"312\" class=\"center-hint-sm\">{{ text().seeChords }}</text>\n }\n </svg>\n</div>\n\n<!-- \u2500\u2500 Legend \u2500\u2500 -->\n<div class=\"legend\">\n <span class=\"legend-item tonic\">{{ text().tonicLegend }}</span>\n <span class=\"legend-item scale-major\">{{ text().majorLegend }}</span>\n <span class=\"legend-item scale-minor\">{{ text().minorLegend }}</span>\n <span class=\"legend-item scale-diminished\">{{ text().diminishedLegend }}</span>\n</div>\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-background-alt: var(--chords-background-alt, #f8fafc);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);display:block;font-family:Segoe UI,system-ui,sans-serif}:host(.readonly) .major-group,:host(.readonly) .minor-group{cursor:default;pointer-events:none}.circle-wrapper{max-width:580px;margin:0 auto}.circle-wrapper svg{display:block;width:100%;height:auto;overflow:visible}.keysig-segment{fill:var(--_chords-text)}.keysig-label{fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));font-size:12px;font-weight:500;text-anchor:middle;dominant-baseline:central;pointer-events:none}.major-group{cursor:pointer}.major-group path{fill:color-mix(in srgb,var(--_chords-primary) 14%,var(--_chords-surface));stroke:var(--_chords-surface);stroke-width:2;transition:fill .18s ease}.major-group:hover path{fill:color-mix(in srgb,var(--_chords-primary) 24%,var(--_chords-surface))}.major-group .major-label{fill:var(--_chords-text);font-size:15px;font-weight:700;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.major-group.tonic path{fill:var(--_chords-highlight)}.major-group.tonic:hover path{fill:color-mix(in srgb,var(--_chords-highlight) 84%,var(--_chords-text))}.major-group.tonic .major-label{fill:var(--_chords-on-primary)}.major-group.scale-major path{fill:var(--_chords-primary)}.major-group.scale-major:hover path{fill:color-mix(in srgb,var(--_chords-primary) 82%,var(--_chords-text))}.major-group.scale-major .major-label{fill:var(--_chords-on-primary)}.minor-group{cursor:pointer}.minor-group path{fill:color-mix(in srgb,var(--_chords-secondary) 10%,var(--_chords-surface));stroke:var(--_chords-surface);stroke-width:2;transition:fill .18s ease}.minor-group:hover path{fill:color-mix(in srgb,var(--_chords-secondary) 20%,var(--_chords-surface))}.minor-group .minor-label{fill:color-mix(in srgb,var(--_chords-text) 78%,var(--_chords-muted));font-size:12px;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.minor-group.tonic path{fill:var(--_chords-highlight)}.minor-group.tonic:hover path{fill:color-mix(in srgb,var(--_chords-highlight) 84%,var(--_chords-text))}.minor-group.tonic .minor-label{fill:var(--_chords-on-primary)}.minor-group.scale-minor path{fill:var(--_chords-secondary)}.minor-group.scale-minor:hover path{fill:color-mix(in srgb,var(--_chords-secondary) 82%,var(--_chords-text))}.minor-group.scale-minor .minor-label{fill:var(--_chords-on-primary)}.minor-group.scale-diminished path{fill:var(--_chords-danger)}.minor-group.scale-diminished:hover path{fill:color-mix(in srgb,var(--_chords-danger) 82%,var(--_chords-text))}.minor-group.scale-diminished .minor-label{fill:var(--_chords-on-primary)}.role-label{font-size:10px;font-style:italic;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;fill:color-mix(in srgb,var(--_chords-on-primary) 85%,transparent)}.outer-border{fill:none;stroke:var(--_chords-text);stroke-width:2.5}.center-circle{fill:var(--_chords-surface);stroke:var(--_chords-border);stroke-width:2;filter:drop-shadow(0 2px 10px color-mix(in srgb,var(--_chords-text) 8%,transparent))}.center-name{font-size:30px;font-weight:800;text-anchor:middle;dominant-baseline:auto;fill:var(--_chords-text);letter-spacing:-.02em}.center-scale{font-size:13px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:var(--_chords-muted);text-transform:uppercase;letter-spacing:.08em}.center-relative{font-size:9.5px;font-weight:500;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.center-deselect{font-size:8px;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background))}.center-hint{font-size:16px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.center-hint-sm{font-size:11px;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background))}.legend{display:flex;justify-content:center;gap:16px;flex-wrap:wrap;margin:16px 0 0}.legend .legend-item{display:flex;align-items:center;gap:6px;font-size:.8rem;color:var(--_chords-muted)}.legend .legend-item:before{content:\"\";display:inline-block;width:14px;height:14px;border-radius:3px}.legend .legend-item.tonic:before{background:var(--_chords-highlight)}.legend .legend-item.scale-major:before{background:var(--_chords-primary)}.legend .legend-item.scale-minor:before{background:var(--_chords-secondary)}.legend .legend-item.scale-diminished:before{background:var(--_chords-danger)}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
|
|
267
|
+
}
|
|
268
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: CircleOfFifthsWheel, decorators: [{
|
|
269
|
+
type: Component,
|
|
270
|
+
args: [{ selector: 'circle-of-fifths-wheel', standalone: true, imports: [NgClass], host: { '[class.readonly]': '!interactive()' }, template: "<div class=\"circle-wrapper\">\n <svg\n viewBox=\"0 0 600 600\"\n xmlns=\"http://www.w3.org/2000/svg\"\n [attr.aria-label]=\"text().ariaLabel\"\n >\n <!-- \u2500\u2500 Key Signature Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <path [attr.d]=\"arcPath(KEY_SIG.inner, KEY_SIG.outer, key.index)\" class=\"keysig-segment\" />\n <text\n [attr.x]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).y\"\n class=\"keysig-label\"\n >\n {{ getAccidentalText(key) }}\n </text>\n }\n\n <!-- \u2500\u2500 Major Keys Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <g\n [ngClass]=\"['major-group', getMajorState(key.index)]\"\n (click)=\"onKeyClick(key.index, 'major')\"\n >\n <path [attr.d]=\"arcPath(MAJOR.inner, MAJOR.outer, key.index)\" />\n\n <text\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\n [attr.y]=\"\n textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y +\n (getMajorRole(key.index) ? -9 : 0)\n \"\n class=\"major-label\"\n >\n {{ key.major }}\n </text>\n\n @if (getMajorRole(key.index)) {\n <text\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y + 11\"\n class=\"role-label\"\n >\n {{ getMajorRole(key.index) }}\n </text>\n }\n </g>\n }\n\n <!-- \u2500\u2500 Minor Keys Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <g\n [ngClass]=\"['minor-group', getMinorState(key.index)]\"\n (click)=\"onKeyClick(key.index, 'minor')\"\n >\n <path [attr.d]=\"arcPath(MINOR.inner, MINOR.outer, key.index)\" />\n\n <text\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\n [attr.y]=\"\n textPos((MINOR.inner + MINOR.outer) / 2, key.index).y +\n (getMinorRole(key.index) ? -8 : 0)\n \"\n class=\"minor-label\"\n >\n {{ key.minor }}\n </text>\n\n @if (getMinorRole(key.index)) {\n <text\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).y + 9\"\n class=\"role-label\"\n >\n {{ getMinorRole(key.index) }}\n </text>\n }\n </g>\n }\n\n <!-- \u2500\u2500 Outer border \u2500\u2500 -->\n <circle cx=\"300\" cy=\"300\" r=\"289\" class=\"outer-border\" />\n\n <!-- \u2500\u2500 Center Circle \u2500\u2500 -->\n <circle cx=\"300\" cy=\"300\" r=\"99\" class=\"center-circle\" />\n\n @if (selectedInfo) {\n <text x=\"300\" y=\"283\" class=\"center-name\">{{ selectedInfo.name }}</text>\n <text x=\"300\" y=\"304\" class=\"center-scale\">{{ selectedInfo.scale }}</text>\n <text x=\"300\" y=\"322\" class=\"center-relative\">{{ selectedInfo.relativeKey }}</text>\n @if (interactive()) {\n <text x=\"300\" y=\"338\" class=\"center-deselect\">{{ text().deselect }}</text>\n }\n } @else if (interactive()) {\n <text x=\"300\" y=\"293\" class=\"center-hint\">{{ text().clickKey }}</text>\n <text x=\"300\" y=\"312\" class=\"center-hint-sm\">{{ text().seeChords }}</text>\n }\n </svg>\n</div>\n\n<!-- \u2500\u2500 Legend \u2500\u2500 -->\n<div class=\"legend\">\n <span class=\"legend-item tonic\">{{ text().tonicLegend }}</span>\n <span class=\"legend-item scale-major\">{{ text().majorLegend }}</span>\n <span class=\"legend-item scale-minor\">{{ text().minorLegend }}</span>\n <span class=\"legend-item scale-diminished\">{{ text().diminishedLegend }}</span>\n</div>\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-background-alt: var(--chords-background-alt, #f8fafc);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);display:block;font-family:Segoe UI,system-ui,sans-serif}:host(.readonly) .major-group,:host(.readonly) .minor-group{cursor:default;pointer-events:none}.circle-wrapper{max-width:580px;margin:0 auto}.circle-wrapper svg{display:block;width:100%;height:auto;overflow:visible}.keysig-segment{fill:var(--_chords-text)}.keysig-label{fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));font-size:12px;font-weight:500;text-anchor:middle;dominant-baseline:central;pointer-events:none}.major-group{cursor:pointer}.major-group path{fill:color-mix(in srgb,var(--_chords-primary) 14%,var(--_chords-surface));stroke:var(--_chords-surface);stroke-width:2;transition:fill .18s ease}.major-group:hover path{fill:color-mix(in srgb,var(--_chords-primary) 24%,var(--_chords-surface))}.major-group .major-label{fill:var(--_chords-text);font-size:15px;font-weight:700;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.major-group.tonic path{fill:var(--_chords-highlight)}.major-group.tonic:hover path{fill:color-mix(in srgb,var(--_chords-highlight) 84%,var(--_chords-text))}.major-group.tonic .major-label{fill:var(--_chords-on-primary)}.major-group.scale-major path{fill:var(--_chords-primary)}.major-group.scale-major:hover path{fill:color-mix(in srgb,var(--_chords-primary) 82%,var(--_chords-text))}.major-group.scale-major .major-label{fill:var(--_chords-on-primary)}.minor-group{cursor:pointer}.minor-group path{fill:color-mix(in srgb,var(--_chords-secondary) 10%,var(--_chords-surface));stroke:var(--_chords-surface);stroke-width:2;transition:fill .18s ease}.minor-group:hover path{fill:color-mix(in srgb,var(--_chords-secondary) 20%,var(--_chords-surface))}.minor-group .minor-label{fill:color-mix(in srgb,var(--_chords-text) 78%,var(--_chords-muted));font-size:12px;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.minor-group.tonic path{fill:var(--_chords-highlight)}.minor-group.tonic:hover path{fill:color-mix(in srgb,var(--_chords-highlight) 84%,var(--_chords-text))}.minor-group.tonic .minor-label{fill:var(--_chords-on-primary)}.minor-group.scale-minor path{fill:var(--_chords-secondary)}.minor-group.scale-minor:hover path{fill:color-mix(in srgb,var(--_chords-secondary) 82%,var(--_chords-text))}.minor-group.scale-minor .minor-label{fill:var(--_chords-on-primary)}.minor-group.scale-diminished path{fill:var(--_chords-danger)}.minor-group.scale-diminished:hover path{fill:color-mix(in srgb,var(--_chords-danger) 82%,var(--_chords-text))}.minor-group.scale-diminished .minor-label{fill:var(--_chords-on-primary)}.role-label{font-size:10px;font-style:italic;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;fill:color-mix(in srgb,var(--_chords-on-primary) 85%,transparent)}.outer-border{fill:none;stroke:var(--_chords-text);stroke-width:2.5}.center-circle{fill:var(--_chords-surface);stroke:var(--_chords-border);stroke-width:2;filter:drop-shadow(0 2px 10px color-mix(in srgb,var(--_chords-text) 8%,transparent))}.center-name{font-size:30px;font-weight:800;text-anchor:middle;dominant-baseline:auto;fill:var(--_chords-text);letter-spacing:-.02em}.center-scale{font-size:13px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:var(--_chords-muted);text-transform:uppercase;letter-spacing:.08em}.center-relative{font-size:9.5px;font-weight:500;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.center-deselect{font-size:8px;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background))}.center-hint{font-size:16px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.center-hint-sm{font-size:11px;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background))}.legend{display:flex;justify-content:center;gap:16px;flex-wrap:wrap;margin:16px 0 0}.legend .legend-item{display:flex;align-items:center;gap:6px;font-size:.8rem;color:var(--_chords-muted)}.legend .legend-item:before{content:\"\";display:inline-block;width:14px;height:14px;border-radius:3px}.legend .legend-item.tonic:before{background:var(--_chords-highlight)}.legend .legend-item.scale-major:before{background:var(--_chords-primary)}.legend .legend-item.scale-minor:before{background:var(--_chords-secondary)}.legend .legend-item.scale-diminished:before{background:var(--_chords-danger)}\n"] }]
|
|
271
|
+
}], propDecorators: { language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }], selectedIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedIndex", required: false }] }], selectedType: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedType", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], keySelected: [{ type: i0.Output, args: ["keySelected"] }] } });
|
|
4
272
|
|
|
5
273
|
const PROGRESSIONS = {
|
|
6
274
|
major: [
|
|
@@ -104,96 +372,6 @@ const PROGRESSIONS = {
|
|
|
104
372
|
},
|
|
105
373
|
],
|
|
106
374
|
};
|
|
107
|
-
const COPY = {
|
|
108
|
-
en: {
|
|
109
|
-
title: 'Circle of Fifths',
|
|
110
|
-
subtitle: 'Click any major or minor key to highlight its diatonic chords',
|
|
111
|
-
ariaLabel: 'Circle of Fifths',
|
|
112
|
-
clickKey: 'Click a key',
|
|
113
|
-
seeChords: 'to see its chords',
|
|
114
|
-
deselect: '↩ click to deselect',
|
|
115
|
-
tonicLegend: 'Tonic (I / i)',
|
|
116
|
-
majorLegend: 'Major chords (IV, V)',
|
|
117
|
-
minorLegend: 'Minor chords (ii, iii, vi)',
|
|
118
|
-
diminishedLegend: 'Diminished (vii° / ii°)',
|
|
119
|
-
diatonicChords: 'Diatonic Chords',
|
|
120
|
-
commonProgressions: 'Common Progressions in',
|
|
121
|
-
generateProgressions: 'Shuffle',
|
|
122
|
-
chorus: 'Chorus',
|
|
123
|
-
bridge: 'Bridge',
|
|
124
|
-
outro: 'Outro',
|
|
125
|
-
verse: 'Verse',
|
|
126
|
-
copy: 'Copy',
|
|
127
|
-
expand: 'Expand',
|
|
128
|
-
collapse: 'Collapse',
|
|
129
|
-
major: 'Major',
|
|
130
|
-
minor: 'Minor',
|
|
131
|
-
diminished: 'diminished',
|
|
132
|
-
relativeMinor: 'Relative minor',
|
|
133
|
-
relativeMajor: 'Relative major',
|
|
134
|
-
roles: {
|
|
135
|
-
tonic: 'Tonic',
|
|
136
|
-
supertonic: 'Supertonic',
|
|
137
|
-
mediant: 'Mediant',
|
|
138
|
-
subdominant: 'Subdominant',
|
|
139
|
-
dominant: 'Dominant',
|
|
140
|
-
submediant: 'Submediant',
|
|
141
|
-
leadingTone: 'Leading Tone',
|
|
142
|
-
subtonic: 'Subtonic',
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
es: {
|
|
146
|
-
title: 'Círculo de quintas',
|
|
147
|
-
subtitle: 'Haz clic en una tonalidad mayor o menor para resaltar sus acordes diatónicos',
|
|
148
|
-
ariaLabel: 'Círculo de quintas',
|
|
149
|
-
clickKey: 'Haz clic en una tonalidad',
|
|
150
|
-
seeChords: 'para ver sus acordes',
|
|
151
|
-
deselect: '↩ haz clic para deseleccionar',
|
|
152
|
-
tonicLegend: 'Tónica (I / i)',
|
|
153
|
-
majorLegend: 'Acordes mayores (IV, V)',
|
|
154
|
-
minorLegend: 'Acordes menores (ii, iii, vi)',
|
|
155
|
-
diminishedLegend: 'Disminuido (vii° / ii°)',
|
|
156
|
-
diatonicChords: 'Acordes diatónicos',
|
|
157
|
-
commonProgressions: 'Progresiones comunes en',
|
|
158
|
-
generateProgressions: 'Generar nuevos',
|
|
159
|
-
chorus: 'Coro',
|
|
160
|
-
bridge: 'Puente',
|
|
161
|
-
outro: 'Final',
|
|
162
|
-
verse: 'Estrofa',
|
|
163
|
-
copy: 'Copiar',
|
|
164
|
-
expand: 'Expandir',
|
|
165
|
-
collapse: 'Colapsar',
|
|
166
|
-
major: 'Mayor',
|
|
167
|
-
minor: 'Menor',
|
|
168
|
-
diminished: 'disminuido',
|
|
169
|
-
relativeMinor: 'Relativa menor',
|
|
170
|
-
relativeMajor: 'Relativa mayor',
|
|
171
|
-
roles: {
|
|
172
|
-
tonic: 'Tónica',
|
|
173
|
-
supertonic: 'Supertónica',
|
|
174
|
-
mediant: 'Mediante',
|
|
175
|
-
subdominant: 'Subdominante',
|
|
176
|
-
dominant: 'Dominante',
|
|
177
|
-
submediant: 'Submediante',
|
|
178
|
-
leadingTone: 'Sensible',
|
|
179
|
-
subtonic: 'Subtónica',
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
};
|
|
183
|
-
const KEYS = [
|
|
184
|
-
{ index: 0, major: 'C', minor: 'Am', sharps: 0, flats: 0 },
|
|
185
|
-
{ index: 1, major: 'G', minor: 'Em', sharps: 1, flats: 0 },
|
|
186
|
-
{ index: 2, major: 'D', minor: 'Bm', sharps: 2, flats: 0 },
|
|
187
|
-
{ index: 3, major: 'A', minor: 'F♯m', sharps: 3, flats: 0 },
|
|
188
|
-
{ index: 4, major: 'E', minor: 'C♯m', sharps: 4, flats: 0 },
|
|
189
|
-
{ index: 5, major: 'B', minor: 'G♯m', sharps: 5, flats: 0 },
|
|
190
|
-
{ index: 6, major: 'F♯', minor: 'D♯m', sharps: 6, flats: 0 },
|
|
191
|
-
{ index: 7, major: 'D♭', minor: 'B♭m', sharps: 0, flats: 5 },
|
|
192
|
-
{ index: 8, major: 'A♭', minor: 'Fm', sharps: 0, flats: 4 },
|
|
193
|
-
{ index: 9, major: 'E♭', minor: 'Cm', sharps: 0, flats: 3 },
|
|
194
|
-
{ index: 10, major: 'B♭', minor: 'Gm', sharps: 0, flats: 2 },
|
|
195
|
-
{ index: 11, major: 'F', minor: 'Dm', sharps: 0, flats: 1 },
|
|
196
|
-
];
|
|
197
375
|
function dimChord(idx) {
|
|
198
376
|
// The diminished chord root is the minor key two steps clockwise (idx+2)
|
|
199
377
|
return KEYS[(idx + 2) % 12].minor.replace(/m$/, '°');
|
|
@@ -224,69 +402,129 @@ function buildChordRows(idx, type, copy) {
|
|
|
224
402
|
];
|
|
225
403
|
}
|
|
226
404
|
}
|
|
405
|
+
// Suffix to append per numeral at spice levels 1, 2, 3.
|
|
406
|
+
// Minor chords already carry 'm' (e.g. "Dm"), so suffix starts from the number.
|
|
407
|
+
// Diminished chords carry '°' which gets replaced entirely.
|
|
408
|
+
const SPICE_SUFFIXES = {
|
|
409
|
+
'I': ['maj7', 'maj9', 'maj9'],
|
|
410
|
+
'ii': ['7', '9', '11'],
|
|
411
|
+
'iii': ['7', '7', '7'],
|
|
412
|
+
'IV': ['maj7', 'maj9', 'maj7♯11'],
|
|
413
|
+
'V': ['7', '9', '13'],
|
|
414
|
+
'vi': ['7', '9', '11'],
|
|
415
|
+
'vii°': ['m7♭5', 'm7♭5', 'm7♭5'],
|
|
416
|
+
'i': ['7', '9', '9'],
|
|
417
|
+
'iv': ['7', '9', '11'],
|
|
418
|
+
'v': ['7', '7', '7'],
|
|
419
|
+
'III': ['maj7', 'maj7', 'maj7'],
|
|
420
|
+
'VI': ['maj7', 'maj9', 'maj9'],
|
|
421
|
+
'VII': ['7', '9', '13'],
|
|
422
|
+
'ii°': ['m7♭5', 'm7♭5', 'm7♭5'],
|
|
423
|
+
};
|
|
424
|
+
function spiceChord(chord, numeral, level) {
|
|
425
|
+
if (level === 0)
|
|
426
|
+
return chord;
|
|
427
|
+
const suffixes = SPICE_SUFFIXES[numeral];
|
|
428
|
+
if (!suffixes)
|
|
429
|
+
return chord;
|
|
430
|
+
const suffix = suffixes[level - 1];
|
|
431
|
+
// Diminished chords: strip '°', then the suffix already starts with 'm'
|
|
432
|
+
return chord.endsWith('°') ? chord.slice(0, -1) + suffix : chord + suffix;
|
|
433
|
+
}
|
|
227
434
|
class ChordSectionComponent {
|
|
228
|
-
|
|
229
|
-
|
|
435
|
+
chordService = inject(ChordService);
|
|
436
|
+
selectedIndex = input(null, /* @ts-ignore */
|
|
437
|
+
...(ngDevMode ? [{ debugName: "selectedIndex" }] : /* istanbul ignore next */ []));
|
|
438
|
+
selectedType = input(null, /* @ts-ignore */
|
|
439
|
+
...(ngDevMode ? [{ debugName: "selectedType" }] : /* istanbul ignore next */ []));
|
|
230
440
|
language = input('en', /* @ts-ignore */
|
|
231
441
|
...(ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []));
|
|
232
442
|
text = computed(() => COPY[this.language()], /* @ts-ignore */
|
|
233
443
|
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
444
|
+
diagramsExpanded = signal(false, /* @ts-ignore */
|
|
445
|
+
...(ngDevMode ? [{ debugName: "diagramsExpanded" }] : /* istanbul ignore next */ []));
|
|
446
|
+
// Per-diagram fret position, keyed by ChordSearchResult.id.
|
|
447
|
+
positionIndices = signal({}, /* @ts-ignore */
|
|
448
|
+
...(ngDevMode ? [{ debugName: "positionIndices" }] : /* istanbul ignore next */ []));
|
|
449
|
+
constructor() {
|
|
450
|
+
// A new key means a new set of diagrams — don't carry stale position
|
|
451
|
+
// choices over from the previous key's chords.
|
|
452
|
+
effect(() => {
|
|
453
|
+
this.selectedIndex();
|
|
454
|
+
this.selectedType();
|
|
455
|
+
this.positionIndices.set({});
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
toggleDiagrams() {
|
|
459
|
+
this.diagramsExpanded.update((v) => !v);
|
|
460
|
+
}
|
|
461
|
+
positionIndexFor(id) {
|
|
462
|
+
return this.positionIndices()[id] ?? 0;
|
|
463
|
+
}
|
|
464
|
+
stepPosition(id, total, delta) {
|
|
465
|
+
if (total <= 1)
|
|
466
|
+
return;
|
|
467
|
+
this.positionIndices.update((m) => ({
|
|
468
|
+
...m,
|
|
469
|
+
[id]: ((m[id] ?? 0) + delta + total) % total,
|
|
470
|
+
}));
|
|
471
|
+
}
|
|
472
|
+
stepAllPositions(delta) {
|
|
473
|
+
this.positionIndices.update((m) => {
|
|
474
|
+
const next = { ...m };
|
|
475
|
+
for (const result of this.chordDiagrams()) {
|
|
476
|
+
const total = result.positions.length;
|
|
477
|
+
if (total <= 1)
|
|
478
|
+
continue;
|
|
479
|
+
next[result.id] = ((next[result.id] ?? 0) + delta + total) % total;
|
|
246
480
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
...(ngDevMode ? [{ debugName: "match" }] : /* istanbul ignore next */ []));
|
|
481
|
+
return next;
|
|
482
|
+
});
|
|
483
|
+
}
|
|
251
484
|
rows = computed(() => {
|
|
252
|
-
const
|
|
253
|
-
|
|
485
|
+
const idx = this.selectedIndex();
|
|
486
|
+
const type = this.selectedType();
|
|
487
|
+
if (idx === null || type === null)
|
|
254
488
|
return [];
|
|
255
|
-
return buildChordRows(
|
|
489
|
+
return buildChordRows(idx, type, this.text());
|
|
256
490
|
}, /* @ts-ignore */
|
|
257
491
|
...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
|
|
258
492
|
info = computed(() => {
|
|
259
|
-
const
|
|
260
|
-
|
|
493
|
+
const idx = this.selectedIndex();
|
|
494
|
+
const type = this.selectedType();
|
|
495
|
+
if (idx === null || type === null)
|
|
261
496
|
return null;
|
|
262
497
|
const copy = this.text();
|
|
263
|
-
const key = KEYS[
|
|
498
|
+
const key = KEYS[idx];
|
|
264
499
|
return {
|
|
265
|
-
fullName: `${
|
|
266
|
-
relativeKey:
|
|
500
|
+
fullName: `${type === 'major' ? key.major : key.minor} ${type === 'major' ? copy.major : copy.minor}`,
|
|
501
|
+
relativeKey: type === 'major'
|
|
267
502
|
? `${copy.relativeMinor}: ${key.minor}`
|
|
268
503
|
: `${copy.relativeMajor}: ${key.major}`,
|
|
269
504
|
};
|
|
270
505
|
}, /* @ts-ignore */
|
|
271
506
|
...(ngDevMode ? [{ debugName: "info" }] : /* istanbul ignore next */ []));
|
|
507
|
+
chordDiagrams = computed(() => {
|
|
508
|
+
const rows = this.rows();
|
|
509
|
+
if (!rows.length)
|
|
510
|
+
return [];
|
|
511
|
+
return this.chordService
|
|
512
|
+
.search(rows.map((r) => r.chord).join(','), this.language())
|
|
513
|
+
.filter((r) => !r.error && r.positions.length > 0);
|
|
514
|
+
}, /* @ts-ignore */
|
|
515
|
+
...(ngDevMode ? [{ debugName: "chordDiagrams" }] : /* istanbul ignore next */ []));
|
|
272
516
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChordSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
273
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ChordSectionComponent, isStandalone: true, selector: "chord-section", inputs: {
|
|
517
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ChordSectionComponent, isStandalone: true, selector: "chord-section", inputs: { selectedIndex: { classPropertyName: "selectedIndex", publicName: "selectedIndex", isSignal: true, isRequired: false, transformFunction: null }, selectedType: { classPropertyName: "selectedType", publicName: "selectedType", isSignal: true, isRequired: false, transformFunction: null }, language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (rows().length) {\n <section class=\"chord-section\">\n @if (info(); as info) {\n <h2>{{ info.fullName }} \u2014 {{ text().diatonicChords }}</h2>\n <p class=\"relative-label\">{{ info.relativeKey }}</p>\n }\n <div class=\"chord-grid\">\n @for (row of rows(); track row.numeral) {\n <div class=\"chord-card\" [ngClass]=\"row.state\">\n <div class=\"chord-numeral\">{{ row.numeral }}</div>\n <div class=\"chord-name\">{{ row.chord }}</div>\n <div class=\"chord-type\">{{ row.chordType }}</div>\n <div class=\"chord-role\">{{ row.role }}</div>\n </div>\n }\n </div>\n\n <button type=\"button\" class=\"btn-diagrams-toggle\" (click)=\"toggleDiagrams()\">\n {{ diagramsExpanded() ? text().hideChordDiagrams : text().showChordDiagrams }}\n </button>\n\n @if (diagramsExpanded()) {\n <div class=\"diagram-grid-row\">\n <div class=\"diagram-grid\">\n @for (result of chordDiagrams(); track result.id) {\n @if (result.positions[positionIndexFor(result.id)]; as position) {\n <div class=\"diagram-wrap\">\n <button\n type=\"button\"\n class=\"diagram-nav-btn\"\n [disabled]=\"result.positions.length <= 1\"\n [attr.aria-label]=\"text().previousPosition\"\n (click)=\"stepPosition(result.id, result.positions.length, -1)\"\n >\n \u25B2\n </button>\n <app-chord-diagram\n [title]=\"result.displayName\"\n [position]=\"position\"\n [positionIndex]=\"positionIndexFor(result.id)\"\n />\n <button\n type=\"button\"\n class=\"diagram-nav-btn\"\n [disabled]=\"result.positions.length <= 1\"\n [attr.aria-label]=\"text().nextPosition\"\n (click)=\"stepPosition(result.id, result.positions.length, 1)\"\n >\n \u25BC\n </button>\n </div>\n }\n }\n </div>\n\n <div class=\"diagram-group-nav\">\n <button type=\"button\" [attr.aria-label]=\"text().previousPositionAll\" (click)=\"stepAllPositions(-1)\">\u25B2</button>\n <button type=\"button\" [attr.aria-label]=\"text().nextPositionAll\" (click)=\"stepAllPositions(1)\">\u25BC</button>\n </div>\n </div>\n }\n </section>\n}\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);display:block;font-family:Segoe UI,system-ui,sans-serif}.chord-section{margin-top:36px;text-align:center}.chord-section h2{margin:0 0 4px;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.chord-section .relative-label{margin:0 0 20px;font-size:.88rem;color:var(--_chords-muted)}.chord-grid{display:flex;flex-wrap:wrap;gap:8px;justify-content:center}.chord-card{border-radius:12px;padding:12px 14px;min-width:82px;flex-shrink:0;text-align:center;background:color-mix(in srgb,var(--_chords-text) 8%,var(--_chords-surface));border:2px solid transparent;transition:transform .15s ease,box-shadow .15s ease}.chord-card:hover{transform:translateY(-2px);box-shadow:0 4px 16px color-mix(in srgb,var(--_chords-text) 10%,transparent)}.chord-card .chord-numeral{font-size:.85rem;font-weight:700;font-style:italic;color:var(--_chords-muted)}.chord-card .chord-name{font-size:1.4rem;font-weight:800;color:var(--_chords-text);margin:2px 0;letter-spacing:-.02em}.chord-card .chord-type{font-size:.65rem;text-transform:uppercase;letter-spacing:.06em;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.chord-card .chord-role{font-size:.7rem;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));margin-top:2px}.chord-card.tonic{background:color-mix(in srgb,var(--_chords-highlight) 10%,var(--_chords-surface));border-color:var(--_chords-highlight)}.chord-card.tonic .chord-numeral,.chord-card.tonic .chord-name{color:color-mix(in srgb,var(--_chords-highlight) 58%,var(--_chords-text))}.chord-card.scale-major{background:color-mix(in srgb,var(--_chords-primary) 9%,var(--_chords-surface));border-color:var(--_chords-primary)}.chord-card.scale-major .chord-numeral,.chord-card.scale-major .chord-name{color:color-mix(in srgb,var(--_chords-primary) 65%,var(--_chords-text))}.chord-card.scale-diminished{background:color-mix(in srgb,var(--_chords-danger) 9%,var(--_chords-surface));border-color:var(--_chords-danger)}.chord-card.scale-diminished .chord-numeral,.chord-card.scale-diminished .chord-name{color:color-mix(in srgb,var(--_chords-danger) 65%,var(--_chords-text))}@media(max-width:480px){.chord-card{min-width:75px;padding:10px 12px}}.btn-diagrams-toggle{display:block;width:100%;margin-top:14px;padding:10px 14px;font-size:.82rem;font-weight:600;border:1px solid var(--_chords-border);border-radius:10px;background:transparent;color:var(--_chords-text);cursor:pointer;transition:border-color .15s,color .15s}.btn-diagrams-toggle:hover{border-color:var(--_chords-primary);color:var(--_chords-primary)}.diagram-grid-row{display:flex;align-items:center;gap:10px;margin-top:16px}.diagram-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(90px,1fr));justify-items:center;gap:12px;flex:1 1 auto;min-width:0}.diagram-wrap{display:flex;flex-direction:column;align-items:center;gap:4px;width:100%;max-width:130px}.diagram-nav-btn,.diagram-group-nav button{padding:2px 10px;font-size:.7rem;line-height:1.4;border:1px solid var(--_chords-border);border-radius:8px;background:transparent;color:var(--_chords-muted);cursor:pointer;transition:border-color .15s,color .15s}.diagram-nav-btn:hover:not(:disabled),.diagram-group-nav button:hover:not(:disabled){border-color:var(--_chords-primary);color:var(--_chords-primary)}.diagram-nav-btn:disabled,.diagram-group-nav button:disabled{opacity:.35;cursor:not-allowed}.diagram-nav-btn{width:100%}.diagram-group-nav{display:flex;flex-direction:column;justify-content:center;gap:8px;flex:0 0 auto}.diagram-group-nav button{padding:10px 8px}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: ChordDiagram, selector: "app-chord-diagram", inputs: ["title", "position", "positionIndex"] }] });
|
|
274
518
|
}
|
|
275
519
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChordSectionComponent, decorators: [{
|
|
276
520
|
type: Component,
|
|
277
|
-
args: [{ selector: 'chord-section', standalone: true, imports: [NgClass], template: "@if (rows().length) {\n <section class=\"chord-section\">\n @if (info(); as info) {\n <h2>{{ info.fullName }} \u2014 {{ text().diatonicChords }}</h2>\n <p class=\"relative-label\">{{ info.relativeKey }}</p>\n }\n <div class=\"chord-grid\">\n @for (row of rows(); track row.numeral) {\n <div class=\"chord-card\" [ngClass]=\"row.state\">\n <div class=\"chord-numeral\">{{ row.numeral }}</div>\n <div class=\"chord-name\">{{ row.chord }}</div>\n <div class=\"chord-type\">{{ row.chordType }}</div>\n <div class=\"chord-role\">{{ row.role }}</div>\n </div>\n }\n </div>\n </section>\n}\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);display:block;font-family:Segoe UI,system-ui,sans-serif}.chord-section{margin-top:36px;text-align:center}.chord-section h2{margin:0 0 4px;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.chord-section .relative-label{margin:0 0 20px;font-size:.88rem;color:var(--_chords-muted)}.chord-grid{display:flex;flex-wrap:
|
|
278
|
-
}], propDecorators: {
|
|
521
|
+
args: [{ selector: 'chord-section', standalone: true, imports: [NgClass, ChordDiagram], template: "@if (rows().length) {\n <section class=\"chord-section\">\n @if (info(); as info) {\n <h2>{{ info.fullName }} \u2014 {{ text().diatonicChords }}</h2>\n <p class=\"relative-label\">{{ info.relativeKey }}</p>\n }\n <div class=\"chord-grid\">\n @for (row of rows(); track row.numeral) {\n <div class=\"chord-card\" [ngClass]=\"row.state\">\n <div class=\"chord-numeral\">{{ row.numeral }}</div>\n <div class=\"chord-name\">{{ row.chord }}</div>\n <div class=\"chord-type\">{{ row.chordType }}</div>\n <div class=\"chord-role\">{{ row.role }}</div>\n </div>\n }\n </div>\n\n <button type=\"button\" class=\"btn-diagrams-toggle\" (click)=\"toggleDiagrams()\">\n {{ diagramsExpanded() ? text().hideChordDiagrams : text().showChordDiagrams }}\n </button>\n\n @if (diagramsExpanded()) {\n <div class=\"diagram-grid-row\">\n <div class=\"diagram-grid\">\n @for (result of chordDiagrams(); track result.id) {\n @if (result.positions[positionIndexFor(result.id)]; as position) {\n <div class=\"diagram-wrap\">\n <button\n type=\"button\"\n class=\"diagram-nav-btn\"\n [disabled]=\"result.positions.length <= 1\"\n [attr.aria-label]=\"text().previousPosition\"\n (click)=\"stepPosition(result.id, result.positions.length, -1)\"\n >\n \u25B2\n </button>\n <app-chord-diagram\n [title]=\"result.displayName\"\n [position]=\"position\"\n [positionIndex]=\"positionIndexFor(result.id)\"\n />\n <button\n type=\"button\"\n class=\"diagram-nav-btn\"\n [disabled]=\"result.positions.length <= 1\"\n [attr.aria-label]=\"text().nextPosition\"\n (click)=\"stepPosition(result.id, result.positions.length, 1)\"\n >\n \u25BC\n </button>\n </div>\n }\n }\n </div>\n\n <div class=\"diagram-group-nav\">\n <button type=\"button\" [attr.aria-label]=\"text().previousPositionAll\" (click)=\"stepAllPositions(-1)\">\u25B2</button>\n <button type=\"button\" [attr.aria-label]=\"text().nextPositionAll\" (click)=\"stepAllPositions(1)\">\u25BC</button>\n </div>\n </div>\n }\n </section>\n}\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);display:block;font-family:Segoe UI,system-ui,sans-serif}.chord-section{margin-top:36px;text-align:center}.chord-section h2{margin:0 0 4px;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.chord-section .relative-label{margin:0 0 20px;font-size:.88rem;color:var(--_chords-muted)}.chord-grid{display:flex;flex-wrap:wrap;gap:8px;justify-content:center}.chord-card{border-radius:12px;padding:12px 14px;min-width:82px;flex-shrink:0;text-align:center;background:color-mix(in srgb,var(--_chords-text) 8%,var(--_chords-surface));border:2px solid transparent;transition:transform .15s ease,box-shadow .15s ease}.chord-card:hover{transform:translateY(-2px);box-shadow:0 4px 16px color-mix(in srgb,var(--_chords-text) 10%,transparent)}.chord-card .chord-numeral{font-size:.85rem;font-weight:700;font-style:italic;color:var(--_chords-muted)}.chord-card .chord-name{font-size:1.4rem;font-weight:800;color:var(--_chords-text);margin:2px 0;letter-spacing:-.02em}.chord-card .chord-type{font-size:.65rem;text-transform:uppercase;letter-spacing:.06em;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.chord-card .chord-role{font-size:.7rem;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));margin-top:2px}.chord-card.tonic{background:color-mix(in srgb,var(--_chords-highlight) 10%,var(--_chords-surface));border-color:var(--_chords-highlight)}.chord-card.tonic .chord-numeral,.chord-card.tonic .chord-name{color:color-mix(in srgb,var(--_chords-highlight) 58%,var(--_chords-text))}.chord-card.scale-major{background:color-mix(in srgb,var(--_chords-primary) 9%,var(--_chords-surface));border-color:var(--_chords-primary)}.chord-card.scale-major .chord-numeral,.chord-card.scale-major .chord-name{color:color-mix(in srgb,var(--_chords-primary) 65%,var(--_chords-text))}.chord-card.scale-diminished{background:color-mix(in srgb,var(--_chords-danger) 9%,var(--_chords-surface));border-color:var(--_chords-danger)}.chord-card.scale-diminished .chord-numeral,.chord-card.scale-diminished .chord-name{color:color-mix(in srgb,var(--_chords-danger) 65%,var(--_chords-text))}@media(max-width:480px){.chord-card{min-width:75px;padding:10px 12px}}.btn-diagrams-toggle{display:block;width:100%;margin-top:14px;padding:10px 14px;font-size:.82rem;font-weight:600;border:1px solid var(--_chords-border);border-radius:10px;background:transparent;color:var(--_chords-text);cursor:pointer;transition:border-color .15s,color .15s}.btn-diagrams-toggle:hover{border-color:var(--_chords-primary);color:var(--_chords-primary)}.diagram-grid-row{display:flex;align-items:center;gap:10px;margin-top:16px}.diagram-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(90px,1fr));justify-items:center;gap:12px;flex:1 1 auto;min-width:0}.diagram-wrap{display:flex;flex-direction:column;align-items:center;gap:4px;width:100%;max-width:130px}.diagram-nav-btn,.diagram-group-nav button{padding:2px 10px;font-size:.7rem;line-height:1.4;border:1px solid var(--_chords-border);border-radius:8px;background:transparent;color:var(--_chords-muted);cursor:pointer;transition:border-color .15s,color .15s}.diagram-nav-btn:hover:not(:disabled),.diagram-group-nav button:hover:not(:disabled){border-color:var(--_chords-primary);color:var(--_chords-primary)}.diagram-nav-btn:disabled,.diagram-group-nav button:disabled{opacity:.35;cursor:not-allowed}.diagram-nav-btn{width:100%}.diagram-group-nav{display:flex;flex-direction:column;justify-content:center;gap:8px;flex:0 0 auto}.diagram-group-nav button{padding:10px 8px}\n"] }]
|
|
522
|
+
}], ctorParameters: () => [], propDecorators: { selectedIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedIndex", required: false }] }], selectedType: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedType", required: false }] }], language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }] } });
|
|
279
523
|
class CircleOfFifthsComponent {
|
|
280
524
|
language = input('en', /* @ts-ignore */
|
|
281
525
|
...(ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []));
|
|
282
526
|
text = computed(() => COPY[this.language()], /* @ts-ignore */
|
|
283
527
|
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
284
|
-
CX = 300;
|
|
285
|
-
CY = 300;
|
|
286
|
-
KEY_SIG = { inner: 250, outer: 288 };
|
|
287
|
-
MAJOR = { inner: 172, outer: 250 };
|
|
288
|
-
MINOR = { inner: 102, outer: 172 };
|
|
289
|
-
keys = KEYS;
|
|
290
528
|
selectedIndex = signal(null, /* @ts-ignore */
|
|
291
529
|
...(ngDevMode ? [{ debugName: "selectedIndex" }] : /* istanbul ignore next */ []));
|
|
292
530
|
selectedType = signal(null, /* @ts-ignore */
|
|
@@ -295,15 +533,25 @@ class CircleOfFifthsComponent {
|
|
|
295
533
|
...(ngDevMode ? [{ debugName: "activeProgressionDefs" }] : /* istanbul ignore next */ []));
|
|
296
534
|
expandedCards = signal(new Set(), /* @ts-ignore */
|
|
297
535
|
...(ngDevMode ? [{ debugName: "expandedCards" }] : /* istanbul ignore next */ []));
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
536
|
+
transposeMap = signal(new Map(), /* @ts-ignore */
|
|
537
|
+
...(ngDevMode ? [{ debugName: "transposeMap" }] : /* istanbul ignore next */ []));
|
|
538
|
+
spiceMap = signal(new Map(), /* @ts-ignore */
|
|
539
|
+
...(ngDevMode ? [{ debugName: "spiceMap" }] : /* istanbul ignore next */ []));
|
|
540
|
+
constructor() {
|
|
541
|
+
// Restore selected key from URL hash (format: #{index}-{type})
|
|
542
|
+
const [idxStr, type] = location.hash.slice(1).split('-');
|
|
543
|
+
const idx = Number(idxStr);
|
|
544
|
+
if (!isNaN(idx) && idx >= 0 && idx < 12 && (type === 'major' || type === 'minor')) {
|
|
545
|
+
this.selectedIndex.set(idx);
|
|
546
|
+
this.selectedType.set(type);
|
|
547
|
+
}
|
|
548
|
+
// Keep URL hash in sync with selected key
|
|
549
|
+
effect(() => {
|
|
550
|
+
const idx = this.selectedIndex();
|
|
551
|
+
const type = this.selectedType();
|
|
552
|
+
const hash = idx !== null && type !== null ? `#${idx}-${type}` : '';
|
|
553
|
+
history.replaceState(null, '', location.pathname + location.search + hash);
|
|
554
|
+
});
|
|
307
555
|
}
|
|
308
556
|
selectKey(index, type) {
|
|
309
557
|
if (this.selectedIndex() === index && this.selectedType() === type) {
|
|
@@ -316,6 +564,8 @@ class CircleOfFifthsComponent {
|
|
|
316
564
|
}
|
|
317
565
|
this.activeProgressionDefs.set(null);
|
|
318
566
|
this.expandedCards.set(new Set());
|
|
567
|
+
this.transposeMap.set(new Map());
|
|
568
|
+
this.spiceMap.set(new Map());
|
|
319
569
|
}
|
|
320
570
|
randomizeProgressions() {
|
|
321
571
|
const type = this.selectedType();
|
|
@@ -324,6 +574,8 @@ class CircleOfFifthsComponent {
|
|
|
324
574
|
// ponytail: sort-shuffle is fine for UI randomness
|
|
325
575
|
this.activeProgressionDefs.set([...PROGRESSIONS[type]].sort(() => Math.random() - 0.5).slice(0, 4));
|
|
326
576
|
this.expandedCards.set(new Set());
|
|
577
|
+
this.transposeMap.set(new Map());
|
|
578
|
+
this.spiceMap.set(new Map());
|
|
327
579
|
}
|
|
328
580
|
toggleCard(name) {
|
|
329
581
|
this.expandedCards.update((set) => {
|
|
@@ -338,100 +590,26 @@ class CircleOfFifthsComponent {
|
|
|
338
590
|
: prog.chords.join(', ');
|
|
339
591
|
navigator.clipboard.writeText(text);
|
|
340
592
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return type === 'major' ? 'tonic' : 'scale-major';
|
|
348
|
-
if (index === s.prev || index === s.next)
|
|
349
|
-
return 'scale-major';
|
|
350
|
-
return '';
|
|
351
|
-
}
|
|
352
|
-
getMinorState(index) {
|
|
353
|
-
const s = this.slice;
|
|
354
|
-
if (!s)
|
|
355
|
-
return '';
|
|
356
|
-
const type = this.selectedType();
|
|
357
|
-
const dimIdx = (s.curr + 2) % 12;
|
|
358
|
-
if (index === s.curr)
|
|
359
|
-
return type === 'minor' ? 'tonic' : 'scale-minor';
|
|
360
|
-
if (index === s.prev || index === s.next)
|
|
361
|
-
return 'scale-minor';
|
|
362
|
-
if (index === dimIdx)
|
|
363
|
-
return 'scale-diminished';
|
|
364
|
-
return '';
|
|
593
|
+
setTranspose(nameEn, targetIdx) {
|
|
594
|
+
this.transposeMap.update((m) => {
|
|
595
|
+
const next = new Map(m);
|
|
596
|
+
targetIdx === this.selectedIndex() ? next.delete(nameEn) : next.set(nameEn, targetIdx);
|
|
597
|
+
return next;
|
|
598
|
+
});
|
|
365
599
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return 'I';
|
|
374
|
-
if (index === s.prev)
|
|
375
|
-
return 'IV';
|
|
376
|
-
if (index === s.next)
|
|
377
|
-
return 'V';
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
if (index === s.curr)
|
|
381
|
-
return 'III';
|
|
382
|
-
if (index === s.prev)
|
|
383
|
-
return 'VI';
|
|
384
|
-
if (index === s.next)
|
|
385
|
-
return 'VII';
|
|
386
|
-
}
|
|
387
|
-
return '';
|
|
600
|
+
cycleSpice(nameEn) {
|
|
601
|
+
this.spiceMap.update((m) => {
|
|
602
|
+
const next = new Map(m);
|
|
603
|
+
const nextLevel = ((next.get(nameEn) ?? 0) + 1) % 4;
|
|
604
|
+
nextLevel === 0 ? next.delete(nameEn) : next.set(nameEn, nextLevel);
|
|
605
|
+
return next;
|
|
606
|
+
});
|
|
388
607
|
}
|
|
389
|
-
|
|
390
|
-
const s = this.slice;
|
|
391
|
-
if (!s)
|
|
392
|
-
return '';
|
|
608
|
+
get transposeKeys() {
|
|
393
609
|
const type = this.selectedType();
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
return 'vi';
|
|
398
|
-
if (index === s.prev)
|
|
399
|
-
return 'ii';
|
|
400
|
-
if (index === s.next)
|
|
401
|
-
return 'iii';
|
|
402
|
-
if (index === dimIdx)
|
|
403
|
-
return 'vii°';
|
|
404
|
-
}
|
|
405
|
-
else {
|
|
406
|
-
if (index === s.curr)
|
|
407
|
-
return 'i';
|
|
408
|
-
if (index === s.prev)
|
|
409
|
-
return 'iv';
|
|
410
|
-
if (index === s.next)
|
|
411
|
-
return 'v';
|
|
412
|
-
if (index === dimIdx)
|
|
413
|
-
return 'ii°';
|
|
414
|
-
}
|
|
415
|
-
return '';
|
|
416
|
-
}
|
|
417
|
-
arcPath(innerR, outerR, index, gap = 1.5) {
|
|
418
|
-
const mid = index * 30 - 90;
|
|
419
|
-
const s = (mid - 15 + gap) * (Math.PI / 180);
|
|
420
|
-
const e = (mid + 15 - gap) * (Math.PI / 180);
|
|
421
|
-
const cx = this.CX, cy = this.CY;
|
|
422
|
-
const x1 = cx + outerR * Math.cos(s), y1 = cy + outerR * Math.sin(s);
|
|
423
|
-
const x2 = cx + outerR * Math.cos(e), y2 = cy + outerR * Math.sin(e);
|
|
424
|
-
const x3 = cx + innerR * Math.cos(e), y3 = cy + innerR * Math.sin(e);
|
|
425
|
-
const x4 = cx + innerR * Math.cos(s), y4 = cy + innerR * Math.sin(s);
|
|
426
|
-
const f = (n) => n.toFixed(2);
|
|
427
|
-
return `M${f(x1)} ${f(y1)} A${outerR} ${outerR} 0 0 1 ${f(x2)} ${f(y2)} L${f(x3)} ${f(y3)} A${innerR} ${innerR} 0 0 0 ${f(x4)} ${f(y4)}Z`;
|
|
428
|
-
}
|
|
429
|
-
textPos(midR, index) {
|
|
430
|
-
const a = (index * 30 - 90) * (Math.PI / 180);
|
|
431
|
-
return {
|
|
432
|
-
x: this.CX + midR * Math.cos(a),
|
|
433
|
-
y: this.CY + midR * Math.sin(a),
|
|
434
|
-
};
|
|
610
|
+
if (!type)
|
|
611
|
+
return [];
|
|
612
|
+
return KEYS.map((k) => ({ idx: k.index, name: type === 'major' ? k.major : k.minor }));
|
|
435
613
|
}
|
|
436
614
|
get chordTable() {
|
|
437
615
|
const idx = this.selectedIndex();
|
|
@@ -440,16 +618,6 @@ class CircleOfFifthsComponent {
|
|
|
440
618
|
return [];
|
|
441
619
|
return buildChordRows(idx, type, this.text());
|
|
442
620
|
}
|
|
443
|
-
get chordNames() {
|
|
444
|
-
return this.chordTable.map((r) => r.chord);
|
|
445
|
-
}
|
|
446
|
-
getAccidentalText(key) {
|
|
447
|
-
if (key.sharps > 0)
|
|
448
|
-
return `${key.sharps}♯`;
|
|
449
|
-
if (key.flats > 0)
|
|
450
|
-
return `${key.flats}♭`;
|
|
451
|
-
return '—';
|
|
452
|
-
}
|
|
453
621
|
get selectedInfo() {
|
|
454
622
|
const idx = this.selectedIndex();
|
|
455
623
|
const type = this.selectedType();
|
|
@@ -457,15 +625,7 @@ class CircleOfFifthsComponent {
|
|
|
457
625
|
return null;
|
|
458
626
|
const key = KEYS[idx];
|
|
459
627
|
const copy = this.text();
|
|
460
|
-
|
|
461
|
-
return {
|
|
462
|
-
name: type === 'major' ? key.major : key.minor,
|
|
463
|
-
scale,
|
|
464
|
-
fullName: `${type === 'major' ? key.major : key.minor} ${scale}`,
|
|
465
|
-
relativeKey: type === 'major'
|
|
466
|
-
? `${copy.relativeMinor}: ${key.minor}`
|
|
467
|
-
: `${copy.relativeMajor}: ${key.major}`,
|
|
468
|
-
};
|
|
628
|
+
return { fullName: `${type === 'major' ? key.major : key.minor} ${type === 'major' ? copy.major : copy.minor}` };
|
|
469
629
|
}
|
|
470
630
|
deriveSections(numerals, lookup) {
|
|
471
631
|
const type = this.selectedType();
|
|
@@ -495,26 +655,37 @@ class CircleOfFifthsComponent {
|
|
|
495
655
|
const table = this.chordTable;
|
|
496
656
|
if (!table.length)
|
|
497
657
|
return [];
|
|
498
|
-
const
|
|
658
|
+
const selIdx = this.selectedIndex();
|
|
659
|
+
const transposeMap = this.transposeMap();
|
|
660
|
+
const spiceMap = this.spiceMap();
|
|
499
661
|
const language = this.language();
|
|
500
662
|
const type = this.selectedType() === 'major' ? 'major' : 'minor';
|
|
501
663
|
const defs = this.activeProgressionDefs() ?? PROGRESSIONS[type].slice(0, 4);
|
|
502
|
-
return defs.map((definition) =>
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
664
|
+
return defs.map((definition) => {
|
|
665
|
+
const targetIdx = transposeMap.get(definition.name.en) ?? selIdx;
|
|
666
|
+
const spiceLevel = spiceMap.get(definition.name.en) ?? 0;
|
|
667
|
+
const baseRows = buildChordRows(targetIdx, type, this.text());
|
|
668
|
+
const lookup = new Map(baseRows.map((r) => [r.numeral, spiceChord(r.chord, r.numeral, spiceLevel)]));
|
|
669
|
+
return {
|
|
670
|
+
nameEn: definition.name.en,
|
|
671
|
+
name: definition.name[language],
|
|
672
|
+
mood: definition.mood[language],
|
|
673
|
+
genre: definition.genre[language],
|
|
674
|
+
numerals: definition.numerals,
|
|
675
|
+
chords: definition.numerals.map((n) => lookup.get(n) ?? n),
|
|
676
|
+
sections: this.deriveSections(definition.numerals, lookup),
|
|
677
|
+
transposeIdx: targetIdx,
|
|
678
|
+
spiceLevel,
|
|
679
|
+
};
|
|
680
|
+
});
|
|
510
681
|
}
|
|
511
682
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: CircleOfFifthsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
512
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: CircleOfFifthsComponent, isStandalone: true, selector: "the-chords-circle-of-fifths", inputs: { language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.lang": "language()" } }, ngImport: i0, template: "<div class=\"page\">\n <header class=\"page-header\">\n <h1>{{ text().title }}</h1>\n <p>{{ text().subtitle }}</p>\n </header>\n\n <div class=\"circle-wrapper\">\n <svg\n viewBox=\"0 0 600 600\"\n xmlns=\"http://www.w3.org/2000/svg\"\n [attr.aria-label]=\"text().ariaLabel\"\n >\n <!-- \u2500\u2500 Key Signature Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <path [attr.d]=\"arcPath(KEY_SIG.inner, KEY_SIG.outer, key.index)\" class=\"keysig-segment\" />\n <text\n [attr.x]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).y\"\n class=\"keysig-label\"\n >\n {{ getAccidentalText(key) }}\n </text>\n }\n\n <!-- \u2500\u2500 Major Keys Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <g\n [ngClass]=\"['major-group', getMajorState(key.index)]\"\n (click)=\"selectKey(key.index, 'major')\"\n >\n <path [attr.d]=\"arcPath(MAJOR.inner, MAJOR.outer, key.index)\" />\n\n <text\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\n [attr.y]=\"\n textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y +\n (getMajorRole(key.index) ? -9 : 0)\n \"\n class=\"major-label\"\n >\n {{ key.major }}\n </text>\n\n @if (getMajorRole(key.index)) {\n <text\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y + 11\"\n class=\"role-label\"\n >\n {{ getMajorRole(key.index) }}\n </text>\n }\n </g>\n }\n\n <!-- \u2500\u2500 Minor Keys Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <g\n [ngClass]=\"['minor-group', getMinorState(key.index)]\"\n (click)=\"selectKey(key.index, 'minor')\"\n >\n <path [attr.d]=\"arcPath(MINOR.inner, MINOR.outer, key.index)\" />\n\n <text\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\n [attr.y]=\"\n textPos((MINOR.inner + MINOR.outer) / 2, key.index).y +\n (getMinorRole(key.index) ? -8 : 0)\n \"\n class=\"minor-label\"\n >\n {{ key.minor }}\n </text>\n\n @if (getMinorRole(key.index)) {\n <text\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).y + 9\"\n class=\"role-label\"\n >\n {{ getMinorRole(key.index) }}\n </text>\n }\n </g>\n }\n\n <!-- \u2500\u2500 Outer border \u2500\u2500 -->\n <circle cx=\"300\" cy=\"300\" r=\"289\" class=\"outer-border\" />\n\n <!-- \u2500\u2500 Center Circle \u2500\u2500 -->\n <circle cx=\"300\" cy=\"300\" r=\"99\" class=\"center-circle\" />\n\n @if (selectedInfo) {\n <text x=\"300\" y=\"283\" class=\"center-name\">{{ selectedInfo.name }}</text>\n <text x=\"300\" y=\"304\" class=\"center-scale\">{{ selectedInfo.scale }}</text>\n <text x=\"300\" y=\"322\" class=\"center-relative\">{{ selectedInfo.relativeKey }}</text>\n <text x=\"300\" y=\"338\" class=\"center-deselect\">{{ text().deselect }}</text>\n } @else {\n <text x=\"300\" y=\"293\" class=\"center-hint\">{{ text().clickKey }}</text>\n <text x=\"300\" y=\"312\" class=\"center-hint-sm\">{{ text().seeChords }}</text>\n }\n </svg>\n </div>\n\n <!-- \u2500\u2500 Legend \u2500\u2500 -->\n <div class=\"legend\">\n <span class=\"legend-item tonic\">{{ text().tonicLegend }}</span>\n <span class=\"legend-item scale-major\">{{ text().majorLegend }}</span>\n <span class=\"legend-item scale-minor\">{{ text().minorLegend }}</span>\n <span class=\"legend-item scale-diminished\">{{ text().diminishedLegend }}</span>\n </div>\n\n <chord-section [chords]=\"chordNames\" [language]=\"language()\"></chord-section>\n\n <!-- \u2500\u2500 Chord Progressions \u2500\u2500 -->\n @if (progressions.length) {\n <section class=\"progressions-section\">\n <div class=\"progressions-header\">\n <h2>{{ text().commonProgressions }} {{ selectedInfo?.fullName }}</h2>\n <button class=\"btn-randomize\" (click)=\"randomizeProgressions()\">{{ text().generateProgressions }}</button>\n </div>\n <div class=\"progression-list\">\n @for (prog of progressions; track prog.name) {\n <div class=\"progression-card\">\n <div class=\"prog-header\">\n <span class=\"prog-mood\">{{ prog.mood }}</span>\n <span class=\"prog-name\">{{ prog.name }}</span>\n <span class=\"prog-genre\">{{ prog.genre }}</span>\n </div>\n <div class=\"prog-chords\">\n @for (chord of prog.chords; track $index; let last = $last) {\n <span class=\"prog-chord\">{{ chord }}</span>\n @if (!last) {\n <span class=\"prog-arrow\">\u2192</span>\n }\n }\n </div>\n <div class=\"prog-numerals\">\n @for (num of prog.numerals; track $index; let last = $last) {\n <span>{{ num }}</span>\n @if (!last) {\n <span class=\"prog-arrow\">\u2192</span>\n }\n }\n </div>\n <div class=\"prog-expand-row\">\n <button class=\"btn-expand\" (click)=\"toggleCard(prog.name)\">\n {{ expandedCards().has(prog.name) ? text().collapse : text().expand }}\n </button>\n <button class=\"btn-expand\" (click)=\"copyChords(prog)\">\n {{ text().copy }}\n </button>\n </div>\n @if (expandedCards().has(prog.name)) {\n <div class=\"prog-sections\">\n @for (section of prog.sections; track section.label) {\n <div class=\"prog-section\">\n <div class=\"prog-section-label\">{{ section.label }}</div>\n <div class=\"prog-chords\">\n @for (chord of section.chords; track $index; let last = $last) {\n <span class=\"prog-chord\">{{ chord }}</span>\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\n }\n </div>\n <div class=\"prog-numerals\">\n @for (num of section.numerals; track $index; let last = $last) {\n <span>{{ num }}</span>\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </section>\n }\n</div>\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-background-alt: var(--chords-background-alt, #f8fafc);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);box-sizing:border-box;display:block;min-height:100%;background:var(--_chords-background);color:var(--_chords-text)}:host *,:host *:before,:host *:after{box-sizing:inherit}.page{max-width:720px;margin:0 auto;padding:24px 16px 48px;font-family:Segoe UI,system-ui,sans-serif}.page-header{text-align:center;margin-bottom:28px}.page-header h1{margin:0 0 6px;font-size:2rem;font-weight:800;color:var(--_chords-text);letter-spacing:-.02em}.page-header p{margin:0;color:var(--_chords-muted);font-size:.95rem}.circle-wrapper{max-width:580px;margin:0 auto}.circle-wrapper svg{display:block;width:100%;height:auto;overflow:visible}.keysig-segment{fill:var(--_chords-text)}.keysig-label{fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));font-size:12px;font-weight:500;text-anchor:middle;dominant-baseline:central;pointer-events:none}.major-group{cursor:pointer}.major-group path{fill:color-mix(in srgb,var(--_chords-primary) 14%,var(--_chords-surface));stroke:var(--_chords-surface);stroke-width:2;transition:fill .18s ease}.major-group:hover path{fill:color-mix(in srgb,var(--_chords-primary) 24%,var(--_chords-surface))}.major-group .major-label{fill:var(--_chords-text);font-size:15px;font-weight:700;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.major-group.tonic path{fill:var(--_chords-highlight)}.major-group.tonic:hover path{fill:color-mix(in srgb,var(--_chords-highlight) 84%,var(--_chords-text))}.major-group.tonic .major-label{fill:var(--_chords-on-primary)}.major-group.scale-major path{fill:var(--_chords-primary)}.major-group.scale-major:hover path{fill:color-mix(in srgb,var(--_chords-primary) 82%,var(--_chords-text))}.major-group.scale-major .major-label{fill:var(--_chords-on-primary)}.minor-group{cursor:pointer}.minor-group path{fill:color-mix(in srgb,var(--_chords-secondary) 10%,var(--_chords-surface));stroke:var(--_chords-surface);stroke-width:2;transition:fill .18s ease}.minor-group:hover path{fill:color-mix(in srgb,var(--_chords-secondary) 20%,var(--_chords-surface))}.minor-group .minor-label{fill:color-mix(in srgb,var(--_chords-text) 78%,var(--_chords-muted));font-size:12px;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.minor-group.tonic path{fill:var(--_chords-highlight)}.minor-group.tonic:hover path{fill:color-mix(in srgb,var(--_chords-highlight) 84%,var(--_chords-text))}.minor-group.tonic .minor-label{fill:var(--_chords-on-primary)}.minor-group.scale-minor path{fill:var(--_chords-secondary)}.minor-group.scale-minor:hover path{fill:color-mix(in srgb,var(--_chords-secondary) 82%,var(--_chords-text))}.minor-group.scale-minor .minor-label{fill:var(--_chords-on-primary)}.minor-group.scale-diminished path{fill:var(--_chords-danger)}.minor-group.scale-diminished:hover path{fill:color-mix(in srgb,var(--_chords-danger) 82%,var(--_chords-text))}.minor-group.scale-diminished .minor-label{fill:var(--_chords-on-primary)}.role-label{font-size:10px;font-style:italic;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;fill:color-mix(in srgb,var(--_chords-on-primary) 85%,transparent)}.outer-border{fill:none;stroke:var(--_chords-text);stroke-width:2.5}.center-circle{fill:var(--_chords-surface);stroke:var(--_chords-border);stroke-width:2;filter:drop-shadow(0 2px 10px color-mix(in srgb,var(--_chords-text) 8%,transparent))}.center-name{font-size:30px;font-weight:800;text-anchor:middle;dominant-baseline:auto;fill:var(--_chords-text);letter-spacing:-.02em}.center-scale{font-size:13px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:var(--_chords-muted);text-transform:uppercase;letter-spacing:.08em}.center-relative{font-size:9.5px;font-weight:500;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.center-deselect{font-size:8px;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background))}.center-hint{font-size:16px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.center-hint-sm{font-size:11px;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background))}.legend{display:flex;justify-content:center;gap:16px;flex-wrap:wrap;margin:16px 0 0}.legend .legend-item{display:flex;align-items:center;gap:6px;font-size:.8rem;color:var(--_chords-muted)}.legend .legend-item:before{content:\"\";display:inline-block;width:14px;height:14px;border-radius:3px}.legend .legend-item.tonic:before{background:var(--_chords-highlight)}.legend .legend-item.scale-major:before{background:var(--_chords-primary)}.legend .legend-item.scale-minor:before{background:var(--_chords-secondary)}.legend .legend-item.scale-diminished:before{background:var(--_chords-danger)}.progressions-section{margin-top:36px;text-align:center}.progressions-header{display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:20px;flex-wrap:wrap}.progressions-header h2{margin:0;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.btn-randomize{padding:6px 14px;font-size:.8rem;font-weight:600;border:1.5px solid var(--_chords-primary);border-radius:20px;background:transparent;color:var(--_chords-primary);cursor:pointer;transition:background .15s,color .15s;white-space:nowrap}.btn-randomize:hover{background:var(--_chords-primary);color:var(--_chords-on-primary)}.progression-list{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}@media(max-width:480px){.progression-list{grid-template-columns:1fr}}.progression-card{background:var(--_chords-surface);border:1.5px solid var(--_chords-border);border-radius:14px;padding:16px 18px;text-align:left;transition:box-shadow .15s ease,transform .15s ease}.progression-card:hover{box-shadow:0 4px 16px color-mix(in srgb,var(--_chords-text) 8%,transparent);transform:translateY(-2px)}.prog-header{display:flex;align-items:baseline;gap:8px;margin-bottom:12px;flex-wrap:wrap}.prog-mood{font-size:.95rem;font-weight:700;color:var(--_chords-text)}.prog-name{font-size:.75rem;font-weight:600;color:var(--_chords-muted);text-transform:uppercase;letter-spacing:.06em}.prog-genre{margin-left:auto;font-size:.7rem;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));font-style:italic}.prog-chords{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:6px}.prog-chord{font-size:1.15rem;font-weight:800;color:var(--_chords-text);letter-spacing:-.02em}.prog-numerals{display:flex;align-items:center;gap:6px;font-size:.75rem;font-style:italic;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.prog-expand-row{display:flex;justify-content:flex-end;margin-top:10px}.btn-expand{padding:3px 10px;font-size:.72rem;font-weight:600;border:1px solid var(--_chords-border);border-radius:12px;background:transparent;color:var(--_chords-muted);cursor:pointer;transition:border-color .15s,color .15s}.btn-expand:hover{border-color:var(--_chords-primary);color:var(--_chords-primary)}.prog-sections{margin-top:12px;border-top:1px solid var(--_chords-border);padding-top:10px;display:flex;flex-direction:column;gap:10px}.prog-section{padding:8px 10px;border-radius:8px;background:color-mix(in srgb,var(--_chords-text) 4%,var(--_chords-surface))}.prog-section-label{font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--_chords-muted);margin-bottom:5px}.prog-arrow{color:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background));font-size:.8rem}@media(max-width:480px){.page-header h1{font-size:1.5rem}}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: ChordSectionComponent, selector: "chord-section", inputs: ["chords", "language"] }] });
|
|
683
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: CircleOfFifthsComponent, isStandalone: true, selector: "the-chords-circle-of-fifths", inputs: { language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.lang": "language()" } }, ngImport: i0, template: "<div class=\"page\">\n <header class=\"page-header\">\n <h1>{{ text().title }}</h1>\n <p>{{ text().subtitle }}</p>\n </header>\n\n <div class=\"circle-and-chords\">\n <circle-of-fifths-wheel\n class=\"circle-and-chords-wheel\"\n [language]=\"language()\"\n [selectedIndex]=\"selectedIndex()\"\n [selectedType]=\"selectedType()\"\n (keySelected)=\"selectKey($event.index, $event.type)\"\n />\n\n <chord-section\n class=\"circle-and-chords-section\"\n [selectedIndex]=\"selectedIndex()\"\n [selectedType]=\"selectedType()\"\n [language]=\"language()\"\n ></chord-section>\n </div>\n\n <!-- \u2500\u2500 Chord Progressions \u2500\u2500 -->\n @if (progressions.length) {\n <section class=\"progressions-section\">\n <div class=\"progressions-header\">\n <h2>{{ text().commonProgressions }} {{ selectedInfo?.fullName }}</h2>\n <button class=\"btn-randomize\" (click)=\"randomizeProgressions()\">{{ text().generateProgressions }}</button>\n </div>\n <div class=\"progression-list\">\n @for (prog of progressions; track prog.name) {\n <div class=\"progression-card\">\n <div class=\"prog-header\">\n <span class=\"prog-mood\">{{ prog.mood }}</span>\n <span class=\"prog-name\">{{ prog.name }}</span>\n <span class=\"prog-genre\">{{ prog.genre }}</span>\n </div>\n <div class=\"prog-chords\">\n @for (chord of prog.chords; track $index; let last = $last) {\n <span class=\"prog-chord\">{{ chord }}</span>\n @if (!last) {\n <span class=\"prog-arrow\">\u2192</span>\n }\n }\n </div>\n <div class=\"prog-numerals\">\n @for (num of prog.numerals; track $index; let last = $last) {\n <span>{{ num }}</span>\n @if (!last) {\n <span class=\"prog-arrow\">\u2192</span>\n }\n }\n </div>\n <div class=\"prog-transpose-row\">\n <label class=\"prog-transpose-label\">{{ text().transposeLabel }}</label>\n <select class=\"prog-transpose-select\" (change)=\"setTranspose(prog.nameEn, +$any($event.target).value)\">\n @for (key of transposeKeys; track key.idx) {\n <option [value]=\"key.idx\" [selected]=\"key.idx === prog.transposeIdx\">{{ key.name }}</option>\n }\n </select>\n </div>\n <div class=\"prog-expand-row\">\n <button class=\"btn-expand\" (click)=\"toggleCard(prog.name)\">\n {{ expandedCards().has(prog.name) ? text().collapse : text().expand }}\n </button>\n <button class=\"btn-expand\" (click)=\"copyChords(prog)\">\n {{ text().copy }}\n </button>\n <button class=\"btn-spice\" [class.btn-spice--active]=\"prog.spiceLevel > 0\" (click)=\"cycleSpice(prog.nameEn)\">\n {{ text().spiceLabels[prog.spiceLevel] }}\n </button>\n </div>\n @if (expandedCards().has(prog.name)) {\n <div class=\"prog-sections\">\n @for (section of prog.sections; track section.label) {\n <div class=\"prog-section\">\n <div class=\"prog-section-label\">{{ section.label }}</div>\n <div class=\"prog-chords\">\n @for (chord of section.chords; track $index; let last = $last) {\n <span class=\"prog-chord\">{{ chord }}</span>\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\n }\n </div>\n <div class=\"prog-numerals\">\n @for (num of section.numerals; track $index; let last = $last) {\n <span>{{ num }}</span>\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </section>\n }\n</div>\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-background-alt: var(--chords-background-alt, #f8fafc);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);box-sizing:border-box;display:block;min-height:100%;background:var(--_chords-background);color:var(--_chords-text)}:host *,:host *:before,:host *:after{box-sizing:inherit}.page{max-width:720px;margin:0 auto;padding:24px 16px 48px;font-family:Segoe UI,system-ui,sans-serif}@media(min-width:900px){.page{max-width:1320px}}.circle-and-chords{display:flex;flex-direction:column;gap:24px}@media(min-width:900px){.circle-and-chords{flex-direction:row;align-items:flex-start}}@media(min-width:900px){.circle-and-chords-wheel{flex:0 1 460px}}.circle-and-chords-section{min-width:0}@media(min-width:900px){.circle-and-chords-section{flex:1 1 auto}}.page-header{text-align:center;margin-bottom:28px}.page-header h1{margin:0 0 6px;font-size:2rem;font-weight:800;color:var(--_chords-text);letter-spacing:-.02em}.page-header p{margin:0;color:var(--_chords-muted);font-size:.95rem}.progressions-section{margin-top:36px;text-align:center}.progressions-header{display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:20px;flex-wrap:wrap}.progressions-header h2{margin:0;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.btn-randomize{padding:6px 14px;font-size:.8rem;font-weight:600;border:1.5px solid var(--_chords-primary);border-radius:20px;background:transparent;color:var(--_chords-primary);cursor:pointer;transition:background .15s,color .15s;white-space:nowrap}.btn-randomize:hover{background:var(--_chords-primary);color:var(--_chords-on-primary)}.progression-list{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}@media(max-width:480px){.progression-list{grid-template-columns:1fr}}.progression-card{background:var(--_chords-surface);border:1.5px solid var(--_chords-border);border-radius:14px;padding:16px 18px;text-align:left;transition:box-shadow .15s ease,transform .15s ease}.progression-card:hover{box-shadow:0 4px 16px color-mix(in srgb,var(--_chords-text) 8%,transparent);transform:translateY(-2px)}.prog-header{display:flex;align-items:baseline;gap:8px;margin-bottom:12px;flex-wrap:wrap}.prog-mood{font-size:.95rem;font-weight:700;color:var(--_chords-text)}.prog-name{font-size:.75rem;font-weight:600;color:var(--_chords-muted);text-transform:uppercase;letter-spacing:.06em}.prog-genre{margin-left:auto;font-size:.7rem;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));font-style:italic}.prog-chords{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:6px}.prog-chord{font-size:1.15rem;font-weight:800;color:var(--_chords-text);letter-spacing:-.02em}.prog-numerals{display:flex;align-items:center;gap:6px;font-size:.75rem;font-style:italic;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.prog-transpose-row{display:flex;align-items:center;gap:6px;margin-top:8px}.prog-transpose-label{font-size:.68rem;font-weight:600;color:var(--_chords-muted);white-space:nowrap}.prog-transpose-select{font-size:.72rem;font-weight:600;color:var(--_chords-muted);background:transparent;border:1px solid var(--_chords-border);border-radius:8px;padding:2px 6px;cursor:pointer;transition:border-color .15s,color .15s;appearance:auto}.prog-transpose-select:hover,.prog-transpose-select:focus{border-color:var(--_chords-primary);color:var(--_chords-primary);outline:none}.prog-expand-row{display:flex;justify-content:flex-end;margin-top:10px}.btn-spice{background:transparent;border:1px solid var(--cof-accent, #e09030);border-radius:6px;color:var(--cof-accent, #e09030);cursor:pointer;font-size:.75rem;padding:4px 10px;transition:background .2s,color .2s}.btn-spice:hover{background:var(--cof-accent, #e09030);color:#fff}.btn-spice.btn-spice--active{background:var(--cof-accent, #e09030);color:#fff;font-weight:600}.btn-expand{padding:3px 10px;font-size:.72rem;font-weight:600;border:1px solid var(--_chords-border);border-radius:12px;background:transparent;color:var(--_chords-muted);cursor:pointer;transition:border-color .15s,color .15s}.btn-expand:hover{border-color:var(--_chords-primary);color:var(--_chords-primary)}.prog-sections{margin-top:12px;border-top:1px solid var(--_chords-border);padding-top:10px;display:flex;flex-direction:column;gap:10px}.prog-section{padding:8px 10px;border-radius:8px;background:color-mix(in srgb,var(--_chords-text) 4%,var(--_chords-surface))}.prog-section-label{font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--_chords-muted);margin-bottom:5px}.prog-arrow{color:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background));font-size:.8rem}@media(max-width:480px){.page-header h1{font-size:1.5rem}}\n"], dependencies: [{ kind: "component", type: ChordSectionComponent, selector: "chord-section", inputs: ["selectedIndex", "selectedType", "language"] }, { kind: "component", type: CircleOfFifthsWheel, selector: "circle-of-fifths-wheel", inputs: ["language", "selectedIndex", "selectedType", "interactive"], outputs: ["keySelected"] }] });
|
|
513
684
|
}
|
|
514
685
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: CircleOfFifthsComponent, decorators: [{
|
|
515
686
|
type: Component,
|
|
516
|
-
args: [{ selector: 'the-chords-circle-of-fifths', standalone: true, imports: [NgClass, ChordSectionComponent], host: { '[attr.lang]': 'language()' }, template: "<div class=\"page\">\n <header class=\"page-header\">\n <h1>{{ text().title }}</h1>\n <p>{{ text().subtitle }}</p>\n </header>\n\n <div class=\"circle-wrapper\">\n <svg\n viewBox=\"0 0 600 600\"\n xmlns=\"http://www.w3.org/2000/svg\"\n [attr.aria-label]=\"text().ariaLabel\"\n >\n <!-- \u2500\u2500 Key Signature Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <path [attr.d]=\"arcPath(KEY_SIG.inner, KEY_SIG.outer, key.index)\" class=\"keysig-segment\" />\n <text\n [attr.x]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).y\"\n class=\"keysig-label\"\n >\n {{ getAccidentalText(key) }}\n </text>\n }\n\n <!-- \u2500\u2500 Major Keys Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <g\n [ngClass]=\"['major-group', getMajorState(key.index)]\"\n (click)=\"selectKey(key.index, 'major')\"\n >\n <path [attr.d]=\"arcPath(MAJOR.inner, MAJOR.outer, key.index)\" />\n\n <text\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\n [attr.y]=\"\n textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y +\n (getMajorRole(key.index) ? -9 : 0)\n \"\n class=\"major-label\"\n >\n {{ key.major }}\n </text>\n\n @if (getMajorRole(key.index)) {\n <text\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y + 11\"\n class=\"role-label\"\n >\n {{ getMajorRole(key.index) }}\n </text>\n }\n </g>\n }\n\n <!-- \u2500\u2500 Minor Keys Ring \u2500\u2500 -->\n @for (key of keys; track key.index) {\n <g\n [ngClass]=\"['minor-group', getMinorState(key.index)]\"\n (click)=\"selectKey(key.index, 'minor')\"\n >\n <path [attr.d]=\"arcPath(MINOR.inner, MINOR.outer, key.index)\" />\n\n <text\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\n [attr.y]=\"\n textPos((MINOR.inner + MINOR.outer) / 2, key.index).y +\n (getMinorRole(key.index) ? -8 : 0)\n \"\n class=\"minor-label\"\n >\n {{ key.minor }}\n </text>\n\n @if (getMinorRole(key.index)) {\n <text\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\n [attr.y]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).y + 9\"\n class=\"role-label\"\n >\n {{ getMinorRole(key.index) }}\n </text>\n }\n </g>\n }\n\n <!-- \u2500\u2500 Outer border \u2500\u2500 -->\n <circle cx=\"300\" cy=\"300\" r=\"289\" class=\"outer-border\" />\n\n <!-- \u2500\u2500 Center Circle \u2500\u2500 -->\n <circle cx=\"300\" cy=\"300\" r=\"99\" class=\"center-circle\" />\n\n @if (selectedInfo) {\n <text x=\"300\" y=\"283\" class=\"center-name\">{{ selectedInfo.name }}</text>\n <text x=\"300\" y=\"304\" class=\"center-scale\">{{ selectedInfo.scale }}</text>\n <text x=\"300\" y=\"322\" class=\"center-relative\">{{ selectedInfo.relativeKey }}</text>\n <text x=\"300\" y=\"338\" class=\"center-deselect\">{{ text().deselect }}</text>\n } @else {\n <text x=\"300\" y=\"293\" class=\"center-hint\">{{ text().clickKey }}</text>\n <text x=\"300\" y=\"312\" class=\"center-hint-sm\">{{ text().seeChords }}</text>\n }\n </svg>\n </div>\n\n <!-- \u2500\u2500 Legend \u2500\u2500 -->\n <div class=\"legend\">\n <span class=\"legend-item tonic\">{{ text().tonicLegend }}</span>\n <span class=\"legend-item scale-major\">{{ text().majorLegend }}</span>\n <span class=\"legend-item scale-minor\">{{ text().minorLegend }}</span>\n <span class=\"legend-item scale-diminished\">{{ text().diminishedLegend }}</span>\n </div>\n\n <chord-section [chords]=\"chordNames\" [language]=\"language()\"></chord-section>\n\n <!-- \u2500\u2500 Chord Progressions \u2500\u2500 -->\n @if (progressions.length) {\n <section class=\"progressions-section\">\n <div class=\"progressions-header\">\n <h2>{{ text().commonProgressions }} {{ selectedInfo?.fullName }}</h2>\n <button class=\"btn-randomize\" (click)=\"randomizeProgressions()\">{{ text().generateProgressions }}</button>\n </div>\n <div class=\"progression-list\">\n @for (prog of progressions; track prog.name) {\n <div class=\"progression-card\">\n <div class=\"prog-header\">\n <span class=\"prog-mood\">{{ prog.mood }}</span>\n <span class=\"prog-name\">{{ prog.name }}</span>\n <span class=\"prog-genre\">{{ prog.genre }}</span>\n </div>\n <div class=\"prog-chords\">\n @for (chord of prog.chords; track $index; let last = $last) {\n <span class=\"prog-chord\">{{ chord }}</span>\n @if (!last) {\n <span class=\"prog-arrow\">\u2192</span>\n }\n }\n </div>\n <div class=\"prog-numerals\">\n @for (num of prog.numerals; track $index; let last = $last) {\n <span>{{ num }}</span>\n @if (!last) {\n <span class=\"prog-arrow\">\u2192</span>\n }\n }\n </div>\n <div class=\"prog-expand-row\">\n <button class=\"btn-expand\" (click)=\"toggleCard(prog.name)\">\n {{ expandedCards().has(prog.name) ? text().collapse : text().expand }}\n </button>\n <button class=\"btn-expand\" (click)=\"copyChords(prog)\">\n {{ text().copy }}\n </button>\n </div>\n @if (expandedCards().has(prog.name)) {\n <div class=\"prog-sections\">\n @for (section of prog.sections; track section.label) {\n <div class=\"prog-section\">\n <div class=\"prog-section-label\">{{ section.label }}</div>\n <div class=\"prog-chords\">\n @for (chord of section.chords; track $index; let last = $last) {\n <span class=\"prog-chord\">{{ chord }}</span>\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\n }\n </div>\n <div class=\"prog-numerals\">\n @for (num of section.numerals; track $index; let last = $last) {\n <span>{{ num }}</span>\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </section>\n }\n</div>\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-background-alt: var(--chords-background-alt, #f8fafc);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);box-sizing:border-box;display:block;min-height:100%;background:var(--_chords-background);color:var(--_chords-text)}:host *,:host *:before,:host *:after{box-sizing:inherit}.page{max-width:720px;margin:0 auto;padding:24px 16px 48px;font-family:Segoe UI,system-ui,sans-serif}.page-header{text-align:center;margin-bottom:28px}.page-header h1{margin:0 0 6px;font-size:2rem;font-weight:800;color:var(--_chords-text);letter-spacing:-.02em}.page-header p{margin:0;color:var(--_chords-muted);font-size:.95rem}.circle-wrapper{max-width:580px;margin:0 auto}.circle-wrapper svg{display:block;width:100%;height:auto;overflow:visible}.keysig-segment{fill:var(--_chords-text)}.keysig-label{fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));font-size:12px;font-weight:500;text-anchor:middle;dominant-baseline:central;pointer-events:none}.major-group{cursor:pointer}.major-group path{fill:color-mix(in srgb,var(--_chords-primary) 14%,var(--_chords-surface));stroke:var(--_chords-surface);stroke-width:2;transition:fill .18s ease}.major-group:hover path{fill:color-mix(in srgb,var(--_chords-primary) 24%,var(--_chords-surface))}.major-group .major-label{fill:var(--_chords-text);font-size:15px;font-weight:700;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.major-group.tonic path{fill:var(--_chords-highlight)}.major-group.tonic:hover path{fill:color-mix(in srgb,var(--_chords-highlight) 84%,var(--_chords-text))}.major-group.tonic .major-label{fill:var(--_chords-on-primary)}.major-group.scale-major path{fill:var(--_chords-primary)}.major-group.scale-major:hover path{fill:color-mix(in srgb,var(--_chords-primary) 82%,var(--_chords-text))}.major-group.scale-major .major-label{fill:var(--_chords-on-primary)}.minor-group{cursor:pointer}.minor-group path{fill:color-mix(in srgb,var(--_chords-secondary) 10%,var(--_chords-surface));stroke:var(--_chords-surface);stroke-width:2;transition:fill .18s ease}.minor-group:hover path{fill:color-mix(in srgb,var(--_chords-secondary) 20%,var(--_chords-surface))}.minor-group .minor-label{fill:color-mix(in srgb,var(--_chords-text) 78%,var(--_chords-muted));font-size:12px;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.minor-group.tonic path{fill:var(--_chords-highlight)}.minor-group.tonic:hover path{fill:color-mix(in srgb,var(--_chords-highlight) 84%,var(--_chords-text))}.minor-group.tonic .minor-label{fill:var(--_chords-on-primary)}.minor-group.scale-minor path{fill:var(--_chords-secondary)}.minor-group.scale-minor:hover path{fill:color-mix(in srgb,var(--_chords-secondary) 82%,var(--_chords-text))}.minor-group.scale-minor .minor-label{fill:var(--_chords-on-primary)}.minor-group.scale-diminished path{fill:var(--_chords-danger)}.minor-group.scale-diminished:hover path{fill:color-mix(in srgb,var(--_chords-danger) 82%,var(--_chords-text))}.minor-group.scale-diminished .minor-label{fill:var(--_chords-on-primary)}.role-label{font-size:10px;font-style:italic;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;fill:color-mix(in srgb,var(--_chords-on-primary) 85%,transparent)}.outer-border{fill:none;stroke:var(--_chords-text);stroke-width:2.5}.center-circle{fill:var(--_chords-surface);stroke:var(--_chords-border);stroke-width:2;filter:drop-shadow(0 2px 10px color-mix(in srgb,var(--_chords-text) 8%,transparent))}.center-name{font-size:30px;font-weight:800;text-anchor:middle;dominant-baseline:auto;fill:var(--_chords-text);letter-spacing:-.02em}.center-scale{font-size:13px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:var(--_chords-muted);text-transform:uppercase;letter-spacing:.08em}.center-relative{font-size:9.5px;font-weight:500;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.center-deselect{font-size:8px;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background))}.center-hint{font-size:16px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.center-hint-sm{font-size:11px;text-anchor:middle;dominant-baseline:auto;fill:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background))}.legend{display:flex;justify-content:center;gap:16px;flex-wrap:wrap;margin:16px 0 0}.legend .legend-item{display:flex;align-items:center;gap:6px;font-size:.8rem;color:var(--_chords-muted)}.legend .legend-item:before{content:\"\";display:inline-block;width:14px;height:14px;border-radius:3px}.legend .legend-item.tonic:before{background:var(--_chords-highlight)}.legend .legend-item.scale-major:before{background:var(--_chords-primary)}.legend .legend-item.scale-minor:before{background:var(--_chords-secondary)}.legend .legend-item.scale-diminished:before{background:var(--_chords-danger)}.progressions-section{margin-top:36px;text-align:center}.progressions-header{display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:20px;flex-wrap:wrap}.progressions-header h2{margin:0;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.btn-randomize{padding:6px 14px;font-size:.8rem;font-weight:600;border:1.5px solid var(--_chords-primary);border-radius:20px;background:transparent;color:var(--_chords-primary);cursor:pointer;transition:background .15s,color .15s;white-space:nowrap}.btn-randomize:hover{background:var(--_chords-primary);color:var(--_chords-on-primary)}.progression-list{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}@media(max-width:480px){.progression-list{grid-template-columns:1fr}}.progression-card{background:var(--_chords-surface);border:1.5px solid var(--_chords-border);border-radius:14px;padding:16px 18px;text-align:left;transition:box-shadow .15s ease,transform .15s ease}.progression-card:hover{box-shadow:0 4px 16px color-mix(in srgb,var(--_chords-text) 8%,transparent);transform:translateY(-2px)}.prog-header{display:flex;align-items:baseline;gap:8px;margin-bottom:12px;flex-wrap:wrap}.prog-mood{font-size:.95rem;font-weight:700;color:var(--_chords-text)}.prog-name{font-size:.75rem;font-weight:600;color:var(--_chords-muted);text-transform:uppercase;letter-spacing:.06em}.prog-genre{margin-left:auto;font-size:.7rem;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));font-style:italic}.prog-chords{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:6px}.prog-chord{font-size:1.15rem;font-weight:800;color:var(--_chords-text);letter-spacing:-.02em}.prog-numerals{display:flex;align-items:center;gap:6px;font-size:.75rem;font-style:italic;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.prog-expand-row{display:flex;justify-content:flex-end;margin-top:10px}.btn-expand{padding:3px 10px;font-size:.72rem;font-weight:600;border:1px solid var(--_chords-border);border-radius:12px;background:transparent;color:var(--_chords-muted);cursor:pointer;transition:border-color .15s,color .15s}.btn-expand:hover{border-color:var(--_chords-primary);color:var(--_chords-primary)}.prog-sections{margin-top:12px;border-top:1px solid var(--_chords-border);padding-top:10px;display:flex;flex-direction:column;gap:10px}.prog-section{padding:8px 10px;border-radius:8px;background:color-mix(in srgb,var(--_chords-text) 4%,var(--_chords-surface))}.prog-section-label{font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--_chords-muted);margin-bottom:5px}.prog-arrow{color:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background));font-size:.8rem}@media(max-width:480px){.page-header h1{font-size:1.5rem}}\n"] }]
|
|
517
|
-
}], propDecorators: { language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }] } });
|
|
687
|
+
args: [{ selector: 'the-chords-circle-of-fifths', standalone: true, imports: [ChordSectionComponent, CircleOfFifthsWheel], host: { '[attr.lang]': 'language()' }, template: "<div class=\"page\">\n <header class=\"page-header\">\n <h1>{{ text().title }}</h1>\n <p>{{ text().subtitle }}</p>\n </header>\n\n <div class=\"circle-and-chords\">\n <circle-of-fifths-wheel\n class=\"circle-and-chords-wheel\"\n [language]=\"language()\"\n [selectedIndex]=\"selectedIndex()\"\n [selectedType]=\"selectedType()\"\n (keySelected)=\"selectKey($event.index, $event.type)\"\n />\n\n <chord-section\n class=\"circle-and-chords-section\"\n [selectedIndex]=\"selectedIndex()\"\n [selectedType]=\"selectedType()\"\n [language]=\"language()\"\n ></chord-section>\n </div>\n\n <!-- \u2500\u2500 Chord Progressions \u2500\u2500 -->\n @if (progressions.length) {\n <section class=\"progressions-section\">\n <div class=\"progressions-header\">\n <h2>{{ text().commonProgressions }} {{ selectedInfo?.fullName }}</h2>\n <button class=\"btn-randomize\" (click)=\"randomizeProgressions()\">{{ text().generateProgressions }}</button>\n </div>\n <div class=\"progression-list\">\n @for (prog of progressions; track prog.name) {\n <div class=\"progression-card\">\n <div class=\"prog-header\">\n <span class=\"prog-mood\">{{ prog.mood }}</span>\n <span class=\"prog-name\">{{ prog.name }}</span>\n <span class=\"prog-genre\">{{ prog.genre }}</span>\n </div>\n <div class=\"prog-chords\">\n @for (chord of prog.chords; track $index; let last = $last) {\n <span class=\"prog-chord\">{{ chord }}</span>\n @if (!last) {\n <span class=\"prog-arrow\">\u2192</span>\n }\n }\n </div>\n <div class=\"prog-numerals\">\n @for (num of prog.numerals; track $index; let last = $last) {\n <span>{{ num }}</span>\n @if (!last) {\n <span class=\"prog-arrow\">\u2192</span>\n }\n }\n </div>\n <div class=\"prog-transpose-row\">\n <label class=\"prog-transpose-label\">{{ text().transposeLabel }}</label>\n <select class=\"prog-transpose-select\" (change)=\"setTranspose(prog.nameEn, +$any($event.target).value)\">\n @for (key of transposeKeys; track key.idx) {\n <option [value]=\"key.idx\" [selected]=\"key.idx === prog.transposeIdx\">{{ key.name }}</option>\n }\n </select>\n </div>\n <div class=\"prog-expand-row\">\n <button class=\"btn-expand\" (click)=\"toggleCard(prog.name)\">\n {{ expandedCards().has(prog.name) ? text().collapse : text().expand }}\n </button>\n <button class=\"btn-expand\" (click)=\"copyChords(prog)\">\n {{ text().copy }}\n </button>\n <button class=\"btn-spice\" [class.btn-spice--active]=\"prog.spiceLevel > 0\" (click)=\"cycleSpice(prog.nameEn)\">\n {{ text().spiceLabels[prog.spiceLevel] }}\n </button>\n </div>\n @if (expandedCards().has(prog.name)) {\n <div class=\"prog-sections\">\n @for (section of prog.sections; track section.label) {\n <div class=\"prog-section\">\n <div class=\"prog-section-label\">{{ section.label }}</div>\n <div class=\"prog-chords\">\n @for (chord of section.chords; track $index; let last = $last) {\n <span class=\"prog-chord\">{{ chord }}</span>\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\n }\n </div>\n <div class=\"prog-numerals\">\n @for (num of section.numerals; track $index; let last = $last) {\n <span>{{ num }}</span>\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </section>\n }\n</div>\n", styles: [":host{--_chords-background: var(--chords-background, #f0f4f8);--_chords-background-alt: var(--chords-background-alt, #f8fafc);--_chords-surface: var(--chords-surface, #ffffff);--_chords-surface-elevated: var(--chords-surface-elevated, #f8fafc);--_chords-text: var(--chords-text, #1e293b);--_chords-muted: var(--chords-muted, #64748b);--_chords-primary: var(--chords-primary, #3b82f6);--_chords-secondary: var(--chords-secondary, #06b6d4);--_chords-highlight: var(--chords-highlight, #f59e0b);--_chords-danger: var(--chords-danger, #a855f7);--_chords-border: var(--chords-border, #e2e8f0);--_chords-on-primary: var(--chords-on-primary, #ffffff);box-sizing:border-box;display:block;min-height:100%;background:var(--_chords-background);color:var(--_chords-text)}:host *,:host *:before,:host *:after{box-sizing:inherit}.page{max-width:720px;margin:0 auto;padding:24px 16px 48px;font-family:Segoe UI,system-ui,sans-serif}@media(min-width:900px){.page{max-width:1320px}}.circle-and-chords{display:flex;flex-direction:column;gap:24px}@media(min-width:900px){.circle-and-chords{flex-direction:row;align-items:flex-start}}@media(min-width:900px){.circle-and-chords-wheel{flex:0 1 460px}}.circle-and-chords-section{min-width:0}@media(min-width:900px){.circle-and-chords-section{flex:1 1 auto}}.page-header{text-align:center;margin-bottom:28px}.page-header h1{margin:0 0 6px;font-size:2rem;font-weight:800;color:var(--_chords-text);letter-spacing:-.02em}.page-header p{margin:0;color:var(--_chords-muted);font-size:.95rem}.progressions-section{margin-top:36px;text-align:center}.progressions-header{display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:20px;flex-wrap:wrap}.progressions-header h2{margin:0;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.btn-randomize{padding:6px 14px;font-size:.8rem;font-weight:600;border:1.5px solid var(--_chords-primary);border-radius:20px;background:transparent;color:var(--_chords-primary);cursor:pointer;transition:background .15s,color .15s;white-space:nowrap}.btn-randomize:hover{background:var(--_chords-primary);color:var(--_chords-on-primary)}.progression-list{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}@media(max-width:480px){.progression-list{grid-template-columns:1fr}}.progression-card{background:var(--_chords-surface);border:1.5px solid var(--_chords-border);border-radius:14px;padding:16px 18px;text-align:left;transition:box-shadow .15s ease,transform .15s ease}.progression-card:hover{box-shadow:0 4px 16px color-mix(in srgb,var(--_chords-text) 8%,transparent);transform:translateY(-2px)}.prog-header{display:flex;align-items:baseline;gap:8px;margin-bottom:12px;flex-wrap:wrap}.prog-mood{font-size:.95rem;font-weight:700;color:var(--_chords-text)}.prog-name{font-size:.75rem;font-weight:600;color:var(--_chords-muted);text-transform:uppercase;letter-spacing:.06em}.prog-genre{margin-left:auto;font-size:.7rem;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background));font-style:italic}.prog-chords{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:6px}.prog-chord{font-size:1.15rem;font-weight:800;color:var(--_chords-text);letter-spacing:-.02em}.prog-numerals{display:flex;align-items:center;gap:6px;font-size:.75rem;font-style:italic;color:color-mix(in srgb,var(--_chords-muted) 72%,var(--_chords-background))}.prog-transpose-row{display:flex;align-items:center;gap:6px;margin-top:8px}.prog-transpose-label{font-size:.68rem;font-weight:600;color:var(--_chords-muted);white-space:nowrap}.prog-transpose-select{font-size:.72rem;font-weight:600;color:var(--_chords-muted);background:transparent;border:1px solid var(--_chords-border);border-radius:8px;padding:2px 6px;cursor:pointer;transition:border-color .15s,color .15s;appearance:auto}.prog-transpose-select:hover,.prog-transpose-select:focus{border-color:var(--_chords-primary);color:var(--_chords-primary);outline:none}.prog-expand-row{display:flex;justify-content:flex-end;margin-top:10px}.btn-spice{background:transparent;border:1px solid var(--cof-accent, #e09030);border-radius:6px;color:var(--cof-accent, #e09030);cursor:pointer;font-size:.75rem;padding:4px 10px;transition:background .2s,color .2s}.btn-spice:hover{background:var(--cof-accent, #e09030);color:#fff}.btn-spice.btn-spice--active{background:var(--cof-accent, #e09030);color:#fff;font-weight:600}.btn-expand{padding:3px 10px;font-size:.72rem;font-weight:600;border:1px solid var(--_chords-border);border-radius:12px;background:transparent;color:var(--_chords-muted);cursor:pointer;transition:border-color .15s,color .15s}.btn-expand:hover{border-color:var(--_chords-primary);color:var(--_chords-primary)}.prog-sections{margin-top:12px;border-top:1px solid var(--_chords-border);padding-top:10px;display:flex;flex-direction:column;gap:10px}.prog-section{padding:8px 10px;border-radius:8px;background:color-mix(in srgb,var(--_chords-text) 4%,var(--_chords-surface))}.prog-section-label{font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--_chords-muted);margin-bottom:5px}.prog-arrow{color:color-mix(in srgb,var(--_chords-muted) 42%,var(--_chords-background));font-size:.8rem}@media(max-width:480px){.page-header h1{font-size:1.5rem}}\n"] }]
|
|
688
|
+
}], ctorParameters: () => [], propDecorators: { language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }] } });
|
|
518
689
|
|
|
519
690
|
/*
|
|
520
691
|
* Public API Surface of circle-of-fifths-lib
|
|
@@ -524,5 +695,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
|
|
|
524
695
|
* Generated bundle index. Do not edit.
|
|
525
696
|
*/
|
|
526
697
|
|
|
527
|
-
export { ChordSectionComponent, CircleOfFifthsComponent };
|
|
698
|
+
export { ChordSectionComponent, CircleOfFifthsComponent, CircleOfFifthsWheel };
|
|
528
699
|
//# sourceMappingURL=gblp-circle-of-fifths.mjs.map
|