• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

excaliburjs / Excalibur / 21456684866

28 Jan 2026 09:46PM UTC coverage: 88.598% (-0.2%) from 88.757%
21456684866

Pull #3670

github

web-flow
Merge 4e874ac0c into 8cba88917
Pull Request #3670: feat: Implement Plugin System

5402 of 7361 branches covered (73.39%)

15 of 47 new or added lines in 3 files covered. (31.91%)

44 existing lines in 2 files now uncovered.

14880 of 16795 relevant lines covered (88.6%)

24431.98 hits per line

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

84.72
/src/engine/engine.ts
1
import { EX_VERSION } from './';
2
import { Future } from './util/future';
3
import type { EventKey, Handler, Subscription } from './event-emitter';
4
import { EventEmitter } from './event-emitter';
5
import { PointerScope } from './input/pointer-scope';
6
import { Flags } from './flags';
7
import { polyfill } from './polyfill';
8
polyfill();
248✔
9
import type { CanUpdate, CanDraw, CanInitialize } from './interfaces/lifecycle-events';
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 './screen-element';
14
import type { Actor } from './actor';
15
import type { Timer } from './timer';
16
import type { TileMap } from './tile-map';
17
import { DefaultLoader } from './director/default-loader';
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 './entity-component-system/entity';
38
import type { DebugStats } from './debug/debug-config';
39
import { DebugConfig } from './debug/debug-config';
40
import { BrowserEvents } from './util/browser';
41
import type { AntialiasOptions, ExcaliburGraphicsContext, ExcaliburGraphicsContextWebGLOptions } 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/graphics-diagnostics';
53
import { Toaster } from './util/toaster';
54
import type { InputMapper } from './input/input-mapper';
55
import type { GoToOptions, SceneMap, StartOptions, SceneWithOptions, WithRoot } from './director/director';
56
import { Director, DirectorEvents } from './director/director';
57
import { InputHost } from './input/input-host';
58
import type { PhysicsConfig } from './collision/physics-config';
59
import { getDefaultPhysicsConfig } from './collision/physics-config';
60
import type { DeepRequired } from './util/required';
61
import type { Context } from './context';
62
import { createContext, useContext } from './context';
63
import type { GarbageCollectionOptions } from './garbage-collector';
64
import { DefaultGarbageCollectionOptions, GarbageCollector } from './garbage-collector';
65
import { mergeDeep } from './util/util';
66
import { getDefaultGlobal } from './util/iframe';
67
import { Plugin } from './plugin';
68

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

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

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

118
/**
119
 * Defines the available options to configure the Excalibur engine at constructor time.
120
 */
121
export interface EngineOptions<TKnownScenes extends string = any> {
122
  /**
123
   * Optionally configure Excalibur plugins
124
   */
125
  plugins?: Plugin[];
126
  /**
127
   * Optionally configure the width of the viewport in css pixels
128
   */
129
  width?: number;
130

131
  /**
132
   * Optionally configure the height of the viewport in css pixels
133
   */
134
  height?: number;
135

136
  /**
137
   * Optionally configure the width & height of the viewport in css pixels.
138
   * Use `viewport` instead of {@apilink EngineOptions.width} and {@apilink EngineOptions.height}, or vice versa.
139
   */
140
  viewport?: ViewportDimension;
141

142
  /**
143
   * Optionally specify the size the logical pixel resolution, if not specified it will be width x height.
144
   * See {@apilink Resolution} for common presets.
145
   */
146
  resolution?: Resolution;
147

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

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

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

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

205
  /**
206
   * Optionally hint the graphics context into a specific power profile
207
   *
208
   * Default "high-performance"
209
   */
210
  powerPreference?: 'default' | 'high-performance' | 'low-power';
211

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

223
  /**
224
   * Optionally configure the native canvas transparent backdrop
225
   */
226
  enableCanvasTransparency?: boolean;
227

228
  /**
229
   * Optionally specify the target canvas DOM element to render the game in
230
   */
231
  canvasElementId?: string;
232

233
  /**
234
   * Optionally specify the target canvas DOM element directly
235
   */
236
  canvasElement?: HTMLCanvasElement;
237

238
  /**
239
   * Optionally enable the right click context menu on the canvas
240
   *
241
   * Default if unset is false
242
   */
243
  enableCanvasContextMenu?: boolean;
244

245
  /**
246
   * Optionally snap graphics to nearest pixel, default is false
247
   */
248
  snapToPixel?: boolean;
249

250
  /**
251
   * The {@apilink DisplayMode} of the game, by default {@apilink DisplayMode.FitScreen} with aspect ratio 4:3 (800x600).
252
   * Depending on this value, {@apilink width} and {@apilink height} may be ignored.
253
   */
254
  displayMode?: DisplayMode;
255

256
  /**
257
   * Optionally configure the global, or a factory to produce it to listen to for browser events for Excalibur to listen to
258
   */
259
  global?: GlobalEventHandlers | (() => GlobalEventHandlers);
260

261
  /**
262
   * Configures the pointer scope. Pointers scoped to the 'Canvas' can only fire events within the canvas viewport; whereas, 'Document'
263
   * (default) scoped will fire anywhere on the page.
264
   */
265
  pointerScope?: PointerScope;
266

267
  /**
268
   * Suppress boot up console message, which contains the "powered by Excalibur message"
269
   */
270
  suppressConsoleBootMessage?: boolean;
271

272
  /**
273
   * Suppress minimum browser feature detection, it is not recommended users of excalibur switch this off. This feature ensures that
274
   * the currently running browser meets the minimum requirements for running excalibur. This can be useful if running on non-standard
275
   * browsers or if there is a bug in excalibur preventing execution.
276
   */
277
  suppressMinimumBrowserFeatureDetection?: boolean;
278

279
  /**
280
   * Suppress HiDPI auto detection and scaling, it is not recommended users of excalibur switch off this feature. This feature detects
281
   * and scales the drawing canvas appropriately to accommodate HiDPI screens.
282
   */
283
  suppressHiDPIScaling?: boolean;
284

285
  /**
286
   * Suppress play button, it is not recommended users of excalibur switch this feature. Some browsers require a user gesture (like a click)
287
   * for certain browser features to work like web audio.
288
   */
289
  suppressPlayButton?: boolean;
290

291
  /**
292
   * Sets the focus of the window, this is needed when hosting excalibur in a cross-origin/same-origin iframe in order for certain events
293
   * (like keyboard) to work. You can use
294
   * For example: itch.io or codesandbox.io
295
   *
296
   * By default set to true,
297
   */
298
  grabWindowFocus?: boolean;
299

300
  /**
301
   * Scroll prevention method.
302
   */
303
  scrollPreventionMode?: ScrollPreventionMode;
304

305
  /**
306
   * Optionally set the background color
307
   */
308
  backgroundColor?: Color;
309

310
  /**
311
   * Optionally set the maximum fps if not set Excalibur will go as fast as the device allows.
312
   *
313
   * 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
314
   * one that bounces between 30fps and 60fps
315
   */
316
  maxFps?: number;
317

318
  /**
319
   * Optionally configure a fixed update timestep in milliseconds, this can be desirable if you need the physics simulation to be very stable. When
320
   * 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
321
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
322
   * there could be X updates and 1 draw each clock step.
323
   *
324
   * **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
325
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
326
   *
327
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
328
   *
329
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
330
   */
331
  fixedUpdateTimestep?: number;
332

333
  /**
334
   * Optionally configure a fixed update fps, this can be desirable if you need the physics simulation to be very stable. When set
335
   * the update step and physics will use the same elapsed time for each tick even if the graphical framerate drops. In order for the
336
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
337
   * there could be X updates and 1 draw each clock step.
338
   *
339
   * **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
340
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
341
   *
342
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
343
   *
344
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
345
   */
346
  fixedUpdateFps?: number;
347

348
  /**
349
   * Default `true`, optionally configure excalibur to use optimal draw call sorting, to opt out set this to `false`.
350
   *
351
   * Excalibur will automatically sort draw calls by z and priority into renderer batches for maximal draw performance,
352
   * this can disrupt a specific desired painter order.
353
   *
354
   */
355
  useDrawSorting?: boolean;
356

357
  /**
358
   * Optionally provide a custom handler for the webgl context lost event
359
   */
360
  handleContextLost?: (e: Event) => void;
361

362
  /**
363
   * Optionally provide a custom handler for the webgl context restored event
364
   */
365
  handleContextRestored?: (e: Event) => void;
366

367
  /**
368
   * Optionally configure how excalibur handles poor performance on a player's browser
369
   */
370
  configurePerformanceCanvas2DFallback?: {
371
    /**
372
     * By default `false`, this will switch the internal graphics context to Canvas2D which can improve performance on non hardware
373
     * accelerated browsers.
374
     */
375
    allow: boolean;
376
    /**
377
     * By default `false`, if set to `true` a dialogue will be presented to the player about their browser and how to potentially
378
     * address any issues.
379
     */
380
    showPlayerMessage?: boolean;
381
    /**
382
     * Default `{ numberOfFrames: 100, fps: 20 }`, optionally configure excalibur to fallback to the 2D Canvas renderer
383
     * if bad performance is detected.
384
     *
385
     * 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
386
     * Canvas renderer.
387
     */
388
    threshold?: { numberOfFrames: number; fps: number };
389
  };
390

391
  /**
392
   * Optionally configure the physics simulation in excalibur
393
   *
394
   * If false, Excalibur will not produce a physics simulation.
395
   *
396
   * Default is configured to use {@apilink SolverStrategy.Arcade} physics simulation
397
   */
398
  physics?: boolean | PhysicsConfig;
399

400
  /**
401
   * Optionally specify scenes with their transitions and loaders to excalibur's scene {@apilink Director}
402
   *
403
   * Scene transitions can can overridden dynamically by the `Scene` or by the call to `.goToScene`
404
   */
405
  scenes?: SceneMap<TKnownScenes>;
406
}
407

