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

excaliburjs / Excalibur / 29056445523

09 Jul 2026 11:06PM UTC coverage: 88.902% (-0.004%) from 88.906%
29056445523

Pull #3793

github

web-flow
Merge 139f9ef84 into 3ee8a530a
Pull Request #3793: chore: TS6 Upgrades + Typedoc update

6936 of 9094 branches covered (76.27%)

274 of 373 new or added lines in 55 files covered. (73.46%)

3 existing lines in 2 files now uncovered.

15516 of 17453 relevant lines covered (88.9%)

24902.82 hits per line

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

89.66
/src/engine/camera.ts
1
import type { Engine } from './engine';
2
import type { Screen } from './screen';
3
import type { EasingFunction } from './util/easing-functions';
4
import { EasingFunctions, isLegacyEasing } from './util/easing-functions';
5
import { Vector, vec } from './math/vector';
6
import type { Actor } from './actor';
7
import { removeItemFromArray } from './util/util';
8
import type { CanUpdate, CanInitialize } from './interfaces/lifecycle-events';
9
import { PreUpdateEvent, PostUpdateEvent, InitializeEvent } from './events';
10
import { BoundingBox } from './collision/bounding-box';
11
import { Logger } from './util/log';
12
import type { ExcaliburGraphicsContext } from './graphics/context/excalibur-graphics-context';
13
import { AffineMatrix } from './math/affine-matrix';
14
import type { EventKey, Handler, Subscription } from './event-emitter';
15
import { EventEmitter } from './event-emitter';
16
import { pixelSnapEpsilon } from './graphics';
17
import { sign } from './math/util';
18
import { WatchVector } from './math/watch-vector';
19
import type { Easing } from './math';
20
import { easeInOutCubic, lerp } from './math';
21

22
/**
23
 * Interface that describes a custom camera strategy for tracking targets
24
 */
25
export interface CameraStrategy<T> {
26
  /**
27
   * Target of the camera strategy that will be passed to the action
28
   */
29
  target: T;
30

31
  /**
32
   * Camera strategies perform an action to calculate a new focus returned out of the strategy
33
   * @param target The target object to apply this camera strategy (if any)
34
   * @param camera The current camera implementation in excalibur running the game
35
   * @param engine The current engine running the game
36
   * @param elapsed The elapsed time in milliseconds since the last frame
37
   */
38
  action: (target: T, camera: Camera, engine: Engine, elapsed: number) => Vector;
39
}
40

41
/**
42
 * Container to house convenience strategy methods
43
 * @internal
44
 */
45
export class StrategyContainer {
46
  constructor(public camera: Camera) {}
1,404✔
47

48
  /**
49
   * Creates and adds the {@apilink LockCameraToActorStrategy} on the current camera.
50
   * @param actor The actor to lock the camera to
51
   */
52
  public lockToActor(actor: Actor) {
53
    this.camera.addStrategy(new LockCameraToActorStrategy(actor));
2✔
54
  }
55

56
  /**
57
   * Creates and adds the {@apilink LockCameraToActorAxisStrategy} on the current camera
58
   * @param actor The actor to lock the camera to
59
   * @param axis The axis to follow the actor on
60
   */
61
  public lockToActorAxis(actor: Actor, axis: Axis) {
62
    this.camera.addStrategy(new LockCameraToActorAxisStrategy(actor, axis));
2✔
63
  }
64

65
  /**
66
   * Creates and adds the {@apilink ElasticToActorStrategy} on the current camera
67
   * If cameraElasticity < cameraFriction < 1.0, the behavior will be a dampened spring that will slowly end at the target without bouncing
68
   * If cameraFriction < cameraElasticity < 1.0, the behavior will be an oscillating spring that will over
69
   * correct and bounce around the target
70
   * @param actor Target actor to elastically follow
71
   * @param cameraElasticity [0 - 1.0] The higher the elasticity the more force that will drive the camera towards the target
72
   * @param cameraFriction [0 - 1.0] The higher the friction the more that the camera will resist motion towards the target
73
   */
74
  public elasticToActor(actor: Actor, cameraElasticity: number, cameraFriction: number) {
75
    this.camera.addStrategy(new ElasticToActorStrategy(actor, cameraElasticity, cameraFriction));
1✔
76
  }
77

78
  /**
79
   * Creates and adds the {@apilink RadiusAroundActorStrategy} on the current camera
80
   * @param actor Target actor to follow when it is "radius" pixels away
81
   * @param radius Number of pixels away before the camera will follow
82
   */
83
  public radiusAroundActor(actor: Actor, radius: number) {
84
    this.camera.addStrategy(new RadiusAroundActorStrategy(actor, radius));
1✔
85
  }
86

87
  /**
88
   * Creates and adds the {@apilink LimitCameraBoundsStrategy} on the current camera
89
   * @param box The bounding box to limit the camera to.
90
   */
91
  public limitCameraBounds(box: BoundingBox) {
92
    this.camera.addStrategy(new LimitCameraBoundsStrategy(box));
1✔
93
  }
94
}
95

