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

excaliburjs / Excalibur / 17325181646

29 Aug 2025 01:30PM UTC coverage: 87.353% (-0.02%) from 87.368%
17325181646

push

github

web-flow
fix(Engine): Remove canvas only when it has been created (#3506)

I have noticed this when I have embedded the game inside React with React strict mode on and using the canvasElement option.

5179 of 7245 branches covered (71.48%)

4 of 6 new or added lines in 1 file covered. (66.67%)

1 existing line in 1 file now uncovered.

14021 of 16051 relevant lines covered (87.35%)

24492.81 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

251
  /**
252
   * Optionally configure the global, or a factory to produce it to listen to for browser events for Excalibur to listen to
253
   */
254
  global?: GlobalEventHandlers | (() => GlobalEventHandlers);
255

256
  /**
257
   * Configures the pointer scope. Pointers scoped to the 'Canvas' can only fire events within the canvas viewport; whereas, 'Document'
258
   * (default) scoped will fire anywhere on the page.
259
   */
260
  pointerScope?: PointerScope;
261

262
  /**
263
   * Suppress boot up console message, which contains the "powered by Excalibur message"
264
   */
265
  suppressConsoleBootMessage?: boolean;
266

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

274
  /**
275
   * Suppress HiDPI auto detection and scaling, it is not recommended users of excalibur switch off this feature. This feature detects
276
   * and scales the drawing canvas appropriately to accommodate HiDPI screens.
277
   */
278
  suppressHiDPIScaling?: boolean;
279

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

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

295
  /**
296
   * Scroll prevention method.
297
   */
298
  scrollPreventionMode?: ScrollPreventionMode;
299

300
  /**
301
   * Optionally set the background color
302
   */
303
  backgroundColor?: Color;
304

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

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

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

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

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

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

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

386
  /**
387
   * Optionally configure the physics simulation in excalibur
388
   *
389
   * If false, Excalibur will not produce a physics simulation.
390
   *
391
   * Default is configured to use {@apilink SolverStrategy.Arcade} physics simulation
392
   */
393
  physics?: boolean | PhysicsConfig;
394

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

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

415
    if (!value) {
20✔
416
      throw new Error('Cannot inject engine with `useEngine()`, `useEngine()` was called outside of Engine lifecycle scope.');
1✔
417
    }
418

419
    return value;
19✔
420
  }
421
  static InstanceCount = 0;
422

423
  /**
424
   * Anything run under scope can use `useEngine()` to inject the current engine
425
   * @param cb
426
   */
427
  scope = <TReturn>(cb: () => TReturn) => Engine.Context.scope(this, cb);
3,757✔
428

429
  public global: GlobalEventHandlers;
430

431
  private _garbageCollector: GarbageCollector;
432

433
  public readonly garbageCollectorConfig: GarbageCollectionOptions | null;
434

435
  /**
436
   * Current Excalibur version string
437
   *
438
   * Useful for plugins or other tools that need to know what features are available
439
   */
440
  public readonly version = EX_VERSION;
730✔
441

442
  /**
443
   * Listen to and emit events on the Engine
444
   */
445
  public events = new EventEmitter<EngineEvents>();
730✔
446

447
  /**
448
   * Excalibur browser events abstraction used for wiring to native browser events safely
449
   */
450
  public browser: BrowserEvents;
451

452
  /**
453
   * Screen abstraction
454
   */
455
  public screen: Screen;
456

457
  /**
458
   * Scene director, manages all scenes, scene transitions, and loaders in excalibur
459
   */
460
  public director: Director<TKnownScenes>;
461

462
  /**
463
   * Direct access to the engine's canvas element
464
   */
465
  public canvas: HTMLCanvasElement;
466

467
  /**
468
   * Direct access to the ExcaliburGraphicsContext used for drawing things to the screen
469
   */
470
  public graphicsContext: ExcaliburGraphicsContext;
471

472
  /**
473
   * Direct access to the canvas element ID, if an ID exists
474
   */
475
  public canvasElementId: string;
476

477
  /**
478
   * Direct access to the physics configuration for excalibur
479
   */
480
  public physics: DeepRequired<PhysicsConfig>;
481

482
  /**
483
   * Optionally set the maximum fps if not set Excalibur will go as fast as the device allows.
484
   *
485
   * 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
486
   * one that bounces between 30fps and 60fps
487
   */
488
  public maxFps: number = Number.POSITIVE_INFINITY;
730✔
489

490
  /**
491
   * Optionally configure a fixed update fps, this can be desirable if you need the physics simulation to be very stable. When set
492
   * the update step and physics will use the same elapsed time for each tick even if the graphical framerate drops. In order for the
493
   * simulation to be correct, excalibur will run multiple updates in a row (at the configured update elapsed) to catch up, for example
494
   * there could be X updates and 1 draw each clock step.
495
   *
496
   * **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
497
   * the current instantaneous framerate, or perf gain if the fixed rate is less than the current framerate.
498
   *
499
   * By default is unset and updates will use the current instantaneous framerate with 1 update and 1 draw each clock step.
500
   *
501
   * **WARN:** `fixedUpdateTimestep` takes precedence over `fixedUpdateFps` use whichever is most convenient.
502
   */
503
  public readonly fixedUpdateFps?: number;
504

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

520
  /**
521
   * Direct access to the excalibur clock
522
   */
523
  public clock: Clock;
524

525
  public readonly pointerScope: PointerScope;
526
  public readonly grabWindowFocus: boolean;
527

528
  /**
529
   * The width of the game canvas in pixels (physical width component of the
530
   * resolution of the canvas element)
531
   */
532
  public get canvasWidth(): number {
533
    return this.screen.canvasWidth;
837✔
534
  }
535

536
  /**
537
   * Returns half width of the game canvas in pixels (half physical width component)
538
   */
539
  public get halfCanvasWidth(): number {
540
    return this.screen.halfCanvasWidth;
3✔
541
  }
542

543
  /**
544
   * The height of the game canvas in pixels, (physical height component of
545
   * the resolution of the canvas element)
546
   */
547
  public get canvasHeight(): number {
548
    return this.screen.canvasHeight;
837✔
549
  }
550

551
  /**
552
   * Returns half height of the game canvas in pixels (half physical height component)
553
   */
554
  public get halfCanvasHeight(): number {
555
    return this.screen.halfCanvasHeight;
3✔
556
  }
557

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

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

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

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

586
  /**
587
   * Returns whether excalibur detects the current screen to be HiDPI
588
   */
589
  public get isHiDpi(): boolean {
590
    return this.screen.isHiDpi;
3✔
591
  }
592

593
  /**
594
   * Access engine input like pointer, keyboard, or gamepad
595
   */
596
  public input: InputHost;
597

598
  /**
599
   * Map multiple input sources to specific game actions actions
600
   */
601
  public inputMapper: InputMapper;
602

603
  private _inputEnabled: boolean = true;
730✔
604

605
  /**
606
   * Access Excalibur debugging functionality.
607
   *
608
   * Useful when you want to debug different aspects of built in engine features like
609
   *   * Transform
610
   *   * Graphics
611
   *   * Colliders
612
   */
613
  public debug: DebugConfig;
614

615
  /**
616
   * Access {@apilink stats} that holds frame statistics.
617
   */
618
  public get stats(): DebugStats {
619
    return this.debug.stats;
24,879✔
620
  }
621