408
/**
409
 * The Excalibur Engine
410
 *
411
 * The {@apilink Engine} is the main driver for a game. It is responsible for
412
 * starting/stopping the game, maintaining state, transmitting events,
413
 * loading resources, and managing the scene.
414
 */
415
export class Engine<TKnownScenes extends string = any> implements CanInitialize, CanUpdate, CanDraw {
248✔
416
  static Context: Context<Engine | null> = createContext<Engine | null>();
417
  static useEngine(): Engine {
418
    const value = useContext(Engine.Context);
20✔
419

420
    if (!value) {
20✔
421
      throw new Error('Cannot inject engine with `useEngine()`, `useEngine()` was called outside of Engine lifecycle scope.');
1✔
422
    }
423

424
    return value;
19✔
425
  }
426
  static InstanceCount = 0;
427

428
  /**
429
   * Anything run under scope can use `useEngine()` to inject the current engine
430
   * @param cb
431
   */
432
  scope = <TReturn>(cb: () => TReturn) => Engine.Context.scope(this, cb);
3,781✔
433

434
  public global: GlobalEventHandlers;
435

436
  private _plugins: Plugin[] = [];
746✔
437

438
  private _garbageCollector: GarbageCollector;
439

440
  public readonly garbageCollectorConfig: GarbageCollectionOptions | null;
441

442
  /**
443
   * Current Excalibur version string
444
   *
445
   * Useful for plugins or other tools that need to know what features are available
446
   */
447
  public readonly version = EX_VERSION;
746✔
448

449
  /**
450
   * Listen to and emit events on the Engine
451
   */
452
  public events = new EventEmitter<EngineEvents>();
746✔
453

454
  /**
455
   * Excalibur browser events abstraction used for wiring to native browser events safely
456
   */
457
  public browser: BrowserEvents;
458

459
  /**
460
   * Screen abstraction
461
   */
462
  public screen: Screen;
463

464
  /**
465
   * Scene director, manages all scenes, scene transitions, and loaders in excalibur
466
   */
467
  public director: Director<TKnownScenes>;
468

469
  /**
470
   * Direct access to the engine's canvas element
471
   */
472
  public canvas: HTMLCanvasElement;
473

474
  /**
475
   * Direct access to the ExcaliburGraphicsContext used for drawing things to the screen
476
   */
477
  public graphicsContext: ExcaliburGraphicsContext;
478

479
  /**
480
   * Direct access to the canvas element ID, if an ID exists
481
   */
482
  public canvasElementId: string;
483

484
  /**
485
   * Direct access to the physics configuration for excalibur
486
   */
487
  public physics: DeepRequired<PhysicsConfig>;
488

489
  /**
490
   * Optionally set the maximum fps if not set Excalibur will go as fast as the device allows.
491
   *
492
   * 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
493
   * one that bounces between 30fps and 60fps
494
   */
495
  public maxFps: number = Number.POSITIVE_INFINITY;
746✔
496

497
  /**
498
   * Optionally configure a fixed update fps, this can be desirable if you need the physics simulation to be very stable. When set
499
   * 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 fixedUpdateFps?: number;
511

512
  /**
513
   * Optionally configure a fixed update timestep in milliseconds, this can be desirable if you need the physics simulation to be very stable. When
514
   * 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
515
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
516
   * there could be X updates and 1 draw each clock step.
517
   *
518
   * **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
519
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
520
   *
521
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
522
   *
523
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
524
   */
525
  public readonly fixedUpdateTimestep?: number;
526

527
  /**
528
   * Direct access to the excalibur clock
529
   */
530
  public clock: Clock;
531

532
  public readonly pointerScope: PointerScope;
533
  public readonly grabWindowFocus: boolean;
534

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

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

550
  /**
551
   * The height of the game canvas in pixels, (physical height component of
552
   * the resolution of the canvas element)
553
   */
554
  public get canvasHeight(): number {
555
    return this.screen.canvasHeight;
837✔
556
  }
557

558
  /**
559
   * Returns half height of the game canvas in pixels (half physical height component)
560
   */
561
  public get halfCanvasHeight(): number {
562
    return this.screen.halfCanvasHeight;
3✔
563
  }
564

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

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

579
  /**
580
   * Returns the height of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
581
   */
582
  public get drawHeight(): number {
583
    return this.screen.drawHeight;
5✔
584
  }
585

586
  /**
587
   * Returns half the height of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
588
   */
589
  public get halfDrawHeight(): number {
590
    return this.screen.halfDrawHeight;
1,905✔
591
  }
592

593
  /**
594
   * Returns whether excalibur detects the current screen to be HiDPI
595
   */
596
  public get isHiDpi(): boolean {
597
    return this.screen.isHiDpi;
3✔
598
  }
599

600
  /**
601
   * Access engine input like pointer, keyboard, or gamepad
602
   */
603
  public input: InputHost;
604

605
  /**
606
   * Map multiple input sources to specific game actions actions
607
   */
608
  public inputMapper: InputMapper;
609

610
  private _inputEnabled: boolean = true;
746✔
611

612
  /**
613
   * Access Excalibur debugging functionality.
614
   *
615
   * Useful when you want to debug different aspects of built in engine features like
616
   *   * Transform
617
   *   * Graphics
618
   *   * Colliders
619
   */
620
  public debug: DebugConfig;
621

622
  /**
623
   * Access {@apilink stats} that holds frame statistics.
624
   */
625
  public get stats(): DebugStats {
626
    return this.debug.stats;
29,395✔
627
  }
628

629
  /**
630
   * The current {@apilink Scene} being drawn and updated on screen
631
   */
632
  public get currentScene(): Scene {
633
    return this.director.currentScene;
7,845✔
634
  }
635

636
  /**
637
   * The current {@apilink Scene} being drawn and updated on screen
638
   */
639
  public get currentSceneName(): string {
640
    return this.director.currentSceneName;
34✔
641
  }
642

643
  /**
644
   * The default {@apilink Scene} of the game, use {@apilink Engine.goToScene} to transition to different scenes.
645
   */
646
  public get rootScene(): Scene {
647
    return this.director.rootScene;
1✔
648
  }
649

650
  /**
651
   * Contains all the scenes currently registered with Excalibur
652
   */
653
  public get scenes(): { [key: string]: Scene | SceneConstructor | SceneWithOptions } {
654
    return this.director.scenes;
4✔
655
  }
656

657
  /**
658
   * Indicates whether the engine is set to fullscreen or not
659
   */
660
  public get isFullscreen(): boolean {
661
    return this.screen.isFullScreen;
1✔
662
  }
663

664
  /**
665
   * Indicates the current {@apilink DisplayMode} of the engine.
666
   */
667
  public get displayMode(): DisplayMode {
UNCOV
668
    return this.screen.displayMode;
×
669
  }
670

671
  private _suppressPlayButton: boolean = false;
746✔
672
  /**
673
   * Returns the calculated pixel ration for use in rendering
674
   */
675
  public get pixelRatio(): number {
676
    return this.screen.pixelRatio;
1,673✔
677
  }
678

679
  /**
680
   * Indicates whether audio should be paused when the game is no longer visible.
681
   */
682
  public pauseAudioWhenHidden: boolean = true;
746✔
683

684
  /**
685
   * Indicates whether the engine should draw with debug information
686
   */
687
  private _isDebug: boolean = false;
746✔
688
  public get isDebug(): boolean {
689
    return this._isDebug;
3,887✔
690
  }
691

692
  /**
693
   * Sets the background color for the engine.
694
   */
695
  public backgroundColor: Color;
696

697
  /**
698
   * Sets the Transparency for the engine.
699
   */
700
  public enableCanvasTransparency: boolean = true;
746✔
701

702
  /**
703
   * Hints the graphics context to truncate fractional world space coordinates
704
   */
705
  public get snapToPixel(): boolean {
706
    return this.graphicsContext.snapToPixel;
2✔
707
  }
708

709
  public set snapToPixel(shouldSnapToPixel: boolean) {
710
    this.graphicsContext.snapToPixel = shouldSnapToPixel;
1✔
711
  }
712

713
  /**
714
   * The action to take when a fatal exception is thrown
715
   */
716
  public onFatalException = (e: any) => {
746✔
UNCOV
717
    Logger.getInstance().fatal(e, e.stack);
×
718
  };
719

720
  /**
721
   * The mouse wheel scroll prevention mode
722
   */
723
  public pageScrollPreventionMode: ScrollPreventionMode;
724

725
  private _logger: Logger;
726

727
  private _toaster: Toaster = new Toaster();
746✔
728

729
  // this determines whether excalibur is compatible with your browser
730
  private _compatible: boolean;
731

732
  private _timescale: number = 1.0;
746✔
733

734
  // loading
735
  private _loader: DefaultLoader;
736

737
  private _isInitialized: boolean = false;
746✔
738

739
  private _hasCreatedCanvas: boolean = false;
746✔
740

741
  public emit<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, event: EngineEvents[TEventName]): void;
