@sudobility/music_drawing 0.0.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 (57) hide show
  1. package/dist/canvas-renderer.d.ts +180 -0
  2. package/dist/canvas-renderer.d.ts.map +1 -0
  3. package/dist/canvas-renderer.js +831 -0
  4. package/dist/canvas-renderer.js.map +1 -0
  5. package/dist/convert.d.ts +122 -0
  6. package/dist/convert.d.ts.map +1 -0
  7. package/dist/convert.js +426 -0
  8. package/dist/convert.js.map +1 -0
  9. package/dist/display-timing.d.ts +68 -0
  10. package/dist/display-timing.d.ts.map +1 -0
  11. package/dist/display-timing.js +192 -0
  12. package/dist/display-timing.js.map +1 -0
  13. package/dist/icon-canvas.d.ts +12 -0
  14. package/dist/icon-canvas.d.ts.map +1 -0
  15. package/dist/icon-canvas.js +62 -0
  16. package/dist/icon-canvas.js.map +1 -0
  17. package/dist/index.d.ts +30 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +30 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/layout.d.ts +159 -0
  22. package/dist/layout.d.ts.map +1 -0
  23. package/dist/layout.js +362 -0
  24. package/dist/layout.js.map +1 -0
  25. package/dist/measure-content.d.ts +114 -0
  26. package/dist/measure-content.d.ts.map +1 -0
  27. package/dist/measure-content.js +553 -0
  28. package/dist/measure-content.js.map +1 -0
  29. package/dist/note-color.d.ts +48 -0
  30. package/dist/note-color.d.ts.map +1 -0
  31. package/dist/note-color.js +81 -0
  32. package/dist/note-color.js.map +1 -0
  33. package/dist/pagination.d.ts +57 -0
  34. package/dist/pagination.d.ts.map +1 -0
  35. package/dist/pagination.js +152 -0
  36. package/dist/pagination.js.map +1 -0
  37. package/dist/percussion.d.ts +47 -0
  38. package/dist/percussion.d.ts.map +1 -0
  39. package/dist/percussion.js +172 -0
  40. package/dist/percussion.js.map +1 -0
  41. package/dist/playhead.d.ts +37 -0
  42. package/dist/playhead.d.ts.map +1 -0
  43. package/dist/playhead.js +89 -0
  44. package/dist/playhead.js.map +1 -0
  45. package/dist/test/canvas-stub.d.ts +21 -0
  46. package/dist/test/canvas-stub.d.ts.map +1 -0
  47. package/dist/test/canvas-stub.js +51 -0
  48. package/dist/test/canvas-stub.js.map +1 -0
  49. package/dist/test/fixtures.d.ts +47 -0
  50. package/dist/test/fixtures.d.ts.map +1 -0
  51. package/dist/test/fixtures.js +433 -0
  52. package/dist/test/fixtures.js.map +1 -0
  53. package/dist/types.d.ts +74 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +9 -0
  56. package/dist/types.js.map +1 -0
  57. package/package.json +62 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Highest precedence first. `playing` wins so playback stays followable even