96
/**
97
 * Camera axis enum
98
 */
99
export enum Axis {
254✔
100
  X,
254✔
101
  Y
254✔
102
}
103

104
/**
105
 * Lock a camera to the exact x/y position of an actor.
106
 */
107
export class LockCameraToActorStrategy implements CameraStrategy<Actor> {
108
  constructor(public target: Actor) {}
7✔
109
  public action = (target: Actor, camera: Camera, engine: Engine, elapsed: number) => {
7✔
110
    const center = target.center;
3✔
111
    return center;
3✔
112
  };
113
}
114

115
/**
116
 * Lock a camera to a specific axis around an actor.
117
 */
118
export class LockCameraToActorAxisStrategy implements CameraStrategy<Actor> {
119
  constructor(
120
    public target: Actor,
2✔
121
    public axis: Axis
2✔
122
  ) {}
123
  public action = (target: Actor, cam: Camera, _eng: Engine, elapsed: number) => {
2✔
124
    const center = target.center;
6✔
125
    const currentFocus = cam.getFocus();
6✔
126
    if (this.axis === Axis.X) {
6✔
127
      return new Vector(center.x, currentFocus.y);
3✔
128
    } else {
129
      return new Vector(currentFocus.x, center.y);
3✔
130
    }
131
  };
132
}
133

134
/**
135
 * Using [Hook's law](https://en.wikipedia.org/wiki/Hooke's_law), elastically move the camera towards the target actor.
136
 */
137
export class ElasticToActorStrategy implements CameraStrategy<Actor> {
138
  /**
139
   * If cameraElasticity < cameraFriction < 1.0, the behavior will be a dampened spring that will slowly end at the target without bouncing
140
   * If cameraFriction < cameraElasticity < 1.0, the behavior will be an oscillating spring that will over
141
   * correct and bounce around the target
142
   * @param target Target actor to elastically follow
143
   * @param cameraElasticity [0 - 1.0] The higher the elasticity the more force that will drive the camera towards the target
144
   * @param cameraFriction [0 - 1.0] The higher the friction the more that the camera will resist motion towards the target
145
   */
146
  constructor(
147
    public target: Actor,
3✔
148
    public cameraElasticity: number,
3✔
149
    public cameraFriction: number
3✔
150
  ) {}
151
  public action = (target: Actor, cam: Camera, _eng: Engine, elapsed: number) => {
3✔
152
    const position = target.center;
8✔
153
    let focus = cam.getFocus();
8✔
154
    let cameraVel = cam.vel.clone();
8✔
155

156
    // Calculate the stretch vector, using the spring equation
157
    // F = kX
158
    // https://en.wikipedia.org/wiki/Hooke's_law
159
    // Apply to the current camera velocity
160
    const stretch = position.sub(focus).scale(this.cameraElasticity); // stretch is X
8✔
161
    cameraVel = cameraVel.add(stretch);
8✔
162

163
    // Calculate the friction (-1 to apply a force in the opposition of motion)
164
    // Apply to the current camera velocity
165
    const friction = cameraVel.scale(-1).scale(this.cameraFriction);
8✔
166
    cameraVel = cameraVel.add(friction);
8✔
167

168
    // Update position by velocity deltas
169
    focus = focus.add(cameraVel);
8✔
170

171
    return focus;
8✔
172
  };
173
}
174

175
export class RadiusAroundActorStrategy implements CameraStrategy<Actor> {
176
  /**
177
   *
178
   * @param target Target actor to follow when it is "radius" pixels away
179
   * @param radius Number of pixels away before the camera will follow
180
   */
181
  constructor(
182
    public target: Actor,
1✔
183
    public radius: number
1✔
184
  ) {}
185
  public action = (target: Actor, cam: Camera, _eng: Engine, elapsed: number) => {
1✔
186
    const position = target.center;
3✔
187
    const focus = cam.getFocus();
3✔
188

189
    const direction = position.sub(focus);
3✔
190
    const distance = direction.magnitude;
3✔
191
    if (distance >= this.radius) {
3✔
192
      const offset = distance - this.radius;
1✔
193
      return focus.add(direction.normalize().scale(offset));
1✔
194
    }
195
    return focus;
2✔
196
  };
197
}
198

199
/**
200
 * Prevent a camera from going beyond the given camera dimensions.
201
 */
