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

excaliburjs / Excalibur / 14840900305

05 May 2025 04:07PM UTC coverage: 87.864% (-1.5%) from 89.319%
14840900305

push

github

web-flow
chore: migrate tests to vitest (#3381)

managed to migrate the engine leak & memory reporters, although it took a bit of rigamarole. Mainly because, as opposed to karma, the reporter runs in node and not the browser. So I have to track the data needed separately in some global hooks _within_ the browser environment, which the reporter then reads and creates the logs if needed.

However, it seems the memory tracking is regularly reporting >1 mb, so I'm not sure if it's tracking properly or if things have changed with the new browsers. My only theory is that because the timing where memory is read is during an `afterEach` _before_ the test's `afterEach`, it's running before any potential cleanup. The timing of this is not something that's easy to control, unfortunately. I did try to prove this theory by doing the memory analysis on the next test's `beforeEach`, but it didnt seem to change the results, so it may not be an issue.

Implementation is done in `src/spec/vitest/__reporters__/memory.ts` and `src/spec/vitest/__reporters/memory.setup.ts`

---

I noticed CouroutineSpec was an offender for >10mb memory, which spins up additional engines. I added a short 100ms wait after the engine.dispose and the memory usage did drop, so it does seem like this is prone to scheduled garbage collecting.

---

I was able to run the garbage collector (if exposed, currently only on chrome) and this makes the reports more accurate

4998 of 6942 branches covered (72.0%)

13655 of 15541 relevant lines covered (87.86%)

25165.01 hits per line

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

88.76
/src/engine/Engine.ts
1
import { EX_VERSION } from './';
2
import { Future } from './Util/Future';
3
import type { EventKey, Handler, Subscription } from './EventEmitter';
4
import { EventEmitter } from './EventEmitter';
5
import { PointerScope } from './Input/PointerScope';
6
import { Flags } from './Flags';
7
import { polyfill } from './Polyfill';
8
polyfill();
117✔
9
import type { CanUpdate, CanDraw, CanInitialize } from './Interfaces/LifecycleEvents';
10
import type { Vector } from './Math/vector';
11
import type { ViewportDimension } from './Screen';
12
import { Screen, DisplayMode, Resolution } from './Screen';
13
import type { ScreenElement } from './ScreenElement';
14
import type { Actor } from './Actor';
15
import type { Timer } from './Timer';
16
import type { TileMap } from './TileMap';
17
import { DefaultLoader } from './Director/DefaultLoader';
18
import { Loader } from './Director/Loader';
19
import { Detector } from './Util/Detector';
20
import {
21
  VisibleEvent,
22
  HiddenEvent,
23
  GameStartEvent,
24
  GameStopEvent,
25
  PreUpdateEvent,
26
  PostUpdateEvent,
27
  PreFrameEvent,
28
  PostFrameEvent,
29
  PreDrawEvent,
30
  PostDrawEvent,
31
  InitializeEvent
32
} from './Events';
33
import { Logger, LogLevel } from './Util/Log';
34
import { Color } from './Color';
35
import type { SceneConstructor } from './Scene';
36
import { Scene, isSceneConstructor } from './Scene';
37
import { Entity } from './EntityComponentSystem/Entity';
38
import type { DebugStats } from './Debug/DebugConfig';
39
import { DebugConfig } from './Debug/DebugConfig';
40
import { BrowserEvents } from './Util/Browser';
41
import type { AntialiasOptions, ExcaliburGraphicsContext } from './Graphics';
42
import {
43
  DefaultAntialiasOptions,
44
  DefaultPixelArtOptions,
45
  ExcaliburGraphicsContext2DCanvas,
46
  ExcaliburGraphicsContextWebGL,
47
  TextureLoader
48
} from './Graphics';
49
import type { Clock } from './Util/Clock';
50
import { StandardClock } from './Util/Clock';
51
import { ImageFiltering } from './Graphics/Filtering';
52
import { GraphicsDiagnostics } from './Graphics/GraphicsDiagnostics';
53
import { Toaster } from './Util/Toaster';
54
import type { InputMapper } from './Input/InputMapper';
55
import type { GoToOptions, SceneMap, StartOptions, SceneWithOptions, WithRoot } from './Director/Director';
56
import { Director, DirectorEvents } from './Director/Director';
57
import { InputHost } from './Input/InputHost';
58
import type { PhysicsConfig } from './Collision/PhysicsConfig';
59
import { getDefaultPhysicsConfig } from './Collision/PhysicsConfig';
60
import type { DeepRequired } from './Util/Required';
61
import type { Context } from './Context';
62
import { createContext, useContext } from './Context';
63
import type { GarbageCollectionOptions } from './GarbageCollector';
64
import { DefaultGarbageCollectionOptions, GarbageCollector } from './GarbageCollector';
65
import { mergeDeep } from './Util/Util';
66

67
export type EngineEvents = DirectorEvents & {
68
  fallbackgraphicscontext: ExcaliburGraphicsContext2DCanvas;
69
  initialize: InitializeEvent<Engine>;
70
  visible: VisibleEvent;
71
  hidden: HiddenEvent;
72
  start: GameStartEvent;
73
  stop: GameStopEvent;
74
  preupdate: PreUpdateEvent<Engine>;
75
  postupdate: PostUpdateEvent<Engine>;
76
  preframe: PreFrameEvent;
77
  postframe: PostFrameEvent;
78
  predraw: PreDrawEvent;
79
  postdraw: PostDrawEvent;
80
};
81

82
export const EngineEvents = {
117✔
83
  FallbackGraphicsContext: 'fallbackgraphicscontext',
84
  Initialize: 'initialize',
85
  Visible: 'visible',
86
  Hidden: 'hidden',
87
  Start: 'start',
88
  Stop: 'stop',
89
  PreUpdate: 'preupdate',
90
  PostUpdate: 'postupdate',
91
  PreFrame: 'preframe',
92
  PostFrame: 'postframe',
93
  PreDraw: 'predraw',
94
  PostDraw: 'postdraw',
95
  ...DirectorEvents
96
} as const;
97

98
/**
99
 * Enum representing the different mousewheel event bubble prevention
100
 */
101
export enum ScrollPreventionMode {
117✔
102
  /**
103
   * Do not prevent any page scrolling
104
   */
105
  None,
117✔
106
  /**
107
   * Prevent page scroll if mouse is over the game canvas
108
   */
109
  Canvas,
117✔
110
  /**
111
   * Prevent all page scrolling via mouse wheel
112
   */
113
  All
117✔
114
}
115

116
/**
117
 * Defines the available options to configure the Excalibur engine at constructor time.
118
 */
119
export interface EngineOptions<TKnownScenes extends string = any> {
120
  /**
121
   * Optionally configure the width of the viewport in css pixels
122
   */
123
  width?: number;
124

125
  /**
126
   * Optionally configure the height of the viewport in css pixels
127
   */
128
  height?: number;
129

130
  /**
131
   * Optionally configure the width & height of the viewport in css pixels.
132
   * Use `viewport` instead of {@apilink EngineOptions.width} and {@apilink EngineOptions.height}, or vice versa.
133
   */
134
  viewport?: ViewportDimension;
135

136
  /**
137
   * Optionally specify the size the logical pixel resolution, if not specified it will be width x height.
138
   * See {@apilink Resolution} for common presets.
139
   */
140
  resolution?: Resolution;
141

142
  /**
143
   * Optionally specify antialiasing (smoothing), by default true (smooth pixels)
144
   *
145
   *  * `true` - useful for high resolution art work you would like smoothed, this also hints excalibur to load images
146
   * with default blending {@apilink ImageFiltering.Blended}
147
   *
148
   *  * `false` - useful for pixel art style art work you would like sharp, this also hints excalibur to load images
149
   * with default blending {@apilink ImageFiltering.Pixel}
150
   *
151
   * * {@apilink AntialiasOptions} Optionally deeply configure the different antialiasing settings, **WARNING** thar be dragons here.
152
   * It is recommended you stick to `true` or `false` unless you understand what you're doing and need to control rendering to
153
   * a high degree.
154
   */
155
  antialiasing?: boolean | AntialiasOptions;
156

157
  /**
158
   * Optionally specify excalibur garbage collection, by default true.
159
   *
160
   * * `true` - garbage collection defaults are enabled (default)
161
   *
162
   * * `false` - garbage collection is completely disabled (not recommended)
163
   *
164
   * * {@apilink GarbageCollectionOptions} Optionally deeply configure garbage collection settings, **WARNING** thar be dragons here.
165
   * It is recommended you stick to `true` or `false` unless you understand what you're doing, it is possible to get into a downward
166
   * spiral if collection timings are set too low where you are stuck in repeated collection.
167
   */
168
  garbageCollection?: boolean | GarbageCollectionOptions;
169

170
  /**
171
   * Quick convenience property to configure Excalibur to use special settings for "pretty" anti-aliased pixel art
172
   *
173
   * 1. Turns on special shader condition to blend for pixel art and enables various antialiasing settings,
174
   *  notice blending is ON for this special mode.
175
   *
176
   * Equivalent to:
177
   * ```javascript
178
   * antialiasing: {
179
   *  pixelArtSampler: true,
180
   *  canvasImageRendering: 'auto',
181
   *  filtering: ImageFiltering.Blended,
182
   *  webglAntialiasing: true
183
   * }
184
   * ```
185
   */
186
  pixelArt?: boolean;
187

188
  /**
189
   * Specify any UV padding you want use in pixels, this brings sampling into the texture if you're using
190
   * a sprite sheet in one image to prevent sampling bleed.
191
   *
192
   * Defaults:
193
   * * `antialiasing: false` or `filtering: ImageFiltering.Pixel` - 0.0;
194
   * * `pixelArt: true` - 0.25
195
   * * All else 0.01
196
   */
197
  uvPadding?: number;
198

199
  /**
200
   * Optionally hint the graphics context into a specific power profile
201
   *
202
   * Default "high-performance"
203
   */
204
  powerPreference?: 'default' | 'high-performance' | 'low-power';
205

206
  /**
207
   * Optionally upscale the number of pixels in the canvas. Normally only useful if you need a smoother look to your assets, especially
208
   * {@apilink Text} or Pixel Art assets.
209
   *
210
   * **WARNING** It is recommended you try using `antialiasing: true` before adjusting pixel ratio. Pixel ratio will consume more memory
211
   * and on mobile may break if the internal size of the canvas exceeds 4k pixels in width or height.
212
   *
213
   * Default is based the display's pixel ratio, for example a HiDPI screen might have the value 2;
214
   */
215
  pixelRatio?: number;
216

217
  /**
218
   * Optionally configure the native canvas transparent backdrop
219
   */
220
  enableCanvasTransparency?: boolean;
221

222
  /**
223
   * Optionally specify the target canvas DOM element to render the game in
224
   */
225
  canvasElementId?: string;
226

227
  /**
228
   * Optionally specify the target canvas DOM element directly
229
   */
230
  canvasElement?: HTMLCanvasElement;
231

232
  /**
233
   * Optionally enable the right click context menu on the canvas
234
   *
235
   * Default if unset is false
236
   */
237
  enableCanvasContextMenu?: boolean;
238

239
  /**
240
   * Optionally snap graphics to nearest pixel, default is false
241
   */
242
  snapToPixel?: boolean;
243

244
  /**
245
   * The {@apilink DisplayMode} of the game, by default {@apilink DisplayMode.FitScreen} with aspect ratio 4:3 (800x600).
246
   * Depending on this value, {@apilink width} and {@apilink height} may be ignored.
247
   */
248
  displayMode?: DisplayMode;
249

250
  /**
251
   * Configures the pointer scope. Pointers scoped to the 'Canvas' can only fire events within the canvas viewport; whereas, 'Document'
252
   * (default) scoped will fire anywhere on the page.
253
   */
254
  pointerScope?: PointerScope;
255

256
  /**
257
   * Suppress boot up console message, which contains the "powered by Excalibur message"
258
   */
259
  suppressConsoleBootMessage?: boolean;
260

261
  /**
262
   * Suppress minimum browser feature detection, it is not recommended users of excalibur switch this off. This feature ensures that
263
   * the currently running browser meets the minimum requirements for running excalibur. This can be useful if running on non-standard
264
   * browsers or if there is a bug in excalibur preventing execution.
265
   */
266
  suppressMinimumBrowserFeatureDetection?: boolean;
267

268
  /**
269
   * Suppress HiDPI auto detection and scaling, it is not recommended users of excalibur switch off this feature. This feature detects
270
   * and scales the drawing canvas appropriately to accommodate HiDPI screens.
271
   */
272
  suppressHiDPIScaling?: boolean;
273

274
  /**
275
   * Suppress play button, it is not recommended users of excalibur switch this feature. Some browsers require a user gesture (like a click)
276
   * for certain browser features to work like web audio.
277
   */
278
  suppressPlayButton?: boolean;
279

280
  /**
281
   * Sets the focus of the window, this is needed when hosting excalibur in a cross-origin iframe in order for certain events
282
   * (like keyboard) to work.
283
   * For example: itch.io or codesandbox.io
284
   *
285
   * By default set to true,
286
   */
287
  grabWindowFocus?: boolean;
288

289
  /**
290
   * Scroll prevention method.
291
   */
292
  scrollPreventionMode?: ScrollPreventionMode;
293

294
  /**
295
   * Optionally set the background color
296
   */
297
  backgroundColor?: Color;
298

299
  /**
300
   * Optionally set the maximum fps if not set Excalibur will go as fast as the device allows.
301
   *
302
   * You may want to constrain max fps if your game cannot maintain fps consistently, it can look and feel better to have a 30fps game than
303
   * one that bounces between 30fps and 60fps
304
   */
305
  maxFps?: number;
306

307
  /**
308
   * Optionally configure a fixed update timestep in milliseconds, this can be desirable if you need the physics simulation to be very stable. When
309
   * set the update step and physics will use the same elapsed time for each tick even if the graphical framerate drops. In order for the
310
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
311
   * there could be X updates and 1 draw each clock step.
312
   *
313
   * **NOTE:** This does come at a potential perf cost because each catch-up update will need to be run if the fixed rate is greater than
314
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
315
   *
316
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
317
   *
318
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
319
   */
320
  fixedUpdateTimestep?: number;
321

322
  /**
323
   * Optionally configure a fixed update fps, this can be desirable if you need the physics simulation to be very stable. When set
324
   * the update step and physics will use the same elapsed time for each tick even if the graphical framerate drops. In order for the
325
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
326
   * there could be X updates and 1 draw each clock step.
327
   *
328
   * **NOTE:** This does come at a potential perf cost because each catch-up update will need to be run if the fixed rate is greater than
329
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
330
   *
331
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
332
   *
333
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
334
   */
335
  fixedUpdateFps?: number;
336

337
  /**
338
   * Default `true`, optionally configure excalibur to use optimal draw call sorting, to opt out set this to `false`.
339
   *
340
   * Excalibur will automatically sort draw calls by z and priority into renderer batches for maximal draw performance,
341
   * this can disrupt a specific desired painter order.
342
   *
343
   */
344
  useDrawSorting?: boolean;
345

346
  /**
347
   * Optionally provide a custom handler for the webgl context lost event
348
   */
349
  handleContextLost?: (e: Event) => void;
350

351
  /**
352
   * Optionally provide a custom handler for the webgl context restored event
353
   */
354
  handleContextRestored?: (e: Event) => void;
355

356
  /**
357
   * Optionally configure how excalibur handles poor performance on a player's browser
358
   */
359
  configurePerformanceCanvas2DFallback?: {
360
    /**
361
     * By default `false`, this will switch the internal graphics context to Canvas2D which can improve performance on non hardware
362
     * accelerated browsers.
363
     */
364
    allow: boolean;
365
    /**
366
     * By default `false`, if set to `true` a dialogue will be presented to the player about their browser and how to potentially
367
     * address any issues.
368
     */
369
    showPlayerMessage?: boolean;
370
    /**
371
     * Default `{ numberOfFrames: 100, fps: 20 }`, optionally configure excalibur to fallback to the 2D Canvas renderer
372
     * if bad performance is detected.
373
     *
374
     * In this example of the default if excalibur is running at 20fps or less for 100 frames it will trigger the fallback to the 2D
375
     * Canvas renderer.
376
     */
377
    threshold?: { numberOfFrames: number; fps: number };
378
  };
379

380
  /**
381
   * Optionally configure the physics simulation in excalibur
382
   *
383
   * If false, Excalibur will not produce a physics simulation.
384
   *
385
   * Default is configured to use {@apilink SolverStrategy.Arcade} physics simulation
386
   */
387
  physics?: boolean | PhysicsConfig;
388

389
  /**
390
   * Optionally specify scenes with their transitions and loaders to excalibur's scene {@apilink Director}
391
   *
392
   * Scene transitions can can overridden dynamically by the `Scene` or by the call to `.goToScene`
393
   */
394
  scenes?: SceneMap<TKnownScenes>;
395
}
396

397
/**
398
 * The Excalibur Engine
399
 *
400
 * The {@apilink Engine} is the main driver for a game. It is responsible for
401
 * starting/stopping the game, maintaining state, transmitting events,
402
 * loading resources, and managing the scene.
403
 */
404
export class Engine<TKnownScenes extends string = any> implements CanInitialize, CanUpdate, CanDraw {
117✔
405
  static Context: Context<Engine | null> = createContext<Engine | null>();
406
  static useEngine(): Engine {
407
    const value = useContext(Engine.Context);
20✔
408

409
    if (!value) {
20✔
410
      throw new Error('Cannot inject engine with `useEngine()`, `useEngine()` was called outside of Engine lifecycle scope.');
1✔
411
    }
412

413
    return value;
19✔
414
  }
415
  static InstanceCount = 0;
416

417
  /**
418
   * Anything run under scope can use `useEngine()` to inject the current engine
419
   * @param cb
420
   */
421
  scope = <TReturn>(cb: () => TReturn) => Engine.Context.scope(this, cb);
3,725✔
422

423
  private _garbageCollector: GarbageCollector;
424

425
  public readonly garbageCollectorConfig: GarbageCollectionOptions | null;
426

427
  /**
428
   * Current Excalibur version string
429
   *
430
   * Useful for plugins or other tools that need to know what features are available
431
   */
432
  public readonly version = EX_VERSION;
727✔
433

434
  /**
435
   * Listen to and emit events on the Engine
436
   */
437
  public events = new EventEmitter<EngineEvents>();
727✔
438

439
  /**
440
   * Excalibur browser events abstraction used for wiring to native browser events safely
441
   */
442
  public browser: BrowserEvents;
443

444
  /**
445
   * Screen abstraction
446
   */
447
  public screen: Screen;
448

449
  /**
450
   * Scene director, manages all scenes, scene transitions, and loaders in excalibur
451
   */
452
  public director: Director<TKnownScenes>;
453

454
  /**
455
   * Direct access to the engine's canvas element
456
   */
457
  public canvas: HTMLCanvasElement;
458

459
  /**
460
   * Direct access to the ExcaliburGraphicsContext used for drawing things to the screen
461
   */
462
  public graphicsContext: ExcaliburGraphicsContext;
463

464
  /**
465
   * Direct access to the canvas element ID, if an ID exists
466
   */
467
  public canvasElementId: string;
468

469
  /**
470
   * Direct access to the physics configuration for excalibur
471
   */
472
  public physics: DeepRequired<PhysicsConfig>;
473

474
  /**
475
   * Optionally set the maximum fps if not set Excalibur will go as fast as the device allows.
476
   *
477
   * You may want to constrain max fps if your game cannot maintain fps consistently, it can look and feel better to have a 30fps game than
478
   * one that bounces between 30fps and 60fps
479
   */
480
  public maxFps: number = Number.POSITIVE_INFINITY;
727✔
481

482
  /**
483
   * Optionally configure a fixed update fps, this can be desirable if you need the physics simulation to be very stable. When set
484
   * the update step and physics will use the same elapsed time for each tick even if the graphical framerate drops. In order for the
485
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
486
   * there could be X updates and 1 draw each clock step.
487
   *
488
   * **NOTE:** This does come at a potential perf cost because each catch-up update will need to be run if the fixed rate is greater than
489
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
490
   *
491
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
492
   *
493
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
494
   */
495
  public readonly fixedUpdateFps?: number;
496

497
  /**
498
   * Optionally configure a fixed update timestep in milliseconds, this can be desirable if you need the physics simulation to be very stable. When
499
   * set the update step and physics will use the same elapsed time for each tick even if the graphical framerate drops. In order for the
500
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
501
   * there could be X updates and 1 draw each clock step.
502
   *
503
   * **NOTE:** This does come at a potential perf cost because each catch-up update will need to be run if the fixed rate is greater than
504
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
505
   *
506
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
507
   *
508
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
509
   */
510
  public readonly fixedUpdateTimestep?: number;
511

512
  /**
513
   * Direct access to the excalibur clock
514
   */
515
  public clock: Clock;
516

517
  public readonly pointerScope: PointerScope;
518
  public readonly grabWindowFocus: boolean;
519

520
  /**
521
   * The width of the game canvas in pixels (physical width component of the
522
   * resolution of the canvas element)
523
   */
524
  public get canvasWidth(): number {
525
    return this.screen.canvasWidth;
837✔
526
  }
527

528
  /**
529
   * Returns half width of the game canvas in pixels (half physical width component)
530
   */
531
  public get halfCanvasWidth(): number {
532
    return this.screen.halfCanvasWidth;
3✔
533
  }
534

535
  /**
536
   * The height of the game canvas in pixels, (physical height component of
537
   * the resolution of the canvas element)
538
   */
539
  public get canvasHeight(): number {
540
    return this.screen.canvasHeight;
837✔
541
  }
542

543
  /**
544
   * Returns half height of the game canvas in pixels (half physical height component)
545
   */
546
  public get halfCanvasHeight(): number {
547
    return this.screen.halfCanvasHeight;
3✔
548
  }
549

550
  /**
551
   * Returns the width of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
552
   */
553
  public get drawWidth(): number {
554
    return this.screen.drawWidth;
5✔
555
  }
556

557
  /**
558
   * Returns half the width of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
559
   */
560
  public get halfDrawWidth(): number {
561
    return this.screen.halfDrawWidth;
1,878✔
562
  }
563

564
  /**
565
   * Returns the height of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
566
   */
567
  public get drawHeight(): number {
568
    return this.screen.drawHeight;
5✔
569
  }
570

571
  /**
572
   * Returns half the height of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
573
   */
574
  public get halfDrawHeight(): number {
575
    return this.screen.halfDrawHeight;
1,878✔
576
  }
577

578
  /**
579
   * Returns whether excalibur detects the current screen to be HiDPI
580
   */
581
  public get isHiDpi(): boolean {
582
    return this.screen.isHiDpi;
3✔
583
  }
584

585
  /**
586
   * Access engine input like pointer, keyboard, or gamepad
587
   */
588
  public input: InputHost;
589

590
  /**
591
   * Map multiple input sources to specific game actions actions
592
   */
593
  public inputMapper: InputMapper;
594

595
  private _inputEnabled: boolean = true;
727✔
596

597
  /**
598
   * Access Excalibur debugging functionality.
599
   *
600
   * Useful when you want to debug different aspects of built in engine features like
601
   *   * Transform
602
   *   * Graphics
603
   *   * Colliders
604
   */
605
  public debug: DebugConfig;
606

607
  /**
608
   * Access {@apilink stats} that holds frame statistics.
609
   */
610
  public get stats(): DebugStats {
611
    return this.debug.stats;
24,839✔
612
  }
613

614
  /**
615
   * The current {@apilink Scene} being drawn and updated on screen
616
   */
617
  public get currentScene(): Scene {
618
    return this.director.currentScene;
7,753✔
619
  }
620

621
  /**
622
   * The current {@apilink Scene} being drawn and updated on screen
623
   */
624
  public get currentSceneName(): string {
625
    return this.director.currentSceneName;
56✔
626
  }
627

628
  /**
629
   * The default {@apilink Scene} of the game, use {@apilink Engine.goToScene} to transition to different scenes.
630
   */
631
  public get rootScene(): Scene {
632
    return this.director.rootScene;
1✔
633
  }
634

635
  /**
636
   * Contains all the scenes currently registered with Excalibur
637
   */
638
  public get scenes(): { [key: string]: Scene | SceneConstructor | SceneWithOptions } {
639
    return this.director.scenes;
4✔
640
  }
641

642
  /**
643
   * Indicates whether the engine is set to fullscreen or not
644
   */
645
  public get isFullscreen(): boolean {
646
    return this.screen.isFullScreen;
1✔
647
  }
648

649
  /**
650
   * Indicates the current {@apilink DisplayMode} of the engine.
651
   */
652
  public get displayMode(): DisplayMode {
653
    return this.screen.displayMode;
×
654
  }
655

656
  private _suppressPlayButton: boolean = false;
727✔
657
  /**
658
   * Returns the calculated pixel ration for use in rendering
659
   */
660
  public get pixelRatio(): number {
661
    return this.screen.pixelRatio;
1,673✔
662
  }
663

664
  /**
665
   * Indicates whether audio should be paused when the game is no longer visible.
666
   */
667
  public pauseAudioWhenHidden: boolean = true;
727✔
668

669
  /**
670
   * Indicates whether the engine should draw with debug information
671
   */
672
  private _isDebug: boolean = false;
727✔
673
  public get isDebug(): boolean {
674
    return this._isDebug;
3,878✔
675
  }
676

677
  /**
678
   * Sets the background color for the engine.
679
   */
680
  public backgroundColor: Color;
681

682
  /**
683
   * Sets the Transparency for the engine.
684
   */
685
  public enableCanvasTransparency: boolean = true;
727✔
686

687
  /**
688
   * Hints the graphics context to truncate fractional world space coordinates
689
   */
690
  public get snapToPixel(): boolean {
691
    return this.graphicsContext.snapToPixel;
2✔
692
  }
693

694
  public set snapToPixel(shouldSnapToPixel: boolean) {
695
    this.graphicsContext.snapToPixel = shouldSnapToPixel;
1✔
696
  }
697

698
  /**
699
   * The action to take when a fatal exception is thrown
700
   */
701
  public onFatalException = (e: any) => {
727✔
702
    Logger.getInstance().fatal(e, e.stack);
×
703
  };
704

705
  /**
706
   * The mouse wheel scroll prevention mode
707
   */
708
  public pageScrollPreventionMode: ScrollPreventionMode;
709

710
  private _logger: Logger;
711

712
  private _toaster: Toaster = new Toaster();
727✔
713

714
  // this determines whether excalibur is compatible with your browser
715
  private _compatible: boolean;
716

717
  private _timescale: number = 1.0;
727✔
718

719
  // loading
720
  private _loader: DefaultLoader;
721

722
  private _isInitialized: boolean = false;
727✔
723

724
  public emit<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, event: EngineEvents[TEventName]): void;
725
  public emit(eventName: string, event?: any): void;
726
  public emit<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, event?: any): void {
727
    this.events.emit(eventName, event);
9,106✔
728
  }
