@coderline/alphatab 1.8.0-alpha.1647 → 1.8.0-alpha.1651

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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * alphaTab v1.8.0-alpha.1647 (develop, build 1647)
2
+ * alphaTab v1.8.0-alpha.1651 (develop, build 1651)
3
3
  *
4
4
  * Copyright © 2025, Daniel Kuschny and Contributors, All rights reserved.
5
5
  *
@@ -203,9 +203,9 @@ class AlphaTabError extends Error {
203
203
  * @internal
204
204
  */
205
205
  class VersionInfo {
206
- static version = '1.8.0-alpha.1647';
207
- static date = '2025-12-17T02:10:13.730Z';
208
- static commit = 'a1bd11d4a3eef22409f1f49a5dd172ace16108cc';
206
+ static version = '1.8.0-alpha.1651';
207
+ static date = '2025-12-21T02:27:24.035Z';
208
+ static commit = '758ae890dfa8cdb21a9dd469ade012f834ac0433';
209
209
  static print(print) {
210
210
  print(`alphaTab ${VersionInfo.version}`);
211
211
  print(`commit: ${VersionInfo.commit}`);
@@ -2744,6 +2744,23 @@ class RenderStylesheet {
2744
2744
  * Whether barlines should be drawn across staves within the same system.
2745
2745
  */
2746
2746
  extendBarLines = false;
2747
+ /**
2748
+ * Whether to hide empty staves.
2749
+ */
2750
+ hideEmptyStaves = false;
2751
+ /**
2752
+ * Whether to also hide empty staves in the first system.
2753
+ * @remarks
2754
+ * Only has an effect when activating {@link hideEmptyStaves}.
2755
+ */
2756
+ hideEmptyStavesInFirstSystem = false;
2757
+ /**
2758
+ * Whether to show brackets and braces across single staves.
2759
+ * @remarks
2760
+ * This allows a more consistent view for identifying staves when using
2761
+ * {@link hideEmptyStaves}
2762
+ */
2763
+ showSingleStaffBrackets = false;
2747
2764
  }
2748
2765
 
2749
2766
  /**
@@ -8673,7 +8690,10 @@ class AlphaTex1LanguageDefinitions {
8673
8690
  ['firstsystemtracknameorientation', [[[[10, 17], 0, ['horizontal', 'vertical']]]]],
8674
8691
  ['othersystemstracknameorientation', [[[[10, 17], 0, ['horizontal', 'vertical']]]]],
8675
8692
  ['extendbarlines', null],
8676
- ['chorddiagramsinscore', [[[[10], 1, ['true', 'false']]]]]
8693
+ ['chorddiagramsinscore', [[[[10], 1, ['true', 'false']]]]],
8694
+ ['hideemptystaves', null],
8695
+ ['hideemptystavesinfirstsystem', null],
8696
+ ['showsinglestaffbrackets', null]
8677
8697
  ]);
8678
8698
  static staffMetaDataSignatures = AlphaTex1LanguageDefinitions._signatures([
8679
8699
  ['tuning', [[[[10, 17], 0, ['piano', 'none', 'voice']]], [[[10, 17], 5]]]],
@@ -9013,6 +9033,9 @@ class AlphaTex1LanguageDefinitions {
9013
9033
  ['othersystemstracknameorientation', null],
9014
9034
  ['extendbarlines', null],
9015
9035
  ['chorddiagramsinscore', null],
9036
+ ['hideemptystaves', null],
9037
+ ['hideemptystavesinfirstsystem', null],
9038
+ ['showsinglestaffbrackets', null],
9016
9039
  [
9017
9040
  'tuning',
9018
9041
  [
@@ -13066,7 +13089,7 @@ class Bounds {
13066
13089
  */
13067
13090
  class MasterBarBounds {
13068
13091
  /**
13069
- * Gets or sets the index of this bounds relative within the parent lookup.
13092
+ * The MasterBar index within the data model represented by these bounds.
13070
13093
  */
13071
13094
  index = 0;
13072
13095
  /**
@@ -13274,6 +13297,7 @@ class BoundsLookup {
13274
13297
  mb.visualBounds = this._boundsToJson(masterBar.visualBounds);
13275
13298
  mb.realBounds = this._boundsToJson(masterBar.realBounds);
13276
13299
  mb.index = masterBar.index;
13300
+ mb.isFirstOfLine = masterBar.isFirstOfLine;
13277
13301
  mb.bars = [];
13278
13302
  for (const bar of masterBar.bars) {
13279
13303
  const b = {};
@@ -13330,7 +13354,7 @@ class BoundsLookup {
13330
13354
  mb.lineAlignedBounds = BoundsLookup._boundsFromJson(masterBar.lineAlignedBounds);
13331
13355
  mb.visualBounds = BoundsLookup._boundsFromJson(masterBar.visualBounds);
13332
13356
  mb.realBounds = BoundsLookup._boundsFromJson(masterBar.realBounds);
13333
- sg.addBar(mb);
13357
+ lookup.addMasterBar(mb);
13334
13358
  for (const bar of masterBar.bars) {
13335
13359
  const b = new BarBounds();
13336
13360
  b.visualBounds = BoundsLookup._boundsFromJson(bar.visualBounds);
@@ -13887,6 +13911,15 @@ class AlphaTex1LanguageHandler {
13887
13911
  ? AlphaTex1LanguageHandler._booleanLikeValue(metaData.arguments.arguments, 0)
13888
13912
  : true;
13889
13913
  return ApplyNodeResult.Applied;
13914
+ case 'hideemptystaves':
13915
+ score.stylesheet.hideEmptyStaves = true;
13916
+ return ApplyNodeResult.Applied;
13917
+ case 'hideemptystavesinfirstsystem':
13918
+ score.stylesheet.hideEmptyStavesInFirstSystem = true;
13919
+ return ApplyNodeResult.Applied;
13920
+ case 'showsinglestaffbrackets':
13921
+ score.stylesheet.showSingleStaffBrackets = true;
13922
+ return ApplyNodeResult.Applied;
13890
13923
  default:
13891
13924
  return ApplyNodeResult.NotAppliedUnrecognizedMarker;
13892
13925
  }
@@ -15718,6 +15751,15 @@ class AlphaTex1LanguageHandler {
15718
15751
  if (stylesheet.globalDisplayChordDiagramsInScore) {
15719
15752
  nodes.push(Atnf.meta('chordDiagramsInScore'));
15720
15753
  }
15754
+ if (stylesheet.hideEmptyStaves) {
15755
+ nodes.push(Atnf.meta('hideEmptyStaves'));
15756
+ }
15757
+ if (stylesheet.hideEmptyStavesInFirstSystem) {
15758
+ nodes.push(Atnf.meta('hideEmptyStavesInFirstSystem'));
15759
+ }
15760
+ if (stylesheet.showSingleStaffBrackets) {
15761
+ nodes.push(Atnf.meta('showSingleStaffBrackets'));
15762
+ }
15721
15763
  // Unsupported:
15722
15764
  // 'globaldisplaychorddiagramsontop',
15723
15765
  // 'pertrackchorddiagramsontop',
@@ -38804,6 +38846,9 @@ class RenderStylesheetSerializer {
38804
38846
  }
38805
38847
  }
38806
38848
  o.set("extendbarlines", obj.extendBarLines);
38849
+ o.set("hideemptystaves", obj.hideEmptyStaves);
38850
+ o.set("hideemptystavesinfirstsystem", obj.hideEmptyStavesInFirstSystem);
38851
+ o.set("showsinglestaffbrackets", obj.showSingleStaffBrackets);
38807
38852
  return o;
38808
38853
  }
38809
38854
  static setProperty(obj, property, v) {
@@ -38865,6 +38910,15 @@ class RenderStylesheetSerializer {
38865
38910
  case "extendbarlines":
38866
38911
  obj.extendBarLines = v;
38867
38912
  return true;
38913
+ case "hideemptystaves":
38914
+ obj.hideEmptyStaves = v;
38915
+ return true;
38916
+ case "hideemptystavesinfirstsystem":
38917
+ obj.hideEmptyStavesInFirstSystem = v;
38918
+ return true;
38919
+ case "showsinglestaffbrackets":
38920
+ obj.showSingleStaffBrackets = v;
38921
+ return true;
38868
38922
  }
38869
38923
  return false;
38870
38924
  }
@@ -43604,6 +43658,12 @@ var ScrollMode;
43604
43658
  * Scrolling happens as soon the cursors exceed the displayed range.
43605
43659
  */
43606
43660
  ScrollMode[ScrollMode["OffScreen"] = 2] = "OffScreen";
43661
+ /**
43662
+ * Scrolling happens constantly in a smooth fashion.
43663
+ * This will disable the use of any native scroll optimizations but
43664
+ * manually scroll the scroll container in the required speed.
43665
+ */
43666
+ ScrollMode[ScrollMode["Smooth"] = 3] = "Smooth";
43607
43667
  })(ScrollMode || (ScrollMode = {}));
43608
43668
  /**
43609
43669
  * This object defines the details on how to generate the vibrato effects.
@@ -45892,6 +45952,19 @@ class BeatTickLookup {
45892
45952
  }
45893
45953
  return null;
45894
45954
  }
45955
+ /**
45956
+ * Looks for the first visible beat which starts at this lookup so it can be used for cursor placement.
45957
+ * @param checker The custom checker to see if a beat is visible.
45958
+ * @returns The first beat which is visible according to the given tracks or null.
45959
+ */
45960
+ getVisibleBeatAtStartWithChecker(checker) {
45961
+ for (const b of this.highlightedBeats) {
45962
+ if (b.playbackStart === this.start && checker.isVisible(b.beat)) {
45963
+ return b.beat;
45964
+ }
45965
+ }
45966
+ return null;
45967
+ }
45895
45968
  }
45896
45969
 
45897
45970
  /**
@@ -46331,6 +46404,18 @@ class MidiTickLookupFindBeatResult {
46331
46404
  }
46332
46405
  }
46333
46406
  }
46407
+ /**
46408
+ * @internal
46409
+ */
46410
+ class TrackLookupBeatVisibilityChecker {
46411
+ _lookup;
46412
+ constructor(lookup) {
46413
+ this._lookup = lookup;
46414
+ }
46415
+ isVisible(beat) {
46416
+ return this._lookup.has(beat.voice.bar.staff.track.index);
46417
+ }
46418
+ }
46334
46419
  /**
46335
46420
  * This class holds all information about when {@link MasterBar}s and {@link Beat}s are played.
46336
46421
  *
@@ -46391,31 +46476,44 @@ class MidiTickLookup {
46391
46476
  * @returns The information about the current beat or null if no beat could be found.
46392
46477
  */
46393
46478
  findBeat(trackLookup, tick, currentBeatHint = null) {
46479
+ return this.findBeatWithChecker(new TrackLookupBeatVisibilityChecker(trackLookup), tick, currentBeatHint);
46480
+ }
46481
+ /**
46482
+ * Finds the currently played beat given a list of tracks and the current time.
46483
+ * @param checker The checker to ask whether a beat is visible and should be considered for result.
46484
+ * @param tick The current time in midi ticks.
46485
+ * @param currentBeatHint Used for optimized lookup during playback. By passing in a previous result lookup of the next one can be optimized using heuristics. (optional).
46486
+ * @returns The information about the current beat or null if no beat could be found.
46487
+ */
46488
+ findBeatWithChecker(checker, tick, currentBeatHint = null) {
46394
46489
  let result = null;
46395
46490
  if (currentBeatHint) {
46396
- result = this._findBeatFast(trackLookup, currentBeatHint, tick);
46491
+ result = this._findBeatFast(checker, currentBeatHint, tick);
46397
46492
  }
46398
46493
  if (!result) {
46399
- result = this._findBeatSlow(trackLookup, currentBeatHint, tick, false);
46494
+ result = this._findBeatSlow(checker, currentBeatHint, tick, false);
46400
46495
  }
46401
46496
  return result;
46402
46497
  }
46403
- _findBeatFast(trackLookup, currentBeatHint, tick) {
46498
+ _findBeatFast(checker, currentBeatHint, tick) {
46404
46499
  // still within current lookup.
46405
46500
  if (tick >= currentBeatHint.start && tick < currentBeatHint.end) {
46406
46501
  return currentBeatHint;
46407
46502
  }
46408
46503
  // already on the next beat?
46409
- if (currentBeatHint.nextBeat && tick >= currentBeatHint.nextBeat.start && tick < currentBeatHint.nextBeat.end) {
46504
+ if (currentBeatHint.nextBeat &&
46505
+ tick >= currentBeatHint.nextBeat.start &&
46506
+ tick < currentBeatHint.nextBeat.end &&
46507
+ (checker === undefined || checker.isVisible(currentBeatHint.nextBeat.beat))) {
46410
46508
  const next = currentBeatHint.nextBeat;
46411
46509
  // fill next in chain
46412
- this._fillNextBeat(next, trackLookup);
46510
+ this._fillNextBeat(next, checker);
46413
46511
  return next;
46414
46512
  }
46415
46513
  // likely a loop or manual seek, need to fallback to slow path
46416
46514
  return null;
46417
46515
  }
46418
- _fillNextBeatMultiBarRest(current, trackLookup) {
46516
+ _fillNextBeatMultiBarRest(current, checker) {
46419
46517
  const group = this.multiBarRestInfo.get(current.masterBar.masterBar.index);
46420
46518
  // this is a bit sensitive. we assume that the sequence of multi-rest bars and the
46421
46519
  // chained nextMasterBar equal. so we just jump over X bars
@@ -46429,7 +46527,7 @@ class MidiTickLookup {
46429
46527
  if (endMasterBar) {
46430
46528
  // one more following -> use start of next
46431
46529
  if (endMasterBar.nextMasterBar) {
46432
- current.nextBeat = this._firstBeatInMasterBar(trackLookup, endMasterBar.nextMasterBar, endMasterBar.nextMasterBar.start, true);
46530
+ current.nextBeat = this._firstBeatInMasterBar(checker, endMasterBar.nextMasterBar, endMasterBar.nextMasterBar.start, true);
46433
46531
  // if we have the next beat take the difference between the times as duration
46434
46532
  if (current.nextBeat) {
46435
46533
  current.tickDuration = current.nextBeat.start - current.start;
@@ -46459,19 +46557,19 @@ class MidiTickLookup {
46459
46557
  }
46460
46558
  current.calculateDuration();
46461
46559
  }
46462
- _fillNextBeat(current, trackLookup) {
46560
+ _fillNextBeat(current, checker) {
46463
46561
  // on multibar rests take the duration until the end.
46464
46562
  if (this._isMultiBarRestResult(current)) {
46465
- this._fillNextBeatMultiBarRest(current, trackLookup);
46563
+ this._fillNextBeatMultiBarRest(current, checker);
46466
46564
  }
46467
46565
  else {
46468
- this._fillNextBeatDefault(current, trackLookup);
46566
+ this._fillNextBeatDefault(current, checker);
46469
46567
  }
46470
46568
  }
46471
- _fillNextBeatDefault(current, trackLookup) {
46472
- current.nextBeat = this._findBeatInMasterBar(current.masterBar, current.beatLookup.nextBeat, current.end, trackLookup, true);
46569
+ _fillNextBeatDefault(current, checker) {
46570
+ current.nextBeat = this._findBeatInMasterBar(current.masterBar, current.beatLookup.nextBeat, current.end, checker, true);
46473
46571
  if (current.nextBeat == null) {
46474
- current.nextBeat = this._findBeatSlow(trackLookup, current, current.end, true);
46572
+ current.nextBeat = this._findBeatSlow(checker, current, current.end, true);
46475
46573
  }
46476
46574
  // if we have the next beat take the difference between the times as duration
46477
46575
  if (current.nextBeat) {
@@ -46502,7 +46600,7 @@ class MidiTickLookup {
46502
46600
  beat.isRest &&
46503
46601
  beat.voice.bar.isRestOnly);
46504
46602
  }
46505
- _findBeatSlow(trackLookup, currentBeatHint, tick, isNextSearch) {
46603
+ _findBeatSlow(checker, currentBeatHint, tick, isNextSearch) {
46506
46604
  // get all beats within the masterbar
46507
46605
  let masterBar = null;
46508
46606
  if (currentBeatHint != null) {
@@ -46524,14 +46622,14 @@ class MidiTickLookup {
46524
46622
  if (!masterBar) {
46525
46623
  return null;
46526
46624
  }
46527
- return this._firstBeatInMasterBar(trackLookup, masterBar, tick, isNextSearch);
46625
+ return this._firstBeatInMasterBar(checker, masterBar, tick, isNextSearch);
46528
46626
  }
46529
- _firstBeatInMasterBar(trackLookup, startMasterBar, tick, isNextSearch) {
46627
+ _firstBeatInMasterBar(checker, startMasterBar, tick, isNextSearch) {
46530
46628
  let masterBar = startMasterBar;
46531
46629
  // scan through beats and find first one which has a beat visible
46532
46630
  while (masterBar) {
46533
46631
  if (masterBar.firstBeat) {
46534
- const beat = this._findBeatInMasterBar(masterBar, masterBar.firstBeat, tick, trackLookup, isNextSearch);
46632
+ const beat = this._findBeatInMasterBar(masterBar, masterBar.firstBeat, tick, checker, isNextSearch);
46535
46633
  if (beat) {
46536
46634
  return beat;
46537
46635
  }
@@ -46549,7 +46647,7 @@ class MidiTickLookup {
46549
46647
  * @param isNextSearch
46550
46648
  * @returns
46551
46649
  */
46552
- _findBeatInMasterBar(masterBar, currentStartLookup, tick, visibleTracks, isNextSearch) {
46650
+ _findBeatInMasterBar(masterBar, currentStartLookup, tick, checker, isNextSearch) {
46553
46651
  if (!currentStartLookup) {
46554
46652
  return null;
46555
46653
  }
@@ -46563,7 +46661,7 @@ class MidiTickLookup {
46563
46661
  (currentStartLookup.start <= relativeTick || (isNextSearch && relativeTick < 0)) &&
46564
46662
  relativeTick < currentStartLookup.end) {
46565
46663
  startBeatLookup = currentStartLookup;
46566
- startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks);
46664
+ startBeat = currentStartLookup.getVisibleBeatAtStartWithChecker(checker);
46567
46665
  // found the matching beat lookup but none of the beats are visible
46568
46666
  // in this case scan further to the next lookup which has any visible beat
46569
46667
  if (!startBeat) {
@@ -46571,7 +46669,7 @@ class MidiTickLookup {
46571
46669
  let currentMasterBar = masterBar;
46572
46670
  while (currentMasterBar != null && startBeat == null) {
46573
46671
  while (currentStartLookup != null) {
46574
- startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks);
46672
+ startBeat = currentStartLookup.getVisibleBeatAtStartWithChecker(checker);
46575
46673
  if (startBeat) {
46576
46674
  startBeatLookup = currentStartLookup;
46577
46675
  masterBar = currentMasterBar;
@@ -46589,7 +46687,7 @@ class MidiTickLookup {
46589
46687
  let currentMasterBar = masterBar;
46590
46688
  while (currentMasterBar != null && startBeat == null) {
46591
46689
  while (currentStartLookup != null) {
46592
- startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks);
46690
+ startBeat = currentStartLookup.getVisibleBeatAtStartWithChecker(checker);
46593
46691
  if (startBeat) {
46594
46692
  startBeatLookup = currentStartLookup;
46595
46693
  masterBar = currentMasterBar;
@@ -46613,17 +46711,17 @@ class MidiTickLookup {
46613
46711
  if (startBeat == null) {
46614
46712
  return null;
46615
46713
  }
46616
- const result = this._createResult(masterBar, startBeatLookup, startBeat, isNextSearch, visibleTracks);
46714
+ const result = this._createResult(masterBar, startBeatLookup, startBeat, isNextSearch, checker);
46617
46715
  return result;
46618
46716
  }
46619
- _createResult(masterBar, beatLookup, beat, isNextSearch, visibleTracks) {
46717
+ _createResult(masterBar, beatLookup, beat, isNextSearch, checker) {
46620
46718
  const result = new MidiTickLookupFindBeatResult(masterBar);
46621
46719
  result.beat = beat;
46622
46720
  result.beatLookup = beatLookup;
46623
46721
  result.tickDuration = beatLookup.end - beatLookup.start;
46624
46722
  if (!isNextSearch) {
46625
46723
  // the next beat filling will adjust this result with the respective durations
46626
- this._fillNextBeat(result, visibleTracks);
46724
+ this._fillNextBeat(result, checker);
46627
46725
  }
46628
46726
  else if (this._isMultiBarRestResult(result)) {
46629
46727
  const multiRest = this.multiBarRestInfo.get(masterBar.masterBar.index);
@@ -48496,6 +48594,25 @@ class MidiFileGenerator {
48496
48594
  }
48497
48595
  }
48498
48596
 
48597
+ /**
48598
+ * Represents the information related to a resize event.
48599
+ * @public
48600
+ */
48601
+ class ResizeEventArgs {
48602
+ /**
48603
+ * Gets the size before the resizing happened.
48604
+ */
48605
+ oldWidth = 0;
48606
+ /**
48607
+ * Gets the size after the resize was complete.
48608
+ */
48609
+ newWidth = 0;
48610
+ /**
48611
+ * Gets the settings currently used for rendering.
48612
+ */
48613
+ settings = null;
48614
+ }
48615
+
48499
48616
  /**
48500
48617
  * Lists the different position modes for {@link BarRendererBase.getBeatX}
48501
48618
  * @internal
@@ -48922,22 +49039,262 @@ class ScoreRendererWrapper {
48922
49039
  }
48923
49040
 
48924
49041
  /**
48925
- * Represents the information related to a resize event.
48926
- * @public
49042
+ * Some basic scroll handler checking for changed offsets and scroll if changed.
49043
+ * @internal
48927
49044
  */
48928
- class ResizeEventArgs {
48929
- /**
48930
- * Gets the size before the resizing happened.
48931
- */
48932
- oldWidth = 0;
48933
- /**
48934
- * Gets the size after the resize was complete.
48935
- */
48936
- newWidth = 0;
48937
- /**
48938
- * Gets the settings currently used for rendering.
48939
- */
48940
- settings = null;
49045
+ class BasicScrollHandler {
49046
+ api;
49047
+ lastScroll = -1;
49048
+ constructor(api) {
49049
+ this.api = api;
49050
+ }
49051
+ [Symbol.dispose]() {
49052
+ }
49053
+ forceScrollTo(currentBeatBounds) {
49054
+ this._scrollToBeat(currentBeatBounds, true);
49055
+ this.lastScroll = -1; // force new scroll on next update
49056
+ }
49057
+ _scrollToBeat(currentBeatBounds, force) {
49058
+ const newLastScroll = this.calculateLastScroll(currentBeatBounds);
49059
+ // no change, and no instant/force scroll
49060
+ if (newLastScroll === this.lastScroll && !force) {
49061
+ return;
49062
+ }
49063
+ this.lastScroll = newLastScroll;
49064
+ this.doScroll(currentBeatBounds);
49065
+ }
49066
+ onBeatCursorUpdating(startBeat, _endBeat, _cursorMode, _actualBeatCursorStartX, _actualBeatCursorEndX, _actualBeatCursorTransitionDuration) {
49067
+ this._scrollToBeat(startBeat, false);
49068
+ }
49069
+ }
49070
+ /**
49071
+ * This is the default scroll handler for vertical layouts using {@link ScrollMode.Continuous}.
49072
+ * Whenever the system changes, we scroll to the new system position vertically.
49073
+ * @internal
49074
+ */
49075
+ class VerticalContinuousScrollHandler extends BasicScrollHandler {
49076
+ calculateLastScroll(currentBeatBounds) {
49077
+ return currentBeatBounds.barBounds.masterBarBounds.realBounds.y;
49078
+ }
49079
+ doScroll(currentBeatBounds) {
49080
+ const ui = this.api.uiFacade;
49081
+ const settings = this.api.settings;
49082
+ const scroll = ui.getScrollContainer();
49083
+ const elementOffset = ui.getOffset(scroll, this.api.container);
49084
+ const y = currentBeatBounds.barBounds.masterBarBounds.realBounds.y + settings.player.scrollOffsetY;
49085
+ ui.scrollToY(scroll, elementOffset.y + y, this.api.settings.player.scrollSpeed);
49086
+ }
49087
+ }
49088
+ /**
49089
+ * This is the default scroll handler for vertical layouts using {@link ScrollMode.OffScreen}.
49090
+ * Whenever the system changes, we check if the new system bounds are out-of-screen and if yes, we scroll.
49091
+ * @internal
49092
+ */
49093
+ class VerticalOffScreenScrollHandler extends BasicScrollHandler {
49094
+ calculateLastScroll(currentBeatBounds) {
49095
+ // check for system change
49096
+ return currentBeatBounds.barBounds.masterBarBounds.realBounds.y;
49097
+ }
49098
+ doScroll(currentBeatBounds) {
49099
+ const ui = this.api.uiFacade;
49100
+ const settings = this.api.settings;
49101
+ const scroll = ui.getScrollContainer();
49102
+ const elementBottom = scroll.scrollTop + ui.getOffset(null, scroll).h;
49103
+ const barBoundings = currentBeatBounds.barBounds.masterBarBounds;
49104
+ if (barBoundings.visualBounds.y + barBoundings.visualBounds.h >= elementBottom ||
49105
+ barBoundings.visualBounds.y < scroll.scrollTop) {
49106
+ const scrollTop = barBoundings.realBounds.y + settings.player.scrollOffsetY;
49107
+ ui.scrollToY(scroll, scrollTop, settings.player.scrollSpeed);
49108
+ }
49109
+ }
49110
+ }
49111
+ /**
49112
+ * This is the default scroll handler for vertical layouts using {@link ScrollMode.Smooth}.
49113
+ * vertical smooth scrolling aims to place the on-time position
49114
+ * at scrollOffsetY **at the time when a system starts**
49115
+ * this means when a system starts, it is at scrollOffsetY,
49116
+ * then gradually scrolls down the system height reaching the bottom
49117
+ * when the system completes.
49118
+ * @internal
49119
+ */
49120
+ class VerticalSmoothScrollHandler {
49121
+ _api;
49122
+ _lastScroll = -1;
49123
+ _scrollContainerResizeUnregister;
49124
+ constructor(api) {
49125
+ this._api = api;
49126
+ // we need a resize listener for the overflow calculation
49127
+ this._scrollContainerResizeUnregister = api.uiFacade.getScrollContainer().resize.on(() => {
49128
+ const scrollContainer = api.uiFacade.getScrollContainer();
49129
+ const overflowNeeded = api.settings.player.scrollOffsetX;
49130
+ const viewPortSize = scrollContainer.width;
49131
+ // the content needs to shift out of screen (and back into screen with the offset)
49132
+ // that's why we need the whole width as additional overflow
49133
+ const overflowNeededAbsolute = viewPortSize + overflowNeeded;
49134
+ api.uiFacade.setCanvasOverflow(api.canvasElement, overflowNeededAbsolute, true);
49135
+ });
49136
+ }
49137
+ [Symbol.dispose]() {
49138
+ this._api.uiFacade.setCanvasOverflow(this._api.canvasElement, 0, true);
49139
+ this._scrollContainerResizeUnregister();
49140
+ }
49141
+ forceScrollTo(currentBeatBounds) {
49142
+ const ui = this._api.uiFacade;
49143
+ const settings = this._api.settings;
49144
+ const scroll = ui.getScrollContainer();
49145
+ const systemTop = currentBeatBounds.barBounds.masterBarBounds.realBounds.y + settings.player.scrollOffsetY;
49146
+ ui.scrollToY(scroll, systemTop, 0);
49147
+ this._lastScroll = -1;
49148
+ }
49149
+ onBeatCursorUpdating(startBeat, _endBeat, _cursorMode, _actualBeatCursorStartX, _actualBeatCursorEndX, actualBeatCursorTransitionDuration) {
49150
+ const ui = this._api.uiFacade;
49151
+ const settings = this._api.settings;
49152
+ const barBoundings = startBeat.barBounds.masterBarBounds;
49153
+ const systemTop = barBoundings.realBounds.y + settings.player.scrollOffsetY;
49154
+ if (systemTop === this._lastScroll && actualBeatCursorTransitionDuration > 0) {
49155
+ return;
49156
+ }
49157
+ // jump to start of new system
49158
+ const scroll = ui.getScrollContainer();
49159
+ ui.scrollToY(scroll, systemTop, 0);
49160
+ // instant scroll
49161
+ if (actualBeatCursorTransitionDuration === 0) {
49162
+ this._lastScroll = -1;
49163
+ return;
49164
+ }
49165
+ // dynamic scrolling
49166
+ this._lastScroll = systemTop;
49167
+ // scroll to bottom over time
49168
+ const systemBottom = systemTop + barBoundings.realBounds.h;
49169
+ // NOTE: this calculation is a bit more expensive, but we only do it once per system
49170
+ // so we should be good:
49171
+ // * the more bars we have, the longer the system will play, hence the duration can take a bit longer
49172
+ // * if we have less bars, we calculate more often, but the calculation will be faster because we sum up less bars.
49173
+ const systemDuration = this._calculateSystemDuration(barBoundings);
49174
+ ui.scrollToY(scroll, systemBottom, systemDuration);
49175
+ }
49176
+ _calculateSystemDuration(barBoundings) {
49177
+ const systemBars = barBoundings.staffSystemBounds.bars;
49178
+ const tickCache = this._api.tickCache;
49179
+ let duration = 0;
49180
+ const masterBars = this._api.score.masterBars;
49181
+ for (const bar of systemBars) {
49182
+ const mb = masterBars[bar.index];
49183
+ const mbInfo = tickCache.getMasterBar(mb);
49184
+ const tempoChanges = tickCache.getMasterBar(mb).tempoChanges;
49185
+ let tempo = tempoChanges[0].tempo;
49186
+ let tick = tempoChanges[0].tick;
49187
+ for (let i = 1; i < tempoChanges.length; i++) {
49188
+ const diff = tempoChanges[i].tick - tick;
49189
+ duration += MidiUtils.ticksToMillis(diff, tempo);
49190
+ tempo = tempoChanges[i].tempo;
49191
+ tick = tempoChanges[i].tick;
49192
+ }
49193
+ const toEnd = mbInfo.end - tick;
49194
+ duration += MidiUtils.ticksToMillis(toEnd, tempo);
49195
+ }
49196
+ return duration;
49197
+ }
49198
+ }
49199
+ /**
49200
+ * This is the default scroll handler for horizontal layouts using {@link ScrollMode.Continuous}.
49201
+ * Whenever the master bar changes, we scroll to the position horizontally.
49202
+ * @internal
49203
+ */
49204
+ class HorizontalContinuousScrollHandler extends BasicScrollHandler {
49205
+ calculateLastScroll(currentBeatBounds) {
49206
+ return currentBeatBounds.barBounds.masterBarBounds.visualBounds.x;
49207
+ }
49208
+ doScroll(currentBeatBounds) {
49209
+ const ui = this.api.uiFacade;
49210
+ const settings = this.api.settings;
49211
+ const scroll = ui.getScrollContainer();
49212
+ const barBoundings = currentBeatBounds.barBounds.masterBarBounds;
49213
+ const scrollLeftContinuous = barBoundings.realBounds.x + settings.player.scrollOffsetX;
49214
+ ui.scrollToX(scroll, scrollLeftContinuous, settings.player.scrollSpeed);
49215
+ }
49216
+ }
49217
+ /**
49218
+ * This is the default scroll handler for horizontal layouts using {@link ScrollMode.OffScreen}.
49219
+ * Whenever the system changes, we check if the new system bounds are out-of-screen and if yes, we scroll.
49220
+ * @internal
49221
+ */
49222
+ class HorizontalOffScreenScrollHandler extends BasicScrollHandler {
49223
+ calculateLastScroll(currentBeatBounds) {
49224
+ return currentBeatBounds.barBounds.masterBarBounds.visualBounds.x;
49225
+ }
49226
+ doScroll(currentBeatBounds) {
49227
+ const ui = this.api.uiFacade;
49228
+ const settings = this.api.settings;
49229
+ const scroll = ui.getScrollContainer();
49230
+ const elementRight = scroll.scrollLeft + ui.getOffset(null, scroll).w;
49231
+ const barBoundings = currentBeatBounds.barBounds.masterBarBounds;
49232
+ if (barBoundings.visualBounds.x + barBoundings.visualBounds.w >= elementRight ||
49233
+ barBoundings.visualBounds.x < scroll.scrollLeft) {
49234
+ const scrollLeftOffScreen = barBoundings.realBounds.x + settings.player.scrollOffsetX;
49235
+ ui.scrollToX(scroll, scrollLeftOffScreen, settings.player.scrollSpeed);
49236
+ }
49237
+ }
49238
+ }
49239
+ /**
49240
+ * This is the default scroll handler for horizontal layouts using {@link ScrollMode.Smooth}.
49241
+ * horiontal smooth scrolling aims to place the on-time position
49242
+ * at scrollOffsetX from a beat-to-beat perspective.
49243
+ * This achieves an steady cursor at the same position with rather the music sheet scrolling past it.
49244
+ * Due to some animation inconsistencies (e.g. CSS animation vs scrolling) there might be a slight
49245
+ * flickering of the cursor.
49246
+ *
49247
+ * To get a fully steady cursor the beat cursor can simply be visually hidden and a cursor can be placed at
49248
+ * `scrollOffsetX` by the integrator.
49249
+ * @internal
49250
+ */
49251
+ class HorizontalSmoothScrollHandler {
49252
+ _api;
49253
+ _lastScroll = -1;
49254
+ _scrollContainerResizeUnregister;
49255
+ constructor(api) {
49256
+ this._api = api;
49257
+ // we need a resize listener for the overflow calculation
49258
+ this._scrollContainerResizeUnregister = api.uiFacade.getScrollContainer().resize.on(() => {
49259
+ const scrollContainer = api.uiFacade.getScrollContainer();
49260
+ const overflowNeeded = api.settings.player.scrollOffsetX;
49261
+ const viewPortSize = scrollContainer.width;
49262
+ // the content needs to shift out of screen (and back into screen with the offset)
49263
+ // that's why we need the whole width as additional overflow
49264
+ const overflowNeededAbsolute = viewPortSize + overflowNeeded;
49265
+ api.uiFacade.setCanvasOverflow(api.canvasElement, overflowNeededAbsolute, false);
49266
+ });
49267
+ }
49268
+ [Symbol.dispose]() {
49269
+ this._scrollContainerResizeUnregister();
49270
+ this._api.uiFacade.setCanvasOverflow(this._api.canvasElement, 0, false);
49271
+ }
49272
+ forceScrollTo(currentBeatBounds) {
49273
+ const ui = this._api.uiFacade;
49274
+ const settings = this._api.settings;
49275
+ const scroll = ui.getScrollContainer();
49276
+ const barStartX = currentBeatBounds.onNotesX + settings.player.scrollOffsetY;
49277
+ ui.scrollToY(scroll, barStartX, 0);
49278
+ this._lastScroll = -1;
49279
+ }
49280
+ onBeatCursorUpdating(_startBeat, _endBeat, _cursorMode, actualBeatCursorStartX, actualBeatCursorEndX, actualBeatCursorTransitionDuration) {
49281
+ const ui = this._api.uiFacade;
49282
+ if (actualBeatCursorEndX === this._lastScroll && actualBeatCursorTransitionDuration > 0) {
49283
+ return;
49284
+ }
49285
+ // jump to start of new system
49286
+ const settings = this._api.settings;
49287
+ const scroll = ui.getScrollContainer();
49288
+ ui.scrollToX(scroll, actualBeatCursorStartX + settings.player.scrollOffsetX, 0);
49289
+ // instant scroll
49290
+ if (actualBeatCursorTransitionDuration === 0) {
49291
+ this._lastScroll = -1;
49292
+ return;
49293
+ }
49294
+ this._lastScroll = actualBeatCursorEndX;
49295
+ const scrollX = actualBeatCursorEndX + settings.player.scrollOffsetX;
49296
+ ui.scrollToX(scroll, scrollX, actualBeatCursorTransitionDuration);
49297
+ }
48941
49298
  }
48942
49299
 
48943
49300
  /**
@@ -49550,6 +49907,19 @@ class ExternalMediaPlayer extends BackingTrackPlayer {
49550
49907
  }
49551
49908
  }
49552
49909
 
49910
+ /**
49911
+ * @internal
49912
+ */
49913
+ class BoundsLookupVisibilityChecker {
49914
+ bounds = null;
49915
+ isVisible(beat) {
49916
+ const bounds = this.bounds;
49917
+ if (!bounds) {
49918
+ return false;
49919
+ }
49920
+ return bounds.findBeat(beat) !== null;
49921
+ }
49922
+ }
49553
49923
  /**
49554
49924
  * This class represents the public API of alphaTab and provides all logic to display
49555
49925
  * a music sheet in any UI using the given {@link IUiFacade}
@@ -49560,12 +49930,14 @@ class AlphaTabApiBase {
49560
49930
  _startTime = 0;
49561
49931
  _trackIndexes = null;
49562
49932
  _trackIndexLookup = null;
49933
+ _beatVisibilityChecker = new BoundsLookupVisibilityChecker();
49563
49934
  _isDestroyed = false;
49564
49935
  _score = null;
49565
49936
  _tracks = [];
49566
49937
  _actualPlayerMode = PlayerMode.Disabled;
49567
49938
  _player;
49568
49939
  _renderer;
49940
+ _defaultScrollHandler;
49569
49941
  /**
49570
49942
  * An indicator by how many midi-ticks the song contents are shifted.
49571
49943
  * Grace beats at start might require a shift for the first beat to start at 0.
@@ -50335,6 +50707,42 @@ class AlphaTabApiBase {
50335
50707
  }
50336
50708
  }
50337
50709
  _tickCache = null;
50710
+ /**
50711
+ * A custom scroll handler which will be used to handle scrolling operations during playback.
50712
+ *
50713
+ * @category Properties - Player
50714
+ * @since 1.8.0
50715
+ * @example
50716
+ * JavaScript
50717
+ * ```js
50718
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
50719
+ * api.customScrollHandler = {
50720
+ * forceScrollTo(currentBeatBounds) {
50721
+ * const scroll = api.uiFacade.getScrollElement();
50722
+ * api.uiFacade.scrollToY(scroll, currentBeatBounds.barBounds.masterBarBounds.realBounds.y, 0);
50723
+ * },
50724
+ * onBeatCursorUpdating(startBeat, endBeat, cursorMode, relativePosition, actualBeatCursorStartX, actualBeatCursorEndX, actualBeatCursorTransitionDuration) {
50725
+ * const scroll = api.uiFacade.getScrollElement();
50726
+ * api.uiFacade.scrollToY(scroll, startBeat.barBounds.masterBarBounds.realBounds.y, 0);
50727
+ * }
50728
+ * }
50729
+ * ```
50730
+ *
50731
+ * @example
50732
+ * C#
50733
+ * ```cs
50734
+ * var api = new AlphaTabApi<MyControl>(...);
50735
+ * api.CustomScrollHandler = new CustomScrollHandler();
50736
+ * ```
50737
+ *
50738
+ * @example
50739
+ * Android
50740
+ * ```kotlin
50741
+ * val api = AlphaTabApi<MyControl>(...)
50742
+ * api.customScrollHandler = CustomScrollHandler();
50743
+ * ```
50744
+ */
50745
+ customScrollHandler;
50338
50746
  /**
50339
50747
  * The tick cache allowing lookup of midi ticks to beats.
50340
50748
  * @remarks
@@ -51313,7 +51721,6 @@ class AlphaTabApiBase {
51313
51721
  _isInitialBeatCursorUpdate = true;
51314
51722
  _previousStateForCursor = PlayerState.Paused;
51315
51723
  _previousCursorCache = null;
51316
- _lastScroll = 0;
51317
51724
  _destroyCursors() {
51318
51725
  if (!this._cursorWrapper) {
51319
51726
  return;
@@ -51324,28 +51731,79 @@ class AlphaTabApiBase {
51324
51731
  this._beatCursor = null;
51325
51732
  this._selectionWrapper = null;
51326
51733
  }
51734
+ _createCursors() {
51735
+ if (this._cursorWrapper) {
51736
+ return;
51737
+ }
51738
+ const cursors = this.uiFacade.createCursors();
51739
+ if (cursors) {
51740
+ // store options and created elements for fast access
51741
+ this._cursorWrapper = cursors.cursorWrapper;
51742
+ this._barCursor = cursors.barCursor;
51743
+ this._beatCursor = cursors.beatCursor;
51744
+ this._selectionWrapper = cursors.selectionWrapper;
51745
+ this._isInitialBeatCursorUpdate = true;
51746
+ }
51747
+ if (this._currentBeat !== null) {
51748
+ this._cursorUpdateBeat(this._currentBeat, false, this._previousTick > 10, 1, true);
51749
+ }
51750
+ }
51327
51751
  _updateCursors() {
51752
+ this._updateScrollHandler();
51328
51753
  const enable = this._hasCursor;
51329
- if (enable && !this._cursorWrapper) {
51330
- //
51331
- // Create cursors
51332
- const cursors = this.uiFacade.createCursors();
51333
- if (cursors) {
51334
- // store options and created elements for fast access
51335
- this._cursorWrapper = cursors.cursorWrapper;
51336
- this._barCursor = cursors.barCursor;
51337
- this._beatCursor = cursors.beatCursor;
51338
- this._selectionWrapper = cursors.selectionWrapper;
51339
- this._isInitialBeatCursorUpdate = true;
51340
- }
51341
- if (this._currentBeat !== null) {
51342
- this._cursorUpdateBeat(this._currentBeat, false, this._previousTick > 10, 1, true);
51343
- }
51754
+ if (enable) {
51755
+ this._createCursors();
51344
51756
  }
51345
51757
  else if (!enable && this._cursorWrapper) {
51346
51758
  this._destroyCursors();
51347
51759
  }
51348
51760
  }
51761
+ _scrollHandlerMode = ScrollMode.Off;
51762
+ _scrollHandlerVertical = true;
51763
+ _updateScrollHandler() {
51764
+ const currentHandler = this._defaultScrollHandler;
51765
+ const scrollMode = this.settings.player.scrollMode;
51766
+ const isVertical = Environment.getLayoutEngineFactory(this.settings.display.layoutMode).vertical;
51767
+ // no change
51768
+ if (this._scrollHandlerMode === scrollMode && this._scrollHandlerVertical === isVertical) {
51769
+ return;
51770
+ }
51771
+ // destroy current handler in favor of new one
51772
+ if (currentHandler) {
51773
+ currentHandler[Symbol.dispose]();
51774
+ const scroll = this.uiFacade.getScrollContainer();
51775
+ this.uiFacade.stopScrolling(scroll);
51776
+ }
51777
+ switch (scrollMode) {
51778
+ case ScrollMode.Off:
51779
+ this._defaultScrollHandler = undefined;
51780
+ break;
51781
+ case ScrollMode.Continuous:
51782
+ if (isVertical) {
51783
+ this._defaultScrollHandler = new VerticalContinuousScrollHandler(this);
51784
+ }
51785
+ else {
51786
+ this._defaultScrollHandler = new HorizontalContinuousScrollHandler(this);
51787
+ }
51788
+ break;
51789
+ case ScrollMode.OffScreen:
51790
+ if (isVertical) {
51791
+ this._defaultScrollHandler = new VerticalOffScreenScrollHandler(this);
51792
+ }
51793
+ else {
51794
+ this._defaultScrollHandler = new HorizontalOffScreenScrollHandler(this);
51795
+ }
51796
+ break;
51797
+ case ScrollMode.Smooth:
51798
+ if (isVertical) {
51799
+ this._defaultScrollHandler = new VerticalSmoothScrollHandler(this);
51800
+ }
51801
+ else {
51802
+ this._defaultScrollHandler = new HorizontalSmoothScrollHandler(this);
51803
+ }
51804
+ break;
51805
+ }
51806
+ }
51349
51807
  /**
51350
51808
  * updates the cursors to highlight the beat at the specified tick position
51351
51809
  * @param tick
@@ -51356,12 +51814,9 @@ class AlphaTabApiBase {
51356
51814
  this._previousTick = tick;
51357
51815
  const cache = this._tickCache;
51358
51816
  if (cache) {
51359
- const tracks = this._trackIndexLookup;
51360
- if (tracks != null && tracks.size > 0) {
51361
- const beat = cache.findBeat(tracks, tick, this._currentBeat);
51362
- if (beat) {
51363
- this._cursorUpdateBeat(beat, stop, shouldScroll, cursorSpeed, forceUpdate || this.playerState === PlayerState.Paused);
51364
- }
51817
+ const beat = cache.findBeatWithChecker(this._beatVisibilityChecker, tick, this._currentBeat);
51818
+ if (beat) {
51819
+ this._cursorUpdateBeat(beat, stop, shouldScroll, cursorSpeed, forceUpdate || this.playerState === PlayerState.Paused);
51365
51820
  }
51366
51821
  }
51367
51822
  }
@@ -51411,57 +51866,9 @@ class AlphaTabApiBase {
51411
51866
  scrollToCursor() {
51412
51867
  const beatBounds = this._currentBeatBounds;
51413
51868
  if (beatBounds) {
51414
- this._internalScrollToCursor(beatBounds.barBounds.masterBarBounds);
51415
- }
51416
- }
51417
- _internalScrollToCursor(barBoundings) {
51418
- const scrollElement = this.uiFacade.getScrollContainer();
51419
- const isVertical = Environment.getLayoutEngineFactory(this.settings.display.layoutMode).vertical;
51420
- const mode = this.settings.player.scrollMode;
51421
- if (isVertical) {
51422
- // when scrolling on the y-axis, we preliminary check if the new beat/bar have
51423
- // moved on the y-axis
51424
- const y = barBoundings.realBounds.y + this.settings.player.scrollOffsetY;
51425
- if (y !== this._lastScroll) {
51426
- this._lastScroll = y;
51427
- switch (mode) {
51428
- case ScrollMode.Continuous:
51429
- const elementOffset = this.uiFacade.getOffset(scrollElement, this.container);
51430
- this.uiFacade.scrollToY(scrollElement, elementOffset.y + y, this.settings.player.scrollSpeed);
51431
- break;
51432
- case ScrollMode.OffScreen:
51433
- const elementBottom = scrollElement.scrollTop + this.uiFacade.getOffset(null, scrollElement).h;
51434
- if (barBoundings.visualBounds.y + barBoundings.visualBounds.h >= elementBottom ||
51435
- barBoundings.visualBounds.y < scrollElement.scrollTop) {
51436
- const scrollTop = barBoundings.realBounds.y + this.settings.player.scrollOffsetY;
51437
- this.uiFacade.scrollToY(scrollElement, scrollTop, this.settings.player.scrollSpeed);
51438
- }
51439
- break;
51440
- }
51441
- }
51442
- }
51443
- else {
51444
- // when scrolling on the x-axis, we preliminary check if the new bar has
51445
- // moved on the x-axis
51446
- const x = barBoundings.visualBounds.x;
51447
- if (x !== this._lastScroll) {
51448
- this._lastScroll = x;
51449
- switch (mode) {
51450
- case ScrollMode.Continuous:
51451
- const scrollLeftContinuous = barBoundings.realBounds.x + this.settings.player.scrollOffsetX;
51452
- this._lastScroll = barBoundings.visualBounds.x;
51453
- this.uiFacade.scrollToX(scrollElement, scrollLeftContinuous, this.settings.player.scrollSpeed);
51454
- break;
51455
- case ScrollMode.OffScreen:
51456
- const elementRight = scrollElement.scrollLeft + this.uiFacade.getOffset(null, scrollElement).w;
51457
- if (barBoundings.visualBounds.x + barBoundings.visualBounds.w >= elementRight ||
51458
- barBoundings.visualBounds.x < scrollElement.scrollLeft) {
51459
- const scrollLeftOffScreen = barBoundings.realBounds.x + this.settings.player.scrollOffsetX;
51460
- this._lastScroll = barBoundings.visualBounds.x;
51461
- this.uiFacade.scrollToX(scrollElement, scrollLeftOffScreen, this.settings.player.scrollSpeed);
51462
- }
51463
- break;
51464
- }
51869
+ const handler = this.customScrollHandler ?? this._defaultScrollHandler;
51870
+ if (handler) {
51871
+ handler.forceScrollTo(beatBounds);
51465
51872
  }
51466
51873
  }
51467
51874
  }
@@ -51477,11 +51884,12 @@ class AlphaTabApiBase {
51477
51884
  }
51478
51885
  const isPlayingUpdate = this._player.state === PlayerState.Playing && !stop;
51479
51886
  let nextBeatX = barBoundings.visualBounds.x + barBoundings.visualBounds.w;
51887
+ let nextBeatBoundings = null;
51480
51888
  // get position of next beat on same system
51481
51889
  if (nextBeat && cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext) {
51482
51890
  // if we are moving within the same bar or to the next bar
51483
51891
  // transition to the next beat, otherwise transition to the end of the bar.
51484
- const nextBeatBoundings = cache.findBeat(nextBeat);
51892
+ nextBeatBoundings = cache.findBeat(nextBeat);
51485
51893
  if (nextBeatBoundings &&
51486
51894
  nextBeatBoundings.barBounds.masterBarBounds.staffSystemBounds === barBoundings.staffSystemBounds) {
51487
51895
  nextBeatX = nextBeatBoundings.onNotesX;
@@ -51507,25 +51915,29 @@ class AlphaTabApiBase {
51507
51915
  beatCursor.transitionToX(0, startBeatX);
51508
51916
  beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
51509
51917
  }
51918
+ // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks)
51919
+ // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time.
51920
+ // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX);
51921
+ const factor = cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext ? 2 : 1;
51922
+ nextBeatX = startBeatX + (nextBeatX - startBeatX) * factor;
51923
+ duration = (duration / cursorSpeed) * factor;
51510
51924
  // we need to put the transition to an own animation frame
51511
51925
  // otherwise the stop animation above is not applied.
51512
51926
  this.uiFacade.beginInvoke(() => {
51513
- // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks)
51514
- // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time.
51515
- // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX);
51516
- const factor = cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext ? 2 : 1;
51517
- const doubleEndBeatX = startBeatX + (nextBeatX - startBeatX) * factor;
51518
- beatCursor.transitionToX((duration / cursorSpeed) * factor, doubleEndBeatX);
51927
+ beatCursor.transitionToX(duration, nextBeatX);
51519
51928
  });
51520
51929
  }
51521
51930
  else {
51522
- beatCursor.transitionToX(0, startBeatX);
51931
+ duration = 0;
51932
+ beatCursor.transitionToX(duration, nextBeatX);
51523
51933
  beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
51524
51934
  }
51525
51935
  }
51526
51936
  else {
51527
51937
  // ticking cursor
51528
- beatCursor.transitionToX(0, startBeatX);
51938
+ duration = 0;
51939
+ nextBeatX = startBeatX;
51940
+ beatCursor.transitionToX(duration, nextBeatX);
51529
51941
  beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
51530
51942
  }
51531
51943
  this._isInitialBeatCursorUpdate = false;
@@ -51548,7 +51960,10 @@ class AlphaTabApiBase {
51548
51960
  shouldNotifyBeatChange = true;
51549
51961
  }
51550
51962
  if (shouldScroll && !this._isBeatMouseDown && this.settings.player.scrollMode !== ScrollMode.Off) {
51551
- this._internalScrollToCursor(barBoundings);
51963
+ const handler = this.customScrollHandler ?? this._defaultScrollHandler;
51964
+ if (handler) {
51965
+ handler.onBeatCursorUpdating(beatBoundings, nextBeatBoundings === null ? undefined : nextBeatBoundings, cursorMode, startBeatX, nextBeatX, duration);
51966
+ }
51552
51967
  }
51553
51968
  // trigger an event for others to indicate which beat/bar is played
51554
51969
  if (shouldNotifyBeatChange) {
@@ -52563,6 +52978,7 @@ class AlphaTabApiBase {
52563
52978
  if (this._isDestroyed) {
52564
52979
  return;
52565
52980
  }
52981
+ this._beatVisibilityChecker.bounds = this.boundsLookup;
52566
52982
  this._currentBeat = null;
52567
52983
  this._cursorUpdateTick(this._previousTick, false, 1, true, true);
52568
52984
  this.postRenderFinished.trigger();
@@ -52921,6 +53337,7 @@ class AlphaTabApiBase {
52921
53337
  const tickCache = this._tickCache;
52922
53338
  if (currentBeat && tickCache) {
52923
53339
  this._player.tickPosition = tickCache.getBeatStart(currentBeat.beat);
53340
+ this.scrollToCursor();
52924
53341
  }
52925
53342
  }
52926
53343
  this.uiFacade.triggerEvent(this.container, 'playerStateChanged', e);
@@ -54881,6 +55298,22 @@ class BrowserUiFacade {
54881
55298
  canvasElement.style.position = 'relative';
54882
55299
  return new HtmlElementContainer(canvasElement);
54883
55300
  }
55301
+ setCanvasOverflow(canvasElement, overflow, isVertical) {
55302
+ const html = canvasElement.element;
55303
+ if (overflow === 0) {
55304
+ html.style.boxSizing = '';
55305
+ html.style.paddingRight = '';
55306
+ html.style.paddingBottom = '';
55307
+ }
55308
+ else if (isVertical) {
55309
+ html.style.boxSizing = 'content-box';
55310
+ html.style.paddingBottom = `${overflow}px`;
55311
+ }
55312
+ else {
55313
+ html.style.boxSizing = 'content-box';
55314
+ html.style.paddingRight = `${overflow}px`;
55315
+ }
55316
+ }
54884
55317
  triggerEvent(container, name, details = null, originalEvent) {
54885
55318
  const element = container.element;
54886
55319
  name = `alphaTab.${name}`;
@@ -55438,54 +55871,79 @@ class BrowserUiFacade {
55438
55871
  scrollToX(element, scrollTargetY, speed) {
55439
55872
  this._internalScrollToX(element.element, scrollTargetY, speed);
55440
55873
  }
55874
+ stopScrolling(scrollElement) {
55875
+ // stop any current animation
55876
+ const currentAnimation = this._scrollAnimationLookup.get(scrollElement.element);
55877
+ if (currentAnimation !== undefined) {
55878
+ this._activeScrollAnimations.delete(currentAnimation);
55879
+ }
55880
+ }
55881
+ get _nativeBrowserSmoothScroll() {
55882
+ const settings = this._api.settings.player;
55883
+ return settings.nativeBrowserSmoothScroll && settings.scrollMode !== ScrollMode.Smooth;
55884
+ }
55885
+ _scrollAnimationId = 0;
55886
+ _activeScrollAnimations = new Set();
55887
+ _scrollAnimationLookup = new Map();
55441
55888
  _internalScrollToY(element, scrollTargetY, speed) {
55442
- if (this._api.settings.player.nativeBrowserSmoothScroll) {
55889
+ if (this._nativeBrowserSmoothScroll) {
55443
55890
  element.scrollTo({
55444
55891
  top: scrollTargetY,
55445
55892
  behavior: 'smooth'
55446
55893
  });
55447
55894
  }
55448
55895
  else {
55449
- const startY = element.scrollTop;
55450
- const diff = scrollTargetY - startY;
55451
- let start = 0;
55452
- const step = (x) => {
55453
- if (start === 0) {
55454
- start = x;
55455
- }
55456
- const time = x - start;
55457
- const percent = Math.min(time / speed, 1);
55458
- element.scrollTop = (startY + diff * percent) | 0;
55459
- if (time < speed) {
55460
- window.requestAnimationFrame(step);
55461
- }
55462
- };
55463
- window.requestAnimationFrame(step);
55896
+ this._internalScrollTo(element, element.scrollTop, scrollTargetY, speed, scroll => {
55897
+ element.scrollTop = scroll;
55898
+ });
55464
55899
  }
55465
55900
  }
55901
+ _internalScrollTo(element, startScroll, endScroll, scrollDuration, setValue) {
55902
+ // stop any current animation
55903
+ const currentAnimation = this._scrollAnimationLookup.get(element);
55904
+ if (currentAnimation !== undefined) {
55905
+ this._activeScrollAnimations.delete(currentAnimation);
55906
+ }
55907
+ if (scrollDuration === 0) {
55908
+ setValue(endScroll);
55909
+ return;
55910
+ }
55911
+ // start new animation
55912
+ const animationId = this._scrollAnimationId++;
55913
+ this._scrollAnimationLookup.set(element, animationId);
55914
+ this._activeScrollAnimations.add(animationId);
55915
+ const diff = endScroll - startScroll;
55916
+ let start = 0;
55917
+ const step = (x) => {
55918
+ if (!this._activeScrollAnimations.has(animationId)) {
55919
+ return;
55920
+ }
55921
+ if (start === 0) {
55922
+ start = x;
55923
+ }
55924
+ const time = x - start;
55925
+ const percent = Math.min(time / scrollDuration, 1);
55926
+ setValue((startScroll + diff * percent) | 0);
55927
+ if (time < scrollDuration) {
55928
+ window.requestAnimationFrame(step);
55929
+ }
55930
+ else {
55931
+ this._activeScrollAnimations.delete(animationId);
55932
+ }
55933
+ };
55934
+ window.requestAnimationFrame(step);
55935
+ }
55466
55936
  _internalScrollToX(element, scrollTargetX, speed) {
55467
- if (this._api.settings.player.nativeBrowserSmoothScroll) {
55937
+ if (this._nativeBrowserSmoothScroll) {
55468
55938
  element.scrollTo({
55469
55939
  left: scrollTargetX,
55470
55940
  behavior: 'smooth'
55471
55941
  });
55472
55942
  }
55473
55943
  else {
55474
- const startX = element.scrollLeft;
55475
- const diff = scrollTargetX - startX;
55476
- let start = 0;
55477
- const step = (t) => {
55478
- if (start === 0) {
55479
- start = t;
55480
- }
55481
- const time = t - start;
55482
- const percent = Math.min(time / speed, 1);
55483
- element.scrollLeft = (startX + diff * percent) | 0;
55484
- if (time < speed) {
55485
- window.requestAnimationFrame(step);
55486
- }
55487
- };
55488
- window.requestAnimationFrame(step);
55944
+ this._internalScrollTo(element, element.scrollLeft, scrollTargetX, speed, scroll => {
55945
+ element.scrollLeft = scroll;
55946
+ });
55489
55947
  }
55490
55948
  }
55491
55949
  createBackingTrackPlayer() {
@@ -60570,8 +61028,10 @@ class RenderStaff {
60570
61028
  height = 0;
60571
61029
  index = 0;
60572
61030
  staffIndex = 0;
61031
+ isVisible = false;
61032
+ _emptyBarCount = 0;
60573
61033
  get isFirstInSystem() {
60574
- return this.index === 0;
61034
+ return this.system.firstVisibleStaff === this;
60575
61035
  }
60576
61036
  topEffectInfos = [];
60577
61037
  bottomEffectInfos = [];
@@ -60606,10 +61066,11 @@ class RenderStaff {
60606
61066
  get contentBottom() {
60607
61067
  return this.y + this.topPadding + this.topOverflow + this.staffBottom;
60608
61068
  }
60609
- constructor(trackIndex, staff, factory) {
61069
+ constructor(system, trackIndex, staff, factory) {
60610
61070
  this._factory = factory;
60611
61071
  this.trackIndex = trackIndex;
60612
61072
  this.modelStaff = staff;
61073
+ this.system = system;
60613
61074
  for (const b of factory.effectBands) {
60614
61075
  if (b.shouldCreate && !b.shouldCreate(staff)) {
60615
61076
  continue;
@@ -60625,6 +61086,7 @@ class RenderStaff {
60625
61086
  break;
60626
61087
  }
60627
61088
  }
61089
+ this._updateVisibility();
60628
61090
  }
60629
61091
  getSharedLayoutData(key, def) {
60630
61092
  if (this._sharedLayoutData.has(key)) {
@@ -60651,6 +61113,20 @@ class RenderStaff {
60651
61113
  renderer.reLayout();
60652
61114
  this.barRenderers.push(renderer);
60653
61115
  this.system.layout.registerBarRenderer(this.staffId, renderer);
61116
+ if (renderer.bar.isEmpty || renderer.bar.isRestOnly) {
61117
+ this._emptyBarCount++;
61118
+ }
61119
+ this._updateVisibility();
61120
+ }
61121
+ _updateVisibility() {
61122
+ const stylesheet = this.modelStaff.track.score.stylesheet;
61123
+ const canHideEmptyStaves = stylesheet.hideEmptyStaves && (stylesheet.hideEmptyStavesInFirstSystem || this.system.index > 0);
61124
+ if (canHideEmptyStaves) {
61125
+ this.isVisible = this._emptyBarCount < this.barRenderers.length;
61126
+ }
61127
+ else {
61128
+ this.isVisible = true;
61129
+ }
60654
61130
  }
60655
61131
  addBar(bar, layoutingInfo, additionalMultiBarsRestBars) {
60656
61132
  const renderer = this._factory.create(this.system.layout.renderer, bar);
@@ -60670,6 +61146,10 @@ class RenderStaff {
60670
61146
  }
60671
61147
  this.barRenderers.push(renderer);
60672
61148
  this.system.layout.registerBarRenderer(this.staffId, renderer);
61149
+ if (bar.isEmpty || bar.isRestOnly) {
61150
+ this._emptyBarCount++;
61151
+ }
61152
+ this._updateVisibility();
60673
61153
  }
60674
61154
  revertLastBar() {
60675
61155
  this._sharedLayoutData = new Map();
@@ -60680,6 +61160,10 @@ class RenderStaff {
60680
61160
  for (const r of this.barRenderers) {
60681
61161
  r.afterStaffBarReverted();
60682
61162
  }
61163
+ if (lastBar.bar.isEmpty || lastBar.bar.isRestOnly) {
61164
+ this._emptyBarCount--;
61165
+ }
61166
+ this._updateVisibility();
60683
61167
  return lastBar;
60684
61168
  }
60685
61169
  scaleToWidth(width) {
@@ -60790,6 +61274,7 @@ class RenderStaff {
60790
61274
  this.height += this.topPadding + topOverflow + this.bottomOverflow + this.bottomPadding;
60791
61275
  }
60792
61276
  this.height = Math.ceil(this.height);
61277
+ this._updateVisibility();
60793
61278
  }
60794
61279
  paint(cx, cy, canvas, startIndex, count) {
60795
61280
  if (this.height === 0 || count === 0) {
@@ -61190,6 +61675,8 @@ class StaffTrackGroup {
61190
61675
  track;
61191
61676
  staffSystem;
61192
61677
  staves = [];
61678
+ firstVisibleStaff;
61679
+ lastVisibleStaff;
61193
61680
  bracket = null;
61194
61681
  constructor(staffSystem, track) {
61195
61682
  this.staffSystem = staffSystem;
@@ -61204,18 +61691,47 @@ class StaffTrackGroup {
61204
61691
  * @internal
61205
61692
  */
61206
61693
  class SystemBracket {
61207
- firstStaffInBracket = null;
61208
- lastStaffInBracket = null;
61694
+ _system;
61695
+ firstStaffInBracket;
61696
+ lastStaffInBracket;
61697
+ firstVisibleStaffInBracket;
61698
+ lastVisibleStaffInBracket;
61209
61699
  drawAsBrace = false;
61210
61700
  braceScale = 1;
61211
61701
  width = 0;
61212
61702
  index = 0;
61213
- get canPaint() {
61214
- return this.firstStaffInBracket !== null && this.lastStaffInBracket !== null;
61703
+ canPaint = false;
61704
+ constructor(system) {
61705
+ this._system = system;
61706
+ }
61707
+ updateCanPaint() {
61708
+ let firstVisibleStaff = undefined;
61709
+ let lastVisibleStaff = undefined;
61710
+ for (let i = this.firstStaffInBracket.index; i <= this.lastStaffInBracket.index; i++) {
61711
+ const staff = this._system.allStaves[i];
61712
+ if (staff.isVisible) {
61713
+ if (!firstVisibleStaff) {
61714
+ firstVisibleStaff = staff;
61715
+ }
61716
+ lastVisibleStaff = staff;
61717
+ }
61718
+ }
61719
+ this.firstVisibleStaffInBracket = firstVisibleStaff;
61720
+ this.lastVisibleStaffInBracket = lastVisibleStaff;
61721
+ if (!firstVisibleStaff || !lastVisibleStaff) {
61722
+ this.canPaint = false;
61723
+ return;
61724
+ }
61725
+ // single staff brackets?
61726
+ const singleStaffBrackets = this._system.layout.renderer.score.stylesheet.showSingleStaffBrackets;
61727
+ if (!singleStaffBrackets && firstVisibleStaff === lastVisibleStaff) {
61728
+ this.canPaint = false;
61729
+ return;
61730
+ }
61731
+ this.canPaint = true;
61215
61732
  }
61216
61733
  finalizeBracket(smuflMetrics) {
61217
- // systems with just a single staff do not have a bracket
61218
- if (this.firstStaffInBracket === this.lastStaffInBracket) {
61734
+ if (!this.canPaint) {
61219
61735
  this.width = 0;
61220
61736
  return;
61221
61737
  }
@@ -61229,11 +61745,11 @@ class SystemBracket {
61229
61745
  else {
61230
61746
  this.width = smuflMetrics.bracketThickness;
61231
61747
  }
61232
- if (!this.drawAsBrace || !this.firstStaffInBracket || !this.lastStaffInBracket) {
61748
+ if (!this.drawAsBrace) {
61233
61749
  return;
61234
61750
  }
61235
- const firstStart = this.firstStaffInBracket.contentTop;
61236
- const lastEnd = this.lastStaffInBracket.contentBottom;
61751
+ const firstStart = this.firstVisibleStaffInBracket.contentTop;
61752
+ const lastEnd = this.lastVisibleStaffInBracket.contentBottom;
61237
61753
  const requiredHeight = lastEnd - firstStart;
61238
61754
  const requiredScaleForBracket = requiredHeight / bravuraBraceHeightAtMusicFontSize;
61239
61755
  this.braceScale = requiredScaleForBracket;
@@ -61245,8 +61761,8 @@ class SystemBracket {
61245
61761
  */
61246
61762
  class SingleTrackSystemBracket extends SystemBracket {
61247
61763
  track;
61248
- constructor(track) {
61249
- super();
61764
+ constructor(system, track) {
61765
+ super(system);
61250
61766
  this.track = track;
61251
61767
  this.drawAsBrace = SingleTrackSystemBracket.isTrackDrawAsBrace(track);
61252
61768
  }
@@ -61311,6 +61827,7 @@ class StaffSystem {
61311
61827
  topPadding;
61312
61828
  bottomPadding;
61313
61829
  allStaves = [];
61830
+ firstVisibleStaff;
61314
61831
  constructor(layout) {
61315
61832
  this.layout = layout;
61316
61833
  this.topPadding = layout.renderer.settings.display.systemPaddingTop;
@@ -61329,14 +61846,40 @@ class StaffSystem {
61329
61846
  this.masterBarsRenderers.push(renderers);
61330
61847
  renderers.layoutingInfo.preBeatSize = 0;
61331
61848
  let src = 0;
61332
- for (let i = 0, j = this.staves.length; i < j; i++) {
61333
- const g = this.staves[i];
61334
- for (let k = 0, l = g.staves.length; k < l; k++) {
61335
- const s = g.staves[k];
61849
+ let firstVisibleStaff = undefined;
61850
+ let anyStaffVisible = false;
61851
+ for (const g of this.staves) {
61852
+ let firstVisibleStaffInGroup = undefined;
61853
+ let lastVisibleStaffInGroup = undefined;
61854
+ for (const s of g.staves) {
61336
61855
  const renderer = renderers.renderers[src++];
61337
61856
  s.addBarRenderer(renderer);
61857
+ if (s.isVisible) {
61858
+ anyStaffVisible = true;
61859
+ if (!firstVisibleStaffInGroup) {
61860
+ firstVisibleStaffInGroup = s;
61861
+ }
61862
+ if (!firstVisibleStaff) {
61863
+ firstVisibleStaff = s;
61864
+ }
61865
+ lastVisibleStaffInGroup = s;
61866
+ }
61867
+ }
61868
+ g.firstVisibleStaff = firstVisibleStaffInGroup;
61869
+ g.lastVisibleStaff = lastVisibleStaffInGroup;
61870
+ if (!firstVisibleStaff) {
61871
+ firstVisibleStaff = firstVisibleStaffInGroup;
61338
61872
  }
61339
61873
  }
61874
+ if (!anyStaffVisible) {
61875
+ const group = this.staves[0];
61876
+ const firstStaff = group.staves[0];
61877
+ firstStaff.isVisible = true;
61878
+ group.firstVisibleStaff = firstStaff;
61879
+ group.lastVisibleStaff = firstStaff;
61880
+ firstVisibleStaff = firstStaff;
61881
+ }
61882
+ this.firstVisibleStaff = firstVisibleStaff;
61340
61883
  this._calculateAccoladeSpacing(tracks);
61341
61884
  this._updateWidthFromLastBar();
61342
61885
  return renderers;
@@ -61347,15 +61890,26 @@ class StaffSystem {
61347
61890
  result.layoutingInfo = new BarLayoutingInfo();
61348
61891
  result.masterBar = tracks[0].score.masterBars[barIndex];
61349
61892
  this.masterBarsRenderers.push(result);
61893
+ let firstVisibleStaff = undefined;
61894
+ let anyStaffVisible = false;
61350
61895
  // add renderers
61351
61896
  const barLayoutingInfo = result.layoutingInfo;
61352
61897
  for (const g of this.staves) {
61898
+ let firstVisibleStaffInGroup = undefined;
61899
+ let lastVisibleStaffInGroup = undefined;
61353
61900
  for (const s of g.staves) {
61354
61901
  const bar = g.track.staves[s.modelStaff.index].bars[barIndex];
61355
61902
  const additionalMultiBarsRestBars = additionalMultiBarRestIndexes == null
61356
61903
  ? null
61357
61904
  : additionalMultiBarRestIndexes.map(b => g.track.staves[s.modelStaff.index].bars[b]);
61358
61905
  s.addBar(bar, barLayoutingInfo, additionalMultiBarsRestBars);
61906
+ if (s.isVisible) {
61907
+ anyStaffVisible = true;
61908
+ if (!firstVisibleStaffInGroup) {
61909
+ firstVisibleStaffInGroup = s;
61910
+ }
61911
+ lastVisibleStaffInGroup = s;
61912
+ }
61359
61913
  const renderer = s.barRenderers[s.barRenderers.length - 1];
61360
61914
  result.renderers.push(renderer);
61361
61915
  if (renderer.isLinkedToPrevious) {
@@ -61365,7 +61919,21 @@ class StaffSystem {
61365
61919
  result.canWrap = false;
61366
61920
  }
61367
61921
  }
61922
+ g.firstVisibleStaff = firstVisibleStaffInGroup;
61923
+ g.lastVisibleStaff = lastVisibleStaffInGroup;
61924
+ if (!firstVisibleStaff) {
61925
+ firstVisibleStaff = firstVisibleStaffInGroup;
61926
+ }
61927
+ }
61928
+ if (!anyStaffVisible) {
61929
+ const group = this.staves[0];
61930
+ const firstStaff = group.staves[0];
61931
+ firstStaff.isVisible = true;
61932
+ group.firstVisibleStaff = firstStaff;
61933
+ group.lastVisibleStaff = firstStaff;
61934
+ firstVisibleStaff = firstStaff;
61368
61935
  }
61936
+ this.firstVisibleStaff = firstVisibleStaff;
61369
61937
  this._calculateAccoladeSpacing(tracks);
61370
61938
  barLayoutingInfo.finish();
61371
61939
  // ensure same widths of new renderer
@@ -61378,19 +61946,35 @@ class StaffSystem {
61378
61946
  this.masterBarsRenderers.splice(this.masterBarsRenderers.length - 1, 1);
61379
61947
  let width = 0;
61380
61948
  let barDisplayScale = 0;
61381
- for (let i = 0, j = this.allStaves.length; i < j; i++) {
61382
- const s = this.allStaves[i];
61383
- const lastBar = s.revertLastBar();
61384
- const computedWidth = lastBar.computedWidth;
61385
- if (computedWidth > width) {
61386
- width = computedWidth;
61949
+ let firstVisibleStaff = undefined;
61950
+ for (const g of this.staves) {
61951
+ let firstVisibleStaffInGroup = undefined;
61952
+ let lastVisibleStaffInGroup = undefined;
61953
+ for (const s of g.staves) {
61954
+ const lastBar = s.revertLastBar();
61955
+ const computedWidth = lastBar.computedWidth;
61956
+ if (computedWidth > width) {
61957
+ width = computedWidth;
61958
+ }
61959
+ const newBarDisplayScale = lastBar.barDisplayScale;
61960
+ if (newBarDisplayScale > barDisplayScale) {
61961
+ barDisplayScale = newBarDisplayScale;
61962
+ }
61963
+ lastBar.afterReverted();
61964
+ if (s.isVisible) {
61965
+ if (!firstVisibleStaffInGroup) {
61966
+ firstVisibleStaffInGroup = s;
61967
+ }
61968
+ lastVisibleStaffInGroup = s;
61969
+ }
61387
61970
  }
61388
- const newBarDisplayScale = lastBar.barDisplayScale;
61389
- if (newBarDisplayScale > barDisplayScale) {
61390
- barDisplayScale = newBarDisplayScale;
61971
+ g.firstVisibleStaff = firstVisibleStaffInGroup;
61972
+ g.lastVisibleStaff = lastVisibleStaffInGroup;
61973
+ if (!firstVisibleStaff) {
61974
+ firstVisibleStaff = firstVisibleStaffInGroup;
61391
61975
  }
61392
- lastBar.afterReverted();
61393
61976
  }
61977
+ this.firstVisibleStaff = firstVisibleStaff;
61394
61978
  this.width -= width;
61395
61979
  this.computedWidth -= width;
61396
61980
  this.totalBarDisplayScale -= barDisplayScale;
@@ -61472,8 +62056,8 @@ class StaffSystem {
61472
62056
  }
61473
62057
  }
61474
62058
  }
62059
+ this.accoladeWidth += settings.display.systemLabelPaddingLeft;
61475
62060
  if (hasAnyTrackName) {
61476
- this.accoladeWidth += settings.display.systemLabelPaddingLeft;
61477
62061
  this.accoladeWidth += settings.display.systemLabelPaddingRight;
61478
62062
  }
61479
62063
  }
@@ -61497,6 +62081,7 @@ class StaffSystem {
61497
62081
  }
61498
62082
  let braceWidth = 0;
61499
62083
  for (const b of this._brackets) {
62084
+ b.updateCanPaint();
61500
62085
  b.finalizeBracket(settings.display.resources.engravingSettings);
61501
62086
  braceWidth = Math.max(braceWidth, b.width);
61502
62087
  }
@@ -61504,6 +62089,12 @@ class StaffSystem {
61504
62089
  this.width += this.accoladeWidth;
61505
62090
  this.computedWidth += this.accoladeWidth;
61506
62091
  }
62092
+ else {
62093
+ for (const b of this._brackets) {
62094
+ b.updateCanPaint();
62095
+ b.finalizeBracket(settings.display.resources.engravingSettings);
62096
+ }
62097
+ }
61507
62098
  }
61508
62099
  _getStaffTrackGroup(track) {
61509
62100
  for (let i = 0, j = this.staves.length; i < j; i++) {
@@ -61533,12 +62124,12 @@ class StaffSystem {
61533
62124
  break;
61534
62125
  case BracketExtendMode.GroupStaves:
61535
62126
  // when grouping staves, we create one bracket for the whole track across all staves
61536
- bracket = new SingleTrackSystemBracket(track);
62127
+ bracket = new SingleTrackSystemBracket(this, track);
61537
62128
  bracket.index = this._brackets.length;
61538
62129
  this._brackets.push(bracket);
61539
62130
  break;
61540
62131
  case BracketExtendMode.GroupSimilarInstruments:
61541
- bracket = new SimilarInstrumentSystemBracket(track);
62132
+ bracket = new SimilarInstrumentSystemBracket(this, track);
61542
62133
  bracket.index = this._brackets.length;
61543
62134
  this._brackets.push(bracket);
61544
62135
  break;
@@ -61585,8 +62176,10 @@ class StaffSystem {
61585
62176
  }
61586
62177
  }
61587
62178
  paintPartial(cx, cy, canvas, startIndex, count) {
61588
- for (let i = 0, j = this.allStaves.length; i < j; i++) {
61589
- this.allStaves[i].paint(cx, cy, canvas, startIndex, count);
62179
+ for (const s of this.allStaves) {
62180
+ if (s.isVisible) {
62181
+ s.paint(cx, cy, canvas, startIndex, count);
62182
+ }
61590
62183
  }
61591
62184
  const res = this.layout.renderer.settings.display.resources;
61592
62185
  if (this.staves.length > 0 && startIndex === 0) {
@@ -61623,9 +62216,9 @@ class StaffSystem {
61623
62216
  const oldBaseLine = canvas.textBaseline;
61624
62217
  const oldTextAlign = canvas.textAlign;
61625
62218
  for (const g of this.staves) {
61626
- if (g.staves.length > 0) {
61627
- const firstStart = cy + g.staves[0].contentTop;
61628
- const lastEnd = cy + g.staves[g.staves.length - 1].contentBottom;
62219
+ if (g.firstVisibleStaff) {
62220
+ const firstStart = cy + g.firstVisibleStaff.contentTop;
62221
+ const lastEnd = cy + g.lastVisibleStaff.contentBottom;
61629
62222
  let trackNameText = '';
61630
62223
  switch (trackNameMode) {
61631
62224
  case TrackNameMode.FullName:
@@ -61679,6 +62272,9 @@ class StaffSystem {
61679
62272
  if (this.allStaves.length > 0 && needsSystemBarLine) {
61680
62273
  let previousStaffInBracket = null;
61681
62274
  for (const s of this.allStaves) {
62275
+ if (!s.isVisible) {
62276
+ continue;
62277
+ }
61682
62278
  if (previousStaffInBracket !== null) {
61683
62279
  const previousBottom = previousStaffInBracket.contentBottom;
61684
62280
  const thisTop = s.contentTop;
@@ -61705,17 +62301,17 @@ class StaffSystem {
61705
62301
  const settings = this.layout.renderer.settings;
61706
62302
  for (const bracket of this._brackets) {
61707
62303
  if (bracket.canPaint) {
61708
- const barStartX = cx + bracket.firstStaffInBracket.x;
62304
+ const barStartX = cx + bracket.firstVisibleStaffInBracket.x;
61709
62305
  const barSize = bracket.width;
61710
62306
  const barOffset = settings.display.accoladeBarPaddingRight;
61711
- const firstStart = cy + bracket.firstStaffInBracket.contentTop;
61712
- const lastEnd = cy + bracket.lastStaffInBracket.contentBottom;
62307
+ const firstStart = cy + bracket.firstVisibleStaffInBracket.contentTop;
62308
+ const lastEnd = cy + bracket.lastVisibleStaffInBracket.contentBottom;
61713
62309
  let accoladeStart = firstStart;
61714
62310
  let accoladeEnd = lastEnd;
61715
62311
  if (bracket.drawAsBrace) {
61716
62312
  CanvasHelper.fillMusicFontSymbolSafe(canvas, barStartX - barOffset - barSize, accoladeEnd, bracket.braceScale, MusicFontSymbol.Brace);
61717
62313
  }
61718
- else if (bracket.firstStaffInBracket !== bracket.lastStaffInBracket) {
62314
+ else if (bracket.firstVisibleStaffInBracket !== bracket.lastVisibleStaffInBracket) {
61719
62315
  // brackets typically overflow by 1/4 staff-space
61720
62316
  const smuflMetrics = settings.display.resources.engravingSettings;
61721
62317
  const bracketOverflow = smuflMetrics.oneStaffSpace * 0.25;
@@ -61747,40 +62343,86 @@ class StaffSystem {
61747
62343
  this.bottomPadding = Math.max(this.bottomPadding, neededHeight);
61748
62344
  this._hasSystemSeparator = true;
61749
62345
  }
62346
+ const anyStaffVisible = this._finalizeTrackGroups();
62347
+ // for now we always force one staff to be visible.
62348
+ // making also whole systems invisible needs separate attention (also on player cursor handling)
62349
+ if (!anyStaffVisible) {
62350
+ const group = this.staves[0];
62351
+ const firstStaff = group.staves[0];
62352
+ firstStaff.isVisible = true;
62353
+ this._finalizeTrackGroups(true);
62354
+ }
62355
+ for (const b of this._brackets) {
62356
+ b.finalizeBracket(settings.display.resources.engravingSettings);
62357
+ }
62358
+ }
62359
+ _finalizeTrackGroups(onlyFirstGroup = false) {
61750
62360
  let currentY = 0;
62361
+ const settings = this.layout.renderer.settings;
61751
62362
  const smufl = settings.display.resources.engravingSettings;
61752
62363
  const topBracketSpikeHeight = smufl.glyphHeights.get(MusicFontSymbol.BracketTop);
61753
62364
  const bottomBracketSpikeHeight = smufl.glyphHeights.get(MusicFontSymbol.BracketBottom);
61754
62365
  let previousStaff = undefined;
61755
- for (const staff of this.allStaves) {
61756
- // check if we need "in-between padding"
61757
- if (previousStaff !== undefined && previousStaff.trackIndex !== staff.trackIndex) {
61758
- currentY += settings.display.trackStaffPaddingBetween;
61759
- }
61760
- const bracket = this._staffToBracket.has(staff) ? this._staffToBracket.get(staff) : undefined;
61761
- const hasBracket = bracket && !bracket.drawAsBrace && bracket.canPaint;
61762
- if (hasBracket && bracket.firstStaffInBracket === staff) {
61763
- const spikeOverflow = topBracketSpikeHeight - staff.topOverflow;
61764
- if (spikeOverflow > 0) {
61765
- currentY += spikeOverflow;
62366
+ let endSpikeOverflow = 0;
62367
+ let anyStaffVisible = false;
62368
+ for (const group of this.staves) {
62369
+ let firstVisibleStaffInGroup = undefined;
62370
+ let lastVisibleStaffInGroup = undefined;
62371
+ for (const staff of group.staves) {
62372
+ // check if we need "in-between padding"
62373
+ if (previousStaff !== undefined && previousStaff.trackIndex !== staff.trackIndex) {
62374
+ currentY += settings.display.trackStaffPaddingBetween;
62375
+ }
62376
+ const bracket = this._staffToBracket.has(staff) ? this._staffToBracket.get(staff) : undefined;
62377
+ const hasBracket = bracket && !bracket.drawAsBrace && bracket.canPaint;
62378
+ if (hasBracket && bracket.firstStaffInBracket === staff) {
62379
+ const spikeOverflow = topBracketSpikeHeight - staff.topOverflow;
62380
+ if (spikeOverflow > 0) {
62381
+ currentY += spikeOverflow;
62382
+ }
62383
+ }
62384
+ staff.x = this.accoladeWidth;
62385
+ staff.y = currentY;
62386
+ if (!onlyFirstGroup) {
62387
+ staff.finalizeStaff();
62388
+ }
62389
+ if (staff.isVisible) {
62390
+ currentY += staff.height;
62391
+ anyStaffVisible = true;
62392
+ previousStaff = staff;
62393
+ if (!firstVisibleStaffInGroup) {
62394
+ firstVisibleStaffInGroup = staff;
62395
+ }
62396
+ lastVisibleStaffInGroup = staff;
62397
+ }
62398
+ endSpikeOverflow = 0;
62399
+ if (hasBracket && bracket.lastStaffInBracket === staff) {
62400
+ const spikeOverflow = bottomBracketSpikeHeight - staff.bottomOverflow;
62401
+ if (spikeOverflow > 0) {
62402
+ if (staff.isVisible) {
62403
+ currentY += spikeOverflow;
62404
+ }
62405
+ else {
62406
+ endSpikeOverflow = spikeOverflow;
62407
+ }
62408
+ }
61766
62409
  }
61767
62410
  }
61768
- staff.x = this.accoladeWidth;
61769
- staff.y = currentY;
61770
- staff.finalizeStaff();
61771
- currentY += staff.height;
61772
- if (hasBracket && bracket.lastStaffInBracket === staff) {
61773
- const spikeOverflow = bottomBracketSpikeHeight - staff.bottomOverflow;
61774
- if (spikeOverflow > 0) {
61775
- currentY += spikeOverflow;
61776
- }
62411
+ group.firstVisibleStaff = firstVisibleStaffInGroup;
62412
+ group.lastVisibleStaff = lastVisibleStaffInGroup;
62413
+ if (!this.firstVisibleStaff) {
62414
+ this.firstVisibleStaff = firstVisibleStaffInGroup;
62415
+ }
62416
+ if (onlyFirstGroup) {
62417
+ break;
61777
62418
  }
61778
- previousStaff = staff;
61779
62419
  }
61780
- this._contentHeight = currentY;
61781
- for (const b of this._brackets) {
61782
- b.finalizeBracket(smufl);
62420
+ // ensure we add overflow if last bracket is hidden
62421
+ if (endSpikeOverflow) {
62422
+ currentY += endSpikeOverflow;
61783
62423
  }
62424
+ this._contentHeight = currentY;
62425
+ return anyStaffVisible;
61784
62426
  }
61785
62427
  buildBoundingsLookup(cx, cy) {
61786
62428
  if (this.layout.renderer.boundsLookup.isFinished) {
@@ -61814,6 +62456,9 @@ class StaffSystem {
61814
62456
  const masterBarBoundsLookup = new Map();
61815
62457
  for (let i = 0; i < this.staves.length; i++) {
61816
62458
  for (const staff of this.staves[i].staves) {
62459
+ if (!staff.isVisible) {
62460
+ continue;
62461
+ }
61817
62462
  for (const renderer of staff.barRenderers) {
61818
62463
  let masterBarBounds;
61819
62464
  if (!masterBarBoundsLookup.has(renderer.bar.masterBar.index)) {
@@ -62124,8 +62769,9 @@ class ScoreLayout {
62124
62769
  }
62125
62770
  firstBarIndex = 0;
62126
62771
  lastBarIndex = 0;
62127
- createEmptyStaffSystem() {
62772
+ createEmptyStaffSystem(index) {
62128
62773
  const system = new StaffSystem(this);
62774
+ system.index = index;
62129
62775
  const allFactories = Environment.defaultRenderers;
62130
62776
  const renderStaves = [];
62131
62777
  for (let trackIndex = 0; trackIndex < this.renderer.tracks.length; trackIndex++) {
@@ -62137,7 +62783,7 @@ class ScoreLayout {
62137
62783
  let previousStaff = undefined;
62138
62784
  for (const factory of allFactories) {
62139
62785
  if (this.profile.has(factory.staffId) && factory.canCreate(track, staff)) {
62140
- const renderStaff = new RenderStaff(trackIndex, staff, factory);
62786
+ const renderStaff = new RenderStaff(system, trackIndex, staff, factory);
62141
62787
  // insert shared effect bands at front
62142
62788
  renderStaff.topEffectInfos.splice(0, 0, ...sharedTopEffects);
62143
62789
  renderStaff.bottomEffectInfos.push(...sharedBottomEffects);
@@ -65295,7 +65941,7 @@ class HorizontalScreenLayout extends ScoreLayout {
65295
65941
  }
65296
65942
  endBarIndex = startIndex + endBarIndex - 1; // map count to array index
65297
65943
  endBarIndex = Math.min(score.masterBars.length - 1, Math.max(0, endBarIndex));
65298
- this._system = this.createEmptyStaffSystem();
65944
+ this._system = this.createEmptyStaffSystem(0);
65299
65945
  this._system.isLast = true;
65300
65946
  this._system.x = this.pagePadding[0];
65301
65947
  this._system.y = this.pagePadding[1];
@@ -65568,8 +66214,7 @@ class PageViewLayout extends ScoreLayout {
65568
66214
  this._systems = [];
65569
66215
  let currentIndex = 0;
65570
66216
  const maxWidth = this._maxWidth;
65571
- let system = this.createEmptyStaffSystem();
65572
- system.index = this._systems.length;
66217
+ let system = this.createEmptyStaffSystem(this._systems.length);
65573
66218
  system.x = this.pagePadding[0];
65574
66219
  system.y = y;
65575
66220
  while (currentIndex < this._allMasterBarRenderers.length) {
@@ -65600,8 +66245,7 @@ class PageViewLayout extends ScoreLayout {
65600
66245
  this._fitSystem(system);
65601
66246
  y += this._paintSystem(system, oldHeight);
65602
66247
  // note: we do not increase currentIndex here to have it added to the next system
65603
- system = this.createEmptyStaffSystem();
65604
- system.index = this._systems.length;
66248
+ system = this.createEmptyStaffSystem(this._systems.length);
65605
66249
  system.x = this.pagePadding[0];
65606
66250
  system.y = y;
65607
66251
  }
@@ -65686,8 +66330,7 @@ class PageViewLayout extends ScoreLayout {
65686
66330
  return barsPerRow;
65687
66331
  }
65688
66332
  _createStaffSystem(currentBarIndex, endIndex) {
65689
- const system = this.createEmptyStaffSystem();
65690
- system.index = this._systems.length;
66333
+ const system = this.createEmptyStaffSystem(this._systems.length);
65691
66334
  const barsPerRow = this._getBarsPerSystem(system.index);
65692
66335
  const maxWidth = this._maxWidth;
65693
66336
  const end = endIndex + 1;
@@ -68367,16 +69010,77 @@ class NumberedBarRendererFactory extends BarRendererFactory {
68367
69010
  class ClefGlyph extends MusicFontGlyph {
68368
69011
  _clef;
68369
69012
  _clefOttava;
69013
+ _ottavaGlyph;
68370
69014
  constructor(x, y, clef, clefOttava) {
68371
69015
  super(x, y, 1, ClefGlyph._getSymbol(clef, clefOttava));
68372
69016
  this._clef = clef;
68373
69017
  this._clefOttava = clefOttava;
68374
69018
  }
69019
+ getBoundingBoxTop() {
69020
+ let top = super.getBoundingBoxTop();
69021
+ const ottava = this._ottavaGlyph;
69022
+ if (ottava) {
69023
+ const ottavaTop = this.y + ottava.getBoundingBoxTop();
69024
+ top = ModelUtils.minBoundingBox(top, ottavaTop);
69025
+ }
69026
+ return top;
69027
+ }
69028
+ getBoundingBoxBottom() {
69029
+ let bottom = super.getBoundingBoxBottom();
69030
+ const ottava = this._ottavaGlyph;
69031
+ if (ottava) {
69032
+ const ottavaBottom = this.y + ottava.getBoundingBoxBottom();
69033
+ bottom = ModelUtils.maxBoundingBox(bottom, ottavaBottom);
69034
+ }
69035
+ return bottom;
69036
+ }
68375
69037
  doLayout() {
68376
69038
  this.center = true;
68377
69039
  super.doLayout();
68378
69040
  this.width = this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.GClef);
68379
69041
  this.offsetX = this.width / 2;
69042
+ this._ottavaGlyph = undefined;
69043
+ switch (this._clef) {
69044
+ case Clef.C3:
69045
+ case Clef.C4:
69046
+ switch (this._clefOttava) {
69047
+ case Ottavia._8vb:
69048
+ return;
69049
+ }
69050
+ break;
69051
+ case Clef.F4:
69052
+ case Clef.G2:
69053
+ return;
69054
+ }
69055
+ let ottavaSymbol;
69056
+ let top = false;
69057
+ switch (this._clefOttava) {
69058
+ case Ottavia._15ma:
69059
+ ottavaSymbol = MusicFontSymbol.Clef15;
69060
+ top = true;
69061
+ break;
69062
+ case Ottavia._8va:
69063
+ ottavaSymbol = MusicFontSymbol.Clef8;
69064
+ top = true;
69065
+ break;
69066
+ case Ottavia._8vb:
69067
+ ottavaSymbol = MusicFontSymbol.Clef8;
69068
+ break;
69069
+ case Ottavia._15mb:
69070
+ ottavaSymbol = MusicFontSymbol.Clef15;
69071
+ break;
69072
+ default:
69073
+ return;
69074
+ }
69075
+ const ottavaX = this.width / 2;
69076
+ const ottavaY = top
69077
+ ? this.renderer.smuflMetrics.glyphTop.get(this.symbol)
69078
+ : this.renderer.smuflMetrics.glyphBottom.get(this.symbol) -
69079
+ this.renderer.smuflMetrics.glyphHeights.get(ottavaSymbol);
69080
+ this._ottavaGlyph = new MusicFontGlyph(ottavaX, -ottavaY, 1, ottavaSymbol);
69081
+ this._ottavaGlyph.center = true;
69082
+ this._ottavaGlyph.renderer = this.renderer;
69083
+ this._ottavaGlyph.doLayout();
68380
69084
  }
68381
69085
  static _getSymbol(clef, clefOttava) {
68382
69086
  switch (clef) {
@@ -68424,44 +69128,10 @@ class ClefGlyph extends MusicFontGlyph {
68424
69128
  const _ = ElementStyleHelper.bar(canvas, BarSubElement.StandardNotationClef, this.renderer.bar);
68425
69129
  try {
68426
69130
  super.paint(cx, cy, canvas);
68427
- switch (this._clef) {
68428
- case Clef.C3:
68429
- case Clef.C4:
68430
- switch (this._clefOttava) {
68431
- case Ottavia._8vb:
68432
- return;
68433
- }
68434
- break;
68435
- case Clef.F4:
68436
- case Clef.G2:
68437
- return;
68438
- }
68439
- let ottavaGlyph;
68440
- let top = false;
68441
- switch (this._clefOttava) {
68442
- case Ottavia._15ma:
68443
- ottavaGlyph = MusicFontSymbol.Clef15;
68444
- top = true;
68445
- break;
68446
- case Ottavia._8va:
68447
- ottavaGlyph = MusicFontSymbol.Clef8;
68448
- top = true;
68449
- break;
68450
- case Ottavia._8vb:
68451
- ottavaGlyph = MusicFontSymbol.Clef8;
68452
- break;
68453
- case Ottavia._15mb:
68454
- ottavaGlyph = MusicFontSymbol.Clef15;
68455
- break;
68456
- default:
68457
- return;
69131
+ const ottava = this._ottavaGlyph;
69132
+ if (ottava) {
69133
+ ottava.paint(cx + this.x, cy + this.y, canvas);
68458
69134
  }
68459
- const ottavaX = this.width / 2;
68460
- const ottavaY = top
68461
- ? this.renderer.smuflMetrics.glyphTop.get(this.symbol)
68462
- : this.renderer.smuflMetrics.glyphBottom.get(this.symbol) -
68463
- this.renderer.smuflMetrics.glyphHeights.get(ottavaGlyph);
68464
- CanvasHelper.fillMusicFontSymbolSafe(canvas, cx + this.x + ottavaX, cy + this.y - ottavaY, 1, ottavaGlyph, true);
68465
69135
  }
68466
69136
  finally {
68467
69137
  _?.[Symbol.dispose]?.();