202
export class LimitCameraBoundsStrategy implements CameraStrategy<BoundingBox> {
203
  /**
204
   * Useful for limiting the camera to a {@apilink TileMap}'s dimensions, or a specific area inside the map.
205
   *
206
   * Note that this strategy does not perform any movement by itself.
207
   * It only sets the camera position to within the given bounds when the camera has gone beyond them.
208
   * Thus, it is a good idea to combine it with other camera strategies and set this strategy as the last one.
209
   *
210
   * Make sure that the camera bounds are at least as large as the viewport size.
211
   * @param target The bounding box to limit the camera to
212
   */
213

214
  boundSizeChecked: boolean = false; // Check and warn only once
5✔
215

216
  constructor(public target: BoundingBox) {}
5✔
217

218
  public action = (target: BoundingBox, cam: Camera, _eng: Engine, elapsed: number) => {
5✔
219
    const focus = cam.getFocus();
3✔
220

221
    if (!this.boundSizeChecked) {
3✔
222
      if (target.bottom - target.top < _eng.drawHeight || target.right - target.left < _eng.drawWidth) {
1!
223
        Logger.getInstance().warn('Camera bounds should not be smaller than the engine viewport');
×
224
      }
225
      this.boundSizeChecked = true;
1✔
226
    }
227

228
    let focusX = focus.x;
3✔
229
    let focusY = focus.y;
3✔
230
    if (focus.x < target.left + _eng.halfDrawWidth) {
3✔
231
      focusX = target.left + _eng.halfDrawWidth;
1✔
232
    } else if (focus.x > target.right - _eng.halfDrawWidth) {
2✔
233
      focusX = target.right - _eng.halfDrawWidth;
1✔
234
    }
235

236
    if (focus.y < target.top + _eng.halfDrawHeight) {
3✔
237
      focusY = target.top + _eng.halfDrawHeight;
1✔
238
    } else if (focus.y > target.bottom - _eng.halfDrawHeight) {
2✔
239
      focusY = target.bottom - _eng.halfDrawHeight;
1✔
240
    }
241

242
    return vec(focusX, focusY);
3✔
243
  };
244
}
245

246
export interface CameraEvents {
247
  preupdate: PreUpdateEvent<Camera>;
248
  postupdate: PostUpdateEvent<Camera>;
249
  initialize: InitializeEvent<Camera>;
250
}
251

252
export const CameraEvents = {
254✔
253
  Initialize: 'initialize',
254
  PreUpdate: 'preupdate',
255
  PostUpdate: 'postupdate'
256
};
257

258
/**
259
 * Cameras
260
 *
261
 * {@apilink Camera} is the base class for all Excalibur cameras. Cameras are used
262
 * to move around your game and set focus. They are used to determine
263
 * what is "off screen" and can be used to scale the game.
264
 *
265
 */
