@coderline/alphatab 1.8.0-alpha.1650 → 1.8.0-alpha.1653

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.1650 (develop, build 1650)
2
+ * alphaTab v1.8.0-alpha.1653 (develop, build 1653)
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.1650';
207
- static date = '2025-12-20T02:07:19.755Z';
208
- static commit = '5789b59da2b7f8897debe003c9b75e4d48b5e9b3';
206
+ static version = '1.8.0-alpha.1653';
207
+ static date = '2025-12-23T02:21:29.054Z';
208
+ static commit = 'b85546e1908f06f3bd049e35e881c4856fb92846';
209
209
  static print(print) {
210
210
  print(`alphaTab ${VersionInfo.version}`);
211
211
  print(`commit: ${VersionInfo.commit}`);
@@ -13089,7 +13089,7 @@ class Bounds {
13089
13089
  */
13090
13090
  class MasterBarBounds {
13091
13091
  /**
13092
- * 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.
13093
13093
  */
13094
13094
  index = 0;
13095
13095
  /**
@@ -13297,6 +13297,7 @@ class BoundsLookup {
13297
13297
  mb.visualBounds = this._boundsToJson(masterBar.visualBounds);
13298
13298
  mb.realBounds = this._boundsToJson(masterBar.realBounds);
13299
13299
  mb.index = masterBar.index;
13300
+ mb.isFirstOfLine = masterBar.isFirstOfLine;
13300
13301
  mb.bars = [];
13301
13302
  for (const bar of masterBar.bars) {
13302
13303
  const b = {};
@@ -13353,7 +13354,7 @@ class BoundsLookup {
13353
13354
  mb.lineAlignedBounds = BoundsLookup._boundsFromJson(masterBar.lineAlignedBounds);
13354
13355
  mb.visualBounds = BoundsLookup._boundsFromJson(masterBar.visualBounds);
13355
13356
  mb.realBounds = BoundsLookup._boundsFromJson(masterBar.realBounds);
13356
- sg.addBar(mb);
13357
+ lookup.addMasterBar(mb);
13357
13358
  for (const bar of masterBar.bars) {
13358
13359
  const b = new BarBounds();
13359
13360
  b.visualBounds = BoundsLookup._boundsFromJson(bar.visualBounds);
@@ -43657,6 +43658,12 @@ var ScrollMode;
43657
43658
  * Scrolling happens as soon the cursors exceed the displayed range.
43658
43659
  */
43659
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";
43660
43667
  })(ScrollMode || (ScrollMode = {}));
43661
43668
  /**
43662
43669
  * This object defines the details on how to generate the vibrato effects.
@@ -48587,6 +48594,25 @@ class MidiFileGenerator {
48587
48594
  }
48588
48595
  }
48589
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
+
48590
48616
  /**
48591
48617
  * Lists the different position modes for {@link BarRendererBase.getBeatX}
48592
48618
  * @internal
@@ -49013,22 +49039,262 @@ class ScoreRendererWrapper {
49013
49039
  }
49014
49040
 
49015
49041
  /**
49016
- * Represents the information related to a resize event.
49017
- * @public
49042
+ * Some basic scroll handler checking for changed offsets and scroll if changed.
49043
+ * @internal
49018
49044
  */
49019
- class ResizeEventArgs {
49020
- /**
49021
- * Gets the size before the resizing happened.
49022
- */
49023
- oldWidth = 0;
49024
- /**
49025
- * Gets the size after the resize was complete.
49026
- */
49027
- newWidth = 0;
49028
- /**
49029
- * Gets the settings currently used for rendering.
49030
- */
49031
- 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
+ }
49032
49298
  }
49033
49299
 
