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

excaliburjs / Excalibur / 29167882829

11 Jul 2026 08:55PM UTC coverage: 88.941% (+0.04%) from 88.897%
29167882829

Pull #3670

github

web-flow
Merge f00ec2720 into ce4186ff1
Pull Request #3670: feat: Implement Plugin System

7052 of 9238 branches covered (76.34%)

103 of 108 new or added lines in 5 files covered. (95.37%)

1 existing line in 1 file now uncovered.

15618 of 17560 relevant lines covered (88.94%)

25371.14 hits per line

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

91.02
/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();
256✔
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 {
42
  AntialiasOptions,
43
  ExcaliburGraphicsContext,
44
  ExcaliburGraphicsContextOptions,
45
  ExcaliburGraphicsContextWebGLOptions
46
} from './graphics';
47
import {
48
  DefaultAntialiasOptions,
49
  DefaultPixelArtOptions,
50
  ExcaliburGraphicsContext2DCanvas,
51
  ExcaliburGraphicsContextWebGL,
52
  TextureLoader
53
} from './graphics';
54
import type { Clock } from './util/clock';
55
import { StandardClock } from './util/clock';
56
import { ImageFiltering } from './graphics/filtering';
57
import { GraphicsDiagnostics } from './graphics/graphics-diagnostics';
58
import { Toaster } from './util/toaster';
59
import type { InputMapper } from './input/input-mapper';
60
import type { GoToOptions, SceneMap, StartOptions, SceneWithOptions, WithRoot } from './director/director';
61
import { Director, DirectorEvents } from './director/director';
62
import { InputHost } from './input/input-host';
63
import type { PhysicsConfig } from './collision/physics-config';
64
import { getDefaultPhysicsConfig } from './collision/physics-config';
65
import type { DeepRequired } from './util/required';
66
import type { Context } from './context';
67
import { createContext, useContext } from './context';
68
import type { GarbageCollectionOptions } from './garbage-collector';
69
import { DefaultGarbageCollectionOptions, GarbageCollector } from './garbage-collector';
70
import { mergeDeep } from './util/util';
71
import { getDefaultGlobal } from './util/iframe';
72
import type { Plugin } from './plugin';
73

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

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

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

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

136
  /**
137
   * Optionally configure the height of the viewport in css pixels
138
   */
139
  height?: number;
140

141
  /**
142
   * Optionally configure the width & height of the viewport in css pixels.
143
   * Use `viewport` instead of {@apilink EngineOptions.width} and {@apilink EngineOptions.height}, or vice versa.
144
   */
145
  viewport?: ViewportDimension;
146

147
  /**
148
   * Optionally specify the size the logical pixel resolution, if not specified it will be width x height.
149
   * See {@apilink Resolution} for common presets.
150
   */
151
  resolution?: Resolution;
152

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

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

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

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

210
  /**
211
   * Optionally hint the graphics context into a specific power profile
212
   *
213
   * Default "high-performance"
214
   */
215
  powerPreference?: 'default' | 'high-performance' | 'low-power';
216

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

228
  /**
229
   * Optionally configure the native canvas transparent backdrop
230
   */
231
  enableCanvasTransparency?: boolean;
232

233
  /**
234
   * Optionally specify the target canvas DOM element to render the game in
235
   */
236
  canvasElementId?: string;
237

238
  /**
239
   * Optionally specify the target canvas DOM element directly
240
   */
241
  canvasElement?: HTMLCanvasElement;
242

243
  /**
244
   * Optionally enable the right click context menu on the canvas
245
   *
246
   * Default if unset is false
247
   */
248
  enableCanvasContextMenu?: boolean;
249

250
  /**
251
   * Optionally snap graphics to nearest pixel, default is false
252
   */
253
  snapToPixel?: boolean;
254

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

261
  /**
262
   * Optionally configure the global, or a factory to produce it to listen to for browser events for Excalibur to listen to
263
   */
264
  global?: GlobalEventHandlers | (() => GlobalEventHandlers);
265

266
  /**
267
   * Configures the pointer scope. Pointers scoped to the 'Canvas' can only fire events within the canvas viewport; whereas, 'Document'
268
   * (default) scoped will fire anywhere on the page.
269
   */
270
  pointerScope?: PointerScope;
271

272
  /**
273
   * Suppress boot up console message, which contains the "powered by Excalibur message"
274
   */
275
  suppressConsoleBootMessage?: boolean;
276

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

284
  /**
285
   * Suppress HiDPI auto detection and scaling, it is not recommended users of excalibur switch off this feature. This feature detects
286
   * and scales the drawing canvas appropriately to accommodate HiDPI screens.
287
   */
288
  suppressHiDPIScaling?: boolean;
289

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

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

305
  /**
306
   * Scroll prevention method.
307
   */
308
  scrollPreventionMode?: ScrollPreventionMode;
309

310
  /**
311
   * Optionally set the background color
312
   */
313
  backgroundColor?: Color;
314

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

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

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

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

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

367
  /**
368
   * Optionally provide a custom handler for the webgl context restored event
369
   */
370
  handleContextRestored?: (e: Event) => void;
371

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

396
  /**
397
   * Optionally configure the physics simulation in excalibur
398
   *
399
   * If false, Excalibur will not produce a physics simulation.
400
   *
401
   * Default is configured to use {@apilink SolverStrategy.Arcade} physics simulation
402
   */
403
  physics?: boolean | PhysicsConfig;
404

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

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

425
    if (!value) {
20✔
426
      throw new Error('Cannot inject engine with `useEngine()`, `useEngine()` was called outside of Engine lifecycle scope.');
1✔
427
    }
428

429
    return value;
19✔
430
  }
431
  static InstanceCount = 0;
256✔
432

433
  /**
434
   * Anything run under scope can use `useEngine()` to inject the current engine
435
   * @param cb
436
   */
437
  scope = <TReturn>(cb: () => TReturn) => Engine.Context.scope(this, cb);
3,850✔
438

439
  public global!: GlobalEventHandlers;
440

441
  private _plugins: Plugin[] = [];
803✔
442
  public get plugins(): readonly Plugin[] {
443
    return this._plugins;
3,715✔
444
  }
445

446
  /**
447
   * Check whether a plugin with the given name is currently installed.
448
   *
449
   * @param name The unique plugin name to check
450
   * @returns `true` if a plugin with that name is installed
451
   */
452
  public hasPlugin(name: string): boolean {
453
    return this._plugins.some((p) => p.name === name);
5✔
454
  }
455

456
  /**
457
   * Add a plugin to the engine after construction.
458
   *
459
   * If the engine has already passed certain lifecycle stages, the plugin's hooks
460
   * for those stages will be called immediately so the plugin can "catch up".
461
   *
462
   * If a plugin with the same {@apilink Plugin.name} is already installed, a warning
463
   * is logged and the plugin is not added.
464
   *
465
   * @param plugin The plugin to add
466
   * @returns `true` if the plugin was added, `false` if it was skipped (duplicate name)
467
   */
468
  public addPlugin(plugin: Plugin): boolean {
469
    if (this._plugins.some((p) => p.name === plugin.name)) {
6✔
470
      this._logger.warn(`Plugin with name "${plugin.name}" is already installed, skipping duplicate.`);
1✔
471
      return false;
1✔
472
    }
473

474
    // Insert maintaining priority sort order
475
    const insertIndex = this._plugins.findIndex((p) => p.priority > plugin.priority);
5✔
476
    if (insertIndex === -1) {
5✔
477
      this._plugins.push(plugin);
4✔
478
    } else {
479
      this._plugins.splice(insertIndex, 0, plugin);
1✔
480
    }
481

482
    // Catch up on lifecycle stages the engine has already passed
483
    // Engine config hooks
484
    plugin.onEnginePreConfig?.(this, this._originalOptions);
5!
485
    plugin.onEnginePostConfig?.(this, this._originalOptions);
5!
486

487
    // Graphics hooks - if the graphics context already exists
488
    if (this.graphicsContext) {
5!
489
      const graphicsOptions = this._originalOptions as unknown as ExcaliburGraphicsContextOptions;
5✔
490
      plugin.onGraphicsPreConfig?.(this.graphicsContext, graphicsOptions);
5!
491
      plugin.onGraphicsPostConfig?.(this.graphicsContext, graphicsOptions);
5!
492
      plugin.onGraphicsPreInitialize?.(this.graphicsContext);
5!
493
      plugin.onGraphicsPostInitialize?.(this.graphicsContext);
5!
494
    }
495

496
    // Engine initialization hooks - if the engine is already initialized
497
    if (this.isInitialized) {
5✔
498
      plugin.onEnginePreInitialize?.(this);
1!
499
      plugin.onEnginePostInitialize?.(this);
1!
500
    }
501

502
    return true;
5✔
503
  }
504

505
  /**
506
   * Remove a plugin from the engine by name.
507
   *
508
   * The plugin's {@apilink Plugin.dispose} method will be called if present.
509
   *
510
   * @param name The unique plugin name to remove
511
   * @returns `true` if the plugin was found and removed
512
   */
513
  public removePlugin(name: string): boolean {
514
    const index = this._plugins.findIndex((p) => p.name === name);
3✔
515
    if (index === -1) {
3✔
516
      return false;
1✔
517
    }
518
    const [removed] = this._plugins.splice(index, 1);
2✔
519
    removed?.dispose?.();
2!
520
    return true;
2✔
521
  }
