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

excaliburjs / Excalibur / 14804036802

02 May 2025 09:58PM UTC coverage: 5.927% (-83.4%) from 89.28%
14804036802

Pull #3404

github

web-flow
Merge 5c103d7f8 into 0f2ccaeb2
Pull Request #3404: feat: added Graph module to Math

234 of 8383 branches covered (2.79%)

229 of 246 new or added lines in 1 file covered. (93.09%)

13145 existing lines in 208 files now uncovered.

934 of 15759 relevant lines covered (5.93%)

4.72 hits per line

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

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

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

38
export class SpriteFont extends Graphic implements FontRenderer {
UNCOV
39
  private _text: string = '';
×
UNCOV
40
  public alphabet: string = '';
×
41
  public spriteSheet: SpriteSheet;
42

UNCOV
43
  public shadow?: { offset: Vector } = undefined;
×
UNCOV
44
  public caseInsensitive = false;
×
UNCOV
45
  public spacing: number = 0;
×
UNCOV
46
  public lineHeight: number | undefined = undefined;
×
47

UNCOV
48
  private _logger = Logger.getInstance();
×
49

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

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

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

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

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

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

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

UNCOV
136
    this._preDraw(ex, x, y);
×
UNCOV
137
    this._drawImage(ex, 0, 0, maxWidth);
×
UNCOV
138
    this._postDraw(ex);
×
139
  }
140

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

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

UNCOV
162
    const lines = text.split('\n');
×
163

UNCOV
164
    if (maxWidth == null) {
×
UNCOV
165
      return lines;
×
166
    }
167

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

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

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

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