@greenblacksky/honmoon 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +81 -2
- package/package.json +58 -4
- package/src/lib/audio/context.ts +29 -0
- package/src/lib/audio/metronome.svelte.ts +146 -0
- package/src/lib/audio/mic.ts +254 -0
- package/src/lib/audio/pitch.ts +133 -0
- package/src/lib/audio/rhythmplay.ts +152 -0
- package/src/lib/audio/synth.ts +409 -0
- package/src/lib/audio/voiceplay.ts +60 -0
- package/src/lib/drills/defs.ts +459 -0
- package/src/lib/drills/questions.ts +920 -0
- package/src/lib/drills/registry.svelte.ts +167 -0
- package/src/lib/drills/runtime.svelte.ts +629 -0
- package/src/lib/drills/verdict.ts +241 -0
- package/src/lib/index.ts +117 -0
- package/src/lib/learn/describe.ts +72 -0
- package/src/lib/learn/pools.ts +44 -0
- package/src/lib/learn/progress.ts +100 -0
- package/src/lib/learn/session.ts +304 -0
- package/src/lib/music/bass.ts +139 -0
- package/src/lib/music/clefs.ts +218 -0
- package/src/lib/music/intervals.ts +94 -0
- package/src/lib/music/notes.ts +133 -0
- package/src/lib/music/rhythm.ts +624 -0
- package/src/lib/music/scales.ts +560 -0
- package/src/lib/music/voice.ts +227 -0
- package/src/lib/music/warmups.ts +318 -0
- package/src/lib/nav.svelte.ts +53 -0
- package/src/lib/notation/fretboard.ts +187 -0
- package/src/lib/notation/glyphdata.ts +101 -0
- package/src/lib/notation/glyphs.ts +71 -0
- package/src/lib/notation/icons.ts +12 -0
- package/src/lib/notation/keyboard.ts +166 -0
- package/src/lib/notation/rhythmsvg.ts +341 -0
- package/src/lib/notation/staff.ts +288 -0
- package/src/lib/notation/svg.ts +37 -0
- package/src/lib/notation/tab.ts +80 -0
- package/src/lib/store/honmoon.svelte.ts +329 -0
- package/src/lib/store/normalize.ts +23 -0
- package/src/lib/store/sync.ts +43 -0
- package/src/lib/store/types.ts +101 -0
- package/src/lib/styles/honmoon.css +2064 -0
- package/src/lib/ui/AccRow.svelte +23 -0
- package/src/lib/ui/BackBar.svelte +23 -0
- package/src/lib/ui/BassRow.svelte +30 -0
- package/src/lib/ui/Chips.svelte +23 -0
- package/src/lib/ui/DataCard.svelte +66 -0
- package/src/lib/ui/Fretboard.svelte +71 -0
- package/src/lib/ui/Keyboard.svelte +54 -0
- package/src/lib/ui/MetronomePanel.svelte +101 -0
- package/src/lib/ui/RefBar.svelte +28 -0
- package/src/lib/ui/RhythmBar.svelte +9 -0
- package/src/lib/ui/Shell.svelte +96 -0
- package/src/lib/ui/StaffRun.svelte +46 -0
- package/src/lib/ui/Stat.svelte +13 -0
- package/src/lib/ui/actions.ts +36 -0
- package/src/lib/ui/keyfollow.svelte.ts +48 -0
- package/src/lib/ui/overlay.ts +15 -0
- package/src/lib/ui/refdata.ts +355 -0
- package/src/lib/ui/scaleboard.ts +420 -0
- package/src/lib/ui/screens/DegreesRef.svelte +405 -0
- package/src/lib/ui/screens/DrillScreen.svelte +1155 -0
- package/src/lib/ui/screens/Explorer.svelte +245 -0
- package/src/lib/ui/screens/FamilyScreen.svelte +280 -0
- package/src/lib/ui/screens/Glossary.svelte +65 -0
- package/src/lib/ui/screens/IntervalRef.svelte +37 -0
- package/src/lib/ui/screens/LearnIndex.svelte +44 -0
- package/src/lib/ui/screens/MetronomeScreen.svelte +17 -0
- package/src/lib/ui/screens/PracticeHome.svelte +159 -0
- package/src/lib/ui/screens/RhythmRef.svelte +239 -0
- package/src/lib/ui/screens/ScalesScreen.svelte +210 -0
- package/src/lib/ui/screens/SessionDone.svelte +119 -0
- package/src/lib/ui/screens/SetupScreen.svelte +548 -0
- package/src/lib/ui/screens/StaffMap.svelte +230 -0
- package/src/lib/ui/screens/StatsScreen.svelte +478 -0
- package/src/lib/ui/screens/TapCalibration.svelte +186 -0
- package/src/lib/ui/tapfix.ts +93 -0
- package/src/lib/ui/types.ts +40 -0
- package/src/lib/util/list.ts +4 -0
- package/src/lib/util/random.ts +23 -0
- package/src/lib/util/text.ts +50 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* Every control is driven from pointer events, not click. On Android Chrome a
|
|
2
|
+
click can be withheld altogether — the button lights up from :active and
|
|
3
|
+
nothing else happens — when the browser is still arbitrating the gesture.
|
|
4
|
+
pointerdown/pointerup always arrive, so a tap is recognised here: press and
|
|
5
|
+
release on the same button, little movement, no long press. The button is
|
|
6
|
+
then clicked programmatically, so every Svelte onclick handler just works.
|
|
7
|
+
|
|
8
|
+
The browser may still send its own click afterwards, and by then the screen
|
|
9
|
+
has often re-drawn — it would hit whatever button moved under the finger
|
|
10
|
+
and fire a second, wrong action. Real pointer clicks that arrive just after
|
|
11
|
+
a recognised tap are swallowed. Keyboard Enter and Space produce a click
|
|
12
|
+
with no pointer sequence in front of it, and those always go through. */
|
|
13
|
+
|
|
14
|
+
let installed = false;
|
|
15
|
+
/** A press that moves further than this is a pan, not a tap. */
|
|
16
|
+
const MAX_MOVE_PX = 14;
|
|
17
|
+
/** A press held longer than this is a long press, not a tap. */
|
|
18
|
+
const MAX_PRESS_MS = 800;
|
|
19
|
+
/** The browser's own click arriving this soon after a recognised tap is a duplicate. */
|
|
20
|
+
const DUPLICATE_CLICK_MS = 600;
|
|
21
|
+
|
|
22
|
+
export function installTapFix(root: Document = document): void {
|
|
23
|
+
if (installed || typeof window === 'undefined' || !window.PointerEvent) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
installed = true;
|
|
27
|
+
let press: { button: HTMLButtonElement; x: number; y: number; t: number } | null = null;
|
|
28
|
+
let tappedAt = 0;
|
|
29
|
+
let clickingOurselves = false;
|
|
30
|
+
const buttonAt = (node: EventTarget | null) =>
|
|
31
|
+
node && (node as Element).closest
|
|
32
|
+
? ((node as Element).closest('button') as HTMLButtonElement | null)
|
|
33
|
+
: null;
|
|
34
|
+
|
|
35
|
+
root.addEventListener(
|
|
36
|
+
'pointerdown',
|
|
37
|
+
(event) => {
|
|
38
|
+
const button = buttonAt(event.target);
|
|
39
|
+
press = button ? { button, x: event.clientX, y: event.clientY, t: Date.now() } : null;
|
|
40
|
+
},
|
|
41
|
+
true,
|
|
42
|
+
);
|
|
43
|
+
root.addEventListener(
|
|
44
|
+
'pointercancel',
|
|
45
|
+
() => {
|
|
46
|
+
press = null;
|
|
47
|
+
},
|
|
48
|
+
true,
|
|
49
|
+
);
|
|
50
|
+
root.addEventListener(
|
|
51
|
+
'pointerup',
|
|
52
|
+
(event) => {
|
|
53
|
+
const started = press;
|
|
54
|
+
press = null;
|
|
55
|
+
if (!started || buttonAt(event.target) !== started.button) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const moved =
|
|
59
|
+
Math.abs(event.clientX - started.x) > MAX_MOVE_PX ||
|
|
60
|
+
Math.abs(event.clientY - started.y) > MAX_MOVE_PX;
|
|
61
|
+
const heldTooLong = Date.now() - started.t > MAX_PRESS_MS;
|
|
62
|
+
if (moved || heldTooLong || started.button.disabled) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
tappedAt = Date.now();
|
|
66
|
+
clickingOurselves = true;
|
|
67
|
+
try {
|
|
68
|
+
started.button.click();
|
|
69
|
+
} finally {
|
|
70
|
+
clickingOurselves = false;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
true,
|
|
74
|
+
);
|
|
75
|
+
root.addEventListener(
|
|
76
|
+
'click',
|
|
77
|
+
(event) => {
|
|
78
|
+
if (clickingOurselves) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const mouse = event as MouseEvent;
|
|
82
|
+
const fromKeyboard = mouse.detail === 0 && !mouse.clientX && !mouse.clientY;
|
|
83
|
+
if (fromKeyboard || !buttonAt(event.target)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (Date.now() - tappedAt < DUPLICATE_CLICK_MS) {
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
true,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** One tab in the header. */
|
|
2
|
+
export interface Tab {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** the screen the tab opens */
|
|
6
|
+
to: string;
|
|
7
|
+
/** every screen that belongs to this tab, so it stays lit there */
|
|
8
|
+
views: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The sections the Stats screen can show; each app lists the ones it wants. */
|
|
12
|
+
export type StatsSection =
|
|
13
|
+
| 'overall'
|
|
14
|
+
| 'twoweeks'
|
|
15
|
+
| 'areas'
|
|
16
|
+
| 'bynote'
|
|
17
|
+
| 'neck'
|
|
18
|
+
| 'staff'
|
|
19
|
+
| 'scales'
|
|
20
|
+
| 'byscale'
|
|
21
|
+
| 'weakest'
|
|
22
|
+
| 'tempo'
|
|
23
|
+
| 'data';
|
|
24
|
+
|
|
25
|
+
/** The rows the Setup screen can show; each app lists the ones it wants. */
|
|
26
|
+
export type SetupRow =
|
|
27
|
+
| 'range'
|
|
28
|
+
| 'acc'
|
|
29
|
+
| 'pref'
|
|
30
|
+
| 'appearance'
|
|
31
|
+
| 'bass'
|
|
32
|
+
| 'hints'
|
|
33
|
+
| 'labels'
|
|
34
|
+
| 'clefs'
|
|
35
|
+
| 'mdo'
|
|
36
|
+
| 'rhythm'
|
|
37
|
+
| 'sound'
|
|
38
|
+
| 'mic'
|
|
39
|
+
| 'how'
|
|
40
|
+
| 'data';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* Every random choice in Honmoon goes through Math.random, on purpose: a test
|
|
2
|
+
can replace it with a seeded generator and get the same questions every
|
|
3
|
+
run. That only holds while the code asks for random numbers in the same
|
|
4
|
+
order, so keep the order of calls when refactoring, and do not reach for a
|
|
5
|
+
second source of randomness. */
|
|
6
|
+
|
|
7
|
+
/** A random whole number from 0 up to (not including) n. */
|
|
8
|
+
export function rnd(n: number): number {
|
|
9
|
+
return Math.floor(Math.random() * n);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function pickOf<T>(items: readonly T[]): T {
|
|
13
|
+
return items[rnd(items.length)];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Fisher–Yates, in place. Returns the same array for chaining. */
|
|
17
|
+
export function shuffle<T>(items: T[]): T[] {
|
|
18
|
+
for (let i = items.length - 1; i > 0; i--) {
|
|
19
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
20
|
+
[items[i], items[j]] = [items[j], items[i]];
|
|
21
|
+
}
|
|
22
|
+
return items;
|
|
23
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Escapes text for use inside the SVG/HTML strings the notation code builds. */
|
|
2
|
+
export function esc(s: unknown): string {
|
|
3
|
+
return String(s).replace(
|
|
4
|
+
/[&<>]/g,
|
|
5
|
+
(c) => ({ '&': '&', '<': '<', '>': '>' })[c] as string,
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** "a fifth", "an octave" */
|
|
10
|
+
export function anA(w: string): string {
|
|
11
|
+
return (/^[aeiou]/i.test(w) ? 'an ' : 'a ') + w;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const NUMWORD = [
|
|
15
|
+
'zero',
|
|
16
|
+
'one',
|
|
17
|
+
'two',
|
|
18
|
+
'three',
|
|
19
|
+
'four',
|
|
20
|
+
'five',
|
|
21
|
+
'six',
|
|
22
|
+
'seven',
|
|
23
|
+
'eight',
|
|
24
|
+
'nine',
|
|
25
|
+
'ten',
|
|
26
|
+
'eleven',
|
|
27
|
+
'twelve',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** Local calendar day as YYYY-MM-DD — the key the daily log is stored under. */
|
|
31
|
+
export function dayKey(d: Date): string {
|
|
32
|
+
return (
|
|
33
|
+
d.getFullYear() +
|
|
34
|
+
'-' +
|
|
35
|
+
String(d.getMonth() + 1).padStart(2, '0') +
|
|
36
|
+
'-' +
|
|
37
|
+
String(d.getDate()).padStart(2, '0')
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function todayKey(): string {
|
|
42
|
+
return dayKey(new Date());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** "3m 07s"-style durations used on the summary screens. */
|
|
46
|
+
export function minsSecs(totalSecs: number): string {
|
|
47
|
+
const mins = Math.floor((totalSecs || 0) / 60);
|
|
48
|
+
const secs = (totalSecs || 0) % 60;
|
|
49
|
+
return (mins ? mins + 'm ' : '') + secs + 's';
|
|
50
|
+
}
|