@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.
package/dist/alphaTab.js CHANGED
@@ -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
  *
@@ -209,9 +209,9 @@
209
209
  * @internal
210
210
  */
211
211
  class VersionInfo {
212
- static version = '1.8.0-alpha.1650';
213
- static date = '2025-12-20T02:07:19.755Z';
214
- static commit = '5789b59da2b7f8897debe003c9b75e4d48b5e9b3';
212
+ static version = '1.8.0-alpha.1653';
213
+ static date = '2025-12-23T02:21:29.054Z';
214
+ static commit = 'b85546e1908f06f3bd049e35e881c4856fb92846';
215
215
  static print(print) {
216
216
  print(`alphaTab ${VersionInfo.version}`);
217
217
  print(`commit: ${VersionInfo.commit}`);
@@ -13095,7 +13095,7 @@
13095
13095
  */
13096
13096
  class MasterBarBounds {
13097
13097
  /**
13098
- * 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.
13099
13099
  */
13100
13100
  index = 0;
13101
13101
  /**
@@ -13303,6 +13303,7 @@
13303
13303
  mb.visualBounds = this._boundsToJson(masterBar.visualBounds);
13304
13304
  mb.realBounds = this._boundsToJson(masterBar.realBounds);
13305
13305
  mb.index = masterBar.index;
13306
+ mb.isFirstOfLine = masterBar.isFirstOfLine;
13306
13307
  mb.bars = [];
13307
13308
  for (const bar of masterBar.bars) {
13308
13309
  const b = {};
@@ -13359,7 +13360,7 @@
13359
13360
  mb.lineAlignedBounds = BoundsLookup._boundsFromJson(masterBar.lineAlignedBounds);
13360
13361
  mb.visualBounds = BoundsLookup._boundsFromJson(masterBar.visualBounds);
13361
13362
  mb.realBounds = BoundsLookup._boundsFromJson(masterBar.realBounds);
13362
- sg.addBar(mb);
13363
+ lookup.addMasterBar(mb);
13363
13364
  for (const bar of masterBar.bars) {
13364
13365
  const b = new BarBounds();
13365
13366
  b.visualBounds = BoundsLookup._boundsFromJson(bar.visualBounds);
@@ -43663,6 +43664,12 @@
43663
43664
  * Scrolling happens as soon the cursors exceed the displayed range.
43664
43665
  */
43665
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";
43666
43673
  })(exports.ScrollMode || (exports.ScrollMode = {}));
43667
43674
  /**
43668
43675
  * This object defines the details on how to generate the vibrato effects.
@@ -48593,6 +48600,25 @@
48593
48600
  }
48594
48601
  }
48595
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
+
48596
48622
  /**
48597
48623
  * Lists the different position modes for {@link BarRendererBase.getBeatX}
48598
48624
  * @internal
@@ -49019,22 +49045,262 @@
49019
49045
  }
49020
49046
 
49021
49047
  /**
49022
- * Represents the information related to a resize event.
49023
- * @public
49048
+ * Some basic scroll handler checking for changed offsets and scroll if changed.
49049
+ * @internal
49024
49050
  */
49025
- class ResizeEventArgs {
49026
- /**
49027
- * Gets the size before the resizing happened.
49028
- */
49029
- oldWidth = 0;
49030
- /**
49031
- * Gets the size after the resize was complete.
49032
- */
49033
- newWidth = 0;
49034
- /**
49035
- * Gets the settings currently used for rendering.
49036
- */
49037
- 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
+ }
49038
49304
  }
49039
49305
 
