From b980c5126a605e149a52bacbd9e36919965f8637 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sat, 14 Aug 2021 12:04:47 +0200 Subject: [PATCH] Changed cursor positioning and added options for disabling some features + some performance tweaks --- .../WinForms/ControlContainer.cs | 48 +++- .../WinForms/WinFormsUiFacade.cs | 2 +- .../Wpf/FrameworkElementContainer.cs | 50 ++-- .../AlphaTab.Windows/Wpf/WpfUiFacade.cs | 2 +- .../Platform/CSharp/ManagedUiFacade.cs | 2 +- src/AlphaTabApiBase.ts | 221 ++++++++++-------- src/PlayerSettings.ts | 17 ++ src/generated/PlayerSettingsSerializer.ts | 14 ++ src/platform/IContainer.ts | 30 ++- src/platform/IUiFacade.ts | 3 +- src/platform/javascript/BrowserUiFacade.ts | 150 ++++++++---- .../javascript/HtmlElementContainer.ts | 80 ++++--- 12 files changed, 391 insertions(+), 228 deletions(-) diff --git a/src.csharp/AlphaTab.Windows/WinForms/ControlContainer.cs b/src.csharp/AlphaTab.Windows/WinForms/ControlContainer.cs index ea7defd03..533961807 100644 --- a/src.csharp/AlphaTab.Windows/WinForms/ControlContainer.cs +++ b/src.csharp/AlphaTab.Windows/WinForms/ControlContainer.cs @@ -1,6 +1,7 @@ using System.Drawing; using System.Windows.Forms; using AlphaTab.Platform; +using AlphaTab.Rendering.Utils; namespace AlphaTab.WinForms { @@ -42,18 +43,6 @@ public ControlContainer(Control control) ); } - public double Top - { - get => Control.Top; - set => Control.Top = (int)value; - } - - public double Left - { - get => Control.Left; - set => Control.Left = (int)value; - } - public double Width { get => Control.ClientSize.Width - Control.Padding.Horizontal; @@ -116,6 +105,41 @@ public void Clear() Control.Controls.Clear(); } + private readonly Bounds _lastBounds = new Bounds(); + + public Bounds GetBounds() + { + return _lastBounds; + } + + public void SetBounds(double x, double y, double w, double h) + { + if (double.IsNaN(x)) + { + x = _lastBounds.X; + } + if (double.IsNaN(y)) + { + y = _lastBounds.Y; + } + if (double.IsNaN(w)) + { + w = _lastBounds.W; + } + if (double.IsNaN(h)) + { + h = _lastBounds.H; + } + Control.Left = (int)x; + Control.Top = (int)y; + Control.Width = (int)w; + Control.Height = (int)h; + _lastBounds.X = x; + _lastBounds.Y = y; + _lastBounds.W = w; + _lastBounds.H = h; + } + public IEventEmitter Resize { get; set; } public IEventEmitterOfT MouseDown { get; set; } public IEventEmitterOfT MouseMove { get; set; } diff --git a/src.csharp/AlphaTab.Windows/WinForms/WinFormsUiFacade.cs b/src.csharp/AlphaTab.Windows/WinForms/WinFormsUiFacade.cs index d1745eb52..5bbeff98a 100644 --- a/src.csharp/AlphaTab.Windows/WinForms/WinFormsUiFacade.cs +++ b/src.csharp/AlphaTab.Windows/WinForms/WinFormsUiFacade.cs @@ -212,7 +212,7 @@ public override void RemoveHighlights() { } - public override void HighlightElements(string groupId) + public override void HighlightElements(string groupId, double masterBarIndex) { } diff --git a/src.csharp/AlphaTab.Windows/Wpf/FrameworkElementContainer.cs b/src.csharp/AlphaTab.Windows/Wpf/FrameworkElementContainer.cs index cec8eb998..b0703da52 100644 --- a/src.csharp/AlphaTab.Windows/Wpf/FrameworkElementContainer.cs +++ b/src.csharp/AlphaTab.Windows/Wpf/FrameworkElementContainer.cs @@ -3,6 +3,7 @@ using System.Windows.Controls; using System.Windows.Media.Animation; using AlphaTab.Platform; +using AlphaTab.Rendering.Utils; namespace AlphaTab.Wpf { @@ -44,18 +45,6 @@ public FrameworkElementContainer(FrameworkElement control) ); } - public double Top - { - get => (float) Canvas.GetTop(Control); - set => Canvas.SetTop(Control, value); - } - - public double Left - { - get => (float) Canvas.GetLeft(Control); - set => Canvas.SetLeft(Control, value); - } - public double Width { get => (float) Control.ActualWidth; @@ -133,7 +122,6 @@ public void TransitionToX(double duration, double x) new DoubleAnimation(x, new Duration(TimeSpan.FromMilliseconds(duration)))); } - public void Clear() { if (Control is Panel p) @@ -142,6 +130,42 @@ public void Clear() } } + private readonly Bounds _lastBounds = new Bounds(); + + public Bounds GetBounds() + { + return _lastBounds; + } + + public void SetBounds(double x, double y, double w, double h) + { + if (double.IsNaN(x)) + { + x = _lastBounds.X; + } + if (double.IsNaN(y)) + { + y = _lastBounds.Y; + } + if (double.IsNaN(w)) + { + w = _lastBounds.W; + } + if (double.IsNaN(h)) + { + h = _lastBounds.H; + } + + Canvas.SetLeft(Control, x); + Canvas.SetTop(Control, y); + Control.Width = w; + Control.Height = h; + _lastBounds.X = x; + _lastBounds.Y = y; + _lastBounds.W = w; + _lastBounds.H = h; + } + public IEventEmitter Resize { get; set; } public IEventEmitterOfT MouseDown { get; set; } public IEventEmitterOfT MouseMove { get; set; } diff --git a/src.csharp/AlphaTab.Windows/Wpf/WpfUiFacade.cs b/src.csharp/AlphaTab.Windows/Wpf/WpfUiFacade.cs index b7d020f99..edde39167 100644 --- a/src.csharp/AlphaTab.Windows/Wpf/WpfUiFacade.cs +++ b/src.csharp/AlphaTab.Windows/Wpf/WpfUiFacade.cs @@ -258,7 +258,7 @@ public override void RemoveHighlights() { } - public override void HighlightElements(string groupId) + public override void HighlightElements(string groupId, double masterBarIndex) { } diff --git a/src.csharp/AlphaTab/Platform/CSharp/ManagedUiFacade.cs b/src.csharp/AlphaTab/Platform/CSharp/ManagedUiFacade.cs index fd60462eb..1e47ad6fb 100644 --- a/src.csharp/AlphaTab/Platform/CSharp/ManagedUiFacade.cs +++ b/src.csharp/AlphaTab/Platform/CSharp/ManagedUiFacade.cs @@ -91,7 +91,7 @@ public virtual void InitialRender() public abstract Cursors? CreateCursors(); public abstract void BeginInvoke(Action action); public abstract void RemoveHighlights(); - public abstract void HighlightElements(string groupId); + public abstract void HighlightElements(string groupId, double masterBarIndex); public abstract IContainer? CreateSelectionElement(); public abstract IContainer GetScrollContainer(); public abstract Bounds GetOffset(IContainer? scrollElement, IContainer container); diff --git a/src/AlphaTabApiBase.ts b/src/AlphaTabApiBase.ts index 2646c4305..a7c8f27f7 100644 --- a/src/AlphaTabApiBase.ts +++ b/src/AlphaTabApiBase.ts @@ -836,119 +836,131 @@ export class AlphaTabApiBase { if (beat === previousBeat && cache === previousCache && previousState === this._playerState) { return; } - let barCursor: IContainer | null = this._barCursor; - let beatCursor: IContainer | null = this._beatCursor; + let barCursor: IContainer = this._barCursor!; + let beatCursor: IContainer = this._beatCursor!; let beatBoundings: BeatBounds | null = cache.findBeat(beat); if (!beatBoundings) { return; } + let barBoundings: MasterBarBounds = beatBoundings.barBounds.masterBarBounds; let barBounds: Bounds = barBoundings.visualBounds; - if (barCursor) { - barCursor.top = barBounds.y; - barCursor.left = barBounds.x; - barCursor.width = barBounds.w; - barCursor.height = barBounds.h; - } + barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h); - if (beatCursor) { - // move beat to start position immediately + // move beat to start position immediately + if (this.settings.player.enableAnimatedBeatCursor) { beatCursor.stopAnimation(); - beatCursor.top = barBounds.y; - beatCursor.left = beatBoundings.visualBounds.x; - beatCursor.height = barBounds.h; } + beatCursor.setBounds(beatBoundings.visualBounds.x, barBounds.y, 1, barBounds.h); // if playing, animate the cursor to the next beat - this.uiFacade.removeHighlights(); + if (this.settings.player.enableElementHighlighting) { + this.uiFacade.removeHighlights(); + } if (this._playerState === PlayerState.Playing || stop) { duration /= this.playbackSpeed; if (!stop) { - if (beatsToHighlight) { + if (this.settings.player.enableElementHighlighting && beatsToHighlight) { for (let highlight of beatsToHighlight) { let className: string = BeatContainerGlyph.getGroupId(highlight); - this.uiFacade.highlightElements(className); + this.uiFacade.highlightElements(className, beat.voice.bar.index); } } - let nextBeatX: number = barBoundings.visualBounds.x + barBoundings.visualBounds.w; - // get position of next beat on same stavegroup - if (nextBeat) { - // if we are moving within the same bar or to the next bar - // transition to the next beat, otherwise transition to the end of the bar. - if ( - nextBeat.voice.bar.index === beat.voice.bar.index || - nextBeat.voice.bar.index === beat.voice.bar.index + 1 - ) { - let nextBeatBoundings: BeatBounds | null = cache.findBeat(nextBeat); + + if (this.settings.player.enableAnimatedBeatCursor) { + let nextBeatX: number = barBoundings.visualBounds.x + barBoundings.visualBounds.w; + // get position of next beat on same stavegroup + if (nextBeat) { + // if we are moving within the same bar or to the next bar + // transition to the next beat, otherwise transition to the end of the bar. if ( - nextBeatBoundings && - nextBeatBoundings.barBounds.masterBarBounds.staveGroupBounds === - barBoundings.staveGroupBounds + nextBeat.voice.bar.index === beat.voice.bar.index || + nextBeat.voice.bar.index === beat.voice.bar.index + 1 ) { - nextBeatX = nextBeatBoundings.visualBounds.x; + let nextBeatBoundings: BeatBounds | null = cache.findBeat(nextBeat); + if ( + nextBeatBoundings && + nextBeatBoundings.barBounds.masterBarBounds.staveGroupBounds === + barBoundings.staveGroupBounds + ) { + nextBeatX = nextBeatBoundings.visualBounds.x; + } } } - } - if (beatCursor) { - this.uiFacade.beginInvoke(() => { - // Logger.Info("Player", - // "Transition from " + beatBoundings.VisualBounds.X + " to " + nextBeatX + " in " + duration + - // "(" + Player.PlaybackRange + ")"); - beatCursor!.transitionToX(duration, nextBeatX); - }); + if (beatCursor) { + this.uiFacade.beginInvoke(() => { + // Logger.Info("Player", + // "Transition from " + beatBoundings.VisualBounds.X + " to " + nextBeatX + " in " + duration + + // "(" + Player.PlaybackRange + ")"); + beatCursor!.transitionToX(duration, nextBeatX); + }); + } } } if (!this._beatMouseDown && this.settings.player.scrollMode !== ScrollMode.Off) { let scrollElement: IContainer = this.uiFacade.getScrollContainer(); let isVertical: boolean = Environment.getLayoutEngineFactory(this.settings).vertical; let mode: ScrollMode = this.settings.player.scrollMode; - let elementOffset: Bounds = this.uiFacade.getOffset(scrollElement, this.container); if (isVertical) { - switch (mode) { - case ScrollMode.Continuous: - let y: number = - elementOffset.y + barBoundings.realBounds.y + this.settings.player.scrollOffsetY; - if (y !== this._lastScroll) { - this._lastScroll = y; - this.uiFacade.scrollToY(scrollElement, y, this.settings.player.scrollSpeed); - } - break; - case ScrollMode.OffScreen: - let elementBottom: number = - scrollElement.scrollTop + this.uiFacade.getOffset(null, scrollElement).h; - if ( - barBoundings.visualBounds.y + barBoundings.visualBounds.h >= elementBottom || - barBoundings.visualBounds.y < scrollElement.scrollTop - ) { - let scrollTop: number = barBoundings.realBounds.y + this.settings.player.scrollOffsetY; - this._lastScroll = barBoundings.visualBounds.x; - this.uiFacade.scrollToY(scrollElement, scrollTop, this.settings.player.scrollSpeed); - } - break; + // when scrolling on the y-axis, we preliminary check if the new beat/bar have + // moved on the y-axis + let y: number = barBoundings.realBounds.y + this.settings.player.scrollOffsetY; + if (y !== this._lastScroll) { + this._lastScroll = y; + switch (mode) { + case ScrollMode.Continuous: + let elementOffset: Bounds = this.uiFacade.getOffset(scrollElement, this.container); + this.uiFacade.scrollToY( + scrollElement, + elementOffset.y + y, + this.settings.player.scrollSpeed + ); + break; + case ScrollMode.OffScreen: + let elementBottom: number = + scrollElement.scrollTop + this.uiFacade.getOffset(null, scrollElement).h; + if ( + barBoundings.visualBounds.y + barBoundings.visualBounds.h >= elementBottom || + barBoundings.visualBounds.y < scrollElement.scrollTop + ) { + let scrollTop: number = + barBoundings.realBounds.y + this.settings.player.scrollOffsetY; + this.uiFacade.scrollToY(scrollElement, scrollTop, this.settings.player.scrollSpeed); + } + break; + } } } else { - switch (mode) { - case ScrollMode.Continuous: - let x: number = barBoundings.visualBounds.x; - if (x !== this._lastScroll) { - let scrollLeft: number = barBoundings.realBounds.x + this.settings.player.scrollOffsetX; - this._lastScroll = barBoundings.visualBounds.x; - this.uiFacade.scrollToX(scrollElement, scrollLeft, this.settings.player.scrollSpeed); - } - break; - case ScrollMode.OffScreen: - let elementRight: number = - scrollElement.scrollLeft + this.uiFacade.getOffset(null, scrollElement).w; - if ( - barBoundings.visualBounds.x + barBoundings.visualBounds.w >= elementRight || - barBoundings.visualBounds.x < scrollElement.scrollLeft - ) { - let scrollLeft: number = barBoundings.realBounds.x + this.settings.player.scrollOffsetX; + // when scrolling on the x-axis, we preliminary check if the new bar has + // moved on the x-axis + let x: number = barBoundings.visualBounds.x; + if (x !== this._lastScroll) { + this._lastScroll = x; + switch (mode) { + case ScrollMode.Continuous: + let scrollLeftContinuous: number = barBoundings.realBounds.x + this.settings.player.scrollOffsetX; this._lastScroll = barBoundings.visualBounds.x; - this.uiFacade.scrollToX(scrollElement, scrollLeft, this.settings.player.scrollSpeed); - } - break; + this.uiFacade.scrollToX(scrollElement, scrollLeftContinuous, this.settings.player.scrollSpeed); + break; + case ScrollMode.OffScreen: + let elementRight: number = + scrollElement.scrollLeft + this.uiFacade.getOffset(null, scrollElement).w; + if ( + barBoundings.visualBounds.x + barBoundings.visualBounds.w >= elementRight || + barBoundings.visualBounds.x < scrollElement.scrollLeft + ) { + let scrollLeftOffScreen: number = + barBoundings.realBounds.x + this.settings.player.scrollOffsetX; + this._lastScroll = barBoundings.visualBounds.x; + this.uiFacade.scrollToX( + scrollElement, + scrollLeftOffScreen, + this.settings.player.scrollSpeed + ); + } + break; + } } } } @@ -1178,35 +1190,43 @@ export class AlphaTabApiBase { startBeat.bounds!.barBounds.masterBarBounds.staveGroupBounds!.visualBounds.x + startBeat.bounds!.barBounds.masterBarBounds.staveGroupBounds!.visualBounds.w; let startSelection: IContainer = this.uiFacade.createSelectionElement()!; - startSelection.top = startBeat.bounds!.barBounds.masterBarBounds.visualBounds.y; - startSelection.left = startX; - startSelection.width = staffEndX - startX; - startSelection.height = startBeat.bounds!.barBounds.masterBarBounds.visualBounds.h; + startSelection.setBounds( + startX, + startBeat.bounds!.barBounds.masterBarBounds.visualBounds.y, + staffEndX - startX, + startBeat.bounds!.barBounds.masterBarBounds.visualBounds.h + ); selectionWrapper.appendChild(startSelection); let staffStartIndex: number = startBeat.bounds!.barBounds.masterBarBounds.staveGroupBounds!.index + 1; let staffEndIndex: number = endBeat.bounds!.barBounds.masterBarBounds.staveGroupBounds!.index; for (let staffIndex: number = staffStartIndex; staffIndex < staffEndIndex; staffIndex++) { let staffBounds: StaveGroupBounds = cache.staveGroups[staffIndex]; let middleSelection: IContainer = this.uiFacade.createSelectionElement()!; - middleSelection.top = staffBounds.visualBounds.y; - middleSelection.left = staffStartX; - middleSelection.width = staffEndX - staffStartX; - middleSelection.height = staffBounds.visualBounds.h; + middleSelection.setBounds( + staffStartX, + staffBounds.visualBounds.y, + staffEndX - staffStartX, + staffBounds.visualBounds.h + ); selectionWrapper.appendChild(middleSelection); } let endSelection: IContainer = this.uiFacade.createSelectionElement()!; - endSelection.top = endBeat.bounds!.barBounds.masterBarBounds.visualBounds.y; - endSelection.left = staffStartX; - endSelection.width = endX - staffStartX; - endSelection.height = endBeat.bounds!.barBounds.masterBarBounds.visualBounds.h; + endSelection.setBounds( + staffStartX, + endBeat.bounds!.barBounds.masterBarBounds.visualBounds.y, + endX - staffStartX, + endBeat.bounds!.barBounds.masterBarBounds.visualBounds.h + ); selectionWrapper.appendChild(endSelection); } else { // if the beats are on the same staff, we simply highlight from the startbeat to endbeat let selection: IContainer = this.uiFacade.createSelectionElement()!; - selection.top = startBeat.bounds!.barBounds.masterBarBounds.visualBounds.y; - selection.left = startX; - selection.width = endX - startX; - selection.height = startBeat.bounds!.barBounds.masterBarBounds.visualBounds.h; + selection.setBounds( + startX, + startBeat.bounds!.barBounds.masterBarBounds.visualBounds.y, + endX - startX, + startBeat.bounds!.barBounds.masterBarBounds.visualBounds.h + ); selectionWrapper.appendChild(selection); } } @@ -1311,7 +1331,8 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'midiFileLoaded', e); } - public playerStateChanged: IEventEmitterOfT = new EventEmitterOfT(); + public playerStateChanged: IEventEmitterOfT = + new EventEmitterOfT(); private onPlayerStateChanged(e: PlayerStateChangedEventArgs): void { if (this._isDestroyed) { return; @@ -1320,7 +1341,8 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'playerStateChanged', e); } - public playerPositionChanged: IEventEmitterOfT = new EventEmitterOfT(); + public playerPositionChanged: IEventEmitterOfT = + new EventEmitterOfT(); private onPlayerPositionChanged(e: PositionChangedEventArgs): void { if (this._isDestroyed) { return; @@ -1329,7 +1351,8 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'playerPositionChanged', e); } - public midiEventsPlayed: IEventEmitterOfT = new EventEmitterOfT(); + public midiEventsPlayed: IEventEmitterOfT = + new EventEmitterOfT(); private onMidiEventsPlayed(e: MidiEventsPlayedEventArgs): void { if (this._isDestroyed) { return; diff --git a/src/PlayerSettings.ts b/src/PlayerSettings.ts index eb09ca1e7..72d261c81 100644 --- a/src/PlayerSettings.ts +++ b/src/PlayerSettings.ts @@ -115,6 +115,17 @@ export class PlayerSettings { */ public enableCursor: boolean = true; + /** + * Gets or sets whether the beat cursor should be animated or just ticking. + */ + public enableAnimatedBeatCursor: boolean = true; + + /** + * Gets or sets whether the notation elements of the currently played beat should be + * highlighted. + */ + public enableElementHighlighting: boolean = true; + /** * Gets or sets alphaTab should provide user interaction features to * select playback ranges and jump to the playback position by click (aka. seeking). @@ -141,6 +152,12 @@ export class PlayerSettings { */ public scrollSpeed: number = 300; + /** + * Gets or sets whether the native browser smooth scroll mechanism should be used over a custom animation. + * @target web + */ + public nativeBrowserSmoothScroll: boolean = true; + /** * Gets or sets the bend duration in milliseconds for songbook bends. */ diff --git a/src/generated/PlayerSettingsSerializer.ts b/src/generated/PlayerSettingsSerializer.ts index 56bacf126..87944ee49 100644 --- a/src/generated/PlayerSettingsSerializer.ts +++ b/src/generated/PlayerSettingsSerializer.ts @@ -24,11 +24,15 @@ export class PlayerSettingsSerializer { o.set("scrollElement", obj.scrollElement); o.set("enablePlayer", obj.enablePlayer); o.set("enableCursor", obj.enableCursor); + o.set("enableAnimatedBeatCursor", obj.enableAnimatedBeatCursor); + o.set("enableElementHighlighting", obj.enableElementHighlighting); o.set("enableUserInteraction", obj.enableUserInteraction); o.set("scrollOffsetX", obj.scrollOffsetX); o.set("scrollOffsetY", obj.scrollOffsetY); o.set("scrollMode", obj.scrollMode as number); o.set("scrollSpeed", obj.scrollSpeed); + /*@target web*/ + o.set("nativeBrowserSmoothScroll", obj.nativeBrowserSmoothScroll); o.set("songBookBendDuration", obj.songBookBendDuration); o.set("songBookDipDuration", obj.songBookDipDuration); o.set("vibrato", VibratoPlaybackSettingsSerializer.toJson(obj.vibrato)); @@ -50,6 +54,12 @@ export class PlayerSettingsSerializer { case "enablecursor": obj.enableCursor = v! as boolean; return true; + case "enableanimatedbeatcursor": + obj.enableAnimatedBeatCursor = v! as boolean; + return true; + case "enableelementhighlighting": + obj.enableElementHighlighting = v! as boolean; + return true; case "enableuserinteraction": obj.enableUserInteraction = v! as boolean; return true; @@ -65,6 +75,10 @@ export class PlayerSettingsSerializer { case "scrollspeed": obj.scrollSpeed = v! as number; return true; + /*@target web*/ + case "nativebrowsersmoothscroll": + obj.nativeBrowserSmoothScroll = v! as boolean; + return true; case "songbookbendduration": obj.songBookBendDuration = v! as number; return true; diff --git a/src/platform/IContainer.ts b/src/platform/IContainer.ts index 85a433c7d..b838fbd1d 100644 --- a/src/platform/IContainer.ts +++ b/src/platform/IContainer.ts @@ -1,20 +1,11 @@ import { IEventEmitter, IEventEmitterOfT } from '@src/EventEmitter'; import { IMouseEventArgs } from '@src/platform/IMouseEventArgs'; +import { Bounds } from '@src/rendering/utils/Bounds'; /** * This interface represents a container control in the UI layer. */ export interface IContainer { - /** - * Gets or sets the Y-position of the control, relative to its parent. - */ - top: number; - - /** - * Gets or sets the X-position of the control, relative to its parent. - */ - left: number; - /** * Gets or sets the width of the control. */ @@ -51,6 +42,21 @@ export interface IContainer { */ stopAnimation(): void; + /** + * Sets the position and size of the container for efficient repositioning. + * @param x The X-position + * @param y The Y-position + * @param w The width + * @param h The height + */ + setBounds(x: number, y: number, w: number, h: number): void; + + /** + * Gets the current position and size of the container. These + * values might be only filled correctly after a call to setBounds. + */ + getBounds(): Bounds; + /** * Tells the control to move to the given X-position in the given time. * @param duration The milliseconds that should be needed to reach the new X-position @@ -76,10 +82,10 @@ export interface IContainer { /** * This event occurs when a mouse/finger moves on top of the control. */ - mouseMove: IEventEmitterOfT + mouseMove: IEventEmitterOfT; /** * This event occurs when a mouse/finger is released from the control. */ - mouseUp: IEventEmitterOfT + mouseUp: IEventEmitterOfT; } diff --git a/src/platform/IUiFacade.ts b/src/platform/IUiFacade.ts index ba6d997f9..62de8906a 100644 --- a/src/platform/IUiFacade.ts +++ b/src/platform/IUiFacade.ts @@ -110,8 +110,9 @@ export interface IUiFacade { /** * Tells the UI layer to highlight the music notation elements with the given ID. * @param groupId The group id that identifies the elements to be highlighted. + * @param masterBarIndex The index of the related masterbar of the highlighted group. */ - highlightElements(groupId: string): void; + highlightElements(groupId: string, masterBarIndex:number): void; /** * Creates a new UI element that is used to display the selection rectangle. diff --git a/src/platform/javascript/BrowserUiFacade.ts b/src/platform/javascript/BrowserUiFacade.ts index dc17a4175..7e3b34aa7 100644 --- a/src/platform/javascript/BrowserUiFacade.ts +++ b/src/platform/javascript/BrowserUiFacade.ts @@ -40,6 +40,7 @@ export class BrowserUiFacade implements IUiFacade { private _totalResultCount: number = 0; private _initialTrackIndexes: number[] | null = null; private _intersectionObserver: IntersectionObserver; + private _barToElementLookup: Map = new Map(); public rootContainerBecameVisible: IEventEmitter = new EventEmitter(); public canRenderChanged: IEventEmitter = new EventEmitter(); @@ -84,10 +85,11 @@ export class BrowserUiFacade implements IUiFacade { } public constructor(rootElement: HTMLElement) { - if(Environment.webPlatform !== WebPlatform.Browser) { - throw new AlphaTabError(AlphaTabErrorType.General, - 'Usage of AlphaTabApi is only possible in browser environments. For usage in node use the Low Level APIs' - ); + if (Environment.webPlatform !== WebPlatform.Browser) { + throw new AlphaTabError( + AlphaTabErrorType.General, + 'Usage of AlphaTabApi is only possible in browser environments. For usage in node use the Low Level APIs' + ); } rootElement.classList.add('alphaTab'); this.rootContainer = new HtmlElementContainer(rootElement); @@ -255,6 +257,7 @@ export class BrowserUiFacade implements IUiFacade { public initialRender(): void { this._api.renderer.preRender.on((_: boolean) => { this._totalResultCount = 0; + this._barToElementLookup.clear(); }); const initialRender = () => { @@ -402,6 +405,12 @@ export class BrowserUiFacade implements IUiFacade { placeholder.dataset['svg'] = body; this._intersectionObserver.observe(placeholder); } + + // remember which bar is contained in which node for faster lookup + // on highlight/unhighlight + for (let i = renderResult.firstMasterBarIndex; i <= renderResult.lastMasterBarIndex; i++) { + this._barToElementLookup.set(i, placeholder); + } } else { if (this._totalResultCount < canvasElement.childElementCount) { canvasElement.replaceChild( @@ -417,13 +426,8 @@ export class BrowserUiFacade implements IUiFacade { } private replacePlaceholder(placeholder: HTMLElement, body: any) { - if (typeof placeholder.outerHTML === 'string') { - placeholder.outerHTML = body; - } else { - const display = document.createElement('div'); - display.innerHTML = body; - placeholder.parentNode?.replaceChild(display.firstChild!, placeholder); - } + placeholder.innerHTML = body; + delete placeholder.dataset['svg']; } /** @@ -466,20 +470,27 @@ export class BrowserUiFacade implements IUiFacade { }); } - public highlightElements(groupId: string): void { - let element: HTMLElement = (this._api.container as HtmlElementContainer).element; - let elementsToHighlight: HTMLCollection = element.getElementsByClassName(groupId); - for (let i: number = 0; i < elementsToHighlight.length; i++) { - elementsToHighlight.item(i)!.classList.add('at-highlight'); + private _highlightedElements: HTMLElement[] = []; + public highlightElements(groupId: string, masterBarIndex: number): void { + const element = this._barToElementLookup.get(masterBarIndex); + if (element) { + let elementsToHighlight: HTMLCollection = element.getElementsByClassName(groupId); + for (let i: number = 0; i < elementsToHighlight.length; i++) { + elementsToHighlight.item(i)!.classList.add('at-highlight'); + this._highlightedElements.push(elementsToHighlight.item(i) as HTMLElement); + } } } public removeHighlights(): void { - let element: HTMLElement = (this._api.container as HtmlElementContainer).element; - let elements: HTMLCollection = element.getElementsByClassName('at-highlight'); - while (elements.length > 0) { - elements.item(0)!.classList.remove('at-highlight'); + const highlightedElements = this._highlightedElements; + if (!highlightedElements) { + return; + } + for (const element of highlightedElements) { + element.classList.remove('at-highlight'); } + this._highlightedElements = []; } public destroyCursors(): void { @@ -501,14 +512,29 @@ export class BrowserUiFacade implements IUiFacade { // required css styles element.style.position = 'relative'; element.style.textAlign = 'left'; + cursorWrapper.style.position = 'absolute'; cursorWrapper.style.zIndex = '1000'; cursorWrapper.style.display = 'inline'; cursorWrapper.style.pointerEvents = 'none'; + selectionWrapper.style.position = 'absolute'; + barCursor.style.position = 'absolute'; + barCursor.style.left = '0'; + barCursor.style.top = '0'; + barCursor.style.willChange = 'transform'; + barCursor.style.width = '1px'; + barCursor.style.height = '1px'; + beatCursor.style.position = 'absolute'; beatCursor.style.transition = 'all 0s linear'; + beatCursor.style.left = '0'; + beatCursor.style.top = '0'; + beatCursor.style.willChange = 'transform'; + beatCursor.style.width = '3px'; + beatCursor.style.height = '1px'; + // add cursors to UI element.insertBefore(cursorWrapper, element.firstChild); cursorWrapper.appendChild(selectionWrapper); @@ -545,7 +571,12 @@ export class BrowserUiFacade implements IUiFacade { return b; } + private _scrollContainer: IContainer | null = null; public getScrollContainer(): IContainer { + if (this._scrollContainer) { + return this._scrollContainer; + } + let scrollElement: HTMLElement = // tslint:disable-next-line: strict-type-predicates typeof this._api.settings.player.scrollElement === 'string' @@ -567,7 +598,9 @@ export class BrowserUiFacade implements IUiFacade { } } } - return new HtmlElementContainer(scrollElement); + + this._scrollContainer = new HtmlElementContainer(scrollElement); + return this._scrollContainer; } public createSelectionElement(): IContainer | null { @@ -585,38 +618,53 @@ export class BrowserUiFacade implements IUiFacade { } private internalScrollToY(element: HTMLElement, scrollTargetY: number, speed: number): void { - let startY: number = element.scrollTop; - let diff: number = scrollTargetY - startY; - let start: number = 0; - let step = (x: number) => { - if (start === 0) { - start = x; - } - let time: number = x - start; - let percent: number = Math.min(time / speed, 1); - element.scrollTop = (startY + diff * percent) | 0; - if (time < speed) { - window.requestAnimationFrame(step); - } - }; - window.requestAnimationFrame(step); + if (this._api.settings.player.nativeBrowserSmoothScroll) { + element.scrollTo({ + top: scrollTargetY, + behavior: 'smooth' + }); + } else { + let startY: number = element.scrollTop; + let diff: number = scrollTargetY - startY; + + let start: number = 0; + let step = (x: number) => { + if (start === 0) { + start = x; + } + let time: number = x - start; + let percent: number = Math.min(time / speed, 1); + element.scrollTop = (startY + diff * percent) | 0; + if (time < speed) { + window.requestAnimationFrame(step); + } + }; + window.requestAnimationFrame(step); + } } private internalScrollToX(element: HTMLElement, scrollTargetX: number, speed: number): void { - let startX: number = element.scrollLeft; - let diff: number = scrollTargetX - startX; - let start: number = 0; - let step = (t: number) => { - if (start === 0) { - start = t; - } - let time: number = t - start; - let percent: number = Math.min(time / speed, 1); - element.scrollLeft = (startX + diff * percent) | 0; - if (time < speed) { - window.requestAnimationFrame(step); - } - }; - window.requestAnimationFrame(step); + if (this._api.settings.player.nativeBrowserSmoothScroll) { + element.scrollTo({ + left: scrollTargetX, + behavior: 'smooth' + }); + } else { + let startX: number = element.scrollLeft; + let diff: number = scrollTargetX - startX; + let start: number = 0; + let step = (t: number) => { + if (start === 0) { + start = t; + } + let time: number = t - start; + let percent: number = Math.min(time / speed, 1); + element.scrollLeft = (startX + diff * percent) | 0; + if (time < speed) { + window.requestAnimationFrame(step); + } + }; + window.requestAnimationFrame(step); + } } } diff --git a/src/platform/javascript/HtmlElementContainer.ts b/src/platform/javascript/HtmlElementContainer.ts index 8f71c69ad..92e1f3684 100644 --- a/src/platform/javascript/HtmlElementContainer.ts +++ b/src/platform/javascript/HtmlElementContainer.ts @@ -2,39 +2,27 @@ import { IEventEmitter, IEventEmitterOfT } from '@src/EventEmitter'; import { IContainer } from '@src/platform/IContainer'; import { IMouseEventArgs } from '@src/platform/IMouseEventArgs'; import { BrowserMouseEventArgs } from '@src/platform/javascript/BrowserMouseEventArgs'; +import { Bounds } from '@src/rendering/utils/Bounds'; import { Lazy } from '@src/util/Lazy'; /** * @target web */ export class HtmlElementContainer implements IContainer { - private static resizeObserver: Lazy = new Lazy(() => new ResizeObserver((entries:ResizeObserverEntry[]) => { - for (const e of entries) { - let evt = new CustomEvent('resize', { - detail: e - }); - e.target.dispatchEvent(evt); - } - })); + private static resizeObserver: Lazy = new Lazy( + () => + new ResizeObserver((entries: ResizeObserverEntry[]) => { + for (const e of entries) { + let evt = new CustomEvent('resize', { + detail: e + }); + e.target.dispatchEvent(evt); + } + }) + ); private _resizeListeners: number = 0; - public get top(): number { - return parseFloat(this.element.style.top); - } - - public set top(value: number) { - this.element.style.top = value + 'px'; - } - - public get left(): number { - return parseFloat(this.element.style.top); - } - - public set left(value: number) { - this.element.style.left = value + 'px'; - } - public get width(): number { return this.element.offsetWidth; } @@ -130,19 +118,11 @@ export class HtmlElementContainer implements IContainer { if (this._resizeListeners === 0) { HtmlElementContainer.resizeObserver.value.observe(this.element); } - this.element.addEventListener( - 'resize', - value, - true - ); + this.element.addEventListener('resize', value, true); this._resizeListeners++; }, off: (value: any) => { - this.element.removeEventListener( - 'resize', - value, - true - ); + this.element.removeEventListener('resize', value, true); this._resizeListeners--; if (this._resizeListeners <= 0) { this._resizeListeners = 0; @@ -157,9 +137,35 @@ export class HtmlElementContainer implements IContainer { } public transitionToX(duration: number, x: number): void { - this.element.style.transition = 'all 0s linear'; - this.element.style.transitionDuration = duration + 'ms'; - this.element.style.left = x + 'px'; + this.element.style.transition = `transform ${duration}ms linear`; + this.setBounds(x, NaN, NaN, NaN); + } + + private _lastBounds: Bounds = new Bounds(); + + public getBounds() { + return this._lastBounds; + } + + public setBounds(x: number, y: number, w: number, h: number) { + if (isNaN(x)) { + x = this._lastBounds.x; + } + if (isNaN(y)) { + y = this._lastBounds.y; + } + if (isNaN(w)) { + w = this._lastBounds.w; + } + if (isNaN(h)) { + h = this._lastBounds.h; + } + this.element.style.transform = `translate(${x}px, ${y}px) scale(${w}, ${h})`; + this.element.style.transformOrigin = 'top left'; + this._lastBounds.x = x; + this._lastBounds.y = y; + this._lastBounds.w = w; + this._lastBounds.h = h; } /**