729

730
  public on<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): Subscription;
731
  public on(eventName: string, handler: Handler<unknown>): Subscription;
732
  public on<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
733
    return this.events.on(eventName, handler);
29✔
734
  }
735

736
  public once<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): Subscription;
737
  public once(eventName: string, handler: Handler<unknown>): Subscription;
738
  public once<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
739
    return this.events.once(eventName, handler);
10✔
740
  }
741

742
  public off<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): void;
743
  public off(eventName: string, handler: Handler<unknown>): void;
744
  public off(eventName: string): void;
745
  public off<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler?: Handler<any>): void {
746
    this.events.off(eventName, handler);
×
747
  }
748

749
  /**
750
   * Default {@apilink EngineOptions}
751
   */
752
  private static _DEFAULT_ENGINE_OPTIONS: EngineOptions = {
753
    width: 0,
754
    height: 0,
755
    enableCanvasTransparency: true,
756
    useDrawSorting: true,
757
    configurePerformanceCanvas2DFallback: {
758
      allow: false,
759
      showPlayerMessage: false,
760
      threshold: { fps: 20, numberOfFrames: 100 }
761
    },
762
    canvasElementId: '',
763
    canvasElement: undefined,
764
    enableCanvasContextMenu: false,
765
    snapToPixel: false,
766
    antialiasing: true,
767
    pixelArt: false,
768
    garbageCollection: true,
769
    powerPreference: 'high-performance',
770
    pointerScope: PointerScope.Canvas,
771
    suppressConsoleBootMessage: null,
772
    suppressMinimumBrowserFeatureDetection: null,
773
    suppressHiDPIScaling: null,
774
    suppressPlayButton: null,
775
    grabWindowFocus: true,
776
    scrollPreventionMode: ScrollPreventionMode.Canvas,
777
    backgroundColor: Color.fromHex('#2185d0') // Excalibur blue
778
  };
