From 9ab78eeea24a7be874c736e76a06c36270796533 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sun, 8 Dec 2024 17:35:14 -0600 Subject: [PATCH] feat: [#3312] Add getTiledSprite to SpriteSheet closes: #3312 --- CHANGELOG.md | 1 + sandbox/tests/tiling/index.ts | 29 +++++++++++- src/engine/Graphics/SpriteSheet.ts | 43 ++++++++++++++++-- src/engine/Graphics/TiledSprite.ts | 17 +++++-- src/spec/SpriteSheetSpec.ts | 12 +++-- src/spec/TiledSpriteSpec.ts | 31 +++++++++++++ .../TiledSpriteSpec/from-spritesheet.png | Bin 0 -> 22905 bytes 7 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 src/spec/images/TiledSpriteSpec/from-spritesheet.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 85569b6e5..dca3f247f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Added `ex.SpriteSheet.getTiledSprite(...)` to help pulling tiling sprites out of a sprite sheet - Alias the `engine.screen.drawWidth/drawHeight` with `engine.screen.width/height`; - Added convenience types `ex.TiledSprite` and `ex.TiledAnimation` for Tiling Sprites and Animations ```typescript diff --git a/sandbox/tests/tiling/index.ts b/sandbox/tests/tiling/index.ts index 4d3542122..1d98e7124 100644 --- a/sandbox/tests/tiling/index.ts +++ b/sandbox/tests/tiling/index.ts @@ -22,6 +22,13 @@ var cardSpriteSheet = ex.SpriteSheet.fromImageSource({ }); cardSpriteSheet.sprites.forEach((s) => (s.scale = ex.vec(2, 2))); + +var tilingCard = cardSpriteSheet.getTiledSprite(0, 0, { + width: 300, + height: 300, + scale: ex.vec(2, 2) +}); + var cardAnimation = ex.Animation.fromSpriteSheet(cardSpriteSheet, ex.range(0, 14 * 4), 200); var groundImage = new ex.ImageSource('./ground.png'); @@ -47,9 +54,17 @@ var tiledGroundSprite = ex.TiledSprite.fromSprite(groundSprite, { } }); +var tiledCardFromCtor = new ex.TiledSprite({ + image: cards, + sourceView: { x: 11, y: 2, width: 42, height: 60 }, + scale: ex.vec(0.5, 0.5), + width: 200, + height: 200 +}); + var tilingAnimation = new ex.TiledAnimation({ animation: cardAnimation, - sourceView: { x: 20, y: 20 }, + // sourceView: { x: 20, y: 20 }, width: 200, height: 200, wrapping: ex.ImageWrapping.Repeat @@ -58,6 +73,18 @@ var tilingAnimation = new ex.TiledAnimation({ // tilingAnimation.sourceView = {x: 0, y: 0}; game.start(loader).then(() => { + var otherCardActor = new ex.Actor({ + pos: ex.vec(200, 200) + }); + otherCardActor.graphics.use(tilingCard); + game.add(otherCardActor); + + var otherOtherCardActor = new ex.Actor({ + pos: ex.vec(600, 200) + }); + otherOtherCardActor.graphics.use(tiledCardFromCtor); + game.add(otherOtherCardActor); + var cardActor = new ex.Actor({ pos: ex.vec(400, 400) }); diff --git a/src/engine/Graphics/SpriteSheet.ts b/src/engine/Graphics/SpriteSheet.ts index a28410e0f..59946bc8d 100644 --- a/src/engine/Graphics/SpriteSheet.ts +++ b/src/engine/Graphics/SpriteSheet.ts @@ -1,6 +1,7 @@ import { ImageSource } from './ImageSource'; import { SourceView, Sprite } from './Sprite'; import { GraphicOptions } from './Graphic'; +import { TiledSprite, TiledSpriteOptions } from './TiledSprite'; /** * Specify sprite sheet spacing options, useful if your sprites are not tightly packed @@ -112,10 +113,10 @@ export class SpriteSheet { */ public getSprite(x: number, y: number, options?: GetSpriteOptions): Sprite { if (x >= this.columns || x < 0) { - throw Error(`No sprite exists in the SpriteSheet at (${x}, ${y}), x: ${x} should be between 0 and ${this.columns - 1}`); + throw Error(`No sprite exists in the SpriteSheet at (${x}, ${y}), x: ${x} should be between 0 and ${this.columns - 1} columns`); } if (y >= this.rows || y < 0) { - throw Error(`No sprite exists in the SpriteSheet at (${x}, ${y}), y: ${y} should be between 0 and ${this.rows - 1}`); + throw Error(`No sprite exists in the SpriteSheet at (${x}, ${y}), y: ${y} should be between 0 and ${this.rows - 1} rows`); } const spriteIndex = x + y * this.columns; const sprite = this.sprites[spriteIndex]; @@ -135,7 +136,43 @@ export class SpriteSheet { } return sprite; } - throw Error(`Invalid sprite coordinates (${x}, ${y}`); + throw Error(`Invalid sprite coordinates (${x}, ${y})`); + } + + /** + * Find a sprite by their x/y integer coordinates in the SpriteSheet and configures tiling to repeat by default, + * for example `getTiledSprite(0, 0)` is the {@apilink TiledSprite} in the top-left + * and `getTiledSprite(1, 0)` is the sprite one to the right. + * + * Example: + * + * ```typescript + * spriteSheet.getTiledSprite(1, 0, { + * width: game.screen.width, + * height: 200, + * wrapping: { + * x: ex.ImageWrapping.Repeat, + * y: ex.ImageWrapping.Clamp + * } + * }); + * ``` + * @param x + * @param y + * @param options + */ + public getTiledSprite(x: number, y: number, options?: Partial>): TiledSprite { + if (x >= this.columns || x < 0) { + throw Error(`No sprite exists in the SpriteSheet at (${x}, ${y}), x: ${x} should be between 0 and ${this.columns - 1} columns`); + } + if (y >= this.rows || y < 0) { + throw Error(`No sprite exists in the SpriteSheet at (${x}, ${y}), y: ${y} should be between 0 and ${this.rows - 1} rows`); + } + const spriteIndex = x + y * this.columns; + const sprite = this.sprites[spriteIndex]; + if (sprite) { + return TiledSprite.fromSprite(sprite, options); + } + throw Error(`Invalid sprite coordinates (${x}, ${y})`); } /** diff --git a/src/engine/Graphics/TiledSprite.ts b/src/engine/Graphics/TiledSprite.ts index 86445e597..406a407ac 100644 --- a/src/engine/Graphics/TiledSprite.ts +++ b/src/engine/Graphics/TiledSprite.ts @@ -1,5 +1,6 @@ import { Future } from '../Util/Future'; import { ImageFiltering } from './Filtering'; +import { GraphicOptions } from './Graphic'; import { ImageSource, ImageWrapConfiguration } from './ImageSource'; import { SourceView, Sprite } from './Sprite'; import { ImageWrapping } from './Wrapping'; @@ -31,12 +32,19 @@ export interface TiledSpriteOptions { export class TiledSprite extends Sprite { private _ready = new Future(); public ready = this._ready.promise; - private _options: TiledSpriteOptions; - constructor(options: TiledSpriteOptions) { + private _options: TiledSpriteOptions & GraphicOptions; + constructor(options: TiledSpriteOptions & GraphicOptions) { super({ image: options.image, sourceView: options.sourceView, - destSize: { width: options.width, height: options.height } + destSize: { width: options.width, height: options.height }, + flipHorizontal: options.flipHorizontal, + flipVertical: options.flipVertical, + rotation: options.rotation, + scale: options.scale, + opacity: options.opacity, + tint: options.tint, + origin: options.origin }); this._options = options; @@ -47,8 +55,9 @@ export class TiledSprite extends Sprite { } } - public static fromSprite(sprite: Sprite, options?: Partial>): TiledSprite { + public static fromSprite(sprite: Sprite, options?: Partial>): TiledSprite { return new TiledSprite({ + sourceView: { ...sprite.sourceView }, width: sprite.width, height: sprite.height, ...options, diff --git a/src/spec/SpriteSheetSpec.ts b/src/spec/SpriteSheetSpec.ts index 4a576c16f..3aa9fbcb2 100644 --- a/src/spec/SpriteSheetSpec.ts +++ b/src/spec/SpriteSheetSpec.ts @@ -122,13 +122,17 @@ describe('A SpriteSheet for Graphics', () => { expect(ss.getSprite(0, 0)).withContext('top left sprite').toEqual(ss.sprites[0]); expect(ss.getSprite(13, 3)).withContext('bottom right sprite').not.toBeNull(); - expect(() => ss.getSprite(13, 4)).toThrowError('No sprite exists in the SpriteSheet at (13, 4), y: 4 should be between 0 and 3'); + expect(() => ss.getSprite(13, 4)).toThrowError('No sprite exists in the SpriteSheet at (13, 4), y: 4 should be between 0 and 3 rows'); - expect(() => ss.getSprite(14, 3)).toThrowError('No sprite exists in the SpriteSheet at (14, 3), x: 14 should be between 0 and 13'); + expect(() => ss.getSprite(14, 3)).toThrowError( + 'No sprite exists in the SpriteSheet at (14, 3), x: 14 should be between 0 and 13 columns' + ); - expect(() => ss.getSprite(-1, 3)).toThrowError('No sprite exists in the SpriteSheet at (-1, 3), x: -1 should be between 0 and 13'); + expect(() => ss.getSprite(-1, 3)).toThrowError( + 'No sprite exists in the SpriteSheet at (-1, 3), x: -1 should be between 0 and 13 columns' + ); - expect(() => ss.getSprite(1, -1)).toThrowError('No sprite exists in the SpriteSheet at (1, -1), y: -1 should be between 0 and 3'); + expect(() => ss.getSprite(1, -1)).toThrowError('No sprite exists in the SpriteSheet at (1, -1), y: -1 should be between 0 and 3 rows'); }); it('can retrieve a sprite by x,y, with options', async () => { diff --git a/src/spec/TiledSpriteSpec.ts b/src/spec/TiledSpriteSpec.ts index 3be72b8b8..707ad20b7 100644 --- a/src/spec/TiledSpriteSpec.ts +++ b/src/spec/TiledSpriteSpec.ts @@ -91,4 +91,35 @@ describe('A TiledSprite', () => { await expectAsync(canvasElement).toEqualImage('src/spec/images/TiledSpriteSpec/tiled.png'); }); + + it('can grab a tiled sprite from a sprite sheet', async () => { + const cardsImage = new ex.ImageSource('src/spec/images/TiledAnimationSpec/kenny-cards.png'); + await cardsImage.load(); + const cardSpriteSheet = ex.SpriteSheet.fromImageSource({ + image: cardsImage, + grid: { + rows: 4, + columns: 14, + spriteWidth: 42, + spriteHeight: 60 + }, + spacing: { + originOffset: { x: 11, y: 2 }, + margin: { x: 23, y: 5 } + } + }); + + const sut = cardSpriteSheet.getTiledSprite(0, 0, { + width: 300, + height: 300, + scale: ex.vec(2, 2) + }); + await sut.ready; + + ctx.clear(); + sut.draw(ctx, 0, 0); + ctx.flush(); + + await expectAsync(canvasElement).toEqualImage('src/spec/images/TiledSpriteSpec/from-spritesheet.png'); + }); }); diff --git a/src/spec/images/TiledSpriteSpec/from-spritesheet.png b/src/spec/images/TiledSpriteSpec/from-spritesheet.png new file mode 100644 index 0000000000000000000000000000000000000000..0f66d560b6c4db175bdded6f0394e94d855ddb85 GIT binary patch literal 22905 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 cbbtSsT+7f4o8mkJc$PAQr>mdKI;Vst02