622
  /**
623
   * The current {@apilink Scene} being drawn and updated on screen
624
   */
625
  public get currentScene(): Scene {
626
    return this.director.currentScene;
7,769✔
627
  }
628

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

636
  /**
637
   * The default {@apilink Scene} of the game, use {@apilink Engine.goToScene} to transition to different scenes.
638
   */
639
  public get rootScene(): Scene {
640
    return this.director.rootScene;
1✔
641
  }
642

643
  /**
644
   * Contains all the scenes currently registered with Excalibur
645
   */
646
  public get scenes(): { [key: string]: Scene | SceneConstructor | SceneWithOptions } {
647
    return this.director.scenes;
4✔
648
  }
649

650
  /**
651
   * Indicates whether the engine is set to fullscreen or not
652
   */
653
  public get isFullscreen(): boolean {
654
    return this.screen.isFullScreen;
1✔
655
  }
656

657
  /**
658
   * Indicates the current {@apilink DisplayMode} of the engine.
659
   */
660
  public get displayMode(): DisplayMode {
661
    return this.screen.displayMode;
×
662
  }
663

664
  private _suppressPlayButton: boolean = false;
730✔
665
  /**
666
   * Returns the calculated pixel ration for use in rendering
667
   */
668
  public get pixelRatio(): number {
669
    return this.screen.pixelRatio;
1,673✔
670
  }
671

672
  /**
673
   * Indicates whether audio should be paused when the game is no longer visible.
674
   */
675
  public pauseAudioWhenHidden: boolean = true;
730✔
676

677
  /**
678
   * Indicates whether the engine should draw with debug information
679
   */
680
  private _isDebug: boolean = false;
730✔
681
  public get isDebug(): boolean {
682
    return this._isDebug;
3,887✔
683
  }
684

685
  /**
686
   * Sets the background color for the engine.
687
   */
688
  public backgroundColor: Color;
689

690
  /**
691
   * Sets the Transparency for the engine.
692
   */
693
  public enableCanvasTransparency: boolean = true;
730✔
694

695
  /**
696
   * Hints the graphics context to truncate fractional world space coordinates
697
   */
698
  public get snapToPixel(): boolean {
699
    return this.graphicsContext.snapToPixel;
2✔
700
  }
701

702
  public set snapToPixel(shouldSnapToPixel: boolean) {
703
    this.graphicsContext.snapToPixel = shouldSnapToPixel;
1✔
704
  }
705

706
  /**
707
   * The action to take when a fatal exception is thrown
708
   */
709
  public onFatalException = (e: any) => {
730✔
710
    Logger.getInstance().fatal(e, e.stack);
×
711
  };
712

713
  /**
714
   * The mouse wheel scroll prevention mode
715
   */
716
  public pageScrollPreventionMode: ScrollPreventionMode;
717

718
  private _logger: Logger;
719

720
  private _toaster: Toaster = new Toaster();
730✔
721

722
  // this determines whether excalibur is compatible with your browser
723
  private _compatible: boolean;
724

725
  private _timescale: number = 1.0;
730✔
726

727
  // loading
728
  private _loader: DefaultLoader;
729

730
  private _isInitialized: boolean = false;
730✔
731

732
  private _hasCreatedCanvas: boolean = false;
730✔
733

734
  public emit<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, event: EngineEvents[TEventName]): void;
735
  public emit(eventName: string, event?: any): void;
736
  public emit<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, event?: any): void {
737
    this.events.emit(eventName, event);
9,130✔
738
  }
739

740
  public on<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): Subscription;
741
  public on(eventName: string, handler: Handler<unknown>): Subscription;
742
  public on<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
743
    return this.events.on(eventName, handler);
29✔
744
  }
745

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

752
  public off<TEventName extends EventKey<EngineEvents>>(eventName: TEventName, handler: Handler<EngineEvents[TEventName]>): void;
753
  public off(eventName: string, handler: Handler<unknown>): void;
754
  public off(eventName: string): void;
755
  public off<TEventName extends EventKey<EngineEvents> | string>(eventName: TEventName, handler?: Handler<any>): void {
756
    this.events.off(eventName, handler);
×
757
  }
758

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

790
  private _originalOptions: EngineOptions = {};
730✔
791
  public readonly _originalDisplayMode: DisplayMode;
792

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

821
    Flags.freeze();
730✔
822

823
    // Initialize browser events facade
824
    this.browser = new BrowserEvents(window, document);
730✔
825

826
    // Check compatibility
827
    const detector = new Detector();
730✔
828
    if (!options.suppressMinimumBrowserFeatureDetection && !(this._compatible = detector.test())) {
730!
829
      const message = document.createElement('div');
×
830
      message.innerText = 'Sorry, your browser does not support all the features needed for Excalibur';
×
831
      document.body.appendChild(message);
×
832

833
      detector.failedTests.forEach(function (test) {
×
834
        const testMessage = document.createElement('div');
×
835
        testMessage.innerText = 'Browser feature missing ' + test;
×
836
        document.body.appendChild(testMessage);
×
837
      });
838

839
      if (options.canvasElementId) {
×
840
        const canvas = document.getElementById(options.canvasElementId);
×
841
        if (canvas) {
×
842
          canvas.parentElement.removeChild(canvas);
×
843
        }
844
      }
845

846
      return;
×
847
    } else {
848
      this._compatible = true;
730✔
849
    }
850

851
    // Use native console API for color fun
852
    // eslint-disable-next-line no-console
853
    if (console.log && !options.suppressConsoleBootMessage) {
730✔
854
      // eslint-disable-next-line no-console
855
      console.log(
3✔
856
        `%cPowered by Excalibur.js (v${EX_VERSION})`,
857
        'background: #176BAA; color: white; border-radius: 5px; padding: 15px; font-size: 1.5em; line-height: 80px;'
858
      );
859
      // eslint-disable-next-line no-console
860
      console.log(
3✔
861
        '\n\
862
      /| ________________\n\
863
O|===|* >________________>\n\
864
      \\|'
865
      );
866
      // eslint-disable-next-line no-console
867
      console.log('Visit', 'http://excaliburjs.com', 'for more information');
3✔
868
    }
869

870
    // Suppress play button
871
    if (options.suppressPlayButton) {
730✔
872
      this._suppressPlayButton = true;
718✔
873
    }
874

875
    this._logger = Logger.getInstance();
730✔
876

877
    // If debug is enabled, let's log browser features to the console.
878
    if (this._logger.defaultLevel === LogLevel.Debug) {
730!
879
      detector.logBrowserFeatures();
×
880
    }
881

882
    this._logger.debug('Building engine...');
730✔
883
    if (options.garbageCollection === true) {
730!
884
      this.garbageCollectorConfig = {
730✔
885
        ...DefaultGarbageCollectionOptions
886
      };
887
    } else if (options.garbageCollection === false) {
×
888
      this._logger.warn(
×
889
        'WebGL Garbage Collection Disabled!!! If you leak any images over time your game will crash when GPU memory is exhausted'
890
      );
891
      this.garbageCollectorConfig = null;
×
892
    } else {
893
      this.garbageCollectorConfig = {
×
894
        ...DefaultGarbageCollectionOptions,
895
        ...options.garbageCollection
896
      };
897
    }
898
    this._garbageCollector = new GarbageCollector({ getTimestamp: Date.now });
730✔
899

900
    this.canvasElementId = options.canvasElementId;
730✔
901

