@greenblacksky/honmoon 0.0.0-stage → 0.4.1

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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -2
  3. package/package.json +58 -4
  4. package/src/lib/audio/context.ts +29 -0
  5. package/src/lib/audio/metronome.svelte.ts +146 -0
  6. package/src/lib/audio/mic.ts +254 -0
  7. package/src/lib/audio/pitch.ts +133 -0
  8. package/src/lib/audio/rhythmplay.ts +152 -0
  9. package/src/lib/audio/synth.ts +409 -0
  10. package/src/lib/audio/voiceplay.ts +60 -0
  11. package/src/lib/drills/defs.ts +459 -0
  12. package/src/lib/drills/questions.ts +920 -0
  13. package/src/lib/drills/registry.svelte.ts +167 -0
  14. package/src/lib/drills/runtime.svelte.ts +629 -0
  15. package/src/lib/drills/verdict.ts +241 -0
  16. package/src/lib/index.ts +117 -0
  17. package/src/lib/learn/describe.ts +72 -0
  18. package/src/lib/learn/pools.ts +44 -0
  19. package/src/lib/learn/progress.ts +100 -0
  20. package/src/lib/learn/session.ts +304 -0
  21. package/src/lib/music/bass.ts +139 -0
  22. package/src/lib/music/clefs.ts +218 -0
  23. package/src/lib/music/intervals.ts +94 -0
  24. package/src/lib/music/notes.ts +133 -0
  25. package/src/lib/music/rhythm.ts +624 -0
  26. package/src/lib/music/scales.ts +560 -0
  27. package/src/lib/music/voice.ts +227 -0
  28. package/src/lib/music/warmups.ts +318 -0
  29. package/src/lib/nav.svelte.ts +53 -0
  30. package/src/lib/notation/fretboard.ts +187 -0
  31. package/src/lib/notation/glyphdata.ts +101 -0
  32. package/src/lib/notation/glyphs.ts +71 -0
  33. package/src/lib/notation/icons.ts +12 -0
  34. package/src/lib/notation/keyboard.ts +166 -0
  35. package/src/lib/notation/rhythmsvg.ts +341 -0
  36. package/src/lib/notation/staff.ts +288 -0
  37. package/src/lib/notation/svg.ts +37 -0
  38. package/src/lib/notation/tab.ts +80 -0
  39. package/src/lib/store/honmoon.svelte.ts +329 -0
  40. package/src/lib/store/normalize.ts +23 -0
  41. package/src/lib/store/sync.ts +43 -0
  42. package/src/lib/store/types.ts +101 -0
  43. package/src/lib/styles/honmoon.css +2064 -0
  44. package/src/lib/ui/AccRow.svelte +23 -0
  45. package/src/lib/ui/BackBar.svelte +23 -0
  46. package/src/lib/ui/BassRow.svelte +30 -0
  47. package/src/lib/ui/Chips.svelte +23 -0
  48. package/src/lib/ui/DataCard.svelte +66 -0
  49. package/src/lib/ui/Fretboard.svelte +71 -0
  50. package/src/lib/ui/Keyboard.svelte +54 -0
  51. package/src/lib/ui/MetronomePanel.svelte +101 -0
  52. package/src/lib/ui/RefBar.svelte +28 -0
  53. package/src/lib/ui/RhythmBar.svelte +9 -0
  54. package/src/lib/ui/Shell.svelte +96 -0
  55. package/src/lib/ui/StaffRun.svelte +46 -0
  56. package/src/lib/ui/Stat.svelte +13 -0
  57. package/src/lib/ui/actions.ts +36 -0
  58. package/src/lib/ui/keyfollow.svelte.ts +48 -0
  59. package/src/lib/ui/overlay.ts +15 -0
  60. package/src/lib/ui/refdata.ts +355 -0
  61. package/src/lib/ui/scaleboard.ts +420 -0
  62. package/src/lib/ui/screens/DegreesRef.svelte +405 -0
  63. package/src/lib/ui/screens/DrillScreen.svelte +1155 -0
  64. package/src/lib/ui/screens/Explorer.svelte +245 -0
  65. package/src/lib/ui/screens/FamilyScreen.svelte +280 -0
  66. package/src/lib/ui/screens/Glossary.svelte +65 -0
  67. package/src/lib/ui/screens/IntervalRef.svelte +37 -0
  68. package/src/lib/ui/screens/LearnIndex.svelte +44 -0
  69. package/src/lib/ui/screens/MetronomeScreen.svelte +17 -0
  70. package/src/lib/ui/screens/PracticeHome.svelte +159 -0
  71. package/src/lib/ui/screens/RhythmRef.svelte +239 -0
  72. package/src/lib/ui/screens/ScalesScreen.svelte +210 -0
  73. package/src/lib/ui/screens/SessionDone.svelte +119 -0
  74. package/src/lib/ui/screens/SetupScreen.svelte +548 -0
  75. package/src/lib/ui/screens/StaffMap.svelte +230 -0
  76. package/src/lib/ui/screens/StatsScreen.svelte +478 -0
  77. package/src/lib/ui/screens/TapCalibration.svelte +186 -0
  78. package/src/lib/ui/tapfix.ts +93 -0
  79. package/src/lib/ui/types.ts +40 -0
  80. package/src/lib/util/list.ts +4 -0
  81. package/src/lib/util/random.ts +23 -0
  82. package/src/lib/util/text.ts +50 -0
