From a1e7f6f5557edbd17c44a81de3088790e8eec8bf Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Mon, 5 Aug 2024 19:22:45 -0500 Subject: [PATCH] feat: Add ex.Screen.worldToPagePixelRatio API New `ex.Screen.worldToPagePixelRatio` API that will return the ratio between excalibur pixels and the HTML pixels. * Additionally excalibur will now decorate the document root with this same value as a CSS variable `--ex-pixel-ratio` * Useful for scaling HTML UIs to match your game ```css .ui-container { pointer-events: none; position: absolute; transform-origin: 0 0; transform: scale( calc(var(--pixel-conversion)), calc(var(--pixel-conversion))); } ``` --- CHANGELOG.md | 13 +++++++++++ src/engine/Screen.ts | 20 +++++++++++++++++ src/engine/Util/Util.ts | 2 +- src/spec/ScreenSpec.ts | 48 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3db9d58f8..a3c4e10d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,19 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- New `ex.Screen.worldToPagePixelRatio` API that will return the ratio between excalibur pixels and the HTML pixels. + * Additionally excalibur will now decorate the document root with this same value as a CSS variable `--ex-pixel-ratio` + * Useful for scaling HTML UIs to match your game + ```css + .ui-container { + pointer-events: none; + position: absolute; + transform-origin: 0 0; + transform: scale( + calc(var(--pixel-conversion)), + calc(var(--pixel-conversion))); + } + ``` - New updates to `ex.coroutine(...)` * New `ex.CoroutineInstance` is returned (still awaitable) * Control coroutine autostart with `ex.coroutine(function*(){...}, {autostart: false})` diff --git a/src/engine/Screen.ts b/src/engine/Screen.ts index 21ccc478f..e1d976645 100644 --- a/src/engine/Screen.ts +++ b/src/engine/Screen.ts @@ -359,6 +359,7 @@ export class Screen { this._listenForPixelRatio(); this._devicePixelRatio = this._calculateDevicePixelRatio(); this.applyResolutionAndViewport(); + this.events.emit('pixelratio', { pixelRatio: this.pixelRatio } satisfies PixelRatioChangeEvent); @@ -372,6 +373,7 @@ export class Screen { this._logger.debug('View port resized'); this._setResolutionAndViewportByDisplayMode(parent); this.applyResolutionAndViewport(); + // Emit resize event this.events.emit('resize', { resolution: this.resolution, @@ -403,6 +405,22 @@ export class Screen { return this._devicePixelRatio; } + /** + * This calculates the ratio between excalibur pixels and the HTML pixels. + * + * This is useful for scaling HTML UI so that it matches your game. + */ + public get worldToPagePixelRatio(): number { + if (this._canvas) { + const pageOrigin = this.worldToPageCoordinates(Vector.Zero); + const pageDistance = this.worldToPageCoordinates(vec(1, 0)).sub(pageOrigin); + const pixelConversion = pageDistance.x; + return pixelConversion; + } else { + return 1; + } + } + /** * Get or set the pixel ratio override * @@ -563,6 +581,8 @@ export class Screen { if (this.graphicsContext instanceof ExcaliburGraphicsContext2DCanvas) { this.graphicsContext.scale(this.pixelRatio, this.pixelRatio); } + // Add the excalibur world pixel to page pixel + document.documentElement.style.setProperty('--ex-pixel-ratio', this.worldToPagePixelRatio.toString()); } public get antialiasing() { diff --git a/src/engine/Util/Util.ts b/src/engine/Util/Util.ts index 99adc8b09..3c048b723 100644 --- a/src/engine/Util/Util.ts +++ b/src/engine/Util/Util.ts @@ -7,7 +7,7 @@ import { Future } from './Future'; */ export function getPosition(el: HTMLElement): Vector { // do we need the scroll too? technically the offset method before did that - if (el) { + if (el && el.getBoundingClientRect) { const rect = el.getBoundingClientRect(); return vec(rect.x + window.scrollX, rect.y + window.scrollY); } diff --git a/src/spec/ScreenSpec.ts b/src/spec/ScreenSpec.ts index 467438553..f8ea47681 100644 --- a/src/spec/ScreenSpec.ts +++ b/src/spec/ScreenSpec.ts @@ -753,6 +753,54 @@ describe('A Screen', () => { expect(sut.worldToScreenCoordinates(ex.vec(600, 450))).toBeVector(ex.vec(800, 600)); }); + it('can calculate the excalibur worldToPagePixelRatio 2x', () => { + Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1600 }); + Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: 1200 }); + const sut = new ex.Screen({ + canvas, + context, + browser, + displayMode: ex.DisplayMode.FitScreen, + viewport: { width: 800, height: 600 }, + pixelRatio: 2 + }); + + expect(sut.worldToPagePixelRatio).toBe(2); + expect(document.documentElement.style.getPropertyValue('--ex-pixel-ratio')).toBe('2'); + }); + + it('can calculate the excalibur worldToPagePixelRatio .5x', () => { + Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 400 }); + Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: 300 }); + const sut = new ex.Screen({ + canvas, + context, + browser, + displayMode: ex.DisplayMode.FitScreen, + viewport: { width: 800, height: 600 }, + pixelRatio: 2 + }); + + expect(sut.worldToPagePixelRatio).toBe(0.5); + expect(document.documentElement.style.getPropertyValue('--ex-pixel-ratio')).toBe('0.5'); + }); + + it('can calculate the excalibur worldToPagePixelRatio 1.5x', () => { + Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1200 }); + Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: 1000 }); + const sut = new ex.Screen({ + canvas, + context, + browser, + displayMode: ex.DisplayMode.FitScreen, + viewport: { width: 800, height: 600 }, + pixelRatio: 2 + }); + + expect(sut.worldToPagePixelRatio).toBe(1.5); + expect(document.documentElement.style.getPropertyValue('--ex-pixel-ratio')).toBe('1.5'); + }); + it('can return world bounds', () => { const sut = new ex.Screen({ canvas,