902
    if (options.canvasElementId) {
730!
903
      this._logger.debug('Using Canvas element specified: ' + options.canvasElementId);
×
904

905
      //test for existence of element
906
      if (document.getElementById(options.canvasElementId) === null) {
×
907
        throw new Error('Cannot find existing element in the DOM, please ensure element is created prior to engine creation.');
×
908
      }
909

910
      this.canvas = <HTMLCanvasElement>document.getElementById(options.canvasElementId);
×
NEW
911
      this._hasCreatedCanvas = false;
×
912
    } else if (options.canvasElement) {
730!
913
      this._logger.debug('Using Canvas element specified:', options.canvasElement);
×
914
      this.canvas = options.canvasElement;
×
NEW
915
      this._hasCreatedCanvas = false;
×
916
    } else {
917
      this._logger.debug('Using generated canvas element');
730✔
918
      this.canvas = <HTMLCanvasElement>document.createElement('canvas');
730✔
919
      this._hasCreatedCanvas = true;
730✔
920
    }
921

922
    if (this.canvas && !options.enableCanvasContextMenu) {
730✔
923
      this.canvas.addEventListener('contextmenu', (evt) => {
729✔
924
        evt.preventDefault();
1✔
925
      });
926
    }
927

928
    let displayMode = options.displayMode ?? DisplayMode.Fixed;
730✔
929
    if ((options.width && options.height) || options.viewport) {
730✔
930
      if (options.displayMode === undefined) {
725✔
931
        displayMode = DisplayMode.Fixed;
6✔
932
      }
933
      this._logger.debug('Engine viewport is size ' + options.width + ' x ' + options.height);
725✔
934
    } else if (!options.displayMode) {
5✔
935
      this._logger.debug('Engine viewport is fit');
2✔
936
      displayMode = DisplayMode.FitScreen;
2✔
937
    }
938

939
    const global = (options.global && typeof options.global === 'function' ? options.global() : options.global) as GlobalEventHandlers;
730!
940

941
    this.global = global ?? getDefaultGlobal();
730!
942
    this.grabWindowFocus = options.grabWindowFocus;
730✔
943
    this.pointerScope = options.pointerScope;
730✔
944

945
    this._originalDisplayMode = displayMode;
730✔
946

947
    let pixelArtSampler: boolean;
948
    let uvPadding: number;
949
    let nativeContextAntialiasing: boolean;
950
    let canvasImageRendering: 'pixelated' | 'auto';
951
    let filtering: ImageFiltering;
952
    let multiSampleAntialiasing: boolean | { samples: number };
953
    if (typeof options.antialiasing === 'object') {
730!
954
      ({ pixelArtSampler, nativeContextAntialiasing, multiSampleAntialiasing, filtering, canvasImageRendering } = {
×
955
        ...(options.pixelArt ? DefaultPixelArtOptions : DefaultAntialiasOptions),
×
956
        ...options.antialiasing
957
      });
958
    } else {
959
      pixelArtSampler = !!options.pixelArt;
730✔
960
      nativeContextAntialiasing = false;
730✔
961
      multiSampleAntialiasing = options.antialiasing;
730✔
962
      canvasImageRendering = options.antialiasing ? 'auto' : 'pixelated';
730✔
963
      filtering = options.antialiasing ? ImageFiltering.Blended : ImageFiltering.Pixel;
730✔
964
    }
965

966
    if (nativeContextAntialiasing && multiSampleAntialiasing) {
730!
967
      this._logger.warnOnce(
×
968
        `Cannot use antialias setting nativeContextAntialiasing and multiSampleAntialiasing` +
969
          ` at the same time, they are incompatible settings. If you aren\'t sure use multiSampleAntialiasing`
970
      );
971
    }
972

973
    if (options.pixelArt) {
730✔
974
      uvPadding = 0.25;
1✔
975
    }
976

977
    if (!options.antialiasing || filtering === ImageFiltering.Pixel) {
730✔
978
      uvPadding = 0;
721✔
979
    }
980

981
    // Override with any user option, if non default to .25 for pixel art, 0.01 for everything else
982
    uvPadding = options.uvPadding ?? uvPadding ?? 0.01;
730!
983

984
    // Canvas 2D fallback can be flagged on
985
    let useCanvasGraphicsContext = Flags.isEnabled('use-canvas-context');
730✔
986
    if (!useCanvasGraphicsContext) {
730✔
987
      // Attempt webgl first
988
      try {
728✔
989
        this.graphicsContext = new ExcaliburGraphicsContextWebGL({
728✔
990
          canvasElement: this.canvas,
991
          enableTransparency: this.enableCanvasTransparency,
992
          pixelArtSampler: pixelArtSampler,
993
          antialiasing: nativeContextAntialiasing,
994
          multiSampleAntialiasing: multiSampleAntialiasing,
995
          uvPadding: uvPadding,
996
          powerPreference: options.powerPreference,
997
          backgroundColor: options.backgroundColor,
998
          snapToPixel: options.snapToPixel,
999
          useDrawSorting: options.useDrawSorting,
1000
          garbageCollector: this.garbageCollectorConfig
728!
1001
            ? {
1002
                garbageCollector: this._garbageCollector,
1003
                collectionInterval: this.garbageCollectorConfig.textureCollectInterval
1004
              }
1005
            : null,
1006
          handleContextLost: options.handleContextLost ?? this._handleWebGLContextLost,
728!
1007
          handleContextRestored: options.handleContextRestored
1008
        });
1009
      } catch (e) {
1010
        this._logger.warn(
×
1011
          `Excalibur could not load webgl for some reason (${(e as Error).message}) and loaded a Canvas 2D fallback. ` +
1012
            `Some features of Excalibur will not work in this mode. \n\n` +
1013
            'Read more about this issue at https://excaliburjs.com/docs/performance'
1014
        );
1015
        // fallback to canvas in case of failure
1016
        useCanvasGraphicsContext = true;
×
1017
      }
1018
    }
1019

1020
    if (useCanvasGraphicsContext) {
730✔
1021
      this.graphicsContext = new ExcaliburGraphicsContext2DCanvas({
2✔
1022
        canvasElement: this.canvas,
1023
        enableTransparency: this.enableCanvasTransparency,
1024
        antialiasing: nativeContextAntialiasing,
1025
        backgroundColor: options.backgroundColor,
1026
        snapToPixel: options.snapToPixel,
1027
        useDrawSorting: options.useDrawSorting
1028
      });
1029
    }
1030

1031
    this.screen = new Screen({
730✔
1032
      canvas: this.canvas,
1033
      context: this.graphicsContext,
1034
      antialiasing: nativeContextAntialiasing,
1035
      canvasImageRendering: canvasImageRendering,
1036
      browser: this.browser,
1037
      viewport: options.viewport ?? (options.width && options.height ? { width: options.width, height: options.height } : Resolution.SVGA),
2,903✔
1038
      resolution: options.resolution,
1039
      displayMode,
1040
      pixelRatio: options.suppressHiDPIScaling ? 1 : (options.pixelRatio ?? null)
743✔
1041
    });
1042

1043
    // TODO REMOVE STATIC!!!
1044
    // Set default filtering based on antialiasing
1045
    TextureLoader.filtering = filtering;
730✔
1046

1047
    if (options.backgroundColor) {
730!
1048
      this.backgroundColor = options.backgroundColor.clone();
730✔
1049
    }
1050