779

780
  private _originalOptions: EngineOptions = {};
727✔
781
  public readonly _originalDisplayMode: DisplayMode;
782

783
  /**
784
   * Creates a new game using the given {@apilink EngineOptions}. By default, if no options are provided,
785
   * the game will be rendered full screen (taking up all available browser window space).
786
   * You can customize the game rendering through {@apilink EngineOptions}.
787
   *
788
   * Example:
789
   *
790
   * ```js
791
   * var game = new ex.Engine({
792
   *   width: 0, // the width of the canvas
793
   *   height: 0, // the height of the canvas
794
   *   enableCanvasTransparency: true, // the transparencySection of the canvas
795
   *   canvasElementId: '', // the DOM canvas element ID, if you are providing your own
796
   *   displayMode: ex.DisplayMode.FullScreen, // the display mode
797
   *   pointerScope: ex.PointerScope.Document, // the scope of capturing pointer (mouse/touch) events
798
   *   backgroundColor: ex.Color.fromHex('#2185d0') // background color of the engine
799
   * });
800
   *
801
   * // call game.start, which is a Promise
802
   * game.start().then(function () {
803
   *   // ready, set, go!
804
   * });
805
   * ```
806
   */
807
  constructor(options?: EngineOptions<TKnownScenes>) {
808
    options = { ...Engine._DEFAULT_ENGINE_OPTIONS, ...options };
727✔
809
    this._originalOptions = options;
727✔
810

811
    Flags.freeze();
727✔
812

813
    // Initialize browser events facade
814
    this.browser = new BrowserEvents(window, document);
727✔
815

816
    // Check compatibility
817
    const detector = new Detector();
727✔
818
    if (!options.suppressMinimumBrowserFeatureDetection && !(this._compatible = detector.test())) {
727!
819
      const message = document.createElement('div');
×
820
      message.innerText = 'Sorry, your browser does not support all the features needed for Excalibur';
×
821
      document.body.appendChild(message);
×
822

823
      detector.failedTests.forEach(function (test) {
×
824
        const testMessage = document.createElement('div');
×
825
        testMessage.innerText = 'Browser feature missing ' + test;
×
826
        document.body.appendChild(testMessage);
×
827
      });
828

829
      if (options.canvasElementId) {
×
830
        const canvas = document.getElementById(options.canvasElementId);
×
831
        if (canvas) {
×
832
          canvas.parentElement.removeChild(canvas);
×
833
        }
834
      }
835

836
      return;
×
837
    } else {
838
      this._compatible = true;
727✔
839
    }
840

841
    // Use native console API for color fun
842
    // eslint-disable-next-line no-console
843
    if (console.log && !options.suppressConsoleBootMessage) {
727✔
844
      // eslint-disable-next-line no-console
845
      console.log(
3✔
846
        `%cPowered by Excalibur.js (v${EX_VERSION})`,
847
        'background: #176BAA; color: white; border-radius: 5px; padding: 15px; font-size: 1.5em; line-height: 80px;'
848
      );
849
      // eslint-disable-next-line no-console
850
      console.log(
3✔
851
        '\n\
852
      /| ________________\n\
853
O|===|* >________________>\n\
854
      \\|'
855
      );
856
      // eslint-disable-next-line no-console
857
      console.log('Visit', 'http://excaliburjs.com', 'for more information');
3✔
858
    }
859

860
    // Suppress play button
861
    if (options.suppressPlayButton) {
727✔
862
      this._suppressPlayButton = true;
715✔
863
    }
864

865
    this._logger = Logger.getInstance();
727✔
866

867
    // If debug is enabled, let's log browser features to the console.
868
    if (this._logger.defaultLevel === LogLevel.Debug) {
727!
869
      detector.logBrowserFeatures();
×
870
    }
871

872
    this._logger.debug('Building engine...');
727✔
873
    if (options.garbageCollection === true) {
727!
874
      this.garbageCollectorConfig = {
727✔
875
        ...DefaultGarbageCollectionOptions
876
      };
877
    } else if (options.garbageCollection === false) {
×
878
      this._logger.warn(
×
879
        'WebGL Garbage Collection Disabled!!! If you leak any images over time your game will crash when GPU memory is exhausted'
880
      );
881
      this.garbageCollectorConfig = null;
×
882
    } else {
883
      this.garbageCollectorConfig = {
×
884
        ...DefaultGarbageCollectionOptions,
885
        ...options.garbageCollection
886
      };
887
    }
888
    this._garbageCollector = new GarbageCollector({ getTimestamp: Date.now });
727✔
889

890
    this.canvasElementId = options.canvasElementId;
727✔
891

892
    if (options.canvasElementId) {
727!
893
      this._logger.debug('Using Canvas element specified: ' + options.canvasElementId);
×
894

895
      //test for existence of element
896
      if (document.getElementById(options.canvasElementId) === null) {
×
897
        throw new Error('Cannot find existing element in the DOM, please ensure element is created prior to engine creation.');
×
898
      }
899

900
      this.canvas = <HTMLCanvasElement>document.getElementById(options.canvasElementId);
×
901
    } else if (options.canvasElement) {
727!
902
      this._logger.debug('Using Canvas element specified:', options.canvasElement);
×
903
      this.canvas = options.canvasElement;
×
904
    } else {
905
      this._logger.debug('Using generated canvas element');
727✔
906
      this.canvas = <HTMLCanvasElement>document.createElement('canvas');
727✔
907
    }
908

909
    if (this.canvas && !options.enableCanvasContextMenu) {
727✔
910
      this.canvas.addEventListener('contextmenu', (evt) => {
726✔
911
        evt.preventDefault();
1✔
912
      });
913
    }
914

915
    let displayMode = options.displayMode ?? DisplayMode.Fixed;
727✔
916
    if ((options.width && options.height) || options.viewport) {
727✔
917
      if (options.displayMode === undefined) {
722✔
918
        displayMode = DisplayMode.Fixed;
6✔
919
      }
920
      this._logger.debug('Engine viewport is size ' + options.width + ' x ' + options.height);
722✔
921
    } else if (!options.displayMode) {
5✔
922
      this._logger.debug('Engine viewport is fit');
2✔
923
      displayMode = DisplayMode.FitScreen;
2✔
924
    }
925

926
    this.grabWindowFocus = options.grabWindowFocus;
727✔
927
    this.pointerScope = options.pointerScope;
727✔
928

929
    this._originalDisplayMode = displayMode;
727✔
930

931
    let pixelArtSampler: boolean;
932
    let uvPadding: number;
933
    let nativeContextAntialiasing: boolean;
934
    let canvasImageRendering: 'pixelated' | 'auto';
935
    let filtering: ImageFiltering;
936
    let multiSampleAntialiasing: boolean | { samples: number };
937
    if (typeof options.antialiasing === 'object') {
727!
938
      ({ pixelArtSampler, nativeContextAntialiasing, multiSampleAntialiasing, filtering, canvasImageRendering } = {
×
939
        ...(options.pixelArt ? DefaultPixelArtOptions : DefaultAntialiasOptions),
×
940
        ...options.antialiasing
941
      });
942
    } else {
943
      pixelArtSampler = !!options.pixelArt;
727✔
944
      nativeContextAntialiasing = false;
727✔
945
      multiSampleAntialiasing = options.antialiasing;
727✔
946
      canvasImageRendering = options.antialiasing ? 'auto' : 'pixelated';
727✔
947
      filtering = options.antialiasing ? ImageFiltering.Blended : ImageFiltering.Pixel;
727✔
948
    }
949

950
    if (nativeContextAntialiasing && multiSampleAntialiasing) {
727!
951
      this._logger.warnOnce(
×
952
        `Cannot use antialias setting nativeContextAntialiasing and multiSampleAntialiasing` +
953
          ` at the same time, they are incompatible settings. If you aren\'t sure use multiSampleAntialiasing`
954
      );
955
    }
956

957
    if (options.pixelArt) {
727✔
958
      uvPadding = 0.25;
1✔
959
    }
960

961
    if (!options.antialiasing || filtering === ImageFiltering.Pixel) {
727✔
962
      uvPadding = 0;
718✔
963
    }
964

965
    // Override with any user option, if non default to .25 for pixel art, 0.01 for everything else
966
    uvPadding = options.uvPadding ?? uvPadding ?? 0.01;
727!
967

968
    // Canvas 2D fallback can be flagged on
969
    let useCanvasGraphicsContext = Flags.isEnabled('use-canvas-context');
727✔
970
    if (!useCanvasGraphicsContext) {
727✔
971
      // Attempt webgl first
972
      try {
725✔
973
        this.graphicsContext = new ExcaliburGraphicsContextWebGL({
725✔
974
          canvasElement: this.canvas,
975
          enableTransparency: this.enableCanvasTransparency,
976
          pixelArtSampler: pixelArtSampler,
977
          antialiasing: nativeContextAntialiasing,
978
          multiSampleAntialiasing: multiSampleAntialiasing,
979
          uvPadding: uvPadding,
980
          powerPreference: options.powerPreference,
981
          backgroundColor: options.backgroundColor,
982
          snapToPixel: options.snapToPixel,
983
          useDrawSorting: options.useDrawSorting,
984
          garbageCollector: this.garbageCollectorConfig
725!
985
            ? {
986
                garbageCollector: this._garbageCollector,
987
                collectionInterval: this.garbageCollectorConfig.textureCollectInterval
988
              }
989
            : null,
990
          handleContextLost: options.handleContextLost ?? this._handleWebGLContextLost,
725!
991
          handleContextRestored: options.handleContextRestored
992
        });
993
      } catch (e) {
994
        this._logger.warn(
×
995
          `Excalibur could not load webgl for some reason (${(e as Error).message}) and loaded a Canvas 2D fallback. ` +
996
            `Some features of Excalibur will not work in this mode. \n\n` +
997
            'Read more about this issue at https://excaliburjs.com/docs/performance'
998
        );
999
        // fallback to canvas in case of failure
1000
        useCanvasGraphicsContext = true;
×
1001
      }
1002
    }
1003

1004
    if (useCanvasGraphicsContext) {
727✔
1005
      this.graphicsContext = new ExcaliburGraphicsContext2DCanvas({
2✔
1006
        canvasElement: this.canvas,
1007
        enableTransparency: this.enableCanvasTransparency,
1008
        antialiasing: nativeContextAntialiasing,
1009
        backgroundColor: options.backgroundColor,
1010
        snapToPixel: options.snapToPixel,
1011
        useDrawSorting: options.useDrawSorting
1012
      });
1013
    }
1014

1015
    this.screen = new Screen({
727✔
1016
      canvas: this.canvas,
1017
      context: this.graphicsContext,
1018
      antialiasing: nativeContextAntialiasing,
1019
      canvasImageRendering: canvasImageRendering,
1020
      browser: this.browser,
1021
      viewport: options.viewport ?? (options.width && options.height ? { width: options.width, height: options.height } : Resolution.SVGA),
2,891✔
1022
      resolution: options.resolution,
1023
      displayMode,
1024
      pixelRatio: options.suppressHiDPIScaling ? 1 : options.pixelRatio ?? null
740✔
1025
    });
1026

1027
    // TODO REMOVE STATIC!!!
1028
    // Set default filtering based on antialiasing
1029
    TextureLoader.filtering = filtering;
727✔
1030

1031
    if (options.backgroundColor) {
727!
1032
      this.backgroundColor = options.backgroundColor.clone();
727✔
1033
    }
1034

1035
    this.maxFps = options.maxFps ?? this.maxFps;
727!
1036

1037
    this.fixedUpdateTimestep = options.fixedUpdateTimestep ?? this.fixedUpdateTimestep;
727!
1038
    this.fixedUpdateFps = options.fixedUpdateFps ?? this.fixedUpdateFps;
727✔
1039
    this.fixedUpdateTimestep = this.fixedUpdateTimestep || 1000 / this.fixedUpdateFps;
727✔
1040

1041
    this.clock = new StandardClock({
727✔
1042
      maxFps: this.maxFps,
1043
      tick: this._mainloop.bind(this),
1044
      onFatalException: (e) => this.onFatalException(e)
×
1045
    });
1046

1047
    this.enableCanvasTransparency = options.enableCanvasTransparency;
727✔
1048

1049
    if (typeof options.physics === 'boolean') {
727!
1050
      this.physics = {
×
1051
        ...getDefaultPhysicsConfig(),
1052
        enabled: options.physics
1053
      };
1054
    } else {
1055
      this.physics = {
727✔
1056
        ...getDefaultPhysicsConfig()
1057
      };
1058
      mergeDeep(this.physics, options.physics);
727✔
1059
    }
1060

1061
    this.debug = new DebugConfig(this);
727✔
1062

1063
    this.director = new Director(this, options.scenes);
727✔
1064
    this.director.events.pipe(this.events);
727✔
1065

1066
    this._initialize(options);
727✔
1067

1068
    (window as any).___EXCALIBUR_DEVTOOL = this;
727✔
1069
    Engine.InstanceCount++;
727✔
1070
  }