742
  public emit(eventName: string, event?: any): void;
743
  public emit<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, event?: any): void {
744
    this.events.emit(eventName, event);
9,155✔
745
  }
746

747
  public on<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): Subscription;
748
  public on(eventName: string, handler: Handler<unknown>): Subscription;
749
  public on<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
750
    return this.events.on(eventName, handler);
29✔
751
  }
752

753
  public once<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): Subscription;
754
  public once(eventName: string, handler: Handler<unknown>): Subscription;
755
  public once<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
756
    return this.events.once(eventName, handler);
10✔
757
  }
758

759
  public off<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): void;
760
  public off(eventName: string, handler: Handler<unknown>): void;
761
  public off(eventName: string): void;
762
  public off<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler?: Handler<any>): void {
UNCOV
763
    this.events.off(eventName, handler);
×
764
  }
765

766
  /**
767
   * Default {@apilink EngineOptions}
768
   */
769
  private static _DEFAULT_ENGINE_OPTIONS: EngineOptions = {
770
    width: 0,
771
    height: 0,
772
    enableCanvasTransparency: true,
773
    useDrawSorting: true,
774
    configurePerformanceCanvas2DFallback: {
775
      allow: false,
776
      showPlayerMessage: false,
777
      threshold: { fps: 20, numberOfFrames: 100 }
778
    },
779
    canvasElementId: '',
780
    canvasElement: undefined,
781
    enableCanvasContextMenu: false,
782
    snapToPixel: false,
783
    antialiasing: true,
784
    pixelArt: false,
785
    garbageCollection: true,
786
    powerPreference: 'high-performance',
787
    pointerScope: PointerScope.Canvas,
788
    suppressConsoleBootMessage: null,
789
    suppressMinimumBrowserFeatureDetection: null,
790
    suppressHiDPIScaling: null,
791
    suppressPlayButton: null,
792
    grabWindowFocus: true,
793
    scrollPreventionMode: ScrollPreventionMode.Canvas,
794
    backgroundColor: Color.fromHex('#2185d0') // Excalibur blue
795
  };
796

797
  private _originalOptions: EngineOptions = {};
746✔
798
  public readonly _originalDisplayMode: DisplayMode;
799

800
  /**
801
   * Creates a new game using the given {@apilink EngineOptions}. By default, if no options are provided,
802
   * the game will be rendered full screen (taking up all available browser window space).
803
   * You can customize the game rendering through {@apilink EngineOptions}.
804
   *
805
   * Example:
806
   *
807
   * ```js
808
   * var game = new ex.Engine({
809
   *   width: 0, // the width of the canvas
810
   *   height: 0, // the height of the canvas
811
   *   enableCanvasTransparency: true, // the transparencySection of the canvas
812
   *   canvasElementId: '', // the DOM canvas element ID, if you are providing your own
813
   *   displayMode: ex.DisplayMode.FullScreen, // the display mode
814
   *   pointerScope: ex.PointerScope.Document, // the scope of capturing pointer (mouse/touch) events
815
   *   backgroundColor: ex.Color.fromHex('#2185d0') // background color of the engine
816
   * });
817
   *
818
   * // call game.start, which is a Promise
819
   * game.start().then(function () {
820
   *   // ready, set, go!
821
   * });
822
   * ```
823
   */
824
  constructor(options?: EngineOptions<TKnownScenes>) {
825
    options = { ...Engine._DEFAULT_ENGINE_OPTIONS, ...options };
746✔
826
    this._originalOptions = options;
746✔
827

828
    Flags.freeze();
746✔
829

830
    if (options.plugins && options.plugins.length > 0) {
746!
NEW
UNCOV
831
      this._plugins = [...options.plugins];
×
832
    }
833

834
    for (let plugin of this._plugins) {
746✔
NEW
835
      plugin.onEnginePreConfig(this, options);
×
836
    }
837

838
    // Initialize browser events facade
839
    this.browser = new BrowserEvents(window, document);
746✔
840

841
    // Check compatibility
842
    const detector = new Detector();
746✔
843
    if (!options.suppressMinimumBrowserFeatureDetection && !(this._compatible = detector.test())) {
746!
UNCOV
844
      const message = document.createElement('div');
×
UNCOV
845
      message.innerText = 'Sorry, your browser does not support all the features needed for Excalibur';
×
UNCOV
846
      document.body.appendChild(message);
×
847

848
      detector.failedTests.forEach(function (test) {
×
849
        const testMessage = document.createElement('div');
×
850
        testMessage.innerText = 'Browser feature missing ' + test;
×
UNCOV
851
        document.body.appendChild(testMessage);
×
852
      });
853

854
      if (options.canvasElementId) {
×
855
        const canvas = document.getElementById(options.canvasElementId);
×
UNCOV
856
        if (canvas) {
×
UNCOV
857
          canvas.parentElement.removeChild(canvas);
×
858
        }
859
      }
860

861
      return;
×
862
    } else {
863
      this._compatible = true;
746✔
864
    }
865

866
    // Use native console API for color fun
867
    // eslint-disable-next-line no-console
868
    if (console.log && !options.suppressConsoleBootMessage) {
746✔
869
      // eslint-disable-next-line no-console
870
      console.log(
2✔
871
        `%cPowered by Excalibur.js (v${EX_VERSION})`,
872
        'background: #176BAA; color: white; border-radius: 5px; padding: 15px; font-size: 1.5em; line-height: 80px;'
873
      );
874
      // eslint-disable-next-line no-console
875
      console.log(
2✔
876
        '\n\
877
      /| ________________\n\
878
O|===|* >________________>\n\
879
      \\|'
880
      );
881
      // eslint-disable-next-line no-console
882
      console.log('Visit', 'http://excaliburjs.com', 'for more information');
2✔
883
    }
884

885
    // Suppress play button
886
    if (options.suppressPlayButton) {
746✔
887
      this._suppressPlayButton = true;
734✔
888
    }
889

890
    this._logger = Logger.getInstance();
746✔
891

892
    this.debug = new DebugConfig(this);
746✔
893

894
    // If debug is enabled, let's log browser features to the console.
895
    if (this._logger.defaultLevel === LogLevel.Debug) {
746!
UNCOV
896
      detector.logBrowserFeatures();
×
897
    }
898

899
    this._logger.debug('Building engine...');
746✔
900
    if (options.garbageCollection === true) {
746!
901
      this.garbageCollectorConfig = {
746✔
902
        ...DefaultGarbageCollectionOptions
903
      };
UNCOV
904
    } else if (options.garbageCollection === false) {
×
UNCOV
905
      this._logger.warn(
×
906
        'WebGL Garbage Collection Disabled!!! If you leak any images over time your game will crash when GPU memory is exhausted'
907
      );
908
      this.garbageCollectorConfig = null;
×
909
    } else {
UNCOV
910
      this.garbageCollectorConfig = {
×
911
        ...DefaultGarbageCollectionOptions,
912
        ...options.garbageCollection
913
      };
914
    }
915
    this._garbageCollector = new GarbageCollector({ getTimestamp: Date.now });
746✔
916

917
    this.canvasElementId = options.canvasElementId;
746✔
918

919
    if (options.canvasElementId) {
746!
UNCOV
920
      this._logger.debug('Using Canvas element specified: ' + options.canvasElementId);
×
921

922
      //test for existence of element
UNCOV
923
      if (document.getElementById(options.canvasElementId) === null) {
×
924
        throw new Error('Cannot find existing element in the DOM, please ensure element is created prior to engine creation.');
×
925
      }
926

927
      this.canvas = <HTMLCanvasElement>document.getElementById(options.canvasElementId);
×
928
      this._hasCreatedCanvas = false;
×
929
    } else if (options.canvasElement) {
746!
UNCOV
930
      this._logger.debug('Using Canvas element specified:', options.canvasElement);
×
931
      this.canvas = options.canvasElement;
×
932
      this._hasCreatedCanvas = false;
×
933
    } else {
934
      this._logger.debug('Using generated canvas element');
746✔
935
      this.canvas = <HTMLCanvasElement>document.createElement('canvas');
746✔
936
      this._hasCreatedCanvas = true;
746✔
937
    }
938

939
    if (this.canvas && !options.enableCanvasContextMenu) {
746✔
940
      this.canvas.addEventListener('contextmenu', (evt) => {
745✔
941
        evt.preventDefault();
1✔
942
      });
943
    }
944

945
    let displayMode = options.displayMode ?? DisplayMode.Fixed;
746✔
946
    if ((options.width && options.height) || options.viewport) {
746✔
947
      if (options.displayMode === undefined) {
741✔
948
        displayMode = DisplayMode.Fixed;
6✔
949
      }
950
      this._logger.debug('Engine viewport is size ' + options.width + ' x ' + options.height);
741✔
951
    } else if (!options.displayMode) {
5✔
952
      this._logger.debug('Engine viewport is fit');
2✔
953
      displayMode = DisplayMode.FitScreen;
2✔
954
    }
955

956
    const global = (options.global && typeof options.global === 'function' ? options.global() : options.global) as GlobalEventHandlers;
746!
957

958
    this.global = global ?? getDefaultGlobal();
746!
959
    this.grabWindowFocus = options.grabWindowFocus;
746✔
960
    this.pointerScope = options.pointerScope;
746✔
961

962
    this._originalDisplayMode = displayMode;
746✔
963

964
    let pixelArtSampler: boolean;
965
    let uvPadding: number;
966
    let nativeContextAntialiasing: boolean;
967
    let canvasImageRendering: 'pixelated' | 'auto';
968
    let filtering: ImageFiltering;
969
    let multiSampleAntialiasing: boolean | { samples: number };
970
    if (typeof options.antialiasing === 'object') {
746!
UNCOV
971
      ({ pixelArtSampler, nativeContextAntialiasing, multiSampleAntialiasing, filtering, canvasImageRendering } = {
×
972
        ...(options.pixelArt ? DefaultPixelArtOptions : DefaultAntialiasOptions),
×
973
        ...options.antialiasing
974
      });
975
    } else {
976
      pixelArtSampler = !!options.pixelArt;
746✔
977
      nativeContextAntialiasing = false;
746✔
978
      multiSampleAntialiasing = options.antialiasing;
746✔
979
      canvasImageRendering = options.antialiasing ? 'auto' : 'pixelated';
746✔
980
      filtering = options.antialiasing ? ImageFiltering.Blended : ImageFiltering.Pixel;
746✔
981
    }
982

983
    if (nativeContextAntialiasing && multiSampleAntialiasing) {
746!
UNCOV
984
      this._logger.warnOnce(
×
985
        `Cannot use antialias setting nativeContextAntialiasing and multiSampleAntialiasing` +
986
          ` at the same time, they are incompatible settings. If you aren\'t sure use multiSampleAntialiasing`
987
      );
988
    }
989

990
    if (options.pixelArt) {
746✔
991
      uvPadding = 0.25;
1✔
992
    }
993

994
    if (!options.antialiasing || filtering === ImageFiltering.Pixel) {
746✔
995
      uvPadding = 0;
737✔
996
    }
997

998
    // Override with any user option, if non default to .25 for pixel art, 0.01 for everything else
999
    uvPadding = options.uvPadding ?? uvPadding ?? 0.01;
746!
1000

1001
    // Canvas 2D fallback can be flagged on
1002
    let useCanvasGraphicsContext = Flags.isEnabled('use-canvas-context');
746✔
1003
    if (!useCanvasGraphicsContext) {
746✔
1004
      // Attempt webgl first
1005
      try {
744✔
1006
        let onGraphicsPreConfig: (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => void;
1007
        let onGraphicsPostConfig: (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => void;
1008
        let onGraphicsPreInitialize: (context: ExcaliburGraphicsContext) => void;
1009
        let onGraphicsPostInitialize: (context: ExcaliburGraphicsContext) => void;
1010
        if (this._plugins.length > 0) {
744!
NEW
1011
          onGraphicsPreConfig = (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => {
×
NEW
1012
            for (let plugin of this._plugins) {
×
NEW
1013
              plugin.onGraphicsPreConfig(context, options);
×
1014
            }
1015
          };
1016

NEW
1017
          onGraphicsPostConfig = (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => {
×
NEW
1018
            for (let plugin of this._plugins) {
×
NEW
1019
              plugin.onGraphicsPostConfig(context, options);
×
1020
            }
1021
          };
1022

NEW
1023
          onGraphicsPreInitialize = (context: ExcaliburGraphicsContext) => {
×
NEW
1024
            for (let plugin of this._plugins) {
×
NEW
1025
              plugin.onGraphicsPreInitialize(context);
×
1026
            }
1027
          };
1028

NEW
1029
          onGraphicsPostInitialize = (context: ExcaliburGraphicsContext) => {
×
NEW
1030
            for (let plugin of this._plugins) {
×
NEW
1031
              plugin.onGraphicsPostInitialize(context);
×
1032
            }
1033
          };
1034
        }
1035

1036
        this.graphicsContext = new ExcaliburGraphicsContextWebGL({
744✔
1037
          canvasElement: this.canvas,
1038
          enableTransparency: this.enableCanvasTransparency,
1039
          pixelArtSampler: pixelArtSampler,
1040
          antialiasing: nativeContextAntialiasing,
1041
          multiSampleAntialiasing: multiSampleAntialiasing,
1042
          uvPadding: uvPadding,
1043
          powerPreference: options.powerPreference,
1044
          backgroundColor: options.backgroundColor,
1045
          snapToPixel: options.snapToPixel,
1046
          useDrawSorting: options.useDrawSorting,
1047
          garbageCollector: this.garbageCollectorConfig
744!
1048
            ? {
1049
                garbageCollector: this._garbageCollector,
1050
                collectionInterval: this.garbageCollectorConfig.textureCollectInterval
1051
              }
1052
            : null,
1053
          handleContextLost: options.handleContextLost ?? this._handleWebGLContextLost,
744!
1054
          handleContextRestored: options.handleContextRestored,
1055
          onGraphicsPreConfig,
1056
          onGraphicsPostConfig,
1057
          onGraphicsPreInitialize,
1058
          onGraphicsPostInitialize
1059
        });
1060
      } catch (e) {
1061
        this._logger.warn(
×
1062
          `Excalibur could not load webgl for some reason (${(e as Error).message}) and loaded a Canvas 2D fallback. ` +
1063
            `Some features of Excalibur will not work in this mode. \n\n` +
1064
            'Read more about this issue at https://excaliburjs.com/docs/performance'
1065
        );
1066
        // fallback to canvas in case of failure
UNCOV
1067
        useCanvasGraphicsContext = true;
×
1068
      }
1069
    }
1070

1071
    if (useCanvasGraphicsContext) {
746✔
1072
      this.graphicsContext = new ExcaliburGraphicsContext2DCanvas({
2✔
1073
        canvasElement: this.canvas,
1074
        enableTransparency: this.enableCanvasTransparency,
1075
        antialiasing: nativeContextAntialiasing,
1076
        backgroundColor: options.backgroundColor,
1077
        snapToPixel: options.snapToPixel,
1078
        useDrawSorting: options.useDrawSorting
1079
      });
1080
    }
1081

1082
    this.screen = new Screen({
746✔
1083
      canvas: this.canvas,
1084
      context: this.graphicsContext,
1085
      antialiasing: nativeContextAntialiasing,
1086
      canvasImageRendering: canvasImageRendering,
1087
      browser: this.browser,
1088
      viewport: options.viewport ?? (options.width && options.height ? { width: options.width, height: options.height } : Resolution.SVGA),
2,967✔
1089
      resolution: options.resolution,
1090
      displayMode,
1091
      pixelRatio: options.suppressHiDPIScaling ? 1 : (options.pixelRatio ?? null)
759✔
1092
    });
1093

1094
    // TODO REMOVE STATIC!!!
1095
    // Set default filtering based on antialiasing
1096
    TextureLoader.filtering = filtering;
746✔
1097

1098
    if (options.backgroundColor) {
746!
1099
      this.backgroundColor = options.backgroundColor.clone();
746✔
1100
    }
1101

1102
    this.maxFps = options.maxFps ?? this.maxFps;
746!
1103

1104
    this.fixedUpdateTimestep = options.fixedUpdateTimestep ?? this.fixedUpdateTimestep;
746!
1105
    this.fixedUpdateFps = options.fixedUpdateFps ?? this.fixedUpdateFps;
746✔
1106
    this.fixedUpdateTimestep = this.fixedUpdateTimestep || 1000 / this.fixedUpdateFps;
746✔
1107

1108
    this.clock = new StandardClock({
746✔
1109
      maxFps: this.maxFps,
1110
      tick: this._mainloop.bind(this),
UNCOV
1111
      onFatalException: (e) => this.onFatalException(e)
×
1112
    });
1113

1114
    this.enableCanvasTransparency = options.enableCanvasTransparency;
746✔
1115

1116
    if (typeof options.physics === 'boolean') {
746!
UNCOV
1117
      this.physics = {
×
1118
        ...getDefaultPhysicsConfig(),
1119
        enabled: options.physics
1120
      };
1121
    } else {
1122
      this.physics = {
746✔
1123
        ...getDefaultPhysicsConfig()
1124
      };
1125
      mergeDeep(this.physics, options.physics);
746✔
1126
    }
1127

1128
    this.director = new Director(this, options.scenes);
746✔
1129
    this.director.events.pipe(this.events);
746✔
1130

1131
    this._initialize(options);
746✔
1132

1133
    (window as any).___EXCALIBUR_DEVTOOL = this;
746✔
1134
    Engine.InstanceCount++;
746✔
1135

1136
    for (let plugin of this._plugins) {
746✔
NEW
UNCOV
1137
      plugin.onEnginePreConfig(this, options);
×
1138
    }
1139
  }
1140

1141
  private _handleWebGLContextLost = (e: Event) => {
746✔
1142
    e.preventDefault();
734✔
1143
    this.clock.stop();
734✔
1144
    this._logger.fatalOnce('WebGL Graphics Lost', e);
734✔
1145
    const container = document.createElement('div');
734✔
1146
    container.id = 'ex-webgl-graphics-context-lost';
734✔
1147
    container.style.position = 'absolute';
734✔
1148
    container.style.zIndex = '99';
734✔
1149
    container.style.left = '50%';
734✔
1150
    container.style.top = '50%';
734✔
1151
    container.style.display = 'flex';
734✔
1152
    container.style.flexDirection = 'column';
734✔
1153
    container.style.transform = 'translate(-50%, -50%)';
734✔
1154
    container.style.backgroundColor = 'white';
734✔
1155
    container.style.padding = '10px';
734✔
1156
    container.style.borderStyle = 'solid 1px';
734✔
1157

1158
    const div = document.createElement('div');
734✔
1159
    div.innerHTML = `
734✔
1160
      <h1>There was an issue rendering, please refresh the page.</h1>
1161
      <div>
1162
        <p>WebGL Graphics Context Lost</p>
1163

1164
        <button id="ex-webgl-graphics-reload">Refresh Page</button>
1165

1166
        <p>There are a few reasons this might happen:</p>
1167
        <ul>
1168
          <li>Two or more pages are placing a high demand on the GPU</li>
1169
          <li>Another page or operation has stalled the GPU and the browser has decided to reset the GPU</li>
1170
          <li>The computer has multiple GPUs and the user has switched between them</li>
1171
          <li>Graphics driver has crashed or restarted</li>
1172
          <li>Graphics driver was updated</li>
1173
        </ul>
1174
      </div>
1175
    `;
1176
    container.appendChild(div);
734✔
1177
    if (this.canvas?.parentElement) {
734!
1178
      this.canvas.parentElement.appendChild(container);
×
1179
      const button = div.querySelector('#ex-webgl-graphics-reload');
×
1180
      button?.addEventListener('click', () => location.reload());
×
1181
    }
1182
  };
1183

1184
  private _performanceThresholdTriggered = false;
746✔
1185
  private _fpsSamples: number[] = [];
746✔
1186
  private _monitorPerformanceThresholdAndTriggerFallback() {
1187
    const { allow } = this._originalOptions.configurePerformanceCanvas2DFallback;
1,746✔
1188
    let { threshold, showPlayerMessage } = this._originalOptions.configurePerformanceCanvas2DFallback;
1,746✔
1189
    if (threshold === undefined) {
1,746✔
1190
      threshold = Engine._DEFAULT_ENGINE_OPTIONS.configurePerformanceCanvas2DFallback.threshold;
100✔
1191
    }
1192
    if (showPlayerMessage === undefined) {
1,746✔
1193
      showPlayerMessage = Engine._DEFAULT_ENGINE_OPTIONS.configurePerformanceCanvas2DFallback.showPlayerMessage;
100✔
1194
    }
1195
    if (!Flags.isEnabled('use-canvas-context') && allow && this.ready && !this._performanceThresholdTriggered) {
1,746✔
1196
      // Calculate Average fps for last X number of frames after start
1197
      if (this._fpsSamples.length === threshold.numberOfFrames) {
100!
1198
        this._fpsSamples.splice(0, 1);
×
1199
      }
1200
      this._fpsSamples.push(this.clock.fpsSampler.fps);
100✔
1201
      let total = 0;
100✔
1202
      for (let i = 0; i < this._fpsSamples.length; i++) {
100✔
1203
        total += this._fpsSamples[i];
5,050✔
1204
      }
1205
      const average = total / this._fpsSamples.length;
100✔
1206

1207
      if (this._fpsSamples.length === threshold.numberOfFrames) {
100✔
1208
        if (average <= threshold.fps) {
1!
1209
          this._performanceThresholdTriggered = true;
1✔
1210
          this._logger.warn(
1✔
1211
            `Switching to browser 2D Canvas fallback due to performance. Some features of Excalibur will not work in this mode.\n` +
1212
              "this might mean your browser doesn't have webgl enabled or hardware acceleration is unavailable.\n\n" +
1213
              'If in Chrome:\n' +
1214
              '  * Visit Settings > Advanced > System, and ensure "Use Hardware Acceleration" is checked.\n' +
1215
              '  * Visit chrome://flags/#ignore-gpu-blocklist and ensure "Override software rendering list" is "enabled"\n' +
1216
              'If in Firefox, visit about:config\n' +
1217
              '  * Ensure webgl.disabled = false\n' +
1218
              '  * Ensure webgl.force-enabled = true\n' +
1219
              '  * Ensure layers.acceleration.force-enabled = true\n\n' +
1220
              'Read more about this issue at https://excaliburjs.com/docs/performance'
1221
          );
1222

1223
          if (showPlayerMessage) {
1!
1224
            this._toaster.toast(
×
1225
              'Excalibur is encountering performance issues. ' +
1226
                "It's possible that your browser doesn't have hardware acceleration enabled. " +
1227
                'Visit [LINK] for more information and potential solutions.',
1228
              'https://excaliburjs.com/docs/performance'
1229
            );
1230
          }
1231
          this.useCanvas2DFallback();
1✔
1232
          this.emit('fallbackgraphicscontext', this.graphicsContext);
1✔
1233
        }
1234
      }
1235
    }
1236
  }
1237

1238
  /**
1239
   * Switches the engine's graphics context to the 2D Canvas.
1240
   * @warning Some features of Excalibur will not work in this mode.
1241
   */
1242
  public useCanvas2DFallback() {
1243
    // Swap out the canvas
1244
    const newCanvas = this.canvas.cloneNode(false) as HTMLCanvasElement;
2✔
1245
    this.canvas.parentNode.replaceChild(newCanvas, this.canvas);
2✔
1246
    this.canvas = newCanvas;
2✔
1247

1248
    const options = { ...this._originalOptions, antialiasing: this.screen.antialiasing };
2✔
1249
    const displayMode = this._originalDisplayMode;
2✔
1250

1251
    // New graphics context
1252
    this.graphicsContext = new ExcaliburGraphicsContext2DCanvas({
2✔
1253
      canvasElement: this.canvas,
1254
      enableTransparency: this.enableCanvasTransparency,
1255
      antialiasing: options.antialiasing,
1256
      backgroundColor: options.backgroundColor,
1257
      snapToPixel: options.snapToPixel,
1258
      useDrawSorting: options.useDrawSorting
1259
    });
1260

1261
    // Reset screen
1262
    if (this.screen) {
2!
1263
      this.screen.dispose();
2✔
1264
    }
1265

1266
    this.screen = new Screen({
2✔
1267
      canvas: this.canvas,
1268
      context: this.graphicsContext,
1269
      antialiasing: options.antialiasing ?? true,
2!
1270
      browser: this.browser,
1271
      viewport: options.viewport ?? (options.width && options.height ? { width: options.width, height: options.height } : Resolution.SVGA),
8!
1272
      resolution: options.resolution,
1273
      displayMode,
1274
      pixelRatio: options.suppressHiDPIScaling ? 1 : (options.pixelRatio ?? null)
2!
1275
    });
1276
    this.screen.setCurrentCamera(this.currentScene.camera);
2✔
1277

1278
    // Reset pointers
1279
    this.input.pointers.detach();
2✔
1280
    const pointerTarget = options && options.pointerScope === PointerScope.Document ? document : this.canvas;
2!
1281
    this.input.pointers = this.input.pointers.recreate(pointerTarget, this);
2✔
1282
    this.input.pointers.init();
2✔
1283
  }
1284

1285
  private _disposed = false;
746✔
1286
  /**
1287
   * Attempts to completely clean up excalibur resources, including removing the canvas from the dom.
1288
   *
1289
   * To start again you will need to new up an Engine.
1290
   */
1291
  public dispose() {
1292
    if (!this._disposed) {
748✔
1293
      this._disposed = true;
744✔
1294
      this.stop();
744✔
1295
      this._garbageCollector.forceCollectAll();
744✔
1296
      this.input.toggleEnabled(false);
744✔
1297
      if (this._hasCreatedCanvas) {
744!
1298
        this.canvas.parentNode.removeChild(this.canvas);
744✔
1299
      }
1300
      this.canvas = null;
744✔
1301
      this.screen.dispose();
744✔
1302
      this.graphicsContext.dispose();
744✔
1303
      this.graphicsContext = null;
744✔
1304
      Engine.InstanceCount--;
744✔
1305
    }
1306
  }
1307

1308
  public isDisposed() {
1309
    return this._disposed;
1,442✔
1310
  }
1311

1312
  /**
1313
   * Returns a BoundingBox of the top left corner of the screen
1314
   * and the bottom right corner of the screen.
1315
   */
1316
  public getWorldBounds() {
1317
    return this.screen.getWorldBounds();
1✔
1318
  }
1319

1320
  /**
1321
   * Gets the current engine timescale factor (default is 1.0 which is 1:1 time)
1322
   */
1323
  public get timescale() {
1324
    return this._timescale;
1,747✔
1325
  }
1326

1327
  /**
1328
   * Sets the current engine timescale factor. Useful for creating slow-motion effects or fast-forward effects
1329
   * when using time-based movement.
1330
   */
1331
  public set timescale(value: number) {
1332
    if (value < 0) {
2!
UNCOV
1333
      Logger.getInstance().warnOnce('engine.timescale to a value less than 0 are ignored');
×
UNCOV
1334
      return;
×
1335
    }
1336

1337
    this._timescale = value;
2✔
1338
  }
1339

1340
  /**
1341
   * Adds a {@apilink Timer} to the {@apilink currentScene}.
1342
   * @param timer  The timer to add to the {@apilink currentScene}.
1343
   */
1344
  public addTimer(timer: Timer): Timer {
UNCOV
1345
    return this.currentScene.addTimer(timer);
×
1346
  }
1347

1348
  /**
1349
   * Removes a {@apilink Timer} from the {@apilink currentScene}.
1350
   * @param timer  The timer to remove to the {@apilink currentScene}.
1351
   */
1352
  public removeTimer(timer: Timer): Timer {
UNCOV
1353
    return this.currentScene.removeTimer(timer);
×
1354
  }
1355

1356
  /**
1357
   * Adds a {@apilink Scene} to the engine, think of scenes in Excalibur as you
1358
   * would levels or menus.
1359
   * @param key  The name of the scene, must be unique
1360
   * @param scene The scene to add to the engine
1361
   */
1362
  public addScene<TScene extends string>(key: TScene, scene: Scene | SceneConstructor | SceneWithOptions): Engine<TKnownScenes | TScene> {
1363
    this.director.add(key, scene);
419✔
1364
    return this as Engine<TKnownScenes | TScene>;
419✔
1365
  }
1366

1367
  /**
1368
   * Removes a {@apilink Scene} instance from the engine
1369
   * @param scene  The scene to remove
1370
   */
1371
  public removeScene(scene: Scene | SceneConstructor): void;
1372
  /**
1373
   * Removes a scene from the engine by key
1374
   * @param key  The scene key to remove
1375
   */
1376
  public removeScene(key: string): void;
1377
  /**
1378
   * @internal
1379
   */
1380
  public removeScene(entity: any): void {
1381
    this.director.remove(entity);
8✔
1382
  }
1383

1384
  /**
1385
   * Adds a plugin to Excalibur
1386
   */
1387
  public addPlugin(plugin: Plugin): void {
NEW
UNCOV
1388
    this._plugins.push(plugin);
×
1389
  }
1390

1391
  public removePlugin(plugin: Plugin): void {
NEW
UNCOV
1392
    const index = this._plugins.indexOf(plugin);
×
NEW
UNCOV
1393
    if (index > -1) {
×
NEW
UNCOV
1394
      this._plugins.splice(index, 1);
×
1395
    }
1396
  }
1397

1398
  /**
1399
   * Adds a {@apilink Scene} to the engine, think of scenes in Excalibur as you
1400
   * would levels or menus.
1401
   * @param sceneKey  The key of the scene, must be unique
1402
   * @param scene     The scene to add to the engine
1403
   */
1404
  public add(sceneKey: string, scene: Scene | SceneConstructor | SceneWithOptions): void;
1405
  /**
1406
   * Adds a {@apilink Timer} to the {@apilink currentScene}.
1407
   * @param timer  The timer to add to the {@apilink currentScene}.
1408
   */
1409
  public add(timer: Timer): void;
1410
  /**
1411
   * Adds a {@apilink TileMap} to the {@apilink currentScene}, once this is done the TileMap
1412
   * will be drawn and updated.
1413
   */
1414
  public add(tileMap: TileMap): void;
1415
  /**
1416
   * Adds an actor to the {@apilink currentScene} of the game. This is synonymous
1417
   * to calling `engine.currentScene.add(actor)`.
1418
   *
1419
   * Actors can only be drawn if they are a member of a scene, and only
1420
   * the {@apilink currentScene} may be drawn or updated.
1421
   * @param actor  The actor to add to the {@apilink currentScene}
1422
   */
1423
  public add(actor: Actor): void;
1424

1425
  public add(entity: Entity): void;
1426

1427
  /**
1428
   * Adds a {@apilink ScreenElement} to the {@apilink currentScene} of the game,
1429
   * ScreenElements do not participate in collisions, instead the
1430
   * remain in the same place on the screen.
1431
   * @param screenElement  The ScreenElement to add to the {@apilink currentScene}
1432
   */
1433
  public add(screenElement: ScreenElement): void;
1434
  public add(entity: any): void {
1435
    if (arguments.length === 2) {
265✔
1436
      this.director.add(<string>arguments[0], <Scene | SceneConstructor | SceneWithOptions>arguments[1]);
95✔
1437
      return;
95✔
1438
    }
1439
    const maybeDeferred = this.director.getDeferredScene();
170✔
1440
    if (maybeDeferred instanceof Scene) {
170!
UNCOV
1441
      maybeDeferred.add(entity);
×
1442
    } else {
1443
      this.currentScene.add(entity);
170✔
1444
    }
1445
  }
1446

1447
  /**
1448
   * Removes a scene instance from the engine
1449
   * @param scene  The scene to remove
1450
   */
1451
  public remove(scene: Scene | SceneConstructor): void;
1452
  /**
1453
   * Removes a scene from the engine by key
1454
   * @param sceneKey  The scene to remove
1455
   */
1456
  public remove(sceneKey: string): void;
1457
  /**
1458
   * Removes a {@apilink Timer} from the {@apilink currentScene}.
1459
   * @param timer  The timer to remove to the {@apilink currentScene}.
1460
   */
1461
  public remove(timer: Timer): void;
1462
  /**
1463
   * Removes a {@apilink TileMap} from the {@apilink currentScene}, it will no longer be drawn or updated.
1464
   */
1465
  public remove(tileMap: TileMap): void;
1466
  /**
1467
   * Removes an actor from the {@apilink currentScene} of the game. This is synonymous
1468
   * to calling `engine.currentScene.removeChild(actor)`.
1469
   * Actors that are removed from a scene will no longer be drawn or updated.
1470
   * @param actor  The actor to remove from the {@apilink currentScene}.
1471
   */
1472
  public remove(actor: Actor): void;
1473
  /**
1474
   * Removes a {@apilink ScreenElement} to the scene, it will no longer be drawn or updated
1475
   * @param screenElement  The ScreenElement to remove from the {@apilink currentScene}
1476
   */
1477
  public remove(screenElement: ScreenElement): void;
1478
  public remove(entity: any): void {
1479
    if (entity instanceof Entity) {
4✔
1480
      this.currentScene.remove(entity);
2✔
1481
    }
1482

1483
    if (entity instanceof Scene || isSceneConstructor(entity)) {
4✔
1484
      this.removeScene(entity);
1✔
1485
    }
1486

1487
    if (typeof entity === 'string') {
4✔
1488
      this.removeScene(entity);
1✔
1489
    }
1490
  }
1491

1492
  /**
1493
   * Changes the current scene with optionally supplied:
1494
   * * Activation data
1495
   * * Transitions
1496
   * * Loaders
1497
   *
1498
   * Example:
1499
   * ```typescript
1500
   * game.goToScene('myScene', {
1501
   *   sceneActivationData: {any: 'thing at all'},
1502
   *   destinationIn: new FadeInOut({duration: 1000, direction: 'in'}),
1503
   *   sourceOut: new FadeInOut({duration: 1000, direction: 'out'}),
1504
   *   loader: MyLoader
1505
   * });
1506
   * ```
1507
   *
1508
   * Scenes are defined in the Engine constructor
1509
   * ```typescript
1510
   * const engine = new ex.Engine({
1511
      scenes: {...}
1512
    });
1513
   * ```
1514
   * Or by adding dynamically
1515
   *
1516
   * ```typescript
1517
   * engine.addScene('myScene', new ex.Scene());
1518
   * ```
1519
   * @param destinationScene
1520
   * @param options
1521
   */
1522
  public async goToScene<TData = undefined>(destinationScene: WithRoot<TKnownScenes>, options?: GoToOptions<TData>): Promise<void> {
1523
    await this.scope(async () => {
424✔
1524
      await this.director.goToScene(destinationScene, options);
424✔
1525
    });
1526
  }
1527

1528
  /**
1529
   * Transforms the current x, y from screen coordinates to world coordinates
1530
   * @param point  Screen coordinate to convert
1531
   */
1532
  public screenToWorldCoordinates(point: Vector): Vector {
1533
    return this.screen.screenToWorldCoordinates(point);
2✔
1534
  }
1535

1536
  /**
1537
   * Transforms a world coordinate, to a screen coordinate
1538
   * @param point  World coordinate to convert
1539
   */
1540
  public worldToScreenCoordinates(point: Vector): Vector {
1541
    return this.screen.worldToScreenCoordinates(point);
3✔
1542
  }
1543

1544
  /**
1545
   * Initializes the internal canvas, rendering context, display mode, and native event listeners
1546
   */
1547
  private _initialize(options?: EngineOptions) {
1548
    for (let plugin of this._plugins) {
746✔
NEW
UNCOV
1549
      plugin.onEnginePreInitialize(this);
×
1550
    }
1551

1552
    this.pageScrollPreventionMode = options.scrollPreventionMode;
746✔
1553

1554
    // initialize inputs
1555
    const pointerTarget = options && options.pointerScope === PointerScope.Document ? document : this.canvas;
746✔
1556
    const grabWindowFocus = this._originalOptions?.grabWindowFocus ?? true;
746!
1557
    this.input = new InputHost({
746✔
1558
      global: this.global,
1559
      pointerTarget,
1560
      grabWindowFocus,
1561
      engine: this
1562
    });
1563
    this.inputMapper = this.input.inputMapper;
746✔
1564

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

1568
    this.browser.document.on('visibilitychange', () => {
746✔
1569
      if (document.visibilityState === 'hidden') {
20✔
1570
        this.events.emit('hidden', new HiddenEvent(this));
10✔
1571
        this._logger.debug('Window hidden');
10✔
1572
      } else if (document.visibilityState === 'visible') {
10!
1573
        this.events.emit('visible', new VisibleEvent(this));
10✔
1574
        this._logger.debug('Window visible');
10✔
1575
      }
1576
    });
1577

1578
    if (!this.canvasElementId && !options.canvasElement) {
746!
1579
      document.body.appendChild(this.canvas);
746✔
1580
    }
1581

1582
    for (let plugin of this._plugins) {
746✔
NEW
UNCOV
1583
      plugin.onEnginePostConfig(this, options);
×
1584
    }
1585
  }
1586

1587
  public toggleInputEnabled(enabled: boolean) {
UNCOV
1588
    this._inputEnabled = enabled;
×
UNCOV
1589
    this.input.toggleEnabled(this._inputEnabled);
×
1590
  }
1591

1592
  public onInitialize(engine: Engine) {
1593
    // Override me
1594
  }
1595

1596
  /**
1597
   * Gets whether the actor is Initialized
1598
   */
1599
  public get isInitialized(): boolean {
1600
    return this._isInitialized;
579✔
1601
  }
1602

1603
  private async _overrideInitialize(engine: Engine) {
1604
    if (!this.isInitialized) {
579✔
1605
      for (let plugin of this._plugins) {
570✔
NEW
UNCOV
1606
        plugin.onEnginePreInitialize(this);
×
1607
      }
1608

1609
      await this.director.onInitialize();
570✔
1610
      await this.onInitialize(engine);
570✔
1611
      this.events.emit('initialize', new InitializeEvent(engine, this));
570✔
1612
      this._isInitialized = true;
570✔
1613

1614
      for (let plugin of this._plugins) {
570✔
NEW
UNCOV
1615
        plugin.onEnginePostInitialize(this);
×
1616
      }
1617
    }
1618
  }
1619

1620
  /**
1621
   * Updates the entire state of the game
1622
   * @param elapsed  Number of milliseconds elapsed since the last update.
1623
   */
1624
  private _update(elapsed: number) {
1625
    if (this._isLoading) {
1,744✔
1626
      // suspend updates until loading is finished
1627
      this._loader?.onUpdate(this, elapsed);
829!
1628
      // Update input listeners
1629
      this.input.update();
829✔
1630
      return;
829✔
1631
    }
1632

1633
    // Publish preupdate events
1634
    this.clock.__runScheduledCbs('preupdate');
915✔
1635
    this._preupdate(elapsed);
915✔
1636

1637
    // process engine level events
1638
    this.currentScene.update(this, elapsed);
915✔
1639

1640
    // Update graphics postprocessors
1641
    this.graphicsContext.updatePostProcessors(elapsed);
915✔
1642

1643
    // Publish update event
1644
    this.clock.__runScheduledCbs('postupdate');
915✔
1645
    this._postupdate(elapsed);
915✔
1646

1647
    // Update input listeners
1648
    this.input.update();
915✔
1649
  }
1650

1651
  /**
1652
   * @internal
1653
   */
1654
  public _preupdate(elapsed: number) {
1655
    this.emit('preupdate', new PreUpdateEvent(this, elapsed, this));
915✔
1656
    this.onPreUpdate(this, elapsed);
915✔
1657
  }
1658

1659
  /**
1660
   * Safe to override method
1661
   * @param engine The reference to the current game engine
1662
   * @param elapsed  The time elapsed since the last update in milliseconds
1663
   */
1664
  public onPreUpdate(engine: Engine, elapsed: number) {
1665
    // Override me
1666
  }
1667

1668
  /**
1669
   * @internal
1670
   */
1671
  public _postupdate(elapsed: number) {
1672
    this.emit('postupdate', new PostUpdateEvent(this, elapsed, this));
915✔
1673
    this.onPostUpdate(this, elapsed);
915✔
1674
  }
1675

1676
  /**
1677
   * Safe to override method
1678
   * @param engine The reference to the current game engine
1679
   * @param elapsed  The time elapsed since the last update in milliseconds
1680
   */
1681
  public onPostUpdate(engine: Engine, elapsed: number) {
1682
    // Override me
1683
  }
1684

1685
  /**
1686
   * Draws the entire game
1687
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1688
   */
1689
  private _draw(elapsed: number) {
1690
    // Use scene background color if present, fallback to engine
1691
    this.graphicsContext.backgroundColor = this.currentScene.backgroundColor ?? this.backgroundColor;
1,747✔
1692
    this.graphicsContext.beginDrawLifecycle();
1,747✔
1693
    this.graphicsContext.clear();
1,747✔
1694
    this.clock.__runScheduledCbs('predraw');
1,747✔
1695
    this._predraw(this.graphicsContext, elapsed);
1,747✔
1696

1697
    // Drawing nothing else while loading
1698
    if (this._isLoading) {
1,747✔
1699
      if (!this._hideLoader) {
829!
1700
        this._loader?.canvas.draw(this.graphicsContext, 0, 0);
829!
1701
        this.clock.__runScheduledCbs('postdraw');
829✔
1702
        this.graphicsContext.flush();
829✔
1703
        this.graphicsContext.endDrawLifecycle();
829✔
1704
      }
1705
      return;
829✔
1706
    }
1707

1708
    this.currentScene.draw(this.graphicsContext, elapsed);
918✔
1709

1710
    this.clock.__runScheduledCbs('postdraw');
918✔
1711
    this._postdraw(this.graphicsContext, elapsed);
918✔
1712

1713
    // Flush any pending drawings
1714
    this.graphicsContext.flush();
918✔
1715
    this.graphicsContext.endDrawLifecycle();
918✔
1716

1717
    this._checkForScreenShots();
918✔
1718
  }
1719

1720
  /**
1721
   * @internal
1722
   */
1723
  public _predraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1724
    this.emit('predraw', new PreDrawEvent(ctx, elapsed, this));
1,747✔
1725
    this.onPreDraw(ctx, elapsed);
1,747✔
1726
  }
1727

1728
  /**
1729
   * Safe to override method to hook into pre draw
1730
   * @param ctx {@link ExcaliburGraphicsContext} for drawing
1731
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1732
   */
1733
  public onPreDraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1734
    // Override me
1735
  }
1736

1737
  /**
1738
   * @internal
1739
   */
1740
  public _postdraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1741
    this.emit('postdraw', new PostDrawEvent(ctx, elapsed, this));
918✔
1742
    this.onPostDraw(ctx, elapsed);
918✔
1743
  }
1744

1745
  /**
1746
   * Safe to override method to hook into pre draw
1747
   * @param ctx {@link ExcaliburGraphicsContext} for drawing
1748
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1749
   */
1750
  public onPostDraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1751
    // Override me
1752
  }
1753

1754
  /**
1755
   * Enable or disable Excalibur debugging functionality.
1756
   * @param toggle a value that debug drawing will be changed to
1757
   */
1758
  public showDebug(toggle: boolean): void {
1759
    this._isDebug = toggle;
2✔
1760
  }
1761

1762
  /**
1763
   * Toggle Excalibur debugging functionality.
1764
   */
1765
  public toggleDebug(): boolean {
1766
    this._isDebug = !this._isDebug;
14✔
1767
    return this._isDebug;
14✔
1768
  }
1769

1770
  /**
1771
   * Returns true when loading is totally complete and the player has clicked start
1772
   */
1773
  public get loadingComplete() {
1774
    return !this._isLoading;
734✔
1775
  }
1776

1777
  private _isLoading = false;
746✔
1778
  private _hideLoader = false;
746✔
1779
  private _isReadyFuture = new Future<void>();
746✔
1780
  public get ready() {
1781
    return this._isReadyFuture.isCompleted;
100✔
1782
  }
1783
  public isReady(): Promise<void> {
1784
    return this._isReadyFuture.promise;
14✔
1785
  }
1786

1787
  /**
1788
   * Starts the internal game loop for Excalibur after loading
1789
   * any provided assets.
1790
   * @param loader  Optional {@apilink Loader} to use to load resources. The default loader is {@apilink Loader},
1791
   * override to provide your own custom loader.
1792
   *
1793
   * Note: start() only resolves AFTER the user has clicked the play button
1794
   */