1051
    this.maxFps = options.maxFps ?? this.maxFps;
730!
1052

1053
    this.fixedUpdateTimestep = options.fixedUpdateTimestep ?? this.fixedUpdateTimestep;
730!
1054
    this.fixedUpdateFps = options.fixedUpdateFps ?? this.fixedUpdateFps;
730✔
1055
    this.fixedUpdateTimestep = this.fixedUpdateTimestep || 1000 / this.fixedUpdateFps;
730✔
1056

1057
    this.clock = new StandardClock({
730✔
1058
      maxFps: this.maxFps,
1059
      tick: this._mainloop.bind(this),
1060
      onFatalException: (e) => this.onFatalException(e)
×
1061
    });
1062

1063
    this.enableCanvasTransparency = options.enableCanvasTransparency;
730✔
1064

1065
    if (typeof options.physics === 'boolean') {
730!
1066
      this.physics = {
×
1067
        ...getDefaultPhysicsConfig(),
1068
        enabled: options.physics
1069
      };
1070
    } else {
1071
      this.physics = {
730✔
1072
        ...getDefaultPhysicsConfig()
1073
      };
1074
      mergeDeep(this.physics, options.physics);
730✔
1075
    }
1076

1077
    this.debug = new DebugConfig(this);
730✔
1078

1079
    this.director = new Director(this, options.scenes);
730✔
1080
    this.director.events.pipe(this.events);
730✔
1081

1082
    this._initialize(options);
730✔
1083

1084
    (window as any).___EXCALIBUR_DEVTOOL = this;
730✔
1085
    Engine.InstanceCount++;
730✔
1086
  }
1087

1088
  private _handleWebGLContextLost = (e: Event) => {
730✔
1089
    e.preventDefault();
765✔
1090
    this.clock.stop();
765✔
1091
    this._logger.fatalOnce('WebGL Graphics Lost', e);
765✔
1092
    const container = document.createElement('div');
765✔
1093
    container.id = 'ex-webgl-graphics-context-lost';
765✔
1094
    container.style.position = 'absolute';
765✔
1095
    container.style.zIndex = '99';
765✔
1096
    container.style.left = '50%';
765✔
1097
    container.style.top = '50%';
765✔
1098
    container.style.display = 'flex';
765✔
1099
    container.style.flexDirection = 'column';
765✔
1100
    container.style.transform = 'translate(-50%, -50%)';
765✔
1101
    container.style.backgroundColor = 'white';
765✔
1102
    container.style.padding = '10px';
765✔
1103
    container.style.borderStyle = 'solid 1px';
765✔
1104

1105
    const div = document.createElement('div');
765✔
1106
    div.innerHTML = `
765✔
1107
      <h1>There was an issue rendering, please refresh the page.</h1>
1108
      <div>
1109
        <p>WebGL Graphics Context Lost</p>
1110

1111
        <button id="ex-webgl-graphics-reload">Refresh Page</button>
1112

1113
        <p>There are a few reasons this might happen:</p>
1114
        <ul>
1115
          <li>Two or more pages are placing a high demand on the GPU</li>
1116
          <li>Another page or operation has stalled the GPU and the browser has decided to reset the GPU</li>
1117
          <li>The computer has multiple GPUs and the user has switched between them</li>
1118
          <li>Graphics driver has crashed or restarted</li>
1119
          <li>Graphics driver was updated</li>
1120
        </ul>
1121
      </div>
1122
    `;
1123
    container.appendChild(div);
765✔
1124
    if (this.canvas?.parentElement) {
765!
1125
      this.canvas.parentElement.appendChild(container);
×
1126
      const button = div.querySelector('#ex-webgl-graphics-reload');
×
1127
      button?.addEventListener('click', () => location.reload());
×
1128
    }
1129
  };
1130

1131
  private _performanceThresholdTriggered = false;
730✔
1132
  private _fpsSamples: number[] = [];
730✔
1133
  private _monitorPerformanceThresholdAndTriggerFallback() {
1134
    const { allow } = this._originalOptions.configurePerformanceCanvas2DFallback;
1,745✔
1135
    let { threshold, showPlayerMessage } = this._originalOptions.configurePerformanceCanvas2DFallback;
1,745✔
1136
    if (threshold === undefined) {
1,745✔
1137
      threshold = Engine._DEFAULT_ENGINE_OPTIONS.configurePerformanceCanvas2DFallback.threshold;
100✔
1138
    }
1139
    if (showPlayerMessage === undefined) {
1,745✔
1140
      showPlayerMessage = Engine._DEFAULT_ENGINE_OPTIONS.configurePerformanceCanvas2DFallback.showPlayerMessage;
100✔
1141
    }
1142
    if (!Flags.isEnabled('use-canvas-context') && allow && this.ready && !this._performanceThresholdTriggered) {
1,745✔
1143
      // Calculate Average fps for last X number of frames after start
1144
      if (this._fpsSamples.length === threshold.numberOfFrames) {
100!
1145
        this._fpsSamples.splice(0, 1);
×
1146
      }
1147
      this._fpsSamples.push(this.clock.fpsSampler.fps);
100✔
1148
      let total = 0;
100✔
1149
      for (let i = 0; i < this._fpsSamples.length; i++) {
100✔
1150
        total += this._fpsSamples[i];
5,050✔
1151
      }
1152
      const average = total / this._fpsSamples.length;
100✔
1153

1154
      if (this._fpsSamples.length === threshold.numberOfFrames) {
100✔
1155
        if (average <= threshold.fps) {
1!
1156
          this._performanceThresholdTriggered = true;
1✔
1157
          this._logger.warn(
1✔
1158
            `Switching to browser 2D Canvas fallback due to performance. Some features of Excalibur will not work in this mode.\n` +
1159
              "this might mean your browser doesn't have webgl enabled or hardware acceleration is unavailable.\n\n" +
1160
              'If in Chrome:\n' +
1161
              '  * Visit Settings > Advanced > System, and ensure "Use Hardware Acceleration" is checked.\n' +
1162
              '  * Visit chrome://flags/#ignore-gpu-blocklist and ensure "Override software rendering list" is "enabled"\n' +
1163
              'If in Firefox, visit about:config\n' +
1164
              '  * Ensure webgl.disabled = false\n' +
1165
              '  * Ensure webgl.force-enabled = true\n' +
1166
              '  * Ensure layers.acceleration.force-enabled = true\n\n' +
1167
              'Read more about this issue at https://excaliburjs.com/docs/performance'
1168
          );
1169

1170
          if (showPlayerMessage) {
1!
1171
            this._toaster.toast(
×
1172
              'Excalibur is encountering performance issues. ' +
1173
                "It's possible that your browser doesn't have hardware acceleration enabled. " +
1174
                'Visit [LINK] for more information and potential solutions.',
1175
              'https://excaliburjs.com/docs/performance'
1176
            );
1177
          }
1178
          this.useCanvas2DFallback();
1✔
1179
          this.emit('fallbackgraphicscontext', this.graphicsContext);
1✔
1180
        }
1181
      }
1182
    }
1183
  }
1184

1185
  /**
1186
   * Switches the engine's graphics context to the 2D Canvas.
1187
   * @warning Some features of Excalibur will not work in this mode.
1188
   */