522

523
  private _garbageCollector!: GarbageCollector;
524

525
  public readonly garbageCollectorConfig!: GarbageCollectionOptions | null;
526

527
  /**
528
   * Current Excalibur version string
529
   *
530
   * Useful for plugins or other tools that need to know what features are available
531
   */
532
  public readonly version = EX_VERSION;
803✔
533

534
  /**
535
   * Listen to and emit events on the Engine
536
   */
537
  public events = new EventEmitter<EngineEvents>();
803✔
538

539
  /**
540
   * Excalibur browser events abstraction used for wiring to native browser events safely
541
   */
542
  public browser: BrowserEvents;
543

544
  /**
545
   * Screen abstraction
546
   */
547
  public screen!: Screen;
548

549
  /**
550
   * Scene director, manages all scenes, scene transitions, and loaders in excalibur
551
   */
552
  public director!: Director<TKnownScenes>;
553

554
  /**
555
   * Direct access to the engine's canvas element
556
   */
557
  public canvas!: HTMLCanvasElement;
558

559
  /**
560
   * Direct access to the ExcaliburGraphicsContext used for drawing things to the screen
561
   */
562
  public graphicsContext!: ExcaliburGraphicsContext;
563

564
  /**
565
   * Direct access to the canvas element ID, if an ID exists
566
   */
567
  public canvasElementId?: string;
568

569
  /**
570
   * Direct access to the physics configuration for excalibur
571
   */
572
  public physics!: DeepRequired<PhysicsConfig>;
573

574
  /**
575
   * Optionally set the maximum fps if not set Excalibur will go as fast as the device allows.
576
   *
577
   * 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
578
   * one that bounces between 30fps and 60fps
579
   */
580
  public maxFps: number = Number.POSITIVE_INFINITY;
803✔
581

582
  /**
583
   * Optionally configure a fixed update fps, this can be desirable if you need the physics simulation to be very stable. When set
584
   * the update step and physics will use the same elapsed time for each tick even if the graphical framerate drops. In order for the
585
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
586
   * there could be X updates and 1 draw each clock step.
587
   *
588
   * **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
589
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
590
   *
591
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
592
   *
593
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
594
   */
595
  public readonly fixedUpdateFps?: number;
596

597
  /**
598
   * Optionally configure a fixed update timestep in milliseconds, this can be desirable if you need the physics simulation to be very stable. When
599
   * 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
600
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
601
   * there could be X updates and 1 draw each clock step.
602
   *
603
   * **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
604
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
605
   *
606
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
607
   *
608
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
609
   */
610
  public readonly fixedUpdateTimestep?: number;
611

612
  /**
613
   * Direct access to the excalibur clock
614
   */
615
  public clock!: Clock;
616

617
  public readonly pointerScope!: PointerScope;
618
  public readonly grabWindowFocus!: boolean;
619

620
  /**
621
   * The width of the game canvas in pixels (physical width component of the
622
   * resolution of the canvas element)
623
   */
624
  public get canvasWidth(): number {
625
    return this.screen.canvasWidth;
837✔
626
  }
627

628
  /**
629
   * Returns half width of the game canvas in pixels (half physical width component)
630
   */
631
  public get halfCanvasWidth(): number {
632
    return this.screen.halfCanvasWidth;
3✔
633
  }
634

635
  /**
636
   * The height of the game canvas in pixels, (physical height component of
637
   * the resolution of the canvas element)
638
   */
639
  public get canvasHeight(): number {
640
    return this.screen.canvasHeight;
837✔
641
  }
642

643
  /**
644
   * Returns half height of the game canvas in pixels (half physical height component)
645
   */
646
  public get halfCanvasHeight(): number {
647
    return this.screen.halfCanvasHeight;
3✔
648
  }
649

650
  /**
651
   * Returns the width of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
652
   */
653
  public get drawWidth(): number {
654
    return this.screen.drawWidth;
5✔
655
  }
656

657
  /**
658
   * Returns half the width of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
659
   */
660
  public get halfDrawWidth(): number {
661
    return this.screen.halfDrawWidth;
1,948✔
662
  }
663

664
  /**
665
   * Returns the height of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
666
   */
667
  public get drawHeight(): number {
668
    return this.screen.drawHeight;
5✔
669
  }
670

671
  /**
672
   * Returns half the height of the engine's visible drawing surface in pixels including zoom and device pixel ratio.
673
   */
674
  public get halfDrawHeight(): number {
675
    return this.screen.halfDrawHeight;
1,948✔
676
  }
677

678
  /**
679
   * Returns whether excalibur detects the current screen to be HiDPI
680
   */
681
  public get isHiDpi(): boolean {
682
    return this.screen.isHiDpi;
3✔
683
  }
684

685
  /**
686
   * Access engine input like pointer, keyboard, or gamepad
687
   */
688
  public input!: InputHost;
689

690
  /**
691
   * Map multiple input sources to specific game actions actions
692
   */
693
  public inputMapper!: InputMapper;
694

695
  private _inputEnabled: boolean = true;
803✔
696

697
  /**
698
   * Access Excalibur debugging functionality.
699
   *
700
   * Useful when you want to debug different aspects of built in engine features like
701
   *   * Transform
702
   *   * Graphics
703
   *   * Colliders
704
   */
705
  public debug!: DebugConfig;
706

707
  /**
708
   * Access {@apilink stats} that holds frame statistics.
709
   */
710
  public get stats(): DebugStats {
711
    return this.debug.stats;
29,600✔
712
  }
713

714
  /**
715
   * The current {@apilink Scene} being drawn and updated on screen
716
   */
717
  public get currentScene(): Scene {
718
    return this.director.currentScene;
7,986✔
719
  }
720

721
  /**
722
   * The current {@apilink Scene} being drawn and updated on screen
723
   */
724
  public get currentSceneName(): string {
725
    return this.director.currentSceneName;
29✔
726
  }
727

728
  /**
729
   * The default {@apilink Scene} of the game, use {@apilink Engine.goToScene} to transition to different scenes.
730
   */
731
  public get rootScene(): Scene {
732
    return this.director.rootScene;
1✔
733
  }
734

735
  /**
736
   * Contains all the scenes currently registered with Excalibur
737
   */
738
  public get scenes(): { [key: string]: Scene | SceneConstructor | SceneWithOptions } {
739
    return this.director.scenes;
4✔
740
  }
741

742
  /**
743
   * Indicates whether the engine is set to fullscreen or not
744
   */
745
  public get isFullscreen(): boolean {
746
    return this.screen.isFullscreen;
1✔
747
  }
748

749
  /**
750
   * Indicates the current {@apilink DisplayMode} of the engine.
751
   */
752
  public get displayMode(): DisplayMode {
753
    return this.screen.displayMode;
×
754
  }
755

756
  private _suppressPlayButton: boolean = false;
803✔
757
  /**
758
   * Returns the calculated pixel ration for use in rendering
759
   */
760
  public get pixelRatio(): number {
761
    return this.screen.pixelRatio;
1,673✔
762
  }
763

764
  /**
765
   * Indicates whether audio should be paused when the game is no longer visible.
766
   */
767
  public pauseAudioWhenHidden: boolean = true;
803✔
768

769
  /**
770
   * Indicates whether the engine should draw with debug information
771
   */
772
  private _isDebug: boolean = false;
803✔
773
  public get isDebug(): boolean {
774
    return this._isDebug;
3,914✔
775
  }
776

777
  /**
778
   * Sets the background color for the engine.
779
   */
780
  public backgroundColor!: Color;
781

782
  /**
783
   * Sets the Transparency for the engine.
784
   */
785
  public enableCanvasTransparency: boolean = true;
803✔
786

787
  /**
788
   * Hints the graphics context to truncate fractional world space coordinates
789
   */
790
  public get snapToPixel(): boolean {
791
    return this.graphicsContext.snapToPixel;
2✔
792
  }
793

794
  public set snapToPixel(shouldSnapToPixel: boolean) {
795
    this.graphicsContext.snapToPixel = shouldSnapToPixel;
1✔
796
  }
797

798
  /**
799
   * The action to take when a fatal exception is thrown
800
   */
801
  public onFatalException = (e: any) => {
803✔
802
    Logger.getInstance().fatal(e, e.stack);
×
803
  };
804

805
  /**
806
   * The mouse wheel scroll prevention mode
807
   */
808
  public pageScrollPreventionMode!: ScrollPreventionMode;
809

810
  private _logger!: Logger;
811

812
  private _toaster: Toaster = new Toaster();
803✔
813

814
  // this determines whether excalibur is compatible with your browser
815
  private _compatible: boolean;
816

817
  private _timescale: number = 1.0;
803✔
818

819
  // loading
820
  private _loader!: DefaultLoader;
821

822
  private _isInitialized: boolean = false;
803✔
823

824
  private _hasCreatedCanvas: boolean = false;
803✔
825

826
  public emit<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, event: EngineEvents[TEventName]): void;
827
  public emit(eventName: string, event?: any): void;
828
  public emit<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, event?: any): void {
829
    this.events.emit(eventName, event);
9,245✔
830
  }
831

832
  public on<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): Subscription;
833
  public on(eventName: string, handler: Handler<unknown>): Subscription;
834
  public on<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
