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

excaliburjs / Excalibur / 15354777440

30 May 2025 08:03PM UTC coverage: 87.858% (-1.5%) from 89.344%
15354777440

Pull #3385

github

web-flow
Merge a00f57733 into e6ec66358
Pull Request #3385: updated Meet action to add tolerance

5002 of 6948 branches covered (71.99%)

3 of 5 new or added lines in 2 files covered. (60.0%)

872 existing lines in 83 files now uncovered.

13661 of 15549 relevant lines covered (87.86%)

25187.01 hits per line

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

92.98
/src/engine/Graphics/SpriteSheet.ts
1
import type { ImageSource } from './ImageSource';
2
import type { SourceView } from './Sprite';
3
import { Sprite } from './Sprite';
4
import type { GraphicOptions } from './Graphic';
5
import type { TiledSpriteOptions } from './TiledSprite';
6
import { TiledSprite } from './TiledSprite';
7
import { Vector } from '../Math/vector';
8

9
/**
10
 * Specify sprite sheet spacing options, useful if your sprites are not tightly packed
11
 * and have space between them.
12
 */
13
export interface SpriteSheetSpacingDimensions {
14
  /**
15
   * The starting point to offset and start slicing the sprite sheet from the top left of the image.
16
   * Default is (0, 0)
17
   */
18
  originOffset?: { x?: number; y?: number } | Vector;
19

20
  /**
21
   * The margin between sprites.
22
   * Default is (0, 0)
23
   */
24
  margin?: { x?: number; y?: number } | Vector;
25
}
26

27
/**
28
 * Sprite sheet options for slicing up images
29
 */
30
export interface SpriteSheetGridOptions {
31
  /**
32
   * Source image to use for each sprite
33
   */
34
  image: ImageSource;
35
  /**
36
   * Grid definition for the sprite sheet
37
   */
38
  grid: {
39
    /**
40
     * Number of rows in the sprite sheet
41
     */
42
    rows: number;
43
    /**
44
     * Number of columns in the sprite sheet
45
     */
46
    columns: number;
47
    /**
48
     * Width of each individual sprite
49
     */
50
    spriteWidth: number;
51
    /**
52
     * Height of each individual sprite
53
     */
54
    spriteHeight: number;
55
  };
56
  /**
57
   * Optionally specify any spacing information between sprites
58
   */
59
  spacing?: SpriteSheetSpacingDimensions;
60
}
61

62
export interface SpriteSheetSparseOptions {
63
  /**
64
   * Source image to use for each sprite
65
   */
66
  image: ImageSource;
67
  /**
68
   * List of source view rectangles to create a sprite sheet from
69
   */
70
  sourceViews: SourceView[];
71
}
72

73
export interface SpriteSheetOptions {
74
  /**
75
   * Source sprites for the sprite sheet
76
   */
77
  sprites: Sprite[];
78
  /**
79
   * Optionally specify the number of rows in a sprite sheet (default 1 row)
80
   */
81
  rows?: number;
82
  /**
83
   * Optionally specify the number of columns in a sprite sheet (default sprites.length)
84
   */
85
  columns?: number;
86
}
87

88
export interface GetSpriteOptions extends GraphicOptions {}
89

90
/**
91
 * Represents a collection of sprites from a source image with some organization in a grid
92
 */