1795
  public async start(loader?: DefaultLoader): Promise<void>;
1796
  /**
1797
   * Starts the internal game loop for Excalibur after configuring any routes, loaders, or transitions
1798
   * @param startOptions Optional {@apilink StartOptions} to configure the routes for scenes in Excalibur
1799
   *
1800
   * Note: start() only resolves AFTER the user has clicked the play button
1801
   */
1802
  public async start(sceneName: WithRoot<TKnownScenes>, options?: StartOptions): Promise<void>;
1803
  /**
1804
   * Starts the internal game loop after any loader is finished
1805
   * @param loader
1806
   */
1807
  public async start(loader?: DefaultLoader): Promise<void>;
1808
  public async start(sceneNameOrLoader?: WithRoot<TKnownScenes> | DefaultLoader, options?: StartOptions): Promise<void> {
1809
    await this.scope(async () => {
578✔
1810
      if (!this._compatible) {
578!
UNCOV
1811
        throw new Error('Excalibur is incompatible with your browser');
×
1812
      }
1813
      this._isLoading = true;
578✔
1814
      let loader: DefaultLoader;
1815
      if (sceneNameOrLoader instanceof DefaultLoader) {
578✔
1816
        loader = sceneNameOrLoader;
15✔
1817
      } else if (typeof sceneNameOrLoader === 'string') {
563✔
1818
        this.director.configureStart(sceneNameOrLoader, options);
1✔
1819
        loader = this.director.mainLoader;
1✔
1820
      }
1821

1822
      // Start the excalibur clock which drives the mainloop
1823
      this._logger.debug('Starting game clock...');
578✔
1824
      this.browser.resume();
578✔
1825
      this.clock.start();
578✔
1826
      if (this.garbageCollectorConfig) {
578!
1827
        this._garbageCollector.start();
578✔
1828
      }
1829
      this._logger.debug('Game clock started');
578✔
1830

1831
      await this.load(loader ?? new Loader());
578✔
1832

1833
      // Initialize before ready
1834
      await this._overrideInitialize(this);
577✔
1835

1836
      this._isReadyFuture.resolve();
577✔
1837
      this.emit('start', new GameStartEvent(this));
577✔
1838
      return this._isReadyFuture.promise;
577✔
1839
    });
1840
  }
