diff --git a/CHANGELOG.md b/CHANGELOG.md index 12c0cb9b..64608cd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Added multi-line support to `Text` graphics ([#1866](https://github.com/excaliburjs/Excalibur/issues/1866)) - Added `TileMap` arbitrary graphics support with `.addGraphic()` ([#1862](https://github.com/excaliburjs/Excalibur/issues/1862)) - Added `TileMap` row and column accessors `getRows()` and `getColumns()` ([#1859](https://github.com/excaliburjs/Excalibur/issues/1859)) - Added the ability to store arbitrary data in `TileMap` cells with `Cell.data.set('key', 'value')` and `Cell.data.get('key')` ([#1861](https://github.com/excaliburjs/Excalibur/issues/1861)) diff --git a/src/engine/Graphics/Font.ts b/src/engine/Graphics/Font.ts index 55c9b982..c5d67232 100644 --- a/src/engine/Graphics/Font.ts +++ b/src/engine/Graphics/Font.ts @@ -74,6 +74,7 @@ export class Font extends Raster implements FontRenderer { } private _text: string; + private _lines: string[]; private _textBounds: BoundingBox = new BoundingBox(); private _textWidth: number = 0; private _textHeight: number = 0; @@ -87,11 +88,13 @@ export class Font extends Raster implements FontRenderer { } public get height() { - return this._textHeight; + const numLines = this._lines?.length ?? 1; + return this._textHeight * numLines; } public set height(value: number) { - this._textHeight = value; + const numLines = this._lines?.length ?? 1; + this._textHeight = value / numLines; } private get _rasterWidth() { @@ -125,8 +128,8 @@ export class Font extends Raster implements FontRenderer { 0, this._rasterWidth, this._rasterHeight, - x - (this._rasterWidth / this.quality) / 2, - y - (this._rasterHeight / this.quality) / 2, + x - this._rasterWidth / this.quality / 2, + y - this._rasterHeight / this.quality / 2, this._rasterWidth / this.quality, this._rasterHeight / this.quality ); @@ -155,6 +158,7 @@ export class Font extends Raster implements FontRenderer { public updateText(text: string) { if (this._text !== text) { this._text = text; + this._lines = this._text.split('\n'); this._updateDimensions(); this.flagDirty(); } @@ -163,24 +167,30 @@ export class Font extends Raster implements FontRenderer { private _updateDimensions() { if (this._text) { this._applyFont(this._ctx); - const metrics = this._ctx.measureText(this._text); + const maxWidthLine = this._lines.reduce((a, b) => { + return a.length > b.length ? a : b; + }); + const metrics = this._ctx.measureText(maxWidthLine); this._textWidth = Math.abs(metrics.actualBoundingBoxLeft) + Math.abs(metrics.actualBoundingBoxRight); this._textHeight = Math.abs(metrics.actualBoundingBoxAscent) + Math.abs(metrics.actualBoundingBoxDescent); + // TODO lineheight makes the text bounds wonky + const lineAdjustedHeight = this._textHeight * this._lines.length; + // this._textHeight = lineAdjustedHeight; // Changing the width and height clears the context properties // We double the bitmap width to account for alignment // We scale by "quality" so we render text without jaggies this._bitmap.width = (this._textWidth + this.padding * 2) * 2 * this.quality; - this._bitmap.height = (this._textHeight + this.padding * 2) * 2 * this.quality; + this._bitmap.height = (lineAdjustedHeight + this.padding * 2) * 2 * this.quality; // These bounds exist in raster bitmap space where the top left corner is the corder of the bitmap - // TODO need to account for padding const x = 0; const y = 0; + const bottomBounds = lineAdjustedHeight - Math.abs(metrics.actualBoundingBoxAscent); this._textBounds = new BoundingBox({ left: x - Math.abs(metrics.actualBoundingBoxLeft) - this.padding, top: y - Math.abs(metrics.actualBoundingBoxAscent) - this.padding, - bottom: y + Math.abs(metrics.actualBoundingBoxDescent) + this.padding, + bottom: y + bottomBounds + this.padding, right: x + Math.abs(metrics.actualBoundingBoxRight) + this.padding }); } @@ -222,12 +232,17 @@ export class Font extends Raster implements FontRenderer { // The reason we need to re-apply the font is setting raster properties (like width/height) can reset the context props this._applyRasterProperites(ctx); this._applyFont(ctx); - if (this.color) { - ctx.fillText(this._text, 0, 0); - } - if (this.strokeColor) { - ctx.strokeText(this._text, 0, 0); + const lineHeight = this._textHeight; // TODO user specified line height + for (let i = 0; i < this._lines.length; i++) { + const line = this._lines[i]; + if (this.color) { + ctx.fillText(line, 0, i * lineHeight); + } + + if (this.strokeColor) { + ctx.strokeText(line, 0, i * lineHeight); + } } if (this.showDebug) { diff --git a/src/engine/Graphics/FontCommon.ts b/src/engine/Graphics/FontCommon.ts index 9858adec..01f72efc 100644 --- a/src/engine/Graphics/FontCommon.ts +++ b/src/engine/Graphics/FontCommon.ts @@ -110,6 +110,7 @@ export enum Direction { export interface FontOptions { size?: number; unit?: FontUnit; + lineHeight?: number; family?: string; style?: FontStyle; bold?: boolean; @@ -126,4 +127,4 @@ export interface FontOptions { export interface FontRenderer { render(ex: ExcaliburGraphicsContext, text: string, x: number, y: number): void; -} \ No newline at end of file +} diff --git a/src/spec/GraphicsTextSpec.ts b/src/spec/GraphicsTextSpec.ts index c179d3c1..0a1ee3fa 100644 --- a/src/spec/GraphicsTextSpec.ts +++ b/src/spec/GraphicsTextSpec.ts @@ -152,6 +152,37 @@ describe('A Text Graphic', () => { }); }); + it('can draw multiple lines of text', async () => { + const sut = new ex.Graphics.Text({ + text: 'multiple\nlines\nof text', + color: ex.Color.Green, + font: new ex.Graphics.Font({ + family: 'Open Sans', + size: 18, + quality: 1 + }) + }); + + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const ctx = new ex.Graphics.ExcaliburGraphicsContext2DCanvas({ canvasElement }); + ctx.clear(); + sut.draw(ctx, 10, 20); + + expect(sut.width).toBeCloseTo(69.8, -1); + expect(sut.height).toBeCloseTo(18 * 3, 0); + expect(sut.localBounds.width).toBeCloseTo(69.8, -1); + expect(sut.localBounds.height).toBeCloseTo(18 * 3, 0); + await runOnWindows(async () => { + await expectAsync(canvasElement).toEqualImage('src/spec/images/GraphicsTextSpec/multi-text.png'); + }); + + await runOnLinux(async () => { + await expectAsync(canvasElement).toEqualImage('src/spec/images/GraphicsTextSpec/multi-text-linux.png'); + }); + }); + it('can have width and height', () => { const sut = new ex.Graphics.Text({ text: 'some extra long text that we want to measure', diff --git a/src/spec/images/GraphicsTextSpec/multi-text-linux.png b/src/spec/images/GraphicsTextSpec/multi-text-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..87a7fc4dcd291656c7a4d7e3ea369fb0de4097dd GIT binary patch literal 2823 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 literal 0 HcmV?d00001 diff --git a/src/spec/images/GraphicsTextSpec/multi-text.png b/src/spec/images/GraphicsTextSpec/multi-text.png new file mode 100644 index 0000000000000000000000000000000000000000..8a6a61c9227927807035fd94ab5c72e52a69f026 GIT binary patch literal 3136 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 Hr-UW|$VB)n literal 0 HcmV?d00001