diff --git a/src/platform/javascript/BrowserUiFacade.ts b/src/platform/javascript/BrowserUiFacade.ts index e29e6c300..a8ed83a2c 100644 --- a/src/platform/javascript/BrowserUiFacade.ts +++ b/src/platform/javascript/BrowserUiFacade.ts @@ -28,6 +28,7 @@ import { SettingsSerializer } from '@src/generated/SettingsSerializer'; import { WebPlatform } from '@src/platform/javascript/WebPlatform'; import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; import { AlphaSynthAudioWorkletOutput } from '@src/platform/javascript/AlphaSynthAudioWorkletOutput'; +import { ScalableHtmlElementContainer } from './ScalableHtmlElementContainer'; /** * @target web @@ -577,9 +578,13 @@ export class BrowserUiFacade implements IUiFacade { cursorWrapper.classList.add('at-cursors'); let selectionWrapper: HTMLElement = document.createElement('div'); selectionWrapper.classList.add('at-selection'); - let barCursor: HTMLElement = document.createElement('div'); + + const barCursorContainer = this.createScalingElement(); + const beatCursorContainer = this.createScalingElement(); + + let barCursor: HTMLElement = barCursorContainer.element; barCursor.classList.add('at-cursor-bar'); - let beatCursor: HTMLElement = document.createElement('div'); + let beatCursor: HTMLElement = beatCursorContainer.element; beatCursor.classList.add('at-cursor-beat'); // required css styles element.style.position = 'relative'; @@ -596,16 +601,18 @@ export class BrowserUiFacade implements IUiFacade { barCursor.style.left = '0'; barCursor.style.top = '0'; barCursor.style.willChange = 'transform'; - barCursor.style.width = '1px'; - barCursor.style.height = '1px'; + barCursorContainer.width = 1; + barCursorContainer.height = 1; + barCursorContainer.setBounds(0, 0, 1, 1); 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'; + beatCursorContainer.width = 3; + beatCursorContainer.height = 1; + beatCursorContainer.setBounds(0, 0, 1, 1); // add cursors to UI element.insertBefore(cursorWrapper, element.firstChild); @@ -614,8 +621,8 @@ export class BrowserUiFacade implements IUiFacade { cursorWrapper.appendChild(beatCursor); return new Cursors( new HtmlElementContainer(cursorWrapper), - new HtmlElementContainer(barCursor), - new HtmlElementContainer(beatCursor), + barCursorContainer, + beatCursorContainer, new HtmlElementContainer(selectionWrapper) ); } @@ -676,11 +683,24 @@ export class BrowserUiFacade implements IUiFacade { } public createSelectionElement(): IContainer | null { - let element: HTMLElement = document.createElement('div'); + return this.createScalingElement(); + } + + public createScalingElement(): ScalableHtmlElementContainer { + const element = document.createElement('div'); element.style.position = 'absolute'; - element.style.width = '1px'; - element.style.height = '1px'; - return new HtmlElementContainer(element); + + // to typical browser zoom levels are: + // Chromium: 25,33,50,67,75,80,90, 100, 110, 125, 150, 175, 200, 250, 300, 400, 500 + // Firefox: 30, 50, 67, 80, 90, 100, 110, 120, 133, 150, 170, 200, 240, 300, 400, 500 + + // with having a 100x100 scaling container we should be able to provide appropriate scaling + + const container = new ScalableHtmlElementContainer(element, 100, 100); + container.width = 1; + container.height = 1; + container.setBounds(0, 0, 1, 1); + return container; } public scrollToY(element: IContainer, scrollTargetY: number, speed: number): void { diff --git a/src/platform/javascript/HtmlElementContainer.ts b/src/platform/javascript/HtmlElementContainer.ts index f3760dd04..3eb2e8eb3 100644 --- a/src/platform/javascript/HtmlElementContainer.ts +++ b/src/platform/javascript/HtmlElementContainer.ts @@ -141,27 +141,27 @@ export class HtmlElementContainer implements IContainer { this.setBounds(x, NaN, NaN, NaN); } - private _lastBounds: Bounds = new Bounds(); + protected lastBounds: Bounds = new Bounds(); public setBounds(x: number, y: number, w: number, h: number) { if (isNaN(x)) { - x = this._lastBounds.x; + x = this.lastBounds.x; } if (isNaN(y)) { - y = this._lastBounds.y; + y = this.lastBounds.y; } if (isNaN(w)) { - w = this._lastBounds.w; + w = this.lastBounds.w; } if (isNaN(h)) { - h = this._lastBounds.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; + this.lastBounds.x = x; + this.lastBounds.y = y; + this.lastBounds.w = w; + this.lastBounds.h = h; } /** diff --git a/src/platform/javascript/ScalableHtmlElementContainer.ts b/src/platform/javascript/ScalableHtmlElementContainer.ts new file mode 100644 index 000000000..4215f4260 --- /dev/null +++ b/src/platform/javascript/ScalableHtmlElementContainer.ts @@ -0,0 +1,72 @@ +import { HtmlElementContainer } from './HtmlElementContainer'; + +/** + * An IContainer implementation which can be used for cursors and select ranges + * where browser scaling is relevant. + * + * The problem is that with having 1x1 pixel elements which are sized then to the actual size with a + * scale transform this cannot be combined properly with a browser zoom. + * + * The browser will apply first the browser zoom to the 1x1px element and then apply the scale leaving it always + * at full scale instead of a 50% browser zoom. + * + * This is solved in this container by scaling the element first up to a higher degree (as specified) + * so that the browser can do a scaling according to typical zoom levels and then the scaling will work. + * @target web + */ +export class ScalableHtmlElementContainer extends HtmlElementContainer { + private _xscale: number; + private _yscale: number; + + public constructor(element: HTMLElement, xscale: number, yscale: number) { + super(element); + this._xscale = xscale; + this._yscale = yscale; + } + + public override get width(): number { + return this.element.offsetWidth / this._xscale; + } + + public override set width(value: number) { + this.element.style.width = value * this._xscale + 'px'; + } + + public override get height(): number { + return this.element.offsetHeight / this._yscale; + } + + public override set height(value: number) { + if (value >= 0) { + this.element.style.height = value * this._yscale + 'px'; + } else { + this.element.style.height = '100%'; + } + } + + public override 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; + } else { + w = w / this._xscale; + } + if (isNaN(h)) { + h = this.lastBounds.h; + } else { + h = h / this._yscale; + } + + 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; + } +}