• 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/SpriteSheet.ts
1
import { ImageSource } from './ImageSource';
2
import { SourceView, Sprite } from './Sprite';
3
import { GraphicOptions } from './Graphic';
4
import { TiledSprite, TiledSpriteOptions } from './TiledSprite';
5
import { Vector } from '../Math/vector';
6

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

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

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

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

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

86
export interface GetSpriteOptions extends GraphicOptions {}
87

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

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

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

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

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

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

UNCOV
234
    if (originOffset instanceof Vector) {
×
UNCOV
235
      neworiginOffset = { x: originOffset.x, y: originOffset.y };
×
236
    } else {
UNCOV
237
      if (originOffset) {
×
UNCOV
238
        neworiginOffset = { x: originOffset.x as number, y: originOffset.y as number };
×
239
      }
240
    }
241

UNCOV
242
    if (margin instanceof Vector) {
×
UNCOV
243
      newmargin = { x: margin.x, y: margin.y };
×
244
    } else {
UNCOV
245
      if (margin) {
×
UNCOV
246
        newmargin = { x: margin.x as number, y: margin.y as number };
×
247
      }
248
    }
249

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

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