From 16922b969cbfbe1ee63009d0dfd0b75d57075093 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Fri, 29 Jan 2021 21:41:10 -0600 Subject: [PATCH] feat: New Tiled Support Update (#1755) Related issues: * TileMap PR: https://github.com/excaliburjs/excalibur-tiled/pull/162 * TMX support: https://github.com/excaliburjs/excalibur-tiled/issues/4 * Full TSX external TileMap support: https://github.com/excaliburjs/excalibur-tiled/issues/23, * Object model to easily work with layers: https://github.com/excaliburjs/excalibur-tiled/issues/108 ## Changes: * Remove `TileSprite` in favor of `Sprite` * Fix some bugs with scaling/rotation of Sprites - Add `fromAnchor` to draw around anchor or from the left corner - Anchor still defines where rotations/scaling is applied to the sprite --- CHANGELOG.md | 6 +- sandbox/src/game.ts | 5 +- sandbox/tests/tilemap/tilemap.ts | 6 +- src/engine/Actor.ts | 4 +- src/engine/Drawing/Sprite.ts | 56 ++++++-- src/engine/Drawing/SpriteSheet.ts | 16 ++- src/engine/TileMap.ts | 134 ++++++++++++------ src/engine/index.ts | 2 +- src/spec/SpriteSpec.ts | 72 +++++++++- src/spec/TileMapSpec.ts | 4 +- .../images/SpriteFontSpec/ExpectedFont.png | Bin 5369 -> 5619 bytes src/spec/images/SpriteFontSpec/TextShadow.png | Bin 7186 -> 7755 bytes .../images/SpriteSpec/icon-horiz-flip.png | Bin 0 -> 504 bytes src/spec/images/SpriteSpec/icon-vert-flip.png | Bin 0 -> 506 bytes 14 files changed, 225 insertions(+), 80 deletions(-) create mode 100644 src/spec/images/SpriteSpec/icon-horiz-flip.png create mode 100644 src/spec/images/SpriteSpec/icon-vert-flip.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 7751000c..3fdd55e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Breaking Changes +- TileMap no longer needs registered SpriteSheets, `Sprite`'s can be added directly to `Cell`'s with `addSprite` + - The confusing `TileSprite` type is removed (Related to TileMap plugin updates https://github.com/excaliburjs/excalibur-tiled/issues/4, https://github.com/excaliburjs/excalibur-tiled/issues/23, https://github.com/excaliburjs/excalibur-tiled/issues/108) - Directly changing debug drawing by `engine.isDebug = value` has been replaced by `engine.showDebug(value)` and `engine.toggleDebug()` ([#1655](https://github.com/excaliburjs/Excalibur/issues/1655)) - `UIActor` Class instances need to be replaced to `ScreenElement` (This Class it's marked as Obsolete) ([#1656](https://github.com/excaliburjs/Excalibur/issues/1656)) - Switch to browser based promise, the Excalibur implementation `ex.Promise` is marked deprecated ([#994](https://github.com/excaliburjs/Excalibur/issues/994)) @@ -29,7 +31,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Deprecated - Removed UIActor Stub in favor of ScreenElement ([#1656](https://github.com/excaliburjs/Excalibur/issues/1656)) -- `ex.SortedList` as deprecated +- `ex.SortedList` as deprecated - `ex.Promise` is marked deprecated ([#994](https://github.com/excaliburjs/Excalibur/issues/994)) - `DisplayMode.Position` CSS can accomplish this task better than Excalibur ([#1733](https://github.com/excaliburjs/Excalibur/issues/1733)) @@ -37,6 +39,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +- Fixed issues with `Sprite` not rotating/scaling correctly around the anchor (Related to TileMap plugin updates https://github.com/excaliburjs/excalibur-tiled/issues/4, https://github.com/excaliburjs/excalibur-tiled/issues/23, https://github.com/excaliburjs/excalibur-tiled/issues/108) + - Optionally specify whether to draw around the anchor or not `drawAroundAnchor` - Fixed in the browser "FullScreen" api, coordinates are now correctly mapped from page space to world space ([#1734](https://github.com/excaliburjs/Excalibur/issues/1734)) - Fix audio decoding bug introduced in https://github.com/excaliburjs/Excalibur/pull/1707 - Fixed issue with promise resolve on double resource load ([#1434](https://github.com/excaliburjs/Excalibur/issues/1434)) diff --git a/sandbox/src/game.ts b/sandbox/src/game.ts index 3e543ba0..98ff99ab 100644 --- a/sandbox/src/game.ts +++ b/sandbox/src/game.ts @@ -152,10 +152,9 @@ var tileBlockWidth = 64, // create a collision map //var tileMap = new ex.TileMap(100, 300, tileBlockWidth, tileBlockHeight, 4, 500); var tileMap = new ex.TileMap({ x: 100, y: 300, cellWidth: tileBlockWidth, cellHeight: tileBlockHeight, rows: 4, cols: 500 }); -tileMap.registerSpriteSheet('default', spriteTiles); tileMap.data.forEach(function(cell: ex.Cell) { cell.solid = true; - cell.pushSprite(new ex.TileSprite('default', 0)); + cell.addSprite(spriteTiles.sprites[0]); }); game.add(tileMap); @@ -580,7 +579,7 @@ game.input.pointers.primary.on('down', (evt?: ex.Input.PointerEvent) => { c.sprites.pop(); } else { c.solid = true; - c.pushSprite(new ex.TileSprite('default', 0)); + c.addSprite(spriteTiles.sprites[0]); } } }); diff --git a/sandbox/tests/tilemap/tilemap.ts b/sandbox/tests/tilemap/tilemap.ts index 5dcaef56..c3c47ce1 100644 --- a/sandbox/tests/tilemap/tilemap.ts +++ b/sandbox/tests/tilemap/tilemap.ts @@ -28,12 +28,10 @@ var tm = new ex.TileMap({ rows: 40 }); -var tilesprite = new ex.TileSprite('root', 0); - -tm.registerSpriteSheet('root', ss); +var tilesprite = ss.sprites[0]; for (var i = 0; i < tm.rows * tm.cols; i++) { - tm.getCellByIndex(i).pushSprite(tilesprite); + tm.getCellByIndex(i).addSprite(tilesprite); } game.add(tm); diff --git a/src/engine/Actor.ts b/src/engine/Actor.ts index 2618a664..2324089e 100644 --- a/src/engine/Actor.ts +++ b/src/engine/Actor.ts @@ -1219,8 +1219,8 @@ export class ActorImpl if (this.currentDrawing) { const drawing = this.currentDrawing; // See https://github.com/excaliburjs/Excalibur/pull/619 for discussion on this formula - const offsetX = (this._width - drawing.width * drawing.scale.x) * this.anchor.x; - const offsetY = (this._height - drawing.height * drawing.scale.y) * this.anchor.y; + const offsetX = (this._width - drawing.drawWidth) * this.anchor.x; + const offsetY = (this._height - drawing.drawHeight) * this.anchor.y; this.currentDrawing.draw({ ctx, x: offsetX, y: offsetY, opacity: this.opacity }); } else { diff --git a/src/engine/Drawing/Sprite.ts b/src/engine/Drawing/Sprite.ts index 4de15857..122eb572 100644 --- a/src/engine/Drawing/Sprite.ts +++ b/src/engine/Drawing/Sprite.ts @@ -18,17 +18,22 @@ export class SpriteImpl implements Drawable { public y: number = 0; public get drawWidth(): number { - return this.width * this.scale.x; + return Math.abs(this.width * this.scale.x); } public get drawHeight(): number { - return this.height * this.scale.y; + return Math.abs(this.height * this.scale.y); } public rotation: number = 0.0; - public anchor: Vector = new Vector(0.0, 0.0); + public anchor: Vector = Vector.Half; public offset: Vector = Vector.Zero; public scale: Vector = Vector.One; + /** + * Default: false, should the sprite be drawn around the anchor or from the top left. + * Sprite rotations/scaling still happen around the anchor regardless of this setting. + */ + public drawAroundAnchor = false; public logger: Logger = Logger.getInstance(); @@ -265,7 +270,6 @@ export class SpriteImpl implements Drawable { } private _applyEffects() { - this._flushTexture(); if (this.effects.length > 0) { @@ -337,8 +341,8 @@ export class SpriteImpl implements Drawable { const { ctx, x, y, rotation, drawWidth, drawHeight, anchor, offset, opacity, flipHorizontal, flipVertical } = { ...options, rotation: options.rotation ?? this.rotation, - drawWidth: options.drawWidth ?? this.drawWidth, - drawHeight: options.drawHeight ?? this.drawHeight, + drawWidth: options.drawWidth ?? this.width, + drawHeight: options.drawHeight ?? this.height, flipHorizontal: options.flipHorizontal ?? this.flipHorizontal, flipVertical: options.flipVertical ?? this.flipVertical, anchor: options.anchor ?? this.anchor, @@ -349,13 +353,31 @@ export class SpriteImpl implements Drawable { if (this._dirtyEffect) { this._applyEffects(); } - // calculating current dimensions + const anchorX = drawWidth * anchor.x + offset.x; + const anchorY = drawHeight * anchor.y + offset.y; + const scaleDirX = this.scale.x > 0 ? 1 : -1; + const scaleDirY = this.scale.y > 0 ? 1 : -1; + ctx.save(); - const xpoint = drawWidth * anchor.x + offset.x; - const ypoint = drawHeight * anchor.y + offset.y; + // Move the draw point of origin ctx.translate(x, y); + + // Rotate and scale around anchor point + // This requires a bit of explaination, scale coordinates first positive flipping or rotating + ctx.scale(Math.abs(this.scale.x), Math.abs(this.scale.y)); + + if (this.drawAroundAnchor) { + // In the case where you want the anchor to match with the point of draw + // Otherwise sprites are always drawn from top-left + ctx.translate(-anchorX, -anchorY); + } + + ctx.translate(anchorX, anchorY); ctx.rotate(rotation); + // This is for handling direction changes 1 or -1, that way we don't have mismatched translates() + ctx.scale(scaleDirX, scaleDirY); + ctx.translate(-anchorX, -anchorY); if (flipHorizontal) { ctx.translate(drawWidth, 0); @@ -366,12 +388,21 @@ export class SpriteImpl implements Drawable { ctx.translate(0, drawHeight); ctx.scale(1, -1); } - const oldAlpha = ctx.globalAlpha; ctx.globalAlpha = opacity ?? 1; - ctx.drawImage(this._spriteCanvas, 0, 0, this.width, this.height, -xpoint, -ypoint, drawWidth, drawHeight); + // Context is already rotated and scaled + ctx.drawImage( + this._spriteCanvas, + 0, + 0, + this.width, + this.height, // source + 0, + 0, + this.width, + this.height + ); // dest ctx.globalAlpha = oldAlpha; - ctx.restore(); } @@ -380,6 +411,7 @@ export class SpriteImpl implements Drawable { */ public clone(): SpriteImpl { const result = new Sprite(this._texture, this.x, this.y, this.width, this.height); + result.anchor = this.anchor.clone(); result.scale = this.scale.clone(); result.rotation = this.rotation; result.flipHorizontal = this.flipHorizontal; diff --git a/src/engine/Drawing/SpriteSheet.ts b/src/engine/Drawing/SpriteSheet.ts index 2527a24f..ff8eaafe 100644 --- a/src/engine/Drawing/SpriteSheet.ts +++ b/src/engine/Drawing/SpriteSheet.ts @@ -110,7 +110,7 @@ export class SpriteSheetImpl { return this.sprites[index]; }); - images = images.map(function(i) { + images = images.map(function (i) { return i.clone(); }); return new Animation(engine, images, speed); @@ -127,7 +127,7 @@ export class SpriteSheetImpl { */ public getAnimationBetween(engine: Engine, beginIndex: number, endIndex: number, speed: number) { let images = this.sprites.slice(beginIndex, endIndex); - images = images.map(function(i) { + images = images.map(function (i) { return i.clone(); }); return new Animation(engine, images, speed); @@ -140,7 +140,7 @@ export class SpriteSheetImpl { * @param speed The number in milliseconds to display each frame the animation */ public getAnimationForAll(engine: Engine, speed: number) { - const sprites = this.sprites.map(function(i) { + const sprites = this.sprites.map(function (i) { return i.clone(); }); return new Animation(engine, sprites, speed); @@ -254,8 +254,8 @@ export class SpriteFontImpl extends SpriteSheet { spacing?: number ) { super( - imageOrConfig instanceof Texture ? - { + imageOrConfig instanceof Texture + ? { image: imageOrConfig, spWidth: spWidth, spHeight: spHeight, @@ -346,12 +346,12 @@ export class SpriteFontImpl extends SpriteSheet { const sprite = this.sprites[0]; // find the current height fo the text in pixels - const height = sprite.drawHeight; + const height = sprite.height; // calculate appropriate scale for font size const scale = options.fontSize / height; - const length = text.length * sprite.drawWidth * scale + text.length * options.letterSpacing; + const length = text.length * sprite.width * scale + text.length * options.letterSpacing; let currX = x; if (options.textAlign === TextAlign.Left || options.textAlign === TextAlign.Start) { @@ -383,12 +383,14 @@ export class SpriteFontImpl extends SpriteSheet { try { // if text shadow if (this._textShadowOn) { + this._textShadowSprites[character].drawAroundAnchor = false; this._textShadowSprites[character].scale.x = scale; this._textShadowSprites[character].scale.y = scale; this._textShadowSprites[character].draw(ctx, currX + this._shadowOffsetX, currY + this._shadowOffsetY); } const charSprite = this._sprites[character]; + charSprite.drawAroundAnchor = false; charSprite.scale.x = scale; charSprite.scale.y = scale; charSprite.draw(ctx, currX, currY); diff --git a/src/engine/TileMap.ts b/src/engine/TileMap.ts index 7ca6b208..6d8a05c4 100644 --- a/src/engine/TileMap.ts +++ b/src/engine/TileMap.ts @@ -10,6 +10,9 @@ import { Configurable } from './Configurable'; import { Entity } from './EntityComponentSystem/Entity'; import { CanvasDrawComponent } from './Drawing/CanvasDrawComponent'; import { TransformComponent } from './EntityComponentSystem/Components/TransformComponent'; +import { Sprite } from './Drawing/Index'; +import { removeItemFromArray } from './Util/Util'; +import { obsolete } from './Util/Decorators'; /** * @hidden @@ -24,18 +27,62 @@ export class TileMapImpl extends Entity this.draw(ctx, delta))); } + + /** + * + * @param key + * @param spriteSheet + * @deprecated No longer used, will be removed in v0.26.0 + */ + @obsolete({message: 'No longer used, will be removed in v0.26.0'}) public registerSpriteSheet(key: string, spriteSheet: SpriteSheet) { this._spriteSheets[key] = spriteSheet; } @@ -206,28 +261,18 @@ export class TileMapImpl extends Entity { - return s.spriteId > -1; - }); - - for (csi = 0, cslen = cs.length; csi < cslen; csi++) { - const ss = this._spriteSheets[cs[csi].spriteSheetKey]; + sprites = this.getCell(x, y).sprites; + for (spriteIndex = 0, cslen = sprites.length; spriteIndex < cslen; spriteIndex++) { // draw sprite, warning if sprite doesn't exist - if (ss) { - const sprite = ss.getSprite(cs[csi].spriteId); - if (sprite) { - sprite.draw(ctx, x * this.cellWidth, y * this.cellHeight); - } else { - this.logger.warn('Sprite does not exist for id', cs[csi].spriteId, 'in sprite sheet', cs[csi].spriteSheetKey, sprite, ss); - } - } else { - this.logger.warn('Sprite sheet', cs[csi].spriteSheetKey, 'does not exist', ss); + const sprite = sprites[spriteIndex]; + if (sprite) { + sprite.draw(ctx, x * this.cellWidth, y * this.cellHeight); } } } @@ -302,17 +347,6 @@ export class TileMap extends Configurable(TileMapImpl) { } } -/** - * Tile sprites are used to render a specific sprite from a [[TileMap]]'s spritesheet(s) - */ -export class TileSprite { - /** - * @param spriteSheetKey The key of the spritesheet to use - * @param spriteId The index of the sprite in the [[SpriteSheet]] - */ - constructor(public spriteSheetKey: string, public spriteId: number) {} -} - /** * @hidden */ @@ -324,7 +358,7 @@ export class CellImpl { public height: number; public index: number; public solid: boolean = false; - public sprites: TileSprite[] = []; + public sprites: Sprite[] = []; /** * @param xOrConfig Gets or sets x coordinate of the cell in world coordinates or cell option bag @@ -342,7 +376,7 @@ export class CellImpl { height: number, index: number, solid: boolean = false, - sprites: TileSprite[] = [] + sprites: Sprite[] = [] ) { if (xOrConfig && typeof xOrConfig === 'object') { const config = xOrConfig; @@ -373,19 +407,27 @@ export class CellImpl { } /** - * Add another [[TileSprite]] to this cell + * Add another [[Sprite]] to this cell + * @deprecated Use addSprite, will be removed in v0.26.0 + */ + @obsolete({message: 'Will be removed in v0.26.0', alternateMethod: 'addSprite'}) + public pushSprite(sprite: Sprite) { + this.sprites.push(sprite); + } + + /** + * Add another [[Sprite]] to this TileMap cell + * @param sprite */ - public pushSprite(tileSprite: TileSprite) { - this.sprites.push(tileSprite); + public addSprite(sprite: Sprite) { + this.sprites.push(sprite); } + /** - * Remove an instance of [[TileSprite]] from this cell + * Remove an instance of [[Sprite]] from this cell */ - public removeSprite(tileSprite: TileSprite) { - let index = -1; - if ((index = this.sprites.indexOf(tileSprite)) > -1) { - this.sprites.splice(index, 1); - } + public removeSprite(sprite: Sprite) { + removeItemFromArray(sprite, this.sprites); } /** * Clear all sprites from this cell @@ -402,7 +444,7 @@ export interface CellArgs extends Partial { height: number; index: number; solid?: boolean; - sprites?: TileSprite[]; + sprites?: Sprite[]; } /** @@ -417,7 +459,7 @@ export interface CellArgs extends Partial { */ export class Cell extends Configurable(CellImpl) { constructor(config: CellArgs); - constructor(x: number, y: number, width: number, height: number, index: number, solid?: boolean, sprites?: TileSprite[]); + constructor(x: number, y: number, width: number, height: number, index: number, solid?: boolean, sprites?: Sprite[]); constructor( xOrConfig: number | CellArgs, y?: number, @@ -425,7 +467,7 @@ export class Cell extends Configurable(CellImpl) { height?: number, index?: number, solid?: boolean, - sprites?: TileSprite[] + sprites?: Sprite[] ) { super(xOrConfig, y, width, height, index, solid, sprites); } diff --git a/src/engine/index.ts b/src/engine/index.ts index 1d4221fd..440ae072 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -26,7 +26,7 @@ export * from './Loader'; export { Particle, ParticleEmitter, ParticleArgs, ParticleEmitterArgs, EmitterType } from './Particles'; export * from './Physics'; export * from './Scene'; -export { TileMap, Cell, TileMapArgs, CellArgs, TileSprite } from './TileMap'; +export { TileMap, Cell, TileMapArgs, CellArgs } from './TileMap'; export * from './Timer'; export * from './Trigger'; export * from './ScreenElement'; diff --git a/src/spec/SpriteSpec.ts b/src/spec/SpriteSpec.ts index 064ec385..20d5f07a 100644 --- a/src/spec/SpriteSpec.ts +++ b/src/spec/SpriteSpec.ts @@ -79,7 +79,8 @@ describe('A sprite', () => { width: 62, height: 64, scale: new ex.Vector(2, 2), - anchor: new ex.Vector(0.5, 0.5) + anchor: new ex.Vector(0.5, 0.5), + drawAroundAnchor: true }); sprite.draw(engine.ctx, 62 / 2, 64 / 2); @@ -100,7 +101,8 @@ describe('A sprite', () => { width: 62, height: 64, rotation: Math.PI / 4, - anchor: new ex.Vector(0.5, 0.5) + anchor: new ex.Vector(0.5, 0.5), + drawAroundAnchor: true }); sprite.draw(engine.ctx, 62 / 2, 64 / 2); @@ -112,6 +114,72 @@ describe('A sprite', () => { }); }); + it('should rotate around center and draw drawAroundAnchor top-left', (done) => { + texture.load().then(() => { + const sprite = new ex.Sprite({ + image: texture, + x: 0, + y: 0, + width: 62, + height: 64, + rotation: Math.PI / 4, + anchor: new ex.Vector(0.5, 0.5), + drawAroundAnchor: false + }); + + sprite.draw(engine.ctx, 0, 0); + + ensureImagesLoaded(engine.canvas, 'src/spec/images/SpriteSpec/iconrotate.png').then(([canvas, image]) => { + expect(canvas).toEqualImage(image); + done(); + }); + }); + }); + + it('should flipHorizontally', (done) => { + texture.load().then(() => { + const sprite = new ex.Sprite({ + image: texture, + x: 0, + y: 0, + width: 62, + height: 64, + anchor: new ex.Vector(0.5, 0.5) + }); + + sprite.flipHorizontal = true; + + sprite.draw(engine.ctx, 0, 0); + + ensureImagesLoaded(engine.canvas, 'src/spec/images/SpriteSpec/icon-horiz-flip.png').then(([canvas, image]) => { + expect(canvas).toEqualImage(image); + done(); + }); + }); + }); + + it('should flipVertically', (done) => { + texture.load().then(() => { + const sprite = new ex.Sprite({ + image: texture, + x: 0, + y: 0, + width: 62, + height: 64, + anchor: new ex.Vector(0.5, 0.5) + }); + + sprite.flipVertical = true; + + sprite.draw(engine.ctx, 0, 0); + + ensureImagesLoaded(engine.canvas, 'src/spec/images/SpriteSpec/icon-vert-flip.png').then(([canvas, image]) => { + expect(canvas).toEqualImage(image); + done(); + }); + }); + }); + it('can be drawn with opacity', (done) => { texture.load().then(() => { const sprite = new ex.Sprite({ diff --git a/src/spec/TileMapSpec.ts b/src/spec/TileMapSpec.ts index 75a2816e..700948e3 100644 --- a/src/spec/TileMapSpec.ts +++ b/src/spec/TileMapSpec.ts @@ -64,7 +64,7 @@ describe('A TileMap', () => { tm.registerSpriteSheet('default', spriteTiles); tm.data.forEach(function (cell: ex.Cell) { cell.solid = true; - cell.pushSprite(new ex.TileSprite('default', 0)); + cell.pushSprite(spriteTiles.sprites[0]); }); drawWithTransform(engine.ctx, tm, 100); @@ -90,7 +90,7 @@ describe('A TileMap', () => { tm.registerSpriteSheet('default', spriteTiles); tm.data.forEach(function (cell: ex.Cell) { cell.solid = true; - cell.pushSprite(new ex.TileSprite('default', 0)); + cell.pushSprite(spriteTiles.sprites[0]); }); tm.update(engine, 100); diff --git a/src/spec/images/SpriteFontSpec/ExpectedFont.png b/src/spec/images/SpriteFontSpec/ExpectedFont.png index 0dbec8e57cf06280d07e234be0559e0ed163fa68..185e32acd760dffcbae0f3050bb6e4b66176e799 100644 GIT binary patch literal 5619 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 literal 7186 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 DWS&#f diff --git a/src/spec/images/SpriteSpec/icon-horiz-flip.png b/src/spec/images/SpriteSpec/icon-horiz-flip.png new file mode 100644 index 0000000000000000000000000000000000000000..281195d7f0ec42266a8970de1f6e4f14f2fb60f4 GIT binary patch literal 504 zcmeAS@N?(olHy`uVBq!ia0y~yV6bCgU~u4IV_;x#dU@n40|VoJPZ!6K4Cl8q*83d} z5IJVvJ-*>;*XVeE zW1Fthx$BJck7Y{>=Ij#WeO1JN*`wj=N1;Qa3;B=d?%VU{N`}MzQ-!^=Wm}j_r}G8m zcFg?l6zlv<@lNxH`_YSBYwy3fALo1Hd(-UprN%qYnctYOF`G$YPE5mNGmD_JegXeq zpQ%5y{-(0NpZ|@opI=+=-loWKNLHgz?{3-V=Eqk)B?R0z`+WK9U4>Nk*@Yb6ZO$J2 zDEFY*G4@~mPcAKU{mA#qA$H07#dl2pE9^;+Iav70VZ#dVEuT++K4@ZNw|B$W#Yd|) z)P37#;nec;RxQETA5`$$Wd)d@E!?Sh{`&Xy<8Nk1-`;j^=KtwE&K9K(d1VE2 ztQLrs&Y!rWvS0IaZ1G%Wzjq4uC5;E9nS2FO8;;l5f_NRY9Lz4 Qz`(%Z>FVdQ&MBb@04ikhMgRZ+ literal 0 HcmV?d00001 diff --git a/src/spec/images/SpriteSpec/icon-vert-flip.png b/src/spec/images/SpriteSpec/icon-vert-flip.png new file mode 100644 index 0000000000000000000000000000000000000000..842d7b336ddbe518ece848d1ee20e924c3593ddf GIT binary patch literal 506 zcmeAS@N?(olHy`uVBq!ia0y~yV6bCgU~u4IV_;x#dU@n40|VnhPZ!6K4Cl8I`u;}@ zMD_=(i^(`HT*fKoT)@q?mF1v1!%qGNB@erUHc2N%*!meZF8C%Wz+oaLbZNrt)1|f7 zU&qGZzJKRh_V)Yxub0m~Z?yN>%t!MWEAmdQ5Bc-3;(hwejzt~1lK4rKVXwxxI;-za&_>+%&T_xmUGU0`Fz{`gU@CDlvJ4mqRas?f)m$V zy&n3bws7&%#X8<^>tx*D?#q-~ej>N1;!u@hgYjD4qoRQ)qvpQ5%%1*z^2f;zEz^6L zcU+R}lID@`y_&15{ntMB`qr{%VWCrZoY`!|_n<9Kwo5x{($-(QY&W?m%`xJ8;KR0H zhmzN!Z`*`d2PbmdKI;Vst01B1hUjP6A literal 0 HcmV?d00001 -- 2.51.2