1071

1072
  private _handleWebGLContextLost = (e: Event) => {
727✔
1073
    e.preventDefault();
641✔
1074
    this.clock.stop();
641✔
1075
    this._logger.fatalOnce('WebGL Graphics Lost', e);
641✔
1076
    const container = document.createElement('div');
641✔
1077
    container.id = 'ex-webgl-graphics-context-lost';
641✔
1078
    container.style.position = 'absolute';
641✔
1079
    container.style.zIndex = '99';
641✔
1080
    container.style.left = '50%';
641✔
1081
    container.style.top = '50%';
641✔
1082
    container.style.display = 'flex';
641✔
1083
    container.style.flexDirection = 'column';
641✔
1084
    container.style.transform = 'translate(-50%, -50%)';
641✔
1085
    container.style.backgroundColor = 'white';
641✔
1086
    container.style.padding = '10px';
641✔
1087
    container.style.borderStyle = 'solid 1px';
641✔
1088

1089
    const div = document.createElement('div');
641✔
1090
    div.innerHTML = `
641✔
1091
      <h1>There was an issue rendering, please refresh the page.</h1>
1092
      <div>
1093
        <p>WebGL Graphics Context Lost</p>
1094

1095
        <button id="ex-webgl-graphics-reload">Refresh Page</button>
1096

1097
        <p>There are a few reasons this might happen:</p>
1098
        <ul>
1099
          <li>Two or more pages are placing a high demand on the GPU</li>
1100
          <li>Another page or operation has stalled the GPU and the browser has decided to reset the GPU</li>
1101
          <li>The computer has multiple GPUs and the user has switched between them</li>
1102
          <li>Graphics driver has crashed or restarted</li>
1103
          <li>Graphics driver was updated</li>
1104
        </ul>
1105
      </div>
1106
    `;
1107
    container.appendChild(div);
641✔
1108
    if (this.canvas?.parentElement) {
641!
1109
      this.canvas.parentElement.appendChild(container);
×
1110
      const button = div.querySelector('#ex-webgl-graphics-reload');
×
1111
      button?.addEventListener('click', () => location.reload());
×
1112
    }
1113
  };
