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: contentFitScreen and contentFitContainer Display Modes #2272

Merged
merged 6 commits into from
May 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Added faster `ex.BoundingBox.overlap(...)` implementation.
- Added `ex.Vector.min(...)` and `ex.Vector.max(...)` to find the min/max of each vector component between 2 vectors.
- Added `ex.TransformComponent.zIndexChange$` observable to watch when z index changes.
- Added new display mode `ex.DisplayMode.FitContainerAndFill`.
- Added new display mode `ex.DisplayMode.FitScreenAndFill`.

### Fixed

Expand Down
95 changes: 83 additions & 12 deletions src/engine/Screen.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Vector, vec } from './Math/vector';
import { vec, Vector } from './Math/vector';
import { Logger } from './Util/Log';
import { Camera } from './Camera';
import { BrowserEvents } from './Util/Browser';
Expand All @@ -17,6 +17,18 @@ export enum DisplayMode {
*/
Fixed = 'Fixed',

/**
* Fit an aspect ratio of the screen within the container at all times will fill the screen. This displayed area outside the aspect ratio
* is not guaranteed to be on the screen, only the [[Screen.contentArea]] is guaranteed to be on screen.
*/
FitContainerAndFill = 'ContentFitContainer',

/**
* Fit an aspect ratio within the screen at all times will fill the screen. This displayed area outside the aspect ratio is not
* guaranteed to be on the screen, only the [[Screen.contentArea]] is guaranteed to be on screen.
*/
FitScreenAndFill = 'ContentFitScreen',

/**
* Fit to screen using as much space as possible while maintaining aspect ratio and resolution.
* This is not the same as [[Screen.goFullScreen]] but behaves in a similar way maintaining aspect ratio.
Expand All @@ -41,7 +53,6 @@ export enum DisplayMode {
* width: 100%;
* }
* ```
*
*/
FitScreen = 'FitScreen',

Expand Down Expand Up @@ -159,6 +170,7 @@ export class Screen {
public graphicsContext: ExcaliburGraphicsContext;
private _canvas: HTMLCanvasElement;
private _antialiasing: boolean = true;
private _contentResolution: ScreenDimension;
private _browser: BrowserEvents;
private _camera: Camera;
private _resolution: ScreenDimension;
Expand All @@ -176,6 +188,7 @@ export class Screen {
constructor(options: ScreenOptions) {
this.viewport = options.viewport;
this.resolution = options.resolution ?? { ...this.viewport };
this._contentResolution = this.resolution;
this._displayMode = options.displayMode ?? DisplayMode.Fixed;
this._canvas = options.canvas;
this.graphicsContext = options.context;
Expand Down Expand Up @@ -278,11 +291,14 @@ export class Screen {
}

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

public get resolution(): ScreenDimension {
Expand Down Expand Up @@ -633,6 +649,53 @@ export class Screen {
};
}

private _computeFitScreenAndFill() {
document.body.style.margin = '0px';
document.body.style.overflow = 'hidden';
const vw = window.innerWidth;
const vh = window.innerHeight;
this.viewport = {
width: vw,
height: vh
};

if (vw / vh <= this._contentResolution.width / this._contentResolution.height) {
this.resolution = {
width: vw * this._contentResolution.width / vw,
height: vw * this._contentResolution.width / vw * vh / vw
};
} else {
this.resolution = {
width: vh * this._contentResolution.height / vh * vw / vh,
height: vh * this._contentResolution.height / vh
};
}
}

private _computeFitContainerAndFill() {
document.body.style.margin = '0px';
document.body.style.overflow = 'hidden';
const parent = this.canvas.parentElement;
const vw = parent.clientWidth;
const vh = parent.clientHeight;
this.viewport = {
width: vw,
height: vh
};

if (vw / vh <= this._contentResolution.width / this._contentResolution.height) {
this.resolution = {
width: vw * this._contentResolution.width / vw,
height: vw * this._contentResolution.width / vw * vh / vw
};
} else {
this.resolution = {
width: vh * this._contentResolution.height / vh * vw / vh,
height: vh * this._contentResolution.height / vh
};
}
}

private _computeFitContainer() {
const aspect = this.aspectRatio;
let adjustedWidth = 0;
Expand Down Expand Up @@ -673,8 +736,8 @@ export class Screen {
private _setResolutionAndViewportByDisplayMode(parent: HTMLElement | Window) {
if (this.displayMode === DisplayMode.FillContainer) {
this.resolution = {
width: (<HTMLElement>parent).clientWidth,
height: (<HTMLElement>parent).clientHeight
width: (<HTMLElement> parent).clientWidth,
height: (<HTMLElement> parent).clientHeight
};

this.viewport = this.resolution;
Expand All @@ -684,8 +747,8 @@ export class Screen {
document.body.style.margin = '0px';
document.body.style.overflow = 'hidden';
this.resolution = {
width: (<Window>parent).innerWidth,
height: (<Window>parent).innerHeight
width: (<Window> parent).innerWidth,
height: (<Window> parent).innerHeight
};

this.viewport = this.resolution;
Expand All @@ -698,5 +761,13 @@ export class Screen {
if (this.displayMode === DisplayMode.FitContainer) {
this._computeFitContainer();
}

if (this.displayMode === DisplayMode.FitScreenAndFill) {
this._computeFitScreenAndFill();
}

if (this.displayMode === DisplayMode.FitContainerAndFill){
this._computeFitContainerAndFill();
}
}
}
}
92 changes: 92 additions & 0 deletions src/spec/ScreenSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,98 @@ describe('A Screen', () => {
expect(sut.viewport.height).toBe(800);
});

it('can use the FitScreenAndFill display mode, screen aspectRatio > window aspect ratio', () => {
const sut = new ex.Screen({
canvas,
context,
browser,
displayMode: ex.DisplayMode.FitScreenAndFill,
viewport: { width: 800, height: 600 }
});

Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1300 });
Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: 1300 });

window.dispatchEvent(new Event('resize'));

expect(sut.resolution.width).toBe(800);
expect(sut.resolution.height).toBe(800);
expect(sut.viewport.width).toBe(1300);
expect(sut.viewport.height).toBe(1300);
});

it('can use the FitScreenAndFill display mode, screen aspectRatio < window aspect ratio', () => {
const sut = new ex.Screen({
canvas,
context,
browser,
displayMode: ex.DisplayMode.FitScreenAndFill,
viewport: { width: 800, height: 600 }
});

Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1300 });
Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: 800 });

window.dispatchEvent(new Event('resize'));

expect(sut.resolution.width).toBe(975);
expect(sut.resolution.height).toBe(600);
expect(sut.viewport.width).toBe(1300);
expect(sut.viewport.height).toBe(800);
});

