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