1114

1115
  private _performanceThresholdTriggered = false;
727✔
1116
  private _fpsSamples: number[] = [];
727✔
1117
  private _monitorPerformanceThresholdAndTriggerFallback() {
1118
    const { allow } = this._originalOptions.configurePerformanceCanvas2DFallback;
1,742✔
1119
    let { threshold, showPlayerMessage } = this._originalOptions.configurePerformanceCanvas2DFallback;
1,742✔
1120
    if (threshold === undefined) {
1,742✔
1121
      threshold = Engine._DEFAULT_ENGINE_OPTIONS.configurePerformanceCanvas2DFallback.threshold;
100✔
1122
    }
1123
    if (showPlayerMessage === undefined) {
1,742✔
1124
      showPlayerMessage = Engine._DEFAULT_ENGINE_OPTIONS.configurePerformanceCanvas2DFallback.showPlayerMessage;
100✔
1125
    }
1126
    if (!Flags.isEnabled('use-canvas-context') && allow && this.ready && !this._performanceThresholdTriggered) {
1,742✔
1127
      // Calculate Average fps for last X number of frames after start
1128
      if (this._fpsSamples.length === threshold.numberOfFrames) {
100!
1129
        this._fpsSamples.splice(0, 1);
×
1130
      }
1131
      this._fpsSamples.push(this.clock.fpsSampler.fps);
100✔
1132
      let total = 0;
100✔
1133
      for (let i = 0; i < this._fpsSamples.length; i++) {
100✔
1134
        total += this._fpsSamples[i];
5,050✔
1135
      }
1136
      const average = total / this._fpsSamples.length;
100✔
1137

1138
      if (this._fpsSamples.length === threshold.numberOfFrames) {
100✔
1139
        if (average <= threshold.fps) {
1!
1140
          this._performanceThresholdTriggered = true;
1✔
1141
          this._logger.warn(
1✔
1142
            `Switching to browser 2D Canvas fallback due to performance. Some features of Excalibur will not work in this mode.\n` +
1143
              "this might mean your browser doesn't have webgl enabled or hardware acceleration is unavailable.\n\n" +
1144
              'If in Chrome:\n' +
1145
              '  * Visit Settings > Advanced > System, and ensure "Use Hardware Acceleration" is checked.\n' +
1146
              '  * Visit chrome://flags/#ignore-gpu-blocklist and ensure "Override software rendering list" is "enabled"\n' +
1147
              'If in Firefox, visit about:config\n' +
1148
              '  * Ensure webgl.disabled = false\n' +
1149
              '  * Ensure webgl.force-enabled = true\n' +
1150
              '  * Ensure layers.acceleration.force-enabled = true\n\n' +
1151
              'Read more about this issue at https://excaliburjs.com/docs/performance'
1152
          );
1153

1154
          if (showPlayerMessage) {
1!
1155
            this._toaster.toast(
×
1156
              'Excalibur is encountering performance issues. ' +
1157
                "It's possible that your browser doesn't have hardware acceleration enabled. " +
1158
                'Visit [LINK] for more information and potential solutions.',
1159
              'https://excaliburjs.com/docs/performance'
1160
            );
1161
          }
1162
          this.useCanvas2DFallback();
1✔
1163
          this.emit('fallbackgraphicscontext', this.graphicsContext);
1✔
1164
        }
1165
      }
1166
    }
1167
  }
1168

1169
  /**
1170
   * Switches the engine's graphics context to the 2D Canvas.
1171
   * @warning Some features of Excalibur will not work in this mode.
1172
   */