266
export class Camera implements CanUpdate, CanInitialize {
267
  public events = new EventEmitter<CameraEvents>();
1,404✔
268
  public transform: AffineMatrix = AffineMatrix.identity();
1,404✔
269
  public inverse: AffineMatrix = AffineMatrix.identity();
1,404✔
270

271
  protected _follow!: Actor;
272

273
  private _cameraStrategies: CameraStrategy<any>[] = [];
1,404✔
274
  public get strategies(): CameraStrategy<any>[] {
275
    return this._cameraStrategies;
5✔
276
  }
277

278
  public strategy: StrategyContainer = new StrategyContainer(this);
1,404✔
279

280
  /**
281
   * Get or set current zoom of the camera, defaults to 1
282
   */
283
  private _z = 1;
1,404✔
284
  public get zoom(): number {
285
    return this._z;
23,795✔
286
  }
287

288
  public set zoom(val: number) {
289
    this._z = val;
1,947✔
290
    if (this._engine) {
1,947✔
291
      this._halfWidth = this._engine.halfDrawWidth;
1,939✔
292
      this._halfHeight = this._engine.halfDrawHeight;
1,939✔
293
    }
294
  }
295
  /**
296
   * Get or set rate of change in zoom, defaults to 0
297
   */
298
  public dz: number = 0;
1,404✔
299
  /**
300
   * Get or set zoom acceleration
301
   */
302
  public az: number = 0;
1,404✔
303

304
  /**
305
   * Current rotation of the camera
306
   */
307
  public rotation: number = 0;
1,404✔
308

309
  private _angularVelocity: number = 0;
1,404✔
310

311
  /**
312
   * Get or set the camera's angular velocity
313
   */
314
  public get angularVelocity(): number {
315
    return this._angularVelocity;
1,938✔
316
  }
317

318
  public set angularVelocity(value: number) {
319
    this._angularVelocity = value;
×
320
  }
321

322
  private _posChanged = false;
1,404✔
323
  private _pos: Vector = new WatchVector(Vector.Zero, () => {
1,404✔
324
    this._posChanged = true;
×
325
  });
326
  /**
327
   * Get or set the camera's position
328
   */
329
  public get pos(): Vector {
330
    return this._pos;
24,445✔
331
  }
332
  public set pos(vec: Vector) {
333
    this._posChanged = true;
2,801✔
334
    this._pos = new WatchVector(vec, () => {
2,801✔
335
      this._posChanged = true;
83✔
336
    });
337
  }
338

339
  /**
340
   * Has the position changed since the last update
341
   */
342
  public hasChanged(): boolean {
343
    return this._posChanged;
5,642✔
344
  }
345
  /**
346
   * Interpolated camera position if more draws are running than updates
347
   *
348
   * Enabled when `Engine.fixedUpdateFps` is enabled, in all other cases this will be the same as pos
349
   */
350
  public drawPos: Vector = this.pos.clone();
1,404✔
351

352
  private _oldPos = this.pos.clone();
1,404✔
353

354
  /**
355
   * Get or set the camera's velocity
356
   */
357
  public vel: Vector = Vector.Zero;
1,404✔
358

359
  /**
360
   * Get or set the camera's acceleration
361
   */
362
  public acc: Vector = Vector.Zero;
1,404✔
363

364
  private _cameraMoving: boolean = false;
1,404✔
365
  private _currentLerpTime: number = 0;
1,404✔
366
  private _lerpDuration: number = 1000; // 1 second
1,404✔
367
  private _lerpStart!: Vector;
368
  private _lerpEnd!: Vector;
369
  private _lerpResolve!: (value: Vector) => void;
370
  private _lerpPromise?: Promise<Vector>;
371

372
  //camera effects
373
  protected _isShaking: boolean = false;
1,404✔
374
  private _shakeMagnitudeX: number = 0;
1,404✔
375
  private _shakeMagnitudeY: number = 0;
1,404✔
376
  private _shakeDuration: number = 0;
1,404✔
377
  private _elapsedShakeTime: number = 0;
1,404✔
378
  private _xShake: number = 0;
1,404✔
379
  private _yShake: number = 0;
1,404✔
380

381
  protected _isZooming: boolean = false;
1,404✔
382
  private _zoomStart: number = 1;
1,404✔
383
  private _zoomEnd: number = 1;
1,404✔
384
  private _currentZoomTime: number = 0;
1,404✔
385
  private _zoomDuration: number = 0;
1,404✔
386

387
  private _zoomResolve!: (val: boolean) => void;
388
  private _zoomPromise!: Promise<boolean>;
389
  private _legacyZoomEasing: EasingFunction = EasingFunctions.EaseInOutCubic;
1,404✔
390
  private _useLegacyZoom = false;
1,404✔
391
  private _zoomEasing: Easing = easeInOutCubic;
1,404✔
392
  private _legacyEasing: EasingFunction = EasingFunctions.EaseInOutCubic;
1,404✔
393
  private _useLegacyEasing = false;
1,404✔
394
  private _easing: Easing = easeInOutCubic;
1,404✔
395

396
  private _halfWidth: number = 0;
1,404✔
397
  private _halfHeight: number = 0;
1,404✔
398

399
  /**
400
   * Get the camera's x position
401
   */
402
  public get x() {
403
    return this.pos.x;
5,400✔
404
  }
405

406
  /**
407
   * Set the camera's x position (cannot be set when following an {@apilink Actor} or when moving)
408
   */
409
  public set x(value: number) {
410
    if (!this._follow && !this._cameraMoving) {
12!
411
      this.pos = vec(value, this.pos.y);
12✔
412
    }
413
  }
414

415
  /**
416
   * Get the camera's y position
417
   */
418
  public get y() {
419
    return this.pos.y;
5,400✔
420
  }
421

422
  /**
423
   * Set the camera's y position (cannot be set when following an {@apilink Actor} or when moving)
424
   */
425
  public set y(value: number) {
426
    if (!this._follow && !this._cameraMoving) {
11!
427
      this.pos = vec(this.pos.x, value);
11✔
428
    }
429
  }
430

431
  /**
432
   * Get or set the camera's x velocity
433
   */
434
  public get dx() {
435
    return this.vel.x;
2✔
436
  }
437

438
  public set dx(value: number) {
439
    this.vel = vec(value, this.vel.y);
1✔
440
  }
441

442
  /**
443
   * Get or set the camera's y velocity
444
   */
445
  public get dy() {
446
    return this.vel.y;
2✔
447
  }
448

449
  public set dy(value: number) {
450
    this.vel = vec(this.vel.x, value);
1✔
451
  }
452

453
  /**
454
   * Get or set the camera's x acceleration
455
   */
456
  public get ax() {
457
    return this.acc.x;
2✔
458
  }
459

460
  public set ax(value: number) {
461
    this.acc = vec(value, this.acc.y);
1✔
462
  }
463

464
  /**
465
   * Get or set the camera's y acceleration
466
   */
467
  public get ay() {
468
    return this.acc.y;
2✔
469
  }
470

471
  public set ay(value: number) {
472
    this.acc = vec(this.acc.x, value);
1✔
473
  }
474

475
  /**
476
   * Returns the focal point of the camera, a new point giving the x and y position of the camera
477
   */
478
  public getFocus() {
479
    return this.pos;
38✔
480
  }
481

482
  /**
483
   * This moves the camera focal point to the specified position using specified easing function. Cannot move when following an Actor.
484
   * @param pos The target position to move to
485
   * @param duration The duration in milliseconds the move should last
486
   * @param [easingFn] An optional easing function ({@apilink EasingFunctions.EaseInOutCubic} by default)
487
   * @returns A {@apilink Promise} that resolves when movement is finished, including if it's interrupted.
488
   *          The {@apilink Promise} value is the {@apilink Vector} of the target position. It will be rejected if a move cannot be made.
489
   */
490
  public move(pos: Vector, duration: number, easingFn: Easing | EasingFunction = EasingFunctions.EaseInOutCubic): Promise<Vector> {
4✔
491
    if (typeof easingFn !== 'function') {
6!
492
      throw 'Please specify an EasingFunction';
×
493
    }
494

495
    // cannot move when following an actor
496
    if (this._follow) {
6!
497
      return Promise.reject(pos);
×
498
    }
499

500
    // resolve existing promise, if any
501
    if (this._lerpPromise && this._lerpResolve) {
6✔
502
      this._lerpResolve(pos);
3✔
503
    }
504

505
    this._lerpPromise = new Promise<Vector>((resolve) => {
6✔
506
      this._lerpResolve = resolve;
6✔
507
    });
508
    this._lerpStart = this.getFocus().clone();
6✔
509
    this._lerpDuration = duration;
6✔
510
    this._lerpEnd = pos;
6✔
511
    this._currentLerpTime = 0;
6✔
512
    this._cameraMoving = true;
6✔
513
    if (isLegacyEasing(easingFn)) {
6!
514
      this._legacyEasing = easingFn;
6✔
515
    } else {
516
      this._easing = easingFn;
×
517
    }
518

519
    return this._lerpPromise;
6✔
520
  }
521

522
  /**
523
   * Sets the camera to shake at the specified magnitudes for the specified duration
524
   * @param magnitudeX  The x magnitude of the shake
525
   * @param magnitudeY  The y magnitude of the shake
526
   * @param duration    The duration of the shake in milliseconds
527
   */
528
  public shake(magnitudeX: number, magnitudeY: number, duration: number) {
529
    this._isShaking = true;
1✔
530
    this._shakeMagnitudeX = magnitudeX;
1✔
531
    this._shakeMagnitudeY = magnitudeY;
1✔
532
    this._shakeDuration = duration;
1✔
533
  }
534

535
  /**
536
   * Zooms the camera in or out by the specified scale over the specified duration.
537
   * If no duration is specified, it take effect immediately.
538
   * @param scale    The scale of the zoom
539
   * @param duration The duration of the zoom in milliseconds
540
   */
541
  public zoomOverTime(
542
    scale: number,
543
    duration: number = 0,
×
544
    easingFn: Easing | EasingFunction = EasingFunctions.EaseInOutCubic
1✔
545
  ): Promise<boolean> {
546
    this._zoomPromise = new Promise<boolean>((resolve) => {
1✔
547
      this._zoomResolve = resolve;
1✔
548
    });
549

550
    if (duration) {
1!
551
      this._isZooming = true;
1✔
552
      if (isLegacyEasing(easingFn)) {
1!
553
        this._legacyZoomEasing = easingFn;
1✔
554
      } else {
555
        this._easing = easingFn;
×
556
      }
557
      this._currentZoomTime = 0;
1✔
558
      this._zoomDuration = duration;
1✔
559
      this._zoomStart = this.zoom;
1✔
560
      this._zoomEnd = scale;
1✔
561
    } else {
562
      this._isZooming = false;
×
563
      this.zoom = scale;
×
564
      return Promise.resolve(true);
×
565
    }
566

567
    return this._zoomPromise;
1✔
568
  }
569

570
  private _viewport?: BoundingBox;
571
  /**
572
   * Gets the bounding box of the viewport of this camera in world coordinates
573
   */
574
  public get viewport(): BoundingBox {
575
    if (this._viewport) {
2✔
576
      return this._viewport;
1✔
577
    }
578

579
    return new BoundingBox(0, 0, 0, 0);
1✔
580
  }
581

582
  /**
583
   * Adds one or more new camera strategies to this camera
584
   * @param cameraStrategy Instance of an {@apilink CameraStrategy}
585
   */
586
  public addStrategy<T extends CameraStrategy<any>[]>(...cameraStrategies: T) {
587
    this._cameraStrategies.push(...cameraStrategies);
10✔
588
  }
589

590
  /**
591
   * Sets the strategies of this camera, replacing all existing strategies
592
   * @param cameraStrategies Array of {@apilink CameraStrategy}
593
   */
594
  public setStrategies<T extends CameraStrategy<any>[]>(cameraStrategies: T) {
595
    this._cameraStrategies = [...cameraStrategies];
3✔
596
  }
597

598
  /**
599
   * Removes a camera strategy by reference
600
   * @param cameraStrategy Instance of an {@apilink CameraStrategy}
601
   */
602
  public removeStrategy<T>(cameraStrategy: CameraStrategy<T>) {
603
    removeItemFromArray(cameraStrategy, this._cameraStrategies);
1✔
604
  }
605

606
  /**
607
   * Clears all camera strategies from the camera
608
   */
609
  public clearAllStrategies() {
610
    this._cameraStrategies.length = 0;
1✔
611
  }
612

613
  /**
614
   * It is not recommended that internal excalibur methods be overridden, do so at your own risk.
615
   *
616
   * Internal _preupdate handler for {@apilink onPreUpdate} lifecycle event
617
   * @param engine The reference to the current game engine
618
   * @param elapsed  The time elapsed since the last update in milliseconds
619
   * @internal
620
   */
621
  public _preupdate(engine: Engine, elapsed: number): void {
622
    this.events.emit('preupdate', new PreUpdateEvent(engine, elapsed, this));
1,938✔
623
    this.onPreUpdate(engine, elapsed);
1,938✔
624
  }
625

626
  /**
627
   * Safe to override onPreUpdate lifecycle event handler. Synonymous with `.on('preupdate', (evt) =>{...})`
628
   *
629
   * `onPreUpdate` is called directly before a scene is updated.
630
   * @param engine The reference to the current game engine
631
   * @param elapsed  The time elapsed since the last update in milliseconds
632
   */
633
  public onPreUpdate(engine: Engine, elapsed: number): void {
634
    // Overridable
635
  }
636

637
  /**
638
   *  It is not recommended that internal excalibur methods be overridden, do so at your own risk.
639
   *
640
   * Internal _preupdate handler for {@apilink onPostUpdate} lifecycle event
641
   * @param engine The reference to the current game engine
642
   * @param elapsed  The time elapsed since the last update in milliseconds
643
   * @internal
644
   */
645
  public _postupdate(engine: Engine, elapsed: number): void {
646
    this.events.emit('postupdate', new PostUpdateEvent(engine, elapsed, this));
1,938✔
647
    this.onPostUpdate(engine, elapsed);
1,938✔
648
  }
649

650
  /**
651
   * Safe to override onPostUpdate lifecycle event handler. Synonymous with `.on('preupdate', (evt) =>{...})`
652
   *
653
   * `onPostUpdate` is called directly after a scene is updated.
654
   * @param engine The reference to the current game engine
655
   * @param elapsed  The time elapsed since the last update in milliseconds
656
   */
657
  public onPostUpdate(engine: Engine, elapsed: number): void {
658
    // Overridable
659
  }
660

661
  private _engine!: Engine;
662
  private _screen!: Screen;
663
  private _isInitialized = false;
1,404✔
664
  public get isInitialized() {
665
    return this._isInitialized;
2,684✔
666
  }
667

668
  public _initialize(engine: Engine) {
669
    if (!this.isInitialized) {
2,684✔
670
      this._engine = engine;
755✔
671
      this._screen = engine.screen;
755✔
672

673
      const currentRes = this._screen.contentArea;
755✔
674
      let center = vec(currentRes.width / 2, currentRes.height / 2);
755✔
675
      if (!this._engine.loadingComplete) {
755✔
676
        // If there was a loading screen, we peek the configured resolution
677
        const res = this._screen.peekResolution();
5✔
678
        if (res) {
5✔
679
          center = vec(res.width / 2, res.height / 2);
1✔
680
        }
681
      }
682
      this._halfWidth = center.x;
755✔
683
      this._halfHeight = center.y;
755✔
684

685
      // If the user has not set the camera pos, apply default center screen position
686
      if (!this._posChanged) {
755✔
687
        this.pos = center;
746✔
688
      }
689
      this.pos.clone(this.drawPos);
755✔
690
      // First frame bootstrap
691

692
      // Ensure camera tx is correct
693
      // Run update twice to ensure properties are init'd
694
      this.updateTransform(this.pos);
755✔
695

696
      // Run strategies for first frame
697
      this.runStrategies(engine, engine.clock.elapsed());
755✔
698

699
      // Setup the first frame viewport
700
      this.updateViewport();
755✔
701

702
      // It's important to update the camera after strategies
703
      // This prevents jitter
704
      this.updateTransform(this.pos);
755✔
705
      this.pos.clone(this._oldPos);
755✔
706

707
      this.onInitialize(engine);
755✔
708
      this.events.emit('initialize', new InitializeEvent(engine, this));
755✔
709
      this._isInitialized = true;
755✔
710
    }
711
  }
712

713
  /**
714
   * Safe to override onPostUpdate lifecycle event handler. Synonymous with `.on('preupdate', (evt) =>{...})`
715
   *
716
   * `onPostUpdate` is called directly after a scene is updated.
717
   */
718
  public onInitialize(engine: Engine) {
719
    // Overridable
720
  }
721

722
  public emit<TEventName extends EventKey<CameraEvents>>(eventName: TEventName, event: CameraEvents[TEventName]): void;
723
  public emit(eventName: string, event?: any): void;
724
  public emit<TEventName extends EventKey<CameraEvents> | string>(eventName: TEventName, event?: any): void {
725
    this.events.emit(eventName, event);
×
726
  }
727

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

734
  public once<TEventName extends EventKey<CameraEvents>>(eventName: TEventName, handler: Handler<CameraEvents[TEventName]>): Subscription;
735
  public once(eventName: string, handler: Handler<unknown>): Subscription;
736
  public once<TEventName extends EventKey<CameraEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
737
    return this.events.once(eventName, handler);
×
738
  }
739

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

747
  public runStrategies(engine: Engine, elapsed: number) {
748
    for (const s of this._cameraStrategies) {
2,693✔
749
      this.pos = s.action.call(s, s.target, this, engine, elapsed);
23✔
750
    }
751
  }
752

753
  public updateViewport() {
754
    // recalculate viewport
755
    this._viewport = new BoundingBox(
2,693✔
756
      this.x - this._halfWidth,
757
      this.y - this._halfHeight,
758
      this.x + this._halfWidth,
759
      this.y + this._halfHeight
760
    );
761
  }
762

763
  public update(engine: Engine, elapsed: number) {
764
    this._initialize(engine);
1,938✔
765
    this._preupdate(engine, elapsed);
1,938✔
766
    this.pos.clone(this._oldPos);
1,938✔
767

768
    // Update placements based on linear algebra
769
    this.pos = this.pos.add(this.vel.scale(elapsed / 1000));
1,938✔
770
    this.zoom += (this.dz * elapsed) / 1000;
1,938✔
771

772
    this.vel = this.vel.add(this.acc.scale(elapsed / 1000));
1,938✔
773
    this.dz += (this.az * elapsed) / 1000;
1,938✔
774

775
    this.rotation += (this.angularVelocity * elapsed) / 1000;
1,938✔
776

777
    if (this._isZooming) {
1,938!
778
      if (this._currentZoomTime < this._zoomDuration) {
×
779
        let newZoom = this.zoom;
×
780
        if (this._useLegacyZoom) {
×
781
          const zoomEasing = this._legacyZoomEasing;
×
782
          newZoom = zoomEasing(this._currentZoomTime, this._zoomStart, this._zoomEnd, this._zoomDuration);
×
783
        } else {
784
          newZoom = lerp(this._zoomStart, this._zoomEnd, this._zoomEasing(this._currentZoomTime / this._zoomDuration));
×
785
        }
786

787
        this.zoom = newZoom;
×
788
        this._currentZoomTime += elapsed;
×
789
      } else {
790
        this._isZooming = false;
×
791
        this.zoom = this._zoomEnd;
×
792
        this._currentZoomTime = 0;
×
793
        this._zoomResolve(true);
×
794
      }
795
    }
796

797
    if (this._cameraMoving) {
1,938✔
798
      if (this._currentLerpTime < this._lerpDuration) {
50✔
799
        let lerpPoint = this.pos;
44✔
800
        if (this._useLegacyEasing) {
44!
801
          const moveEasing = EasingFunctions.CreateVectorEasingFunction(this._legacyEasing);
×
802
          lerpPoint = moveEasing(this._currentLerpTime, this._lerpStart, this._lerpEnd, this._lerpDuration);
×
803
        } else {
804
          lerpPoint.x = lerp(this._lerpStart.x, this._lerpEnd.x, this._easing(this._currentLerpTime / this._lerpDuration));
44✔
805
          lerpPoint.y = lerp(this._lerpStart.y, this._lerpEnd.y, this._easing(this._currentLerpTime / this._lerpDuration));
44✔
806
        }
807

808
        this.pos = lerpPoint;
44✔
809

810
        this._currentLerpTime += elapsed;
44✔
811
      } else {
812
        this.pos = this._lerpEnd;
6✔
813
        const end = this._lerpEnd.clone();
6✔
814

815
        this._lerpStart = null as any;
6✔
816
        this._lerpEnd = null as any;
6✔
817
        this._currentLerpTime = 0;
6✔
818
        this._cameraMoving = false;
6✔
819
        // Order matters here, resolve should be last so any chain promises have a clean slate
820
        this._lerpResolve(end);
6✔
821
      }
822
    }
823

824
    if (this._isDoneShaking()) {
1,938!
825
      this._isShaking = false;
1,938✔
826
      this._elapsedShakeTime = 0;
1,938✔
827
      this._shakeMagnitudeX = 0;
1,938✔
828
      this._shakeMagnitudeY = 0;
1,938✔
829
      this._shakeDuration = 0;
1,938✔
830
      this._xShake = 0;
1,938✔
831
      this._yShake = 0;
1,938✔
832
    } else {
833
      this._elapsedShakeTime += elapsed;
×
834
      this._xShake = ((Math.random() * this._shakeMagnitudeX) | 0) + 1;
×
835
      this._yShake = ((Math.random() * this._shakeMagnitudeY) | 0) + 1;
×
836
    }
837

838
    this.runStrategies(engine, elapsed);
1,938✔
839

840
    this.updateViewport();
1,938✔
841

842
    // It's important to update the camera after strategies
843
    // This prevents jitter
844
    this.updateTransform(this.pos);
1,938✔
845
    this._postupdate(engine, elapsed);
1,938✔
846
    this._posChanged = false;
1,938✔
847
  }
