@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,68 @@
1
+ import type { MusicalEvent } from '@sudobility/music_types';
2
+ /** Ticks per display grid step. At the usual 480 ppq this is 60. */
3
+ export declare function displayGridTicks(ppq: number): number;
4
+ /**
5
+ * One drawn tickable: the events that sound at this point, and how long it
6
+ * occupies the bar. A chord is several events in one group.
7
+ */
8
+ export type DisplayGroup = {
9
+ /**
10
+ * The events sounding at this point. **Empty means a spacer**: time that has
11
+ * to be accounted for so the voice still sums to the bar, but that draws
12
+ * nothing. `buildVoiceContent` renders those as VexFlow `GhostNote`s.
13
+ */
14
+ events: MusicalEvent[];
15
+ /** Always a whole number of grid steps; the groups sum to the measure. */
16
+ durationTicks: number;
17
+ };
18
+ /**
19
+ * Where this voice's notes begin, snapped to the display grid.
20
+ *
21
+ * Exported for layout: the number of distinct onsets across a measure's tracks
22
+ * is the number of tick contexts VexFlow will build, and therefore what its
23
+ * minimum width is proportional to. Counting raw events instead over-allocated
24
+ * badly — a drum bar of 37 recorded hits draws as 16 tickables once
25
+ * near-simultaneous strikes are chorded, and it was being given width for 37.
26
+ */
27
+ export declare function snappedOnsetTicks(events: MusicalEvent[], measureStartTick: number, measureDurationTicks: number, ppq: number): number[];
28
+ /**
29
+ * Groups `events` (one voice of one measure, in ascending start order) into
30
+ * drawable tickables whose durations sum to exactly `measureDurationTicks`.
31
+ *
32
+ * `measureStartTick` is subtracted first, so callers pass absolute domain
33
+ * ticks and this works in measure-relative space throughout.
34
+ *
35
+ * Each group runs until the next one begins, which is what a single melodic
36
+ * line wants: the notes join up and the bar is accounted for with no spacers.
37
+ *
38
+ * Events that snap to the same grid step become one group — which is how a
39
+ * rolled chord recorded three ticks apart is drawn as the chord it is, rather
40
+ * than as three consecutive notes that between them overrun the bar.
41
+ */
42
+ export declare function displayGroups(events: MusicalEvent[], measureStartTick: number, measureDurationTicks: number, ppq: number): DisplayGroup[];
43
+ /**
44
+ * The same onsets, laid out for a drum staff: each hit runs to the next one,
45
+ * but never long enough for its notehead to turn hollow.
46
+ *
47
+ * A kit part is not a melodic line, and a drum's recorded length is an
48
+ * artifact — a struck cymbal rings however long the MIDI note says. What
49
+ * matters is the rhythmic slot, so a hit runs to the next hit and the leftover
50
+ * becomes spacers.
51
+ *
52
+ * The cap is what makes it readable. Stretching a kick to reach the next kick
53
+ * two beats later would draw it as a half note, and half notes are hollow: a
54
+ * kick on beats one and three would come out as two open noteheads, which
55
+ * reads as a different instruction entirely. A quarter is the longest value
56
+ * that is still filled and still obviously a hit.
57
+ *
58
+ * Using each hit's own recorded length instead does not work: drum hits are
59
+ * recorded a few ticks long, so every one became the shortest drawable note
60
+ * followed by a spacer — and spacers break beam groups, which turned a running
61
+ * hi-hat into a row of flagged thirty-seconds.
62
+ *
63
+ * Spacers rather than rests: with hands and feet on one staff, a rest drawn in
64
+ * the feet voice under a running hi-hat is clutter that says nothing a reader
65
+ * needs. Drum charts routinely leave it out.
66
+ */
67
+ export declare function drumDisplayGroups(events: MusicalEvent[], measureStartTick: number, measureDurationTicks: number, ppq: number): DisplayGroup[];
68
+ //# sourceMappingURL=display-timing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"display-timing.d.ts","sourceRoot":"","sources":["../src/display-timing.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAoB5D,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB;;;;OAIG;IACH,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,0EAA0E;IAC1E,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,YAAY,EAAE,EACtB,gBAAgB,EAAE,MAAM,EACxB,oBAAoB,EAAE,MAAM,EAC5B,GAAG,EAAE,MAAM,GACV,MAAM,EAAE,CAKV;AA+DD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,YAAY,EAAE,EACtB,gBAAgB,EAAE,MAAM,EACxB,oBAAoB,EAAE,MAAM,EAC5B,GAAG,EAAE,MAAM,GACV,YAAY,EAAE,CAahB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,YAAY,EAAE,EACtB,gBAAgB,EAAE,MAAM,EACxB,oBAAoB,EAAE,MAAM,EAC5B,GAAG,EAAE,MAAM,GACV,YAAY,EAAE,CA8BhB"}
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Turning a voice's recorded timing into timing that can actually be notated.
3
+ *
4
+ * A recorded performance does not land on note values. This score has onsets
5
+ * at ticks 0, 3, 233, 355, 1113 — and there is no such thing as a 233-tick
6
+ * note. `ticksToVexDuration` therefore rounds every duration to the nearest
7
+ * value it *can* draw, and those roundings accumulate: one voice of a 1920-tick
8
+ * bar came out notated as 3360 ticks, and five voices of another bar each
9
+ * drifted by a different amount.
10
+ *
11
+ * That is not merely cosmetic. VexFlow positions notes by accumulating each
12
+ * voice's notated durations, so voices that disagree about how long the bar is
13
+ * end up on different timelines: the same tick drew at x=407 on one stave and
14
+ * x=599 on another, and voices whose arithmetic happened to come out short were
15
+ * squeezed into the left third of the bar while the longest one filled it.
16
+ *
17
+ * The fix is to decide display timing *before* anything is rounded. Onsets snap
18
+ * to a grid, each group runs until the next one starts, and the last runs to the
19
+ * barline. Two properties follow, and they are the whole point:
20
+ *
21
+ * - every duration is a whole number of grid steps, which
22
+ * `decomposeDuration` represents exactly (verified across every multiple up
23
+ * to a full bar), so nothing rounds and nothing drifts; and
24
+ * - the durations sum to exactly the bar, for every voice, so every stave
25
+ * shares one timeline.
26
+ *
27
+ * A score that was already quantized passes through unchanged: its onsets are
28
+ * already on the grid, and a gap-filled voice's durations already are the gaps.
29
+ * So this only ever moves notes that could not have been drawn correctly.
30
+ */
31
+ import { isNoteEvent } from '@sudobility/music_types';
32
+ /**
33
+ * The display grid, as a divisor of the quarter note: 8 gives a 1/32 note.
34
+ *
35
+ * Not a free parameter. `decomposeDuration` must represent every multiple of
36
+ * the grid exactly or the drift this module exists to remove comes straight
37
+ * back, and 1/32 is the finest grid for which that holds — at 1/64 twenty-one
38
+ * of the sixty-four multiples in a 4/4 bar cannot be written, and finer grids
39
+ * are worse. Triplets are already approximated here (nothing in this adapter
40
+ * draws tuplets), so the grid costs them nothing they had.
41
+ */
42
+ const GRID_DIVISOR = 8;
43
+ /**
44
+ * The longest note value that still draws a filled notehead, in quarters.
45
+ * A half note is hollow, and a hollow drum hit reads as the wrong instruction.
46
+ */
47
+ const LONGEST_FILLED_QUARTERS = 1;
48
+ /** Ticks per display grid step. At the usual 480 ppq this is 60. */
49
+ export function displayGridTicks(ppq) {
50
+ return Math.max(1, Math.round(ppq / GRID_DIVISOR));
51
+ }
52
+ /**
53
+ * Where this voice's notes begin, snapped to the display grid.
54
+ *
55
+ * Exported for layout: the number of distinct onsets across a measure's tracks
56
+ * is the number of tick contexts VexFlow will build, and therefore what its
57
+ * minimum width is proportional to. Counting raw events instead over-allocated
58
+ * badly — a drum bar of 37 recorded hits draws as 16 tickables once
59
+ * near-simultaneous strikes are chorded, and it was being given width for 37.
60
+ */
61
+ export function snappedOnsetTicks(events, measureStartTick, measureDurationTicks, ppq) {
62
+ if (events.length === 0 || measureDurationTicks <= 0)
63
+ return [];
64
+ return snappedOnsets(events, measureStartTick, measureDurationTicks, ppq).map(o => o.start);
65
+ }
66
+ /**
67
+ * The events of one voice, grouped onto snapped onsets and sorted.
68
+ *
69
+ * Shared by both layouts below so they cannot disagree about where a note
70
+ * begins — only about how long it is drawn.
71
+ */
72
+ function snappedOnsets(events, measureStartTick, measureDurationTicks, ppq) {
73
+ const grid = displayGridTicks(ppq);
74
+ // An onset that rounds up to the barline is the next bar's note recorded a
75
+ // few ticks early, not an onset in this one. This score has such a note at
76
+ // tick 1915 of a 1920-tick bar. Giving it its own step would open a tick
77
+ // context one grid step before the barline, and VexFlow spends width on a
78
+ // context in proportion to it *being* one, not to how long it lasts: that
79
+ // sliver took a fifth of the bar, squeezing the sixteen real notes into 80%
80
+ // of the width and leaving what looked like a gap before the barline.
81
+ //
82
+ // So it joins the last real onset instead. Only onsets that round to the
83
+ // barline are touched — a genuine note a grid step before it rounds to its
84
+ // own step and keeps it.
85
+ const atBarline = [];
86
+ const byStart = new Map();
87
+ for (const event of events) {
88
+ const snapped = Math.max(0, Math.round((event.startTick - measureStartTick) / grid) * grid);
89
+ if (snapped >= measureDurationTicks) {
90
+ atBarline.push(event);
91
+ continue;
92
+ }
93
+ const at = byStart.get(snapped);
94
+ if (at)
95
+ at.push(event);
96
+ else
97
+ byStart.set(snapped, [event]);
98
+ }
99
+ for (const event of atBarline) {
100
+ const last = byStart.size > 0 ? Math.max(...byStart.keys()) : 0;
101
+ const at = byStart.get(last);
102
+ if (at)
103
+ at.push(event);
104
+ else
105
+ byStart.set(last, [event]);
106
+ }
107
+ return ([...byStart.keys()]
108
+ .sort((a, b) => a - b)
109
+ // A rest that snapped onto a note's step would draw a rest through a
110
+ // sounding note, so notes win their step and the rest simply vanishes —
111
+ // it was silence that turned out to be shorter than the grid.
112
+ // `?? []` rather than `!`: the keys come from this very map, so the
113
+ // lookup cannot miss — and an empty group is a harmless answer if it
114
+ // ever did, where an assertion would be a crash.
115
+ .map(start => ({ start, events: preferNotes(byStart.get(start) ?? []) })));
116
+ }
117
+ /**
118
+ * Groups `events` (one voice of one measure, in ascending start order) into
119
+ * drawable tickables whose durations sum to exactly `measureDurationTicks`.
120
+ *
121
+ * `measureStartTick` is subtracted first, so callers pass absolute domain
122
+ * ticks and this works in measure-relative space throughout.
123
+ *
124
+ * Each group runs until the next one begins, which is what a single melodic
125
+ * line wants: the notes join up and the bar is accounted for with no spacers.
126
+ *
127
+ * Events that snap to the same grid step become one group — which is how a
128
+ * rolled chord recorded three ticks apart is drawn as the chord it is, rather
129
+ * than as three consecutive notes that between them overrun the bar.
130
+ */
131
+ export function displayGroups(events, measureStartTick, measureDurationTicks, ppq) {
132
+ if (events.length === 0 || measureDurationTicks <= 0)
133
+ return [];
134
+ const onsets = snappedOnsets(events, measureStartTick, measureDurationTicks, ppq);
135
+ return onsets.map((onset, index) => ({
136
+ events: onset.events,
137
+ durationTicks: (onsets[index + 1]?.start ?? measureDurationTicks) - onset.start,
138
+ }));
139
+ }
140
+ /**
141
+ * The same onsets, laid out for a drum staff: each hit runs to the next one,
142
+ * but never long enough for its notehead to turn hollow.
143
+ *
144
+ * A kit part is not a melodic line, and a drum's recorded length is an
145
+ * artifact — a struck cymbal rings however long the MIDI note says. What
146
+ * matters is the rhythmic slot, so a hit runs to the next hit and the leftover
147
+ * becomes spacers.
148
+ *
149
+ * The cap is what makes it readable. Stretching a kick to reach the next kick
150
+ * two beats later would draw it as a half note, and half notes are hollow: a
151
+ * kick on beats one and three would come out as two open noteheads, which
152
+ * reads as a different instruction entirely. A quarter is the longest value
153
+ * that is still filled and still obviously a hit.
154
+ *
155
+ * Using each hit's own recorded length instead does not work: drum hits are
156
+ * recorded a few ticks long, so every one became the shortest drawable note
157
+ * followed by a spacer — and spacers break beam groups, which turned a running
158
+ * hi-hat into a row of flagged thirty-seconds.
159
+ *
160
+ * Spacers rather than rests: with hands and feet on one staff, a rest drawn in
161
+ * the feet voice under a running hi-hat is clutter that says nothing a reader
162
+ * needs. Drum charts routinely leave it out.
163
+ */
164
+ export function drumDisplayGroups(events, measureStartTick, measureDurationTicks, ppq) {
165
+ if (events.length === 0 || measureDurationTicks <= 0)
166
+ return [];
167
+ const onsets = snappedOnsets(events, measureStartTick, measureDurationTicks, ppq);
168
+ const groups = [];
169
+ // Time before the first hit still has to be accounted for, or the voice
170
+ // would be short and every stave would disagree about the bar's length.
171
+ if (onsets[0].start > 0)
172
+ groups.push({ events: [], durationTicks: onsets[0].start });
173
+ onsets.forEach((onset, index) => {
174
+ const until = onsets[index + 1]?.start ?? measureDurationTicks;
175
+ const available = until - onset.start;
176
+ // The cap is about noteheads, so it applies to hits only. A rest has no
177
+ // head to turn hollow and must span its whole silence: capped, a bar of
178
+ // silence drew a quarter rest with the remaining three beats blank.
179
+ const sounded = onset.events.some(isNoteEvent)
180
+ ? Math.min(available, LONGEST_FILLED_QUARTERS * ppq)
181
+ : available;
182
+ groups.push({ events: onset.events, durationTicks: sounded });
183
+ if (available > sounded)
184
+ groups.push({ events: [], durationTicks: available - sounded });
185
+ });
186
+ return groups;
187
+ }
188
+ function preferNotes(events) {
189
+ const notes = events.filter(isNoteEvent);
190
+ return notes.length > 0 ? notes : events;
191
+ }
192
+ //# sourceMappingURL=display-timing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"display-timing.js","sourceRoot":"","sources":["../src/display-timing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { InstrumentIconArt } from '@sudobility/music_types';
2
+ /**
3
+ * Strokes `art` in the current `strokeStyle`, scaled into a `size`-square box
4
+ * whose top-left corner is (`x`, `y`).
5
+ *
6
+ * One `beginPath`/`stroke` pair for the whole icon: the shapes are all the same
7
+ * colour and weight, so there is nothing to gain from stroking them separately.
8
+ * `save`/`restore` bracket the transform, so the caller's — the gutter's pinned,
9
+ * zoom-and-DPR-scaled one — survives.
10
+ */
11
+ export declare function strokeInstrumentIcon(ctx: CanvasRenderingContext2D, art: InstrumentIconArt, x: number, y: number, size: number): void;
12
+ //# sourceMappingURL=icon-canvas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icon-canvas.d.ts","sourceRoot":"","sources":["../src/icon-canvas.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAa,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAuC5E;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,wBAAwB,EAC7B,GAAG,EAAE,iBAAiB,EACtB,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,IAAI,EAAE,MAAM,GACX,IAAI,CAiBN"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Draws `InstrumentIconArt` onto a 2D canvas context.
3
+ *
4
+ * The SVG side of the same art lives in the app's `InstrumentIcon` component;
5
+ * both replay the shape list, so an icon added to `gm-icon.ts` appears in the
6
+ * gutter and the DOM without either renderer being touched.
7
+ */
8
+ import { ICON_STROKE_WIDTH, ICON_VIEWBOX, parseIconPath, } from '@sudobility/music_types';
9
+ function appendShape(ctx, shape) {
10
+ if (shape.kind === 'circle') {
11
+ // `arc` draws a line from the current point to the arc's start, which
12
+ // would tie every circle to whatever was drawn before it.
13
+ ctx.moveTo(shape.cx + shape.r, shape.cy);
14
+ ctx.arc(shape.cx, shape.cy, shape.r, 0, Math.PI * 2);
15
+ return;
16
+ }
17
+ for (const segment of parseIconPath(shape.d)) {
18
+ switch (segment.kind) {
19
+ case 'move':
20
+ ctx.moveTo(segment.x, segment.y);
21
+ break;
22
+ case 'line':
23
+ ctx.lineTo(segment.x, segment.y);
24
+ break;
25
+ case 'cubic':
26
+ ctx.bezierCurveTo(segment.x1, segment.y1, segment.x2, segment.y2, segment.x, segment.y);
27
+ break;
28
+ case 'quad':
29
+ ctx.quadraticCurveTo(segment.x1, segment.y1, segment.x, segment.y);
30
+ break;
31
+ case 'close':
32
+ ctx.closePath();
33
+ break;
34
+ }
35
+ }
36
+ }
37
+ /**
38
+ * Strokes `art` in the current `strokeStyle`, scaled into a `size`-square box
39
+ * whose top-left corner is (`x`, `y`).
40
+ *
41
+ * One `beginPath`/`stroke` pair for the whole icon: the shapes are all the same
42
+ * colour and weight, so there is nothing to gain from stroking them separately.
43
+ * `save`/`restore` bracket the transform, so the caller's — the gutter's pinned,
44
+ * zoom-and-DPR-scaled one — survives.
45
+ */
46
+ export function strokeInstrumentIcon(ctx, art, x, y, size) {
47
+ const scale = size / ICON_VIEWBOX;
48
+ ctx.save();
49
+ ctx.translate(x, y);
50
+ ctx.scale(scale, scale);
51
+ // Set inside the scaled space, so the weight scales with the art rather than
52
+ // going hairline at gutter size and heavy at display size.
53
+ ctx.lineWidth = ICON_STROKE_WIDTH;
54
+ ctx.lineJoin = 'round';
55
+ ctx.lineCap = 'round';
56
+ ctx.beginPath();
57
+ for (const shape of art.shapes)
58
+ appendShape(ctx, shape);
59
+ ctx.stroke();
60
+ ctx.restore();
61
+ }
62
+ //# sourceMappingURL=icon-canvas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icon-canvas.js","sourceRoot":"","sources":["../src/icon-canvas.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,aAAa,GACd,MAAM,yBAAyB,CAAC;AAGjC,SAAS,WAAW,CAAC,GAA6B,EAAE,KAAgB;IAClE,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC5B,sEAAsE;QACtE,0DAA0D;QAC1D,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;QACzC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;QACrD,OAAO;IACT,CAAC;IAED,KAAK,MAAM,OAAO,IAAI,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;YACrB,KAAK,MAAM;gBACT,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;gBACjC,MAAM;YACR,KAAK,MAAM;gBACT,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;gBACjC,MAAM;YACR,KAAK,OAAO;gBACV,GAAG,CAAC,aAAa,CACf,OAAO,CAAC,EAAE,EACV,OAAO,CAAC,EAAE,EACV,OAAO,CAAC,EAAE,EACV,OAAO,CAAC,EAAE,EACV,OAAO,CAAC,CAAC,EACT,OAAO,CAAC,CAAC,CACV,CAAC;gBACF,MAAM;YACR,KAAK,MAAM;gBACT,GAAG,CAAC,gBAAgB,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;gBACnE,MAAM;YACR,KAAK,OAAO;gBACV,GAAG,CAAC,SAAS,EAAE,CAAC;gBAChB,MAAM;QACV,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAClC,GAA6B,EAC7B,GAAsB,EACtB,CAAS,EACT,CAAS,EACT,IAAY;IAEZ,MAAM,KAAK,GAAG,IAAI,GAAG,YAAY,CAAC;IAElC,GAAG,CAAC,IAAI,EAAE,CAAC;IACX,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpB,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACxB,6EAA6E;IAC7E,2DAA2D;IAC3D,GAAG,CAAC,SAAS,GAAG,iBAAiB,CAAC;IAClC,GAAG,CAAC,QAAQ,GAAG,OAAO,CAAC;IACvB,GAAG,CAAC,OAAO,GAAG,OAAO,CAAC;IAEtB,GAAG,CAAC,SAAS,EAAE,CAAC;IAChB,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,MAAM;QAAE,WAAW,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACxD,GAAG,CAAC,MAAM,EAAE,CAAC;IAEb,GAAG,CAAC,OAAO,EAAE,CAAC;AAChB,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @sudobility/music_drawing — the notation renderer.
3
+ *
4
+ * Everything that turns a `Score` into pixels: the layout that decides where
5
+ * each system and measure sits, the VexFlow objects that draw a bar, and the
6
+ * windowed canvas renderer that paints only what is on screen.
7
+ *
8
+ * Its own package because it is the one part of the frontend that is *large,
9
+ * self-contained and about drawing*. It depends on `@sudobility/music_types`
10
+ * for the model and on `vexflow` to draw, and on nothing else — no store, no
11
+ * network, no audio, no React. That is what makes it usable from the editor,
12
+ * the print view and the public score page alike, and what keeps `music_lib`
13
+ * from having to carry a rendering engine to offer business logic.
14
+ *
15
+ * The canvas is the deliberate choice here. Drawing only the visible systems
16
+ * per scroll or resize frame *is* the virtualization — one canvas, no
17
+ * per-glyph DOM — which is what makes an unbounded score scroll smoothly.
18
+ */
19
+ export * from './types.js';
20
+ export * from './layout.js';
21
+ export * from './pagination.js';
22
+ export * from './display-timing.js';
23
+ export * from './convert.js';
24
+ export * from './measure-content.js';
25
+ export * from './canvas-renderer.js';
26
+ export * from './note-color.js';
27
+ export * from './playhead.js';
28
+ export * from './percussion.js';
29
+ export * from './icon-canvas.js';
30
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @sudobility/music_drawing — the notation renderer.
3
+ *
4
+ * Everything that turns a `Score` into pixels: the layout that decides where
5
+ * each system and measure sits, the VexFlow objects that draw a bar, and the
6
+ * windowed canvas renderer that paints only what is on screen.
7
+ *
8
+ * Its own package because it is the one part of the frontend that is *large,
9
+ * self-contained and about drawing*. It depends on `@sudobility/music_types`
10
+ * for the model and on `vexflow` to draw, and on nothing else — no store, no
11
+ * network, no audio, no React. That is what makes it usable from the editor,
12
+ * the print view and the public score page alike, and what keeps `music_lib`
13
+ * from having to carry a rendering engine to offer business logic.
14
+ *
15
+ * The canvas is the deliberate choice here. Drawing only the visible systems
16
+ * per scroll or resize frame *is* the virtualization — one canvas, no
17
+ * per-glyph DOM — which is what makes an unbounded score scroll smoothly.
18
+ */
19
+ export * from './types.js';
20
+ export * from './layout.js';
21
+ export * from './pagination.js';
22
+ export * from './display-timing.js';
23
+ export * from './convert.js';
24
+ export * from './measure-content.js';
25
+ export * from './canvas-renderer.js';
26
+ export * from './note-color.js';
27
+ export * from './playhead.js';
28
+ export * from './percussion.js';
29
+ export * from './icon-canvas.js';
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC"}
@@ -0,0 +1,159 @@
1
+ /**
2
+ * System layout (spec §7, §26): decides which measures share a system
3
+ * ("row") and the box (x, y, width, height) each track's stave occupies
4
+ * for every measure, for both "page" (wraps to `options.width`) and
5
+ * "continuous" (everything in one system) layout modes.
6
+ *
7
+ * Measure width is derived from how many onsets a bar actually draws, times a
8
+ * per-onset budget measured from VexFlow's own minimum — close enough that a
9
+ * dense bar is neither cramped nor half empty, without laying every glyph out
10
+ * twice to find out exactly.
11
+ *
12
+ * Units are LOGICAL (design-time) pixels, independent of `options.zoom`.
13
+ * Zoom is applied once, uniformly, as a canvas transform scale in
14
+ * `canvas-renderer.ts` (`ctx.setTransform(zoom·dpr, …)`) so glyphs/text
15
+ * scale along with spacing instead of staying a fixed size while only the
16
+ * layout stretches. The one place zoom enters this module is dividing the
17
+ * *available* screen width by zoom to get the logical width budget for
18
+ * page-mode wrapping (a more zoomed-in view fits fewer logical pixels in
19
+ * the same screen width). Callers needing final on-screen coordinates
20
+ * (e.g. `CanvasRenderResult` bboxes) must multiply this module's output by
21
+ * `zoom` themselves.
22
+ */
23
+ import type { Score, Track } from '@sudobility/music_types';
24
+ import type { RenderOptions } from './types.js';
25
+ export type StaveBox = {
26
+ x: number;
27
+ y: number;
28
+ width: number;
29
+ height: number;
30
+ };
31
+ export type MeasureLayout = {
32
+ measureIndex: number;
33
+ isFirstInSystem: boolean;
34
+ box: StaveBox;
35
+ };
36
+ export type TrackLayout = {
37
+ track: Track;
38
+ measures: MeasureLayout[];
39
+ };
40
+ /** A brace/connector-worthy row: the outer bounds of every track's stave sharing this system. */
41
+ export type SystemLayout = {
42
+ measureIndices: number[];
43
+ xLeft: number;
44
+ xRight: number;
45
+ /** Top of the measure-number band; always `yTop - MEASURE_HEADER_HEIGHT`. */
46
+ gutterTop: number;
47
+ yTop: number;
48
+ yBottom: number;
49
+ };
50
+ export type LayoutPlan = {
51
+ tracks: Track[];
52
+ trackLayouts: TrackLayout[];
53
+ systems: SystemLayout[];
54
+ /** Logical (unscaled) units — multiply by zoom for the final on-screen SVG canvas size. */
55
+ totalWidth: number;
56
+ /** Logical (unscaled) units — multiply by zoom for the final on-screen SVG canvas size. */
57
+ totalHeight: number;
58
+ };
59
+ export declare const BASE_MEASURE_WIDTH = 160;
60
+ /**
61
+ * Width budget per drawn onset.
62
+ *
63
+ * Measured, not guessed: `Formatter.preCalculateMinTotalWidth` asks 446 units
64
+ * for a bar of sixteen sixteenths, which is 27.9 each, and the same 27.9
65
+ * whether or not every one of them carries an accidental. Rounded up to 28, so
66
+ * a dense bar is given at least what VexFlow will ask for and never has to
67
+ * overflow into the next measure's space — which would break the shared
68
+ * barline across tracks.
69
+ *
70
+ * A measure with more onsets than the base width comfortably holds grows
71
+ * proportionally (`max(BASE, onsets * SLOT + PADDING)`), and the count is the
72
+ * union across every track, so all tracks agree on the shared width.
73
+ */
74
+ export declare const NOTE_SLOT_WIDTH = 28;
75
+ /** Breathing room added to a density-derived measure width (start padding + last note's stem/flag overhang + barline clearance). */
76
+ export declare const DENSE_MEASURE_PADDING = 35;
77
+ /** Extra width reserved on a system's first measure for clef + key signature + time signature. */
78
+ export declare const SYSTEM_HEADER_WIDTH = 90;
79
+ /** The whole vertical row one track occupies in a system. Exported so the renderer can cull staves against the viewport. */
80
+ export declare const STAVE_HEIGHT = 100;
81
+ /**
82
+ * Vertical distance between adjacent staff positions, in logical units — one
83
+ * line to the space beside it.
84
+ *
85
+ * VexFlow spaces its five lines 10 units apart and the renderer does not
86
+ * override that, so a staff position is half of it. Exported because dragging a
87
+ * note up and down the staff needs to turn pixels into positions, and deriving
88
+ * that from `STAVE_HEIGHT` would be wrong: that is the whole row a track is
89
+ * given, padding included, not the ruled staff.
90
+ */
91
+ export declare const STAVE_POSITION_HEIGHT = 5;
92
+ /**
93
+ * Distance from a track box's top edge down to the stave's **top line**.
94
+ *
95
+ * VexFlow reserves headroom above the ruled staff for anything that sits over
96
+ * it — `space_above_staff_ln: 4` line-spaces, at its 10-unit line distance — so
97
+ * the `y` a `Stave` is constructed with is not where the music starts. Exported
98
+ * because the track-info gutter aligns its instrument row to the first line the
99
+ * reader actually sees, and eyeballing that offset would drift the moment the
100
+ * stave's options changed.
101
+ */
102
+ export declare const STAVE_TOP_LINE_OFFSET: number;
103
+ /**
104
+ * Width reserved at the left of every system for the track-info gutter (track
105
+ * name, instrument, mute/solo), and the width the app's track editing panel
106
+ * matches.
107
+ *
108
+ * Reserving it here is what makes the gutter's alignment with the staves
109
+ * structural: it lives in the same coordinate space, so there is nothing to
110
+ * synchronise. The previous approach positioned a DOM list against reported
111
+ * geometry and had to be stopped from scrolling independently.
112
+ */
113
+ export declare const TRACK_INFO_WIDTH = 220;
114
+ /**
115
+ * Height of the measure-number band above each system's top stave (also the
116
+ * click target for measure selection).
117
+ *
118
+ * Systems after the first take it out of `SYSTEM_GAP`, so nothing reflows.
119
+ * The first system has only `TOP_MARGIN` above it — not enough — so the
120
+ * effective top margin is raised by this much, which is why `totalHeight`
121
+ * grows by twice this value (the margin is counted at both ends).
122
+ */
123
+ export declare const MEASURE_HEADER_HEIGHT = 18;
124
+ /** Guards against a zero/negative/non-finite zoom breaking division or `context.scale`. */
125
+ export declare function resolveZoom(zoom: number): number;
126
+ /**
127
+ * Computes per-track, per-measure stave boxes and per-system outer bounds
128
+ * (for brace/connector drawing), all in LOGICAL (unscaled) units — see the
129
+ * module doc. Assumes all selected tracks share the same measure count; a
130
+ * track with fewer measures than the score's max simply has no box for the
131
+ * missing trailing measures (defensive, not expected in practice — spec §4
132
+ * keeps tracks aligned to the same measure grid).
133
+ */
134
+ export declare function computeLayout(score: Score, options: RenderOptions): LayoutPlan;
135
+ /**
136
+ * The stave box for `measureIndex` on `plan.trackLayouts[trackIndex]`
137
+ * (logical units), or `null` if that track/measure index isn't present in
138
+ * the plan. Lets a caller locate a measure's position (e.g. to scroll to
139
+ * it during playback) directly from layout, independent of whether that
140
+ * measure fell inside the drawn window of a windowed canvas render pass
141
+ * (system boxes never shift with what a given frame chooses to draw).
142
+ */
143
+ export declare function boxForMeasureIndex(plan: LayoutPlan, trackIndex: number, measureIndex: number): StaveBox | null;
144
+ /** Binary search over the y-sorted `plan.systems` for the system containing logical `y`; `null` in inter-system gaps or outside the score. O(log n). */
145
+ export declare function systemAtY(plan: LayoutPlan, y: number): SystemLayout | null;
146
+ /**
147
+ * Binary search over `system`'s x-sorted measures (first track's layouts)
148
+ * for the measure containing logical `x`, clamping x into the system's
149
+ * measure span (clicks left of the clef resolve to the first measure,
150
+ * right of the last barline to the last). `null` only when the system
151
+ * resolves to no measure layouts. O(log n).
152
+ *
153
+ * Indexing note: `computeLayout` pushes one `MeasureLayout` per measure in
154
+ * ascending order for every track that has the measure, so for track 0
155
+ * `trackLayouts[0].measures[i].measureIndex === i` — direct indexing by
156
+ * `system.measureIndices` values is safe (and bounds-guarded here anyway).
157
+ */
158
+ export declare function measureAtXInSystem(plan: LayoutPlan, system: SystemLayout, x: number): MeasureLayout | null;
159
+ //# sourceMappingURL=layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAE5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,QAAQ,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/E,MAAM,MAAM,aAAa,GAAG;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,OAAO,CAAC;IACzB,GAAG,EAAE,QAAQ,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B,CAAC;AAEF,iGAAiG;AACjG,MAAM,MAAM,YAAY,GAAG;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,2FAA2F;IAC3F,UAAU,EAAE,MAAM,CAAC;IACnB,2FAA2F;IAC3F,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAUtC;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,oIAAoI;AACpI,eAAO,MAAM,qBAAqB,KAAK,CAAC;AACxC,kGAAkG;AAClG,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAUtC,4HAA4H;AAC5H,eAAO,MAAM,YAAY,MAAM,CAAC;AAEhC;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,QAA4B,CAAC;AAM/D;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAGpC;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC,2FAA2F;AAC3F,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AA+CD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,aAAa,GACrB,UAAU,CAyMZ;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,GACnB,QAAQ,GAAG,IAAI,CAMjB;AAED,wJAAwJ;AACxJ,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAY1E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,YAAY,EACpB,CAAC,EAAE,MAAM,GACR,aAAa,GAAG,IAAI,CAuBtB"}