1173
  public useCanvas2DFallback() {
1174
    // Swap out the canvas
1175
    const newCanvas = this.canvas.cloneNode(false) as HTMLCanvasElement;
2✔
1176
    this.canvas.parentNode.replaceChild(newCanvas, this.canvas);
2✔
1177
    this.canvas = newCanvas;
2✔
1178

1179
    const options = { ...this._originalOptions, antialiasing: this.screen.antialiasing };
2✔
1180
    const displayMode = this._originalDisplayMode;
2✔
1181

1182
    // New graphics context
1183
    this.graphicsContext = new ExcaliburGraphicsContext2DCanvas({
2✔
1184
      canvasElement: this.canvas,
1185
      enableTransparency: this.enableCanvasTransparency,
1186
      antialiasing: options.antialiasing,
1187
      backgroundColor: options.backgroundColor,
1188
      snapToPixel: options.snapToPixel,
1189
      useDrawSorting: options.useDrawSorting
1190
    });
1191

1192
    // Reset screen
1193
    if (this.screen) {
2!
1194
      this.screen.dispose();
2✔
1195
    }
1196

1197
    this.screen = new Screen({
2✔
1198
      canvas: this.canvas,
1199
      context: this.graphicsContext,
1200
      antialiasing: options.antialiasing ?? true,
2!
1201
      browser: this.browser,
1202
      viewport: options.viewport ?? (options.width && options.height ? { width: options.width, height: options.height } : Resolution.SVGA),
8!
1203
      resolution: options.resolution,
1204
      displayMode,
1205
      pixelRatio: options.suppressHiDPIScaling ? 1 : options.pixelRatio ?? null
2!
1206
    });
1207
    this.screen.setCurrentCamera(this.currentScene.camera);
2✔
1208

1209
    // Reset pointers
1210
    this.input.pointers.detach();
2✔
1211
    const pointerTarget = options && options.pointerScope === PointerScope.Document ? document : this.canvas;
2!
1212
    this.input.pointers = this.input.pointers.recreate(pointerTarget, this);
2✔
1213
    this.input.pointers.init();
2✔
1214
  }
1215

1216
  private _disposed = false;
727✔
1217
  /**
1218
   * Attempts to completely clean up excalibur resources, including removing the canvas from the dom.
1219
   *
1220
   * To start again you will need to new up an Engine.
1221
   */
1222
  public dispose() {
1223
    if (!this._disposed) {
730✔
1224
      this._disposed = true;
726✔
1225
      this.stop();
726✔
1226
      this._garbageCollector.forceCollectAll();
726✔
1227
      this.input.toggleEnabled(false);
726✔
1228
      this.canvas.parentNode.removeChild(this.canvas);
726✔
1229
      this.canvas = null;
726✔
1230
      this.screen.dispose();
726✔
1231
      this.graphicsContext.dispose();
726✔
1232
      this.graphicsContext = null;
726✔
1233
      Engine.InstanceCount--;
726✔
1234
    }
1235
  }
1236

1237
  public isDisposed() {
1238
    return this._disposed;
1,403✔
1239
  }
1240

1241
  /**
1242
   * Returns a BoundingBox of the top left corner of the screen
1243
   * and the bottom right corner of the screen.
1244
   */
1245
  public getWorldBounds() {
1246
    return this.screen.getWorldBounds();
1✔
1247
  }
1248

1249
  /**
1250
   * Gets the current engine timescale factor (default is 1.0 which is 1:1 time)
1251
   */
1252
  public get timescale() {
1253
    return this._timescale;
1,743✔
1254
  }
1255

1256
  /**
1257
   * Sets the current engine timescale factor. Useful for creating slow-motion effects or fast-forward effects
1258
   * when using time-based movement.
1259
   */
1260
  public set timescale(value: number) {
1261
    if (value < 0) {
2!
1262
      Logger.getInstance().warnOnce('engine.timescale to a value less than 0 are ignored');
×
1263
      return;
×
1264
    }
1265

1266
    this._timescale = value;
2✔
1267
  }
1268

1269
  /**
1270
   * Adds a {@apilink Timer} to the {@apilink currentScene}.
1271
   * @param timer  The timer to add to the {@apilink currentScene}.
1272
   */
1273
  public addTimer(timer: Timer): Timer {
1274
    return this.currentScene.addTimer(timer);
×
1275
  }
1276

1277
  /**
1278
   * Removes a {@apilink Timer} from the {@apilink currentScene}.
1279
   * @param timer  The timer to remove to the {@apilink currentScene}.
1280
   */
1281
  public removeTimer(timer: Timer): Timer {
1282
    return this.currentScene.removeTimer(timer);
×
1283
  }
1284

1285
  /**
1286
   * Adds a {@apilink Scene} to the engine, think of scenes in Excalibur as you
1287
   * would levels or menus.
1288
   * @param key  The name of the scene, must be unique
1289
   * @param scene The scene to add to the engine
1290
   */
1291
  public addScene<TScene extends string>(key: TScene, scene: Scene | SceneConstructor | SceneWithOptions): Engine<TKnownScenes | TScene> {
1292
    this.director.add(key, scene);
398✔
1293
    return this as Engine<TKnownScenes | TScene>;
398✔
1294
  }
1295

1296
  /**
1297
   * Removes a {@apilink Scene} instance from the engine
1298
   * @param scene  The scene to remove
1299
   */
1300
  public removeScene(scene: Scene | SceneConstructor): void;
1301
  /**
1302
   * Removes a scene from the engine by key
1303
   * @param key  The scene key to remove
1304
   */
1305
  public removeScene(key: string): void;
1306
  /**
1307
   * @internal
1308
   */
1309
  public removeScene(entity: any): void {
1310
    this.director.remove(entity);
8✔
1311
  }
1312

1313
  /**
1314
   * Adds a {@apilink Scene} to the engine, think of scenes in Excalibur as you
1315
   * would levels or menus.
1316
   * @param sceneKey  The key of the scene, must be unique
1317
   * @param scene     The scene to add to the engine
1318
   */
1319
  public add(sceneKey: string, scene: Scene | SceneConstructor | SceneWithOptions): void;
1320
  /**
1321
   * Adds a {@apilink Timer} to the {@apilink currentScene}.
1322
   * @param timer  The timer to add to the {@apilink currentScene}.
1323
   */
1324
  public add(timer: Timer): void;
1325
  /**
1326
   * Adds a {@apilink TileMap} to the {@apilink currentScene}, once this is done the TileMap
1327
   * will be drawn and updated.
1328
   */
1329
  public add(tileMap: TileMap): void;
1330
  /**
1331
   * Adds an actor to the {@apilink currentScene} of the game. This is synonymous
1332
   * to calling `engine.currentScene.add(actor)`.
1333
   *
1334
   * Actors can only be drawn if they are a member of a scene, and only
1335
   * the {@apilink currentScene} may be drawn or updated.
1336
   * @param actor  The actor to add to the {@apilink currentScene}
1337
   */
1338
  public add(actor: Actor): void;
1339

1340
  public add(entity: Entity): void;
1341

1342
  /**
1343
   * Adds a {@apilink ScreenElement} to the {@apilink currentScene} of the game,
1344
   * ScreenElements do not participate in collisions, instead the
1345
   * remain in the same place on the screen.
1346
   * @param screenElement  The ScreenElement to add to the {@apilink currentScene}
1347
   */
1348
  public add(screenElement: ScreenElement): void;
1349
  public add(entity: any): void {
1350
    if (arguments.length === 2) {
264✔
1351
      this.director.add(<string>arguments[0], <Scene | SceneConstructor | SceneWithOptions>arguments[1]);
95✔
1352
      return;
95✔
1353
    }
1354
    const maybeDeferred = this.director.getDeferredScene();
169✔
1355
    if (maybeDeferred instanceof Scene) {
169!
1356
      maybeDeferred.add(entity);
×
1357
    } else {
1358
      this.currentScene.add(entity);
169✔
1359
    }
1360
  }
1361

1362
  /**
1363
   * Removes a scene instance from the engine
1364
   * @param scene  The scene to remove
1365
   */
1366
  public remove(scene: Scene | SceneConstructor): void;
1367
  /**
1368
   * Removes a scene from the engine by key
1369
   * @param sceneKey  The scene to remove
1370
   */
1371
  public remove(sceneKey: string): void;
1372
  /**
1373
   * Removes a {@apilink Timer} from the {@apilink currentScene}.
1374
   * @param timer  The timer to remove to the {@apilink currentScene}.
1375
   */
1376
  public remove(timer: Timer): void;
1377
  /**
1378
   * Removes a {@apilink TileMap} from the {@apilink currentScene}, it will no longer be drawn or updated.
1379
   */
1380
  public remove(tileMap: TileMap): void;
1381
  /**
1382
   * Removes an actor from the {@apilink currentScene} of the game. This is synonymous
1383
   * to calling `engine.currentScene.removeChild(actor)`.
1384
   * Actors that are removed from a scene will no longer be drawn or updated.
1385
   * @param actor  The actor to remove from the {@apilink currentScene}.
1386
   */
1387
  public remove(actor: Actor): void;
1388
  /**
1389
   * Removes a {@apilink ScreenElement} to the scene, it will no longer be drawn or updated
1390
   * @param screenElement  The ScreenElement to remove from the {@apilink currentScene}
1391
   */
1392
  public remove(screenElement: ScreenElement): void;
1393
  public remove(entity: any): void {
1394
    if (entity instanceof Entity) {
4✔
1395
      this.currentScene.remove(entity);
2✔
1396
    }
1397

1398
    if (entity instanceof Scene || isSceneConstructor(entity)) {
4✔
1399
      this.removeScene(entity);
1✔
1400
    }
1401

1402
    if (typeof entity === 'string') {
4✔
1403
      this.removeScene(entity);
1✔
1404
    }
1405
  }
1406

1407
  /**
1408
   * Changes the current scene with optionally supplied:
1409
   * * Activation data
1410
   * * Transitions
1411
   * * Loaders
1412
   *
1413
   * Example:
1414
   * ```typescript
1415
   * game.goToScene('myScene', {
1416
   *   sceneActivationData: {any: 'thing at all'},
1417
   *   destinationIn: new FadeInOut({duration: 1000, direction: 'in'}),
1418
   *   sourceOut: new FadeInOut({duration: 1000, direction: 'out'}),
1419
   *   loader: MyLoader
1420
   * });
1421
   * ```
1422
   *
1423
   * Scenes are defined in the Engine constructor
1424
   * ```typescript
1425
   * const engine = new ex.Engine({
1426
      scenes: {...}
1427
    });
1428
   * ```
1429
   * Or by adding dynamically
1430
   *
1431
   * ```typescript
1432
   * engine.addScene('myScene', new ex.Scene());
1433
   * ```
1434
   * @param destinationScene
1435
   * @param options
1436
   */
