diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fa9f654b..b40c21502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Added new `lineHeight` property on `SpriteFont` and `Font` to manually adjust the line height when rendering text. - Added missing dual of `ex.GraphicsComponent.add()`, you can now `ex.GraphicsComponent.remove(name)`; - Added additional options to `ex.Animation.fromSpriteSheetCoordinates()` you can now pass any valid `ex.GraphicOptions` to influence the sprite per frame ```typescript diff --git a/src/engine/Graphics/Font.ts b/src/engine/Graphics/Font.ts index 61c15cdb3..28b56b0d7 100644 --- a/src/engine/Graphics/Font.ts +++ b/src/engine/Graphics/Font.ts @@ -43,6 +43,7 @@ export class Font extends Graphic implements FontRenderer { this.textAlign = options?.textAlign ?? this.textAlign; this.baseAlign = options?.baseAlign ?? this.baseAlign; this.direction = options?.direction ?? this.direction; + this.lineHeight = options?.lineHeight ?? this.lineHeight; this.quality = options?.quality ?? this.quality; if (options?.shadow) { this.shadow = {}; @@ -98,6 +99,10 @@ export class Font extends Graphic implements FontRenderer { public textAlign: TextAlign = TextAlign.Left; public baseAlign: BaseAlign = BaseAlign.Alphabetic; public direction: Direction = Direction.LeftToRight; + /** + * Font line height in pixels, default line height if unset + */ + public lineHeight: number | undefined = undefined; public size: number = 10; public shadow: { blur?: number; offset?: Vector; color?: Color } = null; diff --git a/src/engine/Graphics/FontCommon.ts b/src/engine/Graphics/FontCommon.ts index 3d177dbed..b4d698c3d 100644 --- a/src/engine/Graphics/FontCommon.ts +++ b/src/engine/Graphics/FontCommon.ts @@ -147,6 +147,10 @@ export interface FontOptions { * Optionally specify the text direction, by default LeftToRight */ direction?: Direction; + /** + * Optionally override the text line height in pixels, useful for multiline text. If unset will use default. + */ + lineHeight?: number | undefined; /** * Optionally specify the quality of the text bitmap, it is a multiplier on the size size, by default 2. * Higher quality text has a higher memory impact diff --git a/src/engine/Graphics/FontTextInstance.ts b/src/engine/Graphics/FontTextInstance.ts index 1b11cb009..5593f732e 100644 --- a/src/engine/Graphics/FontTextInstance.ts +++ b/src/engine/Graphics/FontTextInstance.ts @@ -57,11 +57,15 @@ export class FontTextInstance { } private _setDimension(textBounds: BoundingBox, bitmap: CanvasRenderingContext2D) { + let lineHeightRatio = 1; + if (this.font.lineHeight) { + lineHeightRatio = (this.font.lineHeight/this.font.size); + } // 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 bitmap.canvas.width = (textBounds.width + this.font.padding * 2) * 2 * this.font.quality; - bitmap.canvas.height = (textBounds.height + this.font.padding * 2) * 2 * this.font.quality; + bitmap.canvas.height = (textBounds.height + this.font.padding * 2) * 2 * this.font.quality * lineHeightRatio; } public static getHashCode(font: Font, text: string, color?: Color) { @@ -73,6 +77,7 @@ export class FontTextInstance { font.textAlign + font.baseAlign + font.direction + + font.lineHeight + JSON.stringify(font.shadow) + (font.padding.toString() + font.smoothing.toString() + @@ -187,7 +192,7 @@ export class FontTextInstance { this.dimensions = this.measureText(this.text, maxWidth); this._setDimension(this.dimensions, this.ctx); const lines = this._getLinesFromText(this.text, maxWidth); - const lineHeight = this.dimensions.height / lines.length; + const lineHeight = this.font.lineHeight ?? this.dimensions.height / lines.length; // draws the text to the main bitmap this._drawText(this.ctx, lines, lineHeight); @@ -245,12 +250,12 @@ export class FontTextInstance { * @param text * @param maxWidth */ - private _chachedText: string; - private _chachedLines: string[]; + private _cachedText: string; + private _cachedLines: string[]; private _cachedRenderWidth: number; private _getLinesFromText(text: string, maxWidth?: number) { - if (this._chachedText === text && this._cachedRenderWidth === maxWidth) { - return this._chachedLines; + if (this._cachedText === text && this._cachedRenderWidth === maxWidth) { + return this._cachedLines; } const lines = text.split('\n'); @@ -275,8 +280,8 @@ export class FontTextInstance { } } - this._chachedText = text; - this._chachedLines = lines; + this._cachedText = text; + this._cachedLines = lines; this._cachedRenderWidth = maxWidth; return lines; diff --git a/src/engine/Graphics/SpriteFont.ts b/src/engine/Graphics/SpriteFont.ts index b62e5eb06..1189a7cc6 100644 --- a/src/engine/Graphics/SpriteFont.ts +++ b/src/engine/Graphics/SpriteFont.ts @@ -22,6 +22,10 @@ export interface SpriteFontOptions { * Optionally ignore case in the supplied text; */ caseInsensitive?: boolean; + /** + * Optionally override the text line height, useful for multiline text. If unset will use default. + */ + lineHeight?: number | undefined; /** * Optionally adjust the spacing between character sprites */ @@ -40,17 +44,19 @@ export class SpriteFont extends Graphic implements FontRenderer { public shadow: { offset: Vector } = null; public caseInsensitive = false; public spacing: number = 0; + public lineHeight: number | undefined = undefined; private _logger = Logger.getInstance(); constructor(options: SpriteFontOptions & GraphicOptions) { super(options); - const { alphabet, spriteSheet, caseInsensitive, spacing, shadow } = options; + const { alphabet, spriteSheet, caseInsensitive, spacing, shadow, lineHeight } = options; this.alphabet = alphabet; this.spriteSheet = spriteSheet; this.caseInsensitive = caseInsensitive ?? this.caseInsensitive; this.spacing = spacing ?? this.spacing; this.shadow = shadow ?? this.shadow; + this.lineHeight = lineHeight ?? this.lineHeight; } private _getCharacterSprites(text: string): Sprite[] { @@ -109,7 +115,7 @@ export class SpriteFont extends Graphic implements FontRenderer { height = Math.max(height, sprite.height); } xCursor = 0; - yCursor += height; + yCursor += this.lineHeight ?? height; } } diff --git a/src/spec/TextSpec.ts b/src/spec/TextSpec.ts index ffb344da6..a327be1ba 100644 --- a/src/spec/TextSpec.ts +++ b/src/spec/TextSpec.ts @@ -444,6 +444,35 @@ describe('A Text Graphic', () => { }); }); + it('can have line height', async () => { + const sut = new ex.Text({ + text: 'green text\nthat has multiple\nlines to it.', + color: ex.Color.Green, + font: new ex.Font({ + family: 'Open Sans', + size: 18, + quality: 1, + lineHeight: 36 + }) + }); + + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const ctx = new ex.ExcaliburGraphicsContext2DCanvas({ canvasElement }); + + ctx.clear(); + sut.draw(ctx, 10, 10); + + await runOnWindows(async () => { + await expectAsync(canvasElement).toEqualImage('src/spec/images/GraphicsTextSpec/line-height.png'); + }); + + await runOnLinux(async () => { + await expectAsync(canvasElement).toEqualImage('src/spec/images/GraphicsTextSpec/line-height-linux.png'); + }); + }); + it('can have a shadow', async () => { const sut = new ex.Text({ text: 'green text', @@ -890,6 +919,45 @@ describe('A Text Graphic', () => { await expectAsync(canvasElement).toEqualImage('src/spec/images/GraphicsTextSpec/spritefont-text.png'); }); + + it('can have a custom lineHeight', 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, + spacing: -5, + spriteSheet: spriteFontSheet, + lineHeight: 32 + }); + + const sut = new ex.Text({ + text: '111\n222\n333\n444', + font: spriteFont + }); + + const canvasElement = document.createElement('canvas'); + canvasElement.width = 200; + 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-line-height.png'); + }); }); it('will log warnings when there are issues', async () => { diff --git a/src/spec/images/GraphicsTextSpec/line-height-linux.png b/src/spec/images/GraphicsTextSpec/line-height-linux.png new file mode 100644 index 000000000..43db22a13 Binary files /dev/null and b/src/spec/images/GraphicsTextSpec/line-height-linux.png differ diff --git a/src/spec/images/GraphicsTextSpec/line-height.png b/src/spec/images/GraphicsTextSpec/line-height.png new file mode 100644 index 000000000..2cb2a261c Binary files /dev/null and b/src/spec/images/GraphicsTextSpec/line-height.png differ diff --git a/src/spec/images/GraphicsTextSpec/sprite-font-line-height.png b/src/spec/images/GraphicsTextSpec/sprite-font-line-height.png new file mode 100644 index 000000000..97790e4c1 Binary files /dev/null and b/src/spec/images/GraphicsTextSpec/sprite-font-line-height.png differ