49034
49300
  /**
@@ -49671,6 +49937,7 @@ class AlphaTabApiBase {
49671
49937
  _actualPlayerMode = PlayerMode.Disabled;
49672
49938
  _player;
49673
49939
  _renderer;
49940
+ _defaultScrollHandler;
49674
49941
  /**
49675
49942
  * An indicator by how many midi-ticks the song contents are shifted.
49676
49943
  * Grace beats at start might require a shift for the first beat to start at 0.
@@ -50440,6 +50707,42 @@ class AlphaTabApiBase {
50440
50707
  }
50441
50708
  }
50442
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;
50443
50746
  /**
50444
50747
  * The tick cache allowing lookup of midi ticks to beats.
50445
50748
  * @remarks
@@ -51418,7 +51721,6 @@ class AlphaTabApiBase {
51418
51721
  _isInitialBeatCursorUpdate = true;
51419
51722
  _previousStateForCursor = PlayerState.Paused;
51420
51723
  _previousCursorCache = null;
51421
- _lastScroll = 0;
51422
51724
  _destroyCursors() {
51423
51725
  if (!this._cursorWrapper) {
51424
51726
  return;
@@ -51429,28 +51731,79 @@ class AlphaTabApiBase {
51429
51731
  this._beatCursor = null;
51430
51732
  this._selectionWrapper = null;
51431
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
+ }
51432
51751
  _updateCursors() {
51752
+ this._updateScrollHandler();
51433
51753
  const enable = this._hasCursor;
51434
- if (enable && !this._cursorWrapper) {
51435
- //
51436
- // Create cursors
51437
- const cursors = this.uiFacade.createCursors();
51438
- if (cursors) {
51439
- // store options and created elements for fast access
51440
- this._cursorWrapper = cursors.cursorWrapper;
51441
- this._barCursor = cursors.barCursor;
51442
- this._beatCursor = cursors.beatCursor;
51443
- this._selectionWrapper = cursors.selectionWrapper;
51444
- this._isInitialBeatCursorUpdate = true;
51445
- }
51446
- if (this._currentBeat !== null) {
51447
- this._cursorUpdateBeat(this._currentBeat, false, this._previousTick > 10, 1, true);
51448
- }
51754
+ if (enable) {
51755
+ this._createCursors();
51449
51756
  }
51450
51757
  else if (!enable && this._cursorWrapper) {
51451
51758
  this._destroyCursors();
51452
51759
  }
51453
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
+ }
51454
51807
  /**
51455
51808
  * updates the cursors to highlight the beat at the specified tick position
51456
51809
  * @param tick
@@ -51513,57 +51866,9 @@ class AlphaTabApiBase {
51513
51866
  scrollToCursor() {
51514
51867
  const beatBounds = this._currentBeatBounds;
51515
51868
  if (beatBounds) {
51516
- this._internalScrollToCursor(beatBounds.barBounds.masterBarBounds);
51517
- }
51518
- }
51519
- _internalScrollToCursor(barBoundings) {
51520
- const scrollElement = this.uiFacade.getScrollContainer();
51521
- const isVertical = Environment.getLayoutEngineFactory(this.settings.display.layoutMode).vertical;
51522
- const mode = this.settings.player.scrollMode;
51523
- if (isVertical) {
51524
- // when scrolling on the y-axis, we preliminary check if the new beat/bar have
51525
- // moved on the y-axis
51526
- const y = barBoundings.realBounds.y + this.settings.player.scrollOffsetY;
51527
- if (y !== this._lastScroll) {
51528
- this._lastScroll = y;
51529
- switch (mode) {
51530
- case ScrollMode.Continuous:
51531
- const elementOffset = this.uiFacade.getOffset(scrollElement, this.container);
51532
- this.uiFacade.scrollToY(scrollElement, elementOffset.y + y, this.settings.player.scrollSpeed);
51533
- break;
51534
- case ScrollMode.OffScreen:
51535
- const elementBottom = scrollElement.scrollTop + this.uiFacade.getOffset(null, scrollElement).h;
51536
- if (barBoundings.visualBounds.y + barBoundings.visualBounds.h >= elementBottom ||
51537
- barBoundings.visualBounds.y < scrollElement.scrollTop) {
51538
- const scrollTop = barBoundings.realBounds.y + this.settings.player.scrollOffsetY;
51539
- this.uiFacade.scrollToY(scrollElement, scrollTop, this.settings.player.scrollSpeed);
51540
- }
51541
- break;
51542
- }
51543
- }
51544
- }
51545
- else {
51546
- // when scrolling on the x-axis, we preliminary check if the new bar has
51547
- // moved on the x-axis
51548
- const x = barBoundings.visualBounds.x;
51549
- if (x !== this._lastScroll) {
51550
- this._lastScroll = x;
51551
- switch (mode) {
51552
- case ScrollMode.Continuous:
51553
- const scrollLeftContinuous = barBoundings.realBounds.x + this.settings.player.scrollOffsetX;
51554
- this._lastScroll = barBoundings.visualBounds.x;
51555
- this.uiFacade.scrollToX(scrollElement, scrollLeftContinuous, this.settings.player.scrollSpeed);
51556
- break;
51557
- case ScrollMode.OffScreen:
51558
- const elementRight = scrollElement.scrollLeft + this.uiFacade.getOffset(null, scrollElement).w;
51559
- if (barBoundings.visualBounds.x + barBoundings.visualBounds.w >= elementRight ||
51560
- barBoundings.visualBounds.x < scrollElement.scrollLeft) {
51561
- const scrollLeftOffScreen = barBoundings.realBounds.x + this.settings.player.scrollOffsetX;
51562
- this._lastScroll = barBoundings.visualBounds.x;
51563
- this.uiFacade.scrollToX(scrollElement, scrollLeftOffScreen, this.settings.player.scrollSpeed);
51564
- }
51565
- break;
51566
- }
51869
+ const handler = this.customScrollHandler ?? this._defaultScrollHandler;
51870
+ if (handler) {
51871
+ handler.forceScrollTo(beatBounds);
51567
51872
  }
51568
51873
  }
51569
51874
  }
@@ -51579,11 +51884,12 @@ class AlphaTabApiBase {
51579
51884
  }
51580
51885
  const isPlayingUpdate = this._player.state === PlayerState.Playing && !stop;
51581
51886
  let nextBeatX = barBoundings.visualBounds.x + barBoundings.visualBounds.w;
51887
+ let nextBeatBoundings = null;
51582
51888
  // get position of next beat on same system
51583
51889
  if (nextBeat && cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext) {
51584
51890
  // if we are moving within the same bar or to the next bar
51585
51891
  // transition to the next beat, otherwise transition to the end of the bar.
51586
- const nextBeatBoundings = cache.findBeat(nextBeat);
51892
+ nextBeatBoundings = cache.findBeat(nextBeat);
51587
51893
  if (nextBeatBoundings &&
51588
51894
  nextBeatBoundings.barBounds.masterBarBounds.staffSystemBounds === barBoundings.staffSystemBounds) {
51589
51895
  nextBeatX = nextBeatBoundings.onNotesX;
@@ -51609,25 +51915,29 @@ class AlphaTabApiBase {
51609
51915
  beatCursor.transitionToX(0, startBeatX);
51610
51916
  beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
51611
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;
51612
51924
  // we need to put the transition to an own animation frame
51613
51925
  // otherwise the stop animation above is not applied.
51614
51926
  this.uiFacade.beginInvoke(() => {
51615
- // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks)
51616
- // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time.
51617
- // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX);
51618
- const factor = cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext ? 2 : 1;
51619
- const doubleEndBeatX = startBeatX + (nextBeatX - startBeatX) * factor;
51620
- beatCursor.transitionToX((duration / cursorSpeed) * factor, doubleEndBeatX);
51927
+ beatCursor.transitionToX(duration, nextBeatX);
51621
51928
  });
51622
51929
  }
51623
51930
  else {
51624
- beatCursor.transitionToX(0, startBeatX);
51931
+ duration = 0;
51932
+ beatCursor.transitionToX(duration, nextBeatX);
51625
51933
  beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
51626
51934
  }
51627
51935
  }
51628
51936
  else {
51629
51937
  // ticking cursor
51630
- beatCursor.transitionToX(0, startBeatX);
51938
+ duration = 0;
51939
+ nextBeatX = startBeatX;
51940
+ beatCursor.transitionToX(duration, nextBeatX);
51631
51941
  beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
51632
51942
  }
51633
51943
  this._isInitialBeatCursorUpdate = false;
@@ -51650,7 +51960,10 @@ class AlphaTabApiBase {
51650
51960
  shouldNotifyBeatChange = true;
51651
51961
  }
51652
51962
  if (shouldScroll && !this._isBeatMouseDown && this.settings.player.scrollMode !== ScrollMode.Off) {
51653
- 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
+ }
51654
51967
  }
51655
51968
  // trigger an event for others to indicate which beat/bar is played
51656
51969
  if (shouldNotifyBeatChange) {
@@ -53024,6 +53337,7 @@ class AlphaTabApiBase {
53024
53337
  const tickCache = this._tickCache;
53025
53338
  if (currentBeat && tickCache) {
53026
53339
  this._player.tickPosition = tickCache.getBeatStart(currentBeat.beat);
53340
+ this.scrollToCursor();
53027
53341
  }
53028
53342
  }
53029
53343
  this.uiFacade.triggerEvent(this.container, 'playerStateChanged', e);
@@ -54984,6 +55298,22 @@ class BrowserUiFacade {
54984
55298
  canvasElement.style.position = 'relative';
54985
55299
  return new HtmlElementContainer(canvasElement);
54986
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
+ }
54987
55317
  triggerEvent(container, name, details = null, originalEvent) {
54988
55318
  const element = container.element;
54989
55319
  name = `alphaTab.${name}`;
@@ -55541,54 +55871,79 @@ class BrowserUiFacade {
55541
55871
  scrollToX(element, scrollTargetY, speed) {
55542
55872
  this._internalScrollToX(element.element, scrollTargetY, speed);
55543
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();
55544
55888
  _internalScrollToY(element, scrollTargetY, speed) {
55545
- if (this._api.settings.player.nativeBrowserSmoothScroll) {
55889
+ if (this._nativeBrowserSmoothScroll) {
55546
55890
  element.scrollTo({
55547
55891
  top: scrollTargetY,
55548
55892
  behavior: 'smooth'
55549
55893
  });
55550
55894
  }
55551
55895
  else {
55552
- const startY = element.scrollTop;
55553
- const diff = scrollTargetY - startY;
55554
- let start = 0;
55555
- const step = (x) => {
55556
- if (start === 0) {
55557
- start = x;
55558
- }
55559
- const time = x - start;
55560
- const percent = Math.min(time / speed, 1);
55561
- element.scrollTop = (startY + diff * percent) | 0;
55562
- if (time < speed) {
55563
- window.requestAnimationFrame(step);
55564
- }
55565
- };
55566
- window.requestAnimationFrame(step);
55896
+ this._internalScrollTo(element, element.scrollTop, scrollTargetY, speed, scroll => {
55897
+ element.scrollTop = scroll;
55898
+ });
55899
+ }
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;
55567
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);
55568
55935
  }
55569
55936
  _internalScrollToX(element, scrollTargetX, speed) {
55570
- if (this._api.settings.player.nativeBrowserSmoothScroll) {
55937
+ if (this._nativeBrowserSmoothScroll) {
55571
55938
  element.scrollTo({
55572
55939
  left: scrollTargetX,
55573
55940
  behavior: 'smooth'
55574
55941
  });
55575
55942
  }
55576
55943
  else {
55577
- const startX = element.scrollLeft;
55578
- const diff = scrollTargetX - startX;
55579
- let start = 0;
55580
- const step = (t) => {
55581
- if (start === 0) {
55582
- start = t;
55583
- }
55584
- const time = t - start;
55585
- const percent = Math.min(time / speed, 1);
55586
- element.scrollLeft = (startX + diff * percent) | 0;
55587
- if (time < speed) {
55588
- window.requestAnimationFrame(step);
55589
- }
55590
- };
55591
- window.requestAnimationFrame(step);
55944
+ this._internalScrollTo(element, element.scrollLeft, scrollTargetX, speed, scroll => {
55945
+ element.scrollLeft = scroll;
55946
+ });
55592
55947
  }
55593
55948
  }
55594
55949
  createBackingTrackPlayer() {
@@ -61701,8 +62056,8 @@ class StaffSystem {
61701
62056
  }
61702
62057
  }
61703
62058
  }
62059
+ this.accoladeWidth += settings.display.systemLabelPaddingLeft;
61704
62060
  if (hasAnyTrackName) {
61705
- this.accoladeWidth += settings.display.systemLabelPaddingLeft;
61706
62061
  this.accoladeWidth += settings.display.systemLabelPaddingRight;
61707
62062
  }
61708
62063
  }