835
    return this.events.on(eventName, handler);
29✔
836
  }
837

838
  public once<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): Subscription;
839
  public once(eventName: string, handler: Handler<unknown>): Subscription;
840
  public once<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
841
    return this.events.once(eventName, handler);
10✔
842
  }
843

844
  public off<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): void;
845
  public off(eventName: string, handler: Handler<unknown>): void;
846
  public off(eventName: string): void;
847
  public off<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler?: Handler<any>): void {
848
    this.events.off(eventName, handler as any);
×
849
  }
850

851
  /**
852
   * Default {@apilink EngineOptions}
853
   */
854
  private static _DEFAULT_ENGINE_OPTIONS: EngineOptions = {
256✔
855
    width: 0,
856
    height: 0,
857
    enableCanvasTransparency: true,
858
    useDrawSorting: true,
859
    configurePerformanceCanvas2DFallback: {
860
      allow: false,
861
      showPlayerMessage: false,
862
      threshold: { fps: 20, numberOfFrames: 100 }
863
    },
864
    canvasElementId: '',
865
    canvasElement: undefined,
866
    enableCanvasContextMenu: false,
867
    snapToPixel: false,
868
    antialiasing: true,
869
    pixelArt: false,
870
    garbageCollection: true,
871
    powerPreference: 'high-performance',
872
    pointerScope: PointerScope.Canvas,
873
    suppressConsoleBootMessage: undefined,
874
    suppressMinimumBrowserFeatureDetection: undefined,
875
    suppressHiDPIScaling: undefined,
876
    suppressPlayButton: undefined,
877
    grabWindowFocus: true,
878
    scrollPreventionMode: ScrollPreventionMode.Canvas,
879
    backgroundColor: Color.fromHex('#2185d0') // Excalibur blue
880
  };
881

882
  private _originalOptions: EngineOptions = {};
803✔
883
  public readonly _originalDisplayMode!: DisplayMode;
884

885
  /**
886
   * Creates a new game using the given {@apilink EngineOptions}. By default, if no options are provided,
887
   * the game will be rendered full screen (taking up all available browser window space).
888
   * You can customize the game rendering through {@apilink EngineOptions}.
889
   *
890
   * Example:
891
   *
892
   * ```js
893
   * var game = new ex.Engine({
894
   *   width: 0, // the width of the canvas
895
   *   height: 0, // the height of the canvas
896
   *   enableCanvasTransparency: true, // the transparencySection of the canvas
897
   *   canvasElementId: '', // the DOM canvas element ID, if you are providing your own
898
   *   displayMode: ex.DisplayMode.FullScreen, // the display mode
899
   *   pointerScope: ex.PointerScope.Document, // the scope of capturing pointer (mouse/touch) events
900
   *   backgroundColor: ex.Color.fromHex('#2185d0') // background color of the engine
901
   * });
902
   *
903
   * // call game.start, which is a Promise
904
   * game.start().then(function () {
905
   *   // ready, set, go!
906
   * });
907
   * ```
908
   */
