diff --git a/CHANGELOG.md b/CHANGELOG.md index 522f3bd3..33d504f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ are returned }); ``` +- Added word-wrap support for `ex.Text` using the optional parameter `maxWidth` - Added the emitted particle transform style as part of `ex.ParticleEmitter({particleTransform: ex.ParticleTransform.Global})`, [[ParticleTransform.Global]] is the default and emits particles as if they were world space objects, useful for most effects. If set to [[ParticleTransform.Local]] particles are children of the emitter and move relative to the emitter as they would in a parent/child actor relationship. - Added `wasButtonReleased` and `wasButtonPressed` methods to [[ex.Input.Gamepad]] - Added `clone()` method to `ex.SpriteSheet` diff --git a/src/engine/Graphics/Font.ts b/src/engine/Graphics/Font.ts index 543290f7..61c15cdb 100644 --- a/src/engine/Graphics/Font.ts +++ b/src/engine/Graphics/Font.ts @@ -8,7 +8,6 @@ import { RasterOptions } from './Raster'; import { ImageFiltering } from './Filtering'; import { FontTextInstance } from './FontTextInstance'; import { FontCache } from './FontCache'; - /** * Represents a system or web font in Excalibur * @@ -112,12 +111,10 @@ export class Font extends Graphic implements FontRenderer { return this._textBounds; } - protected _drawImage(_ex: ExcaliburGraphicsContext, _x: number, _y: number) { // TODO weird vestigial drawimage } - protected _rotate(ex: ExcaliburGraphicsContext) { // TODO this needs to change depending on the bounding box... const origin = this.origin ?? this._textBounds.center; @@ -140,8 +137,8 @@ export class Font extends Graphic implements FontRenderer { private _textMeasurement = new FontTextInstance(this, '', Color.Black); - public measureTextWithoutCache(text: string) { - return this._textMeasurement.measureText(text); + public measureTextWithoutCache(text: string, maxWidth?: number) { + return this._textMeasurement.measureText(text, maxWidth); } /** @@ -151,22 +148,22 @@ export class Font extends Graphic implements FontRenderer { * @param text * @returns BoundingBox */ - public measureText(text: string): BoundingBox { - return FontCache.measureText(text, this); + public measureText(text: string, maxWidth?: number): BoundingBox { + return FontCache.measureText(text, this, maxWidth); } protected _postDraw(ex: ExcaliburGraphicsContext): void { ex.restore(); } - public render(ex: ExcaliburGraphicsContext, text: string, colorOverride: Color, x: number, y: number) { + public render(ex: ExcaliburGraphicsContext, text: string, colorOverride: Color, x: number, y: number, maxWidth?: number) { const textInstance = FontCache.getTextInstance(text, this, colorOverride); // Apply affine transformations this._textBounds = textInstance.dimensions; this._preDraw(ex, x, y); - textInstance.render(ex, x, y); + textInstance.render(ex, x, y, maxWidth); this._postDraw(ex); } diff --git a/src/engine/Graphics/FontCache.ts b/src/engine/Graphics/FontCache.ts index aa85ed1a..fd7c466e 100644 --- a/src/engine/Graphics/FontCache.ts +++ b/src/engine/Graphics/FontCache.ts @@ -10,13 +10,13 @@ export class FontCache { private static _TEXT_CACHE = new Map(); private static _MEASURE_CACHE = new Map(); - static measureText(text: string, font: Font) { + static measureText(text: string, font: Font, maxWidth?: number) { const hash = FontTextInstance.getHashCode(font, text); if (FontCache._MEASURE_CACHE.has(hash)) { return FontCache._MEASURE_CACHE.get(hash); } FontCache._LOGGER.debug('Font text measurement cache miss'); - const measurement = font.measureTextWithoutCache(text); + const measurement = font.measureTextWithoutCache(text, maxWidth); FontCache._MEASURE_CACHE.set(hash, measurement); return measurement; } @@ -25,7 +25,7 @@ export class FontCache { const hash = FontTextInstance.getHashCode(font, text, color); let textInstance = FontCache._TEXT_CACHE.get(hash); if (!textInstance) { - textInstance = new FontTextInstance(font, text, color); + textInstance = new FontTextInstance(font, text, color); FontCache._TEXT_CACHE.set(hash, textInstance); FontCache._LOGGER.debug('Font text instance cache miss'); } @@ -51,7 +51,7 @@ export class FontCache { } } // Deferred removal of text instances - deferred.forEach(t => { + deferred.forEach((t) => { FontCache._TEXT_USAGE.delete(t); }); @@ -72,7 +72,6 @@ export class FontCache { this._MEASURE_CACHE = newTextMeasurementCache; } - public static get cacheSize() { return FontCache._TEXT_USAGE.size; } @@ -88,5 +87,4 @@ export class FontCache { FontCache._TEXT_CACHE.clear(); FontCache._MEASURE_CACHE.clear(); } - -} \ No newline at end of file +} diff --git a/src/engine/Graphics/FontTextInstance.ts b/src/engine/Graphics/FontTextInstance.ts index a8a61f42..e0359c4b 100644 --- a/src/engine/Graphics/FontTextInstance.ts +++ b/src/engine/Graphics/FontTextInstance.ts @@ -8,11 +8,12 @@ import { Font } from './Font'; export class FontTextInstance { public canvas: HTMLCanvasElement; public ctx: CanvasRenderingContext2D; - private _textFragments: {x: number, y: number, canvas: HTMLCanvasElement}[] = []; + private _textFragments: { x: number; y: number; canvas: HTMLCanvasElement }[] = []; public dimensions: BoundingBox; - public disposed: boolean = false;; + public disposed: boolean = false; private _lastHashCode: string; - constructor(public readonly font: Font, public readonly text: string, public readonly color: Color) { + + constructor(public readonly font: Font, public readonly text: string, public readonly color: Color, public readonly maxWidth?: number) { this.canvas = document.createElement('canvas'); this.ctx = this.canvas.getContext('2d'); this.dimensions = this.measureText(text); @@ -20,12 +21,17 @@ export class FontTextInstance { this._lastHashCode = this.getHashCode(); } - measureText(text: string): BoundingBox { + measureText(text: string, maxWidth?: number): BoundingBox { if (this.disposed) { throw Error('Accessing disposed text instance! ' + this.text); } + let lines = null; + if (maxWidth != null) { + lines = this._getLinesFromText(text, maxWidth); + } else { + lines = text.split('\n'); + } - const lines = text.split('\n'); const maxWidthLine = lines.reduce((a, b) => { return a.length > b.length ? a : b; }); @@ -51,7 +57,6 @@ export class FontTextInstance { } private _setDimension(textBounds: BoundingBox, bitmap: CanvasRenderingContext2D) { - // Changing the width and height clears the context properties // We double the bitmap width to account for all possible alignment // We scale by "quality" so we render text without jaggies @@ -60,7 +65,9 @@ export class FontTextInstance { } public static getHashCode(font: Font, text: string, color?: Color) { - const hash = text + '__hashcode__' + + const hash = + text + + '__hashcode__' + font.fontString + font.showDebug + font.textAlign + @@ -68,11 +75,11 @@ export class FontTextInstance { font.direction + JSON.stringify(font.shadow) + (font.padding.toString() + - font.smoothing.toString() + - font.lineWidth.toString() + - font.lineDash.toString() + - font.strokeColor?.toString() + - (color ? color.toString() : font.color.toString())); + font.smoothing.toString() + + font.lineWidth.toString() + + font.lineDash.toString() + + font.strokeColor?.toString() + + (color ? color.toString() : font.color.toString())); return hash; } @@ -90,6 +97,7 @@ export class FontTextInstance { } private _applyFont(ctx: CanvasRenderingContext2D) { + ctx.resetTransform(); ctx.translate(this.font.padding + ctx.canvas.width / 2, this.font.padding + ctx.canvas.height / 2); ctx.scale(this.font.quality, this.font.quality); ctx.textAlign = this.font.textAlign; @@ -105,8 +113,7 @@ export class FontTextInstance { } } - private _drawText(ctx: CanvasRenderingContext2D, text: string, lineHeight: number): void { - const lines = text.split('\n'); + private _drawText(ctx: CanvasRenderingContext2D, lines: string[], lineHeight: number): void { this._applyRasterProperties(ctx); this._applyFont(ctx); @@ -124,7 +131,7 @@ export class FontTextInstance { if (this.font.showDebug) { // Horizontal line /* istanbul ignore next */ - line(ctx, Color.Red, -ctx.canvas.width / 2, 0, ctx.canvas.width / 2, 0, 2); + line(ctx, Color.Green, -ctx.canvas.width / 2, 0, ctx.canvas.width / 2, 0, 2); // Vertical line /* istanbul ignore next */ line(ctx, Color.Red, 0, -ctx.canvas.height / 2, 0, ctx.canvas.height / 2, 2); @@ -132,7 +139,7 @@ export class FontTextInstance { } private _splitTextBitmap(bitmap: CanvasRenderingContext2D) { - const textImages: {x: number, y: number, canvas: HTMLCanvasElement}[] = []; + const textImages: { x: number; y: number; canvas: HTMLCanvasElement }[] = []; let currentX = 0; let currentY = 0; // 4k is the max for mobile devices @@ -151,7 +158,7 @@ export class FontTextInstance { // draw current slice to new bitmap in < 4k chunks ctx.drawImage(bitmap.canvas, currentX, currentY, width, height, 0, 0, width, height); - textImages.push({x: currentX, y: currentY, canvas}); + textImages.push({ x: currentX, y: currentY, canvas }); currentY += height; } currentX += width; @@ -164,7 +171,7 @@ export class FontTextInstance { this._dirty = true; } private _dirty = true; - public render(ex: ExcaliburGraphicsContext, x: number, y: number) { + public render(ex: ExcaliburGraphicsContext, x: number, y: number, maxWidth?: number) { if (this.disposed) { throw Error('Accessing disposed text instance! ' + this.text); } @@ -175,13 +182,13 @@ export class FontTextInstance { // Calculate image chunks if (this._dirty) { - this.dimensions = this.measureText(this.text); + this.dimensions = this.measureText(this.text, maxWidth); this._setDimension(this.dimensions, this.ctx); - const lines = this.text.split('\n'); + const lines = this._getLinesFromText(this.text, maxWidth); const lineHeight = this.dimensions.height / lines.length; // draws the text to the main bitmap - this._drawText(this.ctx, this.text, lineHeight); + this._drawText(this.ctx, lines, lineHeight); // clear any out old fragments for (const frag of this._textFragments) { @@ -224,4 +231,46 @@ export class FontTextInstance { } this._textFragments.length = 0; } -} \ No newline at end of file + + /** + * Return array of lines split based on the \n character, and the maxWidth? constraint + * @param text + * @param maxWidth + */ + private _chachedText: string; + private _chachedLines: string[]; + private _cachedRenderWidth: number; + private _getLinesFromText(text: string, maxWidth?: number) { + if (this._chachedText === text && this._cachedRenderWidth === maxWidth) { + return this._chachedLines; + } + + const lines = text.split('\n'); + + if (maxWidth == null) { + return lines; + } + + // If the current line goes past the maxWidth, append a new line without modifying the underlying text. + for (let i = 0; i < lines.length; i++) { + let line = lines[i]; + let newLine = ''; + if (this.measureText(line).width > maxWidth) { + while (this.measureText(line).width > maxWidth) { + newLine = line[line.length - 1] + newLine; + line = line.slice(0, -1); // Remove last character from line + } + + // Update the array with our new values + lines[i] = line; + lines[i + 1] = newLine; + } + } + + this._chachedText = text; + this._chachedLines = lines; + this._cachedRenderWidth = maxWidth; + + return lines; + } +} diff --git a/src/engine/Graphics/SpriteFont.ts b/src/engine/Graphics/SpriteFont.ts index c56a53d3..a9cc1cca 100644 --- a/src/engine/Graphics/SpriteFont.ts +++ b/src/engine/Graphics/SpriteFont.ts @@ -88,8 +88,8 @@ export class SpriteFont extends Graphic implements FontRenderer { return results; } - public measureText(text: string): BoundingBox { - const lines = text.split('\n'); + public measureText(text: string, maxWidth?: number): BoundingBox { + const lines = this._getLinesFromText(text, maxWidth); const maxWidthLine = lines.reduce((a, b) => { return a.length > b.length ? a : b; }); @@ -103,11 +103,11 @@ export class SpriteFont extends Graphic implements FontRenderer { return BoundingBox.fromDimension(width, height * lines.length, Vector.Zero); } - protected _drawImage(ex: ExcaliburGraphicsContext, x: number, y: number): void { + protected _drawImage(ex: ExcaliburGraphicsContext, x: number, y: number, maxWidth?: number): void { let xCursor = 0; let yCursor = 0; let height = 0; - const lines = this._text.split('\n'); + const lines = this._getLinesFromText(this._text, maxWidth); for (const line of lines) { for (const sprite of this._getCharacterSprites(line)) { // draw it in the right spot and increase the cursor by sprite width @@ -120,20 +120,24 @@ export class SpriteFont extends Graphic implements FontRenderer { } } - render(ex: ExcaliburGraphicsContext, text: string, _color: Color, x: number, y: number) { + render(ex: ExcaliburGraphicsContext, text: string, _color: Color, x: number, y: number, maxWidth?: number) { // SpriteFont doesn't support _color, yet... this._text = text; - const bounds = this.measureText(text); + const bounds = this.measureText(text, maxWidth); this.width = bounds.width; this.height = bounds.height; if (this.shadow) { ex.save(); ex.translate(this.shadow.offset.x, this.shadow.offset.y); - this.draw(ex, x, y); + this._preDraw(ex, x, y); + this._drawImage(ex, 0, 0, maxWidth); + this._postDraw(ex); ex.restore(); } - this.draw(ex, x, y); + this._preDraw(ex, x, y); + this._drawImage(ex, 0, 0, maxWidth); + this._postDraw(ex); } clone(): SpriteFont { @@ -143,4 +147,48 @@ export class SpriteFont extends Graphic implements FontRenderer { spacing: this.spacing }); } + + /** + * Return array of lines split based on the \n character, and the maxWidth? constraint + * @param text + * @param maxWidth + */ + private _cachedText: string; + private _cachedLines: string[]; + private _cachedRenderWidth: number; + private _getLinesFromText(text: string, maxWidth?: number) { + Logger.getInstance().info(this.spacing); + if (this._cachedText === text && this._cachedRenderWidth === maxWidth) { + return this._cachedLines; + } + + const lines = text.split('\n'); + + if (maxWidth == null) { + return lines; + } + + // If the current line goes past the maxWidth, append a new line without modifying the underlying text. + for (let i = 0; i < lines.length; i++) { + let line = lines[i]; + let newLine = ''; + // Note: we subtract the spacing to counter the initial padding on the left side. + if (this.measureText(line).width > maxWidth) { + while (this.measureText(line).width > maxWidth) { + newLine = line[line.length - 1] + newLine; + line = line.slice(0, -1); // Remove last character from line + } + + // Update the array with our new values + lines[i] = line; + lines[i + 1] = newLine; + } + } + + this._cachedText = text; + this._cachedLines = lines; + this._cachedRenderWidth = maxWidth; + + return lines; + } } diff --git a/src/engine/Graphics/Text.ts b/src/engine/Graphics/Text.ts index 462c4269..bd69def3 100644 --- a/src/engine/Graphics/Text.ts +++ b/src/engine/Graphics/Text.ts @@ -20,6 +20,11 @@ export interface TextOptions { * Optionally specify a font, if none specified a default font is used (System sans-serif 10 pixel) */ font?: Font | SpriteFont; + + /** + * Optionally specify a maximum width in pixels for our text, and wrap to the next line if needed. + */ + maxWidth?: number; } /** @@ -29,19 +34,22 @@ export interface TextOptions { */ export class Text extends Graphic { public color?: Color; + public maxWidth?: number; constructor(options: TextOptions & GraphicOptions) { super(options); // This order is important font, color, then text this.font = options.font ?? new Font(); this.color = options.color ?? this.color; this.text = options.text; + this.maxWidth = options.maxWidth; } public clone(): Text { return new Text({ text: this.text.slice(), color: this.color?.clone() ?? Color.Black, - font: this.font.clone() + font: this.font.clone(), + maxWidth: this.maxWidth }); } @@ -52,9 +60,7 @@ export class Text extends Graphic { public set text(value: string) { this._text = value; - const bounds = this.font.measureText(this._text); - this._textWidth = bounds.width; - this._textHeight = bounds.height; + this._calculateDimension(); } private _font: Font | SpriteFont; @@ -83,13 +89,13 @@ export class Text extends Graphic { } private _calculateDimension() { - const { width, height } = this.font.measureText(this._text); + const { width, height } = this.font.measureText(this._text, this.maxWidth); this._textWidth = width; this._textHeight = height; } public get localBounds(): BoundingBox { - return this.font.measureText(this._text).scale(this.scale); + return this.font.measureText(this._text, this.maxWidth).scale(this.scale); } protected override _rotate(_ex: ExcaliburGraphicsContext) { @@ -117,13 +123,19 @@ export class Text extends Graphic { } this.font.tint = this.tint; - const { width, height } = this.font.measureText(this._text); + const { width, height } = this.font.measureText(this._text, this.maxWidth); this._textWidth = width; this._textHeight = height; - this.font.render(ex, this._text, color, x, y); + this.font.render(ex, this._text, color, x, y, this.maxWidth); + if (this.font.showDebug) { ex.debug.drawRect(x - width, y - height, width * 2, height * 2); + if (this.maxWidth != null) { + ex.debug.drawRect(x, y, this.maxWidth, this.height, { + color: Color.Yellow + }); + } } } } diff --git a/src/spec/TextSpec.ts b/src/spec/TextSpec.ts index 1c54c9c6..ec73f5dd 100644 --- a/src/spec/TextSpec.ts +++ b/src/spec/TextSpec.ts @@ -1124,4 +1124,72 @@ describe('A Text Graphic', () => { expect(bounds.width).toBeCloseTo(440, -1); expect(bounds.height).toBeCloseTo(16, 0); }); + + it('can word wrap text for a spritefont', async () => { + const spriteFontImage = new ex.ImageSource('src/spec/images/GraphicsTextSpec/spritefont.png'); + await spriteFontImage.load(); + const spriteFontSheet = ex.SpriteSheet.fromImageSource({ + image: spriteFontImage, + grid: { + rows: 3, + columns: 16, + spriteWidth: 16, + spriteHeight: 16 + } + }); + + const spriteFont = new ex.SpriteFont({ + alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', + caseInsensitive: true, + spriteSheet: spriteFontSheet, + spacing: -4 + }); + + const sut = new ex.Text({ + text: 'some super long text that should wrap after 100 pixels', + color: ex.Color.Green, + font: spriteFont, + maxWidth: 100 + }); + + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const ctx = new ex.ExcaliburGraphicsContext2DCanvas({ canvasElement }); + ctx.clear(); + sut.draw(ctx, 0, 0); + + await expectAsync(canvasElement).toEqualImage('src/spec/images/GraphicsTextSpec/sprite-font-text-wrap.png'); + }); + + it('can word wrap text for a normal font', async () => { + const sut = new ex.Font({ + family: 'Open Sans', + size: 18, + quality: 1 + }); + + const text1 = new ex.Text({ + text: 'some super long text that should wrap after 100 pixels', + font: sut, + maxWidth: 100 + }); + + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const ctx = new ex.ExcaliburGraphicsContext2DCanvas({ canvasElement }); + + ctx.clear(); + text1.draw(ctx, 0, 18); + ctx.flush(); + + await runOnWindows(async () => { + await expectAsync(canvasElement).toEqualImage('src/spec/images/GraphicsTextSpec/font-text-wrap.png'); + }); + + await runOnLinux(async () => { + await expectAsync(canvasElement).toEqualImage('src/spec/images/GraphicsTextSpec/font-text-wrap-linux.png'); + }); + }); }); diff --git a/src/spec/images/GraphicsTextSpec/font-text-wrap-linux.png b/src/spec/images/GraphicsTextSpec/font-text-wrap-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..1c7990044a7d9cd99c11e8dfe73fbcd70723815d GIT binary patch literal 5104 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 RDgy%pgQu&X%Q~loCIBAirt<&* literal 0 HcmV?d00001 diff --git a/src/spec/images/GraphicsTextSpec/font-text-wrap.png b/src/spec/images/GraphicsTextSpec/font-text-wrap.png new file mode 100644 index 0000000000000000000000000000000000000000..4a0d0fae24a48b9e9e227121f40876bf7b04ee4b GIT binary patch literal 5904 zcmeAS@N?(olHy`uVBq!ia0y~yU`PRB4mJh`hJr^^Ll_ts7>k44ofy`glX=O&Aa=^r z#WAEJ?rm&(iOluVeYdR{f)qH-Hk5ZM9E@}IQqk>Pwkw7&_jJ)CA@5CdpFBxFck<-< zCn|j^<~KK{MsCUp+N5ipm6Md8AS}({uttbG!Y66t-sd&t-+9y1(#z5}GVI(`RyVQx z=FIQ+zTd0;UiZDa+_!mNDN*SbPCk(_o*py# zz4GL%=g#`OH;Y(q>{QB)*1THUnbUl1V(}x}C$n~h>|+1!daFEO(dOno#q*Y&IH@aQ zx36*k`^+fUb(*Jt-mma`al(2^r~41vd6E5-kA|<8SQhfYYP)9DvR0#u3YKo?6~Fgf zP1(IVu-u~e3+vnNvYh3&MDHtazN~JmEz3Kh{xSCxnf)y>!eZ&SXTI)EKbFNMAASDs z!KV_Yvu%v!6|?UqxJDbj6ghfMZo!FWRUANnb8 z&3kINuT41lY>Jhjb@_yxue~WNrMw{dl$)CYm}wgU%m62Kjp2xkFbbxt)}RN z4;RZ%zFDPueM#z_=_NP(A3Em?-ip}#Cz4adr|rbW?;4l3weBh2=drErjZW`Oj$77E{8IVolqT1kikVsOe(Od4 z=3VMz<-722et^}Tsky;?FMPBd8}F{4c=Mgw`8Vp7i&p46*J>8FW_)E`P?BHUxrX`p zg!E_0DMm9jDjy4f%Kv5fQ>>=-&EaKQzpnRwjQGbB`I}##&*RPHWSf9_H~S|Szp061 z{iHp)Cph7`c1qdxiRs^j{}dH=?w=_W_vzeUC4M0`R_|?PPrsjdB&~4w^k#A1-*@ak z9q87s{4Dg-{Lh3M?|*%7n$3za7uW^y?!^hH`tJjzP+?-NY_1P+Xjs78? zsY#o*{AzmMVplWUxA^R)_Y-bDT=v3!x8|jNw(E0OUf}(dUo-JeaM+Wt?j;Ue+n3xF zNeFvX)E9qdnd^-b-P>C7oe%z*Ys&7@Tx(!ES>cT5TQ3G97jXf`fKR_F&)#GSUcT-B zC6<#S@4WpPE;9MQwv=cOIBj*-?D64QPtSW+PyVImyIp*fcc5+huJ$QR9;<#wT?>ks8pih4 zC{lJ;w9`QgmC2sLTawjV`Tw_Oh1bp3F>Th^@H+HfMei(~wSE$@iUkS@n>KjG3ctTN zx6tk1f_L0GJLD7&ZP}rAclC!&4_;nf_lGg^wUo&Ekl8wQdAGyljC(>g!!I6~aQ4_S z@e|vhNT=jW%)cw{S2tn%O7(T~m%sDXpH%)ZxXX91jKh&-^ZvX({v`UB@1`BEy;3=qvhzo$a+L;tpEiHu+Qd0U z!S_Gd?3^wC@TAK9?Cw$rMvgy!N*-9aBe{#U?I}}J+o6(;yXSJd zoZZKmB5yVGyJ>Q~w-Tn^jE2@Wv)<}uZR*n9c>dJ=uis|hdsiRX zAX401y?bN%r^--=cTZkq@7c}eq0c1r`$@*MCv*O~^v};?nQ*V-{QW&An_lzevol6n zHi$TH+J0{_gUcJv1qXb?)LZ%Xcy2V-J#%q~+Wpzr)%Myh38}iryq>M3?l|AF>y9T5 zUS?dYUsGW^cWqvem@MOWiw&R6MXa|*7w-NOR~r3RIek+4udGeHGpDF>?bZu6|FI>L zUn8k2XEO$A*~(jeU^lA% zH8J~7(z1ufCs|zk)-V5Y^s&nGU5n)3F8VOB{N(Nznwxkfd}l^ zpg+s^b*bC$+W!4~r^GRqq37M&{Rv7UjxtH>fBbZQv+9?a?b&svdLQ!~zdzpjVAX~2 z4Hr^2iq`Z88tlj|db&s=;lkam+WXee&emY@Sovj7*ca_Uwa4PcG72+NyzeG_dcr1} z>v3<7$KBcWhu&<+eUNds&YNfJ^7wftouUQ(4={CDaPQ;^P~os~jbkgRFL?icr|gOE z3Okq;KGpav{62-ZBlm+#C!59>gRfjKb*tj;T7In0{_9cXAFF7(QpVxO%-wNM=k%pN zTilhGRLu9TGWKBfL6_3}e}*^P@1AYkTztu>mGgD5!;_V#gBUc$rb{e+zSZiu_WIJ| zYbm@^r$zjJ94_bRG&GBpvJ~7Bo4>#%`mwzH#W#z#*0mf;d8D>v>%{W~Ju^?ARG)ZB z@5{WPu=aH)SGhPOm|6Ud+;b2$S|7D}*3CEkbzbu# z`wNB6->P4sWF9xMVS(~q$Me~xe3nxl%)1}>wkzG#*3R~FK*obsNgb7G2Z~$G)7Ivz z_?{QhJ6{?K~QQvz!qmDrq-Vyp=1us!gu>8W2??4ptn>(3NTzUlQ)Ip0`U|I8GF z_z6Ex*qZG9K4nw?7b^v(DMc?TO%)27nXBWTM>H36TTbO%lzfDDf|lc(1=ou27M*Vv z`S;;J$03dw)9-cda~41Uu-|g(g?)1u<=)mPe5%~!!8}joPOito1&27_)I%QX$lf6P5Pqx^d5V`y(bHSuq^44`$tZ<$mhT_oQoo(vGqok66JN$T;PBQM<$1zV-u+tg@HG zHJQ#kHLvlDdU?0|FaH`dle0S_JT7kOa%#V)^!tNaYggbc=dQr33@TMw{$VQZH3t`L zoWgZC|=7k#4^TLgZc4v|4EAr+1BeBO3aLyq4T%x*%YI{7QrX7esAPH zVf#4oWGdTIxjWnY`b{m|&LrHLe^o{P?Xp>^_op%KD`NC)-L1jUbw6vmM%@t;57i~f zj(jFZC$jfU4!W&<%E7gwv}w z$rth$$?8;1o7nqmW>06!lF7+${=C^aeNM)l&oUNq5B4hszJJPe=-{K7wUvjQbPeN9 zHWc(JEdLZWZn>XcwD`#0;%ynmIKqIGv>ooqNU^=&Id;=~(MgIT`DJ$fbS z6=$#QTUGh%iiW}uBbC_;r){m#;?#_LqMX2YbYiEIu=C$}PEDqf^zmII0ncZ820sj;kE#pRJV z$}e=GH21LBhI@DJAT=IJjchC^INrh z%~SR^FVo1Hj2AiGC2p$v&9jUnFQ}~8JZa9ddmn5p)%8p_tjwM>;ZnErm0khwd%GX3 zej;J#E9E|8!@BbSlQsER6{OTxevx7R`uxta4fAWb_n(?KFD_vPn~RJybLEWfE4Qyn zejoVc@==$jO)Pr*3m8~-&ggTfpI~dUYOB}J6^E9FwThoQ5TB*`|JQTT#}l`=KVY`y zx~ZX%#C(kB!@0jgGi^(KVzYNvemGzF@3377*Ukyr=a=kEezmxy$@OSw!jiDUhci#e zRn3w=C980C``0LK!@7-A`PVz$YYFXUUN^CC?wMbw?#8WUo+Hrca%)@Qsrs3*f1DaP zTK6tp))=bK!TX>~+3%2qXi4?vdtGL&IU)l08cdlKbt2zrclaLRdidbYBTlYudmK5B zh$KJDes+B0_J=a;k0dNO)Mj^oSNnctYKr{LGwLh6ZaP=&Ka=t!a?Q8%n)kOdId9zV z7G)mz2cD(nS9aYwX!fa=Q$W)x_K(TU_7u~#I}B$3 zdH>J-UZVTAn6f`sw=1Ui$NJe$G&4)>*d}=4p!K1ps(uZR8}68#;jq!DIsW9!q~bF+ z)9#$;znWRfXDq{*{HBCE|Hh;;?t8tLPp7Lox8GlOV<$7$w4L{r{~!DL<-$Vy%_kgV z3W{B)ZJMDWegB|@(%c8pe@vEcyj670>#p;EE!h@bwk{TqNv<*(SNo^zP?#uxP%u6u zsNoH3yLh3ZhF8h9;(4<#d9u0qFVWq(mRI6`V{^8`rseYX!VO34Esw3+ zl>Mpb*(8@AeV&Ltu=7*S_Yh&#G zhX>d$Zcoq>jDGN6iOoRkk{k0DjnXv&x^9+}y@CqP=kL7xDKu-z;zj(QY8Q$)cnPLX zH!gIE7isXAH0x))+U*CPYaX3YdGuuNXBO7zdn%L9p3`Pp#>L#?_vns?A(!06oIi&+ zySC&nezJ9Q&6KE^zjD9JJ@*A`_~xjz_LR9;mwn9gi?P&w%l^tIMYCaw(kZi9(pn}H zO@u=%+8dKnRs=A7I&GOHmjCXC_XK{&X9ksj=j0r|yj)jBb@BemcZ}s8)gDYK->fU% z*Wh6_m6_wo+za;>nTgJ4{{H3p8)fze_J0dL%&B7P5DY9^&&>41p>63dy<*wb38$@A z?R~#{50}M^TYrT0Rxoin*HtXLz~yMi!pxt#Q0elGtDgUO|2ywH8Xvw!V8Vn~lf2!0 z#3olvzx`=bYmC;)T?D+A2CoYD;@wtW}qU1aFydb5^^ z!z@Rqwu3_;{6wd|3D43$I;PAj{oeZ=vpcuA{=4;NwYvZ3#Whl9W&aE|uc`=t+x1*T z+eLSY!K-hJ5+co$7T)YneGs4*FS@O)*grBG>NyyJ6u;j;bN-n*#6?JV$G%RU*gu)GUf9h%-Hs9 z>fRL^(o7%h7>{&l-wgjQ@o~woS9hm=y3_S&(bsbz%EFANO=SugQU$b{jU`W#sTKcg#Qp+u_ ze7lT8&zXHi?{+FTC`@ikKGb-3ebHAl5816t>=%8Jn}0Ln(-xJ_f}Qm_OD?S2)E2(@ z-1-nsMZu|eUKO#uF01+8$I#T*c)scM3yJLt)hv-=-$F}bye}#LYkOiZ5$^IOp>1cz zvgLZ~1sFrbYdV5MFNfRhSUTleW2(|iTrCpg_?EAzt^uwlRQ^?XO+)c z;}5SS&#k_4=Q|I3%8A&;68mjlw*Ky!!+S(QTKD~0(ddrHXSOviK5_R;<+_^^_Vp1P zk4vsCUo@}fQ_7S*5?dyQCd`NlxxZq&6PG66Q(klHY>}mhpPttj^Vu|cLGY@Wg1k?k z?o}O>^Op;Wm$05c*}pRVH19R*KxQQg4!;+s_II+^l^tC#u=whd(#7w6+RiU~@PjdA za)joih|--`th(;AeE-*{u#5R|1CJud6{o4&y4Nm{V=S`%K4BB%$*bF#WSOG%|6B4h zdRShun)2@lC*L=<+uJ86f64iDHvaNs(aXvXmo^$a_nP25eR98p{LZqD^EXa!x@9?$ z^`FZU|%Dm$aI> z{=J|=(8TqxpLZWqTyXR7@q6KO6pWcrR%v>S!L!6o*ZQ?0R>bjan)vBnkwd_GmRF)Hgs*<- z*;BF9qAOs{y-u;MeSg_^Do)9AV)D7-@i2JLrbly^HU$Odtl#y~E$ivDTMxbV@8|Mp zRSaQW!qxFnaK$X0$H}34H}|_OSmYZf|8?gg`DoT9haRkC3SwPy$Zf%)S;4W2p$=L( kELv+UuZof``1!xo#DB4j*DV$^FfcH9y85}Sb4q9e04T>kasU7T literal 0 HcmV?d00001 diff --git a/src/spec/images/GraphicsTextSpec/reuse-font-linux.png b/src/spec/images/GraphicsTextSpec/reuse-font-linux.png index bbebe312d9a5f733b666de7e2afbabb9ea596033..d42d5abdcc3addb422231a8d2987d3dfa7cf7238 100644 GIT binary patch delta 1471 zcmbQlyPkJ~VST2ji(^Pd+}l}|IU%7EN6%MpF0sl@*LQW9=9MMDyTaqjgatxk;v!20 z8dn%j)DRR`+2OE?gG(z(;mC}H7!EC_Ofn7+zkRp(k*&G}h=Mx0NZ2O5LY&7PBh{ zzCXAf%jS~b6H=_q6fLeOTL0~^VUbnSeBCo#E@iQb2@+L7{MXmIr@U8FxH#$Jid%Cv z>R-ryEH{#J`SJT|hGm%gySUBE&ArvCLQ*S5S$Q0M92 z1-kwRb-5?>v^|*nMJs5d+FiqQrfPyVt!@4DGRpY{%^RQWTGx`CakA_0M_H$Mz1Mejj}O?UXJ(oEM>vZ=YV#%w=pwLUcEz1u{N+{nw*%r|c7RLwHW2|X-my+`_6 z_OXRue!cRsW}D>B_w|g`-baiZ^Iyc?)AUX(`+llM#&!p@@BK9mJNF39d)1osGJCf~ zhls-W`op)rTWw5C-+gAD_re)EGy7+it#|oy;CaB?ybAUGj90^sIrPW`yu7>j>+1A_ zDdh)i%YEA`9;@>yZOF7c@(?){`407%zRgUdOUsuO4)(YVu~L zb%n(XHM(9X-3Ljr@AQxcAAf`g1GK?cKVt zrR4d{s)E}|NBsY>{SIB6_%gD!WRK*<`P&zN{UW&I_<7;|av9U#?7E}-HfE`!!rjVc z^El=R?B`m2wC~KBt&>mv;!i(t>vz@;H+#*0`n%T^n?1j0m9KEOcG)^})BkPa3|Cps z{}8`Dcki2Bch;U;Ym~1r<=mC?b05w8zPx_niu(Z;d6i4<&OE*D^x0>3R)1n!ROrVMq2i^KjuMbR!o<4u?&v0+npcTen%_MF;oL)BLsvu8f>CfA<>$#mBc767-Wj>rF zny<6!+M}|`3q4kyz8{sW%ft9`opEXQ`o<6I*E??dDB_hrhwEt4?u={K;=;Sy-DZ|9 zGn#K$&b!5k#ii!!+cx9U?>j6XJ$>~}@A-`7i7x{+x1KY5@J7)3Wa|zdX3drz{~t^6 rv`Cb)O}1sTpKQ(M2*rwG|5%TvU8_;~*ig^Fz`)??>gTe~DWM4fSK!dx delta 1449 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 delta 1831 zcmbO$Fh^j5Vf{5v7srr_xVN)k=7*$;9GhRA{`ySGE(Pl@CZ!Yan3VF~IIZdwGEtM5 zE}^V9;|06@uY>gmcDO1ly%UTPzM_$>JyogIqm@ai>F6dFuH5J6?B?!#w&$tevdc4# z%B~(JI~AKf7W}x*6hvAuWC<{7B{Fl+}IG%$RQ9naf4+249x>NGKC!s znte=Z?mP$gN(l$4=ymUz6sC!XZzo=8-R1vJ*^igoyX6Y}zk9kdas1iExAAfH!kv388zU|#v9F%k!&b~> z=qtSOF)zoJ+C{f^s_Sa}`WzX^qID1Gab76X&!B{#G<`=Zs0tcP#iG z9@!XiLFn!BhcmZ{G{(Muxcl+OJid)GB6mLP?lISQ{&PkC!rrN_wfjmYf06P~s))O3 zSC!%}etD{H4D0t6>nrJ;i&+=8y%)6H#hs?YaQ#N)hdTxOGT}X8_sh4;ZL0q~yTHFb zx@*$M?T(kXoV(9@ZgK5r=~u?rP0hbm$Y)90g{*aXXK|h7@pDs+_&-kDr%df>75Men zeTVwFR>}SawU1YSxck$kTw&SDpjm6ba9OnVHGe#Pd^(#z)#OPJ%UH_RHJ^=UJl|7o zRj&P3FQZId=%2Ay$ngu=59bwaFA}uXc=}`0PyhPEb}ziH+I@Pi&tdv!!mplRyzip= zQ*Q9zf7{Pw=HCC^vtBxBIp0&3N%z(*{+i;a@baz3(TTtHINJjr>||OdBE055)5(C@ zLQ^kwX3g(7eB&c$$#V94Gx;4Zmwt^*c>a8q{{1Rn01M_*m*o z-{xnZ94>!37m_xML;eQS;$N{lj-6@Ae>ZtUr520FJQ+PL5#AG1>NjOfdor)$y6f)V z?Is8Jf6X;LyTtLnU51E_@w?V#x2+nVTJAU{k;M6QWkdP1Pgk~FmAYbmPWJt`ZRwX5 zemoYxSL)lr#Siy>v@4mDALggIx0H3xVzpA%Nrm&z?9;1OP~kZHHq$D)>bCNR@SXsv zm+y?WvVFY1hRsBF-pTGOCeyF!)w6J>3C&yS?eM^f_x{SG)2A+5^F`Eln{?{uuvMC+ z{@*8N<{!)U%&*9J&vxNW=ekWlwF8aHru{Nsy6!{c+N_7uGORi8n!D`%x<%Bn+GC?I zd%~4VIk~IDERi@_rQOgu2OA`=vQ2{3&_0J&b9|{Uzt#yD_QQ&;7Obsw#g=m+!>n-GY`o zeBX*(j%vT=IPYcXhM#Q9TbAVhizrq;)H~Zjdgg`Ptovt-7i)zYEIziyz<0~Mfa>g! z2ydzMt9o0iPbKX*c4yA5bDClcn;Lmid~GESAFQve=TFmF(v{GccwMP-q1Q}~EemQM zYTY>1R(37sl^wIL%*stKWK1Q~OgcUs%=P-xrK-Q`l|t;7`o(FycY?(0@`aV-9bINVq*p-^) zuHa?Ol%23zhO_>?@98NQwoLR~xA?96>!90~y|W!|TdwMx{4F-#`K76Vp6S<|rYF0t zefINSsqakLuyymMnPJT*-0}z^Xh5=kNo|m z64P$>88N<1JMRC#^5QMOc#CBke3C0>S;@}&e0@!0@slHRu3_!ZUfHph?6!J2$?ihA zXJ*NpF9E%?n3m6PdddB;_-kN@LjB*!;N{H4CdrFd>dzJQo?p7uzQ#!GT?^xiRZHHL zq+dN|GPy%$oomH?e~sDSMc#<>eHRsG`x+~v@HaO2|Na9ntQD9eg2N5A?dL!K;q{eQ zcdR5P{#1)TccH}EE8TvIwQuCPj60WJ_Q))M(fxEvlZ=xiZ;J2SOI*BXHr%`_vR#@d z=PcXr2dl3(oxkx>(<*BA!=)u<4^>ikt@!2`?x_87<>IwdE@dp7!Q1vu&tJA#mu(6- z>C6o~vo?PJoM7=LNA~qE7ahL3lkeTd?^935H71{kD4jMv?CDOuRUgGa=eu&T1@&xy uQuHpk!Ck44ofy`glX=O&z;eUW z#WAEJ?(N)%xpxhCT4rl*ikyDJCRS;7Y8M+{f`h2a=RT{8$6kL;_YHnhE0h{s_xASh z;e_;$1{2yp8VE2~&SK*4PT$pD^Y`ap{}u1$=TBew{agFT zH}fuU+iUl0ckzLV?>|S0h_5oAyk6^H;2UR!zYXC@vYhK#Dp;;|8FCp($&1#eg-_Kt zVpLQJJf!VVb>E+1llFa+31!!|AG{mdl&0z(y6)X6alOdJl1*1CQrNzzzWZ={+FI){ zkJNn^Up0GQ(^L3+td@O3&dIYr2SfzQI5&TbWOO^Z;*{9uDJxEtvmIMIIpdUAy!nKr zgNq;Xs06L}v8z#pTaGtfVWQK~>%2=MK4e%hcZz83%TqYoJRTEu5)nSp4iFKmQyXdo#`9)+=@;zWWL`i z^;k?HmwA&Yo1mm%&e95ne~}73dqp^I{Yaix@LpJO4cCc2Azjv{Gam$dR{wo@(&56p zZ#nf=MXybFvwG~`sW>~9e^TT5H4-VTMye}x&vQ6vPLTEfF*U4%g~NDv&hB*%JDjey zZ(w(0WKO)IwZU`()5MvIJS~B%4_tHLa!G455O7<|6wRzDFjMl7gTz*@9M$KFGcA7q zI~S_mk{i0^ZI{`$7s-WBe621YO5C@IC!FKb?Zn0n_m6IJD7$_BSFo|{j2d}H6{cO$ z3D^E>O<4UqpK+3|rgFiH{cLg(=ZP01DD@~H|E#}_kU}+So-W_ zGwY#gE>r(LWdEy`{_TkPJ4MNjwf=F8%qO$AKlNk2sg!+hW#f~rw~LlH=1eqwf5qX` zt=yfL9k!jARi33#`F7i#Ooix_v({4?67MfbzrV|Ee*2>NbJG59Hd=SgxN7r$QIBs9 z8di%F)j7Bqy-!VjG_zNey@+M{&-;Au-hA8S5b;28Ls^aHgutYX&AHcRyiR{?$;Pyy zUtOzHV}_1^%Dm`C7wwhe^I6w1UFf&Iy*O?1t81c7f6^vcCdWl8?rvOA{UFyNjLF68 zLRbA38O5@jlUjWQJWo1J+Q7T4p)tW~YNVvc%quq>*qUC3q@^mP1|>5~ax4p;Ikj<$ zXD@Gx!VJIEFt#8yQ?m(AzAN6m$9E~`@TD2ihPm8dYLc%cJKsArG3JnHlS<2tEeAKO zWmlOvkyrUcWsbq?xnKW9R-3d>XFJqUePi91Q#U*?Ks#W?0I367_vts3OS-$kh { + const spriteFontImage = new ImageSource(spriteFontTexture); + const loader = new Loader([spriteFontImage]); + await game.start(loader); + + const spriteFontSheet = SpriteSheet.fromImageSource({ + image: spriteFontImage, + grid: { + rows: 3, + columns: 16, + spriteWidth: 16, + spriteHeight: 16 + } + }); + + const spriteFont = new SpriteFont({ + alphabet: '0123456789abcdefghijklmnopqrstuvwxyz,!\'&."?- ', + caseInsensitive: true, + spacing: -5, + spriteSheet: spriteFontSheet + }); + + const textWrapActor = new Actor({ + width: 100, + height: 100, + x: 550, + y: 350, + color: Color.Blue + }); + + const spriteTextWrapActor = new Actor({ + width: 100, + height: 100, + x: 250, + y: 350, + color: Color.Green + }); + + const text = new Text({ + text: 'WORD_WRAP_TEST........', + color: Color.White, + font: new Font({ size: 24, baseAlign: BaseAlign.Top }), + maxWidth: 100 + }); + const spriteText = new Text({ + text: 'spritewordwraptest................', + font: spriteFont, + maxWidth: 100 - 5 // sub 5 to account for padding I don't completely understand + }); + + text.font.showDebug = true; + spriteText.font.showDebug = true; + + textWrapActor.graphics.add(text); + spriteTextWrapActor.graphics.add(spriteText); + + Logger.getInstance().info(spriteText.localBounds.left + ',' + spriteTextWrapActor.pos.x); + + game.add(textWrapActor); + game.add(spriteTextWrapActor); + + game.start(); +}); + export const playButton: Story = withEngine( async (game) => { const heartTx = new ImageSource(heartTexture); diff --git a/src/stories/assets/spritefont.png b/src/stories/assets/spritefont.png new file mode 100644 index 0000000000000000000000000000000000000000..a7fbf645bcf9e05dbcaf779bdf0610385cb06295 GIT binary patch literal 1357 zcmeAS@N?(olHy`uVBq!ia0y~yU}R!oU@+xiV_;xd<^J>|0|NtNage(c!@6@aFBupZ zSkfJR9T^xl_H+M9WMyDr;4JWnEM{QfI}E~%$MaXDFfcGkmAFQf1m~xflqVLYG6W=M z=9TFAxrQi|8S9zq85+8Ob~0yRVAUpVuwY2nPftj@-}x!cmp#CguX(UWL9KmX%(`Cofu z@7BICdVA)DSw{ZS&1Yx(RBPYe`_AZX*sk3Remh!g-_5<3dMowu+eF0%b>4kB&1}lsS9!_hyX@aiRxiIWZ4*~%`F7{6*K^dqfBww(D%fmst?l`Y z@blWsYp>oc<`56uws`mHM}6mP^WS{>-1kEJZK&UG?d9KH?ejlcSq>uS+O`)6rdsdXt+?&3^{(o-uXjFv()U8^?b{c5$=|QpW%LJ3-{e&?^_$l6 zTG#4RS{Flat$v}Gq5oK*ZOYqakAMr?k{#FWpSGc#*nXoPBS)dyZduVB$(RK3fpPEIxrIWn3cxOzWAHKpHq#_hb`R&r4yE_A_>6FGf%h@Mum)@QF z&TsA=;k%Q2tCGF9c$ZAhoDPejE6dji#V!TA_U6T#m)bUYl`LMLen_!LV|BpvO|o-k zb2U~!$*Z%wc$c?w_Wom^z1|%wJ{PvaJM!rg?@xB~inmSL{qtw%J6n*#*`0OE=JFhN zJYF?#p^M*?{SrPI*p$5qDnu7Rb+O|a3YGUp$W{kiVPu1V&bbJTv#EGsM7 zzVY;y&B^8M82Jc2(iYCV+j`i4&41-iPy(8Lao@c3Mc!L}ueYsyFMa#nKi;2r=bkq- zbKIZ){*gf4&ncmcq-$;8U)VR#|H!u8^^aawTSwn5c~{|F_x@%2g?}&aoh$g!`B;4Z z{$ro}UZmfed9in1`O$ToYY$H@xBq!&V{Os#;GcK>?T=sHd$#aLr}nM6FW))cD}Pt9 zsrGdFzW>(s@66}^lUeM)CB42-WB&o4-J*ASZ{56j_t?74y#C)0OP7}%vHtqn|8ubY zcmKl=)%i-|F7BJ>UsATw^6o_QcR%ibF)puf+xR-~ZTdrje~bZP;LzTEL*VVXIo~{= z@6GS{er)&W&+E=T%f0{iUo<@7;s~VKHGhBcF6!wzpqtcV&A`CG;OXk;vd$@?2>?1- B!W94j literal 0 HcmV?d00001