@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.
Files changed (63) hide show
  1. package/dist/canvas-renderer.d.ts +79 -4
  2. package/dist/canvas-renderer.d.ts.map +1 -1
  3. package/dist/canvas-renderer.js +245 -53
  4. package/dist/canvas-renderer.js.map +1 -1
  5. package/dist/cursor.d.ts +60 -0
  6. package/dist/cursor.d.ts.map +1 -0
  7. package/dist/cursor.js +157 -0
  8. package/dist/cursor.js.map +1 -0
  9. package/dist/index.d.ts +16 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +16 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/keyboard-geometry.d.ts +50 -2
  14. package/dist/keyboard-geometry.d.ts.map +1 -1
  15. package/dist/keyboard-geometry.js +71 -2
  16. package/dist/keyboard-geometry.js.map +1 -1
  17. package/dist/keyboard-keys.d.ts +93 -0
  18. package/dist/keyboard-keys.d.ts.map +1 -0
  19. package/dist/keyboard-keys.js +60 -0
  20. package/dist/keyboard-keys.js.map +1 -0
  21. package/dist/layered-paint.d.ts +47 -0
  22. package/dist/layered-paint.d.ts.map +1 -0
  23. package/dist/layered-paint.js +22 -0
  24. package/dist/layered-paint.js.map +1 -0
  25. package/dist/pagination.d.ts +6 -2
  26. package/dist/pagination.d.ts.map +1 -1
  27. package/dist/pagination.js +4 -0
  28. package/dist/pagination.js.map +1 -1
  29. package/dist/playback-binding.d.ts +57 -0
  30. package/dist/playback-binding.d.ts.map +1 -0
  31. package/dist/playback-binding.js +110 -0
  32. package/dist/playback-binding.js.map +1 -0
  33. package/dist/playhead.d.ts.map +1 -1
  34. package/dist/playhead.js +22 -3
  35. package/dist/playhead.js.map +1 -1
  36. package/dist/press.d.ts +66 -0
  37. package/dist/press.d.ts.map +1 -0
  38. package/dist/press.js +64 -0
  39. package/dist/press.js.map +1 -0
  40. package/dist/print-plan.d.ts +64 -0
  41. package/dist/print-plan.d.ts.map +1 -0
  42. package/dist/print-plan.js +101 -0
  43. package/dist/print-plan.js.map +1 -0
  44. package/dist/render-theme.d.ts.map +1 -1
  45. package/dist/render-theme.js +11 -2
  46. package/dist/render-theme.js.map +1 -1
  47. package/dist/score-canvas.d.ts +280 -0
  48. package/dist/score-canvas.d.ts.map +1 -0
  49. package/dist/score-canvas.js +702 -0
  50. package/dist/score-canvas.js.map +1 -0
  51. package/dist/skia/index.d.ts +2 -0
  52. package/dist/skia/index.d.ts.map +1 -1
  53. package/dist/skia/index.js +1 -0
  54. package/dist/skia/index.js.map +1 -1
  55. package/dist/skia/skia-layers.d.ts +43 -0
  56. package/dist/skia/skia-layers.d.ts.map +1 -0
  57. package/dist/skia/skia-layers.js +24 -0
  58. package/dist/skia/skia-layers.js.map +1 -0
  59. package/dist/types.d.ts +13 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/types.js +1 -1
  62. package/dist/types.js.map +1 -1
  63. 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 a note starts sounding or the selection moves — cheap beside the
264
- * building it no longer has to repeat.
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 paintSystem;
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":"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"}
1
+ {"version":3,"file":"canvas-renderer.d.ts","sourceRoot":"","sources":["../src/canvas-renderer.ts"],"names":[],"mappings":"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"}
@@ -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
- const { drawings, channelsByTrack, measureIdToBBox, measureNotePositions, drawnMeasureIndices, } = frame;
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
- for (const drawing of drawings) {
414
+ const visibleStaves = [];
415
+ for (const drawing of frame.drawings) {
416
+ const onScreen = new Set();
417
+ visibleStaves.push(onScreen);
322
418
  try {
323
- this.paintSystem(drawing, plan, vexCtx, ctx, options, drawnStaves, drawnTrackIds);
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
- for (const { span, trackId, what } of frame.spans) {
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
- // After painting, not before — see `ColumnDrawing.bboxes`.
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
- for (const drawing of drawings)
356
- for (const column of drawing.columns)
357
- this.collectColumnBBoxes(column, z, drawnStaves, drawnTrackIds, idToBBox);
358
- // Last, so it overlays any content that scrolled underneath it — and not
359
- // at all for print, where there is nothing to click.
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
- column.voices.push(...voiceGroups.flat());
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 a note starts sounding or the selection moves — cheap beside the
991
- * building it no longer has to repeat.
1142
+ * again when the selection moves — cheap beside the building it no longer
1143
+ * has to repeat.
992
1144
  */
993
- paintSystem(drawing, plan, vexCtx,
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
- this.styleColumnTrackNotes(column, trackId, options);
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.