1189
  public useCanvas2DFallback() {
1190
    // Swap out the canvas
1191
    const newCanvas = this.canvas.cloneNode(false) as HTMLCanvasElement;
2✔
1192
    this.canvas.parentNode.replaceChild(newCanvas, this.canvas);
2✔
1193
    this.canvas = newCanvas;
2✔
1194

1195
    const options = { ...this._originalOptions, antialiasing: this.screen.antialiasing };
2✔
1196
    const displayMode = this._originalDisplayMode;
2✔
1197

1198
    // New graphics context
1199
    this.graphicsContext = new ExcaliburGraphicsContext2DCanvas({
2✔
1200
      canvasElement: this.canvas,
1201
      enableTransparency: this.enableCanvasTransparency,
1202
      antialiasing: options.antialiasing,
1203
      backgroundColor: options.backgroundColor,
1204
      snapToPixel: options.snapToPixel,
1205
      useDrawSorting: options.useDrawSorting
1206
    });
1207

1208
    // Reset screen
1209
    if (this.screen) {
2!
1210
      this.screen.dispose();
2✔
1211
    }
1212

1213
    this.screen = new Screen({
2✔
1214
      canvas: this.canvas,
1215
      context: this.graphicsContext,
1216
      antialiasing: options.antialiasing ?? true,
2!
1217
      browser: this.browser,
1218
      viewport: options.viewport ?? (options.width && options.height ? { width: options.width, height: options.height } : Resolution.SVGA),
8!
1219
      resolution: options.resolution,
1220
      displayMode,
1221
      pixelRatio: options.suppressHiDPIScaling ? 1 : (options.pixelRatio ?? null)
2!
1222
    });
1223
    this.screen.setCurrentCamera(this.currentScene.camera);
2✔
1224

1225
    // Reset pointers
1226
    this.input.pointers.detach();
2✔
1227
    const pointerTarget = options && options.pointerScope === PointerScope.Document ? document : this.canvas;
2!
1228
    this.input.pointers = this.input.pointers.recreate(pointerTarget, this);
2✔
1229
    this.input.pointers.init();
2✔
1230
  }
1231

1232
  private _disposed = false;
730✔
1233
  /**
1234
   * Attempts to completely clean up excalibur resources, including removing the canvas from the dom.
1235
   *
1236
   * To start again you will need to new up an Engine.
1237
   */
1238
  public dispose() {
1239
    if (!this._disposed) {
733✔
1240
      this._disposed = true;
729✔
1241
      this.stop();
729✔
1242
      this._garbageCollector.forceCollectAll();
729✔
1243
      this.input.toggleEnabled(false);
729✔
1244
      if (this._hasCreatedCanvas) {
729!
1245
        this.canvas.parentNode.removeChild(this.canvas);
729✔
1246
      }
1247
      this.canvas = null;
729✔
1248
      this.screen.dispose();
729✔
1249
      this.graphicsContext.dispose();
729✔
1250
      this.graphicsContext = null;
729✔
1251
      Engine.InstanceCount--;
729✔
1252
    }
1253
  }
1254

1255
  public isDisposed() {
1256
    return this._disposed;
1,415✔
1257
  }
1258

1259
  /**
1260
   * Returns a BoundingBox of the top left corner of the screen
1261
   * and the bottom right corner of the screen.
1262
   */
1263
  public getWorldBounds() {
1264
    return this.screen.getWorldBounds();
1✔
1265
  }
1266

1267
  /**
1268
   * Gets the current engine timescale factor (default is 1.0 which is 1:1 time)
1269
   */
1270
  public get timescale() {
1271
    return this._timescale;
1,746✔
1272
  }
1273

1274
  /**
1275
   * Sets the current engine timescale factor. Useful for creating slow-motion effects or fast-forward effects
1276
   * when using time-based movement.
1277
   */
1278
  public set timescale(value: number) {
1279
    if (value < 0) {
2!
1280
      Logger.getInstance().warnOnce('engine.timescale to a value less than 0 are ignored');
×
1281
      return;
×
1282
    }
1283

1284
    this._timescale = value;
2✔
1285
  }
1286

1287
  /**
1288
   * Adds a {@apilink Timer} to the {@apilink currentScene}.
1289
   * @param timer  The timer to add to the {@apilink currentScene}.
1290
   */
1291
  public addTimer(timer: Timer): Timer {
1292
    return this.currentScene.addTimer(timer);
×
1293
  }
1294

1295
  /**
1296
   * Removes a {@apilink Timer} from the {@apilink currentScene}.
1297
   * @param timer  The timer to remove to the {@apilink currentScene}.
1298
   */
1299
  public removeTimer(timer: Timer): Timer {
1300
    return this.currentScene.removeTimer(timer);
×
1301
  }
1302

1303
  /**
1304
   * Adds a {@apilink Scene} to the engine, think of scenes in Excalibur as you
1305
   * would levels or menus.
1306
   * @param key  The name of the scene, must be unique
1307
   * @param scene The scene to add to the engine
1308
   */
1309
  public addScene<TScene extends string>(key: TScene, scene: Scene | SceneConstructor | SceneWithOptions): Engine<TKnownScenes | TScene> {
1310
    this.director.add(key, scene);
405✔
1311
    return this as Engine<TKnownScenes | TScene>;
405✔
1312
  }
1313

1314
  /**
1315
   * Removes a {@apilink Scene} instance from the engine
1316
   * @param scene  The scene to remove
1317
   */
1318
  public removeScene(scene: Scene | SceneConstructor): void;
1319
  /**
1320
   * Removes a scene from the engine by key
1321
   * @param key  The scene key to remove
1322
   */
1323
  public removeScene(key: string): void;
1324
  /**
1325
   * @internal
1326
   */
1327
  public removeScene(entity: any): void {
1328
    this.director.remove(entity);
8✔
1329
  }
1330

1331
  /**
1332
   * Adds a {@apilink Scene} to the engine, think of scenes in Excalibur as you
1333
   * would levels or menus.
1334
   * @param sceneKey  The key of the scene, must be unique
1335
   * @param scene     The scene to add to the engine
1336
   */
1337
  public add(sceneKey: string, scene: Scene | SceneConstructor | SceneWithOptions): void;
1338
  /**
1339
   * Adds a {@apilink Timer} to the {@apilink currentScene}.
1340
   * @param timer  The timer to add to the {@apilink currentScene}.
1341
   */
1342
  public add(timer: Timer): void;
1343
  /**
1344
   * Adds a {@apilink TileMap} to the {@apilink currentScene}, once this is done the TileMap
1345
   * will be drawn and updated.
1346
   */
1347
  public add(tileMap: TileMap): void;
1348
  /**
1349
   * Adds an actor to the {@apilink currentScene} of the game. This is synonymous
1350
   * to calling `engine.currentScene.add(actor)`.
1351
   *
1352
   * Actors can only be drawn if they are a member of a scene, and only
1353
   * the {@apilink currentScene} may be drawn or updated.
1354
   * @param actor  The actor to add to the {@apilink currentScene}
1355
   */
1356
  public add(actor: Actor): void;
1357

1358
  public add(entity: Entity): void;
1359

1360
  /**
1361
   * Adds a {@apilink ScreenElement} to the {@apilink currentScene} of the game,
1362
   * ScreenElements do not participate in collisions, instead the
1363
   * remain in the same place on the screen.
1364
   * @param screenElement  The ScreenElement to add to the {@apilink currentScene}
1365
   */
1366
  public add(screenElement: ScreenElement): void;
