• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excaliburjs / Excalibur / 27357492851

11 Jun 2026 03:19PM UTC coverage: 88.486% (-0.004%) from 88.49%
27357492851

Pull #3776

github

web-flow
Merge 68f2e655b into 1641e2088
Pull Request #3776: fix: Browser handler leak

6734 of 8905 branches covered (75.62%)

12 of 12 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

15340 of 17336 relevant lines covered (88.49%)

24181.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.03
/src/engine/screen.ts
1
import { vec, Vector } from './math/vector';
2
import { Logger } from './util/log';
3
import type { Camera } from './camera';
4
import type { BrowserEvents } from './util/browser';
5
import { BoundingBox } from './collision/index';
6
import type { ExcaliburGraphicsContext } from './graphics/context/excalibur-graphics-context';
7
import { getPosition } from './util/util';
8
import { ExcaliburGraphicsContextWebGL } from './graphics/context/excalibur-graphics-context-webgl';
9
import { ExcaliburGraphicsContext2DCanvas } from './graphics/context/excalibur-graphics-context-2d-canvas';
10
import { EventEmitter } from './event-emitter';
11

12
/**
13
 * Enum representing the different display modes available to Excalibur.
14
 */
15
export enum DisplayMode {
254✔
16
  /**
17
   * Default, use a specified resolution for the game. Like 800x600 pixels for example.
18
   */
19
  Fixed = 'Fixed',
254✔
20

21
  /**
22
   * Fit the aspect ratio given by the game resolution within the container at all times will fill any gaps with canvas.
23
   * The displayed area outside the aspect ratio is not guaranteed to be on the screen, only the {@apilink Screen.contentArea}
24
   * is guaranteed to be on screen.
25
   */
26
  FitContainerAndFill = 'FitContainerAndFill',
254✔
27

28
  /**
29
   * Fit the aspect ratio given by the game resolution the screen at all times will fill the screen.
30
   * This displayed area outside the aspect ratio is not guaranteed to be on the screen, only the {@apilink Screen.contentArea}
31
   * is guaranteed to be on screen.
32
   */
33
  FitScreenAndFill = 'FitScreenAndFill',
254✔
34

35
  /**
36
   * Fit the viewport to the parent element maintaining aspect ratio given by the game resolution, but zooms in to avoid the black bars
37
   * (letterbox) that would otherwise be present in {@apilink FitContainer}.
38
   *
39
   * **warning** This will clip some drawable area from the user because of the zoom,
40
   * use {@apilink Screen.contentArea} to know the safe to draw area.
41
   */
42
  FitContainerAndZoom = 'FitContainerAndZoom',
254✔
43

44
  /**
45
   * Fit the viewport to the device screen maintaining aspect ratio given by the game resolution, but zooms in to avoid the black bars
46
   * (letterbox) that would otherwise be present in {@apilink FitScreen}.
47
   *
48
   * **warning** This will clip some drawable area from the user because of the zoom,
49
   * use {@apilink Screen.contentArea} to know the safe to draw area.
50
   */
51
  FitScreenAndZoom = 'FitScreenAndZoom',
254✔
52

53
  /**
54
   * Fit to screen using as much space as possible while maintaining aspect ratio and resolution.
55
   * This is not the same as {@apilink Screen.enterFullscreen} but behaves in a similar way maintaining aspect ratio.
56
   *
57
   * You may want to center your game here is an example
58
   * ```html
59
   * <!-- html -->
60
   * <body>
61
   * <main>
62
   *   <canvas id="game"></canvas>
63
   * </main>
64
   * </body>
65
   * ```
66
   *
67
   * ```css
68
   * // css
69
   * main {
70
   *   display: flex;
71
   *   align-items: center;
72
   *   justify-content: center;
73
   *   height: 100%;
74
   *   width: 100%;
75
   * }
76
   * ```
77
   */
78
  FitScreen = 'FitScreen',
254✔
79

80
  /**
81
   * Fill the entire screen's css width/height for the game resolution dynamically. This means the resolution of the game will
82
   * change dynamically as the window is resized. This is not the same as {@apilink Screen.enterFullscreen}
83
   */
84
  FillScreen = 'FillScreen',
254✔
85

86
  /**
87
   * Fit to parent element width/height using as much space as possible while maintaining aspect ratio and resolution.
88
   */
89
  FitContainer = 'FitContainer',
254✔
90

91
  /**
92
   * Use the parent DOM container's css width/height for the game resolution dynamically
93
   */
94
  FillContainer = 'FillContainer'
254✔
95
}
96

97
/**
98
 * Convenience class for quick resolutions
99
 * Mostly sourced from https://emulation.gametechwiki.com/index.php/Resolution
100
 */
101
export class Resolution {
102
  /* istanbul ignore next */
103
  public static get SVGA(): Resolution {
104
    return { width: 800, height: 600 };
105
  }
106

107
  /* istanbul ignore next */
108
  public static get Standard(): Resolution {
109
    return { width: 1920, height: 1080 };
110
  }
111

112
  /* istanbul ignore next */
113
  public static get Atari2600(): Resolution {
114
    return { width: 160, height: 192 };
115
  }
116

117
  /* istanbul ignore next */
118
  public static get GameBoy(): Resolution {
119
    return { width: 160, height: 144 };
120
  }
121

122
  /* istanbul ignore next */
123
  public static get GameBoyAdvance(): Resolution {
124
    return { width: 240, height: 160 };
125
  }
126

127
  /* istanbul ignore next */
128
  public static get NintendoDS(): Resolution {
129
    return { width: 256, height: 192 };
130
  }
131

132
  /* istanbul ignore next */
133
  public static get NES(): Resolution {
134
    return { width: 256, height: 224 };
135
  }
136

137
  /* istanbul ignore next */
138
  public static get SNES(): Resolution {
139
    return { width: 256, height: 244 };
140
  }
141
}
142

143
export type ViewportUnit = 'pixel' | 'percent';
144

145
export interface Resolution {
146
  width: number;
147
  height: number;
148
}
149

150
export interface ViewportDimension {
151
  widthUnit?: ViewportUnit;
152
  heightUnit?: ViewportUnit;
153
  width: number;
154
  height: number;
155
}
156