1437
  public async goToScene<TData = undefined>(destinationScene: WithRoot<TKnownScenes>, options?: GoToOptions<TData>): Promise<void> {
1438
    await this.scope(async () => {
416✔
1439
      await this.director.goToScene(destinationScene, options);
416✔
1440
    });
1441
  }
1442

1443
  /**
1444
   * Transforms the current x, y from screen coordinates to world coordinates
1445
   * @param point  Screen coordinate to convert
1446
   */
1447
  public screenToWorldCoordinates(point: Vector): Vector {
1448
    return this.screen.screenToWorldCoordinates(point);
2✔
1449
  }
1450

1451
  /**
1452
   * Transforms a world coordinate, to a screen coordinate
1453
   * @param point  World coordinate to convert
1454
   */
1455
  public worldToScreenCoordinates(point: Vector): Vector {
1456
    return this.screen.worldToScreenCoordinates(point);
3✔
1457
  }
1458

1459
  /**
1460
   * Initializes the internal canvas, rendering context, display mode, and native event listeners
1461
   */
1462
  private _initialize(options?: EngineOptions) {
1463
    this.pageScrollPreventionMode = options.scrollPreventionMode;
727✔
1464

1465
    // initialize inputs
1466
    const pointerTarget = options && options.pointerScope === PointerScope.Document ? document : this.canvas;
727✔
1467
    const grabWindowFocus = this._originalOptions?.grabWindowFocus ?? true;
727!
1468
    this.input = new InputHost({
727✔
1469
      pointerTarget,
1470
      grabWindowFocus,
1471
      engine: this
1472
    });
1473
    this.inputMapper = this.input.inputMapper;
727✔
1474

1475
    // Issue #385 make use of the visibility api
1476
    // https://developer.mozilla.org/en-US/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API
1477

1478
    this.browser.document.on('visibilitychange', () => {
727✔
1479
      if (document.visibilityState === 'hidden') {
20✔
1480
        this.events.emit('hidden', new HiddenEvent(this));
10✔
1481
        this._logger.debug('Window hidden');
10✔
1482
      } else if (document.visibilityState === 'visible') {
10!
1483
        this.events.emit('visible', new VisibleEvent(this));
10✔
1484
        this._logger.debug('Window visible');
10✔
1485
      }
1486
    });
1487

1488
    if (!this.canvasElementId && !options.canvasElement) {
727!
1489
      document.body.appendChild(this.canvas);
727✔
1490
    }
1491
  }
1492

1493
  public toggleInputEnabled(enabled: boolean) {
1494
    this._inputEnabled = enabled;
×
1495
    this.input.toggleEnabled(this._inputEnabled);
×
1496
  }
1497

1498
  public onInitialize(engine: Engine) {
1499
    // Override me
1500
  }
1501

1502
  /**
1503
   * Gets whether the actor is Initialized
1504
   */
1505
  public get isInitialized(): boolean {
1506
    return this._isInitialized;
566✔
1507
  }
1508

1509
  private async _overrideInitialize(engine: Engine) {
1510
    if (!this.isInitialized) {
566✔
1511
      await this.director.onInitialize();
557✔
1512
      await this.onInitialize(engine);
557✔
1513
      this.events.emit('initialize', new InitializeEvent(engine, this));
557✔
1514
      this._isInitialized = true;
557✔
1515
    }
1516
  }
1517

1518
  /**
1519
   * Updates the entire state of the game
1520
   * @param elapsed  Number of milliseconds elapsed since the last update.
1521
   */
1522
  private _update(elapsed: number) {
1523
    if (this._isLoading) {
1,740✔
1524
      // suspend updates until loading is finished
1525
      this._loader?.onUpdate(this, elapsed);
829!
1526
      // Update input listeners
1527
      this.input.update();
829✔
1528
      return;
829✔
1529
    }
1530

1531
    // Publish preupdate events
1532
    this.clock.__runScheduledCbs('preupdate');
911✔
1533
    this._preupdate(elapsed);
911✔
1534

1535
    // process engine level events
1536
    this.currentScene.update(this, elapsed);
911✔
1537

1538
    // Update graphics postprocessors
1539
    this.graphicsContext.updatePostProcessors(elapsed);
911✔
1540

1541
    // Publish update event
1542
    this.clock.__runScheduledCbs('postupdate');
911✔
1543
    this._postupdate(elapsed);
911✔
1544

1545
    // Update input listeners
1546
    this.input.update();
911✔
1547
  }
1548

1549
  /**
1550
   * @internal
1551
   */
1552
  public _preupdate(elapsed: number) {
1553
    this.emit('preupdate', new PreUpdateEvent(this, elapsed, this));
911✔
1554
    this.onPreUpdate(this, elapsed);
911✔
1555
  }
1556

1557
  /**
1558
   * Safe to override method
1559
   * @param engine The reference to the current game engine
1560
   * @param elapsed  The time elapsed since the last update in milliseconds
1561
   */
1562
  public onPreUpdate(engine: Engine, elapsed: number) {
1563
    // Override me
1564
  }
1565

1566
  /**
1567
   * @internal
1568
   */
1569
  public _postupdate(elapsed: number) {
1570
    this.emit('postupdate', new PostUpdateEvent(this, elapsed, this));
911✔
1571
    this.onPostUpdate(this, elapsed);
911✔
1572
  }
1573

1574
  /**
1575
   * Safe to override method
1576
   * @param engine The reference to the current game engine
1577
   * @param elapsed  The time elapsed since the last update in milliseconds
1578
   */
1579
  public onPostUpdate(engine: Engine, elapsed: number) {
1580
    // Override me
1581
  }
1582

1583
  /**
1584
   * Draws the entire game
1585
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1586
   */
1587
  private _draw(elapsed: number) {
1588
    // Use scene background color if present, fallback to engine
1589
    this.graphicsContext.backgroundColor = this.currentScene.backgroundColor ?? this.backgroundColor;
1,743✔
1590
    this.graphicsContext.beginDrawLifecycle();
1,743✔
1591
    this.graphicsContext.clear();
1,743✔
1592
    this.clock.__runScheduledCbs('predraw');
1,743✔
1593
    this._predraw(this.graphicsContext, elapsed);
1,743✔
1594

1595
    // Drawing nothing else while loading
1596
    if (this._isLoading) {
1,743✔
1597
      if (!this._hideLoader) {
829!
1598
        this._loader?.canvas.draw(this.graphicsContext, 0, 0);
829!
1599
        this.clock.__runScheduledCbs('postdraw');
829✔
1600
        this.graphicsContext.flush();
829✔
1601
        this.graphicsContext.endDrawLifecycle();
829✔
1602
      }
1603
      return;
829✔
1604
    }
1605

1606
    this.currentScene.draw(this.graphicsContext, elapsed);
914✔
1607

1608
    this.clock.__runScheduledCbs('postdraw');
914✔
1609
    this._postdraw(this.graphicsContext, elapsed);
914✔
1610

1611
    // Flush any pending drawings
1612
    this.graphicsContext.flush();
914✔
1613
    this.graphicsContext.endDrawLifecycle();
914✔
1614

1615
    this._checkForScreenShots();
914✔
1616
  }
1617

1618
  /**
1619
   * @internal
1620
   */
1621
  public _predraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1622
    this.emit('predraw', new PreDrawEvent(ctx, elapsed, this));
1,743✔
1623
    this.onPreDraw(ctx, elapsed);
1,743✔
1624
  }
1625

1626
  /**
1627
   * Safe to override method to hook into pre draw
1628
   * @param ctx {@link ExcaliburGraphicsContext} for drawing
1629
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1630
   */
1631
  public onPreDraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1632
    // Override me
1633
  }
1634

1635
  /**
1636
   * @internal
1637
   */
1638
  public _postdraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1639
    this.emit('postdraw', new PostDrawEvent(ctx, elapsed, this));
914✔
1640
    this.onPostDraw(ctx, elapsed);
914✔
1641
  }
1642

1643
  /**
1644
   * Safe to override method to hook into pre draw
1645
   * @param ctx {@link ExcaliburGraphicsContext} for drawing
1646
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1647
   */
1648
  public onPostDraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1649
    // Override me
1650
  }
1651

1652
  /**
1653
   * Enable or disable Excalibur debugging functionality.
1654
   * @param toggle a value that debug drawing will be changed to
1655
   */
1656
  public showDebug(toggle: boolean): void {
1657
    this._isDebug = toggle;
2✔
1658
  }
1659

1660
  /**
1661
   * Toggle Excalibur debugging functionality.
1662
   */
1663
  public toggleDebug(): boolean {
1664
    this._isDebug = !this._isDebug;
14✔
1665
    return this._isDebug;
14✔
1666
  }
1667

1668
  /**
1669
   * Returns true when loading is totally complete and the player has clicked start
1670
   */
1671
  public get loadingComplete() {
1672
    return !this._isLoading;
709✔
1673
  }
1674

1675
  private _isLoading = false;
727✔
1676
  private _hideLoader = false;
727✔
1677
  private _isReadyFuture = new Future<void>();
727✔
1678
  public get ready() {
1679
    return this._isReadyFuture.isCompleted;
100✔
1680
  }
1681
  public isReady(): Promise<void> {
1682
    return this._isReadyFuture.promise;
14✔
1683
  }
1684

