Skip to content

Commit

Permalink
feat: ex.FontSource resource type (#2934)
Browse files Browse the repository at this point in the history
## Changes:

- Added `ex.FontSource` resource type
  ```typescript
  const fontSource = new ex.FontSource('/my-font.ttf', 'My Font')
  loader.addResource(fontSource)

  game.start(loader).then(() => {
    const font = fontSource.toFont() // returns ex.Font
  })
  ```

  Font options can be defined either at the source or at the `toFont()` call. If defined in both, `toFont(options)` will
  override the options in the `FontSource`.

  ```typescript
  const fontSource = new ex.FontSource('/my-font.ttf', 'My Font', { 
    filtering: ex.ImageFiltering.Pixel,
    size: 16, // set a default size
  })
  const font = fontSource.toFont({
    // override just the size
    size: 20,
  })
  ```
- adds `"dom.iterable"` to  `lib` in tsconfig.json as it was needed to get the correct types for `document.fonts`
  • Loading branch information
mattjennings authored Feb 17, 2024
1 parent 9d21bae commit a981c5d
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 0 deletions.
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,29 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added

- Added `ex.FontSource` resource type
```typescript
const fontSource = new ex.FontSource('/my-font.ttf', 'My Font')
loader.addResource(fontSource)

game.start(loader).then(() => {
const font = fontSource.toFont() // returns ex.Font
})
```

Font options can be defined either at the source or at the `toFont()` call. If defined in both, `toFont(options)` will
override the options in the `FontSource`.

```typescript
const fontSource = new ex.FontSource('/my-font.ttf', 'My Font', {
filtering: ex.ImageFiltering.Pixel,
size: 16, // set a default size
})
const font = fontSource.toFont({
// override just the size
size: 20,
})
```
- Added fullscreen after load feature! You can optionally provide a `fullscreenContainer` with a string id or an instance of the `HTMLElement`
```typescript
new ex.Loader({
Expand Down
1 change: 1 addition & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ module.exports = (config) => {
{ pattern: 'src/spec/images/**/*.txt', included: false, served: true },
{ pattern: 'src/spec/images/**/*.css', included: false, served: true },
{ pattern: 'src/spec/images/**/*.woff2', included: false, served: true },
{ pattern: 'src/spec/fonts/**/*.ttf', included: false, served: true },
],
mime: { 'text/x-typescript': ['ts', 'tsx'] },
preprocessors: {
Expand Down
76 changes: 76 additions & 0 deletions src/engine/Resources/Font.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Font } from '../Graphics/Font';
import { FontOptions } from '../Graphics/FontCommon';
import { GraphicOptions, RasterOptions } from '../Graphics';
import { Loadable } from '../Interfaces/Loadable';
import { Resource } from './Resource';


export interface FontSourceOptions
extends Omit<FontOptions, 'family'>,
GraphicOptions,
RasterOptions {
/**
* Whether or not to cache-bust requests
*/
bustCache?: boolean
}

export class FontSource implements Loadable<FontFace> {
private _resource: Resource<Blob>;
private _isLoaded = false;
private _options: FontSourceOptions;

data!: FontFace;


constructor(
/**
* Path to the font resource relative from the HTML document hosting the game, or absolute
*/
public readonly path: string,
/**
* The font family name
*/
public readonly family: string,
{ bustCache, ...options }: FontSourceOptions = {}
) {
this._resource = new Resource(path, 'blob', bustCache);
this._options = options;
}

async load(): Promise<FontFace> {
if (this.isLoaded()) {
return this.data;
}

try {
const blob = await this._resource.load();
const url = URL.createObjectURL(blob);

if (!this.data) {
this.data = new FontFace(this.family, `url(${url})`);
document.fonts.add(this.data);
}

await this.data.load();
this._isLoaded = true;
} catch (error) {
throw `Error loading FontSource from path '${this.path}' with error [${
(error as Error).message
}]`;
}
return this.data;
}

isLoaded(): boolean {
return this._isLoaded;
}

/**
* Build a font from this FontSource.
* @param options {FontOptions} Override the font options
*/
toFont(options?: FontOptions): Font {
return new Font({ family: this.family, ...this._options, ...options });
}
}
1 change: 1 addition & 0 deletions src/engine/Resources/Index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './Resource';
export * from './Sound/Index';
export * from './Gif';
export * from './Font';
1 change: 1 addition & 0 deletions src/engine/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"downlevelIteration": true,
"lib": [
"dom",
"dom.iterable",
"es5",
"es2018"
],
Expand Down
82 changes: 82 additions & 0 deletions src/spec/FontSourceSpec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as ex from '@excalibur';

describe('A FontSource', () => {
it('exists', () => {
expect(ex.FontSource).toBeDefined();
});

it('can be constructed', () => {
const fontSource = new ex.FontSource('src/spec/fonts/Gorgeous Pixel.ttf', 'Gorgeous Pixel');
expect(fontSource).toBeDefined();
});

it('can load fonts', async () => {
const fontSource = new ex.FontSource('src/spec/fonts/Gorgeous Pixel.ttf', 'Gorgeous Pixel');

await fontSource.load();

expect(fontSource.data).not.toBeUndefined();
});

it('adds a FontFace to document.fonts', async () => {
const fontSource = new ex.FontSource('src/spec/fonts/Gorgeous Pixel.ttf', 'Gorgeous Pixel');

await fontSource.load();

expect(document.fonts.has(fontSource.data)).toBeTrue();
});

it('can convert to a Font', async () => {
const fontSource = new ex.FontSource('src/spec/fonts/Gorgeous Pixel.ttf', 'Gorgeous Pixel');

await fontSource.load();
const font = fontSource.toFont();

expect(font).toBeInstanceOf(ex.Font);
});

it('will use options from FontSource', async () => {
const fontSource = new ex.FontSource('src/spec/fonts/Gorgeous Pixel.ttf', 'Gorgeous Pixel', {
size: 50
});

await fontSource.load();
const font = fontSource.toFont();

expect(font.size).toBe(50);
});

it('will override options when converting to a Font', async () => {
const fontSource = new ex.FontSource('src/spec/fonts/Gorgeous Pixel.ttf', 'Gorgeous Pixel', {
size: 50,
opacity: 0.5
});

await fontSource.load();
const font = fontSource.toFont({
size: 100
});

expect(font.size).toBe(100);
expect(font.opacity).toBe(0.5);
});

it('will resolve the font if already loaded', async () => {
const fontSource = new ex.FontSource('src/spec/fonts/Gorgeous Pixel.ttf', 'Gorgeous Pixel');

const font = await fontSource.load();

expect(fontSource.isLoaded()).toBe(true);
const alreadyLoadedFont = await fontSource.load();

expect(font).toBe(alreadyLoadedFont);
});

it('will return error if font doesn\'t exist', async () => {
const fontSource = new ex.FontSource('42.ttf', '42');

await expectAsync(fontSource.load()).toBeRejectedWith(
'Error loading FontSource from path \'42.ttf\' with error [Not Found]'
);
});
});
Binary file added src/spec/fonts/Gorgeous Pixel.ttf
Binary file not shown.
1 change: 1 addition & 0 deletions src/spec/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"downlevelIteration": true,
"lib": [
"dom",
"dom.iterable",
"es5",
"es2015.collection",
"es2015.iterable",
Expand Down

0 comments on commit a981c5d

Please sign in to comment.