• 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

70.59
/src/engine/Graphics/SpriteFont.ts
1
import { Vector } from '../Math/vector';
2
import { Logger } from '../Util/Log';
3
import type { ExcaliburGraphicsContext } from './Context/ExcaliburGraphicsContext';
4
import type { FontRenderer } from './FontCommon';
5
import type { GraphicOptions } from './Graphic';
6
import { Graphic } from './Graphic';
7
import type { Sprite } from './Sprite';
8
import type { SpriteSheet } from './SpriteSheet';
9
import { BoundingBox } from '../Collision/BoundingBox';
10

11
export interface SpriteFontOptions {
12
  /**
13
   * Alphabet string in spritesheet order (default is row column order)
14
   * example: 'abcdefghijklmnopqrstuvwxyz'
15
   */
16
  alphabet: string;
17
  /**
18
   * {@apilink SpriteSheet} to source character sprites from
19
   */
20
  spriteSheet: SpriteSheet;
21
  /**
22
   * Optionally ignore case in the supplied text;
23
   */
24
  caseInsensitive?: boolean;
25
  /**
26
   * Optionally override the text line height, useful for multiline text. If unset will use default.
27
   */
28
  lineHeight?: number | undefined;
29
  /**
30
   * Optionally adjust the spacing between character sprites
31
   */
32
  spacing?: number;
33
  /**
34
   * Optionally specify a "shadow"
35
   */
36
  shadow?: { offset: Vector };
37
}
38

