Skip to content

Commit

Permalink
feat: Implement Font lineHeight (#2905)
Browse files Browse the repository at this point in the history
This PR implements a feature suggestion for line height in Excalibur Text
  • Loading branch information
eonarheim authored Jan 29, 2024
1 parent 79b5e68 commit 93d039c
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/engine/Graphics/Font.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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;

Expand Down
4 changes: 4 additions & 0 deletions src/engine/Graphics/FontCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 13 additions & 8 deletions src/engine/Graphics/FontTextInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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() +
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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');
Expand All @@ -275,8 +280,8 @@ export class FontTextInstance {
}
}

this._chachedText = text;
this._chachedLines = lines;
this._cachedText = text;
this._cachedLines = lines;
this._cachedRenderWidth = maxWidth;

return lines;
Expand Down
10 changes: 8 additions & 2 deletions src/engine/Graphics/SpriteFont.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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[] {
Expand Down Expand Up @@ -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;
}
}

Expand Down
68 changes: 68 additions & 0 deletions src/spec/TextSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/spec/images/GraphicsTextSpec/line-height.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 93d039c

Please sign in to comment.