909
  constructor(options?: EngineOptions<TKnownScenes>) {
910
    options = { ...Engine._DEFAULT_ENGINE_OPTIONS, ...options };
803✔
911
    this._originalOptions = options;
803✔
912

913
    if (options.plugins && options.plugins.length > 0) {
803✔
914
      const logger = Logger.getInstance();
21✔
915
      for (const plugin of options.plugins) {
21✔
916
        if (this._plugins.some((p) => p.name === plugin.name)) {
24✔
917
          logger.warn(`Plugin with name "${plugin.name}" is already installed, skipping duplicate.`);
1✔
918
          continue;
1✔
919
        }
920
        this._plugins.push(plugin);
23✔
921
      }
922
      this._plugins.sort((a, b) => a.priority - b.priority);
21✔
923
    }
924

925
    for (const plugin of this._plugins) {
803✔
926
      plugin.onEnginePreConfig?.(this, options);
23✔
927
    }
928

929
    Flags.freeze();
803✔
930

931
    // Initialize browser events facade
932
    this.browser = new BrowserEvents(window, document);
803✔
933

934
    // Check compatibility
935
    const detector = new Detector();
803✔
936
    if (!options.suppressMinimumBrowserFeatureDetection && !(this._compatible = detector.test())) {
803!
937
      const message = document.createElement('div');
×
938
      message.innerText = 'Sorry, your browser does not support all the features needed for Excalibur';
×
939
      document.body.appendChild(message);
×
940

NEW
941
      detector.failedTests.forEach(function(test) {
×
942
        const testMessage = document.createElement('div');
×
943
        testMessage.innerText = 'Browser feature missing ' + test;
×
944
        document.body.appendChild(testMessage);
×
945
      });
946

947
      if (options.canvasElementId) {
×
948
        const canvas = document.getElementById(options.canvasElementId);
×
949
        if (canvas) {
×
950
          canvas.parentElement!.removeChild(canvas);
×
951
        }
952
      }
953

954
      return;
×
955
    } else {
956
      this._compatible = true;
803✔
957
    }
958

959
    // Use native console API for color fun
960
    // eslint-disable-next-line no-console
961
    if (console.log && !options.suppressConsoleBootMessage) {
803✔
962
      // eslint-disable-next-line no-console
963
      console.log(
14✔
964
        `%cPowered by Excalibur.js (v${EX_VERSION})`,
965
        'background: #176BAA; color: white; border-radius: 5px; padding: 15px; font-size: 1.5em; line-height: 80px;'
966
      );
967
      // eslint-disable-next-line no-console
968
      console.log(
14✔
969
        '\n\
970
      /| ________________\n\
971
O|===|* >________________>\n\
972
      \\|'
973
      );
974
      // eslint-disable-next-line no-console
975
      console.log('Visit', 'http://excaliburjs.com', 'for more information');
14✔
976
    }
977

978
    // Suppress play button
979
    if (options.suppressPlayButton) {
803✔
980
      this._suppressPlayButton = true;
779✔
981
    }
982

983
    this._logger = Logger.getInstance();
803✔
984

985
    this.debug = new DebugConfig(this);
803✔
986

987
    // If debug is enabled, let's log browser features to the console.
988
    if (this._logger.defaultLevel === LogLevel.Debug) {
803!
989
      detector.logBrowserFeatures();
×
990
    }
991

992
    this._logger.debug('Building engine...');
803✔
993
    if (options.garbageCollection === true) {
803!
994
      this.garbageCollectorConfig = {
803✔
995
        ...DefaultGarbageCollectionOptions
996
      };
997
    } else if (options.garbageCollection === false) {
×
998
      this._logger.warn(
×
999
        'WebGL Garbage Collection Disabled!!! If you leak any images over time your game will crash when GPU memory is exhausted'
1000
      );
1001
      this.garbageCollectorConfig = null;
×
1002
    } else {
1003
      this.garbageCollectorConfig = {
×
1004
        ...DefaultGarbageCollectionOptions,
1005
        ...options.garbageCollection
1006
      };
1007
    }
1008
    this._garbageCollector = new GarbageCollector({ getTimestamp: Date.now });
803✔
1009

1010
    this.canvasElementId = options.canvasElementId;
803✔
1011

1012
    if (options.canvasElementId) {
803!
1013
      this._logger.debug('Using Canvas element specified: ' + options.canvasElementId);
×
1014

1015
      //test for existence of element
1016
      if (document.getElementById(options.canvasElementId) === null) {
×
1017
        throw new Error('Cannot find existing element in the DOM, please ensure element is created prior to engine creation.');
×
1018
      }
1019

1020
      this.canvas = <HTMLCanvasElement>document.getElementById(options.canvasElementId);
×
1021
      this._hasCreatedCanvas = false;
×
1022
    } else if (options.canvasElement) {
803!
1023
      this._logger.debug('Using Canvas element specified:', options.canvasElement);
×
1024
      this.canvas = options.canvasElement;
×
1025
      this._hasCreatedCanvas = false;
×
1026
    } else {
1027
      this._logger.debug('Using generated canvas element');
803✔
1028
      this.canvas = <HTMLCanvasElement>document.createElement('canvas');
803✔
1029
      this._hasCreatedCanvas = true;
803✔
1030
    }
1031

1032
    if (this.canvas && !options.enableCanvasContextMenu) {
803✔
1033
      this.canvas.addEventListener('contextmenu', (evt) => {
802✔
1034
        evt.preventDefault();
1✔
1035
      });
1036
    }
1037

1038
    let displayMode = options.displayMode ?? DisplayMode.Fixed;
803✔
1039
    if ((options.width && options.height) || options.viewport) {
803✔
1040
      if (options.displayMode === undefined) {
798✔
1041
        displayMode = DisplayMode.Fixed;
18✔
1042
      }
1043
      this._logger.debug('Engine viewport is size ' + options.width + ' x ' + options.height);
798✔
1044
    } else if (!options.displayMode) {
5✔
1045
      this._logger.debug('Engine viewport is fit');
2✔
1046
      displayMode = DisplayMode.FitScreen;
2✔
1047
    }
1048

1049
    const global = (options.global && typeof options.global === 'function' ? options.global() : options.global) as GlobalEventHandlers;
803!
1050

1051
    this.global = global ?? getDefaultGlobal();
803✔
1052
    this.grabWindowFocus = options.grabWindowFocus!;
803✔
1053
    this.pointerScope = options.pointerScope!;
803✔
1054

1055
    this._originalDisplayMode = displayMode;
803✔
1056

1057
    let pixelArtSampler: boolean;
1058
    let uvPadding: number;
1059
    let nativeContextAntialiasing: boolean;
1060
    let canvasImageRendering: 'pixelated' | 'auto';
1061
    let filtering: ImageFiltering;
1062
    let multiSampleAntialiasing: boolean | { samples: number };
1063
    if (typeof options.antialiasing === 'object') {
803!
1064
      ({ pixelArtSampler, nativeContextAntialiasing, multiSampleAntialiasing, filtering, canvasImageRendering } = {
×
1065
        ...(options.pixelArt ? DefaultPixelArtOptions : DefaultAntialiasOptions),
×
1066
        ...options.antialiasing
1067
      });
1068
    } else {
1069
      pixelArtSampler = !!options.pixelArt;
803✔
1070
      nativeContextAntialiasing = false;
803✔
1071
      multiSampleAntialiasing = options.antialiasing!;
803✔
1072
      canvasImageRendering = options.antialiasing ? 'auto' : 'pixelated';
803✔
1073
      filtering = options.antialiasing ? ImageFiltering.Blended : ImageFiltering.Pixel;
803✔
1074
    }
1075

1076
    if (nativeContextAntialiasing && multiSampleAntialiasing) {
803!
1077
      this._logger.warnOnce(
×
1078
        `Cannot use antialias setting nativeContextAntialiasing and multiSampleAntialiasing` +
1079
        ` at the same time, they are incompatible settings. If you aren\'t sure use multiSampleAntialiasing`
1080
      );
1081
    }
1082

1083
    if (options.pixelArt) {
803✔
1084
      uvPadding = 0.25;
1✔
1085
    }
1086

1087
    if (!options.antialiasing || filtering === ImageFiltering.Pixel) {
803✔
1088
      uvPadding = 0;
782✔
1089
    }
1090

1091
    // Override with any user option, if non default to .25 for pixel art, 0.01 for everything else
1092
    uvPadding = options.uvPadding ?? uvPadding! ?? 0.01;
803!
1093

1094
    // Canvas 2D fallback can be flagged on
1095
    let useCanvasGraphicsContext = Flags.isEnabled('use-canvas-context');
803✔
1096
    if (!useCanvasGraphicsContext) {
803✔
1097
      // Attempt webgl first
1098
      try {
801✔
1099
        let onGraphicsPreConfig: (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => void;
1100
        let onGraphicsPostConfig: (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => void;
1101
        let onGraphicsPreInitialize: (context: ExcaliburGraphicsContext) => void;
1102
        let onGraphicsPostInitialize: (context: ExcaliburGraphicsContext) => void;
1103
        onGraphicsPreConfig = (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => {
801✔
1104
          for (const plugin of this._plugins) {
801✔
1105
            plugin.onGraphicsPreConfig?.(context, options);
23✔
1106
          }
1107
        };
1108

1109
        onGraphicsPostConfig = (context: ExcaliburGraphicsContext, options: ExcaliburGraphicsContextWebGLOptions) => {
801✔
1110
          for (const plugin of this._plugins) {
801✔
1111
            plugin.onGraphicsPostConfig?.(context, options);
23✔
1112
          }
1113
        };
1114

1115
        onGraphicsPreInitialize = (context: ExcaliburGraphicsContext) => {
801✔
1116
          for (const plugin of this._plugins) {
801✔
1117
            plugin.onGraphicsPreInitialize?.(context);
23✔
1118
          }
1119
        };
1120

1121
        onGraphicsPostInitialize = (context: ExcaliburGraphicsContext) => {
801✔
1122
          for (const plugin of this._plugins) {
801✔
1123
            plugin.onGraphicsPostInitialize?.(context);
23✔
1124
          }
1125
        };
1126

1127
        this.graphicsContext = new ExcaliburGraphicsContextWebGL({
801✔
1128
          canvasElement: this.canvas,
1129
          enableTransparency: this.enableCanvasTransparency,
1130
          pixelArtSampler: pixelArtSampler,
1131
          antialiasing: nativeContextAntialiasing,
1132
          multiSampleAntialiasing: multiSampleAntialiasing,
1133
          uvPadding: uvPadding,
1134
          powerPreference: options.powerPreference,
1135
          backgroundColor: options.backgroundColor,
1136
          snapToPixel: options.snapToPixel,
1137
          useDrawSorting: options.useDrawSorting,
1138
          garbageCollector: this.garbageCollectorConfig
801!
1139
            ? {
1140
              garbageCollector: this._garbageCollector!,
1141
              collectionInterval: this.garbageCollectorConfig!.textureCollectInterval!
1142
            }
1143
            : undefined,
1144
          handleContextLost: options.handleContextLost ?? this._handleWebGLContextLost,
2,403!
1145
          handleContextRestored: options.handleContextRestored ?? this._handleWebGLContextRestored,
2,403!
1146
          onGraphicsPreConfig,
1147
          onGraphicsPostConfig,
1148
          onGraphicsPreInitialize,
1149
          onGraphicsPostInitialize
1150
        });
1151
      } catch (e) {
1152
        this._logger.warn(
×
1153
          `Excalibur could not load webgl for some reason (${(e as Error).message}) and loaded a Canvas 2D fallback. ` +
1154
          `Some features of Excalibur will not work in this mode. \n\n` +
1155
          'Read more about this issue at https://excaliburjs.com/docs/performance'
1156
        );
1157
        // fallback to canvas in case of failure
1158
        useCanvasGraphicsContext = true;
×
1159
      }
1160
    }
1161

1162
    if (useCanvasGraphicsContext) {
803✔
1163
      this.graphicsContext = new ExcaliburGraphicsContext2DCanvas({
2✔
1164
        canvasElement: this.canvas,
1165
        enableTransparency: this.enableCanvasTransparency,
1166
        antialiasing: nativeContextAntialiasing,
1167
        backgroundColor: options.backgroundColor,
1168
        snapToPixel: options.snapToPixel,
1169
        useDrawSorting: options.useDrawSorting
1170
      });
1171
    }
1172

1173
    this.screen = new Screen({
803✔
1174
      canvas: this.canvas,
1175
      context: this.graphicsContext,
1176
      antialiasing: nativeContextAntialiasing,
1177
      canvasImageRendering: canvasImageRendering,
1178
      browser: this.browser,
1179
      viewport: options.viewport ?? (options.width && options.height ? { width: options.width, height: options.height } : Resolution.SVGA),
4,801✔
1180
      resolution: options.resolution,
1181
      displayMode,
1182
      pixelRatio: options.suppressHiDPIScaling ? 1 : (options.pixelRatio ?? undefined)
878✔
1183
    });
1184

1185
    // TODO REMOVE STATIC!!!
1186
    // Set default filtering based on antialiasing
1187
    TextureLoader.filtering = filtering;
803✔
1188

1189
    if (options.backgroundColor) {
803!
1190
      this.backgroundColor = options.backgroundColor.clone();
803✔
1191
    }
1192

1193
    this.maxFps = options.maxFps ?? this.maxFps;
803!
1194

1195
    this.fixedUpdateTimestep = options.fixedUpdateTimestep ?? this.fixedUpdateTimestep;
803!
1196
    this.fixedUpdateFps = options.fixedUpdateFps ?? this.fixedUpdateFps;
803✔
1197
    this.fixedUpdateTimestep = this.fixedUpdateTimestep || 1000 / this.fixedUpdateFps!;
803✔
1198

1199
    this.clock = new StandardClock({
803✔
1200
      maxFps: this.maxFps,
1201
      tick: this._mainloop.bind(this),
1202
      onFatalException: (e) => this.onFatalException(e)
×
1203
    });
1204

1205
    this.enableCanvasTransparency = options.enableCanvasTransparency!;
803✔
1206

1207
    if (typeof options.physics === 'boolean') {
803!
1208
      this.physics = {
×
1209
        ...getDefaultPhysicsConfig(),
1210
        enabled: options.physics
1211
      };
1212
    } else {
1213
      this.physics = {
803✔
1214
        ...getDefaultPhysicsConfig()
1215
      };
1216
      mergeDeep(this.physics, options.physics!);
803✔
1217
    }
1218

1219
    this.director = new Director(this, options.scenes!);
803✔
1220
    this.director.events.pipe(this.events);
803✔
1221

1222
    this._initialize(options);
803✔
1223

1224
    (window as any).___EXCALIBUR_DEVTOOL = this;
803✔
1225
    Engine.InstanceCount++;
803✔
1226
  }
1227

1228
  private _handleWebGLContextLost = (e: Event) => {
803✔
1229
    e.preventDefault();
507✔
1230
    this.clock.stop();
507✔
1231
    this._logger.fatalOnce('WebGL Graphics Lost', e);
507✔
1232

1233
    // Notify plugins that the graphics context was lost
1234
    for (const plugin of this._plugins) {
507✔
1235
      plugin.onGraphicsContextLost?.(this.graphicsContext);
22!
1236
    }
1237

1238
    const container = document.createElement('div');
507✔
1239
    container.id = 'ex-webgl-graphics-context-lost';
507✔
1240
    container.style.position = 'absolute';
507✔
1241
    container.style.zIndex = '99';
507✔
1242
    container.style.left = '50%';
507✔
1243
    container.style.top = '50%';
507✔
1244
    container.style.display = 'flex';
507✔
1245
    container.style.flexDirection = 'column';
507✔
1246
    container.style.transform = 'translate(-50%, -50%)';
507✔
1247
    container.style.backgroundColor = 'white';
507✔
1248
    container.style.padding = '10px';
507✔
1249
    container.style.borderStyle = 'solid 1px';
507✔
1250

1251
    const div = document.createElement('div');
507✔
1252
    div.innerHTML = `
507✔
1253
      <h1>There was an issue rendering, please refresh the page.</h1>
1254
      <div>
1255
        <p>WebGL Graphics Context Lost</p>
1256

1257
        <button id="ex-webgl-graphics-reload">Refresh Page</button>
1258

1259
        <p>There are a few reasons this might happen:</p>
1260
        <ul>
1261
          <li>Two or more pages are placing a high demand on the GPU</li>
1262
          <li>Another page or operation has stalled the GPU and the browser has decided to reset the GPU</li>
1263
          <li>The computer has multiple GPUs and the user has switched between them</li>
1264
          <li>Graphics driver has crashed or restarted</li>
1265
          <li>Graphics driver was updated</li>
1266
        </ul>
1267
      </div>
1268
    `;
1269
    container.appendChild(div);
507✔
1270
    if (this.canvas?.parentElement) {
507✔
1271
      this.canvas.parentElement.appendChild(container);
6✔
1272
      const button = div.querySelector('#ex-webgl-graphics-reload');
6✔
1273
      button?.addEventListener('click', () => location.reload());
6✔
1274
    }
1275
  };
1276

1277
  private _handleWebGLContextRestored = () => {
803✔
1278
    this._logger.debug('WebGL Graphics Context Restored');
35✔
1279

1280
    // Notify plugins that the graphics context was restored.
1281
    // The graphics context's _init() will re-run and call onGraphicsPreInitialize/onGraphicsPostInitialize
1282
    // so plugins can rebuild their WebGL resources.
1283
    for (const plugin of this._plugins) {
35✔
NEW
1284
      plugin.onGraphicsContextRestored?.(this.graphicsContext);
×
1285
    }
1286

1287
    // Restart the clock
1288
    this.browser.resume();
35✔
1289
    this.clock.start();
35✔
1290
  };
1291

1292
  private _performanceThresholdTriggered = false;
803✔
1293
  private _fpsSamples: number[] = [];
803✔
1294
  private _monitorPerformanceThresholdAndTriggerFallback() {
1295
    const { allow } = this._originalOptions.configurePerformanceCanvas2DFallback!;
1,755✔
1296
    let { threshold, showPlayerMessage } = this._originalOptions.configurePerformanceCanvas2DFallback!;
1,755✔
1297
    if (threshold === undefined) {
1,755✔
1298
      threshold = Engine._DEFAULT_ENGINE_OPTIONS.configurePerformanceCanvas2DFallback!.threshold;
100✔
1299
    }
1300
    if (showPlayerMessage === undefined) {
1,755✔
1301
      showPlayerMessage = Engine._DEFAULT_ENGINE_OPTIONS.configurePerformanceCanvas2DFallback!.showPlayerMessage;
100✔
1302
    }
1303
    if (!Flags.isEnabled('use-canvas-context') && allow && this.ready && !this._performanceThresholdTriggered) {
1,755✔
1304
      // Calculate Average fps for last X number of frames after start
1305
      if (this._fpsSamples.length === threshold!.numberOfFrames) {
100!
1306
        this._fpsSamples.splice(0, 1);
×
1307
      }
1308
      this._fpsSamples.push(this.clock.fpsSampler.fps);
100✔
1309
      let total = 0;
100✔
1310
      for (let i = 0; i < this._fpsSamples.length; i++) {
100✔
1311
        total += this._fpsSamples[i];
5,050✔
1312
      }
1313
      const average = total / this._fpsSamples.length;
100✔
1314

1315
      if (this._fpsSamples.length === threshold!.numberOfFrames) {
100✔
1316
        if (average <= threshold!.fps) {
1!
1317
          this._performanceThresholdTriggered = true;
1✔
1318
          this._logger.warn(
1✔
1319
            `Switching to browser 2D Canvas fallback due to performance. Some features of Excalibur will not work in this mode.\n` +
1320
            "this might mean your browser doesn't have webgl enabled or hardware acceleration is unavailable.\n\n" +
1321
            'If in Chrome:\n' +
1322
            '  * Visit Settings > Advanced > System, and ensure "Use Hardware Acceleration" is checked.\n' +
1323
            '  * Visit chrome://flags/#ignore-gpu-blocklist and ensure "Override software rendering list" is "enabled"\n' +
1324
            'If in Firefox, visit about:config\n' +
1325
            '  * Ensure webgl.disabled = false\n' +
1326
            '  * Ensure webgl.force-enabled = true\n' +
1327
            '  * Ensure layers.acceleration.force-enabled = true\n\n' +
1328
            'Read more about this issue at https://excaliburjs.com/docs/performance'
1329
          );
1330

1331
          if (showPlayerMessage) {
1!
1332
            this._toaster.toast(
×
1333
              'Excalibur is encountering performance issues. ' +
1334
              "It's possible that your browser doesn't have hardware acceleration enabled. " +
1335
              'Visit [LINK] for more information and potential solutions.',
1336
              'https://excaliburjs.com/docs/performance'
1337
            );
1338
          }
1339
          this.useCanvas2DFallback();
1✔
1340
          this.emit('fallbackgraphicscontext', this.graphicsContext);
1✔
1341
        }
1342
      }
1343
    }
1344
  }
1345

1346
  /**
1347
   * Switches the engine's graphics context to the 2D Canvas.
1348
   * @warning Some features of Excalibur will not work in this mode.
1349
   */
1350
  public useCanvas2DFallback() {
1351
    // Swap out the canvas
1352
    const newCanvas = this.canvas.cloneNode(false) as HTMLCanvasElement;
2✔
1353
    this.canvas!.parentNode!.replaceChild(newCanvas, this.canvas);
2✔
1354
    this.canvas = newCanvas;
2✔
1355

1356
    const options = { ...this._originalOptions, antialiasing: this.screen.antialiasing };
2✔
1357
    const displayMode = this._originalDisplayMode;
2✔
1358

1359
    // New graphics context
1360
    this.graphicsContext = new ExcaliburGraphicsContext2DCanvas({
2✔
1361
      canvasElement: this.canvas,
1362
      enableTransparency: this.enableCanvasTransparency,
1363
      antialiasing: options.antialiasing,
1364
      backgroundColor: options.backgroundColor,
1365
      snapToPixel: options.snapToPixel,
1366
      useDrawSorting: options.useDrawSorting
1367
    });
1368

1369
    // Reset screen
1370
    if (this.screen) {
2!
1371
      this.screen.dispose();
2✔
1372
    }
1373

1374
    this.screen = new Screen({
2✔
1375
      canvas: this.canvas,
1376
      context: this.graphicsContext,
1377
      antialiasing: options.antialiasing ?? true,
6!
1378
      browser: this.browser,
1379
      viewport: options.viewport ?? (options.width && options.height ? { width: options.width, height: options.height } : Resolution.SVGA),
12!
1380
      resolution: options.resolution,
1381
      displayMode,
1382
      pixelRatio: options.suppressHiDPIScaling ? 1 : (options.pixelRatio ?? undefined)
2!
1383
    });
1384
    this.screen.setCurrentCamera(this.currentScene.camera);
2✔
1385

1386
    // Reset pointers
1387
    this.input.pointers.detach();
2✔
1388
    const pointerTarget = options && options.pointerScope === PointerScope.Document ? document : this.canvas;
2!
1389
    this.input.pointers = this.input.pointers.recreate(pointerTarget, this);
2✔
1390
    this.input.pointers.init();
2✔
1391
  }
1392

1393
  private _disposed = false;
803✔
1394
  /**
1395
   * Attempts to completely clean up excalibur resources, including removing the canvas from the dom.
1396
   *
1397
   * To start again you will need to new up an Engine.
1398
   */
1399
  public dispose() {
1400
    if (!this._disposed) {
793✔
1401
      this._disposed = true;
789✔
1402
      this.stop();
789✔
1403
      this._garbageCollector.forceCollectAll();
789✔
1404
      this.input.toggleEnabled(false);
789✔
1405

1406
      for (const plugin of this.plugins) {
789✔
1407
        plugin.dispose?.();
26✔
1408
      }
1409

1410
      if (this._hasCreatedCanvas) {
789!
1411
        this.canvas!.parentNode!.removeChild(this.canvas);
789✔
1412
      }
1413
      this.canvas = null as any;
789✔
1414
      this.screen.dispose();
789✔
1415
      this.graphicsContext.dispose();
789✔
1416
      this.graphicsContext = null as any;
789✔
1417
      Engine.InstanceCount--;
789✔
1418
    }
1419
  }
1420

1421
  public isDisposed() {
1422
    return this._disposed;
1,522✔
1423
  }
1424

1425
  /**
1426
   * Returns a BoundingBox of the top left corner of the screen
1427
   * and the bottom right corner of the screen.
1428
   */
1429
  public getWorldBounds() {
1430
    return this.screen.getWorldBounds();
1✔
1431
  }
1432

1433
  /**
1434
   * Gets the current engine timescale factor (default is 1.0 which is 1:1 time)
1435
   */
1436
  public get timescale() {
1437
    return this._timescale;
1,756✔
1438
  }
1439

1440
  /**
1441
   * Sets the current engine timescale factor. Useful for creating slow-motion effects or fast-forward effects
1442
   * when using time-based movement.
1443
   */
1444
  public set timescale(value: number) {
1445
    if (value < 0) {
2!
1446
      Logger.getInstance().warnOnce('engine.timescale to a value less than 0 are ignored');
×
1447
      return;
×
1448
    }
1449

1450
    this._timescale = value;
2✔
1451
  }
1452

1453
  /**
1454
   * Adds a {@apilink Timer} to the {@apilink currentScene}.
1455
   * @param timer  The timer to add to the {@apilink currentScene}.
1456
   */
1457
  public addTimer(timer: Timer): Timer {
1458
    return this.currentScene.addTimer(timer);
×
1459
  }
1460

1461
  /**
1462
   * Removes a {@apilink Timer} from the {@apilink currentScene}.
1463
   * @param timer  The timer to remove to the {@apilink currentScene}.
1464
   */
1465
  public removeTimer(timer: Timer): Timer {
1466
    return this.currentScene.removeTimer(timer);
×
1467
  }
1468

1469
  /**
1470
   * Adds a {@apilink Scene} to the engine, think of scenes in Excalibur as you
1471
   * would levels or menus.
1472
   * @param key  The name of the scene, must be unique
1473
   * @param scene The scene to add to the engine
1474
   */
1475
  public addScene<TScene extends string>(key: TScene, scene: Scene | SceneConstructor | SceneWithOptions): Engine<TKnownScenes | TScene> {
1476
    this.director.add(key, scene);
433✔
1477
    return this as Engine<TKnownScenes | TScene>;
433✔
1478
  }
1479

1480
  /**
1481
   * Removes a {@apilink Scene} instance from the engine
1482
   * @param scene  The scene to remove
1483
   */
1484
  public removeScene(scene: Scene | SceneConstructor): void;
1485
  /**
1486
   * Removes a scene from the engine by key
1487
   * @param key  The scene key to remove
1488
   */
1489
  public removeScene(key: string): void;
1490
  /**
1491
   * @internal
1492
   */
1493
  public removeScene(entity: any): void {
1494
    this.director.remove(entity);
8✔
1495
  }
1496

1497
  /**
1498
   * Adds a {@apilink Scene} to the engine, think of scenes in Excalibur as you
1499
   * would levels or menus.
1500
   * @param sceneKey  The key of the scene, must be unique
1501
   * @param scene     The scene to add to the engine
1502
   */
1503
  public add(sceneKey: string, scene: Scene | SceneConstructor | SceneWithOptions): void;
1504
  /**
1505
   * Adds a {@apilink Timer} to the {@apilink currentScene}.
1506
   * @param timer  The timer to add to the {@apilink currentScene}.
1507
   */
1508
  public add(timer: Timer): void;
1509
  /**
1510
   * Adds a {@apilink TileMap} to the {@apilink currentScene}, once this is done the TileMap
1511
   * will be drawn and updated.
1512
   */
1513
  public add(tileMap: TileMap): void;
1514
  /**
1515
   * Adds an actor to the {@apilink currentScene} of the game. This is synonymous
1516
   * to calling `engine.currentScene.add(actor)`.
1517
   *
1518
   * Actors can only be drawn if they are a member of a scene, and only
1519
   * the {@apilink currentScene} may be drawn or updated.
1520
   * @param actor  The actor to add to the {@apilink currentScene}
1521
   */
1522
  public add(actor: Actor): void;
1523

1524
  public add(entity: Entity): void;
1525

1526
  /**
1527
   * Adds a {@apilink ScreenElement} to the {@apilink currentScene} of the game,
1528
   * ScreenElements do not participate in collisions, instead the
1529
   * remain in the same place on the screen.
1530
   * @param screenElement  The ScreenElement to add to the {@apilink currentScene}
1531
   */
1532
  public add(screenElement: ScreenElement): void;
1533
  public add(entity: any): void {
1534
    if (arguments.length === 2) {
265✔
1535
      this.director.add(<string>arguments[0], <Scene | SceneConstructor | SceneWithOptions>arguments[1]);
95✔
1536
      return;
95✔
1537
    }
1538
    const maybeDeferred = this.director.getDeferredScene();
170✔
1539
    if (maybeDeferred instanceof Scene) {
170!
1540
      maybeDeferred.add(entity);
×
1541
    } else {
1542
      this.currentScene.add(entity);
170✔
1543
    }
1544
  }
1545

1546
  /**
1547
   * Removes a scene instance from the engine
1548
   * @param scene  The scene to remove
1549
   */
1550
  public remove(scene: Scene | SceneConstructor): void;
1551
  /**
1552
   * Removes a scene from the engine by key
1553
   * @param sceneKey  The scene to remove
1554
   */
1555
  public remove(sceneKey: string): void;
1556
  /**
1557
   * Removes a {@apilink Timer} from the {@apilink currentScene}.
1558
   * @param timer  The timer to remove to the {@apilink currentScene}.
1559
   */
1560
  public remove(timer: Timer): void;
1561
  /**
1562
   * Removes a {@apilink TileMap} from the {@apilink currentScene}, it will no longer be drawn or updated.
1563
   */
1564
  public remove(tileMap: TileMap): void;
1565
  /**
1566
   * Removes an actor from the {@apilink currentScene} of the game. This is synonymous
1567
   * to calling `engine.currentScene.removeChild(actor)`.
1568
   * Actors that are removed from a scene will no longer be drawn or updated.
1569
   * @param actor  The actor to remove from the {@apilink currentScene}.
1570
   */
1571
  public remove(actor: Actor): void;
1572
  /**
1573
   * Removes a {@apilink ScreenElement} to the scene, it will no longer be drawn or updated
1574
   * @param screenElement  The ScreenElement to remove from the {@apilink currentScene}
1575
   */
1576
  public remove(screenElement: ScreenElement): void;
1577
  public remove(entity: any): void {
1578
    if (entity instanceof Entity) {
4✔
1579
      this.currentScene.remove(entity);
2✔
1580
    }
1581

1582
    if (entity instanceof Scene || isSceneConstructor(entity)) {
4✔
1583
      this.removeScene(entity);
1✔
1584
    }
1585

1586
    if (typeof entity === 'string') {
4✔
1587
      this.removeScene(entity);
1✔
1588
    }
1589
  }
1590

1591
  /**
1592
   * Changes the current scene with optionally supplied:
1593
   * * Activation data
1594
   * * Transitions
1595
   * * Loaders
1596
   *
1597
   * Example:
1598
   * ```typescript
1599
   * game.goToScene('myScene', {
1600
   *   sceneActivationData: {any: 'thing at all'},
1601
   *   destinationIn: new FadeInOut({duration: 1000, direction: 'in'}),
1602
   *   sourceOut: new FadeInOut({duration: 1000, direction: 'out'}),
1603
   *   loader: MyLoader
1604
   * });
1605
   * ```
1606
   *
1607
   * Scenes are defined in the Engine constructor
1608
   * ```typescript
1609
   * const engine = new ex.Engine({
1610
      scenes: {...}
1611
    });
1612
   * ```
1613
   * Or by adding dynamically
1614
   *
1615
   * ```typescript
1616
   * engine.addScene('myScene', new ex.Scene());
1617
   * ```
1618
   * @param destinationScene
1619
   * @param options
1620
   */
1621
  public async goToScene<TData = undefined>(destinationScene: WithRoot<TKnownScenes>, options?: GoToOptions<TData>): Promise<void> {
1622
    await this.scope(async () => {
436✔
1623
      await this.director.goToScene(destinationScene, options);
436✔
1624
    });
1625
  }
1626

1627
  /**
1628
   * Transforms the current x, y from screen coordinates to world coordinates
1629
   * @param point  Screen coordinate to convert
1630
   */
1631
  public screenToWorldCoordinates(point: Vector): Vector {
1632
    return this.screen.screenToWorldCoordinates(point);
2✔
1633
  }
1634

1635
  /**
1636
   * Transforms a world coordinate, to a screen coordinate
1637
   * @param point  World coordinate to convert
1638
   */
1639
  public worldToScreenCoordinates(point: Vector): Vector {
1640
    return this.screen.worldToScreenCoordinates(point);
3✔
1641
  }
1642

1643
  /**
1644
   * Initializes the internal canvas, rendering context, display mode, and native event listeners
1645
   */
1646
  private _initialize(options?: EngineOptions) {
1647
    this.pageScrollPreventionMode = options?.scrollPreventionMode!;
803✔
1648

1649
    // initialize inputs
1650
    const pointerTarget = options && options.pointerScope === PointerScope.Document ? document : this.canvas;
803✔
1651
    const grabWindowFocus = this._originalOptions?.grabWindowFocus ?? true;
803!
1652
    this.input = new InputHost({
803✔
1653
      global: this.global,
1654
      pointerTarget,
1655
      grabWindowFocus,
1656
      engine: this
1657
    });
1658
    this.inputMapper = this.input.inputMapper;
803✔
1659

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

1663
    this.browser.document.on('visibilitychange', () => {
803✔
1664
      if (document.visibilityState === 'hidden') {
20✔
1665
        this.events.emit('hidden', new HiddenEvent(this));
10✔
1666
        this._logger.debug('Window hidden');
10✔
1667
      } else if (document.visibilityState === 'visible') {
10!
1668
        this.events.emit('visible', new VisibleEvent(this));
10✔
1669
        this._logger.debug('Window visible');
10✔
1670
      }
1671
    });
1672

1673
    if (!this.canvasElementId && !options?.canvasElement) {
803!
1674
      document.body.appendChild(this.canvas);
803✔
1675
    }
1676

1677
    for (const plugin of this._plugins) {
803✔
1678
      if (plugin.onEnginePostConfig) {
23✔
1679
        plugin.onEnginePostConfig?.(this, options ?? {});
21!
1680
      }
1681
    }
1682
  }
1683

1684
  public toggleInputEnabled(enabled: boolean) {
1685
    this._inputEnabled = enabled;
×
1686
    this.input.toggleEnabled(this._inputEnabled);
×
1687
  }
1688

1689
  public onInitialize(engine: Engine) {
1690
    // Override me
1691
  }
1692

1693
  /**
1694
   * Gets whether the actor is Initialized
1695
   */
1696
  public get isInitialized(): boolean {
1697
    return this._isInitialized;
602✔
1698
  }
1699

1700
  private async _overrideInitialize(engine: Engine) {
1701
    if (!this.isInitialized) {
597✔
1702
      for (const plugin of this._plugins) {
588✔
1703
        if (plugin.onEnginePreInitialize) {
6!
1704
          plugin.onEnginePreInitialize(this);
6✔
1705
        }
1706
      }
1707

1708
      await this.director.onInitialize();
588✔
1709
      await this.onInitialize(engine);
588✔
1710
      this.events.emit('initialize', new InitializeEvent(engine, this));
588✔
1711
      this._isInitialized = true;
588✔
1712

1713
      for (const plugin of this._plugins) {
588✔
1714
        if (plugin.onEnginePostInitialize) {
6!
1715
          plugin.onEnginePostInitialize(this);
6✔
1716
        }
1717
      }
1718
    }
1719
  }
1720

1721
  /**
1722
   * Updates the entire state of the game
1723
   * @param elapsed  Number of milliseconds elapsed since the last update.
1724
   */
1725
  private _update(elapsed: number) {
1726
    if (this._isLoading) {
1,753✔
1727
      // suspend updates until loading is finished
1728
      this._loader?.onUpdate(this, elapsed);
829!
1729
      // Update input listeners
1730
      this.input.update();
829✔
1731
      return;
829✔
1732
    }
1733

1734
    // Publish preupdate events
1735
    this.clock.__runScheduledCbs('preupdate');
924✔
1736
    this._preupdate(elapsed);
924✔
1737

1738
    // process engine level events
1739
    this.currentScene.update(this, elapsed);
924✔
1740

1741
    // Update graphics postprocessors
1742
    this.graphicsContext.updatePostProcessors(elapsed);
924✔
1743

1744
    // Publish update event
1745
    this.clock.__runScheduledCbs('postupdate');
924✔
1746
    this._postupdate(elapsed);
924✔
1747

1748
    // Update input listeners
1749
    this.input.update();
924✔
1750
  }
1751

1752
  /**
1753
   * @internal
1754
   */
1755
  public _preupdate(elapsed: number) {
1756
    this.emit('preupdate', new PreUpdateEvent(this, elapsed, this));
924✔
1757
    this.onPreUpdate(this, elapsed);
924✔
1758
  }
1759

1760
  /**
1761
   * Safe to override method
1762
   * @param engine The reference to the current game engine
1763
   * @param elapsed  The time elapsed since the last update in milliseconds
1764
   */
1765
  public onPreUpdate(engine: Engine, elapsed: number) {
1766
    // Override me
1767
  }
1768

1769
  /**
1770
   * @internal
1771
   */
1772
  public _postupdate(elapsed: number) {
1773
    this.emit('postupdate', new PostUpdateEvent(this, elapsed, this));
924✔
1774
    this.onPostUpdate(this, elapsed);
924✔
1775
  }
1776

1777
  /**
1778
   * Safe to override method
1779
   * @param engine The reference to the current game engine
1780
   * @param elapsed  The time elapsed since the last update in milliseconds
1781
   */
1782
  public onPostUpdate(engine: Engine, elapsed: number) {
1783
    // Override me
1784
  }
1785

1786
  /**
1787
   * Draws the entire game
1788
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1789
   */
1790
  private _draw(elapsed: number) {
1791
    // Use scene background color if present, fallback to engine
1792
    this.graphicsContext.backgroundColor = this.currentScene.backgroundColor ?? this.backgroundColor;
1,756✔
1793
    this.graphicsContext.beginDrawLifecycle();
1,756✔
1794
    this.graphicsContext.clear();
1,756✔
1795
    this.clock.__runScheduledCbs('predraw');
1,756✔
1796
    this._predraw(this.graphicsContext, elapsed);
1,756✔
1797

1798
    // Drawing nothing else while loading
1799
    if (this._isLoading) {
1,756✔
1800
      if (!this._hideLoader) {
829!
1801
        this._loader?.canvas.draw(this.graphicsContext, 0, 0);
829!
1802
        this.clock.__runScheduledCbs('postdraw');
829✔
1803
        this.graphicsContext.flush();
829✔
1804
        this.graphicsContext.endDrawLifecycle();
829✔
1805
      }
1806
      return;
829✔
1807
    }
1808

1809
    this.currentScene.draw(this.graphicsContext, elapsed);
927✔
1810

1811
    this.clock.__runScheduledCbs('postdraw');
927✔
1812
    this._postdraw(this.graphicsContext, elapsed);
927✔
1813

1814
    // Flush any pending drawings
1815
    this.graphicsContext.flush();
927✔
1816
    this.graphicsContext.endDrawLifecycle();
927✔
1817

1818
    this._checkForScreenShots();
927✔
1819
  }
1820

1821
  /**
1822
   * @internal
1823
   */
1824
  public _predraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1825
    this.emit('predraw', new PreDrawEvent(ctx, elapsed, this));
1,756✔
1826
    this.onPreDraw(ctx, elapsed);
1,756✔
1827
  }
1828

1829
  /**
1830
   * Safe to override method to hook into pre draw
1831
   * @param ctx {@link ExcaliburGraphicsContext} for drawing
1832
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1833
   */
1834
  public onPreDraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1835
    // Override me
1836
  }
1837

1838
  /**
1839
   * @internal
1840
   */
1841
  public _postdraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1842
    this.emit('postdraw', new PostDrawEvent(ctx, elapsed, this));
927✔
1843
    this.onPostDraw(ctx, elapsed);
927✔
1844
  }