157
export interface ScreenOptions {
158
  /**
159
   * Canvas element to build a screen on
160
   */
161
  canvas: HTMLCanvasElement;
162

163
  /**
164
   * Graphics context for the screen
165
   */
166
  context: ExcaliburGraphicsContext;
167

168
  /**
169
   * Browser abstraction
170
   */
171
  browser: BrowserEvents;
172
  /**
173
   * Optionally set antialiasing, defaults to true. If set to true, images will be smoothed
174
   */
175
  antialiasing?: boolean;
176

177
  /**
178
   * Optionally set the image rendering CSS hint on the canvas element, default is auto
179
   */
180
  canvasImageRendering?: 'auto' | 'pixelated';
181
  /**
182
   * Optionally override the pixel ratio to use for the screen, otherwise calculated automatically from the browser
183
   */
184
  pixelRatio?: number;
185
  /**
186
   * Optionally specify the actual pixel resolution in width/height pixels (also known as logical resolution), by default the
187
   * resolution will be the same as the viewport. Resolution will be overridden by {@apilink DisplayMode.FillContainer} and
188
   * {@apilink DisplayMode.FillScreen}.
189
   */
190
  resolution?: Resolution;
191
  /**
192
   * Visual viewport size in css pixel, if resolution is not specified it will be the same as the viewport
193
   */
194
  viewport: ViewportDimension;
195
  /**
196
   * Set the display mode of the screen, by default DisplayMode.Fixed.
197
   */
198
  displayMode?: DisplayMode;
199
}
200

201
/**
202
 * Fires when the screen resizes, useful if you have logic that needs to be aware of resolution/viewport constraints
203
 */
204
export interface ScreenResizeEvent {
205
  /**
206
   * Current viewport in css pixels of the screen
207
   */
208
  viewport: ViewportDimension;
209
  /**
210
   * Current resolution in world pixels of the screen
211
   */
212
  resolution: Resolution;
213
}
214

215
/**
216
 * Fires when the pixel ratio changes, useful to know if you've moved to a hidpi screen or back
217
 */
218
export interface PixelRatioChangeEvent {
219
  /**
220
   * Current pixel ratio of the screen
221
   */
222
  pixelRatio: number;
223
}
224

225
/**
226
 * Fires when the browser fullscreen api is successfully engaged or disengaged
227
 */
228
export interface FullScreenChangeEvent {
229
  /**
230
   * Current fullscreen state
231
   */
232
  fullscreen: boolean;
233
}
234

235
/**
236
 * Built in events supported by all entities
237
 */
238
export interface ScreenEvents {
239
  /**
240
   * Fires when the screen resizes, useful if you have logic that needs to be aware of resolution/viewport constraints
241
   */
242
  resize: ScreenResizeEvent;
243
  /**
244
   * Fires when the pixel ratio changes, useful to know if you've moved to a hidpi screen or back
245
   */
246
  pixelratio: PixelRatioChangeEvent;
247
  /**
248
   * Fires when the browser fullscreen api is successfully engaged or disengaged
249
   */
250
  fullscreen: FullScreenChangeEvent;
251
}
252

253
export const ScreenEvents = {
254✔
254
  ScreenResize: 'resize',
255
  PixelRatioChange: 'pixelratio',
256
  FullScreenChange: 'fullscreen'
257
} as const;
258

259
/**
260
 * The Screen handles all aspects of interacting with the screen for Excalibur.
261
 */
