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

excaliburjs / Excalibur / 14840900291

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

push

github

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

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

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

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

---

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

---

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

4998 of 6942 branches covered (72.0%)

13655 of 15541 relevant lines covered (87.86%)

25166.19 hits per line

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

96.34
/src/engine/Graphics/Graphic.ts
1
import { Vector, vec } from '../Math/vector';
2
import type { ExcaliburGraphicsContext } from './Context/ExcaliburGraphicsContext';
3
import { BoundingBox } from '../Collision/BoundingBox';
4
import type { Color } from '../Color';
5
import { watch } from '../Util/Watch';
6
import { AffineMatrix } from '../Math/affine-matrix';
7

8
export interface GraphicOptions {
9
  /**
10
   * The width of the graphic
11
   */
12
  width?: number;
13
  /**
14
   * The height of the graphic
15
   */
16
  height?: number;
17
  /**
18
   * Should the graphic be flipped horizontally
19
   */
20
  flipHorizontal?: boolean;
21
  /**
22
   * Should the graphic be flipped vertically
23
   */
24
  flipVertical?: boolean;
25
  /**
26
   * The rotation of the graphic
27
   */
28
  rotation?: number;
29
  /**
30
   * The scale of the graphic
31
   */
32
  scale?: Vector;
33
  /**
34
   * The opacity of the graphic between (0 -1)
35
   */
36
  opacity?: number;
37
  /**
38
   * The tint of the graphic, this color will be multiplied by the original pixel colors
39
   */
40
  tint?: Color;
41
  /**
42
   * The origin of the drawing in pixels to use when applying transforms, by default it will be the center of the image in pixels
43
   */
44
  origin?: Vector;
45
}
46

47
/**
48
 * A Graphic is the base Excalibur primitive for something that can be drawn to the {@apilink ExcaliburGraphicsContext}.
49
 * {@apilink Sprite}, {@apilink Animation}, {@apilink GraphicsGroup}, {@apilink Canvas}, {@apilink Rectangle}, {@apilink Circle}, and {@apilink Polygon} all derive from the
50
 * {@apilink Graphic} abstract class.
51
 *
52
 * Implementors of a Graphic must override the abstract {@apilink Graphic._drawImage} method to render an image to the graphics context. Graphic
53
 * handles all the position, rotation, and scale transformations in {@apilink Graphic._preDraw} and {@apilink Graphic._postDraw}
54
 */