1367
  public add(entity: any): void {
1368
    if (arguments.length === 2) {
264✔
1369
      this.director.add(<string>arguments[0], <Scene | SceneConstructor | SceneWithOptions>arguments[1]);
95✔
1370
      return;
95✔
1371
    }
1372
    const maybeDeferred = this.director.getDeferredScene();
169✔
1373
    if (maybeDeferred instanceof Scene) {
169!
1374
      maybeDeferred.add(entity);
×
1375
    } else {
1376
      this.currentScene.add(entity);
169✔
1377
    }
1378
  }
1379

1380
  /**
1381
   * Removes a scene instance from the engine
1382
   * @param scene  The scene to remove
1383
   */
1384
  public remove(scene: Scene | SceneConstructor): void;
1385
  /**
1386
   * Removes a scene from the engine by key
1387
   * @param sceneKey  The scene to remove
1388
   */
1389
  public remove(sceneKey: string): void;
1390
  /**
1391
   * Removes a {@apilink Timer} from the {@apilink currentScene}.
1392
   * @param timer  The timer to remove to the {@apilink currentScene}.
1393
   */
1394
  public remove(timer: Timer): void;
1395
  /**
1396
   * Removes a {@apilink TileMap} from the {@apilink currentScene}, it will no longer be drawn or updated.
1397
   */
1398
  public remove(tileMap: TileMap): void;
1399
  /**
1400
   * Removes an actor from the {@apilink currentScene} of the game. This is synonymous
1401
   * to calling `engine.currentScene.removeChild(actor)`.
1402
   * Actors that are removed from a scene will no longer be drawn or updated.
1403
   * @param actor  The actor to remove from the {@apilink currentScene}.
1404
   */
1405
  public remove(actor: Actor): void;
1406
  /**
1407
   * Removes a {@apilink ScreenElement} to the scene, it will no longer be drawn or updated
1408
   * @param screenElement  The ScreenElement to remove from the {@apilink currentScene}
1409
   */
1410
  public remove(screenElement: ScreenElement): void;
1411
  public remove(entity: any): void {
1412
    if (entity instanceof Entity) {
4✔
1413
      this.currentScene.remove(entity);
2✔
1414
    }
1415

1416
    if (entity instanceof Scene || isSceneConstructor(entity)) {
4✔
1417
      this.removeScene(entity);
1✔
1418
    }
1419

1420
    if (typeof entity === 'string') {
4✔
1421
      this.removeScene(entity);
1✔
1422
    }
1423
  }
1424

1425
  /**
1426
   * Changes the current scene with optionally supplied:
1427
   * * Activation data
1428
   * * Transitions
1429
   * * Loaders
1430
   *
1431
   * Example:
1432
   * ```typescript
1433
   * game.goToScene('myScene', {
1434
   *   sceneActivationData: {any: 'thing at all'},
1435
   *   destinationIn: new FadeInOut({duration: 1000, direction: 'in'}),
1436
   *   sourceOut: new FadeInOut({duration: 1000, direction: 'out'}),
1437
   *   loader: MyLoader
1438
   * });
1439
   * ```
1440
   *
1441
   * Scenes are defined in the Engine constructor
1442
   * ```typescript
1443
   * const engine = new ex.Engine({
1444
      scenes: {...}
1445
    });
1446
   * ```
1447
   * Or by adding dynamically
1448
   *
1449
   * ```typescript
1450
   * engine.addScene('myScene', new ex.Scene());
1451
   * ```
1452
   * @param destinationScene
1453
   * @param options
1454
   */
1455
  public async goToScene<TData = undefined>(destinationScene: WithRoot<TKnownScenes>, options?: GoToOptions<TData>): Promise<void> {
1456
    await this.scope(async () => {
423✔
1457
      await this.director.goToScene(destinationScene, options);
423✔
1458
    });
1459
  }
1460

1461
  /**
1462
   * Transforms the current x, y from screen coordinates to world coordinates
1463
   * @param point  Screen coordinate to convert
1464
   */
1465
  public screenToWorldCoordinates(point: Vector): Vector {
1466
    return this.screen.screenToWorldCoordinates(point);
2✔
1467
  }
1468

1469
  /**
1470
   * Transforms a world coordinate, to a screen coordinate
1471
   * @param point  World coordinate to convert
1472
   */
1473
  public worldToScreenCoordinates(point: Vector): Vector {
1474
    return this.screen.worldToScreenCoordinates(point);
3✔
1475
  }
1476

1477
  /**
1478
   * Initializes the internal canvas, rendering context, display mode, and native event listeners
1479
   */
1480
  private _initialize(options?: EngineOptions) {
1481
    this.pageScrollPreventionMode = options.scrollPreventionMode;
730✔
1482

1483
    // initialize inputs
1484
    const pointerTarget = options && options.pointerScope === PointerScope.Document ? document : this.canvas;
730✔
1485
    const grabWindowFocus = this._originalOptions?.grabWindowFocus ?? true;
730!
1486
    this.input = new InputHost({
730✔
1487
      global: this.global,
1488
      pointerTarget,
1489
      grabWindowFocus,
1490
      engine: this
1491
    });
1492
    this.inputMapper = this.input.inputMapper;
730✔
1493

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

1497
    this.browser.document.on('visibilitychange', () => {
730✔
1498
      if (document.visibilityState === 'hidden') {
20✔
1499
        this.events.emit('hidden', new HiddenEvent(this));
10✔
1500
        this._logger.debug('Window hidden');
10✔
1501
      } else if (document.visibilityState === 'visible') {
10!
1502
        this.events.emit('visible', new VisibleEvent(this));
10✔
1503
        this._logger.debug('Window visible');
10✔
1504
      }
1505
    });
1506

1507
    if (!this.canvasElementId && !options.canvasElement) {
730!
1508
      document.body.appendChild(this.canvas);
730✔
1509
    }
1510
  }
1511

1512
  public toggleInputEnabled(enabled: boolean) {
1513
    this._inputEnabled = enabled;
×
1514
    this.input.toggleEnabled(this._inputEnabled);
×
1515
  }
1516

1517
  public onInitialize(engine: Engine) {
1518
    // Override me
1519
  }
1520

1521
  /**
1522
   * Gets whether the actor is Initialized
1523
   */
1524
  public get isInitialized(): boolean {
1525
    return this._isInitialized;
569✔
1526
  }
1527

1528
  private async _overrideInitialize(engine: Engine) {
1529
    if (!this.isInitialized) {
569✔
1530
      await this.director.onInitialize();
560✔
1531
      await this.onInitialize(engine);
560✔
1532
      this.events.emit('initialize', new InitializeEvent(engine, this));
560✔
1533
      this._isInitialized = true;
560✔
1534
    }
1535
  }
1536

1537
  /**
1538
   * Updates the entire state of the game
1539
   * @param elapsed  Number of milliseconds elapsed since the last update.
1540
   */
1541
  private _update(elapsed: number) {
1542
    if (this._isLoading) {
1,743✔
1543
      // suspend updates until loading is finished
1544
      this._loader?.onUpdate(this, elapsed);
829!
1545
      // Update input listeners
1546
      this.input.update();
829✔
1547
      return;
829✔
1548
    }
1549

1550
    // Publish preupdate events
1551
    this.clock.__runScheduledCbs('preupdate');
914✔
1552
    this._preupdate(elapsed);
914✔
1553

1554
    // process engine level events
1555
    this.currentScene.update(this, elapsed);
914✔
1556

1557
    // Update graphics postprocessors
1558
    this.graphicsContext.updatePostProcessors(elapsed);
914✔
1559

1560
    // Publish update event
1561
    this.clock.__runScheduledCbs('postupdate');
914✔
1562
    this._postupdate(elapsed);
914✔
1563

1564
    // Update input listeners
1565
    this.input.update();
914✔
1566
  }