262
export class Screen {
263
  public graphicsContext: ExcaliburGraphicsContext;
264
  /**
265
   * Listen to screen events {@apilink ScreenEvents}
266
   */
267
  public events = new EventEmitter<ScreenEvents>();
811✔
268
  private _canvas: HTMLCanvasElement;
269
  private _antialiasing: boolean = true;
811✔
270
  private _canvasImageRendering: 'auto' | 'pixelated' = 'auto';
811✔
271
  private _contentResolution: Resolution;
272
  private _browser: BrowserEvents;
273
  private _camera: Camera;
274
  private _resolution: Resolution;
275
  private _resolutionStack: Resolution[] = [];
811✔
276
  private _viewport: ViewportDimension;
277
  private _viewportStack: ViewportDimension[] = [];
811✔
278
  private _pixelRatioOverride: number | null = null;
811✔
279
  private _displayMode: DisplayMode;
280
  private _isFullscreen = false;
811✔
281
  private _mediaQueryList: MediaQueryList;
282
  private _isDisposed = false;
811✔
283
  private _logger = Logger.getInstance();
811✔
284
  private _resizeObserver: ResizeObserver;
285

286
  constructor(options: ScreenOptions) {
287
    this.viewport = options.viewport;
811✔
288
    this.resolution = options.resolution ?? { ...this.viewport };
811✔
289
    this._contentResolution = this.resolution;
811✔
290
    this._displayMode = options.displayMode ?? DisplayMode.Fixed;
811✔
291
    this._canvas = options.canvas;
811✔
292
    this.graphicsContext = options.context;
811✔
293
    this._antialiasing = options.antialiasing ?? this._antialiasing;
811✔
294
    this._canvasImageRendering = options.canvasImageRendering ?? this._canvasImageRendering;
811✔
295
    this._browser = options.browser;
811✔
296
    this._pixelRatioOverride = options.pixelRatio;
811✔
297

298
    this._applyDisplayMode();
811✔
299

300
    this._listenForPixelRatio();
811✔
301

302
    this._canvas.addEventListener('fullscreenchange', this._fullscreenChangeHandler);
811✔
303
    this.applyResolutionAndViewport();
811✔
304
  }
305

306
  private _listenForPixelRatio() {
307
    if (this._mediaQueryList && !this._mediaQueryList.addEventListener) {
811!
308
      // Safari <=13.1 workaround, remove any existing handlers
309
      this._mediaQueryList.removeListener(this._pixelRatioChangeHandler);
×
310
    }
311
    this._mediaQueryList = this._browser.window.nativeComponent.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
811✔
312

313
    // Safari <=13.1 workaround
314
    if (this._mediaQueryList.addEventListener) {
811!
315
      this._mediaQueryList.addEventListener('change', this._pixelRatioChangeHandler, { once: true });
811✔
316
    } else {
317
      this._mediaQueryList.addListener(this._pixelRatioChangeHandler);
×
318
    }
319
  }
320

321
  public dispose(): void {
322
    if (!this._isDisposed) {
754!
323
      // Clean up handlers
324
      this._isDisposed = true;
754✔
325
      this.events.clear();
754✔
326
      this._browser.window.off('resize', this._resizeHandler);
754✔
327
      this._browser.window.clear();
754✔
328
      if (this._resizeObserver) {
754!
329
        this._resizeObserver.disconnect();
×
330
      }
331
      this.parent.removeEventListener('resize', this._resizeHandler);
754✔
332
      // Safari <=13.1 workaround
333
      if (this._mediaQueryList.removeEventListener) {
754!
334
        this._mediaQueryList.removeEventListener('change', this._pixelRatioChangeHandler);
754✔
335
      } else {
336
        this._mediaQueryList.removeListener(this._pixelRatioChangeHandler);
×
337
      }
338
      this._canvas.removeEventListener('fullscreenchange', this._fullscreenChangeHandler);
754✔
339
      this._canvas = null;
754✔
340
    }
341
  }
342

343
  private _fullscreenChangeHandler = () => {
811✔
344
    if (this._isDisposed) {
2!
345
      return;
×
346
    }
347
    this._isFullscreen = !this._isFullscreen;
2✔
348
    this._logger.debug('Fullscreen Change', this._isFullscreen);
2✔
349
    this.events.emit('fullscreen', {
2✔
350
      fullscreen: this.isFullscreen
351
    } satisfies FullScreenChangeEvent);
352
  };
353

354
  private _pixelRatioChangeHandler = () => {
811✔
355
    if (this._isDisposed) {
×
356
      return;
×
357
    }
358
    this._logger.debug('Pixel Ratio Change', window.devicePixelRatio);
×
359
    this._listenForPixelRatio();
×
360
    this._devicePixelRatio = this._calculateDevicePixelRatio();
×
361
    this.applyResolutionAndViewport();
×
362

363
    this.events.emit('pixelratio', {
×
364
      pixelRatio: this.pixelRatio
365
    } satisfies PixelRatioChangeEvent);
366
  };
367

368
  private _resizeHandler = () => {
811✔
369
    if (this._isDisposed) {
98!
UNCOV
370
      return;
×
371
    }
372
    const parent = this.parent;
98✔
373
    this._logger.debug('View port resized');
98✔
374
    this._setResolutionAndViewportByDisplayMode(parent);
98✔
375
    this.applyResolutionAndViewport();
98✔
376

377
    // Emit resize event
378
    this.events.emit('resize', {
98✔
379
      resolution: this.resolution,
380
      viewport: this.viewport
381
    } satisfies ScreenResizeEvent);
382
  };
383

384
  private _calculateDevicePixelRatio() {
385
    if (window.devicePixelRatio < 1) {
811!
386
      return 1;
×
387
    }
388

389
    const devicePixelRatio = window.devicePixelRatio || 1;
811!
390

391
    return devicePixelRatio;
811✔
392
  }
393

394
  // Asking the window.devicePixelRatio is expensive we do it once
395
  private _devicePixelRatio = this._calculateDevicePixelRatio();
811✔
396

397
  /**
398
   * Returns the computed pixel ratio, first using any override, then the device pixel ratio
399
   */
400
  public get pixelRatio(): number {
401
    if (this._pixelRatioOverride) {
5,520✔
402
      return this._pixelRatioOverride;
4,931✔
403
    }
404

405
    return this._devicePixelRatio;
589✔
406
  }
407

408
  /**
409
   * This calculates the ratio between excalibur pixels and the HTML pixels.
410
   *
411
   * This is useful for scaling HTML UI so that it matches your game.
412
   */
413
  public get worldToPagePixelRatio(): number {
414
    if (this._canvas) {
957!
415
      const pageOrigin = this.worldToPageCoordinates(Vector.Zero);
957✔
416
      const pageDistance = this.worldToPageCoordinates(vec(1, 0)).sub(pageOrigin);
957✔
417
      const pixelConversion = pageDistance.x;
957✔
418
      return pixelConversion;
957✔
419
    } else {
420
      return 1;
×
421
    }
422
  }
423

424
  /**
425
   * Get or set the pixel ratio override
426
   *
427
   * You will need to call applyResolutionAndViewport() affect change on the screen
428
   */
429
  public get pixelRatioOverride(): number | undefined {
430
    return this._pixelRatioOverride;
2✔
431
  }
432

433
  public set pixelRatioOverride(value: number | undefined) {
434
    this._pixelRatioOverride = value;
1✔
435
  }
436

437
  public get isHiDpi() {
438
    return this.pixelRatio !== 1;
3✔
439
  }
440

441
  public get displayMode(): DisplayMode {
442
    return this._displayMode;
10,568✔
443
  }
444

445
  public get canvas(): HTMLCanvasElement {
446
    return this._canvas;
1,892✔
447
  }
448

449
  public get parent(): HTMLElement | Window {
450
    switch (this.displayMode) {
3,295!
451
      case DisplayMode.FillContainer:
452
      case DisplayMode.FitContainer:
453
      case DisplayMode.FitContainerAndFill:
454
      case DisplayMode.FitContainerAndZoom:
455
        return this.canvas.parentElement || document.body;
40!
456
      default:
457
        return window;
3,255✔
458
    }
459
  }
460

461
  public get resolution(): Resolution {
462
    return this._resolution;
28,638✔
463
  }
464

465
  public set resolution(resolution: Resolution) {
466
    this._resolution = resolution;
891✔
467
  }
468

469
  /**
470
   * Returns screen dimensions in pixels or percentage
471
   */
472
  public get viewport(): ViewportDimension {
473
    if (this._viewport) {
8,366!
474
      return this._viewport;
8,366✔
475
    }
476
    return this._resolution;
×
477
  }
478

479
  public set viewport(viewport: ViewportDimension) {
480
    this._viewport = viewport;
931✔
481
  }
482

483
  public get aspectRatio() {
484
    return this._resolution.width / this._resolution.height;
73✔
485
  }
486

487
  public get scaledWidth() {
488
    return this._resolution.width * this.pixelRatio;
1,759✔
489
  }
490

491
  public get scaledHeight() {
492
    return this._resolution.height * this.pixelRatio;
1,759✔
493
  }
494

495
  public setCurrentCamera(camera: Camera) {
496
    this._camera = camera;
707✔
497
  }
498

499
  public pushResolutionAndViewport() {
500
    this._resolutionStack.push(this.resolution);
17✔
501
    this._viewportStack.push(this.viewport);
17✔
502

503
    this.resolution = { ...this.resolution };
17✔
504
    this.viewport = { ...this.viewport };
17✔
505
  }
506

507
  public peekViewport(): ViewportDimension {
508
    return this._viewportStack[this._viewportStack.length - 1];
×
509
  }
510

511
  public peekResolution(): Resolution {
512
    return this._resolutionStack[this._resolutionStack.length - 1];
5✔
513
  }
514

515
  public popResolutionAndViewport() {
516
    if (this._resolutionStack.length && this._viewportStack.length) {
15!
517
      this.resolution = this._resolutionStack.pop();
15✔
518
      this.viewport = this._viewportStack.pop();
15✔
519
    }
520
  }
521

522
  public applyResolutionAndViewport() {
523
    if (this.graphicsContext instanceof ExcaliburGraphicsContextWebGL) {
954✔
524
      const scaledResolutionSupported = this.graphicsContext.checkIfResolutionSupported({
800✔
525
        width: this.scaledWidth,
526
        height: this.scaledHeight
527
      });
528
      if (!scaledResolutionSupported) {
800✔
529
        this._logger.warnOnce(
2✔
530
          `The currently configured resolution (${this.resolution.width}x${this.resolution.height}) and pixel ratio (${this.pixelRatio})` +
531
            ' are too large for the platform WebGL implementation, this may work but cause WebGL rendering to behave oddly.' +
532
            ' Try reducing the resolution or disabling Hi DPI scaling to avoid this' +
533
            ' (read more here https://excaliburjs.com/docs/screens#understanding-viewport--resolution).'
534
        );
535

536
        // Attempt to recover if the user hasn't configured a specific ratio for up scaling
537
        if (!this.pixelRatioOverride) {
2✔
538
          let currentPixelRatio = Math.max(1, this.pixelRatio - 0.5);
1✔
539
          let newResolutionSupported = false;
1✔
540
          while (currentPixelRatio > 1 && !newResolutionSupported) {
1✔
541
            currentPixelRatio = Math.max(1, currentPixelRatio - 0.5);
1✔
542
            const width = this._resolution.width * currentPixelRatio;
1✔
543
            const height = this._resolution.height * currentPixelRatio;
1✔
544
            newResolutionSupported = this.graphicsContext.checkIfResolutionSupported({ width, height });
1✔
545
          }
546
          this.pixelRatioOverride = currentPixelRatio;
1✔
547
          this._logger.warnOnce(
1✔
548
            'Scaled resolution too big attempted recovery!' +
549
              ` Pixel ratio was automatically reduced to (${this.pixelRatio}) to avoid 4k texture limit.` +
550
              ' Setting `ex.Engine({pixelRatio: ...}) will override any automatic recalculation, do so at your own risk.` ' +
551
              ' (read more here https://excaliburjs.com/docs/screens#understanding-viewport--resolution).'
552
          );
553
        }
554
      }
555
    }
556

557
    this._canvas.width = this.scaledWidth;
954✔
558
    this._canvas.height = this.scaledHeight;
954✔
559

560
    if (this._canvasImageRendering === 'auto') {
954✔
561
      this._canvas.style.imageRendering = 'auto';
176✔
562
    } else {
563
      this._canvas.style.imageRendering = 'pixelated';
778✔
564
      // Fall back to 'crisp-edges' if 'pixelated' is not supported
565
      // Currently for firefox https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering
566
      if (this._canvas.style.imageRendering === '') {
778✔
567
        this._canvas.style.imageRendering = 'crisp-edges';
2✔
568
      }
569
    }
570

571
    const widthUnit = this.viewport.widthUnit === 'percent' ? '%' : 'px';
954✔
572
    const heightUnit = this.viewport.heightUnit === 'percent' ? '%' : 'px';
954✔
573

574
    this._canvas.style.width = this.viewport.width + widthUnit;
954✔
575
    this._canvas.style.height = this.viewport.height + heightUnit;
954✔
576

577
    // After messing with the canvas width/height the graphics context is invalidated and needs to have some properties reset
578
    this.graphicsContext.updateViewport(this.resolution);
954✔
579
    this.graphicsContext.resetTransform();
954✔
580
    this.graphicsContext.smoothing = this._antialiasing;
954✔
581
    if (this.graphicsContext instanceof ExcaliburGraphicsContext2DCanvas) {
954✔
582
      this.graphicsContext.scale(this.pixelRatio, this.pixelRatio);
154✔
583
    }
584
    // Add the excalibur world pixel to page pixel
585
    document.documentElement.style.setProperty('--ex-pixel-ratio', this.worldToPagePixelRatio.toString());
954✔
586
  }
587

588
  /**
589
   * Get or set screen antialiasing,
590
   *
591
   * If true smoothing is applied
592
   */
593
  public get antialiasing() {
594
    return this._antialiasing;
847✔
595
  }
596

597
  /**
598
   * Get or set screen antialiasing
599
   */
600
  public set antialiasing(isSmooth: boolean) {
601
    this._antialiasing = isSmooth;
1,673✔
602
    this.graphicsContext.smoothing = this._antialiasing;
1,673✔
603
  }
604

605
  /**
606
   * Returns true if excalibur is fullscreen using the browser fullscreen api
607
   * @deprecated use isFullscreen()
608
   */
609
  public get isFullScreen() {
610
    return this._isFullscreen;
5✔
611
  }
612

613
  /**
614
   * Returns true if excalibur is fullscreen using the browser fullscreen api
615
   */
616
  public get isFullscreen() {
617
    return this._isFullscreen;
2✔
618
  }
619

620
  /**
621
   * Requests to go fullscreen using the browser fullscreen api, requires user interaction to be successful.
622
   * For example, wire this to a user click handler.
623
   *
624
   * Optionally specify a target element id to go fullscreen, by default the game canvas is used
625
   * @param elementId
626
   * @deprecated use enterFullscreen(...)
627
   */
628
  public goFullScreen(elementId?: string): Promise<void> {
629
    return this.enterFullscreen(elementId);
×
630
  }
631

632
  /**
633
   * Requests to enter fullscreen using the browser fullscreen api, requires user interaction to be successful.
634
   * For example, wire this to a user click handler.
635
   *
636
   * Optionally specify a target element id to go fullscreen, by default the game canvas is used
637
   * @param elementId
638
   */
639
  public enterFullscreen(elementId?: string): Promise<void> {
640
    if (elementId) {
2✔
641
      const maybeElement = document.getElementById(elementId);
1✔
642
      // workaround for safari partial support
643
      if (maybeElement?.requestFullscreen || (maybeElement as any)?.webkitRequestFullscreen) {
1!
644
        if (!maybeElement.getAttribute('ex-fullscreen-listener')) {
1!
645
          maybeElement.setAttribute('ex-fullscreen-listener', 'true');
1✔
646
          maybeElement.addEventListener('fullscreenchange', this._fullscreenChangeHandler);
1✔
647
        }
648
        if (maybeElement.requestFullscreen) {
1!
649
          return maybeElement.requestFullscreen() ?? Promise.resolve();
1!
650
        } else if ((maybeElement as any).webkitRequestFullscreen) {
×
651
          return (maybeElement as any).webkitRequestFullscreen() ?? Promise.resolve();
×
652
        }
653
      }
654
    }
655
    if (this._canvas?.requestFullscreen) {
1!
656
      return this._canvas?.requestFullscreen() ?? Promise.resolve();
1!
657
    } else if ((this._canvas as any).webkitRequestFullscreen) {
×
658
      return (this._canvas as any).webkitRequestFullscreen() ?? Promise.resolve();
×
659
    }
660
    this._logger.warnOnce('Could not go fullscreen, is this an iPhone? Currently Apple does not support fullscreen on iPhones');
×
661
    return Promise.resolve();
×
662
  }
663

664
  /**
665
   * Requests to exit fullscreen using the browser fullscreen api
666
   * @deprecated use exitFullscreen()
667
   */
668
  public exitFullScreen(): Promise<void> {
669
    return this.exitFullscreen();
×
670
  }
671

672
  public exitFullscreen(): Promise<void> {
673
    return document.exitFullscreen();
×
674
  }
675

676
  private _viewportToPixels(viewport: ViewportDimension) {
677
    return {
3,387✔
678
      width: viewport.widthUnit === 'percent' ? this.canvas.offsetWidth : viewport.width,
3,387✔
679
      height: viewport.heightUnit === 'percent' ? this.canvas.offsetHeight : viewport.height
3,387✔
680
    } satisfies ViewportDimension;
681
  }
682

683
  /**
684
   * Takes a coordinate in normal html page space, for example from a pointer move event, and translates it to
685
   * Excalibur screen space.
686
   *
687
   * Excalibur screen space starts at the top left (0, 0) corner of the viewport, and extends to the
688
   * bottom right corner (resolutionX, resolutionY). When using *AndFill suffixed display modes screen space
689
   * (0, 0) is the top left of the safe content area bounding box not the viewport.
690
   * @param point
691
   */
692
  public pageToScreenCoordinates(point: Vector): Vector {
693
    let newX = point.x;
1,439✔
694
    let newY = point.y;
1,439✔
695

696
    if (!this._isFullscreen) {
1,439✔
697
      newX -= getPosition(this._canvas).x;
1,437✔
698
      newY -= getPosition(this._canvas).y;
1,437✔
699
    }
700

701
    const viewport = this._viewportToPixels(this.viewport);
1,439✔
702

703
    // if fullscreen api on it centers with black bars
704
    // we need to adjust the screen to world coordinates in this case
705
    if (this._isFullscreen) {
1,439✔
706
      if (window.innerWidth / this.aspectRatio < window.innerHeight) {
2✔
707
        const screenHeight = window.innerWidth / this.aspectRatio;
1✔
708
        const screenMarginY = (window.innerHeight - screenHeight) / 2;
1✔
709
        newY = ((newY - screenMarginY) / screenHeight) * viewport.height;
1✔
710
        newX = (newX / window.innerWidth) * viewport.width;
1✔
711
      } else {
712
        const screenWidth = window.innerHeight * this.aspectRatio;
1✔
713
        const screenMarginX = (window.innerWidth - screenWidth) / 2;
1✔
714
        newX = ((newX - screenMarginX) / screenWidth) * viewport.width;
1✔
715
        newY = (newY / window.innerHeight) * viewport.height;
1✔
716
      }
717
    }
718

719
    newX = (newX / viewport.width) * this.resolution.width;
1,439✔
720
    newY = (newY / viewport.height) * this.resolution.height;
1,439✔
721

722
    // offset by content area
723
    newX = newX - this.contentArea.left;
1,439✔
724
    newY = newY - this.contentArea.top;
1,439✔
725

726
    return new Vector(newX, newY);
1,439✔
727
  }
728

729
  /**
730
   * Takes a coordinate in Excalibur screen space, and translates it to normal html page space. For example,
731
   * this is where html elements might live if you want to position them relative to Excalibur.
732
   *
733
   * Excalibur screen space starts at the top left (0, 0) corner of the viewport, and extends to the
734
   * bottom right corner (resolutionX, resolutionY)
735
   * @param point
736
   */
737
  public screenToPageCoordinates(point: Vector): Vector {
738
    let newX = point.x;
1,948✔
739
    let newY = point.y;
1,948✔
740

741
    // no need to offset by content area, drawing is already offset by this
742

743
    const viewport = this._viewportToPixels(this.viewport);
1,948✔
744

745
    newX = (newX / this.resolution.width) * viewport.width;
1,948✔
746
    newY = (newY / this.resolution.height) * viewport.height;
1,948✔
747

748
    if (this._isFullscreen) {
1,948✔
749
      if (window.innerWidth / this.aspectRatio < window.innerHeight) {
2✔
750
        const screenHeight = window.innerWidth / this.aspectRatio;
1✔
751
        const screenMarginY = (window.innerHeight - screenHeight) / 2;
1✔
752
        newY = (newY / viewport.height) * screenHeight + screenMarginY;
1✔
753
        newX = (newX / viewport.width) * window.innerWidth;
1✔
754
      } else {
755
        const screenWidth = window.innerHeight * this.aspectRatio;
1✔
756
        const screenMarginX = (window.innerWidth - screenWidth) / 2;
1✔
757
        newX = (newX / viewport.width) * screenWidth + screenMarginX;
1✔
758
        newY = (newY / viewport.height) * window.innerHeight;
1✔
759
      }
760
    }
761

762
    if (!this._isFullscreen) {
1,948✔
763
      newX += getPosition(this._canvas).x;
1,946✔
764
      newY += getPosition(this._canvas).y;
1,946✔
765
    }
766

767
    return new Vector(newX, newY);
1,948✔
768
  }
769

770
  /**
771
   * Takes a coordinate in Excalibur screen space, and translates it to Excalibur world space.
772
   *
773
   * World space is where {@apilink Entity | `entities`} in Excalibur live by default {@apilink CoordPlane.World}
774
   * and extends infinitely out relative from the {@apilink Camera}.
775
   * @param point  Screen coordinate to convert
776
   */
777
  public screenToWorldCoordinates(point: Vector): Vector {
778
    // offset by content area
779
    point = point.add(vec(this.contentArea.left, this.contentArea.top));
1,447✔
780

781
    // the only difference between screen & world is the camera transform
782
    if (this._camera) {
1,447✔
783
      return this._camera.inverse.multiply(point);
1,440✔
784
    }
785
    // fallback to center screen camera
786
    return point.sub(vec(this.resolution.width / 2, this.resolution.height / 2));
7✔
787
  }
788

789
  /**
790
   * Takes a coordinate in Excalibur world space, and translates it to Excalibur screen space.
791
   *
792
   * Screen space is where {@apilink ScreenElement | `screen elements`} and {@apilink Entity | `entities`} with {@apilink CoordPlane.Screen} live.
793
   * @param point  World coordinate to convert
794
   */
795
  public worldToScreenCoordinates(point: Vector): Vector {
796
    if (this._camera) {
1,955✔
797
      return this._camera.transform.multiply(point);
42✔
798
    }
799

800
    // fallback to center screen camera
801
    return point.add(vec(this.resolution.width / 2, this.resolution.height / 2));
1,913✔
802
  }
803

804
  public pageToWorldCoordinates(point: Vector): Vector {
805
    const screen = this.pageToScreenCoordinates(point);
1✔
806
    return this.screenToWorldCoordinates(screen);
1✔
807
  }
808

809
  public worldToPageCoordinates(point: Vector): Vector {
810
    const screen = this.worldToScreenCoordinates(point);
1,941✔
811
    return this.screenToPageCoordinates(screen);
1,941✔
812
  }
813

814
  /**
815
   * Returns a BoundingBox of the top left corner of the screen
816
   * and the bottom right corner of the screen.
817
   *
818
   * World bounds are in world coordinates, useful for culling objects offscreen that are in world space
819
   */
820
  public getWorldBounds(): BoundingBox {
821
    const bounds = BoundingBox.fromDimension(this.resolution.width, this.resolution.height, Vector.Half)
965✔
822
      .scale(vec(1 / this._camera.zoom, 1 / this._camera.zoom))
823
      .rotate(this._camera.rotation)
824
      .translate(this._camera.drawPos);
825
    return bounds;
965✔
826
  }
827

828
  /**
829
   * Returns a BoundingBox of the top left corner of the screen and the bottom right corner of the screen.
830
   *
831
   * Screen bounds are in screen coordinates, useful for culling objects offscreen that are in screen space
832
   */
833
  public getScreenBounds(): BoundingBox {
834
    const bounds = BoundingBox.fromDimension(this.resolution.width, this.resolution.height, Vector.Zero, Vector.Zero);
×
835
    return bounds;
×
836
  }
837

838
  /**
839
   * The width of the game canvas in pixels (physical width component of the
840
   * resolution of the canvas element)
841
   */
842
  public get canvasWidth(): number {
843
    return this.canvas.width;
837✔
844
  }
845

846
  /**
847
   * Returns half width of the game canvas in pixels (half physical width component)
848
   */
849
  public get halfCanvasWidth(): number {
850
    return this.canvas.width / 2;
3✔
851
  }
852

853
  /**
854
   * The height of the game canvas in pixels, (physical height component of
855
   * the resolution of the canvas element)
856
   */
857
  public get canvasHeight(): number {
858
    return this.canvas.height;
837✔
859
  }
860

861
  /**
862
   * Returns half height of the game canvas in pixels (half physical height component)
863
   */
864
  public get halfCanvasHeight(): number {
865
    return this.canvas.height / 2;
3✔
866
  }
867

868
  /**
869
   * Returns the width of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
870
   */
871
  public get drawWidth(): number {
872
    if (this._camera) {
1,949✔
873
      return this.resolution.width / this._camera.zoom;
1,843✔
874
    }
875
    return this.resolution.width;
106✔
876
  }
877

878
  /**
879
   * Returns the width of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
880
   */
881
  public get width(): number {
882
    if (this._camera) {
×
883
      return this.resolution.width / this._camera.zoom;
×
884
    }
885
    return this.resolution.width;
×
886
  }
887

888
  /**
889
   * Returns half the width of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
890
   */
891
  public get halfDrawWidth(): number {
892
    return this.drawWidth / 2;
1,944✔
893
  }
894

895
  /**
896
   * Returns the height of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
897
   */
898
  public get drawHeight(): number {
899
    if (this._camera) {
1,949✔
900
      return this.resolution.height / this._camera.zoom;
1,843✔
901
    }
902
    return this.resolution.height;
106✔
903
  }
904

905
  public get height(): number {
906
    if (this._camera) {
×
907
      return this.resolution.height / this._camera.zoom;
×
908
    }
909
    return this.resolution.height;
×
910
  }
911

912
  /**
913
   * Returns half the height of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
914
   */
915
  public get halfDrawHeight(): number {
916
    return this.drawHeight / 2;
1,944✔
917
  }
918

919
  /**
920
   * Returns screen center coordinates including zoom and device pixel ratio.
921
   */
922
  public get center(): Vector {
923
    return vec(this.halfDrawWidth, this.halfDrawHeight);
16✔
924
  }
925

926
  /**
927
   * Returns the content area in screen space where it is safe to place content
928
   */
929
  public get contentArea(): BoundingBox {
930
    return this._contentArea;
6,603✔
931
  }
932

933
  /**
934
   * Returns the unsafe area in screen space, this is the full screen and some space may not be onscreen.
935
   */
936
  public get unsafeArea(): BoundingBox {
937
    return this._unsafeArea;
10✔
938
  }
939

940
  private _contentArea: BoundingBox = new BoundingBox();
811✔
941
  private _unsafeArea: BoundingBox = new BoundingBox();
811✔
942

943
  private _computeFit() {
944
    document.body.style.margin = '0px';
34✔
945
    document.body.style.overflow = 'hidden';
34✔
946
    const aspect = this.aspectRatio;
34✔
947
    let adjustedWidth = 0;
34✔
948
    let adjustedHeight = 0;
34✔
949
    if (window.innerWidth / aspect < window.innerHeight) {
34✔
950
      adjustedWidth = window.innerWidth;
15✔
951
      adjustedHeight = window.innerWidth / aspect;
15✔
952
    } else {
953
      adjustedWidth = window.innerHeight * aspect;
19✔
954
      adjustedHeight = window.innerHeight;
19✔
955
    }
956

957
    this.viewport = {
34✔
958
      width: adjustedWidth,
959
      height: adjustedHeight
960
    };
961
    this._contentArea = BoundingBox.fromDimension(this.resolution.width, this.resolution.height, Vector.Zero);
34✔
962
    this._unsafeArea = BoundingBox.fromDimension(this.resolution.width, this.resolution.height, Vector.Zero);
34✔
963
    this.events.emit('resize', {
34✔
964
      resolution: this.resolution,
965
      viewport: this.viewport
966
    } satisfies ScreenResizeEvent);
967
  }
968

969
  private _computeFitScreenAndFill() {
970
    document.body.style.margin = '0px';
20✔
971
    document.body.style.overflow = 'hidden';
20✔
972
    const vw = window.innerWidth;
20✔
973
    const vh = window.innerHeight;
20✔
974
    this._computeFitAndFill(vw, vh);
20✔
975
    this.events.emit('resize', {
20✔
976
      resolution: this.resolution,
977
      viewport: this.viewport
978
    } satisfies ScreenResizeEvent);
979
  }
980

981
  private _computeFitContainerAndFill() {
982
    this.canvas.style.width = '100%';
6✔
983
    this.canvas.style.height = '100%';
6✔
984

985
    this._computeFitAndFill(this.canvas.offsetWidth, this.canvas.offsetHeight, {
6✔
986
      width: 100,
987
      widthUnit: 'percent',
988
      height: 100,
989
      heightUnit: 'percent'
990
    });
991
    this.events.emit('resize', {
6✔
992
      resolution: this.resolution,
993
      viewport: this.viewport
994
    } satisfies ScreenResizeEvent);
995
  }
996

997
  private _computeFitAndFill(vw: number, vh: number, viewport?: ViewportDimension) {
998
    this.viewport = viewport ?? {
26✔
999
      width: vw,
1000
      height: vh
1001
    };
1002
    // if the current screen aspectRatio is less than the original aspectRatio
1003
    if (vw / vh <= this._contentResolution.width / this._contentResolution.height) {
26✔
1004
      // compute new resolution to match the original aspect ratio
1005
      this.resolution = {
8✔
1006
        width: (vw * this._contentResolution.width) / vw,
1007
        height: (((vw * this._contentResolution.width) / vw) * vh) / vw
1008
      };
1009
      const clip = (this.resolution.height - this._contentResolution.height) / 2;
8✔
1010
      this._contentArea = new BoundingBox({
8✔
1011
        top: clip,
1012
        left: 0,
1013
        right: this._contentResolution.width,
1014
        bottom: this.resolution.height - clip
1015
      });
1016
      this._unsafeArea = new BoundingBox({
8✔
1017
        top: -clip,
1018
        left: 0,
1019
        right: this._contentResolution.width,
1020
        bottom: this.resolution.height + clip
1021
      });
1022
    } else {
1023
      this.resolution = {
18✔
1024
        width: (((vh * this._contentResolution.height) / vh) * vw) / vh,
1025
        height: (vh * this._contentResolution.height) / vh
1026
      };
1027
      const clip = (this.resolution.width - this._contentResolution.width) / 2;
18✔
1028
      this._contentArea = new BoundingBox({
18✔
1029
        top: 0,
1030
        left: clip,
1031
        right: this.resolution.width - clip,
1032
        bottom: this._contentResolution.height
1033
      });
1034
      this._unsafeArea = new BoundingBox({
18✔
1035
        top: 0,
1036
        left: -clip,
1037
        right: this.resolution.width + clip,
1038
        bottom: this._contentResolution.height
1039
      });
1040
    }
1041
  }
1042

1043
  private _computeFitScreenAndZoom() {
1044
    document.body.style.margin = '0px';
12✔
1045
    document.body.style.overflow = 'hidden';
12✔
1046
    this.canvas.style.position = 'absolute';
12✔
1047

1048
    const vw = window.innerWidth;
12✔
1049
    const vh = window.innerHeight;
12✔
1050

1051
    this._computeFitAndZoom(vw, vh);
12✔
1052
    this.events.emit('resize', {
12✔
1053
      resolution: this.resolution,
1054
      viewport: this.viewport
1055
    } satisfies ScreenResizeEvent);
1056
  }
1057

1058
  private _computeFitContainerAndZoom() {
1059
    this.canvas.style.width = '100%';
6✔
1060
    this.canvas.style.height = '100%';
6✔
1061
    this.canvas.style.position = 'relative';
6✔
1062
    const parent = this.canvas.parentElement;
6✔
1063
    parent.style.overflow = 'hidden';
6✔
1064
    const { offsetWidth: vw, offsetHeight: vh } = this.canvas;
6✔
1065

1066
    this._computeFitAndZoom(vw, vh);
6✔
1067
    this.events.emit('resize', {
6✔
1068
      resolution: this.resolution,
1069
      viewport: this.viewport
1070
    } satisfies ScreenResizeEvent);
1071
  }
1072

1073
  private _computeFitAndZoom(vw: number, vh: number) {
1074
    const aspect = this.aspectRatio;
18✔
1075
    let adjustedWidth = 0;
18✔
1076
    let adjustedHeight = 0;
18✔
1077
    if (vw / aspect < vh) {
18✔
1078
      adjustedWidth = vw;
5✔
1079
      adjustedHeight = vw / aspect;
5✔
1080
    } else {
1081
      adjustedWidth = vh * aspect;
13✔
1082
      adjustedHeight = vh;
13✔
1083
    }
1084

1085
    const scaleX = vw / adjustedWidth;
18✔
1086
    const scaleY = vh / adjustedHeight;
18✔
1087

1088
    const maxScaleFactor = Math.max(scaleX, scaleY);
18✔
1089

1090
    const zoomedWidth = adjustedWidth * maxScaleFactor;
18✔
1091
    const zoomedHeight = adjustedHeight * maxScaleFactor;
18✔
1092

1093
    // Center zoomed dimension if bigger than the screen
1094
    if (zoomedWidth > vw) {
18✔
1095
      this.canvas.style.left = -(zoomedWidth - vw) / 2 + 'px';
5✔
1096
    } else {
1097
      this.canvas.style.left = '';
13✔
1098
    }
1099

1100
    if (zoomedHeight > vh) {
18✔
1101
      this.canvas.style.top = -(zoomedHeight - vh) / 2 + 'px';
11✔
1102
    } else {
1103
      this.canvas.style.top = '';
7✔
1104
    }
1105

1106
    this.viewport = {
18✔
1107
      width: zoomedWidth,
1108
      height: zoomedHeight
1109
    };
1110

1111
    const bounds = BoundingBox.fromDimension(this.viewport.width, this.viewport.height, Vector.Zero);
18✔
1112
    // return safe area
1113
    if (this.viewport.width > vw) {
18✔
1114
      const clip = ((this.viewport.width - vw) / this.viewport.width) * this.resolution.width;
5✔
1115
      bounds.top = 0;
5✔
1116
      bounds.left = clip / 2;
5✔
1117
      bounds.right = this.resolution.width - clip / 2;
5✔
1118
      bounds.bottom = this.resolution.height;
5✔
1119
    }
1120

1121
    if (this.viewport.height > vh) {
18✔
1122
      const clip = ((this.viewport.height - vh) / this.viewport.height) * this.resolution.height;
11✔
1123
      bounds.top = clip / 2;
11✔
1124
      bounds.left = 0;
11✔
1125
      bounds.bottom = this.resolution.height - clip / 2;
11✔
1126
      bounds.right = this.resolution.width;
11✔
1127
    }
1128
    this._contentArea = bounds;
18✔
1129
  }
1130

1131
  private _computeFitContainer() {
1132
    const aspect = this.aspectRatio;
6✔
1133
    let adjustedWidth = 0;
6✔
1134
    let adjustedHeight = 0;
6✔
1135
    let widthUnit: ViewportUnit = 'pixel';
6✔
1136
    let heightUnit: ViewportUnit = 'pixel';
6✔
1137
    const parent = this.canvas.parentElement;
6✔
1138
    if (parent.clientWidth / aspect < parent.clientHeight) {
6✔
1139
      this.canvas.style.width = '100%';
1✔
1140
      adjustedWidth = 100;
1✔
1141
      widthUnit = 'percent';
1✔
1142
      adjustedHeight = this.canvas.offsetWidth / aspect;
1✔
1143
    } else {
1144
      this.canvas.style.height = '100%';
5✔
1145
      adjustedHeight = 100;
5✔
1146
      heightUnit = 'percent';
5✔
1147
      adjustedWidth = this.canvas.offsetHeight * aspect;
5✔
1148
    }
1149

1150
    this.viewport = {
6✔
1151
      width: adjustedWidth,
1152
      widthUnit,
1153
      height: adjustedHeight,
1154
      heightUnit
1155
    };
1156
    this._contentArea = BoundingBox.fromDimension(this.resolution.width, this.resolution.height, Vector.Zero);
6✔
1157
    this.events.emit('resize', {
6✔
1158
      resolution: this.resolution,
1159
      viewport: this.viewport
1160
    } satisfies ScreenResizeEvent);
1161
  }
1162

1163
  private _applyDisplayMode() {
1164
    this._setResolutionAndViewportByDisplayMode(this.parent);
811✔
1165

1166
    // watch resizing
1167
    if (this.parent instanceof Window) {
811✔
1168
      this._browser.window.on('resize', this._resizeHandler);
805✔
1169
    } else {
1170
      this._resizeObserver = new ResizeObserver(() => {
6✔
1171
        this._resizeHandler();
6✔
1172
      });
1173
      this._resizeObserver.observe(this.parent);
6✔
1174
    }
1175
    this.parent.addEventListener('resize', this._resizeHandler);
811✔
1176
  }
1177

1178
  /**
1179
   * Sets the resolution and viewport based on the selected display mode.
1180
   */
1181
  private _setResolutionAndViewportByDisplayMode(parent: HTMLElement | Window) {
1182
    if (this.displayMode === DisplayMode.FillContainer) {
909!
1183
      this.canvas.style.width = '100%';
×
1184
      this.canvas.style.height = '100%';
×
1185
      this.viewport = {
×
1186
        width: 100,
1187
        widthUnit: 'percent',
1188
        height: 100,
1189
        heightUnit: 'percent'
1190
      };
1191
      this.resolution = {
×
1192
        width: this.canvas.offsetWidth,
1193
        height: this.canvas.offsetHeight
1194
      };
1195
    }
1196

1197
    if (this.displayMode === DisplayMode.FillScreen) {
909✔
1198
      document.body.style.margin = '0px';
3✔
1199
      document.body.style.overflow = 'hidden';
3✔
1200
      this.resolution = {
3✔
1201
        width: (<Window>parent).innerWidth,
1202
        height: (<Window>parent).innerHeight
1203
      };
1204

1205
      this.viewport = this.resolution;
3✔
1206
    }
1207

1208
    this._contentArea = BoundingBox.fromDimension(this.resolution.width, this.resolution.height, Vector.Zero);
909✔
1209

1210
    if (this.displayMode === DisplayMode.FitScreen) {
909✔
1211
      this._computeFit();
34✔
1212
    }
1213

1214
    if (this.displayMode === DisplayMode.FitContainer) {
909✔
1215
      this._computeFitContainer();
6✔
1216
    }
1217

1218
    if (this.displayMode === DisplayMode.FitScreenAndFill) {
909✔
1219
      this._computeFitScreenAndFill();
20✔
1220
    }
1221

1222
    if (this.displayMode === DisplayMode.FitContainerAndFill) {
909✔
1223
      this._computeFitContainerAndFill();
6✔
1224
    }
1225

1226
    if (this.displayMode === DisplayMode.FitScreenAndZoom) {
909✔
1227
      this._computeFitScreenAndZoom();
12✔
1228
    }
1229

1230
    if (this.displayMode === DisplayMode.FitContainerAndZoom) {
909✔
1231
      this._computeFitContainerAndZoom();
6✔
1232
    }
1233
  }
1234
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc