@sudobility/music_drawing 0.0.33 → 0.0.35
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 +79 -4
- package/dist/canvas-renderer.d.ts.map +1 -1
- package/dist/canvas-renderer.js +245 -53
- package/dist/canvas-renderer.js.map +1 -1
- package/dist/cursor.d.ts +60 -0
- package/dist/cursor.d.ts.map +1 -0
- package/dist/cursor.js +157 -0
- package/dist/cursor.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/dist/keyboard-geometry.d.ts +50 -2
- package/dist/keyboard-geometry.d.ts.map +1 -1
- package/dist/keyboard-geometry.js +71 -2
- package/dist/keyboard-geometry.js.map +1 -1
- package/dist/keyboard-keys.d.ts +93 -0
- package/dist/keyboard-keys.d.ts.map +1 -0
- package/dist/keyboard-keys.js +60 -0
- package/dist/keyboard-keys.js.map +1 -0
- package/dist/layered-paint.d.ts +47 -0
- package/dist/layered-paint.d.ts.map +1 -0
- package/dist/layered-paint.js +22 -0
- package/dist/layered-paint.js.map +1 -0
- package/dist/pagination.d.ts +6 -2
- package/dist/pagination.d.ts.map +1 -1
- package/dist/pagination.js +4 -0
- package/dist/pagination.js.map +1 -1
- package/dist/playback-binding.d.ts +57 -0
- package/dist/playback-binding.d.ts.map +1 -0
- package/dist/playback-binding.js +110 -0
- package/dist/playback-binding.js.map +1 -0
- package/dist/playhead.d.ts.map +1 -1
- package/dist/playhead.js +22 -3
- package/dist/playhead.js.map +1 -1
- package/dist/press.d.ts +66 -0
- package/dist/press.d.ts.map +1 -0
- package/dist/press.js +64 -0
- package/dist/press.js.map +1 -0
- package/dist/print-plan.d.ts +64 -0
- package/dist/print-plan.d.ts.map +1 -0
- package/dist/print-plan.js +101 -0
- package/dist/print-plan.js.map +1 -0
- package/dist/render-theme.d.ts.map +1 -1
- package/dist/render-theme.js +11 -2
- package/dist/render-theme.js.map +1 -1
- package/dist/score-canvas.d.ts +280 -0
- package/dist/score-canvas.d.ts.map +1 -0
- package/dist/score-canvas.js +702 -0
- package/dist/score-canvas.js.map +1 -0
- package/dist/skia/index.d.ts +2 -0
- package/dist/skia/index.d.ts.map +1 -1
- package/dist/skia/index.js +1 -0
- package/dist/skia/index.js.map +1 -1
- package/dist/skia/skia-layers.d.ts +43 -0
- package/dist/skia/skia-layers.d.ts.map +1 -0
- package/dist/skia/skia-layers.js +24 -0
- package/dist/skia/skia-layers.js.map +1 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
|
@@ -19,7 +19,28 @@ export type CanvasRenderOptions = RenderOptions & {
|
|
|
19
19
|
viewport: CanvasViewport;
|
|
20
20
|
/** Backing-store scale (window.devicePixelRatio); default 1. The canvas element's width/height must already be sized to cssSize × this. */
|
|
21
21
|
devicePixelRatio?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Which part of the frame to draw. Default `'all'`.
|
|
24
|
+
*
|
|
25
|
+
* A frame is two layers. The **overlay** is the active track's notes — the
|
|
26
|
+
* noteheads, stems and flags, their beams, tuplet brackets, ties and slurs —
|
|
27
|
+
* with the track-info gutter over them; the **base** is everything else.
|
|
28
|
+
* Playback only ever changes the overlay: the only notes that light up are
|
|
29
|
+
* the active track's (`noteColorsFor`), so a host that keeps the base from
|
|
30
|
+
* the last paint redraws one track's notes per change of lit notes instead
|
|
31
|
+
* of the whole window. See `createLayeredPaint`.
|
|
32
|
+
*
|
|
33
|
+
* `'all'` is exactly `'base'` followed by `'overlay'` on the same context,
|
|
34
|
+
* so a layered host and a single-pass one draw the same picture — the
|
|
35
|
+
* active track's notes sit over the rest of the window either way.
|
|
36
|
+
*
|
|
37
|
+
* `'overlay'` never clears, and draws against the window the last `'base'`
|
|
38
|
+
* pass built. Asked for a window that pass did not paint, it draws the
|
|
39
|
+
* whole frame instead, which is right whatever lies underneath.
|
|
40
|
+
*/
|
|
41
|
+
layer?: RenderLayer;
|
|
22
42
|
};
|
|
43
|
+
export type RenderLayer = 'all' | 'base' | 'overlay';
|
|
23
44
|
/**
|
|
24
45
|
* Where notes actually live inside a measure.
|
|
25
46
|
*
|
|
@@ -144,7 +165,52 @@ export declare class CanvasScoreRenderer {
|
|
|
144
165
|
private columnFor;
|
|
145
166
|
/** Trims the column cache back to twice what the frame just drew, oldest first. */
|
|
146
167
|
private evictColumns;
|
|
168
|
+
/**
|
|
169
|
+
* Builds, without drawing anything, up to `limit` of the measure columns the
|
|
170
|
+
* window in `options.viewport` needs and this renderer does not already
|
|
171
|
+
* hold; answers how many are still missing after that.
|
|
172
|
+
*
|
|
173
|
+
* Building a column — constructing and formatting its VexFlow objects — is
|
|
174
|
+
* what a frame costs when it shows music it has not shown before, which
|
|
175
|
+
* during playback is exactly the frame the view moves to the next system on:
|
|
176
|
+
* measured at 8ms in a browser on a dense score, several times that on a
|
|
177
|
+
* phone's engine, landing on the moment the eye is following the music
|
|
178
|
+
* across the break. A host that calls this ahead of time, a column per idle
|
|
179
|
+
* task, turns that frame into an ordinary repaint.
|
|
180
|
+
*
|
|
181
|
+
* The columns go into the same cache `render` reads, so nothing else
|
|
182
|
+
* changes: the frame that eventually shows them is built from them.
|
|
183
|
+
*/
|
|
184
|
+
prepare(score: Score, options: CanvasRenderOptions, limit?: number): number;
|
|
147
185
|
render(score: Score, ctx: DrawingContext2D, options: CanvasRenderOptions): CanvasRenderResult;
|
|
186
|
+
/**
|
|
187
|
+
* The track whose notes are the overlay: the active one, on screen.
|
|
188
|
+
*
|
|
189
|
+
* Null in print, and with no active track, which is when nothing can light
|
|
190
|
+
* up and the base carries every note.
|
|
191
|
+
*/
|
|
192
|
+
private overlayTrackId;
|
|
193
|
+
/** Sets the frame's transform on `ctx`, content scrolled by the viewport. */
|
|
194
|
+
private applyContentTransform;
|
|
195
|
+
/** Clears the canvas and paints everything but the overlay track's notes. */
|
|
196
|
+
private paintBase;
|
|
197
|
+
/**
|
|
198
|
+
* Paints the overlay track's notes over a base already on the canvas: its
|
|
199
|
+
* voices, beams and tuplets on the staves the base drew, then its ties and
|
|
200
|
+
* slurs. Never clears.
|
|
201
|
+
*/
|
|
202
|
+
private paintOverlay;
|
|
203
|
+
private drawSpans;
|
|
204
|
+
/**
|
|
205
|
+
* What the host gets back.
|
|
206
|
+
*
|
|
207
|
+
* Note boxes only once the frame is complete: VexFlow places a note when it
|
|
208
|
+
* draws it (see `ColumnDrawing.bboxes`), so a base pass that left the active
|
|
209
|
+
* track out would report none for it — and a base on its own is never what a
|
|
210
|
+
* host hit-tests against, because the overlay pass that follows returns the
|
|
211
|
+
* whole frame's.
|
|
212
|
+
*/
|
|
213
|
+
private frameResult;
|
|
148
214
|
/**
|
|
149
215
|
* Assembles the drawn window out of measure columns, building only the ones
|
|
150
216
|
* the cache does not already hold.
|
|
@@ -257,13 +323,22 @@ export declare class CanvasScoreRenderer {
|
|
|
257
323
|
*/
|
|
258
324
|
private collectColumnBBoxes;
|
|
259
325
|
/**
|
|
260
|
-
* Paints an already-built system.
|
|
326
|
+
* Paints an already-built system, less the overlay track's notes.
|
|
261
327
|
*
|
|
262
328
|
* Everything here is a colour decision or a context write, so it is what runs
|
|
263
|
-
* again when
|
|
264
|
-
*
|
|
329
|
+
* again when the selection moves — cheap beside the building it no longer
|
|
330
|
+
* has to repeat.
|
|
331
|
+
*/
|
|
332
|
+
private paintSystemBase;
|
|
333
|
+
/**
|
|
334
|
+
* One track's notes, beams and tuplets over a system the base already drew.
|
|
335
|
+
*
|
|
336
|
+
* Styled here rather than in the base, since this is where the colour a
|
|
337
|
+
* change of lit notes makes is applied.
|
|
265
338
|
*/
|
|
266
|
-
private
|
|
339
|
+
private paintSystemOverlay;
|
|
340
|
+
/** Beams, then tuplet brackets, for the tracks `include` admits. */
|
|
341
|
+
private drawBeamsAndTuplets;
|
|
267
342
|
/**
|
|
268
343
|
* Tints every selected measure across the full height of the system.
|
|
269
344
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas-renderer.d.ts","sourceRoot":"","sources":["../src/canvas-renderer.ts"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAoBrD,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,aAAa,CAAC;AAG5D,OAAO,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEnE,OAAO,KAAK,EAAE,gBAAgB,EAAmB,MAAM,sBAAsB,CAAC;AAE9E;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG;IAChD,QAAQ,EAAE,cAAc,CAAC;IACzB,2IAA2I;IAC3I,gBAAgB,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"canvas-renderer.d.ts","sourceRoot":"","sources":["../src/canvas-renderer.ts"],"names":[],"mappings":"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"}
|
package/dist/canvas-renderer.js
CHANGED
|
@@ -289,20 +289,60 @@ export class CanvasScoreRenderer {
|
|
|
289
289
|
this.columns.delete(oldest.value);
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Builds, without drawing anything, up to `limit` of the measure columns the
|
|
294
|
+
* window in `options.viewport` needs and this renderer does not already
|
|
295
|
+
* hold; answers how many are still missing after that.
|
|
296
|
+
*
|
|
297
|
+
* Building a column — constructing and formatting its VexFlow objects — is
|
|
298
|
+
* what a frame costs when it shows music it has not shown before, which
|
|
299
|
+
* during playback is exactly the frame the view moves to the next system on:
|
|
300
|
+
* measured at 8ms in a browser on a dense score, several times that on a
|
|
301
|
+
* phone's engine, landing on the moment the eye is following the music
|
|
302
|
+
* across the break. A host that calls this ahead of time, a column per idle
|
|
303
|
+
* task, turns that frame into an ordinary repaint.
|
|
304
|
+
*
|
|
305
|
+
* The columns go into the same cache `render` reads, so nothing else
|
|
306
|
+
* changes: the frame that eventually shows them is built from them.
|
|
307
|
+
*/
|
|
308
|
+
prepare(score, options, limit = 1) {
|
|
309
|
+
const z = resolveZoom(options.zoom);
|
|
310
|
+
const plan = this.planFor(score, options);
|
|
311
|
+
if (this.columnsPlan !== plan) {
|
|
312
|
+
this.columns.clear();
|
|
313
|
+
this.columnsPlan = plan;
|
|
314
|
+
}
|
|
315
|
+
const systems = systemsInRange(plan, options.viewport.top, options.viewport.bottom);
|
|
316
|
+
let built = 0;
|
|
317
|
+
let missing = 0;
|
|
318
|
+
for (const system of systems) {
|
|
319
|
+
const indices = this.visibleMeasureIndices(system, plan, options.viewport.left ?? 0, options.viewport.right ?? Number.POSITIVE_INFINITY);
|
|
320
|
+
for (const measureIndex of indices) {
|
|
321
|
+
if (this.columns.has(measureIndex))
|
|
322
|
+
continue;
|
|
323
|
+
if (built >= limit) {
|
|
324
|
+
missing += 1;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
built += 1;
|
|
328
|
+
try {
|
|
329
|
+
this.columns.set(measureIndex, this.buildColumn(measureIndex, plan, score, z));
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
// The frame that needs it will try again and report the failure
|
|
333
|
+
// where it would have anyway; a preparation has nobody to tell.
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return missing;
|
|
338
|
+
}
|
|
292
339
|
render(score, ctx, options) {
|
|
293
340
|
const z = resolveZoom(options.zoom);
|
|
294
341
|
const dpr = options.devicePixelRatio ?? 1;
|
|
295
342
|
const plan = this.planFor(score, options);
|
|
343
|
+
const layer = options.layer ?? 'all';
|
|
296
344
|
const viewportLeft = options.viewport.left ?? 0;
|
|
297
345
|
const viewportRight = options.viewport.right ?? Number.POSITIVE_INFINITY;
|
|
298
|
-
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
299
|
-
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
300
|
-
// `+ 0` normalizes -0 (from a zero scroll offset) to 0.
|
|
301
|
-
ctx.setTransform(z * dpr, 0, 0, z * dpr, -viewportLeft * z * dpr + 0, -options.viewport.top * z * dpr + 0);
|
|
302
|
-
const vexCtx = new CanvasContext(ctx);
|
|
303
|
-
const ink = this.inkColor(options);
|
|
304
|
-
vexCtx.setFillStyle(ink);
|
|
305
|
-
vexCtx.setStrokeStyle(ink);
|
|
306
346
|
const visibleSystems = systemsInRange(plan, options.viewport.top, options.viewport.bottom);
|
|
307
347
|
// Everything the drawn window depends on, and nothing colour does. A
|
|
308
348
|
// repaint driven by a note starting or a selection moving produces the same
|
|
@@ -313,19 +353,81 @@ export class CanvasScoreRenderer {
|
|
|
313
353
|
: null;
|
|
314
354
|
const frame = cached ?? this.buildWindow(key, plan, score, z, options, visibleSystems);
|
|
315
355
|
this.window = frame;
|
|
316
|
-
|
|
356
|
+
/*
|
|
357
|
+
The overlay is drawn against what the base pass inked, and only a base
|
|
358
|
+
pass over this window with the same active track can say what that was.
|
|
359
|
+
Anything else — a window that has just been built, or a base that left
|
|
360
|
+
out a different track — means the host's base is not this frame's, and
|
|
361
|
+
the whole frame is drawn instead: right whatever lies underneath, since
|
|
362
|
+
it clears first.
|
|
363
|
+
*/
|
|
364
|
+
const overlayTrackId = this.overlayTrackId(options);
|
|
365
|
+
const effective = layer === 'overlay' &&
|
|
366
|
+
(frame.painted === null ||
|
|
367
|
+
frame.painted.overlayTrackId !== overlayTrackId)
|
|
368
|
+
? 'all'
|
|
369
|
+
: layer;
|
|
370
|
+
const vexCtx = new CanvasContext(ctx);
|
|
371
|
+
if (effective !== 'overlay') {
|
|
372
|
+
this.paintBase(frame, plan, ctx, vexCtx, z, dpr, options, overlayTrackId);
|
|
373
|
+
}
|
|
374
|
+
if (effective === 'base') {
|
|
375
|
+
// The host composites the overlay over this; the result it needs comes
|
|
376
|
+
// from that pass, which is the one that finishes the frame.
|
|
377
|
+
return this.frameResult(frame, plan, z, options, false);
|
|
378
|
+
}
|
|
379
|
+
this.paintOverlay(frame, ctx, vexCtx, z, dpr, options);
|
|
380
|
+
if (shouldShowTrackInfo(options)) {
|
|
381
|
+
this.drawTrackInfoGutter(plan, ctx, z, dpr, visibleSystems, options);
|
|
382
|
+
}
|
|
383
|
+
return this.frameResult(frame, plan, z, options, true);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* The track whose notes are the overlay: the active one, on screen.
|
|
387
|
+
*
|
|
388
|
+
* Null in print, and with no active track, which is when nothing can light
|
|
389
|
+
* up and the base carries every note.
|
|
390
|
+
*/
|
|
391
|
+
overlayTrackId(options) {
|
|
392
|
+
if (this.isPrint(options))
|
|
393
|
+
return null;
|
|
394
|
+
return options.activeTrackId ?? null;
|
|
395
|
+
}
|
|
396
|
+
/** Sets the frame's transform on `ctx`, content scrolled by the viewport. */
|
|
397
|
+
applyContentTransform(ctx, z, dpr, options) {
|
|
398
|
+
const viewportLeft = options.viewport.left ?? 0;
|
|
399
|
+
// `+ 0` normalizes -0 (from a zero scroll offset) to 0.
|
|
400
|
+
ctx.setTransform(z * dpr, 0, 0, z * dpr, -viewportLeft * z * dpr + 0, -options.viewport.top * z * dpr + 0);
|
|
401
|
+
}
|
|
402
|
+
/** Clears the canvas and paints everything but the overlay track's notes. */
|
|
403
|
+
paintBase(frame, plan, ctx, vexCtx, z, dpr, options, overlayTrackId) {
|
|
404
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
405
|
+
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
406
|
+
this.applyContentTransform(ctx, z, dpr, options);
|
|
407
|
+
const ink = this.inkColor(options);
|
|
408
|
+
vexCtx.setFillStyle(ink);
|
|
409
|
+
vexCtx.setStrokeStyle(ink);
|
|
317
410
|
// Which staves this frame actually put ink on. Spans are drawn after every
|
|
318
411
|
// system, so they need the whole frame's answer, not one system's.
|
|
319
412
|
const drawnStaves = new Set();
|
|
320
413
|
const drawnTrackIds = new Set();
|
|
321
|
-
|
|
414
|
+
const visibleStaves = [];
|
|
415
|
+
for (const drawing of frame.drawings) {
|
|
416
|
+
const onScreen = new Set();
|
|
417
|
+
visibleStaves.push(onScreen);
|
|
322
418
|
try {
|
|
323
|
-
this.
|
|
419
|
+
this.paintSystemBase(drawing, plan, vexCtx, ctx, options, overlayTrackId, onScreen, drawnStaves, drawnTrackIds);
|
|
324
420
|
}
|
|
325
421
|
catch (error) {
|
|
326
422
|
console.error('CanvasScoreRenderer: skipping system after draw failure', drawing.system.measureIndices, error);
|
|
327
423
|
}
|
|
328
424
|
}
|
|
425
|
+
frame.painted = {
|
|
426
|
+
drawnStaves,
|
|
427
|
+
drawnTrackIds,
|
|
428
|
+
visibleStaves,
|
|
429
|
+
overlayTrackId,
|
|
430
|
+
};
|
|
329
431
|
// Ties and the other spans reach across measures — and across systems —
|
|
330
432
|
// within the drawn window, so they go on last, over everything.
|
|
331
433
|
//
|
|
@@ -334,8 +436,41 @@ export class CanvasScoreRenderer {
|
|
|
334
436
|
// window. Only their colour is a per-frame decision, and that is applied
|
|
335
437
|
// below. They cannot be built before the first paint, though, because a
|
|
336
438
|
// span takes its Y from notes that must already have been drawn.
|
|
337
|
-
frame.spans ?? (frame.spans = this.buildSpans(channelsByTrack, drawnStaves, drawnTrackIds));
|
|
338
|
-
|
|
439
|
+
frame.spans ?? (frame.spans = this.buildSpans(frame.channelsByTrack, drawnStaves, drawnTrackIds));
|
|
440
|
+
this.drawSpans(frame.spans, vexCtx, options, span => overlayTrackId === null ? true : span.trackId !== overlayTrackId);
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Paints the overlay track's notes over a base already on the canvas: its
|
|
444
|
+
* voices, beams and tuplets on the staves the base drew, then its ties and
|
|
445
|
+
* slurs. Never clears.
|
|
446
|
+
*/
|
|
447
|
+
paintOverlay(frame, ctx, vexCtx, z, dpr, options) {
|
|
448
|
+
const painted = frame.painted;
|
|
449
|
+
const trackId = painted?.overlayTrackId ?? null;
|
|
450
|
+
if (!painted || trackId === null)
|
|
451
|
+
return;
|
|
452
|
+
this.applyContentTransform(ctx, z, dpr, options);
|
|
453
|
+
frame.drawings.forEach((drawing, index) => {
|
|
454
|
+
try {
|
|
455
|
+
this.paintSystemOverlay(drawing, vexCtx, options, trackId, painted.visibleStaves[index] ?? new Set());
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
console.error('CanvasScoreRenderer: skipping system after draw failure', drawing.system.measureIndices, error);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
// After this track's notes, so a tie takes its Y from a drawn notehead.
|
|
462
|
+
if (frame.spans) {
|
|
463
|
+
this.drawSpans(frame.spans, vexCtx, options, span => span.trackId === trackId);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
drawSpans(spans, vexCtx, options, include) {
|
|
467
|
+
const ink = this.inkColor(options);
|
|
468
|
+
vexCtx.setFillStyle(ink);
|
|
469
|
+
vexCtx.setStrokeStyle(ink);
|
|
470
|
+
for (const drawing of spans) {
|
|
471
|
+
if (!include(drawing))
|
|
472
|
+
continue;
|
|
473
|
+
const { span, trackId, what } = drawing;
|
|
339
474
|
// Per span, not per channel: one bad tie used to abandon every remaining
|
|
340
475
|
// tie in the channel, so visible ties vanished too.
|
|
341
476
|
try {
|
|
@@ -350,21 +485,29 @@ export class CanvasScoreRenderer {
|
|
|
350
485
|
console.error(`CanvasScoreRenderer: skipping ${what} after draw failure`, error);
|
|
351
486
|
}
|
|
352
487
|
}
|
|
353
|
-
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* What the host gets back.
|
|
491
|
+
*
|
|
492
|
+
* Note boxes only once the frame is complete: VexFlow places a note when it
|
|
493
|
+
* draws it (see `ColumnDrawing.bboxes`), so a base pass that left the active
|
|
494
|
+
* track out would report none for it — and a base on its own is never what a
|
|
495
|
+
* host hit-tests against, because the overlay pass that follows returns the
|
|
496
|
+
* whole frame's.
|
|
497
|
+
*/
|
|
498
|
+
frameResult(frame, plan, z, options, complete) {
|
|
354
499
|
const idToBBox = new Map();
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (shouldShowTrackInfo(options)) {
|
|
361
|
-
this.drawTrackInfoGutter(plan, ctx, z, dpr, visibleSystems, options);
|
|
500
|
+
const painted = frame.painted;
|
|
501
|
+
if (complete && painted) {
|
|
502
|
+
for (const drawing of frame.drawings)
|
|
503
|
+
for (const column of drawing.columns)
|
|
504
|
+
this.collectColumnBBoxes(column, z, painted.drawnStaves, painted.drawnTrackIds, idToBBox);
|
|
362
505
|
}
|
|
363
506
|
return {
|
|
364
507
|
idToBBox,
|
|
365
|
-
measureIdToBBox,
|
|
366
|
-
measureNotePositions,
|
|
367
|
-
drawnMeasureIndices,
|
|
508
|
+
measureIdToBBox: frame.measureIdToBBox,
|
|
509
|
+
measureNotePositions: frame.measureNotePositions,
|
|
510
|
+
drawnMeasureIndices: frame.drawnMeasureIndices,
|
|
368
511
|
plan,
|
|
369
512
|
theme: options.theme,
|
|
370
513
|
};
|
|
@@ -447,6 +590,7 @@ export class CanvasScoreRenderer {
|
|
|
447
590
|
measureNotePositions,
|
|
448
591
|
drawnMeasureIndices,
|
|
449
592
|
spans: null,
|
|
593
|
+
painted: null,
|
|
450
594
|
};
|
|
451
595
|
}
|
|
452
596
|
/**
|
|
@@ -824,6 +968,7 @@ export class CanvasScoreRenderer {
|
|
|
824
968
|
};
|
|
825
969
|
const measureStaves = [];
|
|
826
970
|
const voiceGroups = [];
|
|
971
|
+
const voiceGroupTracks = [];
|
|
827
972
|
const allMetas = []; // buildMeasureContent appends; unused here (channels carry the metas)
|
|
828
973
|
plan.trackLayouts.forEach((trackLayout, trackIndex) => {
|
|
829
974
|
const track = trackLayout.track;
|
|
@@ -862,6 +1007,7 @@ export class CanvasScoreRenderer {
|
|
|
862
1007
|
if (voices.length > 0) {
|
|
863
1008
|
voices.forEach(v => v.setStave(stave));
|
|
864
1009
|
voiceGroups.push(voices);
|
|
1010
|
+
voiceGroupTracks.push(track.id);
|
|
865
1011
|
}
|
|
866
1012
|
column.measureIdToBBox.set(measure.id, {
|
|
867
1013
|
x: placement.box.x * z,
|
|
@@ -913,7 +1059,13 @@ export class CanvasScoreRenderer {
|
|
|
913
1059
|
formatter.joinVoices(group);
|
|
914
1060
|
const justifyWidth = Math.max(20, Math.min(...measureStaves.map(s => s.getNoteEndX() - s.getNoteStartX())) - BARLINE_CLEARANCE);
|
|
915
1061
|
formatter.format(voiceGroups.flat(), justifyWidth);
|
|
916
|
-
|
|
1062
|
+
voiceGroups.forEach((group, index) => {
|
|
1063
|
+
const trackId = voiceGroupTracks[index];
|
|
1064
|
+
if (trackId === undefined)
|
|
1065
|
+
return;
|
|
1066
|
+
for (const voice of group)
|
|
1067
|
+
column.voices.push({ voice, trackId });
|
|
1068
|
+
});
|
|
917
1069
|
/*
|
|
918
1070
|
Attach each tickable to its stave, then record where the formatter put
|
|
919
1071
|
it. This is what lets the caret share the notes' own positions rather
|
|
@@ -984,15 +1136,19 @@ export class CanvasScoreRenderer {
|
|
|
984
1136
|
}
|
|
985
1137
|
}
|
|
986
1138
|
/**
|
|
987
|
-
* Paints an already-built system.
|
|
1139
|
+
* Paints an already-built system, less the overlay track's notes.
|
|
988
1140
|
*
|
|
989
1141
|
* Everything here is a colour decision or a context write, so it is what runs
|
|
990
|
-
* again when
|
|
991
|
-
*
|
|
1142
|
+
* again when the selection moves — cheap beside the building it no longer
|
|
1143
|
+
* has to repeat.
|
|
992
1144
|
*/
|
|
993
|
-
|
|
1145
|
+
paintSystemBase(drawing, plan, vexCtx,
|
|
994
1146
|
/** The raw 2D context behind `vexCtx`: the measure gutter is a number and a rect, so it needs nothing VexFlow provides. */
|
|
995
1147
|
ctx, options,
|
|
1148
|
+
/** Whose notes are left for the overlay; null leaves nothing out. */
|
|
1149
|
+
overlayTrackId,
|
|
1150
|
+
/** Collects this system's on-screen staves, for the overlay to draw on. */
|
|
1151
|
+
visibleStaves,
|
|
996
1152
|
/** Collects the staves this system actually drew, for the frame's ties. */
|
|
997
1153
|
drawnStaves,
|
|
998
1154
|
/** Collects track ids that drew at least one stave, so later frame-wide scans can skip wholly off-screen tracks. */
|
|
@@ -1025,7 +1181,6 @@ export class CanvasScoreRenderer {
|
|
|
1025
1181
|
// One pass per kind rather than one per column, so the draw order is by
|
|
1026
1182
|
// kind across the whole system — which is what puts every beam over every
|
|
1027
1183
|
// notehead rather than only over its own measure's.
|
|
1028
|
-
const visibleStaves = new Set();
|
|
1029
1184
|
for (const column of columns) {
|
|
1030
1185
|
for (const { stave, trackId } of column.staves) {
|
|
1031
1186
|
if (!onScreen(stave))
|
|
@@ -1033,7 +1188,9 @@ export class CanvasScoreRenderer {
|
|
|
1033
1188
|
visibleStaves.add(stave);
|
|
1034
1189
|
drawnStaves.add(stave);
|
|
1035
1190
|
drawnTrackIds.add(trackId);
|
|
1036
|
-
|
|
1191
|
+
if (trackId !== overlayTrackId) {
|
|
1192
|
+
this.styleColumnTrackNotes(column, trackId, options);
|
|
1193
|
+
}
|
|
1037
1194
|
const dimmed = this.notesDimmed(trackId, options);
|
|
1038
1195
|
stave.setStyle({
|
|
1039
1196
|
strokeStyle: this.staveColor(trackId, options),
|
|
@@ -1055,7 +1212,9 @@ export class CanvasScoreRenderer {
|
|
|
1055
1212
|
vexCtx.setFillStyle(ink);
|
|
1056
1213
|
vexCtx.setStrokeStyle(ink);
|
|
1057
1214
|
for (const column of columns) {
|
|
1058
|
-
for (const voice of column.voices) {
|
|
1215
|
+
for (const { voice, trackId } of column.voices) {
|
|
1216
|
+
if (trackId === overlayTrackId)
|
|
1217
|
+
continue;
|
|
1059
1218
|
const stave = voice.getStave();
|
|
1060
1219
|
if (stave && !visibleStaves.has(stave))
|
|
1061
1220
|
continue;
|
|
@@ -1063,6 +1222,7 @@ export class CanvasScoreRenderer {
|
|
|
1063
1222
|
}
|
|
1064
1223
|
}
|
|
1065
1224
|
// After the formatter has run, so the stave's final geometry is settled.
|
|
1225
|
+
// Rests stay in the base: a bar's multi-measure rest never lights up.
|
|
1066
1226
|
for (const column of columns) {
|
|
1067
1227
|
for (const { rest, trackId } of column.rests) {
|
|
1068
1228
|
const stave = rest.getStave?.();
|
|
@@ -1076,9 +1236,60 @@ export class CanvasScoreRenderer {
|
|
|
1076
1236
|
}
|
|
1077
1237
|
vexCtx.setFillStyle(ink);
|
|
1078
1238
|
vexCtx.setStrokeStyle(ink);
|
|
1239
|
+
this.drawBeamsAndTuplets(columns, vexCtx, options, visibleStaves, trackId => trackId !== overlayTrackId);
|
|
1240
|
+
if (plan.tracks.length > 1) {
|
|
1241
|
+
const topStave = firstStaveByTrack.get(0);
|
|
1242
|
+
const bottomStave = firstStaveByTrack.get(plan.tracks.length - 1);
|
|
1243
|
+
// Culled like everything else. Without this the brace was drawn even
|
|
1244
|
+
// when both of the staves it joins were scrolled off — on a tall score
|
|
1245
|
+
// it stretched thousands of pixels and the slice inside the viewport
|
|
1246
|
+
// read as a stray vertical mark in the corner rather than as notation.
|
|
1247
|
+
if (topStave &&
|
|
1248
|
+
bottomStave &&
|
|
1249
|
+
topStave !== bottomStave &&
|
|
1250
|
+
visibleStaves.has(topStave) &&
|
|
1251
|
+
visibleStaves.has(bottomStave)) {
|
|
1252
|
+
const connector = new StaveConnector(topStave, bottomStave);
|
|
1253
|
+
connector.setType('brace');
|
|
1254
|
+
connector.setContext(vexCtx);
|
|
1255
|
+
connector.draw();
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
this.drawMeasureGutter(system, plan, ctx, windowIndices, options);
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* One track's notes, beams and tuplets over a system the base already drew.
|
|
1262
|
+
*
|
|
1263
|
+
* Styled here rather than in the base, since this is where the colour a
|
|
1264
|
+
* change of lit notes makes is applied.
|
|
1265
|
+
*/
|
|
1266
|
+
paintSystemOverlay(drawing, vexCtx, options, trackId, visibleStaves) {
|
|
1267
|
+
const { columns } = drawing;
|
|
1268
|
+
for (const column of columns) {
|
|
1269
|
+
const visible = column.staves.some(entry => entry.trackId === trackId && visibleStaves.has(entry.stave));
|
|
1270
|
+
if (visible)
|
|
1271
|
+
this.styleColumnTrackNotes(column, trackId, options);
|
|
1272
|
+
}
|
|
1273
|
+
const ink = this.inkColor(options);
|
|
1274
|
+
vexCtx.setFillStyle(ink);
|
|
1275
|
+
vexCtx.setStrokeStyle(ink);
|
|
1276
|
+
for (const column of columns) {
|
|
1277
|
+
for (const entry of column.voices) {
|
|
1278
|
+
if (entry.trackId !== trackId)
|
|
1279
|
+
continue;
|
|
1280
|
+
const stave = entry.voice.getStave();
|
|
1281
|
+
if (stave && !visibleStaves.has(stave))
|
|
1282
|
+
continue;
|
|
1283
|
+
entry.voice.draw(vexCtx);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
this.drawBeamsAndTuplets(columns, vexCtx, options, visibleStaves, id => id === trackId);
|
|
1287
|
+
}
|
|
1288
|
+
/** Beams, then tuplet brackets, for the tracks `include` admits. */
|
|
1289
|
+
drawBeamsAndTuplets(columns, vexCtx, options, visibleStaves, include) {
|
|
1079
1290
|
for (const column of columns) {
|
|
1080
1291
|
for (const { beam, trackId, stave } of column.beams) {
|
|
1081
|
-
if (!visibleStaves.has(stave))
|
|
1292
|
+
if (!include(trackId) || !visibleStaves.has(stave))
|
|
1082
1293
|
continue;
|
|
1083
1294
|
const color = this.trackColor(this.notesDimmed(trackId, options), options);
|
|
1084
1295
|
beam.setStyle({ fillStyle: color, strokeStyle: color });
|
|
@@ -1093,7 +1304,7 @@ export class CanvasScoreRenderer {
|
|
|
1093
1304
|
*/
|
|
1094
1305
|
for (const column of columns) {
|
|
1095
1306
|
for (const { tuplet, trackId, stave } of column.tuplets) {
|
|
1096
|
-
if (!visibleStaves.has(stave))
|
|
1307
|
+
if (!include(trackId) || !visibleStaves.has(stave))
|
|
1097
1308
|
continue;
|
|
1098
1309
|
const color = this.trackColor(this.notesDimmed(trackId, options), options);
|
|
1099
1310
|
tuplet.setStyle({ fillStyle: color, strokeStyle: color });
|
|
@@ -1101,25 +1312,6 @@ export class CanvasScoreRenderer {
|
|
|
1101
1312
|
tuplet.draw();
|
|
1102
1313
|
}
|
|
1103
1314
|
}
|
|
1104
|
-
if (plan.tracks.length > 1) {
|
|
1105
|
-
const topStave = firstStaveByTrack.get(0);
|
|
1106
|
-
const bottomStave = firstStaveByTrack.get(plan.tracks.length - 1);
|
|
1107
|
-
// Culled like everything else. Without this the brace was drawn even
|
|
1108
|
-
// when both of the staves it joins were scrolled off — on a tall score
|
|
1109
|
-
// it stretched thousands of pixels and the slice inside the viewport
|
|
1110
|
-
// read as a stray vertical mark in the corner rather than as notation.
|
|
1111
|
-
if (topStave &&
|
|
1112
|
-
bottomStave &&
|
|
1113
|
-
topStave !== bottomStave &&
|
|
1114
|
-
visibleStaves.has(topStave) &&
|
|
1115
|
-
visibleStaves.has(bottomStave)) {
|
|
1116
|
-
const connector = new StaveConnector(topStave, bottomStave);
|
|
1117
|
-
connector.setType('brace');
|
|
1118
|
-
connector.setContext(vexCtx);
|
|
1119
|
-
connector.draw();
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
this.drawMeasureGutter(system, plan, ctx, windowIndices, options);
|
|
1123
1315
|
}
|
|
1124
1316
|
/**
|
|
1125
1317
|
* Tints every selected measure across the full height of the system.
|