@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.
package/dist/alphaTab.js CHANGED
@@ -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
  *
@@ -209,9 +209,9 @@
209
209
  * @internal
210
210
  */
211
211
  class VersionInfo {
212
- static version = '1.8.0-alpha.1647';
213
- static date = '2025-12-17T02:10:13.730Z';
214
- static commit = 'a1bd11d4a3eef22409f1f49a5dd172ace16108cc';
212
+ static version = '1.8.0-alpha.1651';
213
+ static date = '2025-12-21T02:27:24.035Z';
214
+ static commit = '758ae890dfa8cdb21a9dd469ade012f834ac0433';
215
215
  static print(print) {
216
216
  print(`alphaTab ${VersionInfo.version}`);
217
217
  print(`commit: ${VersionInfo.commit}`);
@@ -2750,6 +2750,23 @@
2750
2750
  * Whether barlines should be drawn across staves within the same system.
2751
2751
  */
2752
2752
  extendBarLines = false;
2753
+ /**
2754
+ * Whether to hide empty staves.
2755
+ */
2756
+ hideEmptyStaves = false;
2757
+ /**
2758
+ * Whether to also hide empty staves in the first system.
2759
+ * @remarks
2760
+ * Only has an effect when activating {@link hideEmptyStaves}.
2761
+ */
2762
+ hideEmptyStavesInFirstSystem = false;
2763
+ /**
2764
+ * Whether to show brackets and braces across single staves.
2765
+ * @remarks
2766
+ * This allows a more consistent view for identifying staves when using
2767
+ * {@link hideEmptyStaves}
2768
+ */
2769
+ showSingleStaffBrackets = false;
2753
2770
  }
2754
2771
 
2755
2772
  /**
@@ -8679,7 +8696,10 @@
8679
8696
  ['firstsystemtracknameorientation', [[[[10, 17], 0, ['horizontal', 'vertical']]]]],
8680
8697
  ['othersystemstracknameorientation', [[[[10, 17], 0, ['horizontal', 'vertical']]]]],
8681
8698
  ['extendbarlines', null],
8682
- ['chorddiagramsinscore', [[[[10], 1, ['true', 'false']]]]]
8699
+ ['chorddiagramsinscore', [[[[10], 1, ['true', 'false']]]]],
8700
+ ['hideemptystaves', null],
8701
+ ['hideemptystavesinfirstsystem', null],
8702
+ ['showsinglestaffbrackets', null]
8683
8703
  ]);
8684
8704
  static staffMetaDataSignatures = AlphaTex1LanguageDefinitions._signatures([
8685
8705
  ['tuning', [[[[10, 17], 0, ['piano', 'none', 'voice']]], [[[10, 17], 5]]]],
@@ -9019,6 +9039,9 @@
9019
9039
  ['othersystemstracknameorientation', null],
9020
9040
  ['extendbarlines', null],
9021
9041
  ['chorddiagramsinscore', null],
9042
+ ['hideemptystaves', null],
9043
+ ['hideemptystavesinfirstsystem', null],
9044
+ ['showsinglestaffbrackets', null],
9022
9045
  [
9023
9046
  'tuning',
9024
9047
  [
@@ -13072,7 +13095,7 @@
13072
13095
  */
13073
13096
  class MasterBarBounds {
13074
13097
  /**
13075
- * Gets or sets the index of this bounds relative within the parent lookup.
13098
+ * The MasterBar index within the data model represented by these bounds.
13076
13099
  */
13077
13100
  index = 0;
13078
13101
  /**
@@ -13280,6 +13303,7 @@
13280
13303
  mb.visualBounds = this._boundsToJson(masterBar.visualBounds);
13281
13304
  mb.realBounds = this._boundsToJson(masterBar.realBounds);
13282
13305
  mb.index = masterBar.index;
13306
+ mb.isFirstOfLine = masterBar.isFirstOfLine;
13283
13307
  mb.bars = [];
13284
13308
  for (const bar of masterBar.bars) {
13285
13309
  const b = {};
@@ -13336,7 +13360,7 @@
13336
13360
  mb.lineAlignedBounds = BoundsLookup._boundsFromJson(masterBar.lineAlignedBounds);
13337
13361
  mb.visualBounds = BoundsLookup._boundsFromJson(masterBar.visualBounds);
13338
13362
  mb.realBounds = BoundsLookup._boundsFromJson(masterBar.realBounds);
13339
- sg.addBar(mb);
13363
+ lookup.addMasterBar(mb);
13340
13364
  for (const bar of masterBar.bars) {
13341
13365
  const b = new BarBounds();
13342
13366
  b.visualBounds = BoundsLookup._boundsFromJson(bar.visualBounds);
@@ -13893,6 +13917,15 @@
13893
13917
  ? AlphaTex1LanguageHandler._booleanLikeValue(metaData.arguments.arguments, 0)
13894
13918
  : true;
13895
13919
  return ApplyNodeResult.Applied;
13920
+ case 'hideemptystaves':
13921
+ score.stylesheet.hideEmptyStaves = true;
13922
+ return ApplyNodeResult.Applied;
13923
+ case 'hideemptystavesinfirstsystem':
13924
+ score.stylesheet.hideEmptyStavesInFirstSystem = true;
13925
+ return ApplyNodeResult.Applied;
13926
+ case 'showsinglestaffbrackets':
13927
+ score.stylesheet.showSingleStaffBrackets = true;
13928
+ return ApplyNodeResult.Applied;
13896
13929
  default:
13897
13930
  return ApplyNodeResult.NotAppliedUnrecognizedMarker;
13898
13931
  }
@@ -15724,6 +15757,15 @@
15724
15757
  if (stylesheet.globalDisplayChordDiagramsInScore) {
15725
15758
  nodes.push(Atnf.meta('chordDiagramsInScore'));
15726
15759
  }
15760
+ if (stylesheet.hideEmptyStaves) {
15761
+ nodes.push(Atnf.meta('hideEmptyStaves'));
15762
+ }
15763
+ if (stylesheet.hideEmptyStavesInFirstSystem) {
15764
+ nodes.push(Atnf.meta('hideEmptyStavesInFirstSystem'));
15765
+ }
15766
+ if (stylesheet.showSingleStaffBrackets) {
15767
+ nodes.push(Atnf.meta('showSingleStaffBrackets'));
15768
+ }
15727
15769
  // Unsupported:
15728
15770
  // 'globaldisplaychorddiagramsontop',
15729
15771
  // 'pertrackchorddiagramsontop',
@@ -38810,6 +38852,9 @@
38810
38852
  }
38811
38853
  }
38812
38854
  o.set("extendbarlines", obj.extendBarLines);
38855
+ o.set("hideemptystaves", obj.hideEmptyStaves);
38856
+ o.set("hideemptystavesinfirstsystem", obj.hideEmptyStavesInFirstSystem);
38857
+ o.set("showsinglestaffbrackets", obj.showSingleStaffBrackets);
38813
38858
  return o;
38814
38859
  }
38815
38860
  static setProperty(obj, property, v) {
@@ -38871,6 +38916,15 @@
38871
38916
  case "extendbarlines":
38872
38917
  obj.extendBarLines = v;
38873
38918
  return true;
38919
+ case "hideemptystaves":
38920
+ obj.hideEmptyStaves = v;
38921
+ return true;
38922
+ case "hideemptystavesinfirstsystem":
38923
+ obj.hideEmptyStavesInFirstSystem = v;
38924
+ return true;
38925
+ case "showsinglestaffbrackets":
38926
+ obj.showSingleStaffBrackets = v;
38927
+ return true;
38874
38928
  }
38875
38929
  return false;
38876
38930
  }
@@ -43610,6 +43664,12 @@
43610
43664
  * Scrolling happens as soon the cursors exceed the displayed range.
43611
43665
  */
43612
43666
  ScrollMode[ScrollMode["OffScreen"] = 2] = "OffScreen";
43667
+ /**
43668
+ * Scrolling happens constantly in a smooth fashion.
43669
+ * This will disable the use of any native scroll optimizations but
43670
+ * manually scroll the scroll container in the required speed.
43671
+ */
43672
+ ScrollMode[ScrollMode["Smooth"] = 3] = "Smooth";
43613
43673
  })(exports.ScrollMode || (exports.ScrollMode = {}));
43614
43674
  /**
43615
43675
  * This object defines the details on how to generate the vibrato effects.
@@ -45898,6 +45958,19 @@
45898
45958
  }
45899
45959
  return null;
45900
45960
  }
45961
+ /**
45962
+ * Looks for the first visible beat which starts at this lookup so it can be used for cursor placement.
45963
+ * @param checker The custom checker to see if a beat is visible.
45964
+ * @returns The first beat which is visible according to the given tracks or null.
45965
+ */
45966
+ getVisibleBeatAtStartWithChecker(checker) {
45967
+ for (const b of this.highlightedBeats) {
45968
+ if (b.playbackStart === this.start && checker.isVisible(b.beat)) {
45969
+ return b.beat;
45970
+ }
45971
+ }
45972
+ return null;
45973
+ }
45901
45974
  }
45902
45975
 
45903
45976
  /**
@@ -46337,6 +46410,18 @@
46337
46410
  }
46338
46411
  }
46339
46412
  }
46413
+ /**
46414
+ * @internal
46415
+ */
46416
+ class TrackLookupBeatVisibilityChecker {
46417
+ _lookup;
46418
+ constructor(lookup) {
46419
+ this._lookup = lookup;
46420
+ }
46421
+ isVisible(beat) {
46422
+ return this._lookup.has(beat.voice.bar.staff.track.index);
46423
+ }
46424
+ }
46340
46425
  /**
46341
46426
  * This class holds all information about when {@link MasterBar}s and {@link Beat}s are played.
46342
46427
  *
@@ -46397,31 +46482,44 @@
46397
46482
  * @returns The information about the current beat or null if no beat could be found.
46398
46483
  */
46399
46484
  findBeat(trackLookup, tick, currentBeatHint = null) {
46485
+ return this.findBeatWithChecker(new TrackLookupBeatVisibilityChecker(trackLookup), tick, currentBeatHint);
46486
+ }
46487
+ /**
46488
+ * Finds the currently played beat given a list of tracks and the current time.
46489
+ * @param checker The checker to ask whether a beat is visible and should be considered for result.
46490
+ * @param tick The current time in midi ticks.
46491
+ * @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).
46492
+ * @returns The information about the current beat or null if no beat could be found.
46493
+ */
46494
+ findBeatWithChecker(checker, tick, currentBeatHint = null) {
46400
46495
  let result = null;
46401
46496
  if (currentBeatHint) {
46402
- result = this._findBeatFast(trackLookup, currentBeatHint, tick);
46497
+ result = this._findBeatFast(checker, currentBeatHint, tick);
46403
46498
  }
46404
46499
  if (!result) {
46405
- result = this._findBeatSlow(trackLookup, currentBeatHint, tick, false);
46500
+ result = this._findBeatSlow(checker, currentBeatHint, tick, false);
46406
46501
  }
46407
46502
  return result;
46408
46503
  }