55
export abstract class Graphic {
117✔
56
  private static _ID: number = 0;
57
  readonly id = Graphic._ID++;
65,489✔
58

59
  public transform: AffineMatrix = AffineMatrix.identity();
65,489✔
60
  public tint?: Color;
61

62
  private _transformStale = true;
65,489✔
63
  public isStale() {
64
    return this._transformStale;
×
65
  }
66

67
  /**
68
   * Gets or sets wether to show debug information about the graphic
69
   */
70
  public showDebug: boolean = false;
65,489✔
71

72
  private _flipHorizontal = false;
65,489✔
73
  /**
74
   * Gets or sets the flipHorizontal, which will flip the graphic horizontally (across the y axis)
75
   */
76
  public get flipHorizontal(): boolean {
77
    return this._flipHorizontal;
66,926✔
78
  }
79

80
  public set flipHorizontal(value: boolean) {
81
    this._flipHorizontal = value;
65,494✔
82
    this._transformStale = true;
65,494✔
83
  }
84

85
  private _flipVertical = false;
65,489✔
86
  /**
87
   * Gets or sets the flipVertical, which will flip the graphic vertically (across the x axis)
88
   */
89
  public get flipVertical(): boolean {
90
    return this._flipVertical;
66,926✔
91
  }
92

93
  public set flipVertical(value: boolean) {
94
    this._flipVertical = value;
65,494✔
95
    this._transformStale = true;
65,494✔
96
  }
97

98
  private _rotation = 0;
65,489✔
99
  /**
100
   * Gets or sets the rotation of the graphic
101
   */
102
  public get rotation(): number {
103
    return this._rotation;
65,858✔
104
  }
105

106
  public set rotation(value: number) {
107
    this._rotation = value;
65,486✔
108
    this._transformStale = true;
65,486✔
109
  }
110

111
  /**
112
   * Gets or sets the opacity of the graphic, 0 is transparent, 1 is solid (opaque).
113
   */
114
  public opacity: number = 1;
65,489✔
115

116
  private _scale = Vector.One;
65,489✔
117
  /**
118
   * Gets or sets the scale of the graphic, this affects the width and
119
   */
120
  public get scale() {
121
    return this._scale;
585,331✔
122
  }
123

124
  public set scale(value: Vector) {
125
    this._scale = watch(value, () => {
65,556✔
126
      this._transformStale = true;
×
127
    });
128
    this._transformStale = true;
65,556✔
129
  }
130

131
  private _origin?: Vector;
132
  /**
133
   * Gets or sets the origin of the graphic, if not set the center of the graphic is the origin
134
   */
135
  public get origin(): Vector | undefined {
136
    return this._origin;
65,879✔
137
  }
138

139
  public set origin(value: Vector | undefined) {
140
    if (value) {
65,485✔
141
      this._origin = watch(value, () => {
3✔
142
        this._transformStale = true;
×
143
      });
144
    }
145
    this._transformStale = true;
65,485✔
146
  }
147

148
  constructor(options?: GraphicOptions) {
149
    if (options) {
65,489✔
150
      this.origin = options.origin ?? this.origin;
65,482✔
151
      this.flipHorizontal = options.flipHorizontal ?? this.flipHorizontal;
65,482✔
152
      this.flipVertical = options.flipVertical ?? this.flipVertical;
65,482✔
153
      this.rotation = options.rotation ?? this.rotation;
65,482✔
154
      this.opacity = options.opacity ?? this.opacity;
65,482✔
155
      this.scale = options.scale ?? this.scale;
65,482✔
156
      this.tint = options.tint ?? this.tint;
65,482✔
157
      if (options.width) {
65,482✔
158
        this._width = options.width;
23✔
159
      }
160

161
      if (options.height) {
65,482✔
162
        this._height = options.height;
23✔
163
      }
164
    }
165
  }
166

167
  public cloneGraphicOptions(): GraphicOptions {
168
    return {
17✔
169
      width: this.width / this.scale.x,
170
      height: this.height / this.scale.y,
171
      origin: this.origin ? this.origin.clone() : undefined,
17✔
172
      flipHorizontal: this.flipHorizontal,
173
      flipVertical: this.flipVertical,
174
      rotation: this.rotation,
175
      opacity: this.opacity,
176
      scale: this.scale ? this.scale.clone() : undefined,
17!
177
      tint: this.tint ? this.tint.clone() : undefined
17✔
178
    };
179
  }
180

181
  private _width: number = 0;
65,489✔
182

183
  /**
184
   * Gets or sets the width of the graphic (always positive)
185
   */
186
  public get width() {
187
    return Math.abs(this._width * this.scale.x);
45✔
188
  }
189

190
  private _height: number = 0;
65,489✔
191

192
  /**
193
   * Gets or sets the height of the graphic (always positive)
194
   */
195
  public get height() {
196
    return Math.abs(this._height * this.scale.y);
45✔
197
  }
198

199
  public set width(value: number) {
200
    this._width = value;
125,910✔
201
    this._transformStale = true;
125,910✔
202
  }
203

204
  public set height(value: number) {
205
    this._height = value;
125,910✔
206
    this._transformStale = true;
125,910✔
207
  }
208

209
  /**
210
   * Gets a copy of the bounds in pixels occupied by the graphic on the the screen. This includes scale.
211
   */
212
  public get localBounds(): BoundingBox {
213
    return BoundingBox.fromDimension(this.width, this.height, Vector.Zero);
985✔
214
  }
215

216
  /**
217
   * Draw the whole graphic to the context including transform
218
   * @param ex The excalibur graphics context
219
   * @param x
220
   * @param y
221
   */
222
  public draw(ex: ExcaliburGraphicsContext, x: number, y: number): void {
223
    this._preDraw(ex, x, y);
3,897✔
224
    this._drawImage(ex, 0, 0);
3,897✔
225
    this._postDraw(ex);
3,897✔
226
  }
227

228
  /**
229
   * Meant to be overridden by the graphic implementation to draw the underlying image (HTMLCanvasElement or HTMLImageElement)
230
   * to the graphics context without transform. Transformations like position, rotation, and scale are handled by {@apilink Graphic._preDraw}
231
   * and {@apilink Graphic._postDraw}
232
   * @param ex The excalibur graphics context
233
   * @param x
234
   * @param y
235
   */
236
  protected abstract _drawImage(ex: ExcaliburGraphicsContext, x: number, y: number): void;
237

238
  /**
239
   * Apply affine transformations to the graphics context to manipulate the graphic before {@apilink Graphic._drawImage}
240
   * @param ex
241
   * @param x
242
   * @param y
243
   */
244
  protected _preDraw(ex: ExcaliburGraphicsContext, x: number, y: number): void {
245
    ex.save();
3,915✔
246
    ex.translate(x, y);
3,915✔
247
    if (this._transformStale) {
3,915✔
248
      this.transform.reset();
376✔
249
      this.transform.scale(Math.abs(this.scale.x), Math.abs(this.scale.y));
376✔
250
      this._rotate(this.transform);
376✔
251
      this._flip(this.transform);
376✔
252
      this._transformStale = false;
376✔
253
    }
254
    ex.multiply(this.transform);
3,915✔
255
    // it is important to multiply alphas so graphics respect the current context
256
    ex.opacity = ex.opacity * this.opacity;
3,915✔
257
    if (this.tint) {
3,915✔
258
      ex.tint = this.tint;
1✔
259
    }
260
  }
261

262
  protected _rotate(ex: ExcaliburGraphicsContext | AffineMatrix) {
263
    const scaleDirX = this.scale.x > 0 ? 1 : -1;
376!
264
    const scaleDirY = this.scale.y > 0 ? 1 : -1;
376!
265
    const origin = this.origin ?? vec(this.width / 2, this.height / 2);
376!
266
    ex.translate(origin.x, origin.y);
376✔
267
    ex.rotate(this.rotation);
376✔
268
    // This is for handling direction changes 1 or -1, that way we don't have mismatched translates()
269
    ex.scale(scaleDirX, scaleDirY);
376✔
270
    ex.translate(-origin.x, -origin.y);
376✔
271
  }
272

273
  protected _flip(ex: ExcaliburGraphicsContext | AffineMatrix) {
274
    if (this.flipHorizontal) {
376✔
275
      ex.translate(this.width / this.scale.x, 0);
5✔
276
      ex.scale(-1, 1);
5✔
277
    }
278

279
    if (this.flipVertical) {
376✔
280
      ex.translate(0, this.height / this.scale.y);
5✔
281
      ex.scale(1, -1);
5✔
282
    }
283
  }
284

285
  /**
286
   * Apply any additional work after {@apilink Graphic._drawImage} and restore the context state.
287
   * @param ex
288
   */
289
  protected _postDraw(ex: ExcaliburGraphicsContext): void {
290
    if (this.showDebug) {
3,915✔
291
      ex.debug.drawRect(0, 0, this.width, this.height);
1✔
292
    }
293
    ex.restore();
3,915✔
294
  }
295

296
  /**
297
   * Returns a new instance of the graphic that has the same properties
298
   */
299
  abstract clone(): Graphic;
300
}
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