@gblp/circle-of-fifths 0.1.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
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# @gblp/circle-of-fifths
|
|
2
|
+
|
|
3
|
+
Standalone Angular 22 component for exploring keys, diatonic chords, and common progressions with an interactive circle of fifths.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Component } from '@angular/core';
|
|
7
|
+
import { CircleOfFifthsComponent } from '@gblp/circle-of-fifths';
|
|
8
|
+
|
|
9
|
+
@Component({
|
|
10
|
+
imports: [CircleOfFifthsComponent],
|
|
11
|
+
template: '<the-chords-circle-of-fifths />'
|
|
12
|
+
})
|
|
13
|
+
export class App {}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Build the package with `npm run build:lib`. The publishable Angular package is written to `dist/circle-of-fifths-lib`.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { signal, Component } from '@angular/core';
|
|
3
|
+
import { NgClass } from '@angular/common';
|
|
4
|
+
|
|
5
|
+
const MAJOR_PROGRESSIONS = [
|
|
6
|
+
{ name: 'Anthemic', mood: '😄 Joyful', genre: 'Folk, Rock', numerals: ['I', 'IV', 'V', 'I'] },
|
|
7
|
+
{ name: 'Hopeful', mood: '✨ Hopeful', genre: 'Pop, Indie', numerals: ['I', 'V', 'vi', 'IV'] },
|
|
8
|
+
{ name: 'Melancholic', mood: '😢 Emotional', genre: 'Ballads, Pop', numerals: ['vi', 'IV', 'I', 'V'] },
|
|
9
|
+
{ name: 'Cinematic', mood: '🎬 Tense', genre: 'Film, Jazz', numerals: ['ii', 'V', 'vii°', 'I'] },
|
|
10
|
+
];
|
|
11
|
+
const MINOR_PROGRESSIONS = [
|
|
12
|
+
{ name: 'Brooding', mood: '🌑 Dark', genre: 'Rock, Metal', numerals: ['i', 'VII', 'VI', 'VII'] },
|
|
13
|
+
{ name: 'Haunting', mood: '👻 Mysterious', genre: 'Film, Gothic', numerals: ['i', 'iv', 'VII', 'III'] },
|
|
14
|
+
{ name: 'Driving', mood: 'âš¡ Urgent', genre: 'Pop, EDM', numerals: ['i', 'VI', 'III', 'VII'] },
|
|
15
|
+
{ name: 'Wistful', mood: '🌙 Wistful', genre: 'Cinematic', numerals: ['i', 'v', 'VI', 'VII'] },
|
|
16
|
+
];
|
|
17
|
+
class CircleOfFifthsComponent {
|
|
18
|
+
CX = 300;
|
|
19
|
+
CY = 300;
|
|
20
|
+
KEY_SIG = { inner: 250, outer: 288 };
|
|
21
|
+
MAJOR = { inner: 172, outer: 250 };
|
|
22
|
+
MINOR = { inner: 102, outer: 172 };
|
|
23
|
+
keys = [
|
|
24
|
+
{ index: 0, major: 'C', minor: 'Am', sharps: 0, flats: 0 },
|
|
25
|
+
{ index: 1, major: 'G', minor: 'Em', sharps: 1, flats: 0 },
|
|
26
|
+
{ index: 2, major: 'D', minor: 'Bm', sharps: 2, flats: 0 },
|
|
27
|
+
{ index: 3, major: 'A', minor: 'F♯m', sharps: 3, flats: 0 },
|
|
28
|
+
{ index: 4, major: 'E', minor: 'C♯m', sharps: 4, flats: 0 },
|
|
29
|
+
{ index: 5, major: 'B', minor: 'G♯m', sharps: 5, flats: 0 },
|
|
30
|
+
{ index: 6, major: 'F♯', minor: 'D♯m', sharps: 6, flats: 0 },
|
|
31
|
+
{ index: 7, major: 'Dâ™', minor: 'Bâ™m', sharps: 0, flats: 5 },
|
|
32
|
+
{ index: 8, major: 'Aâ™', minor: 'Fm', sharps: 0, flats: 4 },
|
|
33
|
+
{ index: 9, major: 'Eâ™', minor: 'Cm', sharps: 0, flats: 3 },
|
|
34
|
+
{ index: 10, major: 'Bâ™', minor: 'Gm', sharps: 0, flats: 2 },
|
|
35
|
+
{ index: 11, major: 'F', minor: 'Dm', sharps: 0, flats: 1 },
|
|
36
|
+
];
|
|
37
|
+
selectedIndex = signal(null, /* @ts-ignore */
|
|
38
|
+
...(ngDevMode ? [{ debugName: "selectedIndex" }] : /* istanbul ignore next */ []));
|
|
39
|
+
selectedType = signal(null, /* @ts-ignore */
|
|
40
|
+
...(ngDevMode ? [{ debugName: "selectedType" }] : /* istanbul ignore next */ []));
|
|
41
|
+
get slice() {
|
|
42
|
+
const idx = this.selectedIndex();
|
|
43
|
+
if (idx === null)
|
|
44
|
+
return null;
|
|
45
|
+
return {
|
|
46
|
+
prev: (idx - 1 + 12) % 12,
|
|
47
|
+
curr: idx,
|
|
48
|
+
next: (idx + 1) % 12,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
selectKey(index, type) {
|
|
52
|
+
if (this.selectedIndex() === index && this.selectedType() === type) {
|
|
53
|
+
this.selectedIndex.set(null);
|
|
54
|
+
this.selectedType.set(null);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
this.selectedIndex.set(index);
|
|
58
|
+
this.selectedType.set(type);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
getMajorState(index) {
|
|
62
|
+
const s = this.slice;
|
|
63
|
+
if (!s)
|
|
64
|
+
return '';
|
|
65
|
+
const type = this.selectedType();
|
|
66
|
+
if (index === s.curr)
|
|
67
|
+
return type === 'major' ? 'tonic' : 'scale-major';
|
|
68
|
+
if (index === s.prev || index === s.next)
|
|
69
|
+
return 'scale-major';
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
getMinorState(index) {
|
|
73
|
+
const s = this.slice;
|
|
74
|
+
if (!s)
|
|
75
|
+
return '';
|
|
76
|
+
const type = this.selectedType();
|
|
77
|
+
const dimIdx = (s.curr + 2) % 12;
|
|
78
|
+
if (index === s.curr)
|
|
79
|
+
return type === 'minor' ? 'tonic' : 'scale-minor';
|
|
80
|
+
if (index === s.prev || index === s.next)
|
|
81
|
+
return 'scale-minor';
|
|
82
|
+
if (index === dimIdx)
|
|
83
|
+
return 'scale-diminished';
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
getMajorRole(index) {
|
|
87
|
+
const s = this.slice;
|
|
88
|
+
if (!s)
|
|
89
|
+
return '';
|
|
90
|
+
const type = this.selectedType();
|
|
91
|
+
if (type === 'major') {
|
|
92
|
+
if (index === s.curr)
|
|
93
|
+
return 'I';
|
|
94
|
+
if (index === s.prev)
|
|
95
|
+
return 'IV';
|
|
96
|
+
if (index === s.next)
|
|
97
|
+
return 'V';
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
if (index === s.curr)
|
|
101
|
+
return 'III';
|
|
102
|
+
if (index === s.prev)
|
|
103
|
+
return 'VI';
|
|
104
|
+
if (index === s.next)
|
|
105
|
+
return 'VII';
|
|
106
|
+
}
|
|
107
|
+
return '';
|
|
108
|
+
}
|
|
109
|
+
getMinorRole(index) {
|
|
110
|
+
const s = this.slice;
|
|
111
|
+
if (!s)
|
|
112
|
+
return '';
|
|
113
|
+
const type = this.selectedType();
|
|
114
|
+
const dimIdx = (s.curr + 2) % 12;
|
|
115
|
+
if (type === 'major') {
|
|
116
|
+
if (index === s.curr)
|
|
117
|
+
return 'vi';
|
|
118
|
+
if (index === s.prev)
|
|
119
|
+
return 'ii';
|
|
120
|
+
if (index === s.next)
|
|
121
|
+
return 'iii';
|
|
122
|
+
if (index === dimIdx)
|
|
123
|
+
return 'vii°';
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
if (index === s.curr)
|
|
127
|
+
return 'i';
|
|
128
|
+
if (index === s.prev)
|
|
129
|
+
return 'iv';
|
|
130
|
+
if (index === s.next)
|
|
131
|
+
return 'v';
|
|
132
|
+
if (index === dimIdx)
|
|
133
|
+
return 'ii°';
|
|
134
|
+
}
|
|
135
|
+
return '';
|
|
136
|
+
}
|
|
137
|
+
arcPath(innerR, outerR, index, gap = 1.5) {
|
|
138
|
+
const mid = index * 30 - 90;
|
|
139
|
+
const s = (mid - 15 + gap) * (Math.PI / 180);
|
|
140
|
+
const e = (mid + 15 - gap) * (Math.PI / 180);
|
|
141
|
+
const cx = this.CX, cy = this.CY;
|
|
142
|
+
const x1 = cx + outerR * Math.cos(s), y1 = cy + outerR * Math.sin(s);
|
|
143
|
+
const x2 = cx + outerR * Math.cos(e), y2 = cy + outerR * Math.sin(e);
|
|
144
|
+
const x3 = cx + innerR * Math.cos(e), y3 = cy + innerR * Math.sin(e);
|
|
145
|
+
const x4 = cx + innerR * Math.cos(s), y4 = cy + innerR * Math.sin(s);
|
|
146
|
+
const f = (n) => n.toFixed(2);
|
|
147
|
+
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`;
|
|
148
|
+
}
|
|
149
|
+
textPos(midR, index) {
|
|
150
|
+
const a = (index * 30 - 90) * (Math.PI / 180);
|
|
151
|
+
return {
|
|
152
|
+
x: this.CX + midR * Math.cos(a),
|
|
153
|
+
y: this.CY + midR * Math.sin(a),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
dimChord(idx) {
|
|
157
|
+
// The diminished chord root is the minor key two steps clockwise (idx+2)
|
|
158
|
+
const minor = this.keys[(idx + 2) % 12].minor;
|
|
159
|
+
return minor.replace(/m$/, '°');
|
|
160
|
+
}
|
|
161
|
+
getAccidentalText(key) {
|
|
162
|
+
if (key.sharps > 0)
|
|
163
|
+
return `${key.sharps}♯`;
|
|
164
|
+
if (key.flats > 0)
|
|
165
|
+
return `${key.flats}â™`;
|
|
166
|
+
return '—';
|
|
167
|
+
}
|
|
168
|
+
get selectedInfo() {
|
|
169
|
+
const idx = this.selectedIndex();
|
|
170
|
+
const type = this.selectedType();
|
|
171
|
+
if (idx === null || type === null)
|
|
172
|
+
return null;
|
|
173
|
+
const key = this.keys[idx];
|
|
174
|
+
return {
|
|
175
|
+
name: type === 'major' ? key.major : key.minor,
|
|
176
|
+
scale: type === 'major' ? 'Major' : 'Minor',
|
|
177
|
+
fullName: type === 'major' ? `${key.major} Major` : `${key.minor} Minor`,
|
|
178
|
+
relativeKey: type === 'major'
|
|
179
|
+
? `Relative minor: ${key.minor}`
|
|
180
|
+
: `Relative major: ${key.major}`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
get chordTable() {
|
|
184
|
+
const idx = this.selectedIndex();
|
|
185
|
+
const type = this.selectedType();
|
|
186
|
+
if (idx === null || type === null)
|
|
187
|
+
return [];
|
|
188
|
+
const prev = (idx - 1 + 12) % 12;
|
|
189
|
+
const next = (idx + 1) % 12;
|
|
190
|
+
if (type === 'major') {
|
|
191
|
+
return [
|
|
192
|
+
{ numeral: 'I', chord: this.keys[idx].major, chordType: 'major', role: 'Tonic', state: 'tonic' },
|
|
193
|
+
{ numeral: 'ii', chord: this.keys[prev].minor, chordType: 'minor', role: 'Supertonic', state: 'scale-minor' },
|
|
194
|
+
{ numeral: 'iii', chord: this.keys[next].minor, chordType: 'minor', role: 'Mediant', state: 'scale-minor' },
|
|
195
|
+
{ numeral: 'IV', chord: this.keys[prev].major, chordType: 'major', role: 'Subdominant', state: 'scale-major' },
|
|
196
|
+
{ numeral: 'V', chord: this.keys[next].major, chordType: 'major', role: 'Dominant', state: 'scale-major' },
|
|
197
|
+
{ numeral: 'vi', chord: this.keys[idx].minor, chordType: 'minor', role: 'Submediant', state: 'scale-minor' },
|
|
198
|
+
{ numeral: 'vii°', chord: this.dimChord(idx), chordType: 'diminished', role: 'Leading Tone', state: 'scale-diminished' },
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
return [
|
|
203
|
+
{ numeral: 'i', chord: this.keys[idx].minor, chordType: 'minor', role: 'Tonic', state: 'tonic' },
|
|
204
|
+
{ numeral: 'iv', chord: this.keys[prev].minor, chordType: 'minor', role: 'Subdominant', state: 'scale-minor' },
|
|
205
|
+
{ numeral: 'v', chord: this.keys[next].minor, chordType: 'minor', role: 'Dominant', state: 'scale-minor' },
|
|
206
|
+
{ numeral: 'III', chord: this.keys[idx].major, chordType: 'major', role: 'Mediant', state: 'scale-major' },
|
|
207
|
+
{ numeral: 'VI', chord: this.keys[prev].major, chordType: 'major', role: 'Submediant', state: 'scale-major' },
|
|
208
|
+
{ numeral: 'VII', chord: this.keys[next].major, chordType: 'major', role: 'Subtonic', state: 'scale-major' },
|
|
209
|
+
{ numeral: 'ii°', chord: this.dimChord(idx), chordType: 'diminished', role: 'Supertonic', state: 'scale-diminished' },
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
get progressions() {
|
|
214
|
+
const table = this.chordTable;
|
|
215
|
+
if (!table.length)
|
|
216
|
+
return [];
|
|
217
|
+
const lookup = new Map(table.map(r => [r.numeral, r.chord]));
|
|
218
|
+
const defs = this.selectedType() === 'major' ? MAJOR_PROGRESSIONS : MINOR_PROGRESSIONS;
|
|
219
|
+
return defs.map(d => ({ ...d, chords: d.numerals.map(n => lookup.get(n) ?? n) }));
|
|
220
|
+
}
|
|
221
|
+
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"] }] });
|
|
223
|
+
}
|
|
224
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: CircleOfFifthsComponent, decorators: [{
|
|
225
|
+
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
|
+
}] });
|
|
228
|
+
|
|
229
|
+
/*
|
|
230
|
+
* Public API Surface of circle-of-fifths-lib
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Generated bundle index. Do not edit.
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
export { CircleOfFifthsComponent };
|
|
238
|
+
//# sourceMappingURL=gblp-circle-of-fifths.mjs.map
|
|
@@ -0,0 +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":";;;;AA2BA,MAAM,kBAAkB,GAAkC;IACxD,EAAE,IAAI,EAAE,UAAU,EAAK,IAAI,EAAE,WAAW,EAAO,KAAK,EAAE,YAAY,EAAK,QAAQ,EAAE,CAAC,GAAG,EAAC,IAAI,EAAC,GAAG,EAAC,GAAG,CAAC,EAAO;IAC1G,EAAE,IAAI,EAAE,SAAS,EAAM,IAAI,EAAE,WAAW,EAAO,KAAK,EAAE,YAAY,EAAK,QAAQ,EAAE,CAAC,GAAG,EAAC,GAAG,EAAC,IAAI,EAAC,IAAI,CAAC,EAAM;IAC1G,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,cAAc,EAAK,KAAK,EAAE,cAAc,EAAG,QAAQ,EAAE,CAAC,IAAI,EAAC,IAAI,EAAC,GAAG,EAAC,GAAG,CAAC,EAAM;IAC3G,EAAE,IAAI,EAAE,WAAW,EAAI,IAAI,EAAE,UAAU,EAAS,KAAK,EAAE,YAAY,EAAK,QAAQ,EAAE,CAAC,IAAI,EAAC,GAAG,EAAC,MAAM,EAAC,GAAG,CAAC,EAAI;CAC5G;AAED,MAAM,kBAAkB,GAAkC;IACxD,EAAE,IAAI,EAAE,UAAU,EAAK,IAAI,EAAE,SAAS,EAAU,KAAK,EAAE,aAAa,EAAI,QAAQ,EAAE,CAAC,GAAG,EAAC,KAAK,EAAC,IAAI,EAAC,KAAK,CAAC,EAAG;IAC3G,EAAE,IAAI,EAAE,UAAU,EAAK,IAAI,EAAE,eAAe,EAAI,KAAK,EAAE,cAAc,EAAG,QAAQ,EAAE,CAAC,GAAG,EAAC,IAAI,EAAC,KAAK,EAAC,KAAK,CAAC,EAAG;IAC3G,EAAE,IAAI,EAAE,SAAS,EAAM,IAAI,EAAE,UAAU,EAAS,KAAK,EAAE,UAAU,EAAO,QAAQ,EAAE,CAAC,GAAG,EAAC,IAAI,EAAC,KAAK,EAAC,KAAK,CAAC,EAAG;IAC3G,EAAE,IAAI,EAAE,SAAS,EAAM,IAAI,EAAE,YAAY,EAAO,KAAK,EAAE,WAAW,EAAM,QAAQ,EAAE,CAAC,GAAG,EAAC,GAAG,EAAC,IAAI,EAAC,KAAK,CAAC,EAAK;CAC5G;MASY,uBAAuB,CAAA;IACzB,EAAE,GAAG,GAAG;IACR,EAAE,GAAG,GAAG;IAER,OAAO,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE;IACpC,KAAK,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE;IAClC,KAAK,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE;AAElC,IAAA,IAAI,GAAgB;AAC3B,QAAA,EAAE,KAAK,EAAE,CAAC,EAAG,KAAK,EAAE,GAAG,EAAI,KAAK,EAAE,IAAI,EAAI,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;AAC/D,QAAA,EAAE,KAAK,EAAE,CAAC,EAAG,KAAK,EAAE,GAAG,EAAI,KAAK,EAAE,IAAI,EAAI,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;AAC/D,QAAA,EAAE,KAAK,EAAE,CAAC,EAAG,KAAK,EAAE,GAAG,EAAI,KAAK,EAAE,IAAI,EAAI,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;AAC/D,QAAA,EAAE,KAAK,EAAE,CAAC,EAAG,KAAK,EAAE,GAAG,EAAI,KAAK,EAAE,KAAK,EAAG,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;AAC/D,QAAA,EAAE,KAAK,EAAE,CAAC,EAAG,KAAK,EAAE,GAAG,EAAI,KAAK,EAAE,KAAK,EAAG,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;AAC/D,QAAA,EAAE,KAAK,EAAE,CAAC,EAAG,KAAK,EAAE,GAAG,EAAI,KAAK,EAAE,KAAK,EAAG,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;AAC/D,QAAA,EAAE,KAAK,EAAE,CAAC,EAAG,KAAK,EAAE,IAAI,EAAG,KAAK,EAAE,KAAK,EAAG,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;AAC/D,QAAA,EAAE,KAAK,EAAE,CAAC,EAAG,KAAK,EAAE,IAAI,EAAG,KAAK,EAAE,KAAK,EAAG,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;AAC/D,QAAA,EAAE,KAAK,EAAE,CAAC,EAAG,KAAK,EAAE,IAAI,EAAG,KAAK,EAAE,IAAI,EAAI,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;AAC/D,QAAA,EAAE,KAAK,EAAE,CAAC,EAAG,KAAK,EAAE,IAAI,EAAG,KAAK,EAAE,IAAI,EAAI,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;AAC/D,QAAA,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAG,KAAK,EAAE,IAAI,EAAI,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;AAC/D,QAAA,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAI,KAAK,EAAE,IAAI,EAAI,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;KAChE;IAED,aAAa,GAAG,MAAM,CAAgB,IAAI;sFAAC;IAC3C,YAAY,GAAG,MAAM,CAA2B,IAAI;qFAAC;AAErD,IAAA,IAAY,KAAK,GAAA;AACf,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,EAAE;QAChC,IAAI,GAAG,KAAK,IAAI;AAAE,YAAA,OAAO,IAAI;QAC7B,OAAO;YACL,IAAI,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE;AACzB,YAAA,IAAI,EAAE,GAAG;AACT,YAAA,IAAI,EAAE,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE;SACrB;IACH;IAEA,SAAS,CAAC,KAAa,EAAE,IAAuB,EAAA;AAC9C,QAAA,IAAI,IAAI,CAAC,aAAa,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC,YAAY,EAAE,KAAK,IAAI,EAAE;AAClE,YAAA,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC;AAC5B,YAAA,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC;QAC7B;aAAO;AACL,YAAA,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC;AAC7B,YAAA,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC;QAC7B;IACF;AAEA,IAAA,aAAa,CAAC,KAAa,EAAA;AACzB,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK;AACpB,QAAA,IAAI,CAAC,CAAC;AAAE,YAAA,OAAO,EAAE;AACjB,QAAA,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE;AAChC,QAAA,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI;YAAE,OAAO,IAAI,KAAK,OAAO,GAAG,OAAO,GAAG,aAAa;QACvE,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI;AAAE,YAAA,OAAO,aAAa;AAC9D,QAAA,OAAO,EAAE;IACX;AAEA,IAAA,aAAa,CAAC,KAAa,EAAA;AACzB,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK;AACpB,QAAA,IAAI,CAAC,CAAC;AAAE,YAAA,OAAO,EAAE;AACjB,QAAA,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE;QAChC,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE;AAChC,QAAA,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI;YAAE,OAAO,IAAI,KAAK,OAAO,GAAG,OAAO,GAAG,aAAa;QACvE,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI;AAAE,YAAA,OAAO,aAAa;QAC9D,IAAI,KAAK,KAAK,MAAM;AAAE,YAAA,OAAO,kBAAkB;AAC/C,QAAA,OAAO,EAAE;IACX;AAEA,IAAA,YAAY,CAAC,KAAa,EAAA;AACxB,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK;AACpB,QAAA,IAAI,CAAC,CAAC;AAAE,YAAA,OAAO,EAAE;AACjB,QAAA,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE;AAChC,QAAA,IAAI,IAAI,KAAK,OAAO,EAAE;AACpB,YAAA,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI;AAAE,gBAAA,OAAO,GAAG;AAChC,YAAA,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI;AAAE,gBAAA,OAAO,IAAI;AACjC,YAAA,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI;AAAE,gBAAA,OAAO,GAAG;QAClC;aAAO;AACL,YAAA,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI;AAAE,gBAAA,OAAO,KAAK;AAClC,YAAA,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI;AAAE,gBAAA,OAAO,IAAI;AACjC,YAAA,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI;AAAE,gBAAA,OAAO,KAAK;QACpC;AACA,QAAA,OAAO,EAAE;IACX;AAEA,IAAA,YAAY,CAAC,KAAa,EAAA;AACxB,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK;AACpB,QAAA,IAAI,CAAC,CAAC;AAAE,YAAA,OAAO,EAAE;AACjB,QAAA,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE;QAChC,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE;AAChC,QAAA,IAAI,IAAI,KAAK,OAAO,EAAE;AACpB,YAAA,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI;AAAE,gBAAA,OAAO,IAAI;AACjC,YAAA,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI;AAAE,gBAAA,OAAO,IAAI;AACjC,YAAA,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI;AAAE,gBAAA,OAAO,KAAK;YAClC,IAAI,KAAK,KAAK,MAAM;AAAE,gBAAA,OAAO,MAAM;QACrC;aAAO;AACL,YAAA,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI;AAAE,gBAAA,OAAO,GAAG;AAChC,YAAA,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI;AAAE,gBAAA,OAAO,IAAI;AACjC,YAAA,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI;AAAE,gBAAA,OAAO,GAAG;YAChC,IAAI,KAAK,KAAK,MAAM;AAAE,gBAAA,OAAO,KAAK;QACpC;AACA,QAAA,OAAO,EAAE;IACX;IAEA,OAAO,CAAC,MAAc,EAAE,MAAc,EAAE,KAAa,EAAE,GAAG,GAAG,GAAG,EAAA;AAC9D,QAAA,MAAM,GAAG,GAAG,KAAK,GAAG,EAAE,GAAG,EAAE;AAC3B,QAAA,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,GAAG,GAAG,KAAK,IAAI,CAAC,EAAE,GAAG,GAAG,CAAC;AAC5C,QAAA,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,GAAG,GAAG,KAAK,IAAI,CAAC,EAAE,GAAG,GAAG,CAAC;QAC5C,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,IAAI,CAAC,EAAE;QAChC,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QACpE,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QACpE,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QACpE,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACpE,QAAA,MAAM,CAAC,GAAG,CAAC,CAAS,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;QACrC,OAAO,CAAA,CAAA,EAAI,CAAC,CAAC,EAAE,CAAC,CAAA,CAAA,EAAI,CAAC,CAAC,EAAE,CAAC,CAAA,EAAA,EAAK,MAAM,CAAA,CAAA,EAAI,MAAM,UAAU,CAAC,CAAC,EAAE,CAAC,CAAA,CAAA,EAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAA,CAAA,EAAI,CAAC,CAAC,EAAE,CAAC,CAAA,EAAA,EAAK,MAAM,CAAA,CAAA,EAAI,MAAM,CAAA,OAAA,EAAU,CAAC,CAAC,EAAE,CAAC,CAAA,CAAA,EAAI,CAAC,CAAC,EAAE,CAAC,CAAA,CAAA,CAAG;IAC3I;IAEA,OAAO,CAAC,IAAY,EAAE,KAAa,EAAA;AACjC,QAAA,MAAM,CAAC,GAAG,CAAC,KAAK,GAAG,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC,EAAE,GAAG,GAAG,CAAC;QAC7C,OAAO;AACL,YAAA,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/B,YAAA,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;SAChC;IACH;AAEQ,IAAA,QAAQ,CAAC,GAAW,EAAA;;AAE1B,QAAA,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK;QAC7C,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC;IACjC;AAEA,IAAA,iBAAiB,CAAC,GAAc,EAAA;AAC9B,QAAA,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;AAAE,YAAA,OAAO,CAAA,EAAG,GAAG,CAAC,MAAM,GAAG;AAC3C,QAAA,IAAI,GAAG,CAAC,KAAK,GAAG,CAAC;AAAE,YAAA,OAAO,CAAA,EAAG,GAAG,CAAC,KAAK,GAAG;AACzC,QAAA,OAAO,GAAG;IACZ;AAEA,IAAA,IAAI,YAAY,GAAA;AACd,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,EAAE;AAChC,QAAA,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE;AAChC,QAAA,IAAI,GAAG,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI;AAAE,YAAA,OAAO,IAAI;QAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;QAC1B,OAAO;AACL,YAAA,IAAI,EAAE,IAAI,KAAK,OAAO,GAAG,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,KAAK;YAC9C,KAAK,EAAE,IAAI,KAAK,OAAO,GAAG,OAAO,GAAG,OAAO;AAC3C,YAAA,QAAQ,EAAE,IAAI,KAAK,OAAO,GAAG,CAAA,EAAG,GAAG,CAAC,KAAK,CAAA,MAAA,CAAQ,GAAG,GAAG,GAAG,CAAC,KAAK,CAAA,MAAA,CAAQ;YACxE,WAAW,EAAE,IAAI,KAAK;AACpB,kBAAE,CAAA,gBAAA,EAAmB,GAAG,CAAC,KAAK,CAAA;AAC9B,kBAAE,CAAA,gBAAA,EAAmB,GAAG,CAAC,KAAK,CAAA,CAAE;SACnC;IACH;AAEA,IAAA,IAAI,UAAU,GAAA;AACZ,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,EAAE;AAChC,QAAA,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE;AAChC,QAAA,IAAI,GAAG,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI;AAAE,YAAA,OAAO,EAAE;QAE5C,MAAM,IAAI,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE;QAChC,MAAM,IAAI,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE;AAE3B,QAAA,IAAI,IAAI,KAAK,OAAO,EAAE;YACpB,OAAO;gBACL,EAAE,OAAO,EAAE,GAAG,EAAI,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAG,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAQ,KAAK,EAAE,OAAO,EAAE;gBACzG,EAAE,OAAO,EAAE,IAAI,EAAG,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAG,KAAK,EAAE,aAAa,EAAE;gBAC/G,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAM,KAAK,EAAE,aAAa,EAAE;gBAC/G,EAAE,OAAO,EAAE,IAAI,EAAG,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,EAAE;gBAC/G,EAAE,OAAO,EAAE,GAAG,EAAI,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAK,KAAK,EAAE,aAAa,EAAE;gBAC/G,EAAE,OAAO,EAAE,IAAI,EAAI,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAG,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAG,KAAK,EAAE,aAAa,EAAE;gBAChH,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAK,SAAS,EAAE,YAAY,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,kBAAkB,EAAE;aAC5H;QACH;aAAO;YACL,OAAO;gBACL,EAAE,OAAO,EAAE,GAAG,EAAI,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAG,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAQ,KAAK,EAAE,OAAO,EAAE;gBACzG,EAAE,OAAO,EAAE,IAAI,EAAG,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,EAAE;gBAC/G,EAAE,OAAO,EAAE,GAAG,EAAI,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAK,KAAK,EAAE,aAAa,EAAE;gBAC/G,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAG,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAM,KAAK,EAAE,aAAa,EAAE;gBAC/G,EAAE,OAAO,EAAE,IAAI,EAAG,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAG,KAAK,EAAE,aAAa,EAAE;gBAC/G,EAAE,OAAO,EAAE,KAAK,EAAG,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAK,KAAK,EAAE,aAAa,EAAE;gBAChH,EAAE,OAAO,EAAE,KAAK,EAAG,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAK,SAAS,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,kBAAkB,EAAE;aAC1H;QACH;IACF;AAEA,IAAA,IAAI,YAAY,GAAA;AACd,QAAA,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU;QAC7B,IAAI,CAAC,KAAK,CAAC,MAAM;AAAE,YAAA,OAAO,EAAE;QAC5B,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AAC5D,QAAA,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,KAAK,OAAO,GAAG,kBAAkB,GAAG,kBAAkB;AACtF,QAAA,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;IACnF;uGA1LW,uBAAuB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;2FAAvB,uBAAuB,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,6BAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EChDpC,isMAiJA,EAAA,MAAA,EAAA,CAAA,++LAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EDrGY,OAAO,EAAA,QAAA,EAAA,WAAA,EAAA,MAAA,EAAA,CAAA,OAAA,EAAA,SAAA,CAAA,EAAA,CAAA,EAAA,CAAA;;2FAIN,uBAAuB,EAAA,UAAA,EAAA,CAAA;kBAPnC,SAAS;AACE,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,6BAA6B,EAAA,UAAA,EAC3B,IAAI,EAAA,OAAA,EACP,CAAC,OAAO,CAAC,EAAA,QAAA,EAAA,isMAAA,EAAA,MAAA,EAAA,CAAA,++LAAA,CAAA,EAAA;;;AE5CpB;;AAEG;;ACFH;;AAEG;;;;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gblp/circle-of-fifths",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"peerDependencies": {
|
|
8
|
+
"@angular/common": "^22.0.0",
|
|
9
|
+
"@angular/core": "^22.0.0"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"tslib": "^2.3.0"
|
|
13
|
+
},
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"module": "fesm2022/gblp-circle-of-fifths.mjs",
|
|
16
|
+
"typings": "types/gblp-circle-of-fifths.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
"./package.json": {
|
|
19
|
+
"default": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./types/gblp-circle-of-fifths.d.ts",
|
|
23
|
+
"default": "./fesm2022/gblp-circle-of-fifths.mjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"type": "module"
|
|
27
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as _angular_core from '@angular/core';
|
|
2
|
+
|
|
3
|
+
interface CircleKey {
|
|
4
|
+
index: number;
|
|
5
|
+
major: string;
|
|
6
|
+
minor: string;
|
|
7
|
+
sharps: number;
|
|
8
|
+
flats: number;
|
|
9
|
+
}
|
|
10
|
+
interface ChordRow {
|
|
11
|
+
numeral: string;
|
|
12
|
+
chord: string;
|
|
13
|
+
chordType: 'major' | 'minor' | 'diminished';
|
|
14
|
+
role: string;
|
|
15
|
+
state: 'tonic' | 'scale-major' | 'scale-minor' | 'scale-diminished';
|
|
16
|
+
}
|
|
17
|
+
interface Progression {
|
|
18
|
+
name: string;
|
|
19
|
+
mood: string;
|
|
20
|
+
genre: string;
|
|
21
|
+
numerals: string[];
|
|
22
|
+
chords: string[];
|
|
23
|
+
}
|
|
24
|
+
declare class CircleOfFifthsComponent {
|
|
25
|
+
readonly CX = 300;
|
|
26
|
+
readonly CY = 300;
|
|
27
|
+
readonly KEY_SIG: {
|
|
28
|
+
inner: number;
|
|
29
|
+
outer: number;
|
|
30
|
+
};
|
|
31
|
+
readonly MAJOR: {
|
|
32
|
+
inner: number;
|
|
33
|
+
outer: number;
|
|
34
|
+
};
|
|
35
|
+
readonly MINOR: {
|
|
36
|
+
inner: number;
|
|
37
|
+
outer: number;
|
|
38
|
+
};
|
|
39
|
+
readonly keys: CircleKey[];
|
|
40
|
+
selectedIndex: _angular_core.WritableSignal<number | null>;
|
|
41
|
+
selectedType: _angular_core.WritableSignal<"major" | "minor" | null>;
|
|
42
|
+
private get slice();
|
|
43
|
+
selectKey(index: number, type: 'major' | 'minor'): void;
|
|
44
|
+
getMajorState(index: number): string;
|
|
45
|
+
getMinorState(index: number): string;
|
|
46
|
+
getMajorRole(index: number): string;
|
|
47
|
+
getMinorRole(index: number): string;
|
|
48
|
+
arcPath(innerR: number, outerR: number, index: number, gap?: number): string;
|
|
49
|
+
textPos(midR: number, index: number): {
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
};
|
|
53
|
+
private dimChord;
|
|
54
|
+
getAccidentalText(key: CircleKey): string;
|
|
55
|
+
get selectedInfo(): {
|
|
56
|
+
name: string;
|
|
57
|
+
scale: string;
|
|
58
|
+
fullName: string;
|
|
59
|
+
relativeKey: string;
|
|
60
|
+
} | null;
|
|
61
|
+
get chordTable(): ChordRow[];
|
|
62
|
+
get progressions(): Progression[];
|
|
63
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<CircleOfFifthsComponent, never>;
|
|
64
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<CircleOfFifthsComponent, "the-chords-circle-of-fifths", never, {}, {}, never, never, true, never>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { CircleOfFifthsComponent };
|