93
export class SpriteSheet {
94
  public readonly sprites: Sprite[] = [];
979✔
95
  public readonly rows: number;
96
  public readonly columns: number;
97

98
  /**
99
   * Build a new sprite sheet from a list of sprites
100
   *
101
   * Use {@apilink SpriteSheet.fromImageSource} to create a SpriteSheet from an {@apilink ImageSource} organized in a grid
102
   * @param options
103
   */
104
  constructor(options: SpriteSheetOptions) {
105
    const { sprites, rows, columns } = options;
979✔
106
    this.sprites = sprites;
979✔
107
    this.rows = rows ?? 1;
979✔
108
    this.columns = columns ?? this.sprites.length;
979✔
109
  }
110

111
  /**
112
   * Find a sprite by their x/y integer coordinates in the SpriteSheet, for example `getSprite(0, 0)` is the {@apilink Sprite} in the top-left
113
   * and `getSprite(1, 0)` is the sprite one to the right.
114
   * @param x
115
   * @param y
116
   */
117
  public getSprite(x: number, y: number, options?: GetSpriteOptions): Sprite {
118
    if (x >= this.columns || x < 0) {
817✔
119
      throw Error(`No sprite exists in the SpriteSheet at (${x}, ${y}), x: ${x} should be between 0 and ${this.columns - 1} columns`);
2✔
120
    }
121
    if (y >= this.rows || y < 0) {
815✔
122
      throw Error(`No sprite exists in the SpriteSheet at (${x}, ${y}), y: ${y} should be between 0 and ${this.rows - 1} rows`);
2✔
123
    }
124
    const spriteIndex = x + y * this.columns;
813✔
125
    const sprite = this.sprites[spriteIndex];
813✔
126
    if (sprite) {
813!
127
      if (options) {
813✔
128
        const spriteWithOptions = sprite.clone();
3✔
129
        spriteWithOptions.flipHorizontal = options.flipHorizontal ?? spriteWithOptions.flipHorizontal;
3✔
130
        spriteWithOptions.flipVertical = options.flipVertical ?? spriteWithOptions.flipVertical;
3✔
131
        spriteWithOptions.width = options.width ?? spriteWithOptions.width;
3✔
132
        spriteWithOptions.height = options.height ?? spriteWithOptions.height;
3✔
133
        spriteWithOptions.rotation = options.rotation ?? spriteWithOptions.rotation;
3✔
134
        spriteWithOptions.scale = options.scale ?? spriteWithOptions.scale;
3✔
135
        spriteWithOptions.opacity = options.opacity ?? spriteWithOptions.opacity;
3✔
136
        spriteWithOptions.tint = options.tint ?? spriteWithOptions.tint;
3✔
137
        spriteWithOptions.origin = options.origin ?? spriteWithOptions.origin;
3✔
138
        return spriteWithOptions;
3✔
139
      }
140
      return sprite;
810✔
141
    }
UNCOV
142
    throw Error(`Invalid sprite coordinates (${x}, ${y})`);
×
143
  }
144

145
  /**
146
   * Find a sprite by their x/y integer coordinates in the SpriteSheet and configures tiling to repeat by default,
147
   * for example `getTiledSprite(0, 0)` is the {@apilink TiledSprite} in the top-left
148
   * and `getTiledSprite(1, 0)` is the sprite one to the right.
149
   *
150
   * Example:
151
   *
152
   * ```typescript
153
   * spriteSheet.getTiledSprite(1, 0, {
154
   * width: game.screen.width,
155
   * height: 200,
156
   * wrapping: {
157
   * x: ex.ImageWrapping.Repeat,
158
   * y: ex.ImageWrapping.Clamp
159
   * }
160
   * });
161
   * ```
162
   * @param x
163
   * @param y
164
   * @param options
165
   */
166
  public getTiledSprite(x: number, y: number, options?: Partial<Omit<TiledSpriteOptions & GraphicOptions, 'image'>>): TiledSprite {
167
    if (x >= this.columns || x < 0) {
1!
UNCOV
168
      throw Error(`No sprite exists in the SpriteSheet at (${x}, ${y}), x: ${x} should be between 0 and ${this.columns - 1} columns`);
×
169
    }
170
    if (y >= this.rows || y < 0) {
1!
UNCOV
171
      throw Error(`No sprite exists in the SpriteSheet at (${x}, ${y}), y: ${y} should be between 0 and ${this.rows - 1} rows`);
×
172
    }
173
    const spriteIndex = x + y * this.columns;
1✔
174
    const sprite = this.sprites[spriteIndex];
1✔
175
    if (sprite) {
1!
176
      return TiledSprite.fromSprite(sprite, options);
1✔
177
    }
UNCOV
178
    throw Error(`Invalid sprite coordinates (${x}, ${y})`);
×
179
  }
180

181
  /**
182
   * Create a sprite sheet from a sparse set of {@apilink SourceView} rectangles
183
   * @param options
184
   */
185
  public static fromImageSourceWithSourceViews(options: SpriteSheetSparseOptions): SpriteSheet {
186
    const sprites: Sprite[] = options.sourceViews.map((sourceView) => {
1✔
187
      return new Sprite({
2✔
188
        image: options.image,
189
        sourceView
190
      });
191
    });
192
    return new SpriteSheet({ sprites });
1✔
193
  }
194

195
  /**
196
   * Create a SpriteSheet from an {@apilink ImageSource} organized in a grid
197
   *
198
   * Example:
199
   * ```
200
   * const spriteSheet = SpriteSheet.fromImageSource({
201
   *   image: imageSource,
202
   *   grid: {
203
   *     rows: 5,
204
   *     columns: 2,
205
   *     spriteWidth: 32, // pixels
206
   *     spriteHeight: 32 // pixels
207
   *   },
208
   *   // Optionally specify spacing
209
   *   spacing: {
210
   *     // pixels from the top left to start the sprite parsing
211
   *     originOffset: {
212
   *       x: 5,
213
   *       y: 5
214
   *     },
215
   *     // pixels between each sprite while parsing
216
   *     margin: {
217
   *       x: 1,
218
   *       y: 1
219
   *     }
220
   *   }
221
   * })
222
   * ```
223
   * @param options
224
   */
225
  public static fromImageSource(options: SpriteSheetGridOptions): SpriteSheet {
226
    const sprites: Sprite[] = [];
974✔
227
    options.spacing = options.spacing ?? {};
974✔
228
    const {
229
      image,
230
      grid: { rows, columns: cols, spriteWidth, spriteHeight },
231
      spacing: { originOffset, margin }
232
    } = options;
974✔
233
    let newmargin: { x: number; y: number } | undefined;
234
    let neworiginOffset: { x: number; y: number } | undefined;
235

236
    if (originOffset instanceof Vector) {
974✔
237
      neworiginOffset = { x: originOffset.x, y: originOffset.y };
1✔
238
    } else {
239
      if (originOffset) {
973✔
240
        neworiginOffset = { x: originOffset.x as number, y: originOffset.y as number };
5✔
241
      }
242
    }
243

244
    if (margin instanceof Vector) {
974✔
245
      newmargin = { x: margin.x, y: margin.y };
1✔
246
    } else {
247
      if (margin) {
973✔
248
        newmargin = { x: margin.x as number, y: margin.y as number };
5✔
249
      }
250
    }
251

252
    const offsetDefaults = { x: 0, y: 0, ...neworiginOffset };
974✔
253
    const marginDefaults = { x: 0, y: 0, ...newmargin };
974✔
254
    for (let x = 0; x < cols; x++) {
974✔
255
      for (let y = 0; y < rows; y++) {
15,485✔
256
        sprites[x + y * cols] = new Sprite({
62,873✔
257
          image: image,
258
          sourceView: {
259
            x: x * spriteWidth + marginDefaults.x * x + offsetDefaults.x,
260
            y: y * spriteHeight + marginDefaults.y * y + offsetDefaults.y,
261
            width: spriteWidth,
262
            height: spriteHeight
263
          },
264
          destSize: { height: spriteHeight, width: spriteWidth }
265
        });
266
      }
267
    }
268
    return new SpriteSheet({
974✔
269
      sprites: sprites,
270
      rows: rows,
271
      columns: cols
272
    });
273
  }
274

275
  public clone(): SpriteSheet {
276
    return new SpriteSheet({
1✔
277
      sprites: this.sprites.map((sprite) => sprite.clone()),
1✔
278
      rows: this.rows,
279
      columns: this.columns
280
    });
281
  }
282
}
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