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

excaliburjs / Excalibur / 14840900305

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

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%)

25165.01 hits per line

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

98.82
/src/engine/Graphics/Raster.ts
1
import type { GraphicOptions } from './Graphic';
2
import { Graphic } from './Graphic';
3
import type { ExcaliburGraphicsContext } from './Context/ExcaliburGraphicsContext';
4
import { Color } from '../Color';
5
import { Vector } from '../Math/vector';
6
import { BoundingBox } from '../Collision/BoundingBox';
7
import { watch } from '../Util/Watch';
8
import type { ImageFiltering } from './Filtering';
9
import { omit } from '../Util/Util';
10

11
export interface RasterOptions extends GraphicOptions {
12
  /**
13
   * Optionally specify a quality number, which is how much to scale the internal Raster. Default is 1.
14
   *
15
   * For example if the quality is set to 2, it doubles the internal raster bitmap in memory.
16
   *
17
   * Adjusting this value can be useful if you are working with small rasters.
18
   */
19
  quality?: number;
20
  /**
21
   * Optionally specify "smoothing" if you want antialiasing to apply to the raster's bitmap context, by default `false`
22
   */
23
  smoothing?: boolean;
24

25
  /**
26
   * Optionally specify the color of the raster's bitmap context, by default {@apilink Color.Black}
27
   */
28
  color?: Color;
29

30
  /**
31
   * Optionally specify the stroke color of the raster's bitmap context, by default undefined
32
   */
33
  strokeColor?: Color;
34

35
  /**
36
   * Optionally specify the line width of the raster's bitmap, by default 1 pixel
37
   */
38
  lineWidth?: number;
39

40
  /**
41
   * Optionally specify the line dash of the raster's bitmap, by default `[]` which means none
42
   */
43
  lineDash?: number[];
44

45
  /**
46
   * Optionally specify the line end style, default is "butt".
47
   */
48
  lineCap?: 'butt' | 'round' | 'square';
49

50
  /**
51
   * Optionally specify the padding to apply to the bitmap
52
   */
53
  padding?: number;
54

55
  /**
56
   * Optionally specify what image filtering mode should be used, {@apilink ImageFiltering.Pixel} for pixel art,
57
   * {@apilink ImageFiltering.Blended} for hi-res art
58
   *
59
   * By default unset, rasters defer to the engine antialiasing setting
60
   */
61
  filtering?: ImageFiltering;
62
}
63

64
/**
65
 * A Raster is a Graphic that needs to be first painted to a HTMLCanvasElement before it can be drawn to the
66
 * {@apilink ExcaliburGraphicsContext}. This is useful for generating custom images using the 2D canvas api.
67
 *
68
 * Implementors must implement the {@apilink Raster.execute} method to rasterize their drawing.
69
 */