49040
49306
  /**
@@ -49677,6 +49943,7 @@
49677
49943
  _actualPlayerMode = exports.PlayerMode.Disabled;
49678
49944
  _player;
49679
49945
  _renderer;
49946
+ _defaultScrollHandler;
49680
49947
  /**
49681
49948
  * An indicator by how many midi-ticks the song contents are shifted.
49682
49949
  * Grace beats at start might require a shift for the first beat to start at 0.
@@ -50446,6 +50713,42 @@
50446
50713
  }
50447
50714
  }
50448
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;
50449
50752
  /**
50450
50753
  * The tick cache allowing lookup of midi ticks to beats.
50451
50754
  * @remarks
@@ -51424,7 +51727,6 @@
51424
51727
  _isInitialBeatCursorUpdate = true;
51425
51728
  _previousStateForCursor = PlayerState.Paused;
51426
51729
  _previousCursorCache = null;
51427
- _lastScroll = 0;
51428
51730
  _destroyCursors() {
51429
51731
  if (!this._cursorWrapper) {
51430
51732
  return;
@@ -51435,28 +51737,79 @@
51435
51737
  this._beatCursor = null;
51436
51738
  this._selectionWrapper = null;
51437
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
+ }
51438
51757
  _updateCursors() {
51758
+ this._updateScrollHandler();
51439
51759
  const enable = this._hasCursor;
51440
- if (enable && !this._cursorWrapper) {
51441
- //
51442
- // Create cursors
51443
- const cursors = this.uiFacade.createCursors();
51444
- if (cursors) {
51445
- // store options and created elements for fast access
51446
- this._cursorWrapper = cursors.cursorWrapper;
51447
- this._barCursor = cursors.barCursor;
51448
- this._beatCursor = cursors.beatCursor;
51449
- this._selectionWrapper = cursors.selectionWrapper;
51450
- this._isInitialBeatCursorUpdate = true;
51451
- }
51452
- if (this._currentBeat !== null) {
51453
- this._cursorUpdateBeat(this._currentBeat, false, this._previousTick > 10, 1, true);
51454
- }
51760
+ if (enable) {
51761
+ this._createCursors();
51455
51762
  }
51456
51763
  else if (!enable && this._cursorWrapper) {
51457
51764
  this._destroyCursors();
51458
51765
  }
51459
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
+ }
51460
51813
  /**
51461
51814
  * updates the cursors to highlight the beat at the specified tick position
51462
51815
  * @param tick
@@ -51519,57 +51872,9 @@
51519
51872
  scrollToCursor() {
51520
51873
  const beatBounds = this._currentBeatBounds;
51521
51874
  if (beatBounds) {
51522
- this._internalScrollToCursor(beatBounds.barBounds.masterBarBounds);
51523
- }
51524
- }
51525
- _internalScrollToCursor(barBoundings) {
51526
- const scrollElement = this.uiFacade.getScrollContainer();
51527
- const isVertical = Environment.getLayoutEngineFactory(this.settings.display.layoutMode).vertical;
51528
- const mode = this.settings.player.scrollMode;
51529
- if (isVertical) {
51530
- // when scrolling on the y-axis, we preliminary check if the new beat/bar have
51531
- // moved on the y-axis
51532
- const y = barBoundings.realBounds.y + this.settings.player.scrollOffsetY;
51533
- if (y !== this._lastScroll) {
51534
- this._lastScroll = y;
51535
- switch (mode) {
51536
- case exports.ScrollMode.Continuous:
51537
- const elementOffset = this.uiFacade.getOffset(scrollElement, this.container);
51538
- this.uiFacade.scrollToY(scrollElement, elementOffset.y + y, this.settings.player.scrollSpeed);
51539
- break;
51540
- case exports.ScrollMode.OffScreen:
51541
- const elementBottom = scrollElement.scrollTop + this.uiFacade.getOffset(null, scrollElement).h;
51542
- if (barBoundings.visualBounds.y + barBoundings.visualBounds.h >= elementBottom ||
51543
- barBoundings.visualBounds.y < scrollElement.scrollTop) {
51544
- const scrollTop = barBoundings.realBounds.y + this.settings.player.scrollOffsetY;
51545
- this.uiFacade.scrollToY(scrollElement, scrollTop, this.settings.player.scrollSpeed);
51546
- }
51547
- break;
51548
- }
51549
- }
51550
- }
51551
- else {
51552
- // when scrolling on the x-axis, we preliminary check if the new bar has
51553
- // moved on the x-axis
51554
- const x = barBoundings.visualBounds.x;
51555
- if (x !== this._lastScroll) {
51556
- this._lastScroll = x;
51557
- switch (mode) {
51558
- case exports.ScrollMode.Continuous:
51559
- const scrollLeftContinuous = barBoundings.realBounds.x + this.settings.player.scrollOffsetX;
51560
- this._lastScroll = barBoundings.visualBounds.x;
51561
- this.uiFacade.scrollToX(scrollElement, scrollLeftContinuous, this.settings.player.scrollSpeed);
51562
- break;
51563
- case exports.ScrollMode.OffScreen:
51564
- const elementRight = scrollElement.scrollLeft + this.uiFacade.getOffset(null, scrollElement).w;
51565
- if (barBoundings.visualBounds.x + barBoundings.visualBounds.w >= elementRight ||
51566
- barBoundings.visualBounds.x < scrollElement.scrollLeft) {
51567
- const scrollLeftOffScreen = barBoundings.realBounds.x + this.settings.player.scrollOffsetX;
51568
- this._lastScroll = barBoundings.visualBounds.x;
51569
- this.uiFacade.scrollToX(scrollElement, scrollLeftOffScreen, this.settings.player.scrollSpeed);
51570
- }
51571
- break;
51572
- }
51875
+ const handler = this.customScrollHandler ?? this._defaultScrollHandler;
51876
+ if (handler) {
51877
+ handler.forceScrollTo(beatBounds);
51573
51878
  }
51574
51879
  }
51575
51880
  }
@@ -51585,11 +51890,12 @@
51585
51890
  }
51586
51891
  const isPlayingUpdate = this._player.state === PlayerState.Playing && !stop;
51587
51892
  let nextBeatX = barBoundings.visualBounds.x + barBoundings.visualBounds.w;
51893
+ let nextBeatBoundings = null;
51588
51894
  // get position of next beat on same system
51589
51895
  if (nextBeat && cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext) {
51590
51896
  // if we are moving within the same bar or to the next bar
51591
51897
  // transition to the next beat, otherwise transition to the end of the bar.
51592
- const nextBeatBoundings = cache.findBeat(nextBeat);
51898
+ nextBeatBoundings = cache.findBeat(nextBeat);
51593
51899
  if (nextBeatBoundings &&
51594
51900
  nextBeatBoundings.barBounds.masterBarBounds.staffSystemBounds === barBoundings.staffSystemBounds) {
51595
51901
  nextBeatX = nextBeatBoundings.onNotesX;
@@ -51615,25 +51921,29 @@
51615
51921
  beatCursor.transitionToX(0, startBeatX);
51616
51922
  beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
51617
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;
51618
51930
  // we need to put the transition to an own animation frame
51619
51931
  // otherwise the stop animation above is not applied.
51620
51932
  this.uiFacade.beginInvoke(() => {
51621
- // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks)
51622
- // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time.
51623
- // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX);
51624
- const factor = cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext ? 2 : 1;
51625
- const doubleEndBeatX = startBeatX + (nextBeatX - startBeatX) * factor;
51626
- beatCursor.transitionToX((duration / cursorSpeed) * factor, doubleEndBeatX);
51933
+ beatCursor.transitionToX(duration, nextBeatX);
51627
51934
  });
51628
51935
  }
51629
51936
  else {
51630
- beatCursor.transitionToX(0, startBeatX);
51937
+ duration = 0;
51938
+ beatCursor.transitionToX(duration, nextBeatX);
51631
51939
  beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
51632
51940
  }
51633
51941
  }
51634
51942
  else {
51635
51943
  // ticking cursor
51636
- beatCursor.transitionToX(0, startBeatX);
51944
+ duration = 0;
51945
+ nextBeatX = startBeatX;
51946
+ beatCursor.transitionToX(duration, nextBeatX);
51637
51947
  beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
51638
51948
  }
51639
51949
  this._isInitialBeatCursorUpdate = false;
@@ -51656,7 +51966,10 @@
51656
51966
  shouldNotifyBeatChange = true;
51657
51967
  }
51658
51968
  if (shouldScroll && !this._isBeatMouseDown && this.settings.player.scrollMode !== exports.ScrollMode.Off) {
51659
- 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
+ }
51660
51973
  }
51661
51974
  // trigger an event for others to indicate which beat/bar is played
51662
51975
  if (shouldNotifyBeatChange) {
@@ -53030,6 +53343,7 @@
53030
53343
  const tickCache = this._tickCache;
53031
53344
  if (currentBeat && tickCache) {
53032
53345
  this._player.tickPosition = tickCache.getBeatStart(currentBeat.beat);
53346
+ this.scrollToCursor();
53033
53347
  }
53034
53348
  }
53035
53349
  this.uiFacade.triggerEvent(this.container, 'playerStateChanged', e);
@@ -54990,6 +55304,22 @@
54990
55304
  canvasElement.style.position = 'relative';
54991
55305
  return new HtmlElementContainer(canvasElement);
54992
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
+ }
54993
55323
  triggerEvent(container, name, details = null, originalEvent) {
54994
55324
  const element = container.element;
54995
55325
  name = `alphaTab.${name}`;
@@ -55547,54 +55877,79 @@
55547
55877
  scrollToX(element, scrollTargetY, speed) {
55548
55878
  this._internalScrollToX(element.element, scrollTargetY, speed);
55549
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();
55550
55894
  _internalScrollToY(element, scrollTargetY, speed) {
55551
- if (this._api.settings.player.nativeBrowserSmoothScroll) {
55895
+ if (this._nativeBrowserSmoothScroll) {
55552
55896
  element.scrollTo({
55553
55897
  top: scrollTargetY,
55554
55898
  behavior: 'smooth'
55555
55899
  });
55556
55900
  }
55557
55901
  else {
55558
- const startY = element.scrollTop;
55559
- const diff = scrollTargetY - startY;
55560
- let start = 0;
55561
- const step = (x) => {
55562
- if (start === 0) {
55563
- start = x;
55564
- }
55565
- const time = x - start;
55566
- const percent = Math.min(time / speed, 1);
55567
- element.scrollTop = (startY + diff * percent) | 0;
55568
- if (time < speed) {
55569
- window.requestAnimationFrame(step);
55570
- }
55571
- };
55572
- window.requestAnimationFrame(step);
55902
+ this._internalScrollTo(element, element.scrollTop, scrollTargetY, speed, scroll => {
55903
+ element.scrollTop = scroll;
55904
+ });
55905
+ }
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;
55573
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);
55574
55941
  }
55575
55942
  _internalScrollToX(element, scrollTargetX, speed) {
55576
- if (this._api.settings.player.nativeBrowserSmoothScroll) {
55943
+ if (this._nativeBrowserSmoothScroll) {
55577
55944
  element.scrollTo({
55578
55945
  left: scrollTargetX,
55579
55946
  behavior: 'smooth'
55580
55947
  });
55581
55948
  }
55582
55949
  else {
55583
- const startX = element.scrollLeft;
55584
- const diff = scrollTargetX - startX;
55585
- let start = 0;
55586
- const step = (t) => {
55587
- if (start === 0) {
55588
- start = t;
55589
- }
55590
- const time = t - start;
55591
- const percent = Math.min(time / speed, 1);
55592
- element.scrollLeft = (startX + diff * percent) | 0;
55593
- if (time < speed) {
55594
- window.requestAnimationFrame(step);
55595
- }
55596
- };
55597
- window.requestAnimationFrame(step);
55950
+ this._internalScrollTo(element, element.scrollLeft, scrollTargetX, speed, scroll => {
55951
+ element.scrollLeft = scroll;
55952
+ });
55598
55953
  }
55599
55954
  }
55600
55955
  createBackingTrackPlayer() {
@@ -61707,8 +62062,8 @@
61707
62062
  }
61708
62063
  }
61709
62064
  }
62065
+ this.accoladeWidth += settings.display.systemLabelPaddingLeft;
61710
62066
  if (hasAnyTrackName) {
61711
- this.accoladeWidth += settings.display.systemLabelPaddingLeft;
61712
62067
  this.accoladeWidth += settings.display.systemLabelPaddingRight;
61713
62068
  }
61714
62069
  }