1845

1846
  /**
1847
   * Safe to override method to hook into pre draw
1848
   * @param ctx {@link ExcaliburGraphicsContext} for drawing
1849
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1850
   */
1851
  public onPostDraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1852
    // Override me
1853
  }
1854

1855
  /**
1856
   * Enable or disable Excalibur debugging functionality.
1857
   * @param toggle a value that debug drawing will be changed to
1858
   */
1859
  public showDebug(toggle: boolean): void {
1860
    this._isDebug = toggle;
2✔
1861
  }
1862

1863
  /**
1864
   * Toggle Excalibur debugging functionality.
1865
   */
1866
  public toggleDebug(): boolean {
1867
    this._isDebug = !this._isDebug;
14✔
1868
    return this._isDebug;
14✔
1869
  }
1870

1871
  /**
1872
   * Returns true when loading is totally complete and the player has clicked start
1873
   */
1874
  public get loadingComplete() {
1875
    return !this._isLoading;
757✔
1876
  }
1877

1878
  private _isLoading = false;
803✔
1879
  private _hideLoader = false;
803✔
1880
  private _isReadyFuture = new Future<void>();
803✔
1881
  public get ready() {
1882
    return this._isReadyFuture.isCompleted;
100✔
1883
  }
1884
  public isReady(): Promise<void> {
1885
    return this._isReadyFuture.promise;
14✔
1886
  }
1887

1888
  /**
1889
   * Starts the internal game loop for Excalibur after loading
1890
   * any provided assets.
1891
   * @param loader  Optional {@apilink Loader} to use to load resources. The default loader is {@apilink Loader},
1892
   * override to provide your own custom loader.
1893
   *
1894
   * Note: start() only resolves AFTER the user has clicked the play button
1895
   */
1896
  public async start(loader?: DefaultLoader): Promise<void>;
1897
  /**
1898
   * Starts the internal game loop for Excalibur after configuring any routes, loaders, or transitions
1899
   * @param sceneName
1900
   * @param options {Optional} {@apilink StartOptions} to configure the routes for scenes in Excalibur
1901
   *
1902
   * Note: start() only resolves AFTER the user has clicked the play button
1903
   */
1904
  public async start(sceneName: WithRoot<TKnownScenes>, options?: StartOptions): Promise<void>;
1905
  /**
1906
   * Starts the internal game loop after any loader is finished
1907
   * @param loader
1908
   */
1909
  public async start(loader?: DefaultLoader): Promise<void>;
1910
  public async start(sceneNameOrLoader?: WithRoot<TKnownScenes> | DefaultLoader, options?: StartOptions): Promise<void> {
1911
    await this.scope(async () => {
596✔
1912
      if (!this._compatible) {
596!
1913
        throw new Error('Excalibur is incompatible with your browser');
×
1914
      }
1915
      this._isLoading = true;
596✔
1916
      let loader: DefaultLoader;
1917
      if (sceneNameOrLoader instanceof DefaultLoader) {
596✔
1918
        loader = sceneNameOrLoader;
15✔
1919
      } else if (typeof sceneNameOrLoader === 'string') {
581✔
1920
        this.director.configureStart(sceneNameOrLoader, options);
1✔
1921
        loader = this.director.mainLoader!;
1✔
1922
      }
1923

1924
      // Start the excalibur clock which drives the mainloop
1925
      this._logger.debug('Starting game clock...');
596✔
1926
      this.browser.resume();
596✔
1927
      this.clock.start();
596✔
1928
      if (this.garbageCollectorConfig) {
596!
1929
        this._garbageCollector.start();
596✔
1930
      }
1931
      this._logger.debug('Game clock started');
596✔
1932

1933
      // Run plugin onLoad hooks before loading resources
1934
      for (const plugin of this._plugins) {
596✔
1935
        await plugin.onLoad?.();
6!
1936
      }
1937

1938
      await this.load(loader! ?? new Loader());
596✔
1939

1940
      // Run plugin onLoadComplete hooks after resources are loaded
1941
      for (const plugin of this._plugins) {
595✔
1942
        await plugin.onLoadComplete?.();
6!
1943
      }
1944

1945
      // Initialize before ready
1946
      await this._overrideInitialize(this);
595✔
1947

1948
      this._isReadyFuture.resolve();
595✔
1949
      this.emit('start', new GameStartEvent(this));
595✔
1950
      return this._isReadyFuture.promise;
595✔
1951
    });
1952
  }
1953

1954
  /**
1955
   * Returns the current frames elapsed milliseconds
1956
   */
1957
  public currentFrameElapsedMs = 0;
803✔
1958

1959
  /**
1960
   * Returns the current frame lag when in fixed update mode
1961
   */
1962
  public currentFrameLagMs = 0;
803✔
1963

1964
  private _lagMs = 0;
803✔
1965
  private _mainloop(elapsed: number) {
1966
    this.scope(() => {
1,755✔
1967
      this.emit('preframe', new PreFrameEvent(this, this.stats.prevFrame));
1,755✔
1968
      const elapsedMs = elapsed * this.timescale;
1,755✔
1969
      this.currentFrameElapsedMs = elapsedMs;
1,755✔
1970

1971
      // reset frame stats (reuse existing instances)
1972
      const frameId = this.stats.prevFrame.id + 1;
1,755✔
1973
      this.stats.currFrame.reset();
1,755✔
1974
      this.stats.currFrame.id = frameId;
1,755✔
1975
      this.stats.currFrame.elapsedMs = elapsedMs;
1,755✔
1976
      this.stats.currFrame.fps = this.clock.fpsSampler.fps;
1,755✔
1977
      GraphicsDiagnostics.clear();
1,755✔
1978

1979
      const beforeUpdate = this.clock.now();
1,755✔
1980
      const fixedTimestepMs = this.fixedUpdateTimestep;
1,755✔
1981
      if (this.fixedUpdateTimestep) {
1,755✔
1982
        this._lagMs += elapsedMs;
44✔
1983
        while (this._lagMs >= fixedTimestepMs!) {
44✔
1984
          this._update(fixedTimestepMs!);
42✔
1985
          this._lagMs -= fixedTimestepMs!;
42✔
1986
        }
1987
      } else {
1988
        this._update(elapsedMs);
1,711✔
1989
      }
1990
      const afterUpdate = this.clock.now();
1,755✔
1991
      this.currentFrameLagMs = this._lagMs;
1,755✔
1992
      this._draw(elapsedMs);
1,755✔
1993
      const afterDraw = this.clock.now();
1,755✔
1994

1995
      this.stats.currFrame.duration.update = afterUpdate - beforeUpdate;
1,755✔
1996
      this.stats.currFrame.duration.draw = afterDraw - afterUpdate;
1,755✔
1997
      this.stats.currFrame.graphics.drawnImages = GraphicsDiagnostics.DrawnImagesCount;
1,755✔
1998
      this.stats.currFrame.graphics.drawCalls = GraphicsDiagnostics.DrawCallCount;
1,755✔
1999
      this.stats.currFrame.graphics.rendererSwaps = GraphicsDiagnostics.RendererSwaps;
1,755✔
2000

2001
      this.emit('postframe', new PostFrameEvent(this, this.stats.currFrame));
1,755✔
2002
      this.stats.prevFrame.reset(this.stats.currFrame);
1,755✔
2003

2004
      this._monitorPerformanceThresholdAndTriggerFallback();
1,755✔
2005
    });
2006
  }
2007

2008
  /**
2009
   * Stops Excalibur's main loop, useful for pausing the game.
2010
   */
2011
  public stop() {
2012
    if (this.clock.isRunning()) {
1,588✔
2013
      this.emit('stop', new GameStopEvent(this));
605✔
2014
      this.browser.pause();
605✔
2015
      this.clock.stop();
605✔
2016
      this._garbageCollector.stop();
605✔
2017
      this._logger.debug('Game stopped');
605✔
2018
    }
2019
  }
2020

2021
  /**
2022
   * Returns the Engine's running status, Useful for checking whether engine is running or paused.
2023
   */
2024
  public isRunning() {
2025
    return this.clock.isRunning();
3✔
2026
  }
2027

2028
  private _screenShotRequests: { preserveHiDPIResolution: boolean; resolve: (image: HTMLImageElement) => void }[] = [];
803✔
2029
  /**
2030
   * Takes a screen shot of the current viewport and returns it as an
2031
   * HTML Image Element.
2032
   * @param preserveHiDPIResolution in the case of HiDPI return the full scaled backing image, by default false
2033
   */
2034
  public screenshot(preserveHiDPIResolution = false): Promise<HTMLImageElement> {
3✔
2035
    const screenShotPromise = new Promise<HTMLImageElement>((resolve) => {
9✔
2036
      this._screenShotRequests.push({ preserveHiDPIResolution, resolve });
9✔
2037
    });
2038
    return screenShotPromise;
9✔
2039
  }
2040

2041
  private _checkForScreenShots() {
2042
    // We must grab the draw buffer before we yield to the browser
2043
    // the draw buffer is cleared after compositing
2044
    // the reason for the asynchrony is setting `preserveDrawingBuffer: true`
2045
    // forces the browser to copy buffers which can have a mass perf impact on mobile
2046
    for (const request of this._screenShotRequests) {
927✔
2047
      const finalWidth = request.preserveHiDPIResolution ? this.canvas.width : this.screen.resolution.width;
9✔
2048
      const finalHeight = request.preserveHiDPIResolution ? this.canvas.height : this.screen.resolution.height;
9✔
2049
      const screenshot = document.createElement('canvas');
9✔
2050
      screenshot.width = finalWidth;
9✔
2051
      screenshot.height = finalHeight;
9✔
2052
      const ctx = screenshot.getContext('2d');
9✔
2053
      ctx!.imageSmoothingEnabled = this.screen.antialiasing;
9✔
2054
      ctx!.drawImage(this.canvas, 0, 0, finalWidth, finalHeight);
9✔
2055

2056
      const result = new Image();
9✔
2057
      const raw = screenshot.toDataURL('image/png');
9✔
2058
      result.onload = () => {
9✔
2059
        request.resolve(result);
9✔
2060
      };
2061
      result.src = raw;
9✔
2062
    }
2063
    // Reset state
2064
    this._screenShotRequests.length = 0;
927✔
2065
  }
2066

2067
  /**
2068
   * Another option available to you to load resources into the game.
2069
   * Immediately after calling this the game will pause and the loading screen
2070
   * will appear.
2071
   * @param loader  Some {@apilink Loadable} such as a {@apilink Loader} collection, {@apilink Sound}, or {@apilink Texture}.
2072
   */
2073
  public async load(loader: DefaultLoader, hideLoader = false): Promise<void> {
1,050✔
2074
    await this.scope(async () => {
1,050✔
2075
      try {
1,050✔
2076
        // early exit if loaded
2077
        if (loader.isLoaded()) {
1,050✔
2078
          return;
1,032✔
2079
        }
2080
        this._loader = loader;
18✔
2081
        this._isLoading = true;
18✔
2082
        this._hideLoader = hideLoader;
18✔
2083

2084
        if (loader instanceof Loader) {
18✔
2085
          loader.suppressPlayButton = loader.suppressPlayButton || this._suppressPlayButton;
15✔
2086
        }
2087
        this._loader.onInitialize(this);
18✔
2088

2089
        await loader.load();
18✔
2090
      } catch (e) {
2091
        this._logger.error('Error loading resources, things may not behave properly', e);
1✔
2092
        await Promise.resolve();
1✔
2093
      } finally {
2094
        this._isLoading = false;
1,049✔
2095
        this._hideLoader = false;
1,049✔
2096
        this._loader = null as any;
1,049✔
2097
      }
2098
    });
2099
  }
2100
}
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