70
export abstract class Raster extends Graphic {
71
  public filtering?: ImageFiltering;
72
  public lineCap: 'butt' | 'round' | 'square' = 'butt';
1,301✔
73
  public quality: number = 1;
1,301✔
74

75
  public _bitmap: HTMLCanvasElement;
76
  protected _ctx: CanvasRenderingContext2D;
77
  private _dirty: boolean = true;
1,301✔
78

79
  constructor(options?: RasterOptions) {
80
    super(omit({ ...options }, ['width', 'height'])); // rasters do some special sauce with width/height
1,301✔
81
    if (options) {
1,301✔
82
      this.quality = options.quality ?? this.quality;
1,293✔
83
      this.color = options.color ?? Color.Black;
1,293✔
84
      this.strokeColor = options?.strokeColor;
1,293!
85
      this.smoothing = options.smoothing ?? this.smoothing;
1,293✔
86
      this.lineWidth = options.lineWidth ?? this.lineWidth;
1,293✔
87
      this.lineDash = options.lineDash ?? this.lineDash;
1,293✔
88
      this.lineCap = options.lineCap ?? this.lineCap;
1,293✔
89
      this.padding = options.padding ?? this.padding;
1,293✔
90
      this.filtering = options.filtering ?? this.filtering;
1,293✔
91
    }
92
    this._bitmap = document.createElement('canvas');
1,301✔
93
    // get the default canvas width/height as a fallback
94
    const bitmapWidth = options?.width ?? this._bitmap.width;
1,301✔
95
    const bitmapHeight = options?.height ?? this._bitmap.height;
1,301✔
96
    this.width = bitmapWidth;
1,301✔
97
    this.height = bitmapHeight;
1,301✔
98
    const maybeCtx = this._bitmap.getContext('2d');
1,301✔
99
    if (!maybeCtx) {
1,301!
100
      /* istanbul ignore next */
101
      throw new Error('Browser does not support 2d canvas drawing, cannot create Raster graphic');
×
102
    } else {
103
      this._ctx = maybeCtx;
1,301✔
104
    }
105
  }
106

107
  public cloneRasterOptions(): RasterOptions {
108
    return {
8✔
109
      color: this.color ? this.color.clone() : undefined,
8!
110
      strokeColor: this.strokeColor ? this.strokeColor.clone() : undefined,
8✔
111
      smoothing: this.smoothing,
112
      lineWidth: this.lineWidth,
113
      lineDash: this.lineDash,
114
      lineCap: this.lineCap,
115
      quality: this.quality,
116
      padding: this.padding
117
    };
118
  }
119

120
  /**
121
   * Gets whether the graphic is dirty, this means there are changes that haven't been re-rasterized
122
   */
123
  public get dirty() {
124
    return this._dirty;
7✔
125
  }
126

127
  /**
128
   * Flags the graphic as dirty, meaning it must be re-rasterized before draw.
129
   * This should be called any time the graphics state changes such that it affects the outputted drawing
130
   */
131
  public flagDirty() {
132
    this._dirty = true;
12,710✔
133
  }
134

135
  private _originalWidth?: number;
136
  /**
137
   * Gets or sets the current width of the Raster graphic. Setting the width will cause the raster
138
   * to be flagged dirty causing a re-raster on the next draw.
139
   *
140
   * Any `padding`s or `quality` set will be factored into the width
141
   */
142
  public get width() {
143
    return Math.abs(this._getTotalWidth() * this.scale.x);
1,508✔
144
  }
145
  public set width(value: number) {
146
    value /= Math.abs(this.scale.x);
1,608✔
147
    this._bitmap.width = value;
1,608✔
148
    this._originalWidth = value;
1,608✔
149
    this.flagDirty();
1,608✔
150
  }
151

152
  private _originalHeight?: number;
153
  /**
154
   * Gets or sets the current height of the Raster graphic. Setting the height will cause the raster
155
   * to be flagged dirty causing a re-raster on the next draw.
156
   *
157
   * Any `padding` or `quality` set will be factored into the height
158
   */
159
  public get height() {
160
    return Math.abs(this._getTotalHeight() * this.scale.y);
1,555✔
161
  }
162

163
  public set height(value: number) {
164
    value /= Math.abs(this.scale.y);
1,608✔
165
    this._bitmap.height = value;
1,608✔
166
    this._originalHeight = value;
1,608✔
167
    this.flagDirty();
1,608✔
168
  }
169

170
  private _getTotalWidth() {
171
    return ((this._originalWidth ?? this._bitmap.width) + this.padding * 2) * 1;
4,113!
172
  }
173

174
  private _getTotalHeight() {
175
    return ((this._originalHeight ?? this._bitmap.height) + this.padding * 2) * 1;
4,160!
176
  }
177

178
  /**
179
   * Returns the local bounds of the Raster including the padding
180
   */
181
  public get localBounds() {
182
    return BoundingBox.fromDimension(this._getTotalWidth() * this.scale.x, this._getTotalHeight() * this.scale.y, Vector.Zero);
383✔
183
  }
184

185
  private _smoothing: boolean = false;
1,301✔
186
  /**
187
   * Gets or sets the smoothing (anti-aliasing of the graphic). Setting the height will cause the raster
188
   * to be flagged dirty causing a re-raster on the next draw.
189
   */
190
  public get smoothing() {
191
    return this._smoothing;
1,387✔
192
  }
193
  public set smoothing(value: boolean) {
194
    this._smoothing = value;
1,294✔
195
    this.flagDirty();
1,294✔
196
  }
197

198
  private _color: Color = watch(Color.Black, () => this.flagDirty());
1,301✔
199
  /**
200
   * Gets or sets the fillStyle of the Raster graphic. Setting the fillStyle will cause the raster to be
201
   * flagged dirty causing a re-raster on the next draw.
202
   */
203
  public get color() {
204
    return this._color;
1,403✔
205
  }
206
  public set color(value) {
207
    this.flagDirty();
1,294✔
208
    this._color = watch(value, () => this.flagDirty());
1,294✔
209
  }
210

211
  private _strokeColor: Color | undefined;
212
  /**
213
   * Gets or sets the strokeStyle of the Raster graphic. Setting the strokeStyle will cause the raster to be
214
   * flagged dirty causing a re-raster on the next draw.
215
   */
216
  public get strokeColor() {
217
    return this._strokeColor;
1,395✔
218
  }
219
  public set strokeColor(value: Color | undefined) {
220
    this.flagDirty();
1,294✔
221
    if (value) {
1,294✔
222
      this._strokeColor = watch(value, () => this.flagDirty());
14✔
223
    }
224
  }
225

226
  private _lineWidth: number = 1;
1,301✔
227
  /**
228
   * Gets or sets the line width of the Raster graphic. Setting the lineWidth will cause the raster to be
229
   * flagged dirty causing a re-raster on the next draw.
230
   */
231
  public get lineWidth() {
232
    return this._lineWidth;
2,403✔
233
  }
234
  public set lineWidth(value) {
235
    this._lineWidth = value;
1,293✔
236
    this.flagDirty();
1,293✔
237
  }
238

239
  private _lineDash: number[] = [];
1,301✔
240
  public get lineDash() {
241
    return this._lineDash;
2,404✔
242
  }
243

244
  public set lineDash(value) {
245
    this._lineDash = value;
1,293✔
246
    this.flagDirty();
1,293✔
247
  }
248

249
  private _padding: number = 0;
1,301✔
250
  public get padding() {
251
    return this._padding;
11,787✔
252
  }
253

254
  public set padding(value: number) {
255
    this._padding = value;
1,306✔
256
    this.flagDirty();
1,306✔
257
  }
258

259
  /**
260
   * Rasterize the graphic to a bitmap making it usable as in excalibur. Rasterize is called automatically if
261
   * the graphic is {@apilink Raster.dirty} on the next {@apilink Graphic.draw} call
262
   */
263
  public rasterize(): void {
264
    this._dirty = false;
1,111✔
265
    this._ctx.clearRect(0, 0, this._getTotalWidth(), this._getTotalHeight());
1,111✔
266
    this._ctx.save();
1,111✔
267
    this._applyRasterProperties(this._ctx);
1,111✔
268
    this.execute(this._ctx);
1,111✔
269
    this._ctx.restore();
1,111✔
270
  }
271

272
  protected _applyRasterProperties(ctx: CanvasRenderingContext2D) {
273
    this._bitmap.width = this._getTotalWidth() * this.quality;
1,111✔
274
    this._bitmap.height = this._getTotalHeight() * this.quality;
1,111✔
275
    // Do a bad thing to pass the filtering as an attribute
276
    this._bitmap.setAttribute('filtering', this.filtering as any);
1,111✔
277
    this._bitmap.setAttribute('forceUpload', 'true');
1,111✔
278
    ctx.scale(this.quality, this.quality);
1,111✔
279
    ctx.translate(this.padding, this.padding);
1,111✔
280
    ctx.imageSmoothingEnabled = this.smoothing;
1,111✔
281
    ctx.lineWidth = this.lineWidth;
1,111✔
282
    ctx.setLineDash(this.lineDash ?? ctx.getLineDash());
1,111!
283
    ctx.lineCap = this.lineCap;
1,111✔
284
    ctx.strokeStyle = this.strokeColor?.toString() ?? '';
1,111✔
285
    ctx.fillStyle = this.color?.toString();
1,111!
286
  }
287

288
  protected _drawImage(ex: ExcaliburGraphicsContext, x: number, y: number) {
289
    if (this._dirty) {
1,998✔
290
      this.rasterize();
844✔
291
    }
292
    ex.scale(1 / this.quality, 1 / this.quality);
1,998✔
293
    ex.drawImage(this._bitmap, x, y);
1,998✔
294
  }
295

296
  /**
297
   * Executes drawing implementation of the graphic, this is where the specific drawing code for the graphic
298
   * should be implemented. Once `rasterize()` the graphic can be drawn to the {@apilink ExcaliburGraphicsContext} via `draw(...)`
299
   * @param ctx Canvas to draw the graphic to
300
   */
301
  abstract execute(ctx: CanvasRenderingContext2D): void;
302
}
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