@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.
- package/dist/canvas-renderer.d.ts +180 -0
- package/dist/canvas-renderer.d.ts.map +1 -0
- package/dist/canvas-renderer.js +831 -0
- package/dist/canvas-renderer.js.map +1 -0
- package/dist/convert.d.ts +122 -0
- package/dist/convert.d.ts.map +1 -0
- package/dist/convert.js +426 -0
- package/dist/convert.js.map +1 -0
- package/dist/display-timing.d.ts +68 -0
- package/dist/display-timing.d.ts.map +1 -0
- package/dist/display-timing.js +192 -0
- package/dist/display-timing.js.map +1 -0
- package/dist/icon-canvas.d.ts +12 -0
- package/dist/icon-canvas.d.ts.map +1 -0
- package/dist/icon-canvas.js +62 -0
- package/dist/icon-canvas.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +159 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +362 -0
- package/dist/layout.js.map +1 -0
- package/dist/measure-content.d.ts +114 -0
- package/dist/measure-content.d.ts.map +1 -0
- package/dist/measure-content.js +553 -0
- package/dist/measure-content.js.map +1 -0
- package/dist/note-color.d.ts +48 -0
- package/dist/note-color.d.ts.map +1 -0
- package/dist/note-color.js +81 -0
- package/dist/note-color.js.map +1 -0
- package/dist/pagination.d.ts +57 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +152 -0
- package/dist/pagination.js.map +1 -0
- package/dist/percussion.d.ts +47 -0
- package/dist/percussion.d.ts.map +1 -0
- package/dist/percussion.js +172 -0
- package/dist/percussion.js.map +1 -0
- package/dist/playhead.d.ts +37 -0
- package/dist/playhead.d.ts.map +1 -0
- package/dist/playhead.js +89 -0
- package/dist/playhead.js.map +1 -0
- package/dist/test/canvas-stub.d.ts +21 -0
- package/dist/test/canvas-stub.d.ts.map +1 -0
- package/dist/test/canvas-stub.js +51 -0
- package/dist/test/canvas-stub.js.map +1 -0
- package/dist/test/fixtures.d.ts +47 -0
- package/dist/test/fixtures.d.ts.map +1 -0
- package/dist/test/fixtures.js +433 -0
- package/dist/test/fixtures.js.map +1 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- 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"}
|
package/dist/index.d.ts
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.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"}
|
package/dist/layout.d.ts
ADDED
|
@@ -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"}
|