848

849
  private _snapPos = vec(0, 0);
1,404✔
850
  /**
851
   * Applies the relevant transformations to the game canvas to "move" or apply effects to the Camera
852
   * @param ctx Canvas context to apply transformations
853
   */
854
  public draw(ctx: ExcaliburGraphicsContext): void {
855
    // default to the current position
856
    this.pos.clone(this.drawPos);
1,237✔
857

858
    // interpolation if fixed update is on
859
    // must happen on the draw, because more draws are potentially happening than updates
860
    if (this._engine.fixedUpdateTimestep) {
1,237✔
861
      const blend = this._engine.currentFrameLagMs / this._engine.fixedUpdateTimestep;
47✔
862
      const interpolatedPos = this.pos.scale(blend).add(this._oldPos.scale(1.0 - blend));
47✔
863
      interpolatedPos.clone(this.drawPos);
47✔
864
      this.updateTransform(interpolatedPos);
47✔
865
    }
866
    // Snap camera to pixel
867
    if (ctx.snapToPixel) {
1,237✔
868
      const snapPos = this.drawPos.clone(this._snapPos);
3✔
869
      snapPos.x = ~~(snapPos.x + pixelSnapEpsilon * sign(snapPos.x));
3✔
870
      snapPos.y = ~~(snapPos.y + pixelSnapEpsilon * sign(snapPos.y));
3✔
871
      snapPos.clone(this.drawPos);
3✔
872
      this.updateTransform(snapPos);
3✔
873
    }
874
    ctx.multiply(this.transform);
1,237✔
875
  }
876

877
  public updateTransform(pos: Vector) {
878
    // center the camera
879
    const newCanvasWidth = this._screen.resolution.width / this.zoom;
4,048✔
880
    const newCanvasHeight = this._screen.resolution.height / this.zoom;
4,048✔
881
    const cameraPos = vec(-pos.x + newCanvasWidth / 2 + this._xShake, -pos.y + newCanvasHeight / 2 + this._yShake);
4,048✔
882

883
    // Calculate camera transform
884
    this.transform.reset();
4,048✔
885

886
    this.transform.scale(this.zoom, this.zoom);
4,048✔
887

888
    // rotate about the focus
889
    this.transform.translate(newCanvasWidth / 2, newCanvasHeight / 2);
4,048✔
890
    this.transform.rotate(this.rotation);
4,048✔
891
    this.transform.translate(-newCanvasWidth / 2, -newCanvasHeight / 2);
4,048✔
892

893
    this.transform.translate(cameraPos.x, cameraPos.y);
4,048✔
894
    this.transform.inverse(this.inverse);
4,048✔
895
  }
896

897
  private _isDoneShaking(): boolean {
898
    return !this._isShaking || this._elapsedShakeTime >= this._shakeDuration;
1,938!
899
  }
900
}
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