39
export class SpriteFont extends Graphic implements FontRenderer {
40
  private _text: string = '';
952✔
41
  public alphabet: string = '';
952✔
42
  public spriteSheet: SpriteSheet;
43

44
  public shadow?: { offset: Vector } = undefined;
952✔
45
  public caseInsensitive = false;
952✔
46
  public spacing: number = 0;
952✔
47
  public lineHeight: number | undefined = undefined;
952✔
48

49
  private _logger = Logger.getInstance();
952✔
50

51
  constructor(options: SpriteFontOptions & GraphicOptions) {
52
    super(options);
952✔
53
    const { alphabet, spriteSheet, caseInsensitive, spacing, shadow, lineHeight } = options;
952✔
54
    this.alphabet = alphabet;
952✔
55
    this.spriteSheet = spriteSheet;
952✔
56
    this.caseInsensitive = caseInsensitive ?? this.caseInsensitive;
952!
57
    this.spacing = spacing ?? this.spacing;
952!
58
    this.shadow = shadow ?? this.shadow;
952!
59
    this.lineHeight = lineHeight ?? this.lineHeight;
952!
60
  }
61

62
  private _getCharacterSprites(text: string): Sprite[] {
63
    const results: Sprite[] = [];
38✔
64
    // handle case insensitive
65
    const textToRender = this.caseInsensitive ? text.toLocaleLowerCase() : text;
38!
66
    const alphabet = this.caseInsensitive ? this.alphabet.toLocaleLowerCase() : this.alphabet;
38!
67

68
    // for each letter in text
69
    for (let letterIndex = 0; letterIndex < textToRender.length; letterIndex++) {
38✔
70
      // find the sprite index in alphabet , if there is an error pick the first
71
      const letter = textToRender[letterIndex];
536✔
72
      let spriteIndex = alphabet.indexOf(letter);
536✔
73
      if (spriteIndex === -1) {
536!
74
        spriteIndex = 0;
×
75
        this._logger.warnOnce(`SpriteFont - Cannot find letter '${letter}' in configured alphabet '${alphabet}'.`);
×
76
        this._logger.warnOnce('There maybe be more issues in the SpriteFont configuration. No additional warnings will be logged.');
×
77
      }
78

79
      const letterSprite = this.spriteSheet.sprites[spriteIndex];
536✔
80
      if (letterSprite) {
536✔
81
        results.push(letterSprite);
522✔
82
      } else {
83
        this._logger.warnOnce(`SpriteFont - Cannot find sprite for '${letter}' at index '${spriteIndex}' in configured SpriteSheet`);
14✔
84
        this._logger.warnOnce('There maybe be more issues in the SpriteFont configuration. No additional warnings will be logged.');
14✔
85
      }
86
    }
87
    return results;
38✔
88
  }
89

90
  public measureText(text: string, maxWidth?: number): BoundingBox {
91
    const lines = this._getLinesFromText(text, maxWidth);
20✔
92
    const maxWidthLine = lines.reduce((a, b) => {
20✔
93
      return a.length > b.length ? a : b;
×
94
    });
95
    const sprites = this._getCharacterSprites(maxWidthLine);
20✔
96
    let width = 0;
20✔
97
    let height = 0;
20✔
98
    for (const sprite of sprites) {
20✔
99
      width += sprite.width + this.spacing;
263✔
100
      height = Math.max(height, sprite.height);
263✔
101
    }
102
    return BoundingBox.fromDimension(width * this.scale.x, height * lines.length * this.scale.y, Vector.Zero);
20✔
103
  }
104

105
  protected _drawImage(ex: ExcaliburGraphicsContext, x: number, y: number, maxWidth?: number): void {
106
    let xCursor = 0;
18✔
107
    let yCursor = 0;
18✔
108
    let height = 0;
18✔
109
    const lines = this._getLinesFromText(this._text, maxWidth);
18✔
110
    for (const line of lines) {
18✔
111
      for (const sprite of this._getCharacterSprites(line)) {
18✔
112
        // draw it in the right spot and increase the cursor by sprite width
113
        sprite.draw(ex, x + xCursor, y + yCursor);
259✔
114
        xCursor += sprite.width + this.spacing;
259✔
115
        height = Math.max(height, sprite.height);
259✔
116
      }
117
      xCursor = 0;
18✔
118
      yCursor += this.lineHeight ?? height;
18!
119
    }
120
  }
121

122
  render(ex: ExcaliburGraphicsContext, text: string, _color: any, x: number, y: number, maxWidth?: number) {
123
    // SpriteFont doesn't support _color, yet...
124
    this._text = text;
18✔
125
    const bounds = this.measureText(text, maxWidth);
18✔
126
    this.width = bounds.width;
18✔
127
    this.height = bounds.height;
18✔
128
    if (this.shadow) {
18!
129
      ex.save();
×
130
      ex.translate(this.shadow.offset.x, this.shadow.offset.y);
×
131
      this._preDraw(ex, x, y);
×
132
      this._drawImage(ex, 0, 0, maxWidth);
×
133
      this._postDraw(ex);
×
134
      ex.restore();
×
135
    }
136

137
    this._preDraw(ex, x, y);
18✔
138
    this._drawImage(ex, 0, 0, maxWidth);
18✔
139
    this._postDraw(ex);
18✔
140
  }
141

142
  clone(): SpriteFont {
143
    return new SpriteFont({
×
144
      alphabet: this.alphabet,
145
      spriteSheet: this.spriteSheet,
146
      spacing: this.spacing
147
    });
148
  }
149

150
  /**
151
   * Return array of lines split based on the \n character, and the maxWidth? constraint
152
   * @param text
153
   * @param maxWidth
154
   */
155
  private _cachedText?: string;
156
  private _cachedLines?: string[];
157
  private _cachedRenderWidth?: number;
158
  private _getLinesFromText(text: string, maxWidth?: number) {
159
    if (this._cachedText === text && this._cachedRenderWidth === maxWidth && this._cachedLines?.length) {
38!
160
      return this._cachedLines;
×
161
    }
162

163
    const lines = text.split('\n');
38✔
164

165
    if (maxWidth == null) {
38!
166
      return lines;
38✔
167
    }
168

169
    // If the current line goes past the maxWidth, append a new line without modifying the underlying text.
170
    for (let i = 0; i < lines.length; i++) {
×
171
      let line = lines[i];
×
172
      let newLine = '';
×
173
      // Note: we subtract the spacing to counter the initial padding on the left side.
174
      if (this.measureText(line).width > maxWidth) {
×
175
        while (this.measureText(line).width > maxWidth) {
×
176
          newLine = line[line.length - 1] + newLine;
×
177
          line = line.slice(0, -1); // Remove last character from line
×
178
        }
179

180
        // Update the array with our new values
181
        lines[i] = line;
×
182
        lines[i + 1] = newLine;
×
183
      }
184
    }
185

186
    this._cachedText = text;
×
187
    this._cachedLines = lines;
×
188
    this._cachedRenderWidth = maxWidth;
×
189

190
    return lines;
×
191
  }
192
}
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