Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Update DisplayMode Names + FitContainer to fit the canvas to its parent element and maintain aspect ratio #1928

Merged
merged 10 commits into from
Jun 26, 2021
13 changes: 10 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,15 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Directly changing debug drawing by `engine.isDebug = value` has been replaced by `engine.showDebug(value)` and `engine.toggleDebug()` ([#1655](https://github.com/excaliburjs/Excalibur/issues/1655))
- `UIActor` Class instances need to be replaced to `ScreenElement` (This Class it's marked as Obsolete) ([#1656](https://github.com/excaliburjs/Excalibur/issues/1656))
- Switch to browser based promise, the Excalibur implementation `ex.Promise` is marked deprecated ([#994](https://github.com/excaliburjs/Excalibur/issues/994))
- `DisplayMode.Fill` now does what `DisplayMode.FullScreen` used to do, the resolution and viewport dynamically adjust to fit the available space, DOES NOT preserve `aspectRatio` ([#1733](https://github.com/excaliburjs/Excalibur/issues/1733))
- `DisplayMode.FullScreen` is now removed, use `Screen.goFullScreen()`.

- `DisplayMode`'s have changed ([#1733](https://github.com/excaliburjs/Excalibur/issues/1733)) & ([#1928](https://github.com/excaliburjs/Excalibur/issues/1928)):

- `DisplayMode.FitContainer` fits the screen to the available width/height in the canvas parent element, while maintaining aspect ratio and resolution
- `DisplayMode.FillContainer` update the resolution and viewport dyanmically to fill the available space in the canvas parent element, DOES NOT preserve `aspectRatio`
- `DisplayMode.FitScreen` fits the screen to the available browser window space, while maintaining aspect ratio and resolution
- `DisplayMode.FillScreen` now does what `DisplayMode.FullScreen` used to do, the resolution and viewport dynamically adjust to fill the available space in the window, DOES NOT preserve `aspectRatio` ([#1733](https://github.com/excaliburjs/Excalibur/issues/1733))
- `DisplayMode.FullScreen` is now removed, use `Screen.goFullScreen()`.

- `SpriteSheet` now is immutable after creation to reduce chance of bugs if you modified a public field. The following properties are read-only: `columns`, `rows`, `spWidth`, `spHeight`, `image`, `sprites` and `spacing`.

### Added
Expand All @@ -54,7 +61,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Add the ability to press enter to start the game after loaded
- Add Excalibur Feature Flag implementation for releasing experimental or preview features ([#1673](https://github.com/excaliburjs/Excalibur/issues/1673))
- Color now can parse RGB/A string using Color.fromRGBString('rgb(255, 255, 255)') or Color.fromRGBString('rgb(255, 255, 255, 1)')
- `DisplayMode.Fit` will now scale the game to fit the available space, preserving the `aspectRatio`. ([#1733](https://github.com/excaliburjs/Excalibur/issues/1733))
- `DisplayMode.FitScreen` will now scale the game to fit the available space, preserving the `aspectRatio`. ([#1733](https://github.com/excaliburjs/Excalibur/issues/1733))
- `SpriteSheet.spacing` now accepts a structure `{ top: number, left: number, margin: number }` for custom spacing dimensions ([#1788](https://github.com/excaliburjs/Excalibur/issues/1778))
- `SpriteSheet.ctor` now has an overload that accepts `spacing` for consistency although the object constructor is recommended ([#1788](https://github.com/excaliburjs/Excalibur/issues/1778))
- Add `SpriteSheet.getSpacingDimensions()` method to retrieve calculated spacing dimensions ([#1788](https://github.com/excaliburjs/Excalibur/issues/1778))
Expand Down
1 change: 1 addition & 0 deletions sandbox/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<li><a href="tests/ui">UI Actors</a></li>
<li><a href="tests/tilemap/tilemap.html">TileMap</a></li>
<li><a href="tests/pointer/index.html">Pointer event propagation</a></li>
<li><a href="tests/screen/index.html">Screen Fit Container</a></li>
</ul>

<h3>Engine</h3>
Expand Down
2 changes: 1 addition & 1 deletion sandbox/tests/graphics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class Game extends ex.Engine {

constructor() {
super({
displayMode: ex.DisplayMode.Fill,
displayMode: ex.DisplayMode.FillScreen,
enableCanvasTransparency: true,

});
Expand Down
38 changes: 38 additions & 0 deletions sandbox/tests/screen/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Screen</title>
<style>
.game-container {
width: 1000px;
height: 1000px;
border: solid;
}
.controls {
position: absolute;
top: 0;
right: 0;
width: 200px;
display: flex;
flex-direction: column;
}
</style>
</head>
<body>
<div class="game-container">
<canvas id="game"></canvas>
</div>
<div class="controls">
<p>The game screen should resize to fit the bordered rectangle and maintain aspect ratio</p>
<label for="containerWidth">Width (vw):</label>
<input id="containerWidth" name="containerWidth" type="range" min="1" max="100" >
<label for="containerHeight">Height (vh):</label>
<input id="containerHeight" name="containerHeight" type="range" min="1" max="100" >
</div>
<script src="../../lib/excalibur.js"></script>
<script src="screen.js"></script>
</body>
</html>
23 changes: 23 additions & 0 deletions sandbox/tests/screen/screen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

var widthEl = document.getElementById('containerWidth') as HTMLInputElement;
var heightEl = document.getElementById('containerHeight') as HTMLInputElement;
var container = document.getElementsByClassName('game-container').item(0) as HTMLDivElement;

widthEl.addEventListener('input', e => {
container.style.width = (e.target as any).value + 'vw';
});

heightEl.addEventListener('input', e => {
container.style.height = (e.target as any).value + 'vh';
});


var game = new ex.Engine({
canvasElementId: 'game',
width: 800,
height: 600,
displayMode: ex.DisplayMode.FitContainer,
pointerScope: ex.Input.PointerScope.Canvas
});

game.start();
2 changes: 1 addition & 1 deletion src/engine/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ O|===|* >________________>\n\
this._logger.debug('Engine viewport is size ' + options.width + ' x ' + options.height);
} else if (!options.displayMode) {
this._logger.debug('Engine viewport is fit');
displayMode = DisplayMode.Fit;
displayMode = DisplayMode.FitScreen;
}

if (Flags.isEnabled(Experiments.WebGL)) {
Expand Down
95 changes: 72 additions & 23 deletions src/engine/Screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ import { getPosition } from './Util/Util';
* Enum representing the different display modes available to Excalibur.
*/
export enum DisplayMode {
/**
* Default, use a specified resolution for the game. Like 800x600 pixels for example.
*/
Fixed = 'Fixed',

/**
* Fit to screen using as much space as possible while maintaining aspect ratio and resolution.
* This is not the same as [[Screen.goFullScreen]]
* This is not the same as [[Screen.goFullScreen]] but behaves in a similar way maintaining aspect ratio.
*
* You may want to center your game here is an example
* ```html
Expand All @@ -36,29 +41,29 @@ export enum DisplayMode {
* ```
*
*/
Fit = 'Fit',
FitScreen = 'FitScreen',

/**
* Fill the entire screen's css width/height for the game resolution dynamically. This means the resolution of the game will
* change dynamically as the window is resized. This is not the same as [[Screen.goFullScreen]]
*/
Fill = 'Fill',
FillScreen = 'FillScreen',

/**
* Default, use a specified resolution for the game. Like 800x600 pixels for example.
* Fit to parent element width/height using as much space as possible while maintaining aspect ratio and resolution.
*/
Fixed = 'Fixed',
FitContainer = 'FitContainer',

/**
* Allow the game to be positioned with the [[EngineOptions.position]] option
* @deprecated Use CSS to position the game canvas, will be removed in v0.26.0
* Use the parent DOM container's css width/height for the game resolution dynamically
*/
Position = 'Position',
FillContainer = 'FillContainer',

/**
* Use the parent DOM container's css width/height for the game resolution dynamically
* Allow the game to be positioned with the [[EngineOptions.position]] option
* @deprecated Use CSS to position the game canvas, will be removed in v0.26.0
*/
Container = 'Container'
Position = 'Position'
}

/**
Expand Down Expand Up @@ -150,7 +155,8 @@ export interface ScreenOptions {
pixelRatio?: number;
/**
* Optionally specify the actual pixel resolution in width/height pixels (also known as logical resolution), by default the
* resolution will be the same as the viewport. Resolution will be overridden by DisplayMode.Container and DisplayMode.FullScreen.
* resolution will be the same as the viewport. Resolution will be overridden by [[DisplayMode.FillContainer]] and
* [[DisplayMode.FillScreen]].
*/
resolution?: ScreenDimension;
/**
Expand Down Expand Up @@ -192,6 +198,7 @@ export class Screen {
private _mediaQueryList: MediaQueryList;
private _isDisposed = false;
private _logger = Logger.getInstance();
private _resizeObserver: ResizeObserver;

constructor(options: ScreenOptions) {
this.viewport = options.viewport;
Expand All @@ -215,7 +222,11 @@ export class Screen {
if (!this._isDisposed) {
// Clean up handlers
this._isDisposed = true;
this._browser.window.off('resize', this._windowResizeHandler);
this._browser.window.off('resize', this._resizeHandler);
if (this._resizeObserver) {
this._resizeObserver.disconnect();
}
this.parent.removeEventListener('resize', this._resizeHandler);
this._mediaQueryList.removeEventListener('change', this._pixelRatioChangeHandler);
this._canvas.removeEventListener('fullscreenchange', this._fullscreenChangeHandler);
}
Expand All @@ -232,8 +243,8 @@ export class Screen {
this.applyResolutionAndViewport();
};

private _windowResizeHandler = () => {
const parent = <any>(this.displayMode === DisplayMode.Container ? <any>(this.canvas.parentElement || document.body) : <any>window);
private _resizeHandler = () => {
const parent = this.parent;
this._logger.debug('View port resized');
this._setResolutionAndViewportByDisplayMode(parent);
this.applyResolutionAndViewport();
Expand Down Expand Up @@ -272,6 +283,14 @@ export class Screen {
return this._canvas;
}

public get parent(): HTMLElement | Window {
return <HTMLElement | Window>(
(this.displayMode === DisplayMode.FillContainer || this.displayMode === DisplayMode.FitContainer
? this.canvas.parentElement || document.body
: window)
);
}

public get resolution(): ScreenDimension {
return this._resolution;
}
Expand Down Expand Up @@ -611,23 +630,49 @@ export class Screen {
};
}

private _applyDisplayMode() {
if (this.displayMode === DisplayMode.Fit || this.displayMode === DisplayMode.Fill || this.displayMode === DisplayMode.Container) {
const parent = <any>(this.displayMode === DisplayMode.Container ? <any>(this.canvas.parentElement || document.body) : <any>window);
private _computeFitContainer() {
const aspect = this.aspectRatio;
let adjustedWidth = 0;
let adjustedHeight = 0;
const parent = this.canvas.parentElement;
if (parent.clientWidth / aspect < parent.clientHeight) {
adjustedWidth = parent.clientWidth;
adjustedHeight = parent.clientWidth / aspect;
} else {
adjustedWidth = parent.clientHeight * aspect;
adjustedHeight = parent.clientHeight;
}

this._setResolutionAndViewportByDisplayMode(parent);
this.viewport = {
width: adjustedWidth,
height: adjustedHeight
};
}

this._browser.window.on('resize', this._windowResizeHandler);
} else if (this.displayMode === DisplayMode.Position) {
private _applyDisplayMode() {
if (this.displayMode === DisplayMode.Position) {
this._initializeDisplayModePosition(this._position);
} else {
this._setResolutionAndViewportByDisplayMode(this.parent);

// watch resizing
if (this.parent instanceof Window) {
this._browser.window.on('resize', this._resizeHandler);
} else {
this._resizeObserver = new ResizeObserver(() => {
this._resizeHandler();
});
this._resizeObserver.observe(this.parent);
}
this.parent.addEventListener('resize', this._resizeHandler);
}
}

/**
* Sets the resoultion and viewport based on the selected display mode.
*/
private _setResolutionAndViewportByDisplayMode(parent: HTMLElement | Window) {
if (this.displayMode === DisplayMode.Container) {
if (this.displayMode === DisplayMode.FillContainer) {
this.resolution = {
width: (<HTMLElement>parent).clientWidth,
height: (<HTMLElement>parent).clientHeight
Expand All @@ -636,7 +681,7 @@ export class Screen {
this.viewport = this.resolution;
}

if (this.displayMode === DisplayMode.Fill) {
if (this.displayMode === DisplayMode.FillScreen) {
document.body.style.margin = '0px';
document.body.style.overflow = 'hidden';
this.resolution = {
Expand All @@ -647,9 +692,13 @@ export class Screen {
this.viewport = this.resolution;
}

if (this.displayMode === DisplayMode.Fit) {
if (this.displayMode === DisplayMode.FitScreen) {
this._computeFit();
}

if (this.displayMode === DisplayMode.FitContainer) {
this._computeFitContainer();
}
}

private _initializeDisplayModePosition(position: CanvasPosition) {
Expand Down
2 changes: 1 addition & 1 deletion src/spec/EngineSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe('The engine', () => {

it('should have a default resolution to SVGA (800x600) if none specified', () => {
const engine = new ex.Engine();
expect(engine.screen.displayMode).toBe(ex.DisplayMode.Fit);
expect(engine.screen.displayMode).toBe(ex.DisplayMode.FitScreen);
expect(engine.screen.resolution.width).toBe(800);
expect(engine.screen.resolution.height).toBe(600);
});
Expand Down
Loading