1685
  /**
1686
   * Starts the internal game loop for Excalibur after loading
1687
   * any provided assets.
1688
   * @param loader  Optional {@apilink Loader} to use to load resources. The default loader is {@apilink Loader},
1689
   * override to provide your own custom loader.
1690
   *
1691
   * Note: start() only resolves AFTER the user has clicked the play button
1692
   */
1693
  public async start(loader?: DefaultLoader): Promise<void>;
1694
  /**
1695
   * Starts the internal game loop for Excalibur after configuring any routes, loaders, or transitions
1696
   * @param startOptions Optional {@apilink StartOptions} to configure the routes for scenes in Excalibur
1697
   *
1698
   * Note: start() only resolves AFTER the user has clicked the play button
1699
   */
1700
  public async start(sceneName: WithRoot<TKnownScenes>, options?: StartOptions): Promise<void>;
1701
  /**
1702
   * Starts the internal game loop after any loader is finished
1703
   * @param loader
1704
   */
1705
  public async start(loader?: DefaultLoader): Promise<void>;
1706
  public async start(sceneNameOrLoader?: WithRoot<TKnownScenes> | DefaultLoader, options?: StartOptions): Promise<void> {
1707
    await this.scope(async () => {
565✔
1708
      if (!this._compatible) {
565!
1709
        throw new Error('Excalibur is incompatible with your browser');
×
1710
      }
1711
      this._isLoading = true;
565✔
1712
      let loader: DefaultLoader;
1713
      if (sceneNameOrLoader instanceof DefaultLoader) {
565✔
1714
        loader = sceneNameOrLoader;
15✔
1715
      } else if (typeof sceneNameOrLoader === 'string') {
550!
1716
        this.director.configureStart(sceneNameOrLoader, options);
×
1717
        loader = this.director.mainLoader;
×
1718
      }
1719

1720
      // Start the excalibur clock which drives the mainloop
1721
      this._logger.debug('Starting game clock...');
565✔
1722
      this.browser.resume();
565✔
1723
      this.clock.start();
565✔
1724
      if (this.garbageCollectorConfig) {
565!
1725
        this._garbageCollector.start();
565✔
1726
      }
1727
      this._logger.debug('Game clock started');
565✔
1728

1729
      await this.load(loader ?? new Loader());
565✔
1730

1731
      // Initialize before ready
1732
      await this._overrideInitialize(this);
564✔
1733

1734
      this._isReadyFuture.resolve();
564✔
1735
      this.emit('start', new GameStartEvent(this));
564✔
1736
      return this._isReadyFuture.promise;
564✔
1737
    });
1738
  }
1739

1740
  /**
1741
   * Returns the current frames elapsed milliseconds
1742
   */
1743
  public currentFrameElapsedMs = 0;
727✔
1744

1745
  /**
1746
   * Returns the current frame lag when in fixed update mode
1747
   */
1748
  public currentFrameLagMs = 0;
727✔
1749

1750
  private _lagMs = 0;
727✔
1751
  private _mainloop(elapsed: number) {
1752
    this.scope(() => {
1,742✔
1753
      this.emit('preframe', new PreFrameEvent(this, this.stats.prevFrame));
1,742✔
1754
      const elapsedMs = elapsed * this.timescale;
1,742✔
1755
      this.currentFrameElapsedMs = elapsedMs;
1,742✔
1756

1757
      // reset frame stats (reuse existing instances)
1758
      const frameId = this.stats.prevFrame.id + 1;
1,742✔
1759
      this.stats.currFrame.reset();
1,742✔
1760
      this.stats.currFrame.id = frameId;
1,742✔
1761
      this.stats.currFrame.elapsedMs = elapsedMs;
1,742✔
1762
      this.stats.currFrame.fps = this.clock.fpsSampler.fps;
1,742✔
1763
      GraphicsDiagnostics.clear();
1,742✔
1764

1765
      const beforeUpdate = this.clock.now();
1,742✔
1766
      const fixedTimestepMs = this.fixedUpdateTimestep;
1,742✔
1767
      if (this.fixedUpdateTimestep) {
1,742✔
1768
        this._lagMs += elapsedMs;
44✔
1769
        while (this._lagMs >= fixedTimestepMs) {
44✔
1770
          this._update(fixedTimestepMs);
42✔
1771
          this._lagMs -= fixedTimestepMs;
42✔
1772
        }
1773
      } else {
1774
        this._update(elapsedMs);
1,698✔
1775
      }
1776
      const afterUpdate = this.clock.now();
1,742✔
1777
      this.currentFrameLagMs = this._lagMs;
1,742✔
1778
      this._draw(elapsedMs);
1,742✔
1779
      const afterDraw = this.clock.now();
1,742✔
1780

1781
      this.stats.currFrame.duration.update = afterUpdate - beforeUpdate;
1,742✔
1782
      this.stats.currFrame.duration.draw = afterDraw - afterUpdate;
1,742✔
1783
      this.stats.currFrame.graphics.drawnImages = GraphicsDiagnostics.DrawnImagesCount;
1,742✔
1784
      this.stats.currFrame.graphics.drawCalls = GraphicsDiagnostics.DrawCallCount;
1,742✔
1785

1786
      this.emit('postframe', new PostFrameEvent(this, this.stats.currFrame));
1,742✔
1787
      this.stats.prevFrame.reset(this.stats.currFrame);
1,742✔
1788

1789
      this._monitorPerformanceThresholdAndTriggerFallback();
1,742✔
1790
    });
1791
  }
1792

1793
  /**
1794
   * Stops Excalibur's main loop, useful for pausing the game.
1795
   */
1796
  public stop() {
1797
    if (this.clock.isRunning()) {
1,455✔
1798
      this.emit('stop', new GameStopEvent(this));
575✔
1799
      this.browser.pause();
575✔
1800
      this.clock.stop();
575✔
1801
      this._garbageCollector.stop();
575✔
1802
      this._logger.debug('Game stopped');
575✔
1803
    }
1804
  }
1805

1806
  /**
1807
   * Returns the Engine's running status, Useful for checking whether engine is running or paused.
1808
   */
1809
  public isRunning() {
1810
    return this.clock.isRunning();
3✔
1811
  }
1812

1813
  private _screenShotRequests: { preserveHiDPIResolution: boolean; resolve: (image: HTMLImageElement) => void }[] = [];
727✔
1814
  /**
1815
   * Takes a screen shot of the current viewport and returns it as an
1816
   * HTML Image Element.
1817
   * @param preserveHiDPIResolution in the case of HiDPI return the full scaled backing image, by default false
1818
   */
1819
  public screenshot(preserveHiDPIResolution = false): Promise<HTMLImageElement> {
3✔
1820
    const screenShotPromise = new Promise<HTMLImageElement>((resolve) => {
9✔
1821
      this._screenShotRequests.push({ preserveHiDPIResolution, resolve });
9✔
1822
    });
1823
    return screenShotPromise;
9✔
1824
  }
1825

1826
  private _checkForScreenShots() {
1827
    // We must grab the draw buffer before we yield to the browser
1828
    // the draw buffer is cleared after compositing
1829
    // the reason for the asynchrony is setting `preserveDrawingBuffer: true`
1830
    // forces the browser to copy buffers which can have a mass perf impact on mobile
1831
    for (const request of this._screenShotRequests) {
914✔
1832
      const finalWidth = request.preserveHiDPIResolution ? this.canvas.width : this.screen.resolution.width;
9✔
1833
      const finalHeight = request.preserveHiDPIResolution ? this.canvas.height : this.screen.resolution.height;
9✔
1834
      const screenshot = document.createElement('canvas');
9✔
1835
      screenshot.width = finalWidth;
9✔
1836
      screenshot.height = finalHeight;
9✔
1837
      const ctx = screenshot.getContext('2d');
9✔
1838
      ctx.imageSmoothingEnabled = this.screen.antialiasing;
9✔
1839
      ctx.drawImage(this.canvas, 0, 0, finalWidth, finalHeight);
9✔
1840

1841
      const result = new Image();
9✔
1842
      const raw = screenshot.toDataURL('image/png');
9✔
1843
      result.onload = () => {
9✔
1844
        request.resolve(result);
9✔
1845
      };
1846
      result.src = raw;
9✔
1847
    }
1848
    // Reset state
1849
    this._screenShotRequests.length = 0;
914✔
1850
  }
1851

1852
  /**
1853
   * Another option available to you to load resources into the game.
1854
   * Immediately after calling this the game will pause and the loading screen
1855
   * will appear.
1856
   * @param loader  Some {@apilink Loadable} such as a {@apilink Loader} collection, {@apilink Sound}, or {@apilink Texture}.
1857
   */
1858
  public async load(loader: DefaultLoader, hideLoader = false): Promise<void> {
989✔
1859
    await this.scope(async () => {
989✔
1860
      try {
989✔
1861
        // early exit if loaded
1862
        if (loader.isLoaded()) {
989✔
1863
          return;
973✔
1864
        }
1865
        this._loader = loader;
16✔
1866
        this._isLoading = true;
16✔
1867
        this._hideLoader = hideLoader;
16✔
1868

1869
        if (loader instanceof Loader) {
16✔
1870
          loader.suppressPlayButton = loader.suppressPlayButton || this._suppressPlayButton;
15✔
1871
        }
1872
        this._loader.onInitialize(this);
16✔
1873

1874
        await loader.load();
16✔
1875
      } catch (e) {
1876
        this._logger.error('Error loading resources, things may not behave properly', e);
1✔
1877
        await Promise.resolve();
1✔
1878
      } finally {
1879
        this._isLoading = false;
988✔
1880
        this._hideLoader = false;
988✔
1881
        this._loader = null;
988✔
1882
      }
1883
    });
1884
  }
1885
}
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