@gblp/circle-of-fifths 0.1.0 → 0.2.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.
package/README.md
CHANGED
|
@@ -8,9 +8,28 @@ import { CircleOfFifthsComponent } from '@gblp/circle-of-fifths';
|
|
|
8
8
|
|
|
9
9
|
@Component({
|
|
10
10
|
imports: [CircleOfFifthsComponent],
|
|
11
|
-
template:
|
|
11
|
+
template: `<the-chords-circle-of-fifths [language]="'es'" />`,
|
|
12
12
|
})
|
|
13
13
|
export class App {}
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
`language` accepts `en` or `es` and defaults to `en`.
|
|
17
|
+
|
|
18
|
+
Override the component theme from any ancestor with these inherited CSS variables:
|
|
19
|
+
|
|
20
|
+
```css
|
|
21
|
+
--chords-background;
|
|
22
|
+
--chords-background-alt;
|
|
23
|
+
--chords-surface;
|
|
24
|
+
--chords-surface-elevated;
|
|
25
|
+
--chords-text;
|
|
26
|
+
--chords-muted;
|
|
27
|
+
--chords-primary;
|
|
28
|
+
--chords-secondary;
|
|
29
|
+
--chords-highlight;
|
|
30
|
+
--chords-danger;
|
|
31
|
+
--chords-border;
|
|
32
|
+
--chords-on-primary;
|
|
33
|
+
```
|
|
34
|
+
|
|
16
35
|
Build the package with `npm run build:lib`. The publishable Angular package is written to `dist/circle-of-fifths-lib`.
|
|
@@ -1,20 +1,126 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, Component } from '@angular/core';
|
|
2
|
+
import { input, computed, signal, Component } from '@angular/core';
|
|
3
3
|
import { NgClass } from '@angular/common';
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
const PROGRESSIONS = {
|
|
6
|
+
major: [
|
|
7
|
+
{
|
|
8
|
+
name: { en: 'Anthemic', es: 'Épica' },
|
|
9
|
+
mood: { en: '😄 Joyful', es: '😄 Alegre' },
|
|
10
|
+
genre: { en: 'Folk, Rock', es: 'Folk, rock' },
|
|
11
|
+
numerals: ['I', 'IV', 'V', 'I'],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: { en: 'Hopeful', es: 'Esperanzadora' },
|
|
15
|
+
mood: { en: '✨ Hopeful', es: '✨ Esperanzadora' },
|
|
16
|
+
genre: { en: 'Pop, Indie', es: 'Pop, indie' },
|
|
17
|
+
numerals: ['I', 'V', 'vi', 'IV'],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: { en: 'Melancholic', es: 'Melancólica' },
|
|
21
|
+
mood: { en: '😢 Emotional', es: '😢 Emocional' },
|
|
22
|
+
genre: { en: 'Ballads, Pop', es: 'Baladas, pop' },
|
|
23
|
+
numerals: ['vi', 'IV', 'I', 'V'],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: { en: 'Cinematic', es: 'Cinemática' },
|
|
27
|
+
mood: { en: '🎬 Tense', es: '🎬 Tensa' },
|
|
28
|
+
genre: { en: 'Film, Jazz', es: 'Cine, jazz' },
|
|
29
|
+
numerals: ['ii', 'V', 'vii°', 'I'],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
minor: [
|
|
33
|
+
{
|
|
34
|
+
name: { en: 'Brooding', es: 'Sombría' },
|
|
35
|
+
mood: { en: '🌑 Dark', es: '🌑 Oscura' },
|
|
36
|
+
genre: { en: 'Rock, Metal', es: 'Rock, metal' },
|
|
37
|
+
numerals: ['i', 'VII', 'VI', 'VII'],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: { en: 'Haunting', es: 'Inquietante' },
|
|
41
|
+
mood: { en: '👻 Mysterious', es: '👻 Misteriosa' },
|
|
42
|
+
genre: { en: 'Film, Gothic', es: 'Cine, gótico' },
|
|
43
|
+
numerals: ['i', 'iv', 'VII', 'III'],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: { en: 'Driving', es: 'Impulsora' },
|
|
47
|
+
mood: { en: '⚡ Urgent', es: '⚡ Urgente' },
|
|
48
|
+
genre: { en: 'Pop, EDM', es: 'Pop, EDM' },
|
|
49
|
+
numerals: ['i', 'VI', 'III', 'VII'],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: { en: 'Wistful', es: 'Nostálgica' },
|
|
53
|
+
mood: { en: '🌙 Wistful', es: '🌙 Nostálgica' },
|
|
54
|
+
genre: { en: 'Cinematic', es: 'Cinemática' },
|
|
55
|
+
numerals: ['i', 'v', 'VI', 'VII'],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
const COPY = {
|
|
60
|
+
en: {
|
|
61
|
+
title: 'Circle of Fifths',
|
|
62
|
+
subtitle: 'Click any major or minor key to highlight its diatonic chords',
|
|
63
|
+
ariaLabel: 'Circle of Fifths',
|
|
64
|
+
clickKey: 'Click a key',
|
|
65
|
+
seeChords: 'to see its chords',
|
|
66
|
+
deselect: '↩ click to deselect',
|
|
67
|
+
tonicLegend: 'Tonic (I / i)',
|
|
68
|
+
majorLegend: 'Major chords (IV, V)',
|
|
69
|
+
minorLegend: 'Minor chords (ii, iii, vi)',
|
|
70
|
+
diminishedLegend: 'Diminished (vii° / ii°)',
|
|
71
|
+
diatonicChords: 'Diatonic Chords',
|
|
72
|
+
commonProgressions: 'Common Progressions in',
|
|
73
|
+
major: 'Major',
|
|
74
|
+
minor: 'Minor',
|
|
75
|
+
diminished: 'diminished',
|
|
76
|
+
relativeMinor: 'Relative minor',
|
|
77
|
+
relativeMajor: 'Relative major',
|
|
78
|
+
roles: {
|
|
79
|
+
tonic: 'Tonic',
|
|
80
|
+
supertonic: 'Supertonic',
|
|
81
|
+
mediant: 'Mediant',
|
|
82
|
+
subdominant: 'Subdominant',
|
|
83
|
+
dominant: 'Dominant',
|
|
84
|
+
submediant: 'Submediant',
|
|
85
|
+
leadingTone: 'Leading Tone',
|
|
86
|
+
subtonic: 'Subtonic',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
es: {
|
|
90
|
+
title: 'Círculo de quintas',
|
|
91
|
+
subtitle: 'Haz clic en una tonalidad mayor o menor para resaltar sus acordes diatónicos',
|
|
92
|
+
ariaLabel: 'Círculo de quintas',
|
|
93
|
+
clickKey: 'Haz clic en una tonalidad',
|
|
94
|
+
seeChords: 'para ver sus acordes',
|
|
95
|
+
deselect: '↩ haz clic para deseleccionar',
|
|
96
|
+
tonicLegend: 'Tónica (I / i)',
|
|
97
|
+
majorLegend: 'Acordes mayores (IV, V)',
|
|
98
|
+
minorLegend: 'Acordes menores (ii, iii, vi)',
|
|
99
|
+
diminishedLegend: 'Disminuido (vii° / ii°)',
|
|
100
|
+
diatonicChords: 'Acordes diatónicos',
|
|
101
|
+
commonProgressions: 'Progresiones comunes en',
|
|
102
|
+
major: 'Mayor',
|
|
103
|
+
minor: 'Menor',
|
|
104
|
+
diminished: 'disminuido',
|
|
105
|
+
relativeMinor: 'Relativa menor',
|
|
106
|
+
relativeMajor: 'Relativa mayor',
|
|
107
|
+
roles: {
|
|
108
|
+
tonic: 'Tónica',
|
|
109
|
+
supertonic: 'Supertónica',
|
|
110
|
+
mediant: 'Mediante',
|
|
111
|
+
subdominant: 'Subdominante',
|
|
112
|
+
dominant: 'Dominante',
|
|
113
|
+
submediant: 'Submediante',
|
|
114
|
+
leadingTone: 'Sensible',
|
|
115
|
+
subtonic: 'Subtónica',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
17
119
|
class CircleOfFifthsComponent {
|
|
120
|
+
language = input('en', /* @ts-ignore */
|
|
121
|
+
...(ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []));
|
|
122
|
+
text = computed(() => COPY[this.language()], /* @ts-ignore */
|
|
123
|
+
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
18
124
|
CX = 300;
|
|
19
125
|
CY = 300;
|
|
20
126
|
KEY_SIG = { inner: 250, outer: 288 };
|
|
@@ -171,13 +277,15 @@ class CircleOfFifthsComponent {
|
|
|
171
277
|
if (idx === null || type === null)
|
|
172
278
|
return null;
|
|
173
279
|
const key = this.keys[idx];
|
|
280
|
+
const copy = this.text();
|
|
281
|
+
const scale = type === 'major' ? copy.major : copy.minor;
|
|
174
282
|
return {
|
|
175
283
|
name: type === 'major' ? key.major : key.minor,
|
|
176
|
-
scale
|
|
177
|
-
fullName: type === 'major' ?
|
|
284
|
+
scale,
|
|
285
|
+
fullName: `${type === 'major' ? key.major : key.minor} ${scale}`,
|
|
178
286
|
relativeKey: type === 'major'
|
|
179
|
-
?
|
|
180
|
-
:
|
|
287
|
+
? `${copy.relativeMinor}: ${key.minor}`
|
|
288
|
+
: `${copy.relativeMajor}: ${key.major}`,
|
|
181
289
|
};
|
|
182
290
|
}
|
|
183
291
|
get chordTable() {
|
|
@@ -187,26 +295,111 @@ class CircleOfFifthsComponent {
|
|
|
187
295
|
return [];
|
|
188
296
|
const prev = (idx - 1 + 12) % 12;
|
|
189
297
|
const next = (idx + 1) % 12;
|
|
298
|
+
const copy = this.text();
|
|
190
299
|
if (type === 'major') {
|
|
191
300
|
return [
|
|
192
|
-
{
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
301
|
+
{
|
|
302
|
+
numeral: 'I',
|
|
303
|
+
chord: this.keys[idx].major,
|
|
304
|
+
chordType: copy.major,
|
|
305
|
+
role: copy.roles.tonic,
|
|
306
|
+
state: 'tonic',
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
numeral: 'ii',
|
|
310
|
+
chord: this.keys[prev].minor,
|
|
311
|
+
chordType: copy.minor,
|
|
312
|
+
role: copy.roles.supertonic,
|
|
313
|
+
state: 'scale-minor',
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
numeral: 'iii',
|
|
317
|
+
chord: this.keys[next].minor,
|
|
318
|
+
chordType: copy.minor,
|
|
319
|
+
role: copy.roles.mediant,
|
|
320
|
+
state: 'scale-minor',
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
numeral: 'IV',
|
|
324
|
+
chord: this.keys[prev].major,
|
|
325
|
+
chordType: copy.major,
|
|
326
|
+
role: copy.roles.subdominant,
|
|
327
|
+
state: 'scale-major',
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
numeral: 'V',
|
|
331
|
+
chord: this.keys[next].major,
|
|
332
|
+
chordType: copy.major,
|
|
333
|
+
role: copy.roles.dominant,
|
|
334
|
+
state: 'scale-major',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
numeral: 'vi',
|
|
338
|
+
chord: this.keys[idx].minor,
|
|
339
|
+
chordType: copy.minor,
|
|
340
|
+
role: copy.roles.submediant,
|
|
341
|
+
state: 'scale-minor',
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
numeral: 'vii°',
|
|
345
|
+
chord: this.dimChord(idx),
|
|
346
|
+
chordType: copy.diminished,
|
|
347
|
+
role: copy.roles.leadingTone,
|
|
348
|
+
state: 'scale-diminished',
|
|
349
|
+
},
|
|
199
350
|
];
|
|
200
351
|
}
|
|
201
352
|
else {
|
|
202
353
|
return [
|
|
203
|
-
{
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
354
|
+
{
|
|
355
|
+
numeral: 'i',
|
|
356
|
+
chord: this.keys[idx].minor,
|
|
357
|
+
chordType: copy.minor,
|
|
358
|
+
role: copy.roles.tonic,
|
|
359
|
+
state: 'tonic',
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
numeral: 'iv',
|
|
363
|
+
chord: this.keys[prev].minor,
|
|
364
|
+
chordType: copy.minor,
|
|
365
|
+
role: copy.roles.subdominant,
|
|
366
|
+
state: 'scale-minor',
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
numeral: 'v',
|
|
370
|
+
chord: this.keys[next].minor,
|
|
371
|
+
chordType: copy.minor,
|
|
372
|
+
role: copy.roles.dominant,
|
|
373
|
+
state: 'scale-minor',
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
numeral: 'III',
|
|
377
|
+
chord: this.keys[idx].major,
|
|
378
|
+
chordType: copy.major,
|
|
379
|
+
role: copy.roles.mediant,
|
|
380
|
+
state: 'scale-major',
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
numeral: 'VI',
|
|
384
|
+
chord: this.keys[prev].major,
|
|
385
|
+
chordType: copy.major,
|
|
386
|
+
role: copy.roles.submediant,
|
|
387
|
+
state: 'scale-major',
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
numeral: 'VII',
|
|
391
|
+
chord: this.keys[next].major,
|
|
392
|
+
chordType: copy.major,
|
|
393
|
+
role: copy.roles.subtonic,
|
|
394
|
+
state: 'scale-major',
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
numeral: 'ii°',
|
|
398
|
+
chord: this.dimChord(idx),
|
|
399
|
+
chordType: copy.diminished,
|
|
400
|
+
role: copy.roles.supertonic,
|
|
401
|
+
state: 'scale-diminished',
|
|
402
|
+
},
|
|
210
403
|
];
|
|
211
404
|
}
|
|
212
405
|
}
|
|
@@ -214,17 +407,24 @@ class CircleOfFifthsComponent {
|
|
|
214
407
|
const table = this.chordTable;
|
|
215
408
|
if (!table.length)
|
|
216
409
|
return [];
|
|
217
|
-
const lookup = new Map(table.map(r => [r.numeral, r.chord]));
|
|
218
|
-
const
|
|
219
|
-
|
|
410
|
+
const lookup = new Map(table.map((r) => [r.numeral, r.chord]));
|
|
411
|
+
const language = this.language();
|
|
412
|
+
const defs = PROGRESSIONS[this.selectedType() === 'major' ? 'major' : 'minor'];
|
|
413
|
+
return defs.map((definition) => ({
|
|
414
|
+
name: definition.name[language],
|
|
415
|
+
mood: definition.mood[language],
|
|
416
|
+
genre: definition.genre[language],
|
|
417
|
+
numerals: definition.numerals,
|
|
418
|
+
chords: definition.numerals.map((numeral) => lookup.get(numeral) ?? numeral),
|
|
419
|
+
}));
|
|
220
420
|
}
|
|
221
421
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: CircleOfFifthsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
222
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: CircleOfFifthsComponent, isStandalone: true, selector: "the-chords-circle-of-fifths", ngImport: i0, template: "<div class=\"page\">\n <header class=\"page-header\">\r\n <h1>Circle of Fifths</h1>\r\n <p>Click any major or minor key to highlight its diatonic chords</p>\r\n </header>\r\n\r\n <div class=\"circle-wrapper\">\r\n <svg viewBox=\"0 0 600 600\" xmlns=\"http://www.w3.org/2000/svg\" aria-label=\"Circle of Fifths\">\r\n\r\n <!-- \u2500\u2500 Key Signature Ring \u2500\u2500 -->\r\n @for (key of keys; track key.index) {\r\n <path\r\n [attr.d]=\"arcPath(KEY_SIG.inner, KEY_SIG.outer, key.index)\"\r\n class=\"keysig-segment\" />\r\n <text\r\n [attr.x]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).x\"\r\n [attr.y]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).y\"\r\n class=\"keysig-label\">\r\n {{ getAccidentalText(key) }}\r\n </text>\r\n }\r\n\r\n <!-- \u2500\u2500 Major Keys Ring \u2500\u2500 -->\r\n @for (key of keys; track key.index) {\r\n <g [ngClass]=\"['major-group', getMajorState(key.index)]\"\r\n (click)=\"selectKey(key.index, 'major')\">\r\n <path [attr.d]=\"arcPath(MAJOR.inner, MAJOR.outer, key.index)\" />\r\n\r\n <text\r\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\r\n [attr.y]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y + (getMajorRole(key.index) ? -9 : 0)\"\r\n class=\"major-label\">\r\n {{ key.major }}\r\n </text>\r\n\r\n @if (getMajorRole(key.index)) {\r\n <text\r\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\r\n [attr.y]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y + 11\"\r\n class=\"role-label\">\r\n {{ getMajorRole(key.index) }}\r\n </text>\r\n }\r\n </g>\r\n }\r\n\r\n <!-- \u2500\u2500 Minor Keys Ring \u2500\u2500 -->\r\n @for (key of keys; track key.index) {\r\n <g [ngClass]=\"['minor-group', getMinorState(key.index)]\"\r\n (click)=\"selectKey(key.index, 'minor')\">\r\n <path [attr.d]=\"arcPath(MINOR.inner, MINOR.outer, key.index)\" />\r\n\r\n <text\r\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\r\n [attr.y]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).y + (getMinorRole(key.index) ? -8 : 0)\"\r\n class=\"minor-label\">\r\n {{ key.minor }}\r\n </text>\r\n\r\n @if (getMinorRole(key.index)) {\r\n <text\r\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\r\n [attr.y]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).y + 9\"\r\n class=\"role-label\">\r\n {{ getMinorRole(key.index) }}\r\n </text>\r\n }\r\n </g>\r\n }\r\n\r\n <!-- \u2500\u2500 Outer border \u2500\u2500 -->\r\n <circle cx=\"300\" cy=\"300\" r=\"289\" fill=\"none\" stroke=\"#1e293b\" stroke-width=\"2.5\" />\r\n\r\n <!-- \u2500\u2500 Center Circle \u2500\u2500 -->\r\n <circle cx=\"300\" cy=\"300\" r=\"99\" class=\"center-circle\" />\r\n\r\n @if (selectedInfo) {\r\n <text x=\"300\" y=\"283\" class=\"center-name\">{{ selectedInfo.name }}</text>\r\n <text x=\"300\" y=\"304\" class=\"center-scale\">{{ selectedInfo.scale }}</text>\r\n <text x=\"300\" y=\"322\" class=\"center-relative\">{{ selectedInfo.relativeKey }}</text>\r\n <text x=\"300\" y=\"338\" class=\"center-deselect\">\u21A9 click to deselect</text>\r\n } @else {\r\n <text x=\"300\" y=\"293\" class=\"center-hint\">Click a key</text>\r\n <text x=\"300\" y=\"312\" class=\"center-hint-sm\">to see its chords</text>\r\n }\r\n\r\n </svg>\r\n </div>\r\n\r\n <!-- \u2500\u2500 Legend \u2500\u2500 -->\r\n <div class=\"legend\">\r\n <span class=\"legend-item tonic\">Tonic (I / i)</span>\r\n <span class=\"legend-item scale-major\">Major chords (IV, V)</span>\r\n <span class=\"legend-item scale-minor\">Minor chords (ii, iii, vi)</span>\r\n <span class=\"legend-item scale-diminished\">Diminished (vii\u00B0 / ii\u00B0)</span>\r\n </div>\r\n\r\n <!-- \u2500\u2500 Chord Table \u2500\u2500 -->\r\n @if (selectedInfo && chordTable.length) {\r\n <section class=\"chord-section\">\r\n <h2>{{ selectedInfo.fullName }} \u2014 Diatonic Chords</h2>\r\n <p class=\"relative-label\">{{ selectedInfo.relativeKey }}</p>\r\n <div class=\"chord-grid\">\r\n @for (row of chordTable; track row.numeral) {\r\n <div class=\"chord-card\" [ngClass]=\"row.state\">\r\n <div class=\"chord-numeral\">{{ row.numeral }}</div>\r\n <div class=\"chord-name\">{{ row.chord }}</div>\r\n <div class=\"chord-type\">{{ row.chordType }}</div>\r\n <div class=\"chord-role\">{{ row.role }}</div>\r\n </div>\r\n }\r\n </div>\r\n </section>\r\n }\r\n\r\n <!-- \u2500\u2500 Chord Progressions \u2500\u2500 -->\r\n @if (progressions.length) {\r\n <section class=\"progressions-section\">\r\n <h2>Common Progressions in {{ selectedInfo?.fullName }}</h2>\r\n <div class=\"progression-list\">\r\n @for (prog of progressions; track prog.name) {\r\n <div class=\"progression-card\">\r\n <div class=\"prog-header\">\r\n <span class=\"prog-mood\">{{ prog.mood }}</span>\r\n <span class=\"prog-name\">{{ prog.name }}</span>\r\n <span class=\"prog-genre\">{{ prog.genre }}</span>\r\n </div>\r\n <div class=\"prog-chords\">\r\n @for (chord of prog.chords; track $index; let last = $last) {\r\n <span class=\"prog-chord\">{{ chord }}</span>\r\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\r\n }\r\n </div>\r\n <div class=\"prog-numerals\">\r\n @for (num of prog.numerals; track $index; let last = $last) {\r\n <span>{{ num }}</span>\r\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </section>\r\n }\r\n</div>\r\n", styles: [":host{box-sizing:border-box;display:block;min-height:100%;background:#f0f4f8;color:#1e293b}: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:#1e293b;letter-spacing:-.02em}.page-header p{margin:0;color:#64748b;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:#1e293b}.keysig-label{fill:#94a3b8;font-size:12px;font-weight:500;text-anchor:middle;dominant-baseline:central;pointer-events:none}.major-group{cursor:pointer}.major-group path{fill:#dde5f0;stroke:#f8fafc;stroke-width:2;transition:fill .18s ease}.major-group:hover path{fill:#c8d4e8}.major-group .major-label{fill:#334155;font-size:15px;font-weight:700;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.major-group.tonic path{fill:#f59e0b}.major-group.tonic:hover path{fill:#d97706}.major-group.tonic .major-label{fill:#fff}.major-group.scale-major path{fill:#3b82f6}.major-group.scale-major:hover path{fill:#2563eb}.major-group.scale-major .major-label{fill:#fff}.minor-group{cursor:pointer}.minor-group path{fill:#eaf0f8;stroke:#f8fafc;stroke-width:2;transition:fill .18s ease}.minor-group:hover path{fill:#d5e1ef}.minor-group .minor-label{fill:#475569;font-size:12px;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.minor-group.tonic path{fill:#f59e0b}.minor-group.tonic:hover path{fill:#d97706}.minor-group.tonic .minor-label{fill:#fff}.minor-group.scale-minor path{fill:#06b6d4}.minor-group.scale-minor:hover path{fill:#0891b2}.minor-group.scale-minor .minor-label{fill:#fff}.minor-group.scale-diminished path{fill:#a855f7}.minor-group.scale-diminished:hover path{fill:#9333ea}.minor-group.scale-diminished .minor-label{fill:#fff}.role-label{font-size:10px;font-style:italic;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;fill:#ffffffd9}.center-circle{fill:#fff;stroke:#e2e8f0;stroke-width:2;filter:drop-shadow(0 2px 10px rgba(0,0,0,.08))}.center-name{font-size:30px;font-weight:800;text-anchor:middle;dominant-baseline:auto;fill:#1e293b;letter-spacing:-.02em}.center-scale{font-size:13px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:#64748b;text-transform:uppercase;letter-spacing:.08em}.center-relative{font-size:9.5px;font-weight:500;text-anchor:middle;dominant-baseline:auto;fill:#94a3b8}.center-deselect{font-size:8px;text-anchor:middle;dominant-baseline:auto;fill:#cbd5e1}.center-hint{font-size:16px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:#94a3b8}.center-hint-sm{font-size:11px;text-anchor:middle;dominant-baseline:auto;fill:#cbd5e1}.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:#64748b}.legend .legend-item:before{content:\"\";display:inline-block;width:14px;height:14px;border-radius:3px}.legend .legend-item.tonic:before{background:#f59e0b}.legend .legend-item.scale-major:before{background:#3b82f6}.legend .legend-item.scale-minor:before{background:#06b6d4}.legend .legend-item.scale-diminished:before{background:#a855f7}.chord-section{margin-top:36px;text-align:center}.chord-section h2{margin:0 0 4px;font-size:1.25rem;font-weight:700;color:#1e293b}.chord-section .relative-label{margin:0 0 20px;font-size:.88rem;color:#64748b}.chord-grid{display:flex;flex-wrap:nowrap;gap:8px;justify-content:center;overflow-x:auto;padding-bottom:4px}.chord-card{border-radius:12px;padding:12px 14px;min-width:82px;flex-shrink:0;text-align:center;background:#e8edf5;border:2px solid transparent;transition:transform .15s ease,box-shadow .15s ease}.chord-card:hover{transform:translateY(-2px);box-shadow:0 4px 16px #0000001a}.chord-card .chord-numeral{font-size:.85rem;font-weight:700;font-style:italic;color:#64748b}.chord-card .chord-name{font-size:1.4rem;font-weight:800;color:#1e293b;margin:2px 0;letter-spacing:-.02em}.chord-card .chord-type{font-size:.65rem;text-transform:uppercase;letter-spacing:.06em;color:#94a3b8}.chord-card .chord-role{font-size:.7rem;color:#94a3b8;margin-top:2px}.chord-card.tonic{background:#fef9ec;border-color:#f59e0b}.chord-card.tonic .chord-numeral,.chord-card.tonic .chord-name{color:#92400e}.chord-card.scale-major{background:#eff6ff;border-color:#3b82f6}.chord-card.scale-major .chord-numeral,.chord-card.scale-major .chord-name{color:#1e40af}.chord-card.scale-diminished{background:#fdf4ff;border-color:#a855f7}.chord-card.scale-diminished .chord-numeral,.chord-card.scale-diminished .chord-name{color:#6b21a8}.progressions-section{margin-top:36px;text-align:center}.progressions-section h2{margin:0 0 20px;font-size:1.25rem;font-weight:700;color:#1e293b}.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:#fff;border:1.5px solid #e2e8f0;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 #00000014;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:#1e293b}.prog-name{font-size:.75rem;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.06em}.prog-genre{margin-left:auto;font-size:.7rem;color:#94a3b8;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:#1e293b;letter-spacing:-.02em}.prog-numerals{display:flex;align-items:center;gap:6px;font-size:.75rem;font-style:italic;color:#94a3b8}.prog-arrow{color:#cbd5e1;font-size:.8rem}@media(max-width:480px){.page-header h1{font-size:1.5rem}.chord-card{min-width:75px;padding:10px 12px}}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
|
|
422
|
+
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 <!-- \u2500\u2500 Chord Table \u2500\u2500 -->\n @if (selectedInfo && chordTable.length) {\n <section class=\"chord-section\">\n <h2>{{ selectedInfo.fullName }} \u2014 {{ text().diatonicChords }}</h2>\n <p class=\"relative-label\">{{ selectedInfo.relativeKey }}</p>\n <div class=\"chord-grid\">\n @for (row of chordTable; 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\n <!-- \u2500\u2500 Chord Progressions \u2500\u2500 -->\n @if (progressions.length) {\n <section class=\"progressions-section\">\n <h2>{{ text().commonProgressions }} {{ selectedInfo?.fullName }}</h2>\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>\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)}.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:nowrap;gap:8px;justify-content:center;overflow-x:auto;padding-bottom:4px}.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))}.progressions-section{margin-top:36px;text-align:center}.progressions-section h2{margin:0 0 20px;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.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-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}.chord-card{min-width:75px;padding:10px 12px}}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
|
|
223
423
|
}
|
|
224
424
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: CircleOfFifthsComponent, decorators: [{
|
|
225
425
|
type: Component,
|
|
226
|
-
args: [{ selector: 'the-chords-circle-of-fifths', standalone: true, imports: [NgClass], template: "<div class=\"page\">\n <header class=\"page-header\">\r\n <h1>Circle of Fifths</h1>\r\n <p>Click any major or minor key to highlight its diatonic chords</p>\r\n </header>\r\n\r\n <div class=\"circle-wrapper\">\r\n <svg viewBox=\"0 0 600 600\" xmlns=\"http://www.w3.org/2000/svg\" aria-label=\"Circle of Fifths\">\r\n\r\n <!-- \u2500\u2500 Key Signature Ring \u2500\u2500 -->\r\n @for (key of keys; track key.index) {\r\n <path\r\n [attr.d]=\"arcPath(KEY_SIG.inner, KEY_SIG.outer, key.index)\"\r\n class=\"keysig-segment\" />\r\n <text\r\n [attr.x]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).x\"\r\n [attr.y]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).y\"\r\n class=\"keysig-label\">\r\n {{ getAccidentalText(key) }}\r\n </text>\r\n }\r\n\r\n <!-- \u2500\u2500 Major Keys Ring \u2500\u2500 -->\r\n @for (key of keys; track key.index) {\r\n <g [ngClass]=\"['major-group', getMajorState(key.index)]\"\r\n (click)=\"selectKey(key.index, 'major')\">\r\n <path [attr.d]=\"arcPath(MAJOR.inner, MAJOR.outer, key.index)\" />\r\n\r\n <text\r\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\r\n [attr.y]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y + (getMajorRole(key.index) ? -9 : 0)\"\r\n class=\"major-label\">\r\n {{ key.major }}\r\n </text>\r\n\r\n @if (getMajorRole(key.index)) {\r\n <text\r\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\r\n [attr.y]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y + 11\"\r\n class=\"role-label\">\r\n {{ getMajorRole(key.index) }}\r\n </text>\r\n }\r\n </g>\r\n }\r\n\r\n <!-- \u2500\u2500 Minor Keys Ring \u2500\u2500 -->\r\n @for (key of keys; track key.index) {\r\n <g [ngClass]=\"['minor-group', getMinorState(key.index)]\"\r\n (click)=\"selectKey(key.index, 'minor')\">\r\n <path [attr.d]=\"arcPath(MINOR.inner, MINOR.outer, key.index)\" />\r\n\r\n <text\r\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\r\n [attr.y]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).y + (getMinorRole(key.index) ? -8 : 0)\"\r\n class=\"minor-label\">\r\n {{ key.minor }}\r\n </text>\r\n\r\n @if (getMinorRole(key.index)) {\r\n <text\r\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\r\n [attr.y]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).y + 9\"\r\n class=\"role-label\">\r\n {{ getMinorRole(key.index) }}\r\n </text>\r\n }\r\n </g>\r\n }\r\n\r\n <!-- \u2500\u2500 Outer border \u2500\u2500 -->\r\n <circle cx=\"300\" cy=\"300\" r=\"289\" fill=\"none\" stroke=\"#1e293b\" stroke-width=\"2.5\" />\r\n\r\n <!-- \u2500\u2500 Center Circle \u2500\u2500 -->\r\n <circle cx=\"300\" cy=\"300\" r=\"99\" class=\"center-circle\" />\r\n\r\n @if (selectedInfo) {\r\n <text x=\"300\" y=\"283\" class=\"center-name\">{{ selectedInfo.name }}</text>\r\n <text x=\"300\" y=\"304\" class=\"center-scale\">{{ selectedInfo.scale }}</text>\r\n <text x=\"300\" y=\"322\" class=\"center-relative\">{{ selectedInfo.relativeKey }}</text>\r\n <text x=\"300\" y=\"338\" class=\"center-deselect\">\u21A9 click to deselect</text>\r\n } @else {\r\n <text x=\"300\" y=\"293\" class=\"center-hint\">Click a key</text>\r\n <text x=\"300\" y=\"312\" class=\"center-hint-sm\">to see its chords</text>\r\n }\r\n\r\n </svg>\r\n </div>\r\n\r\n <!-- \u2500\u2500 Legend \u2500\u2500 -->\r\n <div class=\"legend\">\r\n <span class=\"legend-item tonic\">Tonic (I / i)</span>\r\n <span class=\"legend-item scale-major\">Major chords (IV, V)</span>\r\n <span class=\"legend-item scale-minor\">Minor chords (ii, iii, vi)</span>\r\n <span class=\"legend-item scale-diminished\">Diminished (vii\u00B0 / ii\u00B0)</span>\r\n </div>\r\n\r\n <!-- \u2500\u2500 Chord Table \u2500\u2500 -->\r\n @if (selectedInfo && chordTable.length) {\r\n <section class=\"chord-section\">\r\n <h2>{{ selectedInfo.fullName }} \u2014 Diatonic Chords</h2>\r\n <p class=\"relative-label\">{{ selectedInfo.relativeKey }}</p>\r\n <div class=\"chord-grid\">\r\n @for (row of chordTable; track row.numeral) {\r\n <div class=\"chord-card\" [ngClass]=\"row.state\">\r\n <div class=\"chord-numeral\">{{ row.numeral }}</div>\r\n <div class=\"chord-name\">{{ row.chord }}</div>\r\n <div class=\"chord-type\">{{ row.chordType }}</div>\r\n <div class=\"chord-role\">{{ row.role }}</div>\r\n </div>\r\n }\r\n </div>\r\n </section>\r\n }\r\n\r\n <!-- \u2500\u2500 Chord Progressions \u2500\u2500 -->\r\n @if (progressions.length) {\r\n <section class=\"progressions-section\">\r\n <h2>Common Progressions in {{ selectedInfo?.fullName }}</h2>\r\n <div class=\"progression-list\">\r\n @for (prog of progressions; track prog.name) {\r\n <div class=\"progression-card\">\r\n <div class=\"prog-header\">\r\n <span class=\"prog-mood\">{{ prog.mood }}</span>\r\n <span class=\"prog-name\">{{ prog.name }}</span>\r\n <span class=\"prog-genre\">{{ prog.genre }}</span>\r\n </div>\r\n <div class=\"prog-chords\">\r\n @for (chord of prog.chords; track $index; let last = $last) {\r\n <span class=\"prog-chord\">{{ chord }}</span>\r\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\r\n }\r\n </div>\r\n <div class=\"prog-numerals\">\r\n @for (num of prog.numerals; track $index; let last = $last) {\r\n <span>{{ num }}</span>\r\n @if (!last) { <span class=\"prog-arrow\">\u2192</span> }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </section>\r\n }\r\n</div>\r\n", styles: [":host{box-sizing:border-box;display:block;min-height:100%;background:#f0f4f8;color:#1e293b}: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:#1e293b;letter-spacing:-.02em}.page-header p{margin:0;color:#64748b;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:#1e293b}.keysig-label{fill:#94a3b8;font-size:12px;font-weight:500;text-anchor:middle;dominant-baseline:central;pointer-events:none}.major-group{cursor:pointer}.major-group path{fill:#dde5f0;stroke:#f8fafc;stroke-width:2;transition:fill .18s ease}.major-group:hover path{fill:#c8d4e8}.major-group .major-label{fill:#334155;font-size:15px;font-weight:700;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.major-group.tonic path{fill:#f59e0b}.major-group.tonic:hover path{fill:#d97706}.major-group.tonic .major-label{fill:#fff}.major-group.scale-major path{fill:#3b82f6}.major-group.scale-major:hover path{fill:#2563eb}.major-group.scale-major .major-label{fill:#fff}.minor-group{cursor:pointer}.minor-group path{fill:#eaf0f8;stroke:#f8fafc;stroke-width:2;transition:fill .18s ease}.minor-group:hover path{fill:#d5e1ef}.minor-group .minor-label{fill:#475569;font-size:12px;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;transition:fill .18s ease}.minor-group.tonic path{fill:#f59e0b}.minor-group.tonic:hover path{fill:#d97706}.minor-group.tonic .minor-label{fill:#fff}.minor-group.scale-minor path{fill:#06b6d4}.minor-group.scale-minor:hover path{fill:#0891b2}.minor-group.scale-minor .minor-label{fill:#fff}.minor-group.scale-diminished path{fill:#a855f7}.minor-group.scale-diminished:hover path{fill:#9333ea}.minor-group.scale-diminished .minor-label{fill:#fff}.role-label{font-size:10px;font-style:italic;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;fill:#ffffffd9}.center-circle{fill:#fff;stroke:#e2e8f0;stroke-width:2;filter:drop-shadow(0 2px 10px rgba(0,0,0,.08))}.center-name{font-size:30px;font-weight:800;text-anchor:middle;dominant-baseline:auto;fill:#1e293b;letter-spacing:-.02em}.center-scale{font-size:13px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:#64748b;text-transform:uppercase;letter-spacing:.08em}.center-relative{font-size:9.5px;font-weight:500;text-anchor:middle;dominant-baseline:auto;fill:#94a3b8}.center-deselect{font-size:8px;text-anchor:middle;dominant-baseline:auto;fill:#cbd5e1}.center-hint{font-size:16px;font-weight:600;text-anchor:middle;dominant-baseline:auto;fill:#94a3b8}.center-hint-sm{font-size:11px;text-anchor:middle;dominant-baseline:auto;fill:#cbd5e1}.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:#64748b}.legend .legend-item:before{content:\"\";display:inline-block;width:14px;height:14px;border-radius:3px}.legend .legend-item.tonic:before{background:#f59e0b}.legend .legend-item.scale-major:before{background:#3b82f6}.legend .legend-item.scale-minor:before{background:#06b6d4}.legend .legend-item.scale-diminished:before{background:#a855f7}.chord-section{margin-top:36px;text-align:center}.chord-section h2{margin:0 0 4px;font-size:1.25rem;font-weight:700;color:#1e293b}.chord-section .relative-label{margin:0 0 20px;font-size:.88rem;color:#64748b}.chord-grid{display:flex;flex-wrap:nowrap;gap:8px;justify-content:center;overflow-x:auto;padding-bottom:4px}.chord-card{border-radius:12px;padding:12px 14px;min-width:82px;flex-shrink:0;text-align:center;background:#e8edf5;border:2px solid transparent;transition:transform .15s ease,box-shadow .15s ease}.chord-card:hover{transform:translateY(-2px);box-shadow:0 4px 16px #0000001a}.chord-card .chord-numeral{font-size:.85rem;font-weight:700;font-style:italic;color:#64748b}.chord-card .chord-name{font-size:1.4rem;font-weight:800;color:#1e293b;margin:2px 0;letter-spacing:-.02em}.chord-card .chord-type{font-size:.65rem;text-transform:uppercase;letter-spacing:.06em;color:#94a3b8}.chord-card .chord-role{font-size:.7rem;color:#94a3b8;margin-top:2px}.chord-card.tonic{background:#fef9ec;border-color:#f59e0b}.chord-card.tonic .chord-numeral,.chord-card.tonic .chord-name{color:#92400e}.chord-card.scale-major{background:#eff6ff;border-color:#3b82f6}.chord-card.scale-major .chord-numeral,.chord-card.scale-major .chord-name{color:#1e40af}.chord-card.scale-diminished{background:#fdf4ff;border-color:#a855f7}.chord-card.scale-diminished .chord-numeral,.chord-card.scale-diminished .chord-name{color:#6b21a8}.progressions-section{margin-top:36px;text-align:center}.progressions-section h2{margin:0 0 20px;font-size:1.25rem;font-weight:700;color:#1e293b}.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:#fff;border:1.5px solid #e2e8f0;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 #00000014;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:#1e293b}.prog-name{font-size:.75rem;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.06em}.prog-genre{margin-left:auto;font-size:.7rem;color:#94a3b8;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:#1e293b;letter-spacing:-.02em}.prog-numerals{display:flex;align-items:center;gap:6px;font-size:.75rem;font-style:italic;color:#94a3b8}.prog-arrow{color:#cbd5e1;font-size:.8rem}@media(max-width:480px){.page-header h1{font-size:1.5rem}.chord-card{min-width:75px;padding:10px 12px}}\n"] }]
|
|
227
|
-
}] });
|
|
426
|
+
args: [{ selector: 'the-chords-circle-of-fifths', standalone: true, imports: [NgClass], 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 <!-- \u2500\u2500 Chord Table \u2500\u2500 -->\n @if (selectedInfo && chordTable.length) {\n <section class=\"chord-section\">\n <h2>{{ selectedInfo.fullName }} \u2014 {{ text().diatonicChords }}</h2>\n <p class=\"relative-label\">{{ selectedInfo.relativeKey }}</p>\n <div class=\"chord-grid\">\n @for (row of chordTable; 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\n <!-- \u2500\u2500 Chord Progressions \u2500\u2500 -->\n @if (progressions.length) {\n <section class=\"progressions-section\">\n <h2>{{ text().commonProgressions }} {{ selectedInfo?.fullName }}</h2>\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>\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)}.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:nowrap;gap:8px;justify-content:center;overflow-x:auto;padding-bottom:4px}.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))}.progressions-section{margin-top:36px;text-align:center}.progressions-section h2{margin:0 0 20px;font-size:1.25rem;font-weight:700;color:var(--_chords-text)}.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-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}.chord-card{min-width:75px;padding:10px 12px}}\n"] }]
|
|
427
|
+
}], propDecorators: { language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }] } });
|
|
228
428
|
|
|
229
429
|
/*
|
|
230
430
|
* Public API Surface of circle-of-fifths-lib
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gblp-circle-of-fifths.mjs","sources":["../../../projects/circle-of-fifths-lib/src/lib/circle-of-fifths.ts","../../../projects/circle-of-fifths-lib/src/lib/circle-of-fifths.html","../../../projects/circle-of-fifths-lib/src/public-api.ts","../../../projects/circle-of-fifths-lib/src/gblp-circle-of-fifths.ts"],"sourcesContent":["import { Component, signal } from '@angular/core';\r\nimport { NgClass } from '@angular/common';\r\n\r\ninterface CircleKey {\r\n index: number;\r\n major: string;\r\n minor: string;\r\n sharps: number;\r\n flats: number;\r\n}\r\n\r\ninterface ChordRow {\r\n numeral: string;\r\n chord: string;\r\n chordType: 'major' | 'minor' | 'diminished';\r\n role: string;\r\n state: 'tonic' | 'scale-major' | 'scale-minor' | 'scale-diminished';\r\n}\r\n\r\ninterface Progression {\r\n name: string;\r\n mood: string;\r\n genre: string;\r\n numerals: string[];\r\n chords: string[];\r\n}\r\n\r\nconst MAJOR_PROGRESSIONS: Omit<Progression, 'chords'>[] = [\r\n { name: 'Anthemic', mood: '😄 Joyful', genre: 'Folk, Rock', numerals: ['I','IV','V','I'] },\r\n { name: 'Hopeful', mood: '✨ Hopeful', genre: 'Pop, Indie', numerals: ['I','V','vi','IV'] },\r\n { name: 'Melancholic', mood: '😢 Emotional', genre: 'Ballads, Pop', numerals: ['vi','IV','I','V'] },\r\n { name: 'Cinematic', mood: '🎬 Tense', genre: 'Film, Jazz', numerals: ['ii','V','vii°','I'] },\r\n];\r\n\r\nconst MINOR_PROGRESSIONS: Omit<Progression, 'chords'>[] = [\r\n { name: 'Brooding', mood: '🌑 Dark', genre: 'Rock, Metal', numerals: ['i','VII','VI','VII'] },\r\n { name: 'Haunting', mood: '👻 Mysterious', genre: 'Film, Gothic', numerals: ['i','iv','VII','III'] },\r\n { name: 'Driving', mood: '⚡ Urgent', genre: 'Pop, EDM', numerals: ['i','VI','III','VII'] },\r\n { name: 'Wistful', mood: '🌙 Wistful', genre: 'Cinematic', numerals: ['i','v','VI','VII'] },\r\n];\r\n\r\n@Component({\r\n selector: 'the-chords-circle-of-fifths',\n standalone: true,\r\n imports: [NgClass],\r\n templateUrl: './circle-of-fifths.html',\n styleUrl: './circle-of-fifths.scss',\n})\r\nexport class CircleOfFifthsComponent {\r\n readonly CX = 300;\r\n readonly CY = 300;\r\n\r\n readonly KEY_SIG = { inner: 250, outer: 288 };\r\n readonly MAJOR = { inner: 172, outer: 250 };\r\n readonly MINOR = { inner: 102, outer: 172 };\r\n\r\n readonly keys: CircleKey[] = [\r\n { index: 0, major: 'C', minor: 'Am', sharps: 0, flats: 0 },\r\n { index: 1, major: 'G', minor: 'Em', sharps: 1, flats: 0 },\r\n { index: 2, major: 'D', minor: 'Bm', sharps: 2, flats: 0 },\r\n { index: 3, major: 'A', minor: 'F♯m', sharps: 3, flats: 0 },\r\n { index: 4, major: 'E', minor: 'C♯m', sharps: 4, flats: 0 },\r\n { index: 5, major: 'B', minor: 'G♯m', sharps: 5, flats: 0 },\r\n { index: 6, major: 'F♯', minor: 'D♯m', sharps: 6, flats: 0 },\r\n { index: 7, major: 'D♭', minor: 'B♭m', sharps: 0, flats: 5 },\r\n { index: 8, major: 'A♭', minor: 'Fm', sharps: 0, flats: 4 },\r\n { index: 9, major: 'E♭', minor: 'Cm', sharps: 0, flats: 3 },\r\n { index: 10, major: 'B♭', minor: 'Gm', sharps: 0, flats: 2 },\r\n { index: 11, major: 'F', minor: 'Dm', sharps: 0, flats: 1 },\r\n ];\r\n\r\n selectedIndex = signal<number | null>(null);\r\n selectedType = signal<'major' | 'minor' | null>(null);\r\n\r\n private get slice() {\r\n const idx = this.selectedIndex();\r\n if (idx === null) return null;\r\n return {\r\n prev: (idx - 1 + 12) % 12,\r\n curr: idx,\r\n next: (idx + 1) % 12,\r\n };\r\n }\r\n\r\n selectKey(index: number, type: 'major' | 'minor'): void {\r\n if (this.selectedIndex() === index && this.selectedType() === type) {\r\n this.selectedIndex.set(null);\r\n this.selectedType.set(null);\r\n } else {\r\n this.selectedIndex.set(index);\r\n this.selectedType.set(type);\r\n }\r\n }\r\n\r\n getMajorState(index: number): string {\r\n const s = this.slice;\r\n if (!s) return '';\r\n const type = this.selectedType();\r\n if (index === s.curr) return type === 'major' ? 'tonic' : 'scale-major';\r\n if (index === s.prev || index === s.next) return 'scale-major';\r\n return '';\r\n }\r\n\r\n getMinorState(index: number): string {\r\n const s = this.slice;\r\n if (!s) return '';\r\n const type = this.selectedType();\r\n const dimIdx = (s.curr + 2) % 12;\r\n if (index === s.curr) return type === 'minor' ? 'tonic' : 'scale-minor';\r\n if (index === s.prev || index === s.next) return 'scale-minor';\r\n if (index === dimIdx) return 'scale-diminished';\r\n return '';\r\n }\r\n\r\n getMajorRole(index: number): string {\r\n const s = this.slice;\r\n if (!s) return '';\r\n const type = this.selectedType();\r\n if (type === 'major') {\r\n if (index === s.curr) return 'I';\r\n if (index === s.prev) return 'IV';\r\n if (index === s.next) return 'V';\r\n } else {\r\n if (index === s.curr) return 'III';\r\n if (index === s.prev) return 'VI';\r\n if (index === s.next) return 'VII';\r\n }\r\n return '';\r\n }\r\n\r\n getMinorRole(index: number): string {\r\n const s = this.slice;\r\n if (!s) return '';\r\n const type = this.selectedType();\r\n const dimIdx = (s.curr + 2) % 12;\r\n if (type === 'major') {\r\n if (index === s.curr) return 'vi';\r\n if (index === s.prev) return 'ii';\r\n if (index === s.next) return 'iii';\r\n if (index === dimIdx) return 'vii°';\r\n } else {\r\n if (index === s.curr) return 'i';\r\n if (index === s.prev) return 'iv';\r\n if (index === s.next) return 'v';\r\n if (index === dimIdx) return 'ii°';\r\n }\r\n return '';\r\n }\r\n\r\n arcPath(innerR: number, outerR: number, index: number, gap = 1.5): string {\r\n const mid = index * 30 - 90;\r\n const s = (mid - 15 + gap) * (Math.PI / 180);\r\n const e = (mid + 15 - gap) * (Math.PI / 180);\r\n const cx = this.CX, cy = this.CY;\r\n const x1 = cx + outerR * Math.cos(s), y1 = cy + outerR * Math.sin(s);\r\n const x2 = cx + outerR * Math.cos(e), y2 = cy + outerR * Math.sin(e);\r\n const x3 = cx + innerR * Math.cos(e), y3 = cy + innerR * Math.sin(e);\r\n const x4 = cx + innerR * Math.cos(s), y4 = cy + innerR * Math.sin(s);\r\n const f = (n: number) => n.toFixed(2);\r\n 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`;\r\n }\r\n\r\n textPos(midR: number, index: number): { x: number; y: number } {\r\n const a = (index * 30 - 90) * (Math.PI / 180);\r\n return {\r\n x: this.CX + midR * Math.cos(a),\r\n y: this.CY + midR * Math.sin(a),\r\n };\r\n }\r\n\r\n private dimChord(idx: number): string {\r\n // The diminished chord root is the minor key two steps clockwise (idx+2)\r\n const minor = this.keys[(idx + 2) % 12].minor;\r\n return minor.replace(/m$/, '°');\r\n }\r\n\r\n getAccidentalText(key: CircleKey): string {\r\n if (key.sharps > 0) return `${key.sharps}♯`;\r\n if (key.flats > 0) return `${key.flats}♭`;\r\n return '—';\r\n }\r\n\r\n get selectedInfo() {\r\n const idx = this.selectedIndex();\r\n const type = this.selectedType();\r\n if (idx === null || type === null) return null;\r\n const key = this.keys[idx];\r\n return {\r\n name: type === 'major' ? key.major : key.minor,\r\n scale: type === 'major' ? 'Major' : 'Minor',\r\n fullName: type === 'major' ? `${key.major} Major` : `${key.minor} Minor`,\r\n relativeKey: type === 'major'\r\n ? `Relative minor: ${key.minor}`\r\n : `Relative major: ${key.major}`,\r\n };\r\n }\r\n\r\n get chordTable(): ChordRow[] {\r\n const idx = this.selectedIndex();\r\n const type = this.selectedType();\r\n if (idx === null || type === null) return [];\r\n\r\n const prev = (idx - 1 + 12) % 12;\r\n const next = (idx + 1) % 12;\r\n\r\n if (type === 'major') {\r\n return [\r\n { numeral: 'I', chord: this.keys[idx].major, chordType: 'major', role: 'Tonic', state: 'tonic' },\r\n { numeral: 'ii', chord: this.keys[prev].minor, chordType: 'minor', role: 'Supertonic', state: 'scale-minor' },\r\n { numeral: 'iii', chord: this.keys[next].minor, chordType: 'minor', role: 'Mediant', state: 'scale-minor' },\r\n { numeral: 'IV', chord: this.keys[prev].major, chordType: 'major', role: 'Subdominant', state: 'scale-major' },\r\n { numeral: 'V', chord: this.keys[next].major, chordType: 'major', role: 'Dominant', state: 'scale-major' },\r\n { numeral: 'vi', chord: this.keys[idx].minor, chordType: 'minor', role: 'Submediant', state: 'scale-minor' },\r\n { numeral: 'vii°', chord: this.dimChord(idx), chordType: 'diminished', role: 'Leading Tone', state: 'scale-diminished' },\r\n ];\r\n } else {\r\n return [\r\n { numeral: 'i', chord: this.keys[idx].minor, chordType: 'minor', role: 'Tonic', state: 'tonic' },\r\n { numeral: 'iv', chord: this.keys[prev].minor, chordType: 'minor', role: 'Subdominant', state: 'scale-minor' },\r\n { numeral: 'v', chord: this.keys[next].minor, chordType: 'minor', role: 'Dominant', state: 'scale-minor' },\r\n { numeral: 'III', chord: this.keys[idx].major, chordType: 'major', role: 'Mediant', state: 'scale-major' },\r\n { numeral: 'VI', chord: this.keys[prev].major, chordType: 'major', role: 'Submediant', state: 'scale-major' },\r\n { numeral: 'VII', chord: this.keys[next].major, chordType: 'major', role: 'Subtonic', state: 'scale-major' },\r\n { numeral: 'ii°', chord: this.dimChord(idx), chordType: 'diminished', role: 'Supertonic', state: 'scale-diminished' },\r\n ];\r\n }\r\n }\r\n\r\n get progressions(): Progression[] {\r\n const table = this.chordTable;\r\n if (!table.length) return [];\r\n const lookup = new Map(table.map(r => [r.numeral, r.chord]));\r\n const defs = this.selectedType() === 'major' ? MAJOR_PROGRESSIONS : MINOR_PROGRESSIONS;\r\n return defs.map(d => ({ ...d, chords: d.numerals.map(n => lookup.get(n) ?? n) }));\r\n }\r\n}\r\n","<div class=\"page\">\n <header class=\"page-header\">\r\n <h1>Circle of Fifths</h1>\r\n <p>Click any major or minor key to highlight its diatonic chords</p>\r\n </header>\r\n\r\n <div class=\"circle-wrapper\">\r\n <svg viewBox=\"0 0 600 600\" xmlns=\"http://www.w3.org/2000/svg\" aria-label=\"Circle of Fifths\">\r\n\r\n <!-- ── Key Signature Ring ── -->\r\n @for (key of keys; track key.index) {\r\n <path\r\n [attr.d]=\"arcPath(KEY_SIG.inner, KEY_SIG.outer, key.index)\"\r\n class=\"keysig-segment\" />\r\n <text\r\n [attr.x]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).x\"\r\n [attr.y]=\"textPos((KEY_SIG.inner + KEY_SIG.outer) / 2, key.index).y\"\r\n class=\"keysig-label\">\r\n {{ getAccidentalText(key) }}\r\n </text>\r\n }\r\n\r\n <!-- ── Major Keys Ring ── -->\r\n @for (key of keys; track key.index) {\r\n <g [ngClass]=\"['major-group', getMajorState(key.index)]\"\r\n (click)=\"selectKey(key.index, 'major')\">\r\n <path [attr.d]=\"arcPath(MAJOR.inner, MAJOR.outer, key.index)\" />\r\n\r\n <text\r\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\r\n [attr.y]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y + (getMajorRole(key.index) ? -9 : 0)\"\r\n class=\"major-label\">\r\n {{ key.major }}\r\n </text>\r\n\r\n @if (getMajorRole(key.index)) {\r\n <text\r\n [attr.x]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).x\"\r\n [attr.y]=\"textPos((MAJOR.inner + MAJOR.outer) / 2, key.index).y + 11\"\r\n class=\"role-label\">\r\n {{ getMajorRole(key.index) }}\r\n </text>\r\n }\r\n </g>\r\n }\r\n\r\n <!-- ── Minor Keys Ring ── -->\r\n @for (key of keys; track key.index) {\r\n <g [ngClass]=\"['minor-group', getMinorState(key.index)]\"\r\n (click)=\"selectKey(key.index, 'minor')\">\r\n <path [attr.d]=\"arcPath(MINOR.inner, MINOR.outer, key.index)\" />\r\n\r\n <text\r\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\r\n [attr.y]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).y + (getMinorRole(key.index) ? -8 : 0)\"\r\n class=\"minor-label\">\r\n {{ key.minor }}\r\n </text>\r\n\r\n @if (getMinorRole(key.index)) {\r\n <text\r\n [attr.x]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).x\"\r\n [attr.y]=\"textPos((MINOR.inner + MINOR.outer) / 2, key.index).y + 9\"\r\n class=\"role-label\">\r\n {{ getMinorRole(key.index) }}\r\n </text>\r\n }\r\n </g>\r\n }\r\n\r\n <!-- ── Outer border ── -->\r\n <circle cx=\"300\" cy=\"300\" r=\"289\" fill=\"none\" stroke=\"#1e293b\" stroke-width=\"2.5\" />\r\n\r\n <!-- ── Center Circle ── -->\r\n <circle cx=\"300\" cy=\"300\" r=\"99\" class=\"center-circle\" />\r\n\r\n @if (selectedInfo) {\r\n <text x=\"300\" y=\"283\" class=\"center-name\">{{ selectedInfo.name }}</text>\r\n <text x=\"300\" y=\"304\" class=\"center-scale\">{{ selectedInfo.scale }}</text>\r\n <text x=\"300\" y=\"322\" class=\"center-relative\">{{ selectedInfo.relativeKey }}</text>\r\n <text x=\"300\" y=\"338\" class=\"center-deselect\">↩ click to deselect</text>\r\n } @else {\r\n <text x=\"300\" y=\"293\" class=\"center-hint\">Click a key</text>\r\n <text x=\"300\" y=\"312\" class=\"center-hint-sm\">to see its chords</text>\r\n }\r\n\r\n </svg>\r\n </div>\r\n\r\n <!-- ── Legend ── -->\r\n <div class=\"legend\">\r\n <span class=\"legend-item tonic\">Tonic (I / i)</span>\r\n <span class=\"legend-item scale-major\">Major chords (IV, V)</span>\r\n <span class=\"legend-item scale-minor\">Minor chords (ii, iii, vi)</span>\r\n <span class=\"legend-item scale-diminished\">Diminished (vii° / ii°)</span>\r\n </div>\r\n\r\n <!-- ── Chord Table ── -->\r\n @if (selectedInfo && chordTable.length) {\r\n <section class=\"chord-section\">\r\n <h2>{{ selectedInfo.fullName }} — Diatonic Chords</h2>\r\n <p class=\"relative-label\">{{ selectedInfo.relativeKey }}</p>\r\n <div class=\"chord-grid\">\r\n @for (row of chordTable; track row.numeral) {\r\n <div class=\"chord-card\" [ngClass]=\"row.state\">\r\n <div class=\"chord-numeral\">{{ row.numeral }}</div>\r\n <div class=\"chord-name\">{{ row.chord }}</div>\r\n <div class=\"chord-type\">{{ row.chordType }}</div>\r\n <div class=\"chord-role\">{{ row.role }}</div>\r\n </div>\r\n }\r\n </div>\r\n </section>\r\n }\r\n\r\n <!-- ── Chord Progressions ── -->\r\n @if (progressions.length) {\r\n <section class=\"progressions-section\">\r\n <h2>Common Progressions in {{ selectedInfo?.fullName }}</h2>\r\n <div class=\"progression-list\">\r\n @for (prog of progressions; track prog.name) {\r\n <div class=\"progression-card\">\r\n <div class=\"prog-header\">\r\n <span class=\"prog-mood\">{{ prog.mood }}</span>\r\n <span class=\"prog-name\">{{ prog.name }}</span>\r\n <span class=\"prog-genre\">{{ prog.genre }}</span>\r\n </div>\r\n <div class=\"prog-chords\">\r\n @for (chord of prog.chords; track $index; let last = $last) {\r\n <span class=\"prog-chord\">{{ chord }}</span>\r\n @if (!last) { <span class=\"prog-arrow\">→</span> }\r\n }\r\n </div>\r\n <div class=\"prog-numerals\">\r\n @for (num of prog.numerals; track $index; let last = $last) {\r\n <span>{{ num }}</span>\r\n @if (!last) { <span class=\"prog-arrow\">→</span> }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </section>\r\n }\r\n</div>\r\n","/*\n * Public API Surface of circle-of-fifths-lib\n */\n\nexport * from './lib/circle-of-fifths';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"gblp-circle-of-fifths.mjs","sources":["../../../projects/circle-of-fifths-lib/src/lib/circle-of-fifths.ts","../../../projects/circle-of-fifths-lib/src/lib/circle-of-fifths.html","../../../projects/circle-of-fifths-lib/src/public-api.ts","../../../projects/circle-of-fifths-lib/src/gblp-circle-of-fifths.ts"],"sourcesContent":["import { Component, computed, input, signal } from '@angular/core';\nimport { NgClass } from '@angular/common';\n\ninterface CircleKey {\n index: number;\n major: string;\n minor: string;\n sharps: number;\n flats: number;\n}\n\ninterface ChordRow {\n numeral: string;\n chord: string;\n chordType: string;\n role: string;\n state: 'tonic' | 'scale-major' | 'scale-minor' | 'scale-diminished';\n}\n\ninterface Progression {\n name: string;\n mood: string;\n genre: string;\n numerals: string[];\n chords: string[];\n}\n\ntype Language = 'en' | 'es';\ntype LocalizedText = Record<Language, string>;\n\ninterface ProgressionDefinition {\n name: LocalizedText;\n mood: LocalizedText;\n genre: LocalizedText;\n numerals: string[];\n}\n\nconst PROGRESSIONS: Record<'major' | 'minor', ProgressionDefinition[]> = {\n major: [\n {\n name: { en: 'Anthemic', es: 'Épica' },\n mood: { en: '😄 Joyful', es: '😄 Alegre' },\n genre: { en: 'Folk, Rock', es: 'Folk, rock' },\n numerals: ['I', 'IV', 'V', 'I'],\n },\n {\n name: { en: 'Hopeful', es: 'Esperanzadora' },\n mood: { en: '✨ Hopeful', es: '✨ Esperanzadora' },\n genre: { en: 'Pop, Indie', es: 'Pop, indie' },\n numerals: ['I', 'V', 'vi', 'IV'],\n },\n {\n name: { en: 'Melancholic', es: 'Melancólica' },\n mood: { en: '😢 Emotional', es: '😢 Emocional' },\n genre: { en: 'Ballads, Pop', es: 'Baladas, pop' },\n numerals: ['vi', 'IV', 'I', 'V'],\n },\n {\n name: { en: 'Cinematic', es: 'Cinemática' },\n mood: { en: '🎬 Tense', es: '🎬 Tensa' },\n genre: { en: 'Film, Jazz', es: 'Cine, jazz' },\n numerals: ['ii', 'V', 'vii°', 'I'],\n },\n ],\n minor: [\n {\n name: { en: 'Brooding', es: 'Sombría' },\n mood: { en: '🌑 Dark', es: '🌑 Oscura' },\n genre: { en: 'Rock, Metal', es: 'Rock, metal' },\n numerals: ['i', 'VII', 'VI', 'VII'],\n },\n {\n name: { en: 'Haunting', es: 'Inquietante' },\n mood: { en: '👻 Mysterious', es: '👻 Misteriosa' },\n genre: { en: 'Film, Gothic', es: 'Cine, gótico' },\n numerals: ['i', 'iv', 'VII', 'III'],\n },\n {\n name: { en: 'Driving', es: 'Impulsora' },\n mood: { en: '⚡ Urgent', es: '⚡ Urgente' },\n genre: { en: 'Pop, EDM', es: 'Pop, EDM' },\n numerals: ['i', 'VI', 'III', 'VII'],\n },\n {\n name: { en: 'Wistful', es: 'Nostálgica' },\n mood: { en: '🌙 Wistful', es: '🌙 Nostálgica' },\n genre: { en: 'Cinematic', es: 'Cinemática' },\n numerals: ['i', 'v', 'VI', 'VII'],\n },\n ],\n};\n\nconst COPY = {\n en: {\n title: 'Circle of Fifths',\n subtitle: 'Click any major or minor key to highlight its diatonic chords',\n ariaLabel: 'Circle of Fifths',\n clickKey: 'Click a key',\n seeChords: 'to see its chords',\n deselect: '↩ click to deselect',\n tonicLegend: 'Tonic (I / i)',\n majorLegend: 'Major chords (IV, V)',\n minorLegend: 'Minor chords (ii, iii, vi)',\n diminishedLegend: 'Diminished (vii° / ii°)',\n diatonicChords: 'Diatonic Chords',\n commonProgressions: 'Common Progressions in',\n major: 'Major',\n minor: 'Minor',\n diminished: 'diminished',\n relativeMinor: 'Relative minor',\n relativeMajor: 'Relative major',\n roles: {\n tonic: 'Tonic',\n supertonic: 'Supertonic',\n mediant: 'Mediant',\n subdominant: 'Subdominant',\n dominant: 'Dominant',\n submediant: 'Submediant',\n leadingTone: 'Leading Tone',\n subtonic: 'Subtonic',\n },\n },\n es: {\n title: 'Círculo de quintas',\n subtitle: 'Haz clic en una tonalidad mayor o menor para resaltar sus acordes diatónicos',\n ariaLabel: 'Círculo de quintas',\n clickKey: 'Haz clic en una tonalidad',\n seeChords: 'para ver sus acordes',\n deselect: '↩ haz clic para deseleccionar',\n tonicLegend: 'Tónica (I / i)',\n majorLegend: 'Acordes mayores (IV, V)',\n minorLegend: 'Acordes menores (ii, iii, vi)',\n diminishedLegend: 'Disminuido (vii° / ii°)',\n diatonicChords: 'Acordes diatónicos',\n commonProgressions: 'Progresiones comunes en',\n major: 'Mayor',\n minor: 'Menor',\n diminished: 'disminuido',\n relativeMinor: 'Relativa menor',\n relativeMajor: 'Relativa mayor',\n roles: {\n tonic: 'Tónica',\n supertonic: 'Supertónica',\n mediant: 'Mediante',\n subdominant: 'Subdominante',\n dominant: 'Dominante',\n submediant: 'Submediante',\n leadingTone: 'Sensible',\n subtonic: 'Subtónica',\n },\n },\n} as const;\n\n@Component({\n selector: 'the-chords-circle-of-fifths',\n standalone: true,\n imports: [NgClass],\n templateUrl: './circle-of-fifths.html',\n styleUrl: './circle-of-fifths.scss',\n host: { '[attr.lang]': 'language()' },\n})\nexport class CircleOfFifthsComponent {\n readonly language = input<Language>('en');\n readonly text = computed(() => COPY[this.language()]);\n readonly CX = 300;\n readonly CY = 300;\n\n readonly KEY_SIG = { inner: 250, outer: 288 };\n readonly MAJOR = { inner: 172, outer: 250 };\n readonly MINOR = { inner: 102, outer: 172 };\n\n readonly keys: CircleKey[] = [\n { index: 0, major: 'C', minor: 'Am', sharps: 0, flats: 0 },\n { index: 1, major: 'G', minor: 'Em', sharps: 1, flats: 0 },\n { index: 2, major: 'D', minor: 'Bm', sharps: 2, flats: 0 },\n { index: 3, major: 'A', minor: 'F♯m', sharps: 3, flats: 0 },\n { index: 4, major: 'E', minor: 'C♯m', sharps: 4, flats: 0 },\n { index: 5, major: 'B', minor: 'G♯m', sharps: 5, flats: 0 },\n { index: 6, major: 'F♯', minor: 'D♯m', sharps: 6, flats: 0 },\n { index: 7, major: 'D♭', minor: 'B♭m', sharps: 0, flats: 5 },\n { index: 8, major: 'A♭', minor: 'Fm', sharps: 0, flats: 4 },\n { index: 9, major: 'E♭', minor: 'Cm', sharps: 0, flats: 3 },\n { index: 10, major: 'B♭', minor: 'Gm', sharps: 0, flats: 2 },\n { index: 11, major: 'F', minor: 'Dm', sharps: 0, flats: 1 },\n ];\n\n selectedIndex = signal<number | null>(null);\n selectedType = signal<'major' | 'minor' | null>(null);\n\n private get slice() {\n const idx = this.selectedIndex();\n if (idx === null) return null;\n return {\n prev: (idx - 1 + 12) % 12,\n curr: idx,\n next: (idx + 1) % 12,\n };\n }\n\n selectKey(index: number, type: 'major' | 'minor'): void {\n if (this.selectedIndex() === index && this.selectedType() === type) {\n this.selectedIndex.set(null);\n this.selectedType.set(null);\n } else {\n this.selectedIndex.set(index);\n this.selectedType.set(type);\n }\n }\n\n getMajorState(index: number): string {\n const s = this.slice;\n if (!s) return '';\n const type = this.selectedType();\n if (index === s.curr) return type === 'major' ? 'tonic' : 'scale-major';\n if (index === s.prev || index === s.next) return 'scale-major';\n return '';\n }\n\n getMinorState(index: number): string {\n const s = this.slice;\n if (!s) return '';\n const type = this.selectedType();\n const dimIdx = (s.curr + 2) % 12;\n if (index === s.curr) return type === 'minor' ? 'tonic' : 'scale-minor';\n if (index === s.prev || index === s.next) return 'scale-minor';\n if (index === dimIdx) return 'scale-diminished';\n return '';\n }\n\n getMajorRole(index: number): string {\n const s = this.slice;\n if (!s) return '';\n const type = this.selectedType();\n if (type === 'major') {\n if (index === s.curr) return 'I';\n if (index === s.prev) return 'IV';\n if (index === s.next) return 'V';\n } else {\n if (index === s.curr) return 'III';\n if (index === s.prev) return 'VI';\n if (index === s.next) return 'VII';\n }\n return '';\n }\n\n getMinorRole(index: number): string {\n const s = this.slice;\n if (!s) return '';\n const type = this.selectedType();\n const dimIdx = (s.curr + 2) % 12;\n if (type === 'major') {\n if (index === s.curr) return 'vi';\n if (index === s.prev) return 'ii';\n if (index === s.next) return 'iii';\n if (index === dimIdx) return 'vii°';\n } else {\n if (index === s.curr) return 'i';\n if (index === s.prev) return 'iv';\n if (index === s.next) return 'v';\n if (index === dimIdx) return 'ii°';\n }\n return '';\n }\n\n arcPath(innerR: number, outerR: number, index: number, gap = 1.5): string {\n const mid = index * 30 - 90;\n const s = (mid - 15 + gap) * (Math.PI / 180);\n const e = (mid + 15 - gap) * (Math.PI / 180);\n const cx = this.CX,\n cy = this.CY;\n const x1 = cx + outerR * Math.cos(s),\n y1 = cy + outerR * Math.sin(s);\n const x2 = cx + outerR * Math.cos(e),\n y2 = cy + outerR * Math.sin(e);\n const x3 = cx + innerR * Math.cos(e),\n y3 = cy + innerR * Math.sin(e);\n const x4 = cx + innerR * Math.cos(s),\n y4 = cy + innerR * Math.sin(s);\n const f = (n: number) => n.toFixed(2);\n 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`;\n }\n\n textPos(midR: number, index: number): { x: number; y: number } {\n const a = (index * 30 - 90) * (Math.PI / 180);\n return {\n x: this.CX + midR * Math.cos(a),\n y: this.CY + midR * Math.sin(a),\n };\n }\n\n private dimChord(idx: number): string {\n // The diminished chord root is the minor key two steps clockwise (idx+2)\n const minor = this.keys[(idx + 2) % 12].minor;\n return minor.replace(/m$/, '°');\n }\n\n getAccidentalText(key: CircleKey): string {\n if (key.sharps > 0) return `${key.sharps}♯`;\n if (key.flats > 0) return `${key.flats}♭`;\n return '—';\n }\n\n get selectedInfo() {\n const idx = this.selectedIndex();\n const type = this.selectedType();\n if (idx === null || type === null) return null;\n const key = this.keys[idx];\n const copy = this.text();\n const scale = type === 'major' ? copy.major : copy.minor;\n return {\n name: type === 'major' ? key.major : key.minor,\n scale,\n fullName: `${type === 'major' ? key.major : key.minor} ${scale}`,\n relativeKey:\n type === 'major'\n ? `${copy.relativeMinor}: ${key.minor}`\n : `${copy.relativeMajor}: ${key.major}`,\n };\n }\n\n get chordTable(): ChordRow[] {\n const idx = this.selectedIndex();\n const type = this.selectedType();\n if (idx === null || type === null) return [];\n\n const prev = (idx - 1 + 12) % 12;\n const next = (idx + 1) % 12;\n const copy = this.text();\n\n if (type === 'major') {\n return [\n {\n numeral: 'I',\n chord: this.keys[idx].major,\n chordType: copy.major,\n role: copy.roles.tonic,\n state: 'tonic',\n },\n {\n numeral: 'ii',\n chord: this.keys[prev].minor,\n chordType: copy.minor,\n role: copy.roles.supertonic,\n state: 'scale-minor',\n },\n {\n numeral: 'iii',\n chord: this.keys[next].minor,\n chordType: copy.minor,\n role: copy.roles.mediant,\n state: 'scale-minor',\n },\n {\n numeral: 'IV',\n chord: this.keys[prev].major,\n chordType: copy.major,\n role: copy.roles.subdominant,\n state: 'scale-major',\n },\n {\n numeral: 'V',\n chord: this.keys[next].major,\n chordType: copy.major,\n role: copy.roles.dominant,\n state: 'scale-major',\n },\n {\n numeral: 'vi',\n chord: this.keys[idx].minor,\n chordType: copy.minor,\n role: copy.roles.submediant,\n state: 'scale-minor',\n },\n {\n numeral: 'vii°',\n chord: this.dimChord(idx),\n chordType: copy.diminished,\n role: copy.roles.leadingTone,\n state: 'scale-diminished',\n },\n ];\n } else {\n return [\n {\n numeral: 'i',\n chord: this.keys[idx].minor,\n chordType: copy.minor,\n role: copy.roles.tonic,\n state: 'tonic',\n },\n {\n numeral: 'iv',\n chord: this.keys[prev].minor,\n chordType: copy.minor,\n role: copy.roles.subdominant,\n state: 'scale-minor',\n },\n {\n numeral: 'v',\n chord: this.keys[next].minor,\n chordType: copy.minor,\n role: copy.roles.dominant,\n state: 'scale-minor',\n },\n {\n numeral: 'III',\n chord: this.keys[idx].major,\n chordType: copy.major,\n role: copy.roles.mediant,\n state: 'scale-major',\n },\n {\n numeral: 'VI',\n chord: this.keys[prev].major,\n chordType: copy.major,\n role: copy.roles.submediant,\n state: 'scale-major',\n },\n {\n numeral: 'VII',\n chord: this.keys[next].major,\n chordType: copy.major,\n role: copy.roles.subtonic,\n state: 'scale-major',\n },\n {\n numeral: 'ii°',\n chord: this.dimChord(idx),\n chordType: copy.diminished,\n role: copy.roles.supertonic,\n state: 'scale-diminished',\n },\n ];\n }\n }\n\n get progressions(): Progression[] {\n const table = this.chordTable;\n if (!table.length) return [];\n const lookup = new Map(table.map((r) => [r.numeral, r.chord]));\n const language = this.language();\n const defs = PROGRESSIONS[this.selectedType() === 'major' ? 'major' : 'minor'];\n return defs.map((definition) => ({\n name: definition.name[language],\n mood: definition.mood[language],\n genre: definition.genre[language],\n numerals: definition.numerals,\n chords: definition.numerals.map((numeral) => lookup.get(numeral) ?? numeral),\n }));\n }\n}\n","<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 <!-- ── Key Signature Ring ── -->\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 <!-- ── Major Keys Ring ── -->\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 <!-- ── Minor Keys Ring ── -->\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 <!-- ── Outer border ── -->\n <circle cx=\"300\" cy=\"300\" r=\"289\" class=\"outer-border\" />\n\n <!-- ── Center Circle ── -->\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 <!-- ── Legend ── -->\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 Table ── -->\n @if (selectedInfo && chordTable.length) {\n <section class=\"chord-section\">\n <h2>{{ selectedInfo.fullName }} — {{ text().diatonicChords }}</h2>\n <p class=\"relative-label\">{{ selectedInfo.relativeKey }}</p>\n <div class=\"chord-grid\">\n @for (row of chordTable; 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\n <!-- ── Chord Progressions ── -->\n @if (progressions.length) {\n <section class=\"progressions-section\">\n <h2>{{ text().commonProgressions }} {{ selectedInfo?.fullName }}</h2>\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\">→</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\">→</span>\n }\n }\n </div>\n </div>\n }\n </div>\n </section>\n }\n</div>\n","/*\n * Public API Surface of circle-of-fifths-lib\n */\n\nexport * from './lib/circle-of-fifths';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ interface CircleKey {
|
|
|
10
10
|
interface ChordRow {
|
|
11
11
|
numeral: string;
|
|
12
12
|
chord: string;
|
|
13
|
-
chordType:
|
|
13
|
+
chordType: string;
|
|
14
14
|
role: string;
|
|
15
15
|
state: 'tonic' | 'scale-major' | 'scale-minor' | 'scale-diminished';
|
|
16
16
|
}
|
|
@@ -21,7 +21,66 @@ interface Progression {
|
|
|
21
21
|
numerals: string[];
|
|
22
22
|
chords: string[];
|
|
23
23
|
}
|
|
24
|
+
type Language = 'en' | 'es';
|
|
24
25
|
declare class CircleOfFifthsComponent {
|
|
26
|
+
readonly language: _angular_core.InputSignal<Language>;
|
|
27
|
+
readonly text: _angular_core.Signal<{
|
|
28
|
+
readonly title: "Circle of Fifths";
|
|
29
|
+
readonly subtitle: "Click any major or minor key to highlight its diatonic chords";
|
|
30
|
+
readonly ariaLabel: "Circle of Fifths";
|
|
31
|
+
readonly clickKey: "Click a key";
|
|
32
|
+
readonly seeChords: "to see its chords";
|
|
33
|
+
readonly deselect: "↩ click to deselect";
|
|
34
|
+
readonly tonicLegend: "Tonic (I / i)";
|
|
35
|
+
readonly majorLegend: "Major chords (IV, V)";
|
|
36
|
+
readonly minorLegend: "Minor chords (ii, iii, vi)";
|
|
37
|
+
readonly diminishedLegend: "Diminished (vii° / ii°)";
|
|
38
|
+
readonly diatonicChords: "Diatonic Chords";
|
|
39
|
+
readonly commonProgressions: "Common Progressions in";
|
|
40
|
+
readonly major: "Major";
|
|
41
|
+
readonly minor: "Minor";
|
|
42
|
+
readonly diminished: "diminished";
|
|
43
|
+
readonly relativeMinor: "Relative minor";
|
|
44
|
+
readonly relativeMajor: "Relative major";
|
|
45
|
+
readonly roles: {
|
|
46
|
+
readonly tonic: "Tonic";
|
|
47
|
+
readonly supertonic: "Supertonic";
|
|
48
|
+
readonly mediant: "Mediant";
|
|
49
|
+
readonly subdominant: "Subdominant";
|
|
50
|
+
readonly dominant: "Dominant";
|
|
51
|
+
readonly submediant: "Submediant";
|
|
52
|
+
readonly leadingTone: "Leading Tone";
|
|
53
|
+
readonly subtonic: "Subtonic";
|
|
54
|
+
};
|
|
55
|
+
} | {
|
|
56
|
+
readonly title: "Círculo de quintas";
|
|
57
|
+
readonly subtitle: "Haz clic en una tonalidad mayor o menor para resaltar sus acordes diatónicos";
|
|
58
|
+
readonly ariaLabel: "Círculo de quintas";
|
|
59
|
+
readonly clickKey: "Haz clic en una tonalidad";
|
|
60
|
+
readonly seeChords: "para ver sus acordes";
|
|
61
|
+
readonly deselect: "↩ haz clic para deseleccionar";
|
|
62
|
+
readonly tonicLegend: "Tónica (I / i)";
|
|
63
|
+
readonly majorLegend: "Acordes mayores (IV, V)";
|
|
64
|
+
readonly minorLegend: "Acordes menores (ii, iii, vi)";
|
|
65
|
+
readonly diminishedLegend: "Disminuido (vii° / ii°)";
|
|
66
|
+
readonly diatonicChords: "Acordes diatónicos";
|
|
67
|
+
readonly commonProgressions: "Progresiones comunes en";
|
|
68
|
+
readonly major: "Mayor";
|
|
69
|
+
readonly minor: "Menor";
|
|
70
|
+
readonly diminished: "disminuido";
|
|
71
|
+
readonly relativeMinor: "Relativa menor";
|
|
72
|
+
readonly relativeMajor: "Relativa mayor";
|
|
73
|
+
readonly roles: {
|
|
74
|
+
readonly tonic: "Tónica";
|
|
75
|
+
readonly supertonic: "Supertónica";
|
|
76
|
+
readonly mediant: "Mediante";
|
|
77
|
+
readonly subdominant: "Subdominante";
|
|
78
|
+
readonly dominant: "Dominante";
|
|
79
|
+
readonly submediant: "Submediante";
|
|
80
|
+
readonly leadingTone: "Sensible";
|
|
81
|
+
readonly subtonic: "Subtónica";
|
|
82
|
+
};
|
|
83
|
+
}>;
|
|
25
84
|
readonly CX = 300;
|
|
26
85
|
readonly CY = 300;
|
|
27
86
|
readonly KEY_SIG: {
|
|
@@ -54,14 +113,14 @@ declare class CircleOfFifthsComponent {
|
|
|
54
113
|
getAccidentalText(key: CircleKey): string;
|
|
55
114
|
get selectedInfo(): {
|
|
56
115
|
name: string;
|
|
57
|
-
scale:
|
|
116
|
+
scale: "Major" | "Minor" | "Mayor" | "Menor";
|
|
58
117
|
fullName: string;
|
|
59
118
|
relativeKey: string;
|
|
60
119
|
} | null;
|
|
61
120
|
get chordTable(): ChordRow[];
|
|
62
121
|
get progressions(): Progression[];
|
|
63
122
|
static ɵfac: _angular_core.ɵɵFactoryDeclaration<CircleOfFifthsComponent, never>;
|
|
64
|
-
static ɵcmp: _angular_core.ɵɵComponentDeclaration<CircleOfFifthsComponent, "the-chords-circle-of-fifths", never, {}, {}, never, never, true, never>;
|
|
123
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<CircleOfFifthsComponent, "the-chords-circle-of-fifths", never, { "language": { "alias": "language"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
65
124
|
}
|
|
66
125
|
|
|
67
126
|
export { CircleOfFifthsComponent };
|