1567

1568
  /**
1569
   * @internal
1570
   */
1571
  public _preupdate(elapsed: number) {
1572
    this.emit('preupdate', new PreUpdateEvent(this, elapsed, this));
914✔
1573
    this.onPreUpdate(this, elapsed);
914✔
1574
  }
1575

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

1585
  /**
1586
   * @internal
1587
   */
1588
  public _postupdate(elapsed: number) {
1589
    this.emit('postupdate', new PostUpdateEvent(this, elapsed, this));
914✔
1590
    this.onPostUpdate(this, elapsed);
914✔
1591
  }
1592

1593
  /**
1594
   * Safe to override method
1595
   * @param engine The reference to the current game engine
1596
   * @param elapsed  The time elapsed since the last update in milliseconds
1597
   */
1598
  public onPostUpdate(engine: Engine, elapsed: number) {
1599
    // Override me
1600
  }
1601

1602
  /**
1603
   * Draws the entire game
1604
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1605
   */
1606
  private _draw(elapsed: number) {
1607
    // Use scene background color if present, fallback to engine
1608
    this.graphicsContext.backgroundColor = this.currentScene.backgroundColor ?? this.backgroundColor;
1,746✔
1609
    this.graphicsContext.beginDrawLifecycle();
1,746✔
1610
    this.graphicsContext.clear();
1,746✔
1611
    this.clock.__runScheduledCbs('predraw');
1,746✔
1612
    this._predraw(this.graphicsContext, elapsed);
1,746✔
1613

1614
    // Drawing nothing else while loading
1615
    if (this._isLoading) {
1,746✔
1616
      if (!this._hideLoader) {
829!
1617
        this._loader?.canvas.draw(this.graphicsContext, 0, 0);
829!
1618
        this.clock.__runScheduledCbs('postdraw');
829✔
1619
        this.graphicsContext.flush();
829✔
1620
        this.graphicsContext.endDrawLifecycle();
829✔
1621
      }
1622
      return;
829✔
1623
    }
1624

1625
    this.currentScene.draw(this.graphicsContext, elapsed);
917✔
1626

1627
    this.clock.__runScheduledCbs('postdraw');
917✔
1628
    this._postdraw(this.graphicsContext, elapsed);
917✔
1629

1630
    // Flush any pending drawings
1631
    this.graphicsContext.flush();
917✔
1632
    this.graphicsContext.endDrawLifecycle();
917✔
1633

1634
    this._checkForScreenShots();
917✔
1635
  }
1636

1637
  /**
1638
   * @internal
1639
   */
1640
  public _predraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1641
    this.emit('predraw', new PreDrawEvent(ctx, elapsed, this));
1,746✔
1642
    this.onPreDraw(ctx, elapsed);
1,746✔
1643
  }
1644

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

1654
  /**
1655
   * @internal
1656
   */
1657
  public _postdraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1658
    this.emit('postdraw', new PostDrawEvent(ctx, elapsed, this));
917✔
1659
    this.onPostDraw(ctx, elapsed);
917✔
1660
  }
1661

1662
  /**
1663
   * Safe to override method to hook into pre draw
1664
   * @param ctx {@link ExcaliburGraphicsContext} for drawing
1665
   * @param elapsed  Number of milliseconds elapsed since the last draw.
1666
   */
1667
  public onPostDraw(ctx: ExcaliburGraphicsContext, elapsed: number) {
1668
    // Override me
1669
  }
1670

1671
  /**
1672
   * Enable or disable Excalibur debugging functionality.
1673
   * @param toggle a value that debug drawing will be changed to
1674
   */
1675
  public showDebug(toggle: boolean): void {
1676
    this._isDebug = toggle;
2✔
1677
  }
1678

1679
  /**
1680
   * Toggle Excalibur debugging functionality.
1681
   */
1682
  public toggleDebug(): boolean {
1683
    this._isDebug = !this._isDebug;
14✔
1684
    return this._isDebug;
14✔
1685
  }
1686

1687
  /**
1688
   * Returns true when loading is totally complete and the player has clicked start
1689
   */
1690
  public get loadingComplete() {
1691
    return !this._isLoading;
718✔
1692
  }
1693

1694
  private _isLoading = false;
730✔
1695
  private _hideLoader = false;
730✔
1696
  private _isReadyFuture = new Future<void>();
730✔
1697
  public get ready() {
1698
    return this._isReadyFuture.isCompleted;
100✔
1699
  }
1700
  public isReady(): Promise<void> {
1701
    return this._isReadyFuture.promise;
14✔
1702
  }
1703

1704
  /**
1705
   * Starts the internal game loop for Excalibur after loading
1706
   * any provided assets.
1707
   * @param loader  Optional {@apilink Loader} to use to load resources. The default loader is {@apilink Loader},
1708
   * override to provide your own custom loader.
1709
   *
1710
   * Note: start() only resolves AFTER the user has clicked the play button
1711
   */
1712
  public async start(loader?: DefaultLoader): Promise<void>;
1713
  /**
1714
   * Starts the internal game loop for Excalibur after configuring any routes, loaders, or transitions
1715
   * @param startOptions Optional {@apilink StartOptions} to configure the routes for scenes in Excalibur
1716
   *
1717
   * Note: start() only resolves AFTER the user has clicked the play button
1718
   */
1719
  public async start(sceneName: WithRoot<TKnownScenes>, options?: StartOptions): Promise<void>;
1720
  /**
1721
   * Starts the internal game loop after any loader is finished
1722
   * @param loader
1723
   */
1724
  public async start(loader?: DefaultLoader): Promise<void>;
1725
  public async start(sceneNameOrLoader?: WithRoot<TKnownScenes> | DefaultLoader, options?: StartOptions): Promise<void> {
1726
    await this.scope(async () => {
568✔
1727
      if (!this._compatible) {
568!
1728
        throw new Error('Excalibur is incompatible with your browser');
×
1729
      }
1730
      this._isLoading = true;
568✔
1731
      let loader: DefaultLoader;
1732
      if (sceneNameOrLoader instanceof DefaultLoader) {
568✔
1733
        loader = sceneNameOrLoader;
15✔
1734
      } else if (typeof sceneNameOrLoader === 'string') {
553!
1735
        this.director.configureStart(sceneNameOrLoader, options);
×
1736
        loader = this.director.mainLoader;
×
1737
      }
1738

1739
      // Start the excalibur clock which drives the mainloop
1740
      this._logger.debug('Starting game clock...');
568✔
1741
      this.browser.resume();
568✔
1742
      this.clock.start();
568✔
1743
      if (this.garbageCollectorConfig) {
568!
1744
        this._garbageCollector.start();
568✔
1745
      }
1746
      this._logger.debug('Game clock started');
568✔
1747

1748
      await this.load(loader ?? new Loader());
568✔
1749

1750
      // Initialize before ready
1751
      await this._overrideInitialize(this);
567✔
1752

1753
      this._isReadyFuture.resolve();
567✔
1754
      this.emit('start', new GameStartEvent(this));
567✔
1755
      return this._isReadyFuture.promise;
567✔
1756
    });
1757
  }