1841

1842
  /**
1843
   * Returns the current frames elapsed milliseconds
1844
   */
1845
  public currentFrameElapsedMs = 0;
746✔
1846

1847
  /**
1848
   * Returns the current frame lag when in fixed update mode
1849
   */
1850
  public currentFrameLagMs = 0;
746✔
1851

1852
  private _lagMs = 0;
746✔
1853
  private _mainloop(elapsed: number) {
1854
    this.scope(() => {
1,746✔
1855
      this.emit('preframe', new PreFrameEvent(this, this.stats.prevFrame));
1,746✔
1856
      const elapsedMs = elapsed * this.timescale;
1,746✔
1857
      this.currentFrameElapsedMs = elapsedMs;
1,746✔
1858

1859
      // reset frame stats (reuse existing instances)
1860
      const frameId = this.stats.prevFrame.id + 1;
1,746✔
1861
      this.stats.currFrame.reset();
1,746✔
1862
      this.stats.currFrame.id = frameId;
1,746✔
1863
      this.stats.currFrame.elapsedMs = elapsedMs;
1,746✔
1864
      this.stats.currFrame.fps = this.clock.fpsSampler.fps;
1,746✔
1865
      GraphicsDiagnostics.clear();
1,746✔
1866

1867
      const beforeUpdate = this.clock.now();
1,746✔
1868
      const fixedTimestepMs = this.fixedUpdateTimestep;
1,746✔
1869
      if (this.fixedUpdateTimestep) {
1,746✔
1870
        this._lagMs += elapsedMs;
44✔
1871
        while (this._lagMs >= fixedTimestepMs) {
44✔
1872
          this._update(fixedTimestepMs);
42✔
1873
          this._lagMs -= fixedTimestepMs;
42✔
1874
        }
1875
      } else {
1876
        this._update(elapsedMs);
1,702✔
1877
      }
1878
      const afterUpdate = this.clock.now();
1,746✔
1879
      this.currentFrameLagMs = this._lagMs;
1,746✔
1880
      this._draw(elapsedMs);
1,746✔
1881
      const afterDraw = this.clock.now();
1,746✔
1882

1883
      this.stats.currFrame.duration.update = afterUpdate - beforeUpdate;
1,746✔
1884
      this.stats.currFrame.duration.draw = afterDraw - afterUpdate;
1,746✔
1885
      this.stats.currFrame.graphics.drawnImages = GraphicsDiagnostics.DrawnImagesCount;
1,746✔
1886
      this.stats.currFrame.graphics.drawCalls = GraphicsDiagnostics.DrawCallCount;
1,746✔
1887
      this.stats.currFrame.graphics.rendererSwaps = GraphicsDiagnostics.RendererSwaps;
1,746✔
1888

1889
      this.emit('postframe', new PostFrameEvent(this, this.stats.currFrame));
1,746✔
1890
      this.stats.prevFrame.reset(this.stats.currFrame);
1,746✔
1891

1892
      this._monitorPerformanceThresholdAndTriggerFallback();
1,746✔
1893
    });
1894
  }
