diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e22a8b33..b22e446ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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({ diff --git a/karma.conf.js b/karma.conf.js index 28e4a589e..3342a5448 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -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: { diff --git a/src/engine/Resources/Font.ts b/src/engine/Resources/Font.ts new file mode 100644 index 000000000..546149dd2 --- /dev/null +++ b/src/engine/Resources/Font.ts @@ -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, + GraphicOptions, + RasterOptions { + /** + * Whether or not to cache-bust requests + */ + bustCache?: boolean +} + +export class FontSource implements Loadable { + private _resource: Resource; + 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 { + 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 }); + } +} diff --git a/src/engine/Resources/Index.ts b/src/engine/Resources/Index.ts index ead9e734b..3f91af5bf 100644 --- a/src/engine/Resources/Index.ts +++ b/src/engine/Resources/Index.ts @@ -1,3 +1,4 @@ export * from './Resource'; export * from './Sound/Index'; export * from './Gif'; +export * from './Font'; \ No newline at end of file diff --git a/src/engine/tsconfig.json b/src/engine/tsconfig.json index 82ae668e7..727bfddfd 100644 --- a/src/engine/tsconfig.json +++ b/src/engine/tsconfig.json @@ -20,6 +20,7 @@ "downlevelIteration": true, "lib": [ "dom", + "dom.iterable", "es5", "es2018" ], diff --git a/src/spec/FontSourceSpec.ts b/src/spec/FontSourceSpec.ts new file mode 100644 index 000000000..f7ff4cebf --- /dev/null +++ b/src/spec/FontSourceSpec.ts @@ -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]' + ); + }); +}); diff --git a/src/spec/fonts/Gorgeous Pixel.ttf b/src/spec/fonts/Gorgeous Pixel.ttf new file mode 100644 index 000000000..2917190e2 Binary files /dev/null and b/src/spec/fonts/Gorgeous Pixel.ttf differ diff --git a/src/spec/tsconfig.json b/src/spec/tsconfig.json index 74d81a9a7..0090f2ab4 100644 --- a/src/spec/tsconfig.json +++ b/src/spec/tsconfig.json @@ -11,6 +11,7 @@ "downlevelIteration": true, "lib": [ "dom", + "dom.iterable", "es5", "es2015.collection", "es2015.iterable",