@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 };