1758

1759
  /**
1760
   * Returns the current frames elapsed milliseconds
1761
   */
1762
  public currentFrameElapsedMs = 0;
730✔
1763

1764
  /**
1765
   * Returns the current frame lag when in fixed update mode
1766
   */
1767
  public currentFrameLagMs = 0;
730✔
1768

1769
  private _lagMs = 0;
730✔
1770
  private _mainloop(elapsed: number) {
1771
    this.scope(() => {
1,745✔
1772
      this.emit('preframe', new PreFrameEvent(this, this.stats.prevFrame));
1,745✔
1773
      const elapsedMs = elapsed * this.timescale;
1,745✔
1774
      this.currentFrameElapsedMs = elapsedMs;
1,745✔
1775

1776
      // reset frame stats (reuse existing instances)
1777
      const frameId = this.stats.prevFrame.id + 1;
1,745✔
1778
      this.stats.currFrame.reset();
1,745✔
1779
      this.stats.currFrame.id = frameId;
1,745✔
1780
      this.stats.currFrame.elapsedMs = elapsedMs;
1,745✔
1781
      this.stats.currFrame.fps = this.clock.fpsSampler.fps;
1,745✔
1782
      GraphicsDiagnostics.clear();
1,745✔
1783

1784
      const beforeUpdate = this.clock.now();
1,745✔
1785
      const fixedTimestepMs = this.fixedUpdateTimestep;
1,745✔
1786
      if (this.fixedUpdateTimestep) {
1,745✔
1787
        this._lagMs += elapsedMs;
44✔
1788
        while (this._lagMs >= fixedTimestepMs) {
44✔
1789
          this._update(fixedTimestepMs);
42✔
1790
          this._lagMs -= fixedTimestepMs;
42✔
1791
        }
1792
      } else {
1793
        this._update(elapsedMs);
1,701✔
1794
      }
1795
      const afterUpdate = this.clock.now();
1,745✔
1796
      this.currentFrameLagMs = this._lagMs;
1,745✔
1797
      this._draw(elapsedMs);
1,745✔
1798
      const afterDraw = this.clock.now();
1,745✔
1799

1800
      this.stats.currFrame.duration.update = afterUpdate - beforeUpdate;
1,745✔
1801
      this.stats.currFrame.duration.draw = afterDraw - afterUpdate;
1,745✔
1802
      this.stats.currFrame.graphics.drawnImages = GraphicsDiagnostics.DrawnImagesCount;
1,745✔
1803
      this.stats.currFrame.graphics.drawCalls = GraphicsDiagnostics.DrawCallCount;
1,745✔
1804

1805
      this.emit('postframe', new PostFrameEvent(this, this.stats.currFrame));
1,745✔
1806
      this.stats.prevFrame.reset(this.stats.currFrame);
1,745✔
1807

1808
      this._monitorPerformanceThresholdAndTriggerFallback();
1,745✔
1809
    });
1810
  }
1811

1812
  /**
1813
   * Stops Excalibur's main loop, useful for pausing the game.
1814
   */
1815
  public stop() {
1816
    if (this.clock.isRunning()) {
1,461✔
1817
      this.emit('stop', new GameStopEvent(this));
578✔
1818
      this.browser.pause();
578✔
1819
      this.clock.stop();
578✔
1820
      this._garbageCollector.stop();
578✔
1821
      this._logger.debug('Game stopped');
578✔
1822
    }
1823
  }
1824

1825
  /**
1826
   * Returns the Engine's running status, Useful for checking whether engine is running or paused.
1827
   */
1828
  public isRunning() {
1829
    return this.clock.isRunning();
3✔
1830
  }
1831

1832
  private _screenShotRequests: { preserveHiDPIResolution: boolean; resolve: (image: HTMLImageElement) => void }[] = [];
730✔
1833
  /**
1834
   * Takes a screen shot of the current viewport and returns it as an
1835
   * HTML Image Element.
1836
   * @param preserveHiDPIResolution in the case of HiDPI return the full scaled backing image, by default false
1837
   */
1838
  public screenshot(preserveHiDPIResolution = false): Promise<HTMLImageElement> {
3✔
1839
    const screenShotPromise = new Promise<HTMLImageElement>((resolve) => {
9✔
1840
      this._screenShotRequests.push({ preserveHiDPIResolution, resolve });
9✔
1841
    });
1842
    return screenShotPromise;
9✔
1843
  }
1844

1845
  private _checkForScreenShots() {
1846
    // We must grab the draw buffer before we yield to the browser
1847
    // the draw buffer is cleared after compositing
1848
    // the reason for the asynchrony is setting `preserveDrawingBuffer: true`
1849
    // forces the browser to copy buffers which can have a mass perf impact on mobile
1850
    for (const request of this._screenShotRequests) {
917✔
1851
      const finalWidth = request.preserveHiDPIResolution ? this.canvas.width : this.screen.resolution.width;
9✔
1852
      const finalHeight = request.preserveHiDPIResolution ? this.canvas.height : this.screen.resolution.height;
9✔
1853
      const screenshot = document.createElement('canvas');
9✔
1854
      screenshot.width = finalWidth;
9✔
1855
      screenshot.height = finalHeight;
9✔
1856
      const ctx = screenshot.getContext('2d');
9✔
1857
      ctx.imageSmoothingEnabled = this.screen.antialiasing;
9✔
1858
      ctx.drawImage(this.canvas, 0, 0, finalWidth, finalHeight);
9✔
1859

1860
      const result = new Image();
9✔
1861
      const raw = screenshot.toDataURL('image/png');
9✔
1862
      result.onload = () => {
9✔
1863
        request.resolve(result);
9✔
1864
      };
1865
      result.src = raw;
9✔
1866
    }
1867
    // Reset state
1868
    this._screenShotRequests.length = 0;
917✔
1869
  }
1870

1871
  /**
1872
   * Another option available to you to load resources into the game.
1873
   * Immediately after calling this the game will pause and the loading screen
1874
   * will appear.
1875
   * @param loader  Some {@apilink Loadable} such as a {@apilink Loader} collection, {@apilink Sound}, or {@apilink Texture}.
1876
   */
1877
  public async load(loader: DefaultLoader, hideLoader = false): Promise<void> {
1,008✔
1878
    await this.scope(async () => {
1,008✔
1879
      try {
1,008✔
1880
        // early exit if loaded
1881
        if (loader.isLoaded()) {
1,008✔
1882
          return;
992✔
1883
        }
1884
        this._loader = loader;
16✔
1885
        this._isLoading = true;
16✔
1886
        this._hideLoader = hideLoader;
16✔
1887

1888
        if (loader instanceof Loader) {
16✔
1889
          loader.suppressPlayButton = loader.suppressPlayButton || this._suppressPlayButton;
15✔
1890
        }
1891
        this._loader.onInitialize(this);
16✔
1892

1893
        await loader.load();
16✔
1894
      } catch (e) {
1895
        this._logger.error('Error loading resources, things may not behave properly', e);
1✔
1896
        await Promise.resolve();
1✔
1897
      } finally {
1898
        this._isLoading = false;
1,007✔
1899
        this._hideLoader = false;
1,007✔
1900
        this._loader = null;
1,007✔
1901
      }
1902
    });
1903
  }
1904
}
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