3
+ * over a selection; `regenerated` beats `selected` so a just-regenerated
4
+ * passage stays visible while it is still selected.
5
+ *
6
+ * Pressing play clears the selection, so `playing` and `selected` do not
7
+ * normally coexist — this ordering only resolves the case where the user
8
+ * selects notes while playback is already running.
9
+ */
10
+ const PRECEDENCE = [
11
+ 'playing',
12
+ 'regenerated',
13
+ 'selected',
14
+ 'normal',
15
+ ];
16
+ /**
17
+ * The role to draw a VexFlow note with, given every domain event id it
18
+ * represents (>1 for a chord, or for one segment of a duration-decomposed
19
+ * long note). Ids absent from `noteColors` count as `normal`.
20
+ */
21
+ export function resolveNoteColorRole(eventIds, noteColors) {
22
+ if (!noteColors || noteColors.size === 0)
23
+ return 'normal';
24
+ let best = PRECEDENCE.length - 1;
25
+ for (const eventId of eventIds) {
26
+ const role = noteColors.get(eventId);
27
+ if (!role)
28
+ continue;
29
+ const rank = PRECEDENCE.indexOf(role);
30
+ if (rank !== -1 && rank < best)
31
+ best = rank;
32
+ }
33
+ return PRECEDENCE[best];
34
+ }
35
+ /**
36
+ * The non-color half of a note's state cue (spec §27: "do not rely on color
37
+ * alone"). The old highlight overlay carried this with solid/dashed/dotted
38
+ * stroke patterns; those went away with the rectangles, so the weight of the
39
+ * glyph carries it now — a thicker `lineWidth` on stems, flags and beams.
40
+ *
41
+ * Deliberately NOT a shadow. An earlier version added `shadowBlur` so the cue
42
+ * would reach a notehead too (a filled glyph, which no stroke width can
43
+ * thicken). Canvas shadows force the rasterizer down a separate blur pass per
44
+ * draw, and this is applied to precisely the notes that change most often —
45
+ * every note-on and note-off during playback, on the thread Tone.js schedules
46
+ * from. Playback hesitated. The cue is worth less than smooth audio.
47
+ *
48
+ * The cost of that trade: a *stemless* whole note now has color as its only
49
+ * cue. Every other note keeps the stroke-weight channel, and the selection
50
+ * summary in the status bar and screen-reader text still names the state.
51
+ *
52
+ * One emphasis level for every non-normal state rather than three: the
53
+ * perceptually important distinction is "is this note affected" versus "is it
54
+ * not". `selected` and `regenerated` are mutually exclusive anyway
55
+ * (`regenerated` is a property of the whole selection), and `playing` is
56
+ * transient and accompanied by the moving caret.
57
+ */
58
+ export function noteEmphasisFor(role) {
59
+ return { lineWidth: role === 'normal' ? 1 : 2.5 };
60
+ }
61
+ /**
62
+ * The theme color a role draws in.
63
+ *
64
+ * Only `normal` varies by track. A selected, regenerated or playing note keeps
65
+ * its state color wherever it is: those states are the reason to look at the
66
+ * note, and cmd-shift-click selects across every track, so dimming a selection
67
+ * because it is not on the active track would hide what the user just did.
68
+ */
69
+ export function noteColorFor(role, theme, onActiveTrack = true) {
70
+ switch (role) {
71
+ case 'playing':
72
+ return theme.notePlaying;
73
+ case 'regenerated':
74
+ return theme.noteRegenerated;
75
+ case 'selected':
76
+ return theme.noteSelected;
77
+ case 'normal':
78
+ return onActiveTrack ? theme.noteNormal : theme.noteInactive;
79
+ }
80
+ }
81
+ //# sourceMappingURL=note-color.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"note-color.js","sourceRoot":"","sources":["../src/note-color.ts"],"names":[],"mappings":"AAOA;;;;;;;;GAQG;AACH,MAAM,UAAU,GAA6B;IAC3C,SAAS;IACT,aAAa;IACb,UAAU;IACV,QAAQ;CACT,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,QAA2B,EAC3B,UAA0D;IAE1D,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC1D,IAAI,IAAI,GAAG,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC;IACjC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,IAAI,KAAK,CAAC,CAAC,IAAI,IAAI,GAAG,IAAI;YAAE,IAAI,GAAG,IAAI,CAAC;IAC9C,CAAC;IACD,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,eAAe,CAAC,IAAmB;IACjD,OAAO,EAAE,SAAS,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;AACpD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAmB,EACnB,KAAkB,EAClB,aAAa,GAAG,IAAI;IAEpB,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,SAAS;YACZ,OAAO,KAAK,CAAC,WAAW,CAAC;QAC3B,KAAK,aAAa;YAChB,OAAO,KAAK,CAAC,eAAe,CAAC;QAC/B,KAAK,UAAU;YACb,OAAO,KAAK,CAAC,YAAY,CAAC;QAC5B,KAAK,QAAQ;YACX,OAAO,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC;IACjE,CAAC;AACH,CAAC"}
@@ -0,0 +1,57 @@
1
+ import type { Track } from '@sudobility/music_types';
2
+ import type { LayoutPlan } from './layout.js';
3
+ export type PaperSize = 'a4' | 'letter' | 'legal';
4
+ export type PaperOrientation = 'portrait' | 'landscape';
5
+ /** Portrait dimensions in millimetres; landscape swaps them. */
6
+ export declare const PAPER_DIMENSIONS_MM: Record<PaperSize, {
7
+ width: number;
8
+ height: number;
9
+ }>;
10
+ /**
11
+ * The margin the printed page reserves on every side.
12
+ *
13
+ * The single source: the `@page` rule is emitted from this, and the height
14
+ * below is computed from it. Two numbers here means pagination that silently
15
+ * disagrees with what the printer does.
16
+ */
17
+ export declare const PAGE_MARGIN_MM = 12;
18
+ /**
19
+ * How much height `logicalWidth` units of layout buy on this paper.
20
+ *
21
+ * Paper only ever enters as a ratio: the layout is computed at a fixed logical
22
+ * width and each page is displayed at the paper's printable width, so the
23
+ * scale is uniform and millimetres cancel.
24
+ */
25
+ export declare function usablePageHeight(paper: PaperSize, orientation: PaperOrientation, logicalWidth: number, marginMm?: number): number;
26
+ /** The systems printed on one page, by index into `LayoutPlan.systems`. */
27
+ export type PrintPage = {
28
+ systemIndices: number[];
29
+ };
30
+ /**
31
+ * How many systems earlier than the greedy break a page may end.
32
+ *
33
+ * Two. Every system pulled back makes the part longer, and a turn bought three
34
+ * systems early costs more paper than it saves the player.
35
+ */
36
+ export declare const MAX_PULL_BACK = 2;
37
+ /**
38
+ * Bars `track`'s player has free across a turn taken after system
39
+ * `lastSystemIndex`.
40
+ *
41
+ * They may begin turning once their last note on the page has finished and
42
+ * must be reading again by their first on the next, so both sides count.
43
+ * Zero after the last system: there is no turn there.
44
+ */
45
+ export declare function turnFreeBars(plan: LayoutPlan, track: Track, lastSystemIndex: number): number;
46
+ /**
47
+ * Which systems go on which page.
48
+ *
49
+ * Greedy: take systems in order while they fit. A system taller than the page
50
+ * still gets its own page — it overflows, which is better than dropping it and
51
+ * better than never advancing.
52
+ *
53
+ * With `turnTrack`, each break is then pulled back up to `MAX_PULL_BACK`
54
+ * systems if that buys its player a better page turn.
55
+ */
56
+ export declare function paginate(plan: LayoutPlan, pageHeight: number, turnTrack?: Track): PrintPage[];
57
+ //# sourceMappingURL=pagination.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAW,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,aAAa,CAAC;AAE5D,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,QAAQ,GAAG,OAAO,CAAC;AAClD,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,WAAW,CAAC;AAExD,gEAAgE;AAChE,eAAO,MAAM,mBAAmB,EAAE,MAAM,CACtC,SAAS,EACT;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAKlC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,gBAAgB,EAC7B,YAAY,EAAE,MAAM,EACpB,QAAQ,GAAE,MAAuB,GAChC,MAAM,CAOR;AAED,2EAA2E;AAC3E,MAAM,MAAM,SAAS,GAAG;IAAE,aAAa,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAOpD;;;;;GAKG;AACH,eAAO,MAAM,aAAa,IAAI,CAAC;AA6B/B;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,KAAK,EACZ,eAAe,EAAE,MAAM,GACtB,MAAM,CAQR;AA8BD;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CACtB,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,KAAK,GAChB,SAAS,EAAE,CA+Bb"}
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Which systems go on which page, and where a page turn should fall.
3
+ *
4
+ * Pure over a `LayoutPlan` — no DOM, no canvas, no React — so the decisions
5
+ * about paper and page turns are testable without printing anything.
6
+ *
7
+ * Feature 1 left pagination to the browser: every system was a block with
8
+ * `break-inside: avoid`, and the browser fitted as many as the paper allowed.
9
+ * That has no opinion about *where* the break falls, and a turn in the middle
10
+ * of a phrase costs a real player a hand.
11
+ */
12
+ import { isSilentMeasure } from '@sudobility/music_types';
13
+ /** Portrait dimensions in millimetres; landscape swaps them. */
14
+ export const PAPER_DIMENSIONS_MM = {
15
+ a4: { width: 210, height: 297 },
16
+ letter: { width: 215.9, height: 279.4 }, // 8.5in x 11in
17
+ legal: { width: 215.9, height: 355.6 }, // 8.5in x 14in
18
+ };
19
+ /**
20
+ * The margin the printed page reserves on every side.
21
+ *
22
+ * The single source: the `@page` rule is emitted from this, and the height
23
+ * below is computed from it. Two numbers here means pagination that silently
24
+ * disagrees with what the printer does.
25
+ */
26
+ export const PAGE_MARGIN_MM = 12;
27
+ /**
28
+ * How much height `logicalWidth` units of layout buy on this paper.
29
+ *
30
+ * Paper only ever enters as a ratio: the layout is computed at a fixed logical
31
+ * width and each page is displayed at the paper's printable width, so the
32
+ * scale is uniform and millimetres cancel.
33
+ */
34
+ export function usablePageHeight(paper, orientation, logicalWidth, marginMm = PAGE_MARGIN_MM) {
35
+ const { width, height } = PAPER_DIMENSIONS_MM[paper];
36
+ const shortSide = orientation === 'portrait' ? width : height;
37
+ const longSide = orientation === 'portrait' ? height : width;
38
+ return ((logicalWidth * (longSide - 2 * marginMm)) / (shortSide - 2 * marginMm));
39
+ }
40
+ /** A system's full printed height, measure-number band included. */
41
+ function systemHeight(system) {
42
+ return system.yBottom - system.gutterTop;
43
+ }
44
+ /**
45
+ * How many systems earlier than the greedy break a page may end.
46
+ *
47
+ * Two. Every system pulled back makes the part longer, and a turn bought three
48
+ * systems early costs more paper than it saves the player.
49
+ */
50
+ export const MAX_PULL_BACK = 2;
51
+ /** How many bars `measure` stands for — a multi-measure rest stands for its count. */
52
+ function barSpan(measure) {
53
+ return measure.multiMeasureRestCount ?? 1;
54
+ }
55
+ /** Silent bars at the end of `indices`. */
56
+ function trailingFreeBars(track, indices) {
57
+ let bars = 0;
58
+ for (let i = indices.length - 1; i >= 0; i -= 1) {
59
+ const measure = track.measures[indices[i]];
60
+ if (!measure || !isSilentMeasure(measure))
61
+ break;
62
+ bars += barSpan(measure);
63
+ }
64
+ return bars;
65
+ }
66
+ /** Silent bars at the start of `indices`. */
67
+ function leadingFreeBars(track, indices) {
68
+ let bars = 0;
69
+ for (const index of indices) {
70
+ const measure = track.measures[index];
71
+ if (!measure || !isSilentMeasure(measure))
72
+ break;
73
+ bars += barSpan(measure);
74
+ }
75
+ return bars;
76
+ }
77
+ /**
78
+ * Bars `track`'s player has free across a turn taken after system
79
+ * `lastSystemIndex`.
80
+ *
81
+ * They may begin turning once their last note on the page has finished and
82
+ * must be reading again by their first on the next, so both sides count.
83
+ * Zero after the last system: there is no turn there.
84
+ */
85
+ export function turnFreeBars(plan, track, lastSystemIndex) {
86
+ const last = plan.systems[lastSystemIndex];
87
+ const next = plan.systems[lastSystemIndex + 1];
88
+ if (!last || !next)
89
+ return 0;
90
+ return (trailingFreeBars(track, last.measureIndices) +
91
+ leadingFreeBars(track, next.measureIndices));
92
+ }
93
+ /**
94
+ * The end index (exclusive) that buys the best turn, at most `MAX_PULL_BACK`
95
+ * systems back from `greedyEnd` and never emptying the page.
96
+ *
97
+ * Searched downward with a strict improvement test, so a tie keeps the fullest
98
+ * page — pulling back with nothing to show for it is pure loss.
99
+ */
100
+ function bestTurn(plan, track, start, greedyEnd) {
101
+ let best = greedyEnd;
102
+ let bestScore = turnFreeBars(plan, track, greedyEnd - 1);
103
+ const earliest = Math.max(start + 1, greedyEnd - MAX_PULL_BACK);
104
+ for (let end = greedyEnd - 1; end >= earliest; end -= 1) {
105
+ const score = turnFreeBars(plan, track, end - 1);
106
+ if (score > bestScore) {
107
+ best = end;
108
+ bestScore = score;
109
+ }
110
+ }
111
+ return best;
112
+ }
113
+ /**
114
+ * Which systems go on which page.
115
+ *
116
+ * Greedy: take systems in order while they fit. A system taller than the page
117
+ * still gets its own page — it overflows, which is better than dropping it and
118
+ * better than never advancing.
119
+ *
120
+ * With `turnTrack`, each break is then pulled back up to `MAX_PULL_BACK`
121
+ * systems if that buys its player a better page turn.
122
+ */
123
+ export function paginate(plan, pageHeight, turnTrack) {
124
+ const pages = [];
125
+ let start = 0;
126
+ while (start < plan.systems.length) {
127
+ let end = start;
128
+ let used = 0;
129
+ while (end < plan.systems.length) {
130
+ const height = systemHeight(plan.systems[end]);
131
+ // `end > start` is what guarantees progress: the first system on a page
132
+ // always goes on it, however tall it is.
133
+ if (end > start && used + height > pageHeight)
134
+ break;
135
+ used += height;
136
+ end += 1;
137
+ }
138
+ // Only worth doing when there *is* a turn: the last page ends the piece.
139
+ // A whole-score print passes no track — "the player rests" means nothing
140
+ // when a dozen staves share the page, and a conductor turns at will.
141
+ if (turnTrack && end < plan.systems.length) {
142
+ end = bestTurn(plan, turnTrack, start, end);
143
+ }
144
+ const systemIndices = [];
145
+ for (let i = start; i < end; i += 1)
146
+ systemIndices.push(i);
147
+ pages.push({ systemIndices });
148
+ start = end;
149
+ }
150
+ return pages;
151
+ }
152
+ //# sourceMappingURL=pagination.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.js","sourceRoot":"","sources":["../src/pagination.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Where a drum sits on the staff, and what its notehead looks like.
3
+ *
4
+ * A percussion track's "pitch" is not a pitch — it is which drum was struck.
5
+ * General MIDI puts the kick at 35/36 and the snare at 38, which as pitches
6
+ * are B1 and D2: drawn literally they land six or seven ledger lines below a
7
+ * treble staff, with stems long enough to reach into the next track's row.
8
+ * That is what a drum part looked like here.
9
+ *
10
+ * Percussion notation answers a different question — not "how high is it" but
11
+ * "which instrument is it" — so each drum gets a fixed line or space and, for
12
+ * anything struck with a stick or shaken rather than hit like a drum, a cross
13
+ * notehead. The positions below are the common drum-set convention: kick in
14
+ * the bottom space, snare in the third space, hi-hat above the top line, toms
15
+ * descending between them, cymbals crossed at the top.
16
+ *
17
+ * The keys are VexFlow's `note/octave/notehead` form, and the octaves in them
18
+ * are staff positions read as if in treble clef, not sounding pitches. Nothing
19
+ * outside this file should read them as pitch.
20
+ */
21
+ /** Whether `midi` is struck with a foot rather than a hand. */
22
+ export declare function isFootDrum(midi: number): boolean;
23
+ /**
24
+ * The stem base offsets that join a stem to `keys`, or `null` when they need
25
+ * none.
26
+ *
27
+ * Only when *every* head in the group is a cross: the stem attaches to one
28
+ * outer head, and in a mixed chord that may be an ordinary one, which already
29
+ * meets its stem correctly.
30
+ */
31
+ export declare function crossHeadStemOffsets(keys: string[]): {
32
+ stem_up_y_base_offset: number;
33
+ stem_down_y_base_offset: number;
34
+ } | null;
35
+ /** The VexFlow key for General MIDI percussion note `midi`. */
36
+ export declare function percussionVexKey(midi: number): string;
37
+ /**
38
+ * Whether `midi` has a staff position of its own.
39
+ *
40
+ * Exposed because `UNMAPPED` is `b/4`, which is also a real position — Low Tom
41
+ * sits there — so the return of `percussionVexKey` cannot be used to tell a
42
+ * drum this table knows from one it does not. `gm-percussion.ts` pins its own
43
+ * coverage against this, since a drum in one and not the other is either drawn
44
+ * without a name or named without a place to draw it.
45
+ */
46
+ export declare function hasPercussionMapping(midi: number): boolean;
47
+ //# sourceMappingURL=percussion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"percussion.d.ts","sourceRoot":"","sources":["../src/percussion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAoGH,+DAA+D;AAC/D,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAyBD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EAAE,GACb;IAAE,qBAAqB,EAAE,MAAM,CAAC;IAAC,uBAAuB,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAM3E;AAED,+DAA+D;AAC/D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE1D"}
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Where a drum sits on the staff, and what its notehead looks like.
3
+ *
4
+ * A percussion track's "pitch" is not a pitch — it is which drum was struck.
5
+ * General MIDI puts the kick at 35/36 and the snare at 38, which as pitches
6
+ * are B1 and D2: drawn literally they land six or seven ledger lines below a
7
+ * treble staff, with stems long enough to reach into the next track's row.
8
+ * That is what a drum part looked like here.
9
+ *
10
+ * Percussion notation answers a different question — not "how high is it" but
11
+ * "which instrument is it" — so each drum gets a fixed line or space and, for
12
+ * anything struck with a stick or shaken rather than hit like a drum, a cross
13
+ * notehead. The positions below are the common drum-set convention: kick in
14
+ * the bottom space, snare in the third space, hi-hat above the top line, toms
15
+ * descending between them, cymbals crossed at the top.
16
+ *
17
+ * The keys are VexFlow's `note/octave/notehead` form, and the octaves in them
18
+ * are staff positions read as if in treble clef, not sounding pitches. Nothing
19
+ * outside this file should read them as pitch.
20
+ */
21
+ /**
22
+ * Cross (`noteheadXBlack`): sticks, cymbals, shakers — anything that is not a
23
+ * drumhead.
24
+ */
25
+ const X = 'x';
26
+ /**
27
+ * Circled cross (`noteheadCircleX`): the standard "let it ring" mark, which is
28
+ * how an open hi-hat is told from a closed one.
29
+ *
30
+ * Verified against VexFlow's own codes rather than assumed: `x` and `x2` are
31
+ * both `noteheadXBlack`, `x3` is `noteheadCircleX`, and `ci` is
32
+ * `noteheadCircledBlack` — a filled head inside a circle, which means nothing
33
+ * here and is what open hi-hat was wrongly drawn with.
34
+ */
35
+ const CIRCLE_X = 'x3';
36
+ /** Diamond: bells and cups. */
37
+ const DIAMOND = 'd';
38
+ /** Triangle: the cowbell's usual mark. */
39
+ const TRIANGLE = 'tu';
40
+ /**
41
+ * General MIDI percussion (35-81) to staff position.
42
+ *
43
+ * Numbers outside this range are not General MIDI percussion; `percussionVexKey`
44
+ * keeps them on the middle line rather than inventing a position, so an
45
+ * unexpected note is visible and obviously unplaced instead of flying off the
46
+ * staff.
47
+ */
48
+ const DRUM_MAP = {
49
+ 35: 'f/4', // Acoustic Bass Drum
50
+ 36: 'f/4', // Bass Drum 1
51
+ 37: `c/5/${X}`, // Side Stick
52
+ 38: 'c/5', // Acoustic Snare
53
+ 39: `c/5/${X}`, // Hand Clap
54
+ 40: 'c/5', // Electric Snare
55
+ 41: 'g/4', // Low Floor Tom
56
+ 42: `g/5/${X}`, // Closed Hi-Hat
57
+ 43: 'a/4', // High Floor Tom
58
+ 44: `d/4/${X}`, // Pedal Hi-Hat
59
+ 45: 'b/4', // Low Tom
60
+ 46: `g/5/${CIRCLE_X}`, // Open Hi-Hat
61
+ 47: 'd/5', // Low-Mid Tom
62
+ 48: 'e/5', // Hi-Mid Tom
63
+ 49: `a/5/${X}`, // Crash Cymbal 1
64
+ 50: 'f/5', // High Tom
65
+ 51: `f/5/${X}`, // Ride Cymbal 1
66
+ 52: `b/5/${CIRCLE_X}`, // Chinese Cymbal
67
+ 53: `f/5/${DIAMOND}`, // Ride Bell
68
+ 54: `d/5/${X}`, // Tambourine
69
+ 55: `b/5/${X}`, // Splash Cymbal
70
+ 56: `e/5/${TRIANGLE}`, // Cowbell
71
+ 57: `a/5/${X}`, // Crash Cymbal 2
72
+ 58: `b/4/${X}`, // Vibraslap
73
+ 59: `f/5/${X}`, // Ride Cymbal 2
74
+ // 60-81 are hand and Latin percussion rather than kit pieces, so there is no
75
+ // drum-set convention to follow. They are spread over the staff by family,
76
+ // pitched pairs keeping high above low, so a part stays readable.
77
+ 60: 'e/5', // Hi Bongo
78
+ 61: 'c/5', // Low Bongo
79
+ 62: 'd/5', // Mute Hi Conga
80
+ 63: 'd/5', // Open Hi Conga
81
+ 64: 'b/4', // Low Conga
82
+ 65: 'e/5', // High Timbale
83
+ 66: 'c/5', // Low Timbale
84
+ 67: `f/5/${X}`, // High Agogo
85
+ 68: `d/5/${X}`, // Low Agogo
86
+ 69: `g/5/${X}`, // Cabasa
87
+ 70: `g/5/${X}`, // Maracas
88
+ 71: `a/5/${X}`, // Short Whistle
89
+ 72: `a/5/${X}`, // Long Whistle
90
+ 73: `f/5/${X}`, // Short Guiro
91
+ 74: `f/5/${X}`, // Long Guiro
92
+ 75: `e/5/${X}`, // Claves
93
+ 76: `e/5/${X}`, // Hi Wood Block
94
+ 77: `c/5/${X}`, // Low Wood Block
95
+ 78: `d/5/${X}`, // Mute Cuica
96
+ 79: `d/5/${X}`, // Open Cuica
97
+ 80: `a/5/${X}`, // Mute Triangle
98
+ 81: `a/5/${CIRCLE_X}`, // Open Triangle
99
+ };
100
+ /**
101
+ * The middle line — where anything unrecognised goes, so it stays on the staff.
102
+ *
103
+ * The staff runs from line 1 (bottom, E4) to line 5 (top, F5) in VexFlow's
104
+ * numbering; every mapping above stays within one ledger of that, and the
105
+ * pedal hi-hat is deliberately just below it, as convention has it.
106
+ */
107
+ const UNMAPPED = 'b/4';
108
+ /**
109
+ * The kit pieces played with the feet: both bass drums and the hi-hat pedal.
110
+ *
111
+ * They are stemmed downward against the hands, which is what lets a drum staff
112
+ * carry a whole kit on five lines and still be read at a glance.
113
+ */
114
+ const FOOT_DRUMS = new Set([35, 36, 44]);
115
+ /** Whether `midi` is struck with a foot rather than a hand. */
116
+ export function isFootDrum(midi) {
117
+ return FOOT_DRUMS.has(midi);
118
+ }
119
+ /**
120
+ * How far a cross notehead's stem must reach into the glyph to join it.
121
+ *
122
+ * A stem meets a notehead at its right (or left) edge, halfway up. On an oval
123
+ * that is solid ink; on a cross it is the hollow middle, and the only ink at
124
+ * that edge is the two arm tips at the corners. VexFlow's default stops the
125
+ * stem four units short of even the halfway point, so it grazed the upper tip
126
+ * at a single point and read as a separate mark sitting above the note.
127
+ *
128
+ * Half a notehead — five units at the default glyph scale — runs the stem the
129
+ * full height of the glyph, bridging both arm tips, which is what reads as
130
+ * joined. Measured from the rendered SVG in both directions: +5 lands an
131
+ * up-stem exactly on the head's bottom edge and -5 lands a down-stem exactly
132
+ * on its top edge, while -9 and beyond overshoot past the glyph.
133
+ */
134
+ const CROSS_STEM_OVERLAP = 5;
135
+ /** Whether `key` (VexFlow `note/octave/head` form) draws as a cross. */
136
+ function isCrossHead(key) {
137
+ const head = key.split('/')[2];
138
+ return head === X || head === CIRCLE_X;
139
+ }
140
+ /**
141
+ * The stem base offsets that join a stem to `keys`, or `null` when they need
142
+ * none.
143
+ *
144
+ * Only when *every* head in the group is a cross: the stem attaches to one
145
+ * outer head, and in a mixed chord that may be an ordinary one, which already
146
+ * meets its stem correctly.
147
+ */
148
+ export function crossHeadStemOffsets(keys) {
149
+ if (keys.length === 0 || !keys.every(isCrossHead))
150
+ return null;
151
+ return {
152
+ stem_up_y_base_offset: CROSS_STEM_OVERLAP,
153
+ stem_down_y_base_offset: -CROSS_STEM_OVERLAP,
154
+ };
155
+ }
156
+ /** The VexFlow key for General MIDI percussion note `midi`. */
157
+ export function percussionVexKey(midi) {
158
+ return DRUM_MAP[midi] ?? UNMAPPED;
159
+ }
160
+ /**
161
+ * Whether `midi` has a staff position of its own.
162
+ *
163
+ * Exposed because `UNMAPPED` is `b/4`, which is also a real position — Low Tom
164
+ * sits there — so the return of `percussionVexKey` cannot be used to tell a
165
+ * drum this table knows from one it does not. `gm-percussion.ts` pins its own
166
+ * coverage against this, since a drum in one and not the other is either drawn
167
+ * without a name or named without a place to draw it.
168
+ */
169
+ export function hasPercussionMapping(midi) {
170
+ return midi in DRUM_MAP;
171
+ }
172
+ //# sourceMappingURL=percussion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"percussion.js","sourceRoot":"","sources":["../src/percussion.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Playhead caret geometry: maps a playback tick to a vertical-line position
3
+ * on the notation canvas, and a canvas point back to a tick (click-to-seek).
4
+ * Pure functions over `LayoutPlan` (logical/unscaled units — callers
5
+ * multiply by zoom). Moved here from music_app's score-editor feature and
6
+ * re-based on the binary-search lookups so every call is O(log n) — part of
7
+ * the canvas renderer's "no O(score) interaction work" rule.
8
+ *
9
+ * The tick <-> x mapping interpolates linearly across each measure's stave
10
+ * box. VexFlow doesn't space notes perfectly linearly (and first-in-system
11
+ * measures spend their left edge on clef/key/time), so the caret is a close
12
+ * approximation rather than glyph-exact — the same trade every DAW-style
13
+ * ruler makes.
14
+ */
15
+ import type { Score } from '@sudobility/music_types';
16
+ import type { LayoutPlan } from './layout.js';
17
+ export type CaretPosition = {
18
+ /** Logical x of the caret line. */
19
+ x: number;
20
+ /** Logical top/bottom of the system the tick falls in (the caret spans the whole system, all tracks). */
21
+ yTop: number;
22
+ yBottom: number;
23
+ };
24
+ /**
25
+ * Where the caret for `tick` sits on the canvas, or `null` when there is
26
+ * nothing to draw against (empty score / no layout). Ticks past the end of
27
+ * the score clamp to the final measure's right edge.
28
+ */
29
+ export declare function caretPositionForTick(plan: LayoutPlan, score: Score, tick: number): CaretPosition | null;
30
+ /**
31
+ * The tick a canvas click at logical `(x, y)` should seek to, or `null`
32
+ * when the point is in dead space between/outside systems. Horizontal
33
+ * positions left/right of a system's measures clamp to that system's
34
+ * first/last measure, so clicking the clef area seeks to the system start.
35
+ */
36
+ export declare function tickForPoint(plan: LayoutPlan, score: Score, x: number, y: number): number | null;
37
+ //# sourceMappingURL=playhead.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playhead.d.ts","sourceRoot":"","sources":["../src/playhead.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAErD,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,aAAa,CAAC;AAE5D,MAAM,MAAM,aAAa,GAAG;IAC1B,mCAAmC;IACnC,CAAC,EAAE,MAAM,CAAC;IACV,yGAAyG;IACzG,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AA2CF;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,MAAM,GACX,aAAa,GAAG,IAAI,CAsBtB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,KAAK,EACZ,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,GACR,MAAM,GAAG,IAAI,CAiBf"}