@@ -0,0 +1,17 @@
1
+ <!-- The metronome on its own page. It keeps running while you move around the app. -->
2
+ <script lang="ts">
3
+ import MetronomePanel from '../MetronomePanel.svelte';
4
+ import BackBar from '../BackBar.svelte';
5
+ import { go } from '../../nav.svelte';
6
+ const { back = { label: 'Warm up', to: 'warmhome' } }: { back?: { label: string; to: string } } =
7
+ $props();
8
+ </script>
9
+
10
+ <BackBar label={back.label} title="Metronome" sec onBack={() => go(back.to)} />
11
+ <div class="sec">
12
+ <MetronomePanel />
13
+ <p class="note" style="margin-top:10px">
14
+ It keeps running while you move around the app — the beats are scheduled on the audio clock, not
15
+ by the page.
16
+ </p>
17
+ </div>
@@ -0,0 +1,159 @@
1
+ <!--
2
+ One screen answers "what shall I do now": the session that picks for you,
3
+ and the families to go at by hand when you want to hammer one thing.
4
+ -->
5
+ <script lang="ts">
6
+ import { honmoon, setSetting, queueSave } from '../../store/honmoon.svelte';
7
+ import { todayKey } from '../../util/text';
8
+ import { toggledIn } from '../../util/list';
9
+ import { micUsable } from '../../audio/mic';
10
+ import { bassHere } from '../../music/bass';
11
+ import { allCards } from '../../learn/session';
12
+ import { famMastery, streakDays, totals } from '../../learn/progress';
13
+ import {
14
+ appDrills,
15
+ appGroups,
16
+ famDrills,
17
+ famHidden,
18
+ famNoBass,
19
+ groupOff,
20
+ sessionGroups,
21
+ } from '../../drills/registry.svelte';
22
+ import { IC } from '../../notation/icons';
23
+ import { startSession } from '../../drills/runtime.svelte';
24
+ import { go } from '../../nav.svelte';
25
+ import Stat from '../Stat.svelte';
26
+ import BassRow from '../BassRow.svelte';
27
+
28
+ /** A rough guess at how long one card takes, for the "about N minutes" line. */
29
+ const SECONDS_PER_CARD = 8;
30
+ const SLENS = [
31
+ { n: 15, label: 'Short' },
32
+ { n: 30, label: 'Standard' },
33
+ { n: 50, label: 'Long' },
34
+ ];
35
+ const overall = $derived(totals());
36
+ const today = $derived(honmoon.daily[todayKey()] || { n: 0, c: 0 });
37
+ const sessionLength = $derived(honmoon.settings.slen || 30);
38
+ const poolSize = $derived(allCards().length);
39
+ const needsBass = $derived(appDrills().some((def) => def.bass));
40
+ const groups = sessionGroups();
41
+ const hasScales = appGroups().some((group) => group.k === 'scale');
42
+
43
+ function toggleGroup(group: string) {
44
+ const off = toggledIn(honmoon.settings.off || [], group);
45
+ if (off.length >= appGroups().length) {
46
+ return; // never switch everything off
47
+ }
48
+ honmoon.settings.off = off;
49
+ queueSave();
50
+ }
51
+ const plural = (count: number, word: string) => count + ' ' + word + (count === 1 ? '' : 's');
52
+ function open(group: string) {
53
+ if (group === 'scale') {
54
+ go('scales');
55
+ } else {
56
+ go('fam', { fam: group });
57
+ }
58
+ }
59
+ </script>
60
+
61
+ <div class="sec">
62
+ <Stat
63
+ items={[
64
+ { v: today.n, k: 'Today' },
65
+ { v: overall.n, k: 'All time' },
66
+ { v: overall.n ? overall.acc + '%' : '—', k: 'Accuracy' },
67
+ { v: streakDays(), k: 'Day streak' },
68
+ ]}
69
+ />
70
+ </div>
71
+
72
+ <div class="sec">
73
+ <div class="card sesscard stack">
74
+ <div>
75
+ <div class="nm" style="font-family:var(--f-disp);font-size:22px;letter-spacing:.02em">
76
+ Daily session
77
+ </div>
78
+ <p class="note" style="margin:4px 0 0">
79
+ Your weakest cards from every family you leave switched on, mixed together. Miss one and it
80
+ comes back later in the same session.
81
+ </p>
82
+ </div>
83
+ <div class="levels">
84
+ {#each SLENS as length (length.n)}
85
+ <button
86
+ class="chip sm"
87
+ data-set="slen"
88
+ data-val={length.n}
89
+ aria-pressed={+sessionLength === length.n}
90
+ onclick={() => setSetting('slen', length.n)}>{length.label} · {length.n}</button
91
+ >
92
+ {/each}
93
+ </div>
94
+ {#if groups.length > 1}
95
+ <div>
96
+ <div class="hint" style="margin-bottom:6px">Include</div>
97
+ <div class="levels">
98
+ {#each groups as group (group.k)}
99
+ <button
100
+ class="chip sm tog"
101
+ data-grp={group.k}
102
+ aria-pressed={!groupOff(group.k)}
103
+ onclick={() => toggleGroup(group.k)}>{group.n}</button
104
+ >
105
+ {/each}
106
+ </div>
107
+ </div>
108
+ {/if}
109
+ {#if needsBass}<BassRow />{/if}
110
+ {#if needsBass && bassHere() && !micUsable()}<p class="note" style="margin:0">
111
+ With no microphone here, the three play-along drills ask you to mark yourself. They still
112
+ count.
113
+ </p>{/if}
114
+ <button class="start" id="startsess" onclick={() => startSession('theory')}
115
+ >Start session</button
116
+ >
117
+ <p class="note" style="text-align:center;margin:0">
118
+ {sessionLength} cards · about {Math.max(
119
+ 1,
120
+ Math.round((sessionLength * SECONDS_PER_CARD) / 60),
121
+ )} minutes{poolSize && poolSize < sessionLength
122
+ ? ' · only ' + poolSize + ' cards switched on, so some come round twice'
123
+ : ''}
124
+ </p>
125
+ </div>
126
+ </div>
127
+
128
+ <div class="sec">
129
+ <h2>Families</h2>
130
+ <div class="fams">
131
+ {#each appGroups() as group (group.k)}
132
+ {@const drills = famDrills(group.k)}
133
+ {@const progress = famMastery(group.k)}
134
+ {@const gated = famHidden(group.k)}
135
+ {@const noBass = famNoBass(group.k)}
136
+ <button class="fam" data-open={group.k} onclick={() => open(group.k)}>
137
+ <span class="ic">{@html IC[group.ic]}</span>
138
+ <span class="txt"
139
+ ><span class="nm">{group.n}</span>
140
+ <span class="sb">{group.sb}</span>
141
+ <span class="meta"
142
+ >{plural(drills.length, 'drill')}{gated
143
+ ? ' · ' + gated + ' more waiting in Setup'
144
+ : ''}{noBass ? ' · ' + noBass + ' waiting for the bass' : ''}{progress.pct
145
+ ? ' · ' + progress.pct + '% solid'
146
+ : ''}</span
147
+ ></span
148
+ >
149
+ <span class="mast">→</span></button
150
+ >
151
+ {/each}
152
+ </div>
153
+ <p class="note">
154
+ Switching a family off here takes it out of the daily session; it stays open to practise by
155
+ hand.{hasScales
156
+ ? ' Scales also have a session of their own, on their page, which ignores this row.'
157
+ : ''}
158
+ </p>
159
+ </div>
@@ -0,0 +1,239 @@
1
+ <!-- How rhythm is written: values, dots, ties, rests, bars, time signatures, and counting out loud. -->
2
+ <script lang="ts">
3
+ import { METERS, RVALS, beatsText, type Bar } from '../../music/rhythm';
4
+ import { rhOneSVG, tsGlyphSVG } from '../../notation/rhythmsvg';
5
+ import { playMeter } from '../../audio/synth';
6
+ import RhythmBar from '../RhythmBar.svelte';
7
+ import RefBar from '../RefBar.svelte';
8
+
9
+ interface Props {
10
+ back?: { label: string; to: string };
11
+ /** where the drop-out clock lives, for the last paragraph */
12
+ clockWhere?: string;
13
+ }
14
+ const { back, clockWhere = 'Mira (the bass app), under Warm up,' }: Props = $props();
15
+
16
+ /** An example bar from its events. Compound time groups in dotted quarters (6 sixteenths). */
17
+ const bar = (ts: [number, number], events: Bar['ev'], compound = false): Bar => ({
18
+ ts,
19
+ u: events.reduce((sum, event) => sum + event.u, 0),
20
+ g: compound ? 6 : 4,
21
+ cmp: compound,
22
+ ev: events,
23
+ });
24
+ </script>
25
+
26
+ <RefBar title="Reading rhythm" {back} />
27
+
28
+ <div class="sec">
29
+ <h2>The line is time, not pitch</h2>
30
+ <div class="card stack">
31
+ <p class="note">
32
+ On a rhythm staff there is one line and it means nothing about how high the note is — only
33
+ when it happens and how long it lasts. This is worth learning on its own, because rhythm is
34
+ the half of reading that carries a part. A bassist who reads rhythm perfectly and pitch
35
+ approximately will hold a band together; the reverse will not.
36
+ </p>
37
+ <p class="note">
38
+ Every value below is a whole number of sixteenths, and that is the only arithmetic reading
39
+ ever needs.
40
+ </p>
41
+ </div>
42
+ </div>
43
+
44
+ <div class="sec">
45
+ <h2>The values</h2>
46
+ <div class="card">
47
+ <table class="opens">
48
+ <tbody>
49
+ {#each RVALS as value (value.u)}
50
+ <tr
51
+ ><td style="width:72px">{@html rhOneSVG(value.u, false, 62)}</td>
52
+ <td><b>{value.n}</b><br /><span class="hint">{beatsText(value.u)} in 4/4</span></td>
53
+ <td style="width:72px">{@html rhOneSVG(value.u, true, 62)}</td></tr
54
+ >
55
+ {/each}
56
+ </tbody>
57
+ </table>
58
+ <p class="note" style="margin-top:8px">
59
+ Left: the note. Right: the rest of the same length — the same amount of time, deliberately not
60
+ played. Rests are music, and reading them accurately is what stops a line turning to mush.
61
+ </p>
62
+ <p class="note">
63
+ The whole rest and the half rest are the same little block; the only difference is which side
64
+ of the line it sits on. The whole rest hangs below, the half rest sits on top. Everyone mixes
65
+ them up at first.
66
+ </p>
67
+ </div>
68
+ </div>
69
+
70
+ <div class="sec">
71
+ <h2>The dot</h2>
72
+ <div class="card">
73
+ <RhythmBar bar={bar([4, 4], [{ u: 6 }, { u: 2 }, { u: 4 }, { u: 4 }])} count={true} />
74
+ <p class="note" style="margin-top:8px">
75
+ A dot after a note adds half of it again: a dotted quarter is a quarter plus an eighth, a beat
76
+ and a half. That is where nearly every syncopation in pop and rock comes from — the second
77
+ note lands on the <i>and</i>, and the phrase starts pushing.
78
+ </p>
79
+ </div>
80
+ </div>
81
+
82
+ <div class="sec">
83
+ <h2>The tie</h2>
84
+ <div class="card">
85
+ <RhythmBar
86
+ bar={bar([4, 4], [{ u: 2 }, { u: 2 }, { u: 2, tie: true }, { u: 2 }, { u: 4 }, { u: 4 }])}
87
+ count={true}
88
+ />
89
+ <p class="note" style="margin-top:8px">
90
+ A curved line joining two noteheads means play the first and hold through the second: one
91
+ sound, both lengths. It exists so a note can cross a beat or a barline while the beats stay
92
+ visible underneath. Counting a tie out loud, you say the number for the tied-to note but do
93
+ not attack it.
94
+ </p>
95
+ </div>
96
+ </div>
97
+
98
+ <div class="sec">
99
+ <h2>Counting out loud</h2>
100
+ <div class="card stack">
101
+ <RhythmBar
102
+ bar={bar(
103
+ [4, 4],
104
+ [{ u: 1 }, { u: 1 }, { u: 1 }, { u: 1 }, { u: 2 }, { u: 2 }, { u: 4 }, { u: 2 }, { u: 2 }],
105
+ )}
106
+ count={true}
107
+ />
108
+ <p class="note">
109
+ Four sixteenths in a beat are counted <b>1 e &amp; a</b>. Eighths are <b>1 &amp;</b>. A
110
+ quarter is just <b>1</b>. Say it aloud while you tap: the syllable tells you where you are
111
+ inside the beat without counting anything, and it is the fastest route from the page to your
112
+ hands.
113
+ </p>
114
+ <p class="note">
115
+ This is also why notes shorter than a quarter are beamed in groups of one beat. The beams are
116
+ not decoration — they draw the beat for you, so you can see at a glance that four sixteenths
117
+ belong to beat 3.
118
+ </p>
119
+ <p class="note">
120
+ <b>Writing a counting down.</b> The Count the bar drill writes the whole bar out in a row, one
121
+ slot for each step of it, in order: the syllable where a note starts and a <b>·</b> where
122
+ nothing does. So a bar of four quarter notes is
123
+ <span style="font-family:var(--f-mono)">1 2 3 4</span>, and a bar that plays only on the first
124
+ beat and the <i>and</i> of three is
125
+ <span style="font-family:var(--f-mono)">1 · · · · · &amp; ·</span>. Listing only the syllables
126
+ you play would be shorter, but two different bars can produce the same short list — the dots
127
+ are what pin each note to its place.
128
+ </p>
129
+ </div>
130
+ </div>
131
+
132
+ <div class="sec">
133
+ <h2>Bars and time signatures</h2>
134
+ <div class="card stack">
135
+ <p class="note">
136
+ Music is counted in equal packets called <b>bars</b>, and the vertical line at the end of one
137
+ is the <b>barline</b>. The first beat of every bar is the strong one — it is where a phrase
138
+ lands, where a chord usually changes, and where the bass usually plays the root. Nearly
139
+ everything about feel comes from knowing where beat 1 is.
140
+ </p>
141
+ <p class="note">
142
+ The pair of numbers at the start says how those packets are built. It looks like a fraction
143
+ but it is not one; it is two separate facts stacked up:
144
+ </p>
145
+ <div class="tsrow">
146
+ <div class="tsnum">{@html tsGlyphSVG([4, 4], 62)}</div>
147
+ <div>
148
+ <p class="note" style="margin:0">
149
+ <b>The bottom number</b> names the note value that counts as one beat. 4 means a quarter
150
+ note, 8 means an eighth note, 2 means a half note. It is the <i>denominator</i> in the sense
151
+ that 4 means “quarter”, and that is the only sense in which any of this is a fraction.
152
+ </p>
153
+ <p class="note"><b>The top number</b> says how many of those fit in a bar.</p>
154
+ <p class="note" style="margin-bottom:0">
155
+ So <b>4/4</b> is four quarter-note beats to a bar, and <b>3/4</b> is three. Same beat, different
156
+ sized packet — and that one difference is what separates a rock song from a waltz.
157
+ </p>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ </div>
162
+
163
+ <div class="sec">
164
+ <h2>The ones you will meet</h2>
165
+ <div class="card stack">
166
+ <p class="note">
167
+ Tap any of them to hear two bars of the bare pulse. The accent is on beat 1 — listen for how
168
+ far apart the accents are, because that <i>is</i> the time signature.
169
+ </p>
170
+ {#each METERS as meter (meter.n)}
171
+ <div class="tsrow">
172
+ <div class="tsnum">{@html tsGlyphSVG(meter.ts, 54)}</div>
173
+ <div style="flex:1;min-width:0">
174
+ <div class="setrow" style="border:none;padding:0">
175
+ <span class="lbl">{meter.n}</span>
176
+ <button
177
+ class="chip sm"
178
+ data-meter={meter.ts.join('/')}
179
+ onclick={() => playMeter(meter.ts)}>♪ Hear it</button
180
+ >
181
+ </div>
182
+ <p class="note" style="margin:4px 0 0">{@html meter.d}</p>
183
+ </div>
184
+ </div>
185
+ {/each}
186
+ </div>
187
+ </div>
188
+
189
+ <div class="sec">
190
+ <h2>Counting a bar of 3/4</h2>
191
+ <div class="card stack">
192
+ <RhythmBar bar={bar([3, 4], [{ u: 4 }, { u: 2 }, { u: 2 }, { u: 4 }])} count={true} />
193
+ <p class="note">
194
+ Three beats, so the counting only ever reaches 3 before the barline comes round and it starts
195
+ again at 1. A bar of 3/4 that adds up to four beats is simply wrong — that is the rule the
196
+ Complete the bar drill is testing.
197
+ </p>
198
+ </div>
199
+ </div>
200
+
201
+ <div class="sec">
202
+ <h2>Why 6/8 is different</h2>
203
+ <div class="card stack">
204
+ <RhythmBar
205
+ bar={bar([6, 8], [{ u: 2 }, { u: 2 }, { u: 2 }, { u: 4 }, { u: 2 }], true)}
206
+ count={true}
207
+ />
208
+ <p class="note">
209
+ Read literally, 6/8 is six eighth-note beats in a bar. Nobody counts it that way above a
210
+ crawl. In the ear it is <b>two</b> beats that each divide into <b>three</b> — the accents fall
211
+ on 1 and 4 — and that is what makes it roll rather than march. Time signatures that work like
212
+ this are called <i>compound</i>; 6/8, 9/8 and 12/8 are the common ones.
213
+ </p>
214
+ <p class="note">
215
+ The quick test: if the top number is 6, 9 or 12 over an 8, divide it by three to get the real
216
+ number of beats. Count the eighths out loud at first — 1 2 3 4 5 6 — and let the weight settle
217
+ on 1 and 4 until the two-beat feel takes over.
218
+ </p>
219
+ </div>
220
+ </div>
221
+
222
+ <div class="sec">
223
+ <h2>How to practise it</h2>
224
+ <div class="card stack">
225
+ <p class="note">
226
+ <b>Say it before you play it.</b> Count the bar out loud with the metronome until it is automatic.
227
+ Only then put it on the instrument — sight-reading fails far more often on rhythm than on notes.
228
+ </p>
229
+ <p class="note">
230
+ <b>Subdivide even when nothing is subdivided.</b> If the bar is four quarter notes, still feel the
231
+ eighths underneath. That is what keeps long notes from shrinking.
232
+ </p>
233
+ <p class="note">
234
+ <b>Practise the silence.</b> The Internal clock exercise in {clockWhere} takes the metronome away
235
+ for a few bars at a time. Holding tempo through silence is the skill; playing along with a click
236
+ is only the test.
237
+ </p>
238
+ </div>
239
+ </div>
@@ -0,0 +1,210 @@
1
+ <!-- The Scales family: its own session, the drills, where you stand, and which scales are in. -->
2
+ <script lang="ts">
3
+ import { honmoon, setSetting, queueSave } from '../../store/honmoon.svelte';
4
+ import { bassHere } from '../../music/bass';
5
+ import {
6
+ SCALES,
7
+ SGROUPS,
8
+ degPretty,
9
+ enabledScales,
10
+ scaleCards,
11
+ scaleOn,
12
+ } from '../../music/scales';
13
+ import { IC } from '../../notation/icons';
14
+ import { scaleDrillsInApp, sdrillOn } from '../../drills/registry.svelte';
15
+ import { startDrill, startSession } from '../../drills/runtime.svelte';
16
+ import { go, hasRoute } from '../../nav.svelte';
17
+ import { toggledIn } from '../../util/list';
18
+ import { scaleMatrix, scaleStats } from '../scaleboard';
19
+ import Stat from '../Stat.svelte';
20
+ import BassRow from '../BassRow.svelte';
21
+
22
+ const scaleDrills = scaleDrillsInApp();
23
+ const enabled = $derived(enabledScales());
24
+ const pairs = $derived(scaleCards().length);
25
+ const summary = $derived(scaleStats());
26
+ const needsBass = scaleDrills.some((def) => def.bass);
27
+ /* which groups of scales mean anything for this app's drills: arpeggios
28
+ and the rest are always in play, so every group is shown */
29
+ function toggleSdrill(id: string) {
30
+ const off = toggledIn(honmoon.settings.sdoff || [], id);
31
+ if (scaleDrills.every((def) => off.includes(def.id))) {
32
+ return; // never switch every scale drill off
33
+ }
34
+ honmoon.settings.sdoff = off;
35
+ queueSave();
36
+ }
37
+ function toggleScale(id: string) {
38
+ honmoon.settings.sen = toggledIn(honmoon.settings.sen || [], id);
39
+ queueSave();
40
+ }
41
+ </script>
42
+
43
+ <div class="sec">
44
+ <div class="bar">
45
+ <button class="ghost" id="back" onclick={() => go('home')}>← Practice</button>
46
+ <span class="spacer"></span><span class="tick">Scales</span>
47
+ </div>
48
+ </div>
49
+
50
+ <div class="sec">
51
+ <div class="card sesscard stack">
52
+ <div>
53
+ <div style="font-family:var(--f-disp);font-size:22px;letter-spacing:.02em">Scale session</div>
54
+ <p class="note" style="margin:4px 0 0">
55
+ Nothing but scale-and-key pairs, for when you want a run of them. They also turn up in the
56
+ daily session unless you switch the family off there.
57
+ </p>
58
+ </div>
59
+ <div class="levels">
60
+ {#each [10, 20, 30] as n (n)}
61
+ <button
62
+ class="chip sm"
63
+ data-set="sslen"
64
+ data-val={n}
65
+ aria-pressed={+(honmoon.settings.sslen || 20) === n}
66
+ onclick={() => setSetting('sslen', n)}>{n}</button
67
+ >
68
+ {/each}
69
+ </div>
70
+ <div>
71
+ <div class="hint" style="margin-bottom:6px">Drills</div>
72
+ <div class="levels">
73
+ {#each scaleDrills as def (def.id)}
74
+ {@const away = def.bass && !bassHere()}
75
+ <button
76
+ class="chip sm tog"
77
+ data-sdrill={def.id}
78
+ aria-pressed={sdrillOn(def.id) && !away}
79
+ disabled={away}
80
+ style={away ? 'opacity:.5' : ''}
81
+ onclick={() => toggleSdrill(def.id)}>{def.nm}{def.bass ? ' ♪' : ''}</button
82
+ >
83
+ {/each}
84
+ </div>
85
+ </div>
86
+ {#if needsBass}<BassRow />{/if}
87
+ <div>
88
+ <div class="hint" style="margin-bottom:6px">Roots</div>
89
+ <div class="levels">
90
+ <button
91
+ class="chip sm"
92
+ data-set="sroots"
93
+ data-val="nat"
94
+ aria-pressed={honmoon.settings.sroots !== 'all'}
95
+ onclick={() => setSetting('sroots', 'nat')}>Natural only</button
96
+ >
97
+ <button
98
+ class="chip sm"
99
+ data-set="sroots"
100
+ data-val="all"
101
+ aria-pressed={honmoon.settings.sroots === 'all'}
102
+ onclick={() => setSetting('sroots', 'all')}>All twelve</button
103
+ >
104
+ {#if needsBass || scaleDrills.some((def) => def.id === 'scbuild' || def.id === 'scwrong' || def.id === 'scname')}
105
+ <button
106
+ class="chip sm"
107
+ data-set="sstart"
108
+ data-val="both"
109
+ aria-pressed={honmoon.settings.sstart === 'both'}
110
+ onclick={() => setSetting('sstart', 'both')}>Either string</button
111
+ >
112
+ <button
113
+ class="chip sm"
114
+ data-set="sstart"
115
+ data-val="E"
116
+ aria-pressed={honmoon.settings.sstart === 'E'}
117
+ onclick={() => setSetting('sstart', 'E')}>E only</button
118
+ >
119
+ <button
120
+ class="chip sm"
121
+ data-set="sstart"
122
+ data-val="A"
123
+ aria-pressed={honmoon.settings.sstart === 'A'}
124
+ onclick={() => setSetting('sstart', 'A')}>A only</button
125
+ >
126
+ {/if}
127
+ </div>
128
+ </div>
129
+ <button class="start" id="startscale" onclick={() => startSession('scales')}
130
+ >Start scale session</button
131
+ >
132
+ <p class="note" style="text-align:center;margin:0">
133
+ {enabled.length} scales · {pairs} scale-and-key pairs in rotation
134
+ </p>
135
+ {#if !enabled.length}<p class="err">Switch at least one scale on below.</p>{/if}
136
+ </div>
137
+ </div>
138
+
139
+ {#if hasRoute('sx')}
140
+ <div class="sec">
141
+ <button class="drill ref" data-go="sx" onclick={() => go('sx')}
142
+ ><span class="ic">{@html IC.book}</span>
143
+ <span
144
+ ><span class="nm">Scale explorer</span><br /><span class="sb"
145
+ >Every scale in every key on the neck, with degrees, fingerings and the notes spelled
146
+ properly.</span
147
+ ></span
148
+ >
149
+ <span class="mast">→</span></button
150
+ >
151
+ </div>
152
+ {/if}
153
+
154
+ <div class="sec">
155
+ <h2>Where you stand</h2>
156
+ <Stat
157
+ items={[
158
+ { v: summary.seen, k: 'Met' },
159
+ { v: summary.solid, k: 'Solid' },
160
+ { v: summary.ans, k: 'Answers' },
161
+ { v: summary.ans ? summary.acc + '%' : '—', k: 'Accuracy' },
162
+ ]}
163
+ />
164
+ <div class="card" style="margin-top:10px">
165
+ {@html scaleMatrix()}
166
+ <div class="legend" style="margin-top:10px">
167
+ <span><b class="m0"></b>not met</span><span><b class="m1"></b>shaky</span>
168
+ <span><b class="m3"></b>getting there</span><span><b class="m5"></b>solid</span>
169
+ </div>
170
+ </div>
171
+ <p class="note">
172
+ One square per scale in one key — {summary.total} of them in rotation. Squares you miss come back
173
+ sooner and more often, exactly as they do on the Drills side. This is the same spaced repetition,
174
+ just over scales.
175
+ </p>
176
+ </div>
177
+
178
+ <div class="sec">
179
+ <h2>Single drills</h2>
180
+ <div class="drills">
181
+ {#each scaleDrills as def (def.id)}
182
+ <button class="drill" data-drill={def.id} onclick={() => startDrill(def.id, 'scale')}
183
+ ><span class="ic">{@html IC[def.ic]}</span>
184
+ <span><span class="nm">{def.nm}</span><br /><span class="sb">{def.sb}</span></span><span
185
+ class="mast">→</span
186
+ ></button
187
+ >
188
+ {/each}
189
+ </div>
190
+ </div>
191
+
192
+ {#each SGROUPS as group (group)}
193
+ <div class="sec">
194
+ <h2>{group}</h2>
195
+ <div class="stack">
196
+ {#each SCALES.filter((scale) => scale.g === group) as scale (scale.id)}
197
+ <div class="scrow" class:on={scaleOn(scale.id)}>
198
+ <button
199
+ class="chip sm tog"
200
+ data-scale={scale.id}
201
+ aria-pressed={scaleOn(scale.id)}
202
+ onclick={() => toggleScale(scale.id)}>{scale.n}</button
203
+ >
204
+ <span class="ivs">{scale.dg.map(degPretty).join(' ')}</span>
205
+ <p class="note">{scale.d}</p>
206
+ </div>
207
+ {/each}
208
+ </div>
209
+ </div>
210
+ {/each}