1895

1896
  /**
1897
   * Stops Excalibur's main loop, useful for pausing the game.
1898
   */
1899
  public stop() {
1900
    if (this.clock.isRunning()) {
1,491✔
1901
      this.emit('stop', new GameStopEvent(this));
587✔
1902
      this.browser.pause();
587✔
1903
      this.clock.stop();
587✔
1904
      this._garbageCollector.stop();
587✔
1905
      this._logger.debug('Game stopped');
587✔
1906
    }
1907
  }
1908

1909
  /**
1910
   * Returns the Engine's running status, Useful for checking whether engine is running or paused.
1911
   */
1912
  public isRunning() {
1913
    return this.clock.isRunning();
3✔
1914
  }
1915

1916
  private _screenShotRequests: { preserveHiDPIResolution: boolean; resolve: (image: HTMLImageElement) => void }[] = [];
746✔
1917
  /**
1918
   * Takes a screen shot of the current viewport and returns it as an
1919
   * HTML Image Element.
1920
   * @param preserveHiDPIResolution in the case of HiDPI return the full scaled backing image, by default false
1921
   */
1922
  public screenshot(preserveHiDPIResolution = false): Promise<HTMLImageElement> {
3✔
1923
    const screenShotPromise = new Promise<HTMLImageElement>((resolve) => {
9✔
1924
      this._screenShotRequests.push({ preserveHiDPIResolution, resolve });
9✔
1925
    });
1926
    return screenShotPromise;
9✔
1927
  }
1928

1929
  private _checkForScreenShots() {
1930
    // We must grab the draw buffer before we yield to the browser
1931
    // the draw buffer is cleared after compositing
1932
    // the reason for the asynchrony is setting `preserveDrawingBuffer: true`
1933
    // forces the browser to copy buffers which can have a mass perf impact on mobile
1934
    for (const request of this._screenShotRequests) {
918✔
1935
      const finalWidth = request.preserveHiDPIResolution ? this.canvas.width : this.screen.resolution.width;
9✔
1936
      const finalHeight = request.preserveHiDPIResolution ? this.canvas.height : this.screen.resolution.height;
9✔
1937
      const screenshot = document.createElement('canvas');
9✔
1938
      screenshot.width = finalWidth;
9✔
1939
      screenshot.height = finalHeight;
9✔
1940
      const ctx = screenshot.getContext('2d');
9✔
1941
      ctx.imageSmoothingEnabled = this.screen.antialiasing;
9✔
1942
      ctx.drawImage(this.canvas, 0, 0, finalWidth, finalHeight);
9✔
1943

1944
      const result = new Image();
9✔
1945
      const raw = screenshot.toDataURL('image/png');
9✔
1946
      result.onload = () => {
9✔
1947
        request.resolve(result);
9✔
1948
      };
1949
      result.src = raw;
9✔
1950
    }
1951
    // Reset state
1952
    this._screenShotRequests.length = 0;
918✔
1953
  }
1954

1955
  /**
1956
   * Another option available to you to load resources into the game.
1957
   * Immediately after calling this the game will pause and the loading screen
1958
   * will appear.
1959
   * @param loader  Some {@apilink Loadable} such as a {@apilink Loader} collection, {@apilink Sound}, or {@apilink Texture}.
1960
   */
1961
  public async load(loader: DefaultLoader, hideLoader = false): Promise<void> {
1,020✔
1962
    await this.scope(async () => {
1,020✔
1963
      try {
1,020✔
1964
        // early exit if loaded
1965
        if (loader.isLoaded()) {
1,020✔
1966
          return;
1,002✔
1967
        }
1968
        this._loader = loader;
18✔
1969
        this._isLoading = true;
18✔
1970
        this._hideLoader = hideLoader;
18✔
1971

1972
        if (loader instanceof Loader) {
18✔
1973
          loader.suppressPlayButton = loader.suppressPlayButton || this._suppressPlayButton;
15✔
1974
        }
1975
        this._loader.onInitialize(this);
18✔
1976

1977
        await loader.load();
18✔
1978
      } catch (e) {
1979
        this._logger.error('Error loading resources, things may not behave properly', e);
1✔
1980
        await Promise.resolve();
1✔
1981
      } finally {
1982
        this._isLoading = false;
1,019✔
1983
        this._hideLoader = false;
1,019✔
1984
        this._loader = null;
1,019✔
1985
      }
1986
    });
1987
  }
1988
}
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