46409
- _findBeatFast(trackLookup, currentBeatHint, tick) {
46504
+ _findBeatFast(checker, currentBeatHint, tick) {
46410
46505
  // still within current lookup.
46411
46506
  if (tick >= currentBeatHint.start && tick < currentBeatHint.end) {
46412
46507
  return currentBeatHint;
46413
46508
  }
46414
46509
  // already on the next beat?
46415
- if (currentBeatHint.nextBeat && tick >= currentBeatHint.nextBeat.start && tick < currentBeatHint.nextBeat.end) {
46510
+ if (currentBeatHint.nextBeat &&
46511
+ tick >= currentBeatHint.nextBeat.start &&
46512
+ tick < currentBeatHint.nextBeat.end &&
46513
+ (checker === undefined || checker.isVisible(currentBeatHint.nextBeat.beat))) {
46416
46514
  const next = currentBeatHint.nextBeat;
46417
46515
  // fill next in chain
46418
- this._fillNextBeat(next, trackLookup);
46516
+ this._fillNextBeat(next, checker);
46419
46517
  return next;
46420
46518
  }
46421
46519
  // likely a loop or manual seek, need to fallback to slow path
46422
46520
  return null;
46423
46521
  }
46424
- _fillNextBeatMultiBarRest(current, trackLookup) {
46522
+ _fillNextBeatMultiBarRest(current, checker) {
46425
46523
  const group = this.multiBarRestInfo.get(current.masterBar.masterBar.index);
46426
46524
  // this is a bit sensitive. we assume that the sequence of multi-rest bars and the
46427
46525
  // chained nextMasterBar equal. so we just jump over X bars
@@ -46435,7 +46533,7 @@
46435
46533
  if (endMasterBar) {
46436
46534
  // one more following -> use start of next
46437
46535
  if (endMasterBar.nextMasterBar) {
46438
- current.nextBeat = this._firstBeatInMasterBar(trackLookup, endMasterBar.nextMasterBar, endMasterBar.nextMasterBar.start, true);
46536
+ current.nextBeat = this._firstBeatInMasterBar(checker, endMasterBar.nextMasterBar, endMasterBar.nextMasterBar.start, true);
46439
46537
  // if we have the next beat take the difference between the times as duration
46440
46538
  if (current.nextBeat) {
46441
46539
  current.tickDuration = current.nextBeat.start - current.start;
@@ -46465,19 +46563,19 @@
46465
46563
  }
46466
46564
  current.calculateDuration();
46467
46565
  }
46468
- _fillNextBeat(current, trackLookup) {
46566
+ _fillNextBeat(current, checker) {
46469
46567
  // on multibar rests take the duration until the end.
46470
46568
  if (this._isMultiBarRestResult(current)) {
46471
- this._fillNextBeatMultiBarRest(current, trackLookup);
46569
+ this._fillNextBeatMultiBarRest(current, checker);
46472
46570
  }
46473
46571
  else {
46474
- this._fillNextBeatDefault(current, trackLookup);
46572
+ this._fillNextBeatDefault(current, checker);
46475
46573
  }
46476
46574
  }
46477
- _fillNextBeatDefault(current, trackLookup) {
46478
- current.nextBeat = this._findBeatInMasterBar(current.masterBar, current.beatLookup.nextBeat, current.end, trackLookup, true);
46575
+ _fillNextBeatDefault(current, checker) {
46576
+ current.nextBeat = this._findBeatInMasterBar(current.masterBar, current.beatLookup.nextBeat, current.end, checker, true);
46479
46577
  if (current.nextBeat == null) {
46480
- current.nextBeat = this._findBeatSlow(trackLookup, current, current.end, true);
46578
+ current.nextBeat = this._findBeatSlow(checker, current, current.end, true);
46481
46579
  }
46482
46580
  // if we have the next beat take the difference between the times as duration
46483
46581
  if (current.nextBeat) {
@@ -46508,7 +46606,7 @@
46508
46606
  beat.isRest &&
46509
46607
  beat.voice.bar.isRestOnly);
46510
46608
  }
46511
- _findBeatSlow(trackLookup, currentBeatHint, tick, isNextSearch) {
46609
+ _findBeatSlow(checker, currentBeatHint, tick, isNextSearch) {
46512
46610
  // get all beats within the masterbar
46513
46611
  let masterBar = null;
46514
46612
  if (currentBeatHint != null) {
@@ -46530,14 +46628,14 @@
46530
46628
  if (!masterBar) {
46531
46629
  return null;
46532
46630
  }
46533
- return this._firstBeatInMasterBar(trackLookup, masterBar, tick, isNextSearch);
46631
+ return this._firstBeatInMasterBar(checker, masterBar, tick, isNextSearch);
46534
46632
  }
46535
- _firstBeatInMasterBar(trackLookup, startMasterBar, tick, isNextSearch) {
46633
+ _firstBeatInMasterBar(checker, startMasterBar, tick, isNextSearch) {
46536
46634
  let masterBar = startMasterBar;
46537
46635
  // scan through beats and find first one which has a beat visible
46538
46636
  while (masterBar) {
46539
46637
  if (masterBar.firstBeat) {
46540
- const beat = this._findBeatInMasterBar(masterBar, masterBar.firstBeat, tick, trackLookup, isNextSearch);
46638
+ const beat = this._findBeatInMasterBar(masterBar, masterBar.firstBeat, tick, checker, isNextSearch);
46541
46639
  if (beat) {
46542
46640
  return beat;
46543
46641
  }
@@ -46555,7 +46653,7 @@
46555
46653
  * @param isNextSearch
46556
46654
  * @returns
46557
46655
  */
46558
- _findBeatInMasterBar(masterBar, currentStartLookup, tick, visibleTracks, isNextSearch) {
46656
+ _findBeatInMasterBar(masterBar, currentStartLookup, tick, checker, isNextSearch) {
46559
46657
  if (!currentStartLookup) {
46560
46658
  return null;
46561
46659
  }
@@ -46569,7 +46667,7 @@
46569
46667
  (currentStartLookup.start <= relativeTick || (isNextSearch && relativeTick < 0)) &&
46570
46668
  relativeTick < currentStartLookup.end) {
46571
46669
  startBeatLookup = currentStartLookup;
46572
- startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks);
46670
+ startBeat = currentStartLookup.getVisibleBeatAtStartWithChecker(checker);
46573
46671
  // found the matching beat lookup but none of the beats are visible
46574
46672
  // in this case scan further to the next lookup which has any visible beat
46575
46673
  if (!startBeat) {
@@ -46577,7 +46675,7 @@
46577
46675
  let currentMasterBar = masterBar;
46578
46676
  while (currentMasterBar != null && startBeat == null) {
46579
46677
  while (currentStartLookup != null) {
46580
- startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks);
46678
+ startBeat = currentStartLookup.getVisibleBeatAtStartWithChecker(checker);
46581
46679
  if (startBeat) {
46582
46680
  startBeatLookup = currentStartLookup;
46583
46681
  masterBar = currentMasterBar;
@@ -46595,7 +46693,7 @@
46595
46693
  let currentMasterBar = masterBar;
46596
46694
  while (currentMasterBar != null && startBeat == null) {
46597
46695
  while (currentStartLookup != null) {
46598
- startBeat = currentStartLookup.getVisibleBeatAtStart(visibleTracks);
46696
+ startBeat = currentStartLookup.getVisibleBeatAtStartWithChecker(checker);
46599
46697
  if (startBeat) {
46600
46698
  startBeatLookup = currentStartLookup;
46601
46699
  masterBar = currentMasterBar;
@@ -46619,17 +46717,17 @@
46619
46717
  if (startBeat == null) {
46620
46718
  return null;
46621
46719
  }
46622
- const result = this._createResult(masterBar, startBeatLookup, startBeat, isNextSearch, visibleTracks);
46720
+ const result = this._createResult(masterBar, startBeatLookup, startBeat, isNextSearch, checker);
46623
46721
  return result;
46624
46722
  }
46625
- _createResult(masterBar, beatLookup, beat, isNextSearch, visibleTracks) {
46723
+ _createResult(masterBar, beatLookup, beat, isNextSearch, checker) {
46626
46724
  const result = new MidiTickLookupFindBeatResult(masterBar);
46627
46725
  result.beat = beat;
46628
46726
  result.beatLookup = beatLookup;
46629
46727
  result.tickDuration = beatLookup.end - beatLookup.start;
46630
46728
  if (!isNextSearch) {
46631
46729
  // the next beat filling will adjust this result with the respective durations
46632
- this._fillNextBeat(result, visibleTracks);
46730
+ this._fillNextBeat(result, checker);
46633
46731
  }
46634
46732
  else if (this._isMultiBarRestResult(result)) {
46635
46733
  const multiRest = this.multiBarRestInfo.get(masterBar.masterBar.index);
@@ -48502,6 +48600,25 @@
48502
48600
  }
48503
48601
  }
48504
48602
 
48603
+ /**
48604
+ * Represents the information related to a resize event.
48605
+ * @public
48606
+ */
48607
+ class ResizeEventArgs {
48608
+ /**
48609
+ * Gets the size before the resizing happened.
48610
+ */
48611
+ oldWidth = 0;
48612
+ /**
48613
+ * Gets the size after the resize was complete.
48614
+ */
48615
+ newWidth = 0;
48616
+ /**
48617
+ * Gets the settings currently used for rendering.
48618
+ */
48619
+ settings = null;
48620
+ }
48621
+
48505
48622
  /**
48506
48623
  * Lists the different position modes for {@link BarRendererBase.getBeatX}
48507
48624
  * @internal
@@ -48928,22 +49045,262 @@
48928
49045
  }
48929
49046
 
48930
49047
  /**
48931
- * Represents the information related to a resize event.
48932
- * @public
49048
+ * Some basic scroll handler checking for changed offsets and scroll if changed.
49049
+ * @internal
48933
49050
  */
48934
- class ResizeEventArgs {
48935
- /**
48936
- * Gets the size before the resizing happened.
48937
- */
48938
- oldWidth = 0;
48939
- /**
48940
- * Gets the size after the resize was complete.
48941
- */
48942
- newWidth = 0;
48943
- /**
48944
- * Gets the settings currently used for rendering.
48945
- */
48946
- settings = null;
49051
+ class BasicScrollHandler {
49052
+ api;
49053
+ lastScroll = -1;
49054
+ constructor(api) {
49055
+ this.api = api;
49056
+ }
49057
+ [Symbol.dispose]() {
49058
+ }
49059
+ forceScrollTo(currentBeatBounds) {
49060
+ this._scrollToBeat(currentBeatBounds, true);
49061
+ this.lastScroll = -1; // force new scroll on next update
49062
+ }
49063
+ _scrollToBeat(currentBeatBounds, force) {
49064
+ const newLastScroll = this.calculateLastScroll(currentBeatBounds);
49065
+ // no change, and no instant/force scroll
49066
+ if (newLastScroll === this.lastScroll && !force) {
49067
+ return;
49068
+ }
49069
+ this.lastScroll = newLastScroll;
49070
+ this.doScroll(currentBeatBounds);
49071
+ }
49072
+ onBeatCursorUpdating(startBeat, _endBeat, _cursorMode, _actualBeatCursorStartX, _actualBeatCursorEndX, _actualBeatCursorTransitionDuration) {
49073
+ this._scrollToBeat(startBeat, false);
49074
+ }
49075
+ }
49076
+ /**
49077
+ * This is the default scroll handler for vertical layouts using {@link ScrollMode.Continuous}.
49078
+ * Whenever the system changes, we scroll to the new system position vertically.
49079
+ * @internal
49080
+ */
49081
+ class VerticalContinuousScrollHandler extends BasicScrollHandler {
49082
+ calculateLastScroll(currentBeatBounds) {
49083
+ return currentBeatBounds.barBounds.masterBarBounds.realBounds.y;
49084
+ }
49085
+ doScroll(currentBeatBounds) {
49086
+ const ui = this.api.uiFacade;
49087
+ const settings = this.api.settings;
49088
+ const scroll = ui.getScrollContainer();
49089
+ const elementOffset = ui.getOffset(scroll, this.api.container);
49090
+ const y = currentBeatBounds.barBounds.masterBarBounds.realBounds.y + settings.player.scrollOffsetY;
49091
+ ui.scrollToY(scroll, elementOffset.y + y, this.api.settings.player.scrollSpeed);
49092
+ }
49093
+ }
49094
+ /**
49095
+ * This is the default scroll handler for vertical layouts using {@link ScrollMode.OffScreen}.
49096
+ * Whenever the system changes, we check if the new system bounds are out-of-screen and if yes, we scroll.
49097
+ * @internal
49098
+ */
49099
+ class VerticalOffScreenScrollHandler extends BasicScrollHandler {
49100
+ calculateLastScroll(currentBeatBounds) {
49101
+ // check for system change
49102
+ return currentBeatBounds.barBounds.masterBarBounds.realBounds.y;
49103
+ }
49104
+ doScroll(currentBeatBounds) {
49105
+ const ui = this.api.uiFacade;
49106
+ const settings = this.api.settings;
49107
+ const scroll = ui.getScrollContainer();
49108
+ const elementBottom = scroll.scrollTop + ui.getOffset(null, scroll).h;
49109
+ const barBoundings = currentBeatBounds.barBounds.masterBarBounds;
49110
+ if (barBoundings.visualBounds.y + barBoundings.visualBounds.h >= elementBottom ||
49111
+ barBoundings.visualBounds.y < scroll.scrollTop) {
49112
+ const scrollTop = barBoundings.realBounds.y + settings.player.scrollOffsetY;
49113
+ ui.scrollToY(scroll, scrollTop, settings.player.scrollSpeed);
49114
+ }
49115
+ }
49116
+ }
49117
+ /**
49118
+ * This is the default scroll handler for vertical layouts using {@link ScrollMode.Smooth}.
49119
+ * vertical smooth scrolling aims to place the on-time position
49120
+ * at scrollOffsetY **at the time when a system starts**
49121
+ * this means when a system starts, it is at scrollOffsetY,
49122
+ * then gradually scrolls down the system height reaching the bottom
49123
+ * when the system completes.
49124
+ * @internal
49125
+ */
49126
+ class VerticalSmoothScrollHandler {
49127
+ _api;
49128
+ _lastScroll = -1;
49129
+ _scrollContainerResizeUnregister;
49130
+ constructor(api) {
49131
+ this._api = api;
49132
+ // we need a resize listener for the overflow calculation
49133
+ this._scrollContainerResizeUnregister = api.uiFacade.getScrollContainer().resize.on(() => {
49134
+ const scrollContainer = api.uiFacade.getScrollContainer();
49135
+ const overflowNeeded = api.settings.player.scrollOffsetX;
49136
+ const viewPortSize = scrollContainer.width;
49137
+ // the content needs to shift out of screen (and back into screen with the offset)
49138
+ // that's why we need the whole width as additional overflow
49139
+ const overflowNeededAbsolute = viewPortSize + overflowNeeded;
49140
+ api.uiFacade.setCanvasOverflow(api.canvasElement, overflowNeededAbsolute, true);
49141
+ });
49142
+ }
49143
+ [Symbol.dispose]() {
49144
+ this._api.uiFacade.setCanvasOverflow(this._api.canvasElement, 0, true);
49145
+ this._scrollContainerResizeUnregister();
49146
+ }
49147
+ forceScrollTo(currentBeatBounds) {
49148
+ const ui = this._api.uiFacade;
49149
+ const settings = this._api.settings;
49150
+ const scroll = ui.getScrollContainer();
49151
+ const systemTop = currentBeatBounds.barBounds.masterBarBounds.realBounds.y + settings.player.scrollOffsetY;
49152
+ ui.scrollToY(scroll, systemTop, 0);
49153
+ this._lastScroll = -1;
49154
+ }
49155
+ onBeatCursorUpdating(startBeat, _endBeat, _cursorMode, _actualBeatCursorStartX, _actualBeatCursorEndX, actualBeatCursorTransitionDuration) {
49156
+ const ui = this._api.uiFacade;
49157
+ const settings = this._api.settings;
49158
+ const barBoundings = startBeat.barBounds.masterBarBounds;
49159
+ const systemTop = barBoundings.realBounds.y + settings.player.scrollOffsetY;
49160
+ if (systemTop === this._lastScroll && actualBeatCursorTransitionDuration > 0) {
49161
+ return;
49162
+ }
49163
+ // jump to start of new system
49164
+ const scroll = ui.getScrollContainer();
49165
+ ui.scrollToY(scroll, systemTop, 0);
49166
+ // instant scroll
49167
+ if (actualBeatCursorTransitionDuration === 0) {
49168
+ this._lastScroll = -1;
49169
+ return;
49170
+ }
49171
+ // dynamic scrolling
49172
+ this._lastScroll = systemTop;
49173
+ // scroll to bottom over time
49174
+ const systemBottom = systemTop + barBoundings.realBounds.h;
49175
+ // NOTE: this calculation is a bit more expensive, but we only do it once per system
49176
+ // so we should be good:
49177
+ // * the more bars we have, the longer the system will play, hence the duration can take a bit longer
49178
+ // * if we have less bars, we calculate more often, but the calculation will be faster because we sum up less bars.
49179
+ const systemDuration = this._calculateSystemDuration(barBoundings);
49180
+ ui.scrollToY(scroll, systemBottom, systemDuration);
49181
+ }
49182
+ _calculateSystemDuration(barBoundings) {
49183
+ const systemBars = barBoundings.staffSystemBounds.bars;
49184
+ const tickCache = this._api.tickCache;
49185
+ let duration = 0;
49186
+ const masterBars = this._api.score.masterBars;
49187
+ for (const bar of systemBars) {
49188
+ const mb = masterBars[bar.index];
49189
+ const mbInfo = tickCache.getMasterBar(mb);
49190
+ const tempoChanges = tickCache.getMasterBar(mb).tempoChanges;
49191
+ let tempo = tempoChanges[0].tempo;
49192
+ let tick = tempoChanges[0].tick;
49193
+ for (let i = 1; i < tempoChanges.length; i++) {
49194
+ const diff = tempoChanges[i].tick - tick;
49195
+ duration += MidiUtils.ticksToMillis(diff, tempo);
49196
+ tempo = tempoChanges[i].tempo;
49197
+ tick = tempoChanges[i].tick;
49198
+ }
49199
+ const toEnd = mbInfo.end - tick;
49200
+ duration += MidiUtils.ticksToMillis(toEnd, tempo);
49201
+ }
49202
+ return duration;
49203
+ }
49204
+ }
49205
+ /**
49206
+ * This is the default scroll handler for horizontal layouts using {@link ScrollMode.Continuous}.
49207
+ * Whenever the master bar changes, we scroll to the position horizontally.
49208
+ * @internal
49209
+ */
49210
+ class HorizontalContinuousScrollHandler extends BasicScrollHandler {
49211
+ calculateLastScroll(currentBeatBounds) {
49212
+ return currentBeatBounds.barBounds.masterBarBounds.visualBounds.x;
49213
+ }
49214
+ doScroll(currentBeatBounds) {
49215
+ const ui = this.api.uiFacade;
49216
+ const settings = this.api.settings;
49217
+ const scroll = ui.getScrollContainer();
49218
+ const barBoundings = currentBeatBounds.barBounds.masterBarBounds;
49219
+ const scrollLeftContinuous = barBoundings.realBounds.x + settings.player.scrollOffsetX;
49220
+ ui.scrollToX(scroll, scrollLeftContinuous, settings.player.scrollSpeed);
49221
+ }
49222
+ }
49223
+ /**
49224
+ * This is the default scroll handler for horizontal layouts using {@link ScrollMode.OffScreen}.
49225
+ * Whenever the system changes, we check if the new system bounds are out-of-screen and if yes, we scroll.
49226
+ * @internal
49227
+ */
49228
+ class HorizontalOffScreenScrollHandler extends BasicScrollHandler {
49229
+ calculateLastScroll(currentBeatBounds) {
49230
+ return currentBeatBounds.barBounds.masterBarBounds.visualBounds.x;
49231
+ }
49232
+ doScroll(currentBeatBounds) {
49233
+ const ui = this.api.uiFacade;
49234
+ const settings = this.api.settings;
49235
+ const scroll = ui.getScrollContainer();
49236
+ const elementRight = scroll.scrollLeft + ui.getOffset(null, scroll).w;
49237
+ const barBoundings = currentBeatBounds.barBounds.masterBarBounds;
49238
+ if (barBoundings.visualBounds.x + barBoundings.visualBounds.w >= elementRight ||
49239
+ barBoundings.visualBounds.x < scroll.scrollLeft) {
49240
+ const scrollLeftOffScreen = barBoundings.realBounds.x + settings.player.scrollOffsetX;
49241
+ ui.scrollToX(scroll, scrollLeftOffScreen, settings.player.scrollSpeed);
49242
+ }
49243
+ }
49244
+ }
49245
+ /**
49246
+ * This is the default scroll handler for horizontal layouts using {@link ScrollMode.Smooth}.
49247
+ * horiontal smooth scrolling aims to place the on-time position
49248
+ * at scrollOffsetX from a beat-to-beat perspective.
49249
+ * This achieves an steady cursor at the same position with rather the music sheet scrolling past it.
49250
+ * Due to some animation inconsistencies (e.g. CSS animation vs scrolling) there might be a slight
49251
+ * flickering of the cursor.
49252
+ *
49253
+ * To get a fully steady cursor the beat cursor can simply be visually hidden and a cursor can be placed at
49254
+ * `scrollOffsetX` by the integrator.
49255
+ * @internal
49256
+ */
49257
+ class HorizontalSmoothScrollHandler {
49258
+ _api;
49259
+ _lastScroll = -1;
49260
+ _scrollContainerResizeUnregister;
49261
+ constructor(api) {
49262
+ this._api = api;
49263
+ // we need a resize listener for the overflow calculation
49264
+ this._scrollContainerResizeUnregister = api.uiFacade.getScrollContainer().resize.on(() => {
49265
+ const scrollContainer = api.uiFacade.getScrollContainer();
49266
+ const overflowNeeded = api.settings.player.scrollOffsetX;
49267
+ const viewPortSize = scrollContainer.width;
49268
+ // the content needs to shift out of screen (and back into screen with the offset)
49269
+ // that's why we need the whole width as additional overflow
49270
+ const overflowNeededAbsolute = viewPortSize + overflowNeeded;
49271
+ api.uiFacade.setCanvasOverflow(api.canvasElement, overflowNeededAbsolute, false);
49272
+ });
49273
+ }
49274
+ [Symbol.dispose]() {
49275
+ this._scrollContainerResizeUnregister();
49276
+ this._api.uiFacade.setCanvasOverflow(this._api.canvasElement, 0, false);
49277
+ }
49278
+ forceScrollTo(currentBeatBounds) {
49279
+ const ui = this._api.uiFacade;
49280
+ const settings = this._api.settings;
49281
+ const scroll = ui.getScrollContainer();
49282
+ const barStartX = currentBeatBounds.onNotesX + settings.player.scrollOffsetY;
49283
+ ui.scrollToY(scroll, barStartX, 0);
49284
+ this._lastScroll = -1;
49285
+ }
49286
+ onBeatCursorUpdating(_startBeat, _endBeat, _cursorMode, actualBeatCursorStartX, actualBeatCursorEndX, actualBeatCursorTransitionDuration) {
49287
+ const ui = this._api.uiFacade;
49288
+ if (actualBeatCursorEndX === this._lastScroll && actualBeatCursorTransitionDuration > 0) {
49289
+ return;
49290
+ }
49291
+ // jump to start of new system
49292
+ const settings = this._api.settings;
49293
+ const scroll = ui.getScrollContainer();
49294
+ ui.scrollToX(scroll, actualBeatCursorStartX + settings.player.scrollOffsetX, 0);
49295
+ // instant scroll
49296
+ if (actualBeatCursorTransitionDuration === 0) {
49297
+ this._lastScroll = -1;
49298
+ return;
49299
+ }
49300
+ this._lastScroll = actualBeatCursorEndX;
49301
+ const scrollX = actualBeatCursorEndX + settings.player.scrollOffsetX;
49302
+ ui.scrollToX(scroll, scrollX, actualBeatCursorTransitionDuration);
49303
+ }
48947
49304
  }
48948
49305
 
48949
49306
  /**
@@ -49556,6 +49913,19 @@
49556
49913
  }
49557
49914
  }
49558
49915
 
49916
+ /**
49917
+ * @internal
49918
+ */
49919
+ class BoundsLookupVisibilityChecker {
49920
+ bounds = null;
49921
+ isVisible(beat) {
49922
+ const bounds = this.bounds;
49923
+ if (!bounds) {
49924
+ return false;
49925
+ }
49926
+ return bounds.findBeat(beat) !== null;
49927
+ }
49928
+ }
49559
49929
  /**
49560
49930
  * This class represents the public API of alphaTab and provides all logic to display
49561
49931
  * a music sheet in any UI using the given {@link IUiFacade}
@@ -49566,12 +49936,14 @@
49566
49936
  _startTime = 0;
49567
49937
  _trackIndexes = null;
49568
49938
  _trackIndexLookup = null;
49939
+ _beatVisibilityChecker = new BoundsLookupVisibilityChecker();
49569
49940
  _isDestroyed = false;
49570
49941
  _score = null;
49571
49942
  _tracks = [];
49572
49943
  _actualPlayerMode = exports.PlayerMode.Disabled;
49573
49944
  _player;
49574
49945
  _renderer;
49946
+ _defaultScrollHandler;
49575
49947
  /**
49576
49948
  * An indicator by how many midi-ticks the song contents are shifted.
49577
49949
  * Grace beats at start might require a shift for the first beat to start at 0.
@@ -50341,6 +50713,42 @@
50341
50713
  }
50342
50714
  }
50343
50715
  _tickCache = null;
50716
+ /**
50717
+ * A custom scroll handler which will be used to handle scrolling operations during playback.
50718
+ *
50719
+ * @category Properties - Player
50720
+ * @since 1.8.0
50721
+ * @example
50722
+ * JavaScript
50723
+ * ```js
50724
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
50725
+ * api.customScrollHandler = {
50726
+ * forceScrollTo(currentBeatBounds) {
50727
+ * const scroll = api.uiFacade.getScrollElement();
50728
+ * api.uiFacade.scrollToY(scroll, currentBeatBounds.barBounds.masterBarBounds.realBounds.y, 0);
50729
+ * },
50730
+ * onBeatCursorUpdating(startBeat, endBeat, cursorMode, relativePosition, actualBeatCursorStartX, actualBeatCursorEndX, actualBeatCursorTransitionDuration) {
50731
+ * const scroll = api.uiFacade.getScrollElement();
50732
+ * api.uiFacade.scrollToY(scroll, startBeat.barBounds.masterBarBounds.realBounds.y, 0);
50733
+ * }
50734
+ * }
50735
+ * ```
50736
+ *
50737
+ * @example
50738
+ * C#
50739
+ * ```cs
50740
+ * var api = new AlphaTabApi<MyControl>(...);
50741
+ * api.CustomScrollHandler = new CustomScrollHandler();
50742
+ * ```
50743
+ *
50744
+ * @example
50745
+ * Android
50746
+ * ```kotlin
50747
+ * val api = AlphaTabApi<MyControl>(...)
50748
+ * api.customScrollHandler = CustomScrollHandler();
50749
+ * ```
50750
+ */
50751
+ customScrollHandler;
50344
50752
  /**
50345
50753
  * The tick cache allowing lookup of midi ticks to beats.
50346
50754
  * @remarks
@@ -51319,7 +51727,6 @@
51319
51727
  _isInitialBeatCursorUpdate = true;
51320
51728
  _previousStateForCursor = PlayerState.Paused;
51321
51729
  _previousCursorCache = null;
51322
- _lastScroll = 0;
51323
51730
  _destroyCursors() {
51324
51731
  if (!this._cursorWrapper) {
51325
51732
  return;
@@ -51330,28 +51737,79 @@
51330
51737
  this._beatCursor = null;
51331
51738
  this._selectionWrapper = null;
51332
51739
  }
51740
+ _createCursors() {
51741
+ if (this._cursorWrapper) {
51742
+ return;
51743
+ }
51744
+ const cursors = this.uiFacade.createCursors();
51745
+ if (cursors) {
51746
+ // store options and created elements for fast access
51747
+ this._cursorWrapper = cursors.cursorWrapper;
51748
+ this._barCursor = cursors.barCursor;
51749
+ this._beatCursor = cursors.beatCursor;
51750
+ this._selectionWrapper = cursors.selectionWrapper;
51751
+ this._isInitialBeatCursorUpdate = true;
51752
+ }
51753
+ if (this._currentBeat !== null) {
51754
+ this._cursorUpdateBeat(this._currentBeat, false, this._previousTick > 10, 1, true);
51755
+ }
51756
+ }
51333
51757
  _updateCursors() {
51758
+ this._updateScrollHandler();
51334
51759
  const enable = this._hasCursor;
51335
- if (enable && !this._cursorWrapper) {
51336
- //
51337
- // Create cursors
51338
- const cursors = this.uiFacade.createCursors();
51339
- if (cursors) {
51340
- // store options and created elements for fast access
51341
- this._cursorWrapper = cursors.cursorWrapper;
51342
- this._barCursor = cursors.barCursor;
51343
- this._beatCursor = cursors.beatCursor;
51344
- this._selectionWrapper = cursors.selectionWrapper;
51345
- this._isInitialBeatCursorUpdate = true;
51346
- }
51347
- if (this._currentBeat !== null) {
51348
- this._cursorUpdateBeat(this._currentBeat, false, this._previousTick > 10, 1, true);
51349
- }
51760
+ if (enable) {
51761
+ this._createCursors();
51350
51762
  }
51351
51763
  else if (!enable && this._cursorWrapper) {
51352
51764
  this._destroyCursors();
51353
51765
  }
51354
51766
  }
51767
+ _scrollHandlerMode = exports.ScrollMode.Off;
51768
+ _scrollHandlerVertical = true;
51769
+ _updateScrollHandler() {
51770
+ const currentHandler = this._defaultScrollHandler;
51771
+ const scrollMode = this.settings.player.scrollMode;
51772
+ const isVertical = Environment.getLayoutEngineFactory(this.settings.display.layoutMode).vertical;
51773
+ // no change
51774
+ if (this._scrollHandlerMode === scrollMode && this._scrollHandlerVertical === isVertical) {
51775
+ return;
51776
+ }
51777
+ // destroy current handler in favor of new one
51778
+ if (currentHandler) {
51779
+ currentHandler[Symbol.dispose]();
51780
+ const scroll = this.uiFacade.getScrollContainer();
51781
+ this.uiFacade.stopScrolling(scroll);
51782
+ }
51783
+ switch (scrollMode) {
51784
+ case exports.ScrollMode.Off:
51785
+ this._defaultScrollHandler = undefined;
51786
+ break;
51787
+ case exports.ScrollMode.Continuous:
51788
+ if (isVertical) {
51789
+ this._defaultScrollHandler = new VerticalContinuousScrollHandler(this);
51790
+ }
51791
+ else {
51792
+ this._defaultScrollHandler = new HorizontalContinuousScrollHandler(this);
51793
+ }
51794
+ break;
51795
+ case exports.ScrollMode.OffScreen:
51796
+ if (isVertical) {
51797
+ this._defaultScrollHandler = new VerticalOffScreenScrollHandler(this);
51798
+ }
51799
+ else {
51800
+ this._defaultScrollHandler = new HorizontalOffScreenScrollHandler(this);
51801
+ }
51802
+ break;
51803
+ case exports.ScrollMode.Smooth:
51804
+ if (isVertical) {
51805
+ this._defaultScrollHandler = new VerticalSmoothScrollHandler(this);
51806
+ }
51807
+ else {
51808
+ this._defaultScrollHandler = new HorizontalSmoothScrollHandler(this);
51809
+ }
51810
+ break;
51811
+ }
51812
+ }
51355
51813
  /**
51356
51814
  * updates the cursors to highlight the beat at the specified tick position
51357
51815
  * @param tick
@@ -51362,12 +51820,9 @@
51362
51820
  this._previousTick = tick;
51363
51821
  const cache = this._tickCache;
51364
51822
  if (cache) {
51365
- const tracks = this._trackIndexLookup;
51366
- if (tracks != null && tracks.size > 0) {
51367
- const beat = cache.findBeat(tracks, tick, this._currentBeat);
51368
- if (beat) {
51369
- this._cursorUpdateBeat(beat, stop, shouldScroll, cursorSpeed, forceUpdate || this.playerState === PlayerState.Paused);
51370
- }
51823
+ const beat = cache.findBeatWithChecker(this._beatVisibilityChecker, tick, this._currentBeat);
51824
+ if (beat) {
51825
+ this._cursorUpdateBeat(beat, stop, shouldScroll, cursorSpeed, forceUpdate || this.playerState === PlayerState.Paused);
51371
51826
  }
51372
51827
  }
51373
51828
  }
@@ -51417,57 +51872,9 @@
51417
51872
  scrollToCursor() {
51418
51873
  const beatBounds = this._currentBeatBounds;
51419
51874
  if (beatBounds) {
51420
- this._internalScrollToCursor(beatBounds.barBounds.masterBarBounds);
51421
- }
51422
- }
51423
- _internalScrollToCursor(barBoundings) {
51424
- const scrollElement = this.uiFacade.getScrollContainer();
51425
- const isVertical = Environment.getLayoutEngineFactory(this.settings.display.layoutMode).vertical;
51426
- const mode = this.settings.player.scrollMode;
51427
- if (isVertical) {
51428
- // when scrolling on the y-axis, we preliminary check if the new beat/bar have
51429
- // moved on the y-axis
51430
- const y = barBoundings.realBounds.y + this.settings.player.scrollOffsetY;
51431
- if (y !== this._lastScroll) {
51432
- this._lastScroll = y;
51433
- switch (mode) {
51434
- case exports.ScrollMode.Continuous:
51435
- const elementOffset = this.uiFacade.getOffset(scrollElement, this.container);
51436
- this.uiFacade.scrollToY(scrollElement, elementOffset.y + y, this.settings.player.scrollSpeed);
51437
- break;
51438
- case exports.ScrollMode.OffScreen:
51439
- const elementBottom = scrollElement.scrollTop + this.uiFacade.getOffset(null, scrollElement).h;
51440
- if (barBoundings.visualBounds.y + barBoundings.visualBounds.h >= elementBottom ||
51441
- barBoundings.visualBounds.y < scrollElement.scrollTop) {
51442
- const scrollTop = barBoundings.realBounds.y + this.settings.player.scrollOffsetY;
51443
- this.uiFacade.scrollToY(scrollElement, scrollTop, this.settings.player.scrollSpeed);
51444
- }
51445
- break;
51446
- }
51447
- }
51448
- }
51449
- else {
51450
- // when scrolling on the x-axis, we preliminary check if the new bar has
51451
- // moved on the x-axis
51452
- const x = barBoundings.visualBounds.x;
51453
- if (x !== this._lastScroll) {
51454
- this._lastScroll = x;
51455
- switch (mode) {
51456
- case exports.ScrollMode.Continuous:
51457
- const scrollLeftContinuous = barBoundings.realBounds.x + this.settings.player.scrollOffsetX;
51458
- this._lastScroll = barBoundings.visualBounds.x;
51459
- this.uiFacade.scrollToX(scrollElement, scrollLeftContinuous, this.settings.player.scrollSpeed);
51460
- break;
51461
- case exports.ScrollMode.OffScreen:
51462
- const elementRight = scrollElement.scrollLeft + this.uiFacade.getOffset(null, scrollElement).w;
51463
- if (barBoundings.visualBounds.x + barBoundings.visualBounds.w >= elementRight ||
51464
- barBoundings.visualBounds.x < scrollElement.scrollLeft) {
51465
- const scrollLeftOffScreen = barBoundings.realBounds.x + this.settings.player.scrollOffsetX;
51466
- this._lastScroll = barBoundings.visualBounds.x;
51467
- this.uiFacade.scrollToX(scrollElement, scrollLeftOffScreen, this.settings.player.scrollSpeed);
51468
- }
51469
- break;
51470
- }
51875
+ const handler = this.customScrollHandler ?? this._defaultScrollHandler;
51876
+ if (handler) {
51877
+ handler.forceScrollTo(beatBounds);
51471
51878
  }
51472
51879
  }
51473
51880
  }
@@ -51483,11 +51890,12 @@
51483
51890
  }
51484
51891
  const isPlayingUpdate = this._player.state === PlayerState.Playing && !stop;
51485
51892
  let nextBeatX = barBoundings.visualBounds.x + barBoundings.visualBounds.w;
51893
+ let nextBeatBoundings = null;
51486
51894
  // get position of next beat on same system
51487
51895
  if (nextBeat && cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext) {
51488
51896
  // if we are moving within the same bar or to the next bar
51489
51897
  // transition to the next beat, otherwise transition to the end of the bar.
51490
- const nextBeatBoundings = cache.findBeat(nextBeat);
51898
+ nextBeatBoundings = cache.findBeat(nextBeat);
51491
51899
  if (nextBeatBoundings &&
51492
51900
  nextBeatBoundings.barBounds.masterBarBounds.staffSystemBounds === barBoundings.staffSystemBounds) {
51493
51901
  nextBeatX = nextBeatBoundings.onNotesX;
@@ -51513,25 +51921,29 @@
51513
51921
  beatCursor.transitionToX(0, startBeatX);
51514
51922
  beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
51515
51923
  }
51924
+ // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks)
51925
+ // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time.
51926
+ // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX);
51927
+ const factor = cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext ? 2 : 1;
51928
+ nextBeatX = startBeatX + (nextBeatX - startBeatX) * factor;
51929
+ duration = (duration / cursorSpeed) * factor;
51516
51930
  // we need to put the transition to an own animation frame
51517
51931
  // otherwise the stop animation above is not applied.
51518
51932
  this.uiFacade.beginInvoke(() => {
51519
- // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks)
51520
- // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time.
51521
- // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX);
51522
- const factor = cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext ? 2 : 1;
51523
- const doubleEndBeatX = startBeatX + (nextBeatX - startBeatX) * factor;
51524
- beatCursor.transitionToX((duration / cursorSpeed) * factor, doubleEndBeatX);
51933
+ beatCursor.transitionToX(duration, nextBeatX);
51525
51934
  });
51526
51935
  }
51527
51936
  else {
51528
- beatCursor.transitionToX(0, startBeatX);
51937
+ duration = 0;
51938
+ beatCursor.transitionToX(duration, nextBeatX);
51529
51939
  beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
51530
51940
  }
51531
51941
  }
51532
51942
  else {
51533
51943
  // ticking cursor
51534
- beatCursor.transitionToX(0, startBeatX);
51944
+ duration = 0;
51945
+ nextBeatX = startBeatX;
51946
+ beatCursor.transitionToX(duration, nextBeatX);
51535
51947
  beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
51536
51948
  }
51537
51949
  this._isInitialBeatCursorUpdate = false;
@@ -51554,7 +51966,10 @@
51554
51966
  shouldNotifyBeatChange = true;
51555
51967
  }
51556
51968
  if (shouldScroll && !this._isBeatMouseDown && this.settings.player.scrollMode !== exports.ScrollMode.Off) {
51557
- this._internalScrollToCursor(barBoundings);
51969
+ const handler = this.customScrollHandler ?? this._defaultScrollHandler;
51970
+ if (handler) {
51971
+ handler.onBeatCursorUpdating(beatBoundings, nextBeatBoundings === null ? undefined : nextBeatBoundings, cursorMode, startBeatX, nextBeatX, duration);
51972
+ }
51558
51973
  }
51559
51974
  // trigger an event for others to indicate which beat/bar is played
51560
51975
  if (shouldNotifyBeatChange) {
@@ -52569,6 +52984,7 @@
52569
52984
  if (this._isDestroyed) {
52570
52985
  return;
52571
52986
  }
52987
+ this._beatVisibilityChecker.bounds = this.boundsLookup;
52572
52988
  this._currentBeat = null;
52573
52989
  this._cursorUpdateTick(this._previousTick, false, 1, true, true);
52574
52990
  this.postRenderFinished.trigger();
@@ -52927,6 +53343,7 @@
52927
53343
  const tickCache = this._tickCache;
52928
53344
  if (currentBeat && tickCache) {
52929
53345
  this._player.tickPosition = tickCache.getBeatStart(currentBeat.beat);
53346
+ this.scrollToCursor();
52930
53347
  }
52931
53348
  }
52932
53349
  this.uiFacade.triggerEvent(this.container, 'playerStateChanged', e);
@@ -54887,6 +55304,22 @@
54887
55304
  canvasElement.style.position = 'relative';
54888
55305
  return new HtmlElementContainer(canvasElement);
54889
55306
  }
55307
+ setCanvasOverflow(canvasElement, overflow, isVertical) {
55308
+ const html = canvasElement.element;
55309
+ if (overflow === 0) {
55310
+ html.style.boxSizing = '';
55311
+ html.style.paddingRight = '';
55312
+ html.style.paddingBottom = '';
55313
+ }
55314
+ else if (isVertical) {
55315
+ html.style.boxSizing = 'content-box';
55316
+ html.style.paddingBottom = `${overflow}px`;
55317
+ }
55318
+ else {
55319
+ html.style.boxSizing = 'content-box';
55320
+ html.style.paddingRight = `${overflow}px`;
55321
+ }
55322
+ }
54890
55323
  triggerEvent(container, name, details = null, originalEvent) {
54891
55324
  const element = container.element;
54892
55325
  name = `alphaTab.${name}`;
@@ -55444,54 +55877,79 @@
55444
55877
  scrollToX(element, scrollTargetY, speed) {
55445
55878
  this._internalScrollToX(element.element, scrollTargetY, speed);
55446
55879
  }
55880
+ stopScrolling(scrollElement) {
55881
+ // stop any current animation
55882
+ const currentAnimation = this._scrollAnimationLookup.get(scrollElement.element);
55883
+ if (currentAnimation !== undefined) {
55884
+ this._activeScrollAnimations.delete(currentAnimation);
55885
+ }
55886
+ }
55887
+ get _nativeBrowserSmoothScroll() {
55888
+ const settings = this._api.settings.player;
55889
+ return settings.nativeBrowserSmoothScroll && settings.scrollMode !== exports.ScrollMode.Smooth;
55890
+ }
55891
+ _scrollAnimationId = 0;
55892
+ _activeScrollAnimations = new Set();
55893
+ _scrollAnimationLookup = new Map();
55447
55894
  _internalScrollToY(element, scrollTargetY, speed) {
55448
- if (this._api.settings.player.nativeBrowserSmoothScroll) {
55895
+ if (this._nativeBrowserSmoothScroll) {
55449
55896
  element.scrollTo({
55450
55897
  top: scrollTargetY,
55451
55898
  behavior: 'smooth'
55452
55899
  });
55453
55900
  }
55454
55901
  else {
55455
- const startY = element.scrollTop;
55456
- const diff = scrollTargetY - startY;
55457
- let start = 0;
55458
- const step = (x) => {
55459
- if (start === 0) {
55460
- start = x;
55461
- }
55462
- const time = x - start;
55463
- const percent = Math.min(time / speed, 1);
55464
- element.scrollTop = (startY + diff * percent) | 0;
55465
- if (time < speed) {
55466
- window.requestAnimationFrame(step);
55467
- }
55468
- };
55469
- window.requestAnimationFrame(step);
55902
+ this._internalScrollTo(element, element.scrollTop, scrollTargetY, speed, scroll => {
55903
+ element.scrollTop = scroll;
55904
+ });
55470
55905
  }
55471
55906
  }
55907
+ _internalScrollTo(element, startScroll, endScroll, scrollDuration, setValue) {
55908
+ // stop any current animation
55909
+ const currentAnimation = this._scrollAnimationLookup.get(element);
55910
+ if (currentAnimation !== undefined) {
55911
+ this._activeScrollAnimations.delete(currentAnimation);
55912
+ }
55913
+ if (scrollDuration === 0) {
55914
+ setValue(endScroll);
55915
+ return;
55916
+ }
55917
+ // start new animation
55918
+ const animationId = this._scrollAnimationId++;
55919
+ this._scrollAnimationLookup.set(element, animationId);
55920
+ this._activeScrollAnimations.add(animationId);
55921
+ const diff = endScroll - startScroll;
55922
+ let start = 0;
55923
+ const step = (x) => {
55924
+ if (!this._activeScrollAnimations.has(animationId)) {
55925
+ return;
55926
+ }
55927
+ if (start === 0) {
55928
+ start = x;
55929
+ }
55930
+ const time = x - start;
55931
+ const percent = Math.min(time / scrollDuration, 1);
55932
+ setValue((startScroll + diff * percent) | 0);
55933
+ if (time < scrollDuration) {
55934
+ window.requestAnimationFrame(step);
55935
+ }
55936
+ else {
55937
+ this._activeScrollAnimations.delete(animationId);
55938
+ }
55939
+ };
55940
+ window.requestAnimationFrame(step);
55941
+ }
55472
55942
  _internalScrollToX(element, scrollTargetX, speed) {
55473
- if (this._api.settings.player.nativeBrowserSmoothScroll) {
55943
+ if (this._nativeBrowserSmoothScroll) {
55474
55944
  element.scrollTo({
55475
55945
  left: scrollTargetX,
55476
55946
  behavior: 'smooth'
55477
55947
  });
55478
55948
  }
55479
55949
  else {
55480
- const startX = element.scrollLeft;
55481
- const diff = scrollTargetX - startX;
55482
- let start = 0;
55483
- const step = (t) => {
55484
- if (start === 0) {
55485
- start = t;
55486
- }
55487
- const time = t - start;
55488
- const percent = Math.min(time / speed, 1);
55489
- element.scrollLeft = (startX + diff * percent) | 0;
55490
- if (time < speed) {
55491
- window.requestAnimationFrame(step);
55492
- }
55493
- };
55494
- window.requestAnimationFrame(step);
55950
+ this._internalScrollTo(element, element.scrollLeft, scrollTargetX, speed, scroll => {
55951
+ element.scrollLeft = scroll;
55952
+ });
55495
55953
  }
55496
55954
  }
55497
55955
  createBackingTrackPlayer() {
@@ -60576,8 +61034,10 @@
60576
61034
  height = 0;
60577
61035
  index = 0;
60578
61036
  staffIndex = 0;
61037
+ isVisible = false;
61038
+ _emptyBarCount = 0;
60579
61039
  get isFirstInSystem() {
60580
- return this.index === 0;
61040
+ return this.system.firstVisibleStaff === this;
60581
61041
  }
60582
61042
  topEffectInfos = [];
60583
61043
  bottomEffectInfos = [];
@@ -60612,10 +61072,11 @@
60612
61072
  get contentBottom() {
60613
61073
  return this.y + this.topPadding + this.topOverflow + this.staffBottom;
60614
61074
  }
60615
- constructor(trackIndex, staff, factory) {
61075
+ constructor(system, trackIndex, staff, factory) {
60616
61076
  this._factory = factory;
60617
61077
  this.trackIndex = trackIndex;
60618
61078
  this.modelStaff = staff;
61079
+ this.system = system;
60619
61080
  for (const b of factory.effectBands) {
60620
61081
  if (b.shouldCreate && !b.shouldCreate(staff)) {
60621
61082
  continue;
@@ -60631,6 +61092,7 @@
60631
61092
  break;
60632
61093
  }
60633
61094
  }
61095
+ this._updateVisibility();
60634
61096
  }
60635
61097
  getSharedLayoutData(key, def) {
60636
61098
  if (this._sharedLayoutData.has(key)) {
@@ -60657,6 +61119,20 @@
60657
61119
  renderer.reLayout();
60658
61120
  this.barRenderers.push(renderer);
60659
61121
  this.system.layout.registerBarRenderer(this.staffId, renderer);
61122
+ if (renderer.bar.isEmpty || renderer.bar.isRestOnly) {
61123
+ this._emptyBarCount++;
61124
+ }
61125
+ this._updateVisibility();
61126
+ }
61127
+ _updateVisibility() {
61128
+ const stylesheet = this.modelStaff.track.score.stylesheet;
61129
+ const canHideEmptyStaves = stylesheet.hideEmptyStaves && (stylesheet.hideEmptyStavesInFirstSystem || this.system.index > 0);
61130
+ if (canHideEmptyStaves) {
61131
+ this.isVisible = this._emptyBarCount < this.barRenderers.length;
61132
+ }
61133
+ else {
61134
+ this.isVisible = true;
61135
+ }
60660
61136
  }
60661
61137
  addBar(bar, layoutingInfo, additionalMultiBarsRestBars) {
60662
61138
  const renderer = this._factory.create(this.system.layout.renderer, bar);
@@ -60676,6 +61152,10 @@
60676
61152
  }
60677
61153
  this.barRenderers.push(renderer);
60678
61154
  this.system.layout.registerBarRenderer(this.staffId, renderer);
61155
+ if (bar.isEmpty || bar.isRestOnly) {
61156
+ this._emptyBarCount++;
61157
+ }
61158
+ this._updateVisibility();
60679
61159
  }
60680
61160
  revertLastBar() {
60681
61161
  this._sharedLayoutData = new Map();
@@ -60686,6 +61166,10 @@
60686
61166
  for (const r of this.barRenderers) {
60687
61167
  r.afterStaffBarReverted();
60688
61168
  }
61169
+ if (lastBar.bar.isEmpty || lastBar.bar.isRestOnly) {
61170
+ this._emptyBarCount--;
61171
+ }
61172
+ this._updateVisibility();
60689
61173
  return lastBar;
60690
61174
  }
60691
61175
  scaleToWidth(width) {
@@ -60796,6 +61280,7 @@
60796
61280
  this.height += this.topPadding + topOverflow + this.bottomOverflow + this.bottomPadding;
60797
61281
  }
60798
61282
  this.height = Math.ceil(this.height);
61283
+ this._updateVisibility();
60799
61284
  }
60800
61285
  paint(cx, cy, canvas, startIndex, count) {
60801
61286
  if (this.height === 0 || count === 0) {
@@ -61196,6 +61681,8 @@
61196
61681
  track;
61197
61682
  staffSystem;
61198
61683
  staves = [];
61684
+ firstVisibleStaff;
61685
+ lastVisibleStaff;
61199
61686
  bracket = null;
61200
61687
  constructor(staffSystem, track) {
61201
61688
  this.staffSystem = staffSystem;
@@ -61210,18 +61697,47 @@
61210
61697
  * @internal
61211
61698
  */
61212
61699
  class SystemBracket {
61213
- firstStaffInBracket = null;
61214
- lastStaffInBracket = null;
61700
+ _system;
61701
+ firstStaffInBracket;
61702
+ lastStaffInBracket;
61703
+ firstVisibleStaffInBracket;
61704
+ lastVisibleStaffInBracket;
61215
61705
  drawAsBrace = false;
61216
61706
  braceScale = 1;
61217
61707
  width = 0;
61218
61708
  index = 0;
61219
- get canPaint() {
61220
- return this.firstStaffInBracket !== null && this.lastStaffInBracket !== null;
61709
+ canPaint = false;
61710
+ constructor(system) {
61711
+ this._system = system;
61712
+ }
61713
+ updateCanPaint() {
61714
+ let firstVisibleStaff = undefined;
61715
+ let lastVisibleStaff = undefined;
61716
+ for (let i = this.firstStaffInBracket.index; i <= this.lastStaffInBracket.index; i++) {
61717
+ const staff = this._system.allStaves[i];
61718
+ if (staff.isVisible) {
61719
+ if (!firstVisibleStaff) {
61720
+ firstVisibleStaff = staff;
61721
+ }
61722
+ lastVisibleStaff = staff;
61723
+ }
61724
+ }
61725
+ this.firstVisibleStaffInBracket = firstVisibleStaff;
61726
+ this.lastVisibleStaffInBracket = lastVisibleStaff;
61727
+ if (!firstVisibleStaff || !lastVisibleStaff) {
61728
+ this.canPaint = false;
61729
+ return;
61730
+ }
61731
+ // single staff brackets?
61732
+ const singleStaffBrackets = this._system.layout.renderer.score.stylesheet.showSingleStaffBrackets;
61733
+ if (!singleStaffBrackets && firstVisibleStaff === lastVisibleStaff) {
61734
+ this.canPaint = false;
61735
+ return;
61736
+ }
61737
+ this.canPaint = true;
61221
61738
  }
61222
61739
  finalizeBracket(smuflMetrics) {
61223
- // systems with just a single staff do not have a bracket
61224
- if (this.firstStaffInBracket === this.lastStaffInBracket) {
61740
+ if (!this.canPaint) {
61225
61741
  this.width = 0;
61226
61742
  return;
61227
61743
  }
@@ -61235,11 +61751,11 @@
61235
61751
  else {
61236
61752
  this.width = smuflMetrics.bracketThickness;
61237
61753
  }
61238
- if (!this.drawAsBrace || !this.firstStaffInBracket || !this.lastStaffInBracket) {
61754
+ if (!this.drawAsBrace) {
61239
61755
  return;
61240
61756
  }
61241
- const firstStart = this.firstStaffInBracket.contentTop;
61242
- const lastEnd = this.lastStaffInBracket.contentBottom;
61757
+ const firstStart = this.firstVisibleStaffInBracket.contentTop;
61758
+ const lastEnd = this.lastVisibleStaffInBracket.contentBottom;
61243
61759
  const requiredHeight = lastEnd - firstStart;
61244
61760
  const requiredScaleForBracket = requiredHeight / bravuraBraceHeightAtMusicFontSize;
61245
61761
  this.braceScale = requiredScaleForBracket;
@@ -61251,8 +61767,8 @@
61251
61767
  */
61252
61768
  class SingleTrackSystemBracket extends SystemBracket {
61253
61769
  track;
61254
- constructor(track) {
61255
- super();
61770
+ constructor(system, track) {
61771
+ super(system);
61256
61772
  this.track = track;
61257
61773
  this.drawAsBrace = SingleTrackSystemBracket.isTrackDrawAsBrace(track);
61258
61774
  }
@@ -61317,6 +61833,7 @@
61317
61833
  topPadding;
61318
61834
  bottomPadding;
61319
61835
  allStaves = [];
61836
+ firstVisibleStaff;
61320
61837
  constructor(layout) {
61321
61838
  this.layout = layout;
61322
61839
  this.topPadding = layout.renderer.settings.display.systemPaddingTop;
@@ -61335,14 +61852,40 @@
61335
61852
  this.masterBarsRenderers.push(renderers);
61336
61853
  renderers.layoutingInfo.preBeatSize = 0;
61337
61854
  let src = 0;
61338
- for (let i = 0, j = this.staves.length; i < j; i++) {
61339
- const g = this.staves[i];
61340
- for (let k = 0, l = g.staves.length; k < l; k++) {
61341
- const s = g.staves[k];
61855
+ let firstVisibleStaff = undefined;
61856
+ let anyStaffVisible = false;
61857
+ for (const g of this.staves) {
61858
+ let firstVisibleStaffInGroup = undefined;
61859
+ let lastVisibleStaffInGroup = undefined;
61860
+ for (const s of g.staves) {
61342
61861
  const renderer = renderers.renderers[src++];
61343
61862
  s.addBarRenderer(renderer);
61863
+ if (s.isVisible) {
61864
+ anyStaffVisible = true;
61865
+ if (!firstVisibleStaffInGroup) {
61866
+ firstVisibleStaffInGroup = s;
61867
+ }
61868
+ if (!firstVisibleStaff) {
61869
+ firstVisibleStaff = s;
61870
+ }
61871
+ lastVisibleStaffInGroup = s;
61872
+ }
61873
+ }
61874
+ g.firstVisibleStaff = firstVisibleStaffInGroup;
61875
+ g.lastVisibleStaff = lastVisibleStaffInGroup;
61876
+ if (!firstVisibleStaff) {
61877
+ firstVisibleStaff = firstVisibleStaffInGroup;
61344
61878
  }
61345
61879
  }
61880
+ if (!anyStaffVisible) {
61881
+ const group = this.staves[0];
61882
+ const firstStaff = group.staves[0];
61883
+ firstStaff.isVisible = true;
61884
+ group.firstVisibleStaff = firstStaff;
61885
+ group.lastVisibleStaff = firstStaff;
61886
+ firstVisibleStaff = firstStaff;
61887
+ }
61888
+ this.firstVisibleStaff = firstVisibleStaff;
61346
61889
  this._calculateAccoladeSpacing(tracks);
61347
61890
  this._updateWidthFromLastBar();
61348
61891
  return renderers;
@@ -61353,15 +61896,26 @@
61353
61896
  result.layoutingInfo = new BarLayoutingInfo();
61354
61897
  result.masterBar = tracks[0].score.masterBars[barIndex];
61355
61898
  this.masterBarsRenderers.push(result);
61899
+ let firstVisibleStaff = undefined;
61900
+ let anyStaffVisible = false;
61356
61901
  // add renderers
61357
61902
  const barLayoutingInfo = result.layoutingInfo;
61358
61903
  for (const g of this.staves) {
61904
+ let firstVisibleStaffInGroup = undefined;
61905
+ let lastVisibleStaffInGroup = undefined;
61359
61906
  for (const s of g.staves) {
61360
61907
  const bar = g.track.staves[s.modelStaff.index].bars[barIndex];
61361
61908
  const additionalMultiBarsRestBars = additionalMultiBarRestIndexes == null
61362
61909
  ? null
61363
61910
  : additionalMultiBarRestIndexes.map(b => g.track.staves[s.modelStaff.index].bars[b]);
61364
61911
  s.addBar(bar, barLayoutingInfo, additionalMultiBarsRestBars);
61912
+ if (s.isVisible) {
61913
+ anyStaffVisible = true;
61914
+ if (!firstVisibleStaffInGroup) {
61915
+ firstVisibleStaffInGroup = s;
61916
+ }
61917
+ lastVisibleStaffInGroup = s;
61918
+ }
61365
61919
  const renderer = s.barRenderers[s.barRenderers.length - 1];
61366
61920
  result.renderers.push(renderer);
61367
61921
  if (renderer.isLinkedToPrevious) {
@@ -61371,7 +61925,21 @@
61371
61925
  result.canWrap = false;
61372
61926
  }
61373
61927
  }
61928
+ g.firstVisibleStaff = firstVisibleStaffInGroup;
61929
+ g.lastVisibleStaff = lastVisibleStaffInGroup;
61930
+ if (!firstVisibleStaff) {
61931
+ firstVisibleStaff = firstVisibleStaffInGroup;
61932
+ }
61933
+ }
61934
+ if (!anyStaffVisible) {
61935
+ const group = this.staves[0];
61936
+ const firstStaff = group.staves[0];
61937
+ firstStaff.isVisible = true;
61938
+ group.firstVisibleStaff = firstStaff;
61939
+ group.lastVisibleStaff = firstStaff;
61940
+ firstVisibleStaff = firstStaff;
61374
61941
  }
61942
+ this.firstVisibleStaff = firstVisibleStaff;
61375
61943
  this._calculateAccoladeSpacing(tracks);
61376
61944
  barLayoutingInfo.finish();
61377
61945
  // ensure same widths of new renderer
@@ -61384,19 +61952,35 @@
61384
61952
  this.masterBarsRenderers.splice(this.masterBarsRenderers.length - 1, 1);
61385
61953
  let width = 0;
61386
61954
  let barDisplayScale = 0;
61387
- for (let i = 0, j = this.allStaves.length; i < j; i++) {
61388
- const s = this.allStaves[i];
61389
- const lastBar = s.revertLastBar();
61390
- const computedWidth = lastBar.computedWidth;
61391
- if (computedWidth > width) {
61392
- width = computedWidth;
61955
+ let firstVisibleStaff = undefined;
61956
+ for (const g of this.staves) {
61957
+ let firstVisibleStaffInGroup = undefined;
61958
+ let lastVisibleStaffInGroup = undefined;
61959
+ for (const s of g.staves) {
61960
+ const lastBar = s.revertLastBar();
61961
+ const computedWidth = lastBar.computedWidth;
61962
+ if (computedWidth > width) {
61963
+ width = computedWidth;
61964
+ }
61965
+ const newBarDisplayScale = lastBar.barDisplayScale;
61966
+ if (newBarDisplayScale > barDisplayScale) {
61967
+ barDisplayScale = newBarDisplayScale;
61968
+ }
61969
+ lastBar.afterReverted();
61970
+ if (s.isVisible) {
61971
+ if (!firstVisibleStaffInGroup) {
61972
+ firstVisibleStaffInGroup = s;
61973
+ }
61974
+ lastVisibleStaffInGroup = s;
61975
+ }
61393
61976
  }
61394
- const newBarDisplayScale = lastBar.barDisplayScale;
61395
- if (newBarDisplayScale > barDisplayScale) {
61396
- barDisplayScale = newBarDisplayScale;
61977
+ g.firstVisibleStaff = firstVisibleStaffInGroup;
61978
+ g.lastVisibleStaff = lastVisibleStaffInGroup;
61979
+ if (!firstVisibleStaff) {
61980
+ firstVisibleStaff = firstVisibleStaffInGroup;
61397
61981
  }
61398
- lastBar.afterReverted();
61399
61982
  }
61983
+ this.firstVisibleStaff = firstVisibleStaff;
61400
61984
  this.width -= width;
61401
61985
  this.computedWidth -= width;
61402
61986
  this.totalBarDisplayScale -= barDisplayScale;
@@ -61478,8 +62062,8 @@
61478
62062
  }
61479
62063
  }
61480
62064
  }
62065
+ this.accoladeWidth += settings.display.systemLabelPaddingLeft;
61481
62066
  if (hasAnyTrackName) {
61482
- this.accoladeWidth += settings.display.systemLabelPaddingLeft;
61483
62067
  this.accoladeWidth += settings.display.systemLabelPaddingRight;
61484
62068
  }
61485
62069
  }
@@ -61503,6 +62087,7 @@
61503
62087
  }
61504
62088
  let braceWidth = 0;
61505
62089
  for (const b of this._brackets) {
62090
+ b.updateCanPaint();
61506
62091
  b.finalizeBracket(settings.display.resources.engravingSettings);
61507
62092
  braceWidth = Math.max(braceWidth, b.width);
61508
62093
  }
@@ -61510,6 +62095,12 @@
61510
62095
  this.width += this.accoladeWidth;
61511
62096
  this.computedWidth += this.accoladeWidth;
61512
62097
  }
62098
+ else {
62099
+ for (const b of this._brackets) {
62100
+ b.updateCanPaint();
62101
+ b.finalizeBracket(settings.display.resources.engravingSettings);
62102
+ }
62103
+ }
61513
62104
  }
61514
62105
  _getStaffTrackGroup(track) {
61515
62106
  for (let i = 0, j = this.staves.length; i < j; i++) {
@@ -61539,12 +62130,12 @@
61539
62130
  break;
61540
62131
  case BracketExtendMode.GroupStaves:
61541
62132
  // when grouping staves, we create one bracket for the whole track across all staves
61542
- bracket = new SingleTrackSystemBracket(track);
62133
+ bracket = new SingleTrackSystemBracket(this, track);
61543
62134
  bracket.index = this._brackets.length;
61544
62135
  this._brackets.push(bracket);
61545
62136
  break;
61546
62137
  case BracketExtendMode.GroupSimilarInstruments:
61547
- bracket = new SimilarInstrumentSystemBracket(track);
62138
+ bracket = new SimilarInstrumentSystemBracket(this, track);
61548
62139
  bracket.index = this._brackets.length;
61549
62140
  this._brackets.push(bracket);
61550
62141
  break;
@@ -61591,8 +62182,10 @@
61591
62182
  }
61592
62183
  }
61593
62184
  paintPartial(cx, cy, canvas, startIndex, count) {
61594
- for (let i = 0, j = this.allStaves.length; i < j; i++) {
61595
- this.allStaves[i].paint(cx, cy, canvas, startIndex, count);
62185
+ for (const s of this.allStaves) {
62186
+ if (s.isVisible) {
62187
+ s.paint(cx, cy, canvas, startIndex, count);
62188
+ }
61596
62189
  }
61597
62190
  const res = this.layout.renderer.settings.display.resources;
61598
62191
  if (this.staves.length > 0 && startIndex === 0) {
@@ -61629,9 +62222,9 @@
61629
62222
  const oldBaseLine = canvas.textBaseline;
61630
62223
  const oldTextAlign = canvas.textAlign;
61631
62224
  for (const g of this.staves) {
61632
- if (g.staves.length > 0) {
61633
- const firstStart = cy + g.staves[0].contentTop;
61634
- const lastEnd = cy + g.staves[g.staves.length - 1].contentBottom;
62225
+ if (g.firstVisibleStaff) {
62226
+ const firstStart = cy + g.firstVisibleStaff.contentTop;
62227
+ const lastEnd = cy + g.lastVisibleStaff.contentBottom;
61635
62228
  let trackNameText = '';
61636
62229
  switch (trackNameMode) {
61637
62230
  case TrackNameMode.FullName:
@@ -61685,6 +62278,9 @@
61685
62278
  if (this.allStaves.length > 0 && needsSystemBarLine) {
61686
62279
  let previousStaffInBracket = null;
61687
62280
  for (const s of this.allStaves) {
62281
+ if (!s.isVisible) {
62282
+ continue;
62283
+ }
61688
62284
  if (previousStaffInBracket !== null) {
61689
62285
  const previousBottom = previousStaffInBracket.contentBottom;
61690
62286
  const thisTop = s.contentTop;
@@ -61711,17 +62307,17 @@
61711
62307
  const settings = this.layout.renderer.settings;
61712
62308
  for (const bracket of this._brackets) {
61713
62309
  if (bracket.canPaint) {
61714
- const barStartX = cx + bracket.firstStaffInBracket.x;
62310
+ const barStartX = cx + bracket.firstVisibleStaffInBracket.x;
61715
62311
  const barSize = bracket.width;
61716
62312
  const barOffset = settings.display.accoladeBarPaddingRight;
61717
- const firstStart = cy + bracket.firstStaffInBracket.contentTop;
61718
- const lastEnd = cy + bracket.lastStaffInBracket.contentBottom;
62313
+ const firstStart = cy + bracket.firstVisibleStaffInBracket.contentTop;
62314
+ const lastEnd = cy + bracket.lastVisibleStaffInBracket.contentBottom;
61719
62315
  let accoladeStart = firstStart;
61720
62316
  let accoladeEnd = lastEnd;
61721
62317
  if (bracket.drawAsBrace) {
61722
62318
  CanvasHelper.fillMusicFontSymbolSafe(canvas, barStartX - barOffset - barSize, accoladeEnd, bracket.braceScale, MusicFontSymbol.Brace);
61723
62319
  }
61724
- else if (bracket.firstStaffInBracket !== bracket.lastStaffInBracket) {
62320
+ else if (bracket.firstVisibleStaffInBracket !== bracket.lastVisibleStaffInBracket) {
61725
62321
  // brackets typically overflow by 1/4 staff-space
61726
62322
  const smuflMetrics = settings.display.resources.engravingSettings;
61727
62323
  const bracketOverflow = smuflMetrics.oneStaffSpace * 0.25;
@@ -61753,40 +62349,86 @@
61753
62349
  this.bottomPadding = Math.max(this.bottomPadding, neededHeight);
61754
62350
  this._hasSystemSeparator = true;
61755
62351
  }
62352
+ const anyStaffVisible = this._finalizeTrackGroups();
62353
+ // for now we always force one staff to be visible.
62354
+ // making also whole systems invisible needs separate attention (also on player cursor handling)
62355
+ if (!anyStaffVisible) {
62356
+ const group = this.staves[0];
62357
+ const firstStaff = group.staves[0];
62358
+ firstStaff.isVisible = true;
62359
+ this._finalizeTrackGroups(true);
62360
+ }
62361
+ for (const b of this._brackets) {
62362
+ b.finalizeBracket(settings.display.resources.engravingSettings);
62363
+ }
62364
+ }
62365
+ _finalizeTrackGroups(onlyFirstGroup = false) {
61756
62366
  let currentY = 0;
62367
+ const settings = this.layout.renderer.settings;
61757
62368
  const smufl = settings.display.resources.engravingSettings;
61758
62369
  const topBracketSpikeHeight = smufl.glyphHeights.get(MusicFontSymbol.BracketTop);
61759
62370
  const bottomBracketSpikeHeight = smufl.glyphHeights.get(MusicFontSymbol.BracketBottom);
61760
62371
  let previousStaff = undefined;
61761
- for (const staff of this.allStaves) {
61762
- // check if we need "in-between padding"
61763
- if (previousStaff !== undefined && previousStaff.trackIndex !== staff.trackIndex) {
61764
- currentY += settings.display.trackStaffPaddingBetween;
61765
- }
61766
- const bracket = this._staffToBracket.has(staff) ? this._staffToBracket.get(staff) : undefined;
61767
- const hasBracket = bracket && !bracket.drawAsBrace && bracket.canPaint;
61768
- if (hasBracket && bracket.firstStaffInBracket === staff) {
61769
- const spikeOverflow = topBracketSpikeHeight - staff.topOverflow;
61770
- if (spikeOverflow > 0) {
61771
- currentY += spikeOverflow;
62372
+ let endSpikeOverflow = 0;
62373
+ let anyStaffVisible = false;
62374
+ for (const group of this.staves) {
62375
+ let firstVisibleStaffInGroup = undefined;
62376
+ let lastVisibleStaffInGroup = undefined;
62377
+ for (const staff of group.staves) {
62378
+ // check if we need "in-between padding"
62379
+ if (previousStaff !== undefined && previousStaff.trackIndex !== staff.trackIndex) {
62380
+ currentY += settings.display.trackStaffPaddingBetween;
62381
+ }
62382
+ const bracket = this._staffToBracket.has(staff) ? this._staffToBracket.get(staff) : undefined;
62383
+ const hasBracket = bracket && !bracket.drawAsBrace && bracket.canPaint;
62384
+ if (hasBracket && bracket.firstStaffInBracket === staff) {
62385
+ const spikeOverflow = topBracketSpikeHeight - staff.topOverflow;
62386
+ if (spikeOverflow > 0) {
62387
+ currentY += spikeOverflow;
62388
+ }
62389
+ }
62390
+ staff.x = this.accoladeWidth;
62391
+ staff.y = currentY;
62392
+ if (!onlyFirstGroup) {
62393
+ staff.finalizeStaff();
62394
+ }
62395
+ if (staff.isVisible) {
62396
+ currentY += staff.height;
62397
+ anyStaffVisible = true;
62398
+ previousStaff = staff;
62399
+ if (!firstVisibleStaffInGroup) {
62400
+ firstVisibleStaffInGroup = staff;
62401
+ }
62402
+ lastVisibleStaffInGroup = staff;
62403
+ }
62404
+ endSpikeOverflow = 0;
62405
+ if (hasBracket && bracket.lastStaffInBracket === staff) {
62406
+ const spikeOverflow = bottomBracketSpikeHeight - staff.bottomOverflow;
62407
+ if (spikeOverflow > 0) {
62408
+ if (staff.isVisible) {
62409
+ currentY += spikeOverflow;
62410
+ }
62411
+ else {
62412
+ endSpikeOverflow = spikeOverflow;
62413
+ }
62414
+ }
61772
62415
  }
61773
62416
  }
61774
- staff.x = this.accoladeWidth;
61775
- staff.y = currentY;
61776
- staff.finalizeStaff();
61777
- currentY += staff.height;
61778
- if (hasBracket && bracket.lastStaffInBracket === staff) {
61779
- const spikeOverflow = bottomBracketSpikeHeight - staff.bottomOverflow;
61780
- if (spikeOverflow > 0) {
61781
- currentY += spikeOverflow;
61782
- }
62417
+ group.firstVisibleStaff = firstVisibleStaffInGroup;
62418
+ group.lastVisibleStaff = lastVisibleStaffInGroup;
62419
+ if (!this.firstVisibleStaff) {
62420
+ this.firstVisibleStaff = firstVisibleStaffInGroup;
62421
+ }
62422
+ if (onlyFirstGroup) {
62423
+ break;
61783
62424
  }
61784
- previousStaff = staff;
61785
62425
  }
61786
- this._contentHeight = currentY;
61787
- for (const b of this._brackets) {
61788
- b.finalizeBracket(smufl);
62426
+ // ensure we add overflow if last bracket is hidden
62427
+ if (endSpikeOverflow) {
62428
+ currentY += endSpikeOverflow;
61789
62429
  }
62430
+ this._contentHeight = currentY;
62431
+ return anyStaffVisible;
61790
62432
  }
61791
62433
  buildBoundingsLookup(cx, cy) {
61792
62434
  if (this.layout.renderer.boundsLookup.isFinished) {
@@ -61820,6 +62462,9 @@
61820
62462
  const masterBarBoundsLookup = new Map();
61821
62463
  for (let i = 0; i < this.staves.length; i++) {
61822
62464
  for (const staff of this.staves[i].staves) {
62465
+ if (!staff.isVisible) {
62466
+ continue;
62467
+ }
61823
62468
  for (const renderer of staff.barRenderers) {
61824
62469
  let masterBarBounds;
61825
62470
  if (!masterBarBoundsLookup.has(renderer.bar.masterBar.index)) {
@@ -62130,8 +62775,9 @@
62130
62775
  }
62131
62776
  firstBarIndex = 0;
62132
62777
  lastBarIndex = 0;
62133
- createEmptyStaffSystem() {
62778
+ createEmptyStaffSystem(index) {
62134
62779
  const system = new StaffSystem(this);
62780
+ system.index = index;
62135
62781
  const allFactories = Environment.defaultRenderers;
62136
62782
  const renderStaves = [];
62137
62783
  for (let trackIndex = 0; trackIndex < this.renderer.tracks.length; trackIndex++) {
@@ -62143,7 +62789,7 @@
62143
62789
  let previousStaff = undefined;
62144
62790
  for (const factory of allFactories) {
62145
62791
  if (this.profile.has(factory.staffId) && factory.canCreate(track, staff)) {
62146
- const renderStaff = new RenderStaff(trackIndex, staff, factory);
62792
+ const renderStaff = new RenderStaff(system, trackIndex, staff, factory);
62147
62793
  // insert shared effect bands at front
62148
62794
  renderStaff.topEffectInfos.splice(0, 0, ...sharedTopEffects);
62149
62795
  renderStaff.bottomEffectInfos.push(...sharedBottomEffects);
@@ -65301,7 +65947,7 @@
65301
65947
  }
65302
65948
  endBarIndex = startIndex + endBarIndex - 1; // map count to array index
65303
65949
  endBarIndex = Math.min(score.masterBars.length - 1, Math.max(0, endBarIndex));
65304
- this._system = this.createEmptyStaffSystem();
65950
+ this._system = this.createEmptyStaffSystem(0);
65305
65951
  this._system.isLast = true;
65306
65952
  this._system.x = this.pagePadding[0];
65307
65953
  this._system.y = this.pagePadding[1];
@@ -65574,8 +66220,7 @@
65574
66220
  this._systems = [];
65575
66221
  let currentIndex = 0;
65576
66222
  const maxWidth = this._maxWidth;
65577
- let system = this.createEmptyStaffSystem();
65578
- system.index = this._systems.length;
66223
+ let system = this.createEmptyStaffSystem(this._systems.length);
65579
66224
  system.x = this.pagePadding[0];
65580
66225
  system.y = y;
65581
66226
  while (currentIndex < this._allMasterBarRenderers.length) {
@@ -65606,8 +66251,7 @@
65606
66251
  this._fitSystem(system);
65607
66252
  y += this._paintSystem(system, oldHeight);
65608
66253
  // note: we do not increase currentIndex here to have it added to the next system
65609
- system = this.createEmptyStaffSystem();
65610
- system.index = this._systems.length;
66254
+ system = this.createEmptyStaffSystem(this._systems.length);
65611
66255
  system.x = this.pagePadding[0];
65612
66256
  system.y = y;
65613
66257
  }
@@ -65692,8 +66336,7 @@
65692
66336
  return barsPerRow;
65693
66337
  }
65694
66338
  _createStaffSystem(currentBarIndex, endIndex) {
65695
- const system = this.createEmptyStaffSystem();
65696
- system.index = this._systems.length;
66339
+ const system = this.createEmptyStaffSystem(this._systems.length);
65697
66340
  const barsPerRow = this._getBarsPerSystem(system.index);
65698
66341
  const maxWidth = this._maxWidth;
65699
66342
  const end = endIndex + 1;
@@ -68373,16 +69016,77 @@
68373
69016
  class ClefGlyph extends MusicFontGlyph {
68374
69017
  _clef;
68375
69018
  _clefOttava;
69019
+ _ottavaGlyph;
68376
69020
  constructor(x, y, clef, clefOttava) {
68377
69021
  super(x, y, 1, ClefGlyph._getSymbol(clef, clefOttava));
68378
69022
  this._clef = clef;
68379
69023
  this._clefOttava = clefOttava;
68380
69024
  }
69025
+ getBoundingBoxTop() {
69026
+ let top = super.getBoundingBoxTop();
69027
+ const ottava = this._ottavaGlyph;
69028
+ if (ottava) {
69029
+ const ottavaTop = this.y + ottava.getBoundingBoxTop();
69030
+ top = ModelUtils.minBoundingBox(top, ottavaTop);
69031
+ }
69032
+ return top;
69033
+ }
69034
+ getBoundingBoxBottom() {
69035
+ let bottom = super.getBoundingBoxBottom();
69036
+ const ottava = this._ottavaGlyph;
69037
+ if (ottava) {
69038
+ const ottavaBottom = this.y + ottava.getBoundingBoxBottom();
69039
+ bottom = ModelUtils.maxBoundingBox(bottom, ottavaBottom);
69040
+ }
69041
+ return bottom;
69042
+ }
68381
69043
  doLayout() {
68382
69044
  this.center = true;
68383
69045
  super.doLayout();
68384
69046
  this.width = this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.GClef);
68385
69047
  this.offsetX = this.width / 2;
69048
+ this._ottavaGlyph = undefined;
69049
+ switch (this._clef) {
69050
+ case Clef.C3:
69051
+ case Clef.C4:
69052
+ switch (this._clefOttava) {
69053
+ case Ottavia._8vb:
69054
+ return;
69055
+ }
69056
+ break;
69057
+ case Clef.F4:
69058
+ case Clef.G2:
69059
+ return;
69060
+ }
69061
+ let ottavaSymbol;
69062
+ let top = false;
69063
+ switch (this._clefOttava) {
69064
+ case Ottavia._15ma:
69065
+ ottavaSymbol = MusicFontSymbol.Clef15;
69066
+ top = true;
69067
+ break;
69068
+ case Ottavia._8va:
69069
+ ottavaSymbol = MusicFontSymbol.Clef8;
69070
+ top = true;
69071
+ break;
69072
+ case Ottavia._8vb:
69073
+ ottavaSymbol = MusicFontSymbol.Clef8;
69074
+ break;
69075
+ case Ottavia._15mb:
69076
+ ottavaSymbol = MusicFontSymbol.Clef15;
69077
+ break;
69078
+ default:
69079
+ return;
69080
+ }
69081
+ const ottavaX = this.width / 2;
69082
+ const ottavaY = top
69083
+ ? this.renderer.smuflMetrics.glyphTop.get(this.symbol)
69084
+ : this.renderer.smuflMetrics.glyphBottom.get(this.symbol) -
69085
+ this.renderer.smuflMetrics.glyphHeights.get(ottavaSymbol);
69086
+ this._ottavaGlyph = new MusicFontGlyph(ottavaX, -ottavaY, 1, ottavaSymbol);
69087
+ this._ottavaGlyph.center = true;
69088
+ this._ottavaGlyph.renderer = this.renderer;
69089
+ this._ottavaGlyph.doLayout();
68386
69090
  }
68387
69091
  static _getSymbol(clef, clefOttava) {
68388
69092
  switch (clef) {
@@ -68430,44 +69134,10 @@
68430
69134
  const _ = ElementStyleHelper.bar(canvas, BarSubElement.StandardNotationClef, this.renderer.bar);
68431
69135
  try {
68432
69136
  super.paint(cx, cy, canvas);
68433
- switch (this._clef) {
68434
- case Clef.C3:
68435
- case Clef.C4:
68436
- switch (this._clefOttava) {
68437
- case Ottavia._8vb:
68438
- return;
68439
- }
68440
- break;
68441
- case Clef.F4:
68442
- case Clef.G2:
68443
- return;
68444
- }
68445
- let ottavaGlyph;
68446
- let top = false;
68447
- switch (this._clefOttava) {
68448
- case Ottavia._15ma:
68449
- ottavaGlyph = MusicFontSymbol.Clef15;
68450
- top = true;
68451
- break;
68452
- case Ottavia._8va:
68453
- ottavaGlyph = MusicFontSymbol.Clef8;
68454
- top = true;
68455
- break;
68456
- case Ottavia._8vb:
68457
- ottavaGlyph = MusicFontSymbol.Clef8;
68458
- break;
68459
- case Ottavia._15mb:
68460
- ottavaGlyph = MusicFontSymbol.Clef15;
68461
- break;
68462
- default:
68463
- return;
69137
+ const ottava = this._ottavaGlyph;
69138
+ if (ottava) {
69139
+ ottava.paint(cx + this.x, cy + this.y, canvas);
68464
69140
  }
68465
- const ottavaX = this.width / 2;
68466
- const ottavaY = top
68467
- ? this.renderer.smuflMetrics.glyphTop.get(this.symbol)
68468
- : this.renderer.smuflMetrics.glyphBottom.get(this.symbol) -
68469
- this.renderer.smuflMetrics.glyphHeights.get(ottavaGlyph);
68470
- CanvasHelper.fillMusicFontSymbolSafe(canvas, cx + this.x + ottavaX, cy + this.y - ottavaY, 1, ottavaGlyph, true);
68471
69141
  }
68472
69142
  finally {
68473
69143
  _?.[Symbol.dispose]?.();