it('can use the FitContainerAndFill display mode, screen aspectRatio > container aspect ratio', () => {
const parentEl = document.createElement('div');
document.body.removeChild(canvas);
parentEl.appendChild(canvas);
document.body.appendChild(parentEl);

const sut = new ex.Screen({
canvas,
context,
browser,
displayMode: ex.DisplayMode.FitContainerAndFill,
viewport: { width: 800, height: 600 }
});

parentEl.style.width = '1300px';
parentEl.style.height = '1300px';
parentEl.dispatchEvent(new Event('resize'));

expect(sut.parent).toBe(parentEl);
expect(sut.resolution.width).toBe(800);
expect(sut.resolution.height).toBe(800);
expect(sut.viewport.width).toBe(1300);
expect(sut.viewport.height).toBe(1300);

});

it('can use the FitContainerAndFill display mode, screen aspectRatio < container aspect ratio', () => {
const parentEl = document.createElement('div');
document.body.removeChild(canvas);
parentEl.appendChild(canvas);
document.body.appendChild(parentEl);

const sut = new ex.Screen({
canvas,
context,
browser,
displayMode: ex.DisplayMode.FitContainerAndFill,
viewport: { width: 800, height: 600 }
});

parentEl.style.width = '1300px';
parentEl.style.height = '800px';
parentEl.dispatchEvent(new Event('resize'));

expect(sut.parent).toBe(parentEl);
expect(sut.resolution.width).toBe(975);
expect(sut.resolution.height).toBe(600);
expect(sut.viewport.width).toBe(1300);
expect(sut.viewport.height).toBe(800);

});

describe('can use fit container display mode, the viewport', () => {
it('will adjust to height', () => {
const parentEl = document.createElement('div');
Expand Down