diff --git a/CHANGELOG.md b/CHANGELOG.md index c750d5578..30599d902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Added - **rendering:** `getCameraView(world, camera, renderContext)` (and `computeCameraView(camera, position, renderContext)` for systems that already hold the camera's components) returns what a camera sees: the world area it shows (`bounds`, `size`, accounting for its position and zoom), its `pixelsPerUnit` in CSS pixels, and `worldToViewport`/`viewportToWorld` conversions to and from CSS pixels on the canvas. To place something drawn by one camera over something drawn by another, convert through the viewport: `hudView.viewportToWorld(gameView.worldToViewport(position))` +- **text:** The engine's default font can be imported through a bundler from `@forge-game-engine/forge/fonts/default/default.json` and `@forge-game-engine/forge/fonts/default/default.png`, so you no longer need to copy it out of `node_modules` #### Changed @@ -26,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **rendering:** The render system no longer draws sprites, nine-slice regions or text glyphs whose quads are entirely outside a camera's view. Code that disabled sprites only to save drawing them while off screen can be deleted. A material with a custom vertex shader that moves vertices beyond the sprite's quad may be skipped while partly visible - **rendering:** `createProjectionMatrix` now takes the world-space `Rect` to show, such as `getCameraView(...).bounds`, instead of `(width, height, cameraPosition, zoom, pixelsPerUnit)` - **ui:** A `'screenPixels'`-unit size or margin, and `UiSafeAreaEcsComponent` insets, now follow the canvas camera's `zoom`, so they keep their on-screen size on a world-space canvas whose camera zooms. A screen-space canvas's root rect now fills its camera's view, so it follows a moved or zoomed UI camera +- **text:** `FontAtlasCache.getOrLoad` takes the URL of both atlas files, `getOrLoad({ metricsUrl, imageUrl })`, instead of finding the image next to the JSON, so atlases imported through Vite, webpack or another bundler that renames files now load. Pass the URLs your bundler gives you for the `.json` and `.png` (with Vite, `import metricsUrl from './my-font.json?url'` and `import imageUrl from './my-font.png'`), and look loaded atlases up with `get(metricsUrl)`. `getOrLoad` rejects if the image's size doesn't match the JSON's `atlasSize`, or if one `metricsUrl` is requested with two different image URLs. Concurrent calls for the same atlas now share one load. `FontAtlasCache` no longer implements `AssetCache` and its `load` method and `assets` map are no longer public; call `getOrLoad` instead. `FontAtlasData` and `FontAtlasFileData` no longer have an `atlasImage` field and `forge-generate-font-atlas` no longer writes one; existing JSON files that have it still load #### Removed diff --git a/assets/fonts/default/default.json b/assets/fonts/default/default.json index afc590a54..5a426250d 100644 --- a/assets/fonts/default/default.json +++ b/assets/fonts/default/default.json @@ -1,7 +1,6 @@ { "formatVersion": 2, "type": "msdf", - "atlasImage": "default.png", "atlasSize": { "width": 512, "height": 512 diff --git a/design/font-atlas-loading.md b/design/font-atlas-loading.md index d8329d366..7a29c97ea 100644 --- a/design/font-atlas-loading.md +++ b/design/font-atlas-loading.md @@ -2,7 +2,7 @@ | | | | ------------------------------------- | ------------------------------------------------------------------------------------------------ | -| **Status** | Draft, for review | +| **Status** | Implemented. `documentation-site/docs/docs/text` describes current behavior | | **Kind** | Defect | | **Found in** | Galactic Journey demo: `src/ui/create-ui.ts` (fonts served from `public/` to avoid hashed names) | | **Engine version at time of writing** | `0.25.8` | diff --git a/documentation-site/docs/docs/asset-loading/index.md b/documentation-site/docs/docs/asset-loading/index.md index a4f3cede4..f989e6933 100644 --- a/documentation-site/docs/docs/asset-loading/index.md +++ b/documentation-site/docs/docs/asset-loading/index.md @@ -12,10 +12,12 @@ and [`FontAtlasCache`](/Forge/docs/api/classes/FontAtlasCache) (see [Text](../text/index.md)), plus two supporting building blocks: - [`AssetCache`](/Forge/docs/api/interfaces/AssetCache): the common - `get` / `load` / `getOrLoad` contract that asset caches implement. - `ImageCache` and `FontAtlasCache` both implement it; if you add a cache - for another asset type (audio buffers, arbitrary JSON data), implement + `get` / `load` / `getOrLoad` contract for caches that load an asset from + a single URL, such as `ImageCache`. If you add a cache for another + single-file asset type (audio buffers, arbitrary JSON data), implement this interface so it behaves consistently with the rest of the engine. + `FontAtlasCache` loads each atlas from two URLs, so it has its own + `getOrLoad({ metricsUrl, imageUrl })` instead. - [`AssetRegistry`](/Forge/docs/api/classes/AssetRegistry): maps human-readable string IDs to compact numeric IDs, so hot-path code (like a per-frame animation system) can look up an asset by index instead of by diff --git a/documentation-site/docs/docs/text/generating-a-font-atlas.md b/documentation-site/docs/docs/text/generating-a-font-atlas.md index 3cea57d71..caea6c2ea 100644 --- a/documentation-site/docs/docs/text/generating-a-font-atlas.md +++ b/documentation-site/docs/docs/text/generating-a-font-atlas.md @@ -41,8 +41,10 @@ npm install -g msdf-bmfont-xml npx forge-generate-font-atlas --font my-font.ttf --charset ascii --out assets/fonts/my-font ``` -This writes `assets/fonts/my-font.png` and `assets/fonts/my-font.json`, which -[`FontAtlasCache`](./loading-a-font-atlas.md) loads together at runtime. +This writes `assets/fonts/my-font.png` and `assets/fonts/my-font.json`. +Load them with [`FontAtlasCache`](./loading-a-font-atlas.md), passing the +URL of each. Always replace both files together: the JSON's glyph +positions only match the image from the same run. | Flag | Default | Meaning | | -------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------- | diff --git a/documentation-site/docs/docs/text/index.md b/documentation-site/docs/docs/text/index.md index 6239e8c80..688d1606f 100644 --- a/documentation-site/docs/docs/text/index.md +++ b/documentation-site/docs/docs/text/index.md @@ -19,10 +19,8 @@ Three pieces make up the module: file, sized to your game's actual `charset`. - [`FontAtlasCache`](/Forge/docs/api/classes/FontAtlasCache), which loads that JSON/image pair at runtime into a - [`FontAtlas`](/Forge/docs/api/interfaces/FontAtlas), following the same - [`AssetCache`](/Forge/docs/api/interfaces/AssetCache) contract as the - rest of the engine's asset loading (see - [Asset Loading](../asset-loading/index.md)). + [`FontAtlas`](/Forge/docs/api/interfaces/FontAtlas) (see + [Loading a Font Atlas](./loading-a-font-atlas.md)). - [`addTextComponent`](/Forge/docs/api/functions/addTextComponent) and [`createTextShapingEcsSystem`](/Forge/docs/api/functions/createTextShapingEcsSystem), which draw a string from a loaded `FontAtlas` through the render system @@ -31,34 +29,44 @@ Three pieces make up the module: ## Quick start The engine ships a pre-generated default atlas (Liberation Sans, SIL Open -Font License 1.1) at `assets/fonts/default/` (`default.json`, `default.png`, -and a `License.txt` with the font's attribution) inside the -`@forge-game-engine/forge` package itself, so you can render text with zero -font setup - no `.ttf`, no `forge-generate-font-atlas` run, nothing to -license or commit yourself. `FontAtlasCache.getOrLoad` just needs those -three files reachable at a URL, the same as any atlas you generate -yourself, so copy (or have your build script copy) them from -`node_modules/@forge-game-engine/forge/assets/fonts/default/` into your -project's own served assets directory - most bundlers don't serve -`node_modules` directly: +Font License 1.1) inside the `@forge-game-engine/forge` package, so you can +render text with zero font setup - no `.ttf`, no `forge-generate-font-atlas` +run. Import its two files through the package's `fonts/default` exports and +pass the URLs your bundler gives you (this is Vite; see +[Loading a Font Atlas](./loading-a-font-atlas.md#importing-atlases-through-a-bundler) +for webpack): ```ts +import defaultFontMetricsUrl from '@forge-game-engine/forge/fonts/default/default.json?url'; +import defaultFontImageUrl from '@forge-game-engine/forge/fonts/default/default.png'; import { FontAtlasCache } from '@forge-game-engine/forge/text'; const fontAtlasCache = new FontAtlasCache(); -const fontAtlas = await fontAtlasCache.getOrLoad('assets/fonts/default.json'); +const fontAtlas = await fontAtlasCache.getOrLoad({ + metricsUrl: defaultFontMetricsUrl, + imageUrl: defaultFontImageUrl, +}); ``` +The font's attribution is in `assets/fonts/default/License.txt` in the +package. + When you need your own font (a different look, or characters the default atlas's ASCII charset doesn't cover), generate one: ```bash npm install --save-dev msdf-bmfont-xml -npx forge-generate-font-atlas --font my-font.ttf --charset ascii --out assets/fonts/my-font +npx forge-generate-font-atlas --font my-font.ttf --charset ascii --out src/fonts/my-font ``` ```ts -const fontAtlas = await fontAtlasCache.getOrLoad('assets/fonts/my-font.json'); +import myFontMetricsUrl from './fonts/my-font.json?url'; +import myFontImageUrl from './fonts/my-font.png'; + +const fontAtlas = await fontAtlasCache.getOrLoad({ + metricsUrl: myFontMetricsUrl, + imageUrl: myFontImageUrl, +}); console.log(fontAtlas.data.glyphs.get('A'.codePointAt(0)!)); ``` diff --git a/documentation-site/docs/docs/text/loading-a-font-atlas.md b/documentation-site/docs/docs/text/loading-a-font-atlas.md index 36beec8c3..d7135b5b7 100644 --- a/documentation-site/docs/docs/text/loading-a-font-atlas.md +++ b/documentation-site/docs/docs/text/loading-a-font-atlas.md @@ -5,26 +5,80 @@ sidebar_position: 2 # Loading a Font Atlas [`FontAtlasCache`](/Forge/docs/api/classes/FontAtlasCache) loads a -generated atlas's JSON metrics and PNG texture into a -[`FontAtlas`](/Forge/docs/api/interfaces/FontAtlas). +generated atlas's metrics JSON and PNG image into a +[`FontAtlas`](/Forge/docs/api/interfaces/FontAtlas). Pass it the URL of +each file: ```ts import { FontAtlasCache } from '@forge-game-engine/forge/text'; const fontAtlasCache = new FontAtlasCache(); -const fontAtlas = await fontAtlasCache.getOrLoad('assets/fonts/my-font.json'); +const fontAtlas = await fontAtlasCache.getOrLoad({ + metricsUrl: 'assets/fonts/my-font.json', + imageUrl: 'assets/fonts/my-font.png', +}); ``` -Any atlas works here, including the default one the engine ships at -`assets/fonts/default/default.json` (see [Text](./index.md)'s Quick start) - -there's nothing default-atlas-specific about loading it. +The two URLs are independent: the image doesn't have to be in the same +directory as the JSON, or keep its original file name. -## Cache keys point at the JSON file +## Importing atlases through a bundler -`getOrLoad('assets/fonts/my-font.json')` fetches that file, then loads -whatever image its `atlasImage` field names, resolved relative to the JSON -file itself (so the `.png` doesn't have to share the exact base name, -though `forge-generate-font-atlas` always names them to match). +Bundlers rename the assets they emit (`my-font.png` becomes something like +`my-font-3f2a9c.png`), so import both files and pass the URLs the bundler +gives you rather than writing paths by hand. + +With Vite, import the PNG directly and the JSON with `?url`, which gives +its URL instead of its parsed contents: + +```ts +import { FontAtlasCache } from '@forge-game-engine/forge/text'; +import myFontMetricsUrl from './fonts/my-font.json?url'; +import myFontImageUrl from './fonts/my-font.png'; + +const fontAtlasCache = new FontAtlasCache(); +const fontAtlas = await fontAtlasCache.getOrLoad({ + metricsUrl: myFontMetricsUrl, + imageUrl: myFontImageUrl, +}); +``` + +With webpack 5, use `new URL(path, import.meta.url)` for both files. The +path must be a string literal for webpack to find the file: + +```ts +const fontAtlas = await fontAtlasCache.getOrLoad({ + metricsUrl: new URL('./fonts/my-font.json', import.meta.url).href, + imageUrl: new URL('./fonts/my-font.png', import.meta.url).href, +}); +``` + +If your webpack config runs `file-loader` or `url-loader` on images +(Docusaurus does), those loaders also process `new URL` image requests, and +the emitted `.png` contains JavaScript instead of the image, so it fails to +load. Import the PNG instead (`import myFontImageUrl from +'./fonts/my-font.png'`) and keep `new URL` for the JSON. + +The engine's default font (see [Text](./index.md)'s Quick start) is +imported the same way, from the package's `fonts/default` exports: + +```ts +import defaultFontMetricsUrl from '@forge-game-engine/forge/fonts/default/default.json?url'; +import defaultFontImageUrl from '@forge-game-engine/forge/fonts/default/default.png'; +``` + +## Gotchas + +- **Keep the JSON and PNG from the same generator run.** `getOrLoad` + rejects if the image's size doesn't match the JSON's `atlasSize`. Two + different atlases generated at the same texture size still pass that + check, and render garbled glyphs, so regenerate and replace both files + together. +- **One image per metrics URL.** The cache is keyed by `metricsUrl`. + Requesting the same `metricsUrl` with a different `imageUrl` rejects. +- **Requesting an atlas again doesn't reload it.** Repeated and concurrent + `getOrLoad` calls for the same `metricsUrl` share one load, so the + `Promise.all` pattern in the worked example below fetches each file once. ## Reading glyph metrics @@ -73,8 +127,14 @@ import { FontAtlasCache } from '@forge-game-engine/forge/text'; const fontAtlasCache = new FontAtlasCache(); const [headingAtlas, bodyAtlas] = await Promise.all([ - fontAtlasCache.getOrLoad('assets/fonts/heading.json'), - fontAtlasCache.getOrLoad('assets/fonts/body.json'), + fontAtlasCache.getOrLoad({ + metricsUrl: 'assets/fonts/heading.json', + imageUrl: 'assets/fonts/heading.png', + }), + fontAtlasCache.getOrLoad({ + metricsUrl: 'assets/fonts/body.json', + imageUrl: 'assets/fonts/body.png', + }), ]); const capitalA = bodyAtlas.data.glyphs.get('A'.codePointAt(0)!); diff --git a/documentation-site/docs/docs/text/rendering-text.md b/documentation-site/docs/docs/text/rendering-text.md index 0be3fcfea..20a9d7147 100644 --- a/documentation-site/docs/docs/text/rendering-text.md +++ b/documentation-site/docs/docs/text/rendering-text.md @@ -23,7 +23,10 @@ import { } from '@forge-game-engine/forge/text'; const fontAtlasCache = new FontAtlasCache(); -const fontAtlas = await fontAtlasCache.getOrLoad('assets/fonts/body.json'); +const fontAtlas = await fontAtlasCache.getOrLoad({ + metricsUrl: 'assets/fonts/body.json', + imageUrl: 'assets/fonts/body.png', +}); const label = world.createEntity(); addPositionComponent(world, label, { local: { x: 400, y: 300 } }); diff --git a/documentation-site/docs/docs/ui/labels-and-text.md b/documentation-site/docs/docs/ui/labels-and-text.md index 7b6ef46ac..29299afb2 100644 --- a/documentation-site/docs/docs/ui/labels-and-text.md +++ b/documentation-site/docs/docs/ui/labels-and-text.md @@ -14,9 +14,10 @@ atlas): import { FontAtlasCache } from '@forge-game-engine/forge/text'; import { createLabel, UiAnchor } from '@forge-game-engine/forge/ui'; -const fontAtlas = await new FontAtlasCache().getOrLoad( - 'assets/fonts/default.json', -); +const fontAtlas = await new FontAtlasCache().getOrLoad({ + metricsUrl: 'assets/fonts/my-font.json', + imageUrl: 'assets/fonts/my-font.png', +}); createLabel(world, panel, { text: 'Play', diff --git a/documentation-site/src/pages/demos/layout-groups/_create-game.ts b/documentation-site/src/pages/demos/layout-groups/_create-game.ts index 218fc7a43..5c868d578 100644 --- a/documentation-site/src/pages/demos/layout-groups/_create-game.ts +++ b/documentation-site/src/pages/demos/layout-groups/_create-game.ts @@ -4,6 +4,7 @@ import { Time, } from '@forge-game-engine/forge/common'; import { EcsWorld } from '@forge-game-engine/forge/ecs'; +import defaultFontImageUrl from '@forge-game-engine/forge/fonts/default/default.png'; import { actionResetTypes, Axis2dAction, @@ -140,12 +141,9 @@ function createUiInputs( * (the Menu's buttons, the Options panel's Music slider and Fullscreen * toggle) is clickable and keyboard/gamepad-focus-navigable via * `createUiInputs`. - * @param fontAtlasUrl - The URL of the font atlas JSON to load. * @returns The created game. */ -export const createLayoutGroupsGame = async ( - fontAtlasUrl: string, -): Promise => { +export const createLayoutGroupsGame = async (): Promise => { const { game, world, renderContext, time } = createGame('demo-game'); const camera = createCamera(world, { @@ -157,7 +155,15 @@ export const createLayoutGroupsGame = async ( await createBackdrop(world, camera, renderContext); const fontAtlasCache = new FontAtlasCache(renderContext.imageCache); - const fontAtlas = await fontAtlasCache.getOrLoad(fontAtlasUrl); + const fontAtlas = await fontAtlasCache.getOrLoad({ + // Importing the JSON would give its parsed contents, so `new URL` asks + // webpack for its URL instead. + metricsUrl: new URL( + '@forge-game-engine/forge/fonts/default/default.json', + import.meta.url, + ).href, + imageUrl: defaultFontImageUrl, + }); const { mouseInputSource, submitInput, navigateInput } = createUiInputs( world, diff --git a/documentation-site/src/pages/demos/layout-groups/index.tsx b/documentation-site/src/pages/demos/layout-groups/index.tsx index 5f7f696bf..f8d96d3f0 100644 --- a/documentation-site/src/pages/demos/layout-groups/index.tsx +++ b/documentation-site/src/pages/demos/layout-groups/index.tsx @@ -1,5 +1,4 @@ -import React, { JSX, useCallback } from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import React, { JSX } from 'react'; import { createLayoutGroupsGame } from './_create-game'; import gameCode from '!!raw-loader!./_create-game'; import menuCode from '!!raw-loader!./_create-menu'; @@ -10,13 +9,6 @@ import optionsFormCode from '!!raw-loader!./_create-options-form'; import { Demo } from '@site/src/components/Demo'; export default function LayoutGroups(): JSX.Element { - const { siteConfig } = useDocusaurusContext(); - const fontAtlasUrl = `${siteConfig.baseUrl}fonts/default/default.json`; - const createGame = useCallback( - () => createLayoutGroupsGame(fontAtlasUrl), - [fontAtlasUrl], - ); - return ( void, ): Promise => { const { game, world, renderContext, time } = createGame('demo-game'); @@ -65,7 +64,15 @@ export const createTextGame = async ( }); const fontAtlasCache = new FontAtlasCache(renderContext.imageCache); - const fontAtlas = await fontAtlasCache.getOrLoad(fontAtlasUrl); + const fontAtlas = await fontAtlasCache.getOrLoad({ + // Importing the JSON would give its parsed contents, so `new URL` asks + // webpack for its URL instead. + metricsUrl: new URL( + '@forge-game-engine/forge/fonts/default/default.json', + import.meta.url, + ).href, + imageUrl: defaultFontImageUrl, + }); const whiteImage = await renderContext.imageCache.getOrLoad( getAssetUrl('img/White.png'), diff --git a/documentation-site/src/pages/demos/text/index.tsx b/documentation-site/src/pages/demos/text/index.tsx index edfbfb1c8..6c378b5ed 100644 --- a/documentation-site/src/pages/demos/text/index.tsx +++ b/documentation-site/src/pages/demos/text/index.tsx @@ -1,5 +1,4 @@ import React, { JSX, useCallback, useRef, useState } from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import { createTextGame } from './_create-game'; import gameCode from '!!raw-loader!./_create-game'; import createGuideBoxCode from '!!raw-loader!./_create-guide-box'; @@ -25,9 +24,6 @@ import { import { PlaygroundControls } from './_PlaygroundControls'; export default function Text(): JSX.Element { - const { siteConfig } = useDocusaurusContext(); - const fontAtlasUrl = `${siteConfig.baseUrl}fonts/default/default.json`; - const playgroundRef = useRef(null); const [text, setText] = useState(playgroundDefaults.text); const [size, setSize] = useState(playgroundDefaults.size); @@ -59,10 +55,10 @@ export default function Text(): JSX.Element { const createGame = useCallback( () => - createTextGame(fontAtlasUrl, (playground) => { + createTextGame((playground) => { playgroundRef.current = playground; }), - [fontAtlasUrl], + [], ); const handleTextChange = (value: string) => { diff --git a/documentation-site/src/pages/demos/ui-anchors/_create-game.ts b/documentation-site/src/pages/demos/ui-anchors/_create-game.ts index 69fd5441f..1dd107e10 100644 --- a/documentation-site/src/pages/demos/ui-anchors/_create-game.ts +++ b/documentation-site/src/pages/demos/ui-anchors/_create-game.ts @@ -3,6 +3,7 @@ import { createTransformEcsSystem, } from '@forge-game-engine/forge/common'; import { EcsWorld } from '@forge-game-engine/forge/ecs'; +import defaultFontImageUrl from '@forge-game-engine/forge/fonts/default/default.png'; import { addSpriteComponent, Color, @@ -92,13 +93,11 @@ const playgroundTintColor = new Color(0.95, 0.55, 0.2, 1); * aspect ratio, so toggling fullscreen (or dragging the playground's * controls) keeps each one exactly where its anchor says it should be, at * any window shape. - * @param fontAtlasUrl - The URL of the font atlas JSON to load. * @param onPlaygroundReady - Called once the playground panel's live * components exist, so the page can wire its controls up to them. * @returns The created game. */ export const createAnchorsGame = async ( - fontAtlasUrl: string, onPlaygroundReady?: (playground: AnchorPlayground) => void, ): Promise => { const { game, world, renderContext, time } = createGame('demo-game'); @@ -110,7 +109,15 @@ export const createAnchorsGame = async ( }); const fontAtlasCache = new FontAtlasCache(renderContext.imageCache); - const fontAtlas = await fontAtlasCache.getOrLoad(fontAtlasUrl); + const fontAtlas = await fontAtlasCache.getOrLoad({ + // Importing the JSON would give its parsed contents, so `new URL` asks + // webpack for its URL instead. + metricsUrl: new URL( + '@forge-game-engine/forge/fonts/default/default.json', + import.meta.url, + ).href, + imageUrl: defaultFontImageUrl, + }); registerUiSystems(world, renderContext, time); diff --git a/documentation-site/src/pages/demos/ui-anchors/index.tsx b/documentation-site/src/pages/demos/ui-anchors/index.tsx index 27b01d4dc..dce53b345 100644 --- a/documentation-site/src/pages/demos/ui-anchors/index.tsx +++ b/documentation-site/src/pages/demos/ui-anchors/index.tsx @@ -1,5 +1,4 @@ import React, { JSX, useCallback, useRef, useState } from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import { createAnchorsGame } from './_create-game'; import gameCode from '!!raw-loader!./_create-game'; import createAnchorPlaygroundCode from '!!raw-loader!./_create-anchor-playground'; @@ -30,9 +29,6 @@ const badgeStyle: React.CSSProperties = { }; export default function UiAnchors(): JSX.Element { - const { siteConfig } = useDocusaurusContext(); - const fontAtlasUrl = `${siteConfig.baseUrl}fonts/default/default.json`; - const playgroundRef = useRef(null); const [presetName, setPresetName] = useState( anchorPlaygroundDefaults.presetName, @@ -52,10 +48,10 @@ export default function UiAnchors(): JSX.Element { const createGame = useCallback( () => - createAnchorsGame(fontAtlasUrl, (playground) => { + createAnchorsGame((playground) => { playgroundRef.current = playground; }), - [fontAtlasUrl], + [], ); const handlePresetNameChange = (value: AnchorPlaygroundPresetName) => { diff --git a/documentation-site/src/pages/demos/ui-button/_create-game.ts b/documentation-site/src/pages/demos/ui-button/_create-game.ts index 3b4f529c0..bda15acc9 100644 --- a/documentation-site/src/pages/demos/ui-button/_create-game.ts +++ b/documentation-site/src/pages/demos/ui-button/_create-game.ts @@ -4,6 +4,7 @@ import { Time, } from '@forge-game-engine/forge/common'; import { EcsWorld } from '@forge-game-engine/forge/ecs'; +import defaultFontImageUrl from '@forge-game-engine/forge/fonts/default/default.png'; import { actionResetTypes, Axis2dAction, @@ -140,10 +141,9 @@ function createUiInputs( * path, updating a status label with which one was last invoked. Hovering * a button also focuses it, so the highlight follows the mouse the same * way it follows the keyboard. - * @param fontAtlasUrl - The URL of the font atlas JSON to load. * @returns The created game. */ -export const createButtonGame = async (fontAtlasUrl: string): Promise => { +export const createButtonGame = async (): Promise => { const { game, world, renderContext, time } = createGame('demo-game'); const camera = createCamera(world, { @@ -155,7 +155,15 @@ export const createButtonGame = async (fontAtlasUrl: string): Promise => { await createBackdrop(world, camera, renderContext); const fontAtlasCache = new FontAtlasCache(renderContext.imageCache); - const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad(fontAtlasUrl); + const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad({ + // Importing the JSON would give its parsed contents, so `new URL` asks + // webpack for its URL instead. + metricsUrl: new URL( + '@forge-game-engine/forge/fonts/default/default.json', + import.meta.url, + ).href, + imageUrl: defaultFontImageUrl, + }); const { mouseInputSource, submitInput, navigateInput } = createUiInputs( world, diff --git a/documentation-site/src/pages/demos/ui-button/index.tsx b/documentation-site/src/pages/demos/ui-button/index.tsx index 2157708e3..abf8a40ea 100644 --- a/documentation-site/src/pages/demos/ui-button/index.tsx +++ b/documentation-site/src/pages/demos/ui-button/index.tsx @@ -1,5 +1,4 @@ -import React, { JSX, useCallback } from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import React, { JSX } from 'react'; import { createButtonGame } from './_create-game'; import gameCode from '!!raw-loader!./_create-game'; @@ -19,13 +18,6 @@ const badgeStyle: React.CSSProperties = { }; export default function UiButton(): JSX.Element { - const { siteConfig } = useDocusaurusContext(); - const fontAtlasUrl = `${siteConfig.baseUrl}fonts/default/default.json`; - const createGame = useCallback( - () => createButtonGame(fontAtlasUrl), - [fontAtlasUrl], - ); - return ( => { +export const createCanvasGroupGame = async (): Promise => { const { game, world, renderContext, time } = createGame('demo-game'); const camera = createCamera(world, { @@ -111,7 +109,15 @@ export const createCanvasGroupGame = async ( await createBackdrop(world, camera, renderContext); const fontAtlasCache = new FontAtlasCache(renderContext.imageCache); - const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad(fontAtlasUrl); + const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad({ + // Importing the JSON would give its parsed contents, so `new URL` asks + // webpack for its URL instead. + metricsUrl: new URL( + '@forge-game-engine/forge/fonts/default/default.json', + import.meta.url, + ).href, + imageUrl: defaultFontImageUrl, + }); const mouseInputSource = createPointerInput(world, time, game); diff --git a/documentation-site/src/pages/demos/ui-canvas-group/index.tsx b/documentation-site/src/pages/demos/ui-canvas-group/index.tsx index 204935b1d..5b63f3409 100644 --- a/documentation-site/src/pages/demos/ui-canvas-group/index.tsx +++ b/documentation-site/src/pages/demos/ui-canvas-group/index.tsx @@ -1,5 +1,4 @@ -import React, { JSX, useCallback } from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import React, { JSX } from 'react'; import { createCanvasGroupGame } from './_create-game'; import gameCode from '!!raw-loader!./_create-game'; @@ -18,13 +17,6 @@ const badgeStyle: React.CSSProperties = { }; export default function UiCanvasGroup(): JSX.Element { - const { siteConfig } = useDocusaurusContext(); - const fontAtlasUrl = `${siteConfig.baseUrl}fonts/default/default.json`; - const createGame = useCallback( - () => createCanvasGroupGame(fontAtlasUrl), - [fontAtlasUrl], - ); - return ( => { +export const createDropdownGame = async (): Promise => { const { game, world, renderContext, time } = createGame('demo-game'); const camera = createCamera(world, { @@ -102,7 +100,15 @@ export const createDropdownGame = async ( await createBackdrop(world, camera, renderContext); const fontAtlasCache = new FontAtlasCache(renderContext.imageCache); - const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad(fontAtlasUrl); + const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad({ + // Importing the JSON would give its parsed contents, so `new URL` asks + // webpack for its URL instead. + metricsUrl: new URL( + '@forge-game-engine/forge/fonts/default/default.json', + import.meta.url, + ).href, + imageUrl: defaultFontImageUrl, + }); const mouseInputSource = createPointerInput(world, time, game); diff --git a/documentation-site/src/pages/demos/ui-dropdown/index.tsx b/documentation-site/src/pages/demos/ui-dropdown/index.tsx index 95dd96268..501cbd892 100644 --- a/documentation-site/src/pages/demos/ui-dropdown/index.tsx +++ b/documentation-site/src/pages/demos/ui-dropdown/index.tsx @@ -1,5 +1,4 @@ -import React, { JSX, useCallback } from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import React, { JSX } from 'react'; import { createDropdownGame } from './_create-game'; import gameCode from '!!raw-loader!./_create-game'; @@ -18,13 +17,6 @@ const badgeStyle: React.CSSProperties = { }; export default function UiDropdown(): JSX.Element { - const { siteConfig } = useDocusaurusContext(); - const fontAtlasUrl = `${siteConfig.baseUrl}fonts/default/default.json`; - const createGame = useCallback( - () => createDropdownGame(fontAtlasUrl), - [fontAtlasUrl], - ); - return ( => { +export const createUiMainMenuGame = async (): Promise => { const { game, world, renderContext, time } = createGame('demo-game'); const camera = createCamera(world, { @@ -168,7 +166,15 @@ export const createUiMainMenuGame = async ( await createBackdrop(world, camera, renderContext); const fontAtlasCache = new FontAtlasCache(renderContext.imageCache); - const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad(fontAtlasUrl); + const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad({ + // Importing the JSON would give its parsed contents, so `new URL` asks + // webpack for its URL instead. + metricsUrl: new URL( + '@forge-game-engine/forge/fonts/default/default.json', + import.meta.url, + ).href, + imageUrl: defaultFontImageUrl, + }); const { mouseInputSource, submitInput, navigateInput } = createUiInputs( world, diff --git a/documentation-site/src/pages/demos/ui-main-menu/index.tsx b/documentation-site/src/pages/demos/ui-main-menu/index.tsx index 58e9b8196..ce3a45bca 100644 --- a/documentation-site/src/pages/demos/ui-main-menu/index.tsx +++ b/documentation-site/src/pages/demos/ui-main-menu/index.tsx @@ -1,5 +1,4 @@ -import React, { JSX, useCallback } from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import React, { JSX } from 'react'; import { createUiMainMenuGame } from './_create-game'; import gameCode from '!!raw-loader!./_create-game'; import mainMenuCode from '!!raw-loader!./_create-main-menu'; @@ -23,13 +22,6 @@ const badgeStyle: React.CSSProperties = { }; export default function UiMainMenu(): JSX.Element { - const { siteConfig } = useDocusaurusContext(); - const fontAtlasUrl = `${siteConfig.baseUrl}fonts/default/default.json`; - const createGame = useCallback( - () => createUiMainMenuGame(fontAtlasUrl), - [fontAtlasUrl], - ); - return ( => { +export const createNestedResizeGame = async (): Promise => { const { game, world, renderContext, time } = createGame('demo-game'); createCamera(world, { @@ -94,7 +89,15 @@ export const createNestedResizeGame = async ( }); const fontAtlasCache = new FontAtlasCache(renderContext.imageCache); - const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad(fontAtlasUrl); + const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad({ + // Importing the JSON would give its parsed contents, so `new URL` asks + // webpack for its URL instead. + metricsUrl: new URL( + '@forge-game-engine/forge/fonts/default/default.json', + import.meta.url, + ).href, + imageUrl: defaultFontImageUrl, + }); const mouseInputSource = createPointerInput(world, time, game); diff --git a/documentation-site/src/pages/demos/ui-nested-resize/index.tsx b/documentation-site/src/pages/demos/ui-nested-resize/index.tsx index bfa53e5bc..9abc74d5f 100644 --- a/documentation-site/src/pages/demos/ui-nested-resize/index.tsx +++ b/documentation-site/src/pages/demos/ui-nested-resize/index.tsx @@ -1,5 +1,4 @@ -import React, { JSX, useCallback } from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import React, { JSX } from 'react'; import { createNestedResizeGame } from './_create-game'; import gameCode from '!!raw-loader!./_create-game'; import liveMotionComponentCode from '!!raw-loader!./_live-motion.component'; @@ -8,13 +7,6 @@ import liveMotionSystemCode from '!!raw-loader!./_live-motion.system'; import { Demo } from '@site/src/components/Demo'; export default function UiNestedResize(): JSX.Element { - const { siteConfig } = useDocusaurusContext(); - const fontAtlasUrl = `${siteConfig.baseUrl}fonts/default/default.json`; - const createGame = useCallback( - () => createNestedResizeGame(fontAtlasUrl), - [fontAtlasUrl], - ); - return ( => { +export const createProgressBarGame = async (): Promise => { const { game, world, renderContext, time } = createGame('demo-game'); const camera = createCamera(world, { @@ -90,7 +88,15 @@ export const createProgressBarGame = async ( await createBackdrop(world, camera, renderContext); const fontAtlasCache = new FontAtlasCache(renderContext.imageCache); - const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad(fontAtlasUrl); + const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad({ + // Importing the JSON would give its parsed contents, so `new URL` asks + // webpack for its URL instead. + metricsUrl: new URL( + '@forge-game-engine/forge/fonts/default/default.json', + import.meta.url, + ).href, + imageUrl: defaultFontImageUrl, + }); registerUiSystems(world, renderContext, time); diff --git a/documentation-site/src/pages/demos/ui-progress-bar/index.tsx b/documentation-site/src/pages/demos/ui-progress-bar/index.tsx index 1b1f7280d..8a6f1d96a 100644 --- a/documentation-site/src/pages/demos/ui-progress-bar/index.tsx +++ b/documentation-site/src/pages/demos/ui-progress-bar/index.tsx @@ -1,5 +1,4 @@ -import React, { JSX, useCallback } from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import React, { JSX } from 'react'; import { createProgressBarGame } from './_create-game'; import gameCode from '!!raw-loader!./_create-game'; import pulseComponentCode from '!!raw-loader!./_pulse.component'; @@ -8,13 +7,6 @@ import pulseSystemCode from '!!raw-loader!./_pulse.system'; import { Demo } from '@site/src/components/Demo'; export default function UiProgressBar(): JSX.Element { - const { siteConfig } = useDocusaurusContext(); - const fontAtlasUrl = `${siteConfig.baseUrl}fonts/default/default.json`; - const createGame = useCallback( - () => createProgressBarGame(fontAtlasUrl), - [fontAtlasUrl], - ); - return ( => { +export const createSliderGame = async (): Promise => { const { game, world, renderContext, time } = createGame('demo-game'); createCamera(world, { @@ -87,7 +87,15 @@ export const createSliderGame = async (fontAtlasUrl: string): Promise => { await createBackdrop(world, renderContext); const fontAtlasCache = new FontAtlasCache(renderContext.imageCache); - const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad(fontAtlasUrl); + const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad({ + // Importing the JSON would give its parsed contents, so `new URL` asks + // webpack for its URL instead. + metricsUrl: new URL( + '@forge-game-engine/forge/fonts/default/default.json', + import.meta.url, + ).href, + imageUrl: defaultFontImageUrl, + }); const mouseInputSource = createPointerInput(world, time, game); diff --git a/documentation-site/src/pages/demos/ui-slider/index.tsx b/documentation-site/src/pages/demos/ui-slider/index.tsx index 521f17cc8..ad8063ca6 100644 --- a/documentation-site/src/pages/demos/ui-slider/index.tsx +++ b/documentation-site/src/pages/demos/ui-slider/index.tsx @@ -1,5 +1,4 @@ -import React, { JSX, useCallback } from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import React, { JSX } from 'react'; import { createSliderGame } from './_create-game'; import gameCode from '!!raw-loader!./_create-game'; @@ -18,13 +17,6 @@ const badgeStyle: React.CSSProperties = { }; export default function UiSlider(): JSX.Element { - const { siteConfig } = useDocusaurusContext(); - const fontAtlasUrl = `${siteConfig.baseUrl}fonts/default/default.json`; - const createGame = useCallback( - () => createSliderGame(fontAtlasUrl), - [fontAtlasUrl], - ); - return ( => { +export const createUiStressTestGame = async (): Promise => { const { game, world, renderContext, time } = createGame('demo-game'); const camera = createCamera(world, { @@ -103,7 +101,15 @@ export const createUiStressTestGame = async ( await createBackdrop(world, camera, renderContext); const fontAtlasCache = new FontAtlasCache(renderContext.imageCache); - const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad(fontAtlasUrl); + const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad({ + // Importing the JSON would give its parsed contents, so `new URL` asks + // webpack for its URL instead. + metricsUrl: new URL( + '@forge-game-engine/forge/fonts/default/default.json', + import.meta.url, + ).href, + imageUrl: defaultFontImageUrl, + }); registerUiSystems(world, renderContext, time); diff --git a/documentation-site/src/pages/demos/ui-stress-test/index.tsx b/documentation-site/src/pages/demos/ui-stress-test/index.tsx index d80e702ba..928d5d84c 100644 --- a/documentation-site/src/pages/demos/ui-stress-test/index.tsx +++ b/documentation-site/src/pages/demos/ui-stress-test/index.tsx @@ -1,5 +1,4 @@ -import React, { JSX, useCallback } from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import React, { JSX } from 'react'; import { createUiStressTestGame } from './_create-game'; import gameCode from '!!raw-loader!./_create-game'; import spawnerComponentCode from '!!raw-loader!./_stress-test-spawner.component'; @@ -9,13 +8,6 @@ import fpsMonitorSystemCode from '!!raw-loader!./_fps-monitor.system'; import { Demo } from '@site/src/components/Demo'; export default function UiStressTest(): JSX.Element { - const { siteConfig } = useDocusaurusContext(); - const fontAtlasUrl = `${siteConfig.baseUrl}fonts/default/default.json`; - const createGame = useCallback( - () => createUiStressTestGame(fontAtlasUrl), - [fontAtlasUrl], - ); - return ( => { +export const createToggleGame = async (): Promise => { const { game, world, renderContext, time } = createGame('demo-game'); const camera = createCamera(world, { @@ -105,7 +105,15 @@ export const createToggleGame = async (fontAtlasUrl: string): Promise => { await createBackdrop(world, camera, renderContext); const fontAtlasCache = new FontAtlasCache(renderContext.imageCache); - const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad(fontAtlasUrl); + const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad({ + // Importing the JSON would give its parsed contents, so `new URL` asks + // webpack for its URL instead. + metricsUrl: new URL( + '@forge-game-engine/forge/fonts/default/default.json', + import.meta.url, + ).href, + imageUrl: defaultFontImageUrl, + }); const mouseInputSource = createPointerInput(world, time, game); diff --git a/documentation-site/src/pages/demos/ui-toggle/index.tsx b/documentation-site/src/pages/demos/ui-toggle/index.tsx index 7744ed6db..c5adece94 100644 --- a/documentation-site/src/pages/demos/ui-toggle/index.tsx +++ b/documentation-site/src/pages/demos/ui-toggle/index.tsx @@ -1,5 +1,4 @@ -import React, { JSX, useCallback } from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import React, { JSX } from 'react'; import { createToggleGame } from './_create-game'; import gameCode from '!!raw-loader!./_create-game'; @@ -18,13 +17,6 @@ const badgeStyle: React.CSSProperties = { }; export default function UiToggle(): JSX.Element { - const { siteConfig } = useDocusaurusContext(); - const fontAtlasUrl = `${siteConfig.baseUrl}fonts/default/default.json`; - const createGame = useCallback( - () => createToggleGame(fontAtlasUrl), - [fontAtlasUrl], - ); - return ( { * the enemy. The right enemy's has no parent and is placed above it with its * `anchoredPosition`, so it stays upright regardless of which way the enemy * is facing. - * @param fontAtlasUrl - The URL of the font atlas JSON to load. * @returns The created game. */ -export const createWorldSpaceCanvasGame = async ( - fontAtlasUrl: string, -): Promise => { +export const createWorldSpaceCanvasGame = async (): Promise => { const { game, world, renderContext, time } = createGame('demo-game'); const worldCamera = createCamera(world, { @@ -237,7 +235,15 @@ export const createWorldSpaceCanvasGame = async ( await createBackdrop(world, worldCamera, renderContext); const fontAtlasCache = new FontAtlasCache(renderContext.imageCache); - const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad(fontAtlasUrl); + const fontAtlas: FontAtlas = await fontAtlasCache.getOrLoad({ + // Importing the JSON would give its parsed contents, so `new URL` asks + // webpack for its URL instead. + metricsUrl: new URL( + '@forge-game-engine/forge/fonts/default/default.json', + import.meta.url, + ).href, + imageUrl: defaultFontImageUrl, + }); // Registered once for the whole world, before either // createSpinningEnemyWithHealthBar call creates its own canvas below - diff --git a/documentation-site/src/pages/demos/ui-world-space-canvas/index.tsx b/documentation-site/src/pages/demos/ui-world-space-canvas/index.tsx index 861127134..730021a75 100644 --- a/documentation-site/src/pages/demos/ui-world-space-canvas/index.tsx +++ b/documentation-site/src/pages/demos/ui-world-space-canvas/index.tsx @@ -1,18 +1,10 @@ -import React, { JSX, useCallback } from 'react'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import React, { JSX } from 'react'; import { createWorldSpaceCanvasGame } from './_create-game'; import gameCode from '!!raw-loader!./_create-game'; import { Demo } from '@site/src/components/Demo'; export default function UiWorldSpaceCanvas(): JSX.Element { - const { siteConfig } = useDocusaurusContext(); - const fontAtlasUrl = `${siteConfig.baseUrl}fonts/default/default.json`; - const createGame = useCallback( - () => createWorldSpaceCanvasGame(fontAtlasUrl), - [fontAtlasUrl], - ); - return ( ); diff --git a/documentation-site/src/utils/image-modules.d.ts b/documentation-site/src/utils/image-modules.d.ts new file mode 100644 index 000000000..15d1b9334 --- /dev/null +++ b/documentation-site/src/utils/image-modules.d.ts @@ -0,0 +1,6 @@ +// Docusaurus's webpack config emits imported images as files and gives the +// importing module their URL. +declare module '*.png' { + const url: string; + export default url; +} diff --git a/documentation-site/static/fonts/default/License.txt b/documentation-site/static/fonts/default/License.txt deleted file mode 100644 index 793a2791d..000000000 --- a/documentation-site/static/fonts/default/License.txt +++ /dev/null @@ -1,102 +0,0 @@ - Liberation Sans (MSDF atlas, Forge's shipped default font atlas) - - Font: Liberation Sans Regular - Digitized data copyright (c) 2010 Google Corporation with Reserved Font Arimo, Tinos and Cousine. - Copyright (c) 2012 Red Hat, Inc. with Reserved Font Name Liberation. - Source: https://github.com/liberationfonts - - default.png and default.json in this directory are a multi-channel - signed distance field (MSDF) atlas generated from this font via - `npm run generate-font-atlas` (see scripts/generate-font-atlas.mjs), so - `@forge-game-engine/forge/text` has a font to render with out of the box, - with zero asset setup required. - - ------------------------------ - - License: SIL Open Font License, Version 1.1 - https://scripts.sil.org/OFL - - SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 - - PREAMBLE - The goals of the Open Font License (OFL) are to stimulate worldwide - development of collaborative font projects, to support the font creation - efforts of academic and linguistic communities, and to provide a free and - open framework in which fonts may be shared and improved in partnership - with others. - - The OFL allows the licensed fonts to be used, studied, modified and - redistributed freely as long as they are not sold by themselves. The - fonts, including any derivative works, can be bundled, embedded, - redistributed and/or sold with any software provided that any reserved - names are not used by derivative works. The fonts and derivatives, - however, cannot be released under any other type of license. The - requirement for fonts to remain under this license does not apply to any - document created using the fonts or their derivatives. - - DEFINITIONS - "Font Software" refers to the set of files released by the Copyright - Holder(s) under this license and clearly marked as such. This may include - source files, build scripts and documentation. - - "Reserved Font Name" refers to any names specified as such after the - copyright statement(s). - - "Original Version" refers to the collection of Font Software components - as distributed by the Copyright Holder(s). - - "Modified Version" refers to any derivative made by adding to, deleting, - or substituting, in part or in whole, any of the components of the - Original Version, by changing formats or by porting the Font Software to - a new environment. - - "Author" refers to any designer, engineer, programmer, technical writer - or other person who contributed to the Font Software. - - PERMISSION & CONDITIONS - Permission is hereby granted, free of charge, to any person obtaining a - copy of the Font Software, to use, study, copy, merge, embed, modify, - redistribute, and sell modified and unmodified copies of the Font - Software, subject to the following conditions: - - 1) Neither the Font Software nor any of its individual components, in - Original or Modified Versions, may be sold by itself. - - 2) Original or Modified Versions of the Font Software may be bundled, - redistributed and/or sold with any software, provided that each copy - contains the above copyright notice and this license. These can be - included either as stand-alone text files, human-readable headers or - in the appropriate machine-readable metadata fields within text or - binary files as long as those fields can be easily viewed by the user. - - 3) No Modified Version of the Font Software may use the Reserved Font - Name(s) unless explicit written permission is granted by the - corresponding Copyright Holder. This restriction only applies to the - primary font name as presented to the users. - - 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font - Software shall not be used to promote, endorse or advertise any - Modified Version, except to acknowledge the contribution(s) of the - Copyright Holder(s) and the Author(s) or with their explicit written - permission. - - 5) The Font Software, modified or unmodified, in part or in whole, must - be distributed entirely under this license, and must not be - distributed under any other license. The requirement for fonts to - remain under this license does not apply to any document created - using the Font Software. - - TERMINATION - This license becomes null and void if any of the above conditions are - not met. - - DISCLAIMER - THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF - COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE - COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL - DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER - DEALINGS IN THE FONT SOFTWARE. diff --git a/documentation-site/static/fonts/default/default.json b/documentation-site/static/fonts/default/default.json deleted file mode 100644 index afc590a54..000000000 --- a/documentation-site/static/fonts/default/default.json +++ /dev/null @@ -1,1529 +0,0 @@ -{ - "formatVersion": 2, - "type": "msdf", - "atlasImage": "default.png", - "atlasSize": { - "width": 512, - "height": 512 - }, - "distanceRange": 16, - "metrics": { - "lineHeight": 1.0952380952380953, - "ascender": 0.9285714285714286, - "descender": -0.40476190476190477, - "capHeight": 0.8809523809523809 - }, - "glyphs": [ - { - "codePoint": 40, - "advance": 0.3333333333333333, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.9047619047619048, - "right": 0.5238095238095238, - "bottom": -0.40476190476190477 - }, - "atlasBounds": { - "left": 0, - "top": 1, - "right": 0.052734375, - "bottom": 0.892578125 - } - }, - { - "codePoint": 41, - "advance": 0.3333333333333333, - "planeBounds": { - "left": -0.19047619047619047, - "top": 0.9047619047619048, - "right": 0.4523809523809524, - "bottom": -0.40476190476190477 - }, - "atlasBounds": { - "left": 0, - "top": 0.890625, - "right": 0.052734375, - "bottom": 0.783203125 - } - }, - { - "codePoint": 87, - "advance": 0.9523809523809523, - "planeBounds": { - "left": -0.19047619047619047, - "top": 0.8809523809523809, - "right": 1.119047619047619, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0, - "top": 0.78125, - "right": 0.107421875, - "bottom": 0.693359375 - } - }, - { - "codePoint": 91, - "advance": 0.2857142857142857, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.9047619047619048, - "right": 0.4523809523809524, - "bottom": -0.40476190476190477 - }, - "atlasBounds": { - "left": 0.0546875, - "top": 1, - "right": 0.1015625, - "bottom": 0.892578125 - } - }, - { - "codePoint": 93, - "advance": 0.2857142857142857, - "planeBounds": { - "left": -0.19047619047619047, - "top": 0.9047619047619048, - "right": 0.38095238095238093, - "bottom": -0.40476190476190477 - }, - "atlasBounds": { - "left": 0.0546875, - "top": 0.890625, - "right": 0.1015625, - "bottom": 0.783203125 - } - }, - { - "codePoint": 106, - "advance": 0.21428571428571427, - "planeBounds": { - "left": -0.21428571428571427, - "top": 0.9047619047619048, - "right": 0.35714285714285715, - "bottom": -0.40476190476190477 - }, - "atlasBounds": { - "left": 0.103515625, - "top": 1, - "right": 0.150390625, - "bottom": 0.892578125 - } - }, - { - "codePoint": 123, - "advance": 0.3333333333333333, - "planeBounds": { - "left": -0.16666666666666666, - "top": 0.9047619047619048, - "right": 0.5238095238095238, - "bottom": -0.40476190476190477 - }, - "atlasBounds": { - "left": 0.103515625, - "top": 0.890625, - "right": 0.16015625, - "bottom": 0.783203125 - } - }, - { - "codePoint": 124, - "advance": 0.2619047619047619, - "planeBounds": { - "left": -0.09523809523809523, - "top": 0.9047619047619048, - "right": 0.35714285714285715, - "bottom": -0.40476190476190477 - }, - "atlasBounds": { - "left": 0.15234375, - "top": 1, - "right": 0.189453125, - "bottom": 0.892578125 - } - }, - { - "codePoint": 125, - "advance": 0.3333333333333333, - "planeBounds": { - "left": -0.16666666666666666, - "top": 0.9047619047619048, - "right": 0.5238095238095238, - "bottom": -0.40476190476190477 - }, - "atlasBounds": { - "left": 0, - "top": 0.69140625, - "right": 0.056640625, - "bottom": 0.583984375 - } - }, - { - "codePoint": 81, - "advance": 0.7857142857142857, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.8809523809523809, - "right": 0.9285714285714286, - "bottom": -0.38095238095238093 - }, - "atlasBounds": { - "left": 0, - "top": 0.58203125, - "right": 0.087890625, - "bottom": 0.478515625 - } - }, - { - "codePoint": 64, - "advance": 1.0238095238095237, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.9047619047619048, - "right": 1.119047619047619, - "bottom": -0.3333333333333333 - }, - "atlasBounds": { - "left": 0.05859375, - "top": 0.69140625, - "right": 0.16015625, - "bottom": 0.58984375 - } - }, - { - "codePoint": 36, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.19047619047619047, - "top": 0.9285714285714286, - "right": 0.7142857142857143, - "bottom": -0.2619047619047619 - }, - "atlasBounds": { - "left": 0, - "top": 0.4765625, - "right": 0.07421875, - "bottom": 0.37890625 - } - }, - { - "codePoint": 37, - "advance": 0.8809523809523809, - "planeBounds": { - "left": -0.16666666666666666, - "top": 0.8809523809523809, - "right": 1.0238095238095237, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.109375, - "top": 0.78125, - "right": 0.20703125, - "bottom": 0.693359375 - } - }, - { - "codePoint": 47, - "advance": 0.2857142857142857, - "planeBounds": { - "left": -0.19047619047619047, - "top": 0.9047619047619048, - "right": 0.47619047619047616, - "bottom": -0.21428571428571427 - }, - "atlasBounds": { - "left": 0.162109375, - "top": 0.890625, - "right": 0.216796875, - "bottom": 0.798828125 - } - }, - { - "codePoint": 92, - "advance": 0.2857142857142857, - "planeBounds": { - "left": -0.19047619047619047, - "top": 0.9047619047619048, - "right": 0.47619047619047616, - "bottom": -0.21428571428571427 - }, - "atlasBounds": { - "left": 0.19140625, - "top": 1, - "right": 0.24609375, - "bottom": 0.908203125 - } - }, - { - "codePoint": 98, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.9047619047619048, - "right": 0.7142857142857143, - "bottom": -0.21428571428571427 - }, - "atlasBounds": { - "left": 0, - "top": 0.376953125, - "right": 0.068359375, - "bottom": 0.28515625 - } - }, - { - "codePoint": 100, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.9047619047619048, - "right": 0.6904761904761905, - "bottom": -0.21428571428571427 - }, - "atlasBounds": { - "left": 0, - "top": 0.283203125, - "right": 0.068359375, - "bottom": 0.19140625 - } - }, - { - "codePoint": 103, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.7380952380952381, - "right": 0.6904761904761905, - "bottom": -0.38095238095238093 - }, - "atlasBounds": { - "left": 0, - "top": 0.189453125, - "right": 0.068359375, - "bottom": 0.09765625 - } - }, - { - "codePoint": 112, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.7380952380952381, - "right": 0.7142857142857143, - "bottom": -0.38095238095238093 - }, - "atlasBounds": { - "left": 0, - "top": 0.095703125, - "right": 0.068359375, - "bottom": 0.00390625 - } - }, - { - "codePoint": 113, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.7380952380952381, - "right": 0.6904761904761905, - "bottom": -0.38095238095238093 - }, - "atlasBounds": { - "left": 0.0703125, - "top": 0.376953125, - "right": 0.138671875, - "bottom": 0.28515625 - } - }, - { - "codePoint": 121, - "advance": 0.5, - "planeBounds": { - "left": -0.19047619047619047, - "top": 0.7142857142857143, - "right": 0.6904761904761905, - "bottom": -0.40476190476190477 - }, - "atlasBounds": { - "left": 0.076171875, - "top": 0.4765625, - "right": 0.1484375, - "bottom": 0.384765625 - } - }, - { - "codePoint": 48, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.8809523809523809, - "right": 0.7142857142857143, - "bottom": -0.21428571428571427 - }, - "atlasBounds": { - "left": 0.08984375, - "top": 0.587890625, - "right": 0.16015625, - "bottom": 0.498046875 - } - }, - { - "codePoint": 51, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.8809523809523809, - "right": 0.7142857142857143, - "bottom": -0.21428571428571427 - }, - "atlasBounds": { - "left": 0.0703125, - "top": 0.283203125, - "right": 0.140625, - "bottom": 0.193359375 - } - }, - { - "codePoint": 54, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.8809523809523809, - "right": 0.6904761904761905, - "bottom": -0.21428571428571427 - }, - "atlasBounds": { - "left": 0.140625, - "top": 0.3828125, - "right": 0.208984375, - "bottom": 0.29296875 - } - }, - { - "codePoint": 56, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.8809523809523809, - "right": 0.7142857142857143, - "bottom": -0.21428571428571427 - }, - "atlasBounds": { - "left": 0.150390625, - "top": 0.49609375, - "right": 0.220703125, - "bottom": 0.40625 - } - }, - { - "codePoint": 57, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.8809523809523809, - "right": 0.6904761904761905, - "bottom": -0.21428571428571427 - }, - "atlasBounds": { - "left": 0.0703125, - "top": 0.19140625, - "right": 0.138671875, - "bottom": 0.1015625 - } - }, - { - "codePoint": 67, - "advance": 0.7142857142857143, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.8809523809523809, - "right": 0.8809523809523809, - "bottom": -0.21428571428571427 - }, - "atlasBounds": { - "left": 0.0703125, - "top": 0.099609375, - "right": 0.154296875, - "bottom": 0.009765625 - } - }, - { - "codePoint": 71, - "advance": 0.7857142857142857, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.8809523809523809, - "right": 0.8809523809523809, - "bottom": -0.21428571428571427 - }, - "atlasBounds": { - "left": 0.140625, - "top": 0.19140625, - "right": 0.224609375, - "bottom": 0.1015625 - } - }, - { - "codePoint": 79, - "advance": 0.7857142857142857, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.8809523809523809, - "right": 0.9285714285714286, - "bottom": -0.21428571428571427 - }, - "atlasBounds": { - "left": 0.142578125, - "top": 0.291015625, - "right": 0.23046875, - "bottom": 0.201171875 - } - }, - { - "codePoint": 83, - "advance": 0.6666666666666666, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.8809523809523809, - "right": 0.8095238095238095, - "bottom": -0.21428571428571427 - }, - "atlasBounds": { - "left": 0.15625, - "top": 0.099609375, - "right": 0.234375, - "bottom": 0.009765625 - } - }, - { - "codePoint": 102, - "advance": 0.2857142857142857, - "planeBounds": { - "left": -0.16666666666666666, - "top": 0.9047619047619048, - "right": 0.47619047619047616, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.2265625, - "top": 0.19921875, - "right": 0.279296875, - "bottom": 0.109375 - } - }, - { - "codePoint": 104, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.9047619047619048, - "right": 0.6904761904761905, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.236328125, - "top": 0.107421875, - "right": 0.302734375, - "bottom": 0.017578125 - } - }, - { - "codePoint": 105, - "advance": 0.21428571428571427, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.9047619047619048, - "right": 0.35714285714285715, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.2109375, - "top": 0.404296875, - "right": 0.25, - "bottom": 0.314453125 - } - }, - { - "codePoint": 107, - "advance": 0.5, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.9047619047619048, - "right": 0.6904761904761905, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.232421875, - "top": 0.3125, - "right": 0.298828125, - "bottom": 0.22265625 - } - }, - { - "codePoint": 108, - "advance": 0.21428571428571427, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.9047619047619048, - "right": 0.35714285714285715, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.28125, - "top": 0.220703125, - "right": 0.3203125, - "bottom": 0.130859375 - } - }, - { - "codePoint": 119, - "advance": 0.7142857142857143, - "planeBounds": { - "left": -0.19047619047619047, - "top": 0.7142857142857143, - "right": 0.9047619047619048, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.3046875, - "top": 0.12890625, - "right": 0.39453125, - "bottom": 0.0546875 - } - }, - { - "codePoint": 33, - "advance": 0.2857142857142857, - "planeBounds": { - "left": -0.09523809523809523, - "top": 0.8809523809523809, - "right": 0.38095238095238093, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.162109375, - "top": 0.69140625, - "right": 0.201171875, - "bottom": 0.603515625 - } - }, - { - "codePoint": 35, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.19047619047619047, - "top": 0.8809523809523809, - "right": 0.7380952380952381, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.162109375, - "top": 0.6015625, - "right": 0.23828125, - "bottom": 0.513671875 - } - }, - { - "codePoint": 38, - "advance": 0.6666666666666666, - "planeBounds": { - "left": -0.16666666666666666, - "top": 0.8809523809523809, - "right": 0.8333333333333334, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.203125, - "top": 0.69140625, - "right": 0.28515625, - "bottom": 0.603515625 - } - }, - { - "codePoint": 49, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.8809523809523809, - "right": 0.6904761904761905, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.208984375, - "top": 0.796875, - "right": 0.275390625, - "bottom": 0.708984375 - } - }, - { - "codePoint": 50, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.8809523809523809, - "right": 0.6904761904761905, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.22265625, - "top": 0.51171875, - "right": 0.291015625, - "bottom": 0.423828125 - } - }, - { - "codePoint": 52, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.16666666666666666, - "top": 0.8809523809523809, - "right": 0.7142857142857143, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.240234375, - "top": 0.6015625, - "right": 0.3125, - "bottom": 0.513671875 - } - }, - { - "codePoint": 53, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.8809523809523809, - "right": 0.7142857142857143, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.21875, - "top": 0.90625, - "right": 0.2890625, - "bottom": 0.818359375 - } - }, - { - "codePoint": 55, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.8809523809523809, - "right": 0.6904761904761905, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.248046875, - "top": 1, - "right": 0.31640625, - "bottom": 0.912109375 - } - }, - { - "codePoint": 63, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.8809523809523809, - "right": 0.7142857142857143, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.251953125, - "top": 0.421875, - "right": 0.322265625, - "bottom": 0.333984375 - } - }, - { - "codePoint": 65, - "advance": 0.6666666666666666, - "planeBounds": { - "left": -0.19047619047619047, - "top": 0.8809523809523809, - "right": 0.8571428571428571, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.29296875, - "top": 0.51171875, - "right": 0.37890625, - "bottom": 0.423828125 - } - }, - { - "codePoint": 66, - "advance": 0.6666666666666666, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.8809523809523809, - "right": 0.7857142857142857, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.30078125, - "top": 0.33203125, - "right": 0.375, - "bottom": 0.244140625 - } - }, - { - "codePoint": 68, - "advance": 0.7142857142857143, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.8809523809523809, - "right": 0.8571428571428571, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.32421875, - "top": 0.421875, - "right": 0.404296875, - "bottom": 0.333984375 - } - }, - { - "codePoint": 69, - "advance": 0.6666666666666666, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.8809523809523809, - "right": 0.8095238095238095, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.322265625, - "top": 0.2421875, - "right": 0.3984375, - "bottom": 0.154296875 - } - }, - { - "codePoint": 70, - "advance": 0.6190476190476191, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.8809523809523809, - "right": 0.7619047619047619, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.376953125, - "top": 0.33203125, - "right": 0.44921875, - "bottom": 0.244140625 - } - }, - { - "codePoint": 72, - "advance": 0.7142857142857143, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.8809523809523809, - "right": 0.8095238095238095, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.27734375, - "top": 0.81640625, - "right": 0.353515625, - "bottom": 0.728515625 - } - }, - { - "codePoint": 73, - "advance": 0.2857142857142857, - "planeBounds": { - "left": -0.09523809523809523, - "top": 0.8809523809523809, - "right": 0.38095238095238093, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.291015625, - "top": 0.91015625, - "right": 0.330078125, - "bottom": 0.822265625 - } - }, - { - "codePoint": 74, - "advance": 0.5, - "planeBounds": { - "left": -0.16666666666666666, - "top": 0.8809523809523809, - "right": 0.6190476190476191, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.318359375, - "top": 1, - "right": 0.3828125, - "bottom": 0.912109375 - } - }, - { - "codePoint": 75, - "advance": 0.6666666666666666, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.8809523809523809, - "right": 0.8333333333333334, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.33203125, - "top": 0.91015625, - "right": 0.41015625, - "bottom": 0.822265625 - } - }, - { - "codePoint": 76, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.8809523809523809, - "right": 0.7142857142857143, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.384765625, - "top": 1, - "right": 0.453125, - "bottom": 0.912109375 - } - }, - { - "codePoint": 77, - "advance": 0.8333333333333334, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.8809523809523809, - "right": 0.9285714285714286, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.287109375, - "top": 0.7265625, - "right": 0.373046875, - "bottom": 0.638671875 - } - }, - { - "codePoint": 78, - "advance": 0.7142857142857143, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.8809523809523809, - "right": 0.8095238095238095, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.35546875, - "top": 0.8203125, - "right": 0.431640625, - "bottom": 0.732421875 - } - }, - { - "codePoint": 80, - "advance": 0.6666666666666666, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.8809523809523809, - "right": 0.7857142857142857, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.412109375, - "top": 0.91015625, - "right": 0.486328125, - "bottom": 0.822265625 - } - }, - { - "codePoint": 82, - "advance": 0.7142857142857143, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.8809523809523809, - "right": 0.8571428571428571, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.455078125, - "top": 1, - "right": 0.53515625, - "bottom": 0.912109375 - } - }, - { - "codePoint": 84, - "advance": 0.6190476190476191, - "planeBounds": { - "left": -0.16666666666666666, - "top": 0.8809523809523809, - "right": 0.7857142857142857, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.314453125, - "top": 0.63671875, - "right": 0.392578125, - "bottom": 0.548828125 - } - }, - { - "codePoint": 85, - "advance": 0.7142857142857143, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.8809523809523809, - "right": 0.8333333333333334, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.375, - "top": 0.73046875, - "right": 0.453125, - "bottom": 0.642578125 - } - }, - { - "codePoint": 86, - "advance": 0.6666666666666666, - "planeBounds": { - "left": -0.19047619047619047, - "top": 0.8809523809523809, - "right": 0.8571428571428571, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.43359375, - "top": 0.8203125, - "right": 0.51953125, - "bottom": 0.732421875 - } - }, - { - "codePoint": 88, - "advance": 0.6666666666666666, - "planeBounds": { - "left": -0.16666666666666666, - "top": 0.8809523809523809, - "right": 0.8333333333333334, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.48828125, - "top": 0.91015625, - "right": 0.5703125, - "bottom": 0.822265625 - } - }, - { - "codePoint": 89, - "advance": 0.6666666666666666, - "planeBounds": { - "left": -0.16666666666666666, - "top": 0.8809523809523809, - "right": 0.8333333333333334, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.537109375, - "top": 1, - "right": 0.619140625, - "bottom": 0.912109375 - } - }, - { - "codePoint": 90, - "advance": 0.6190476190476191, - "planeBounds": { - "left": -0.16666666666666666, - "top": 0.8809523809523809, - "right": 0.7619047619047619, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.380859375, - "top": 0.546875, - "right": 0.45703125, - "bottom": 0.458984375 - } - }, - { - "codePoint": 109, - "advance": 0.8333333333333334, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.7380952380952381, - "right": 0.9523809523809523, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.39453125, - "top": 0.640625, - "right": 0.482421875, - "bottom": 0.564453125 - } - }, - { - "codePoint": 59, - "advance": 0.2857142857142857, - "planeBounds": { - "left": -0.09523809523809523, - "top": 0.7142857142857143, - "right": 0.38095238095238093, - "bottom": -0.3333333333333333 - }, - "atlasBounds": { - "left": 0.455078125, - "top": 0.73046875, - "right": 0.494140625, - "bottom": 0.64453125 - } - }, - { - "codePoint": 116, - "advance": 0.2857142857142857, - "planeBounds": { - "left": -0.16666666666666666, - "top": 0.8333333333333334, - "right": 0.47619047619047616, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.40625, - "top": 0.45703125, - "right": 0.458984375, - "bottom": 0.373046875 - } - }, - { - "codePoint": 95, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.21428571428571427, - "top": 0.047619047619047616, - "right": 0.7380952380952381, - "bottom": -0.40476190476190477 - }, - "atlasBounds": { - "left": 0.40625, - "top": 0.37109375, - "right": 0.484375, - "bottom": 0.333984375 - } - }, - { - "codePoint": 97, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.7380952380952381, - "right": 0.7619047619047619, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.458984375, - "top": 0.5625, - "right": 0.533203125, - "bottom": 0.486328125 - } - }, - { - "codePoint": 99, - "advance": 0.5, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.7380952380952381, - "right": 0.6666666666666666, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.484375, - "top": 0.642578125, - "right": 0.55078125, - "bottom": 0.56640625 - } - }, - { - "codePoint": 101, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.7380952380952381, - "right": 0.7142857142857143, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.49609375, - "top": 0.73046875, - "right": 0.56640625, - "bottom": 0.654296875 - } - }, - { - "codePoint": 110, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.7380952380952381, - "right": 0.6904761904761905, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.521484375, - "top": 0.8203125, - "right": 0.587890625, - "bottom": 0.744140625 - } - }, - { - "codePoint": 111, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.7380952380952381, - "right": 0.7142857142857143, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.572265625, - "top": 0.91015625, - "right": 0.642578125, - "bottom": 0.833984375 - } - }, - { - "codePoint": 114, - "advance": 0.3333333333333333, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.7380952380952381, - "right": 0.5238095238095238, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.62109375, - "top": 1, - "right": 0.673828125, - "bottom": 0.923828125 - } - }, - { - "codePoint": 115, - "advance": 0.5, - "planeBounds": { - "left": -0.16666666666666666, - "top": 0.7380952380952381, - "right": 0.6428571428571429, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.4609375, - "top": 0.484375, - "right": 0.52734375, - "bottom": 0.408203125 - } - }, - { - "codePoint": 117, - "advance": 0.5476190476190477, - "planeBounds": { - "left": -0.11904761904761904, - "top": 0.7142857142857143, - "right": 0.6904761904761905, - "bottom": -0.21428571428571427 - }, - "atlasBounds": { - "left": 0.396484375, - "top": 0.15234375, - "right": 0.462890625, - "bottom": 0.076171875 - } - }, - { - "codePoint": 58, - "advance": 0.2857142857142857, - "planeBounds": { - "left": -0.09523809523809523, - "top": 0.7142857142857143, - "right": 0.38095238095238093, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.396484375, - "top": 0.07421875, - "right": 0.435546875, - "bottom": 0 - } - }, - { - "codePoint": 118, - "advance": 0.5, - "planeBounds": { - "left": -0.19047619047619047, - "top": 0.7142857142857143, - "right": 0.6904761904761905, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.4375, - "top": 0.07421875, - "right": 0.509765625, - "bottom": 0 - } - }, - { - "codePoint": 120, - "advance": 0.5, - "planeBounds": { - "left": -0.19047619047619047, - "top": 0.7142857142857143, - "right": 0.6666666666666666, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.400390625, - "top": 0.2421875, - "right": 0.470703125, - "bottom": 0.16796875 - } - }, - { - "codePoint": 122, - "advance": 0.5, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.7142857142857143, - "right": 0.6428571428571429, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.451171875, - "top": 0.33203125, - "right": 0.515625, - "bottom": 0.2578125 - } - }, - { - "codePoint": 43, - "advance": 0.5952380952380952, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.7619047619047619, - "right": 0.7142857142857143, - "bottom": -0.11904761904761904 - }, - "atlasBounds": { - "left": 0.486328125, - "top": 0.40625, - "right": 0.556640625, - "bottom": 0.333984375 - } - }, - { - "codePoint": 60, - "advance": 0.5952380952380952, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.7619047619047619, - "right": 0.7142857142857143, - "bottom": -0.11904761904761904 - }, - "atlasBounds": { - "left": 0.529296875, - "top": 0.484375, - "right": 0.599609375, - "bottom": 0.412109375 - } - }, - { - "codePoint": 62, - "advance": 0.5952380952380952, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.7619047619047619, - "right": 0.7142857142857143, - "bottom": -0.11904761904761904 - }, - "atlasBounds": { - "left": 0.53515625, - "top": 0.564453125, - "right": 0.60546875, - "bottom": 0.4921875 - } - }, - { - "codePoint": 126, - "advance": 0.5952380952380952, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.5952380952380952, - "right": 0.7380952380952381, - "bottom": 0.09523809523809523 - }, - "atlasBounds": { - "left": 0.3046875, - "top": 0.052734375, - "right": 0.376953125, - "bottom": 0.01171875 - } - }, - { - "codePoint": 61, - "advance": 0.5952380952380952, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.6904761904761905, - "right": 0.7142857142857143, - "bottom": -0.023809523809523808 - }, - "atlasBounds": { - "left": 0.552734375, - "top": 0.65234375, - "right": 0.623046875, - "bottom": 0.59375 - } - }, - { - "codePoint": 94, - "advance": 0.47619047619047616, - "planeBounds": { - "left": -0.19047619047619047, - "top": 0.8809523809523809, - "right": 0.6428571428571429, - "bottom": 0.14285714285714285 - }, - "atlasBounds": { - "left": 0.568359375, - "top": 0.7421875, - "right": 0.63671875, - "bottom": 0.681640625 - } - }, - { - "codePoint": 42, - "advance": 0.38095238095238093, - "planeBounds": { - "left": -0.16666666666666666, - "top": 0.8809523809523809, - "right": 0.5714285714285714, - "bottom": 0.14285714285714285 - }, - "atlasBounds": { - "left": 0.58984375, - "top": 0.83203125, - "right": 0.650390625, - "bottom": 0.771484375 - } - }, - { - "codePoint": 34, - "advance": 0.35714285714285715, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.8809523809523809, - "right": 0.5, - "bottom": 0.2857142857142857 - }, - "atlasBounds": { - "left": 0.64453125, - "top": 0.921875, - "right": 0.697265625, - "bottom": 0.873046875 - } - }, - { - "codePoint": 44, - "advance": 0.2857142857142857, - "planeBounds": { - "left": -0.09523809523809523, - "top": 0.2857142857142857, - "right": 0.38095238095238093, - "bottom": -0.3333333333333333 - }, - "atlasBounds": { - "left": 0.67578125, - "top": 1, - "right": 0.71484375, - "bottom": 0.94921875 - } - }, - { - "codePoint": 45, - "advance": 0.3333333333333333, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.5, - "right": 0.47619047619047616, - "bottom": 0.047619047619047616 - }, - "atlasBounds": { - "left": 0.64453125, - "top": 0.87109375, - "right": 0.6953125, - "bottom": 0.833984375 - } - }, - { - "codePoint": 39, - "advance": 0.19047619047619047, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.8809523809523809, - "right": 0.3333333333333333, - "bottom": 0.2857142857142857 - }, - "atlasBounds": { - "left": 0.46484375, - "top": 0.166015625, - "right": 0.50390625, - "bottom": 0.1171875 - } - }, - { - "codePoint": 96, - "advance": 0.3333333333333333, - "planeBounds": { - "left": -0.14285714285714285, - "top": 0.9285714285714286, - "right": 0.4523809523809524, - "bottom": 0.40476190476190477 - }, - "atlasBounds": { - "left": 0.47265625, - "top": 0.255859375, - "right": 0.521484375, - "bottom": 0.212890625 - } - }, - { - "codePoint": 46, - "advance": 0.2857142857142857, - "planeBounds": { - "left": -0.09523809523809523, - "top": 0.2857142857142857, - "right": 0.38095238095238093, - "bottom": -0.19047619047619047 - }, - "atlasBounds": { - "left": 0.46484375, - "top": 0.115234375, - "right": 0.50390625, - "bottom": 0.076171875 - } - }, - { - "codePoint": 32, - "advance": 0.2857142857142857, - "planeBounds": null, - "atlasBounds": null - } - ], - "kerning": {} -} diff --git a/documentation-site/static/fonts/default/default.png b/documentation-site/static/fonts/default/default.png deleted file mode 100644 index b484d79bb..000000000 Binary files a/documentation-site/static/fonts/default/default.png and /dev/null differ diff --git a/e2e/fixtures/scenes/text-effects-overlap.ts b/e2e/fixtures/scenes/text-effects-overlap.ts index c74d48ddd..65b9b4eca 100644 --- a/e2e/fixtures/scenes/text-effects-overlap.ts +++ b/e2e/fixtures/scenes/text-effects-overlap.ts @@ -9,6 +9,7 @@ import { createRenderEcsSystem, createTextShapingEcsSystem, createTransformEcsSystem, + CURRENT_FONT_ATLAS_FORMAT_VERSION, EcsWorld, FontAtlas, Time, @@ -239,9 +240,8 @@ export const createScene: CreateScene = async ( const fontAtlas: FontAtlas = { data: { - formatVersion: 1, + formatVersion: CURRENT_FONT_ATLAS_FORMAT_VERSION, type: 'msdf', - atlasImage: 'synthetic.png', atlasSize: { width: SYNTHETIC_GLYPH_TILE_SIZE, height: SYNTHETIC_GLYPH_TILE_SIZE, @@ -251,7 +251,12 @@ export const createScene: CreateScene = async ( // ink (which spans exactly 1 em vertically around its own center, by // construction) on the entity's position - see the derivation this // module's own doc comment gives for `advanceEm`. - metrics: { lineHeight: 1, ascender: 0.5, descender: 0.5 }, + metrics: { + lineHeight: 1, + ascender: 0.5, + descender: 0.5, + capHeight: 0.5, + }, glyphs: new Map([ [ ATLAS_A_CODE_POINT, diff --git a/package.json b/package.json index 572858bc3..ebc37d332 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,9 @@ "types": "./dist/finite-state-machine/index.d.ts", "default": "./dist/finite-state-machine/index.js" } - } + }, + "./fonts/default/default.json": "./assets/fonts/default/default.json", + "./fonts/default/default.png": "./assets/fonts/default/default.png" }, "types": "./dist/index.d.ts", "bin": { @@ -132,7 +134,7 @@ "test:e2e": "playwright test --config e2e/playwright.config.ts", "pretest:e2e:ui": "node -e \"console.log('[e2e] npm run test:e2e:ui starting', new Date().toISOString())\"", "test:e2e:ui": "playwright test --config e2e/playwright.config.ts --ui", - "check-exports": "attw --pack . --ignore-rules false-esm internal-resolution-error --profile esm-only", + "check-exports": "attw --pack . --ignore-rules false-esm internal-resolution-error --profile esm-only --exclude-entrypoints ./fonts/default/default.json ./fonts/default/default.png", "check-types": "tsc --noEmit --project tsconfig.ci.json", "check-types:e2e": "tsc --noEmit --project e2e/tsconfig.json", "cspell": "cspell \"**\"", diff --git a/scripts/generate-font-atlas.mjs b/scripts/generate-font-atlas.mjs index d8b3967a8..68fbea75a 100755 --- a/scripts/generate-font-atlas.mjs +++ b/scripts/generate-font-atlas.mjs @@ -192,7 +192,7 @@ function computeCapHeight(glyphs, fallback) { * versioned `FontAtlasFileData` schema (see `src/text/font-atlas`), so * runtime consumers never depend on a third-party generator's own format. */ -function normalizeBmfontJson(raw, atlasImageFilename) { +function normalizeBmfontJson(raw) { if (raw.distanceField?.fieldType !== 'msdf') { throw new Error( `Expected a "msdf" distance field, got "${raw.distanceField?.fieldType}". Forge's text renderer only supports MSDF atlases.`, @@ -229,7 +229,6 @@ function normalizeBmfontJson(raw, atlasImageFilename) { return { formatVersion: CURRENT_FONT_ATLAS_FORMAT_VERSION, type: 'msdf', - atlasImage: atlasImageFilename, atlasSize: { width: scaleW, height: scaleH }, distanceRange: raw.distanceField.distanceRange, metrics: { @@ -318,8 +317,7 @@ async function main() { } const raw = JSON.parse(font.data); - const atlasImageFilename = `${resolveOutBasename(resolvedOutPath)}.png`; - const fileData = normalizeBmfontJson(raw, atlasImageFilename); + const fileData = normalizeBmfontJson(raw); writeFileSync(`${resolvedOutPath}.png`, textures[0].texture); writeFileSync(`${resolvedOutPath}.json`, JSON.stringify(fileData, null, 2)); @@ -329,14 +327,6 @@ async function main() { ); } -function resolveOutBasename(resolvedOutPath) { - const separatorIndex = resolvedOutPath.lastIndexOf('/'); - - return separatorIndex === -1 - ? resolvedOutPath - : resolvedOutPath.slice(separatorIndex + 1); -} - try { await main(); } catch (error) { diff --git a/src/text/font-atlas/font-atlas-cache.test.ts b/src/text/font-atlas/font-atlas-cache.test.ts index 82846f7a8..967ee8682 100644 --- a/src/text/font-atlas/font-atlas-cache.test.ts +++ b/src/text/font-atlas/font-atlas-cache.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { ImageCache } from '../../asset-loading/index.js'; import { CURRENT_FONT_ATLAS_FORMAT_VERSION } from './font-atlas-data.js'; import type { FontAtlasFileData } from './font-atlas-file-data.js'; @@ -7,7 +7,6 @@ import { FontAtlasCache } from './font-atlas-cache.js'; const buildValidJson = (): FontAtlasFileData => ({ formatVersion: CURRENT_FONT_ATLAS_FORMAT_VERSION, type: 'msdf', - atlasImage: 'my-font.png', atlasSize: { width: 512, height: 512 }, distanceRange: 4, metrics: { lineHeight: 1.2, ascender: 0.9, descender: -0.2, capHeight: 0.7 }, @@ -22,6 +21,11 @@ const buildValidJson = (): FontAtlasFileData => ({ kerning: {}, }); +const urls = { + metricsUrl: 'assets/fonts/my-font.json', + imageUrl: 'assets/fonts/my-font.png', +}; + function mockFetchJsonResponse(json: unknown, ok = true): void { vi.stubGlobal( 'fetch', @@ -34,86 +38,180 @@ function mockFetchJsonResponse(json: unknown, ok = true): void { ); } +function createImage(width = 512, height = 512): HTMLImageElement { + const image = new Image(); + + Object.defineProperty(image, 'naturalWidth', { value: width }); + Object.defineProperty(image, 'naturalHeight', { value: height }); + + return image; +} + +function createImageCache(image = createImage()): ImageCache { + const imageCache = new ImageCache(); + + vi.spyOn(imageCache, 'getOrLoad').mockResolvedValue(image); + + return imageCache; +} + describe('FontAtlasCache', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + it('should throw when getting a font atlas that has not been loaded', () => { const fontAtlasCache = new FontAtlasCache(); expect(() => fontAtlasCache.get('assets/fonts/my-font.json')).toThrow( - 'Font atlas with key "assets/fonts/my-font.json" not found in store.', + 'Font atlas with metrics URL "assets/fonts/my-font.json" not found in store.', ); }); it('should load and cache a valid font atlas', async () => { mockFetchJsonResponse(buildValidJson()); - const mockImage = new Image(); - const imageCache = new ImageCache(); - const getOrLoadSpy = vi - .spyOn(imageCache, 'getOrLoad') - .mockResolvedValue(mockImage); - + const image = createImage(); + const imageCache = createImageCache(image); const fontAtlasCache = new FontAtlasCache(imageCache); - const fontAtlas = await fontAtlasCache.getOrLoad( - 'assets/fonts/my-font.json', - ); + const fontAtlas = await fontAtlasCache.getOrLoad(urls); expect(fetch).toHaveBeenCalledWith('assets/fonts/my-font.json'); - expect(getOrLoadSpy).toHaveBeenCalledWith('assets/fonts/my-font.png'); - expect(fontAtlas.image).toBe(mockImage); + expect(imageCache.getOrLoad).toHaveBeenCalledWith( + 'assets/fonts/my-font.png', + ); + expect(fontAtlas.image).toBe(image); expect(fontAtlas.data.glyphs.get(65)?.advance).toBeCloseTo(0.6); expect(fontAtlasCache.get('assets/fonts/my-font.json')).toBe(fontAtlas); }); - it('should resolve the atlas image relative to a nested key', async () => { - mockFetchJsonResponse(buildValidJson()); + it.each([ + { + name: 'different directories with query strings', + metricsUrl: '/assets/metrics/my-font.json?v=a/b', + imageUrl: '/static/images/my-font-3f2a.png?v=c/d#e', + }, + { + name: 'blob URLs', + metricsUrl: 'blob:https://example.com/0f1e2d3c', + imageUrl: 'blob:https://example.com/4b5a6978', + }, + ])( + 'should load the image from its own URL ($name)', + async ({ metricsUrl, imageUrl }) => { + mockFetchJsonResponse(buildValidJson()); + + const imageCache = createImageCache(); + const fontAtlasCache = new FontAtlasCache(imageCache); + await fontAtlasCache.getOrLoad({ metricsUrl, imageUrl }); - const imageCache = new ImageCache(); - const getOrLoadSpy = vi - .spyOn(imageCache, 'getOrLoad') - .mockResolvedValue(new Image()); + expect(fetch).toHaveBeenCalledWith(metricsUrl); + expect(imageCache.getOrLoad).toHaveBeenCalledWith(imageUrl); + }, + ); + it('should load a metrics file that still names its atlas image', async () => { + mockFetchJsonResponse({ ...buildValidJson(), atlasImage: 'other.png' }); + + const imageCache = createImageCache(); const fontAtlasCache = new FontAtlasCache(imageCache); - await fontAtlasCache.getOrLoad('assets/fonts/heading/my-font.json'); + await fontAtlasCache.getOrLoad(urls); - expect(getOrLoadSpy).toHaveBeenCalledWith( - 'assets/fonts/heading/my-font.png', + expect(imageCache.getOrLoad).toHaveBeenCalledWith( + 'assets/fonts/my-font.png', ); }); - it('should resolve the atlas image relative to a root-level key', async () => { + it('should not re-fetch a font atlas that is already cached', async () => { mockFetchJsonResponse(buildValidJson()); - const imageCache = new ImageCache(); - const getOrLoadSpy = vi - .spyOn(imageCache, 'getOrLoad') - .mockResolvedValue(new Image()); + const fontAtlasCache = new FontAtlasCache(createImageCache()); + const first = await fontAtlasCache.getOrLoad(urls); + const second = await fontAtlasCache.getOrLoad(urls); + expect(fetch).toHaveBeenCalledTimes(1); + expect(second).toBe(first); + }); + + it('should share one load between concurrent requests for the same atlas', async () => { + mockFetchJsonResponse(buildValidJson()); + + const imageCache = createImageCache(); const fontAtlasCache = new FontAtlasCache(imageCache); - await fontAtlasCache.getOrLoad('my-font.json'); + const [first, second] = await Promise.all([ + fontAtlasCache.getOrLoad(urls), + fontAtlasCache.getOrLoad({ ...urls }), + ]); - expect(getOrLoadSpy).toHaveBeenCalledWith('my-font.png'); + expect(fetch).toHaveBeenCalledTimes(1); + expect(imageCache.getOrLoad).toHaveBeenCalledTimes(1); + expect(second).toBe(first); }); - it('should not re-fetch a font atlas that is already cached', async () => { + it('should throw when a loading atlas is requested with a different image', async () => { mockFetchJsonResponse(buildValidJson()); - const imageCache = new ImageCache(); - vi.spyOn(imageCache, 'getOrLoad').mockResolvedValue(new Image()); + const fontAtlasCache = new FontAtlasCache(createImageCache()); + const firstLoad = fontAtlasCache.getOrLoad(urls); - const fontAtlasCache = new FontAtlasCache(imageCache); - await fontAtlasCache.getOrLoad('assets/fonts/my-font.json'); - await fontAtlasCache.getOrLoad('assets/fonts/my-font.json'); + await expect( + fontAtlasCache.getOrLoad({ ...urls, imageUrl: 'other-font.png' }), + ).rejects.toThrow( + 'Font atlas "assets/fonts/my-font.json" was already requested with image "assets/fonts/my-font.png", so it can\'t also be loaded with image "other-font.png".', + ); + await expect(firstLoad).resolves.toBeDefined(); + }); - expect(fetch).toHaveBeenCalledTimes(1); + it('should throw when a loaded atlas is requested with a different image', async () => { + mockFetchJsonResponse(buildValidJson()); + + const fontAtlasCache = new FontAtlasCache(createImageCache()); + await fontAtlasCache.getOrLoad(urls); + + await expect( + fontAtlasCache.getOrLoad({ ...urls, imageUrl: 'other-font.png' }), + ).rejects.toThrow(/was already requested with image/); + }); + + it("should throw when the image's size doesn't match the metrics' atlasSize", async () => { + mockFetchJsonResponse(buildValidJson()); + + const fontAtlasCache = new FontAtlasCache( + createImageCache(createImage(256, 512)), + ); + + await expect(fontAtlasCache.getOrLoad(urls)).rejects.toThrow( + 'Font atlas image "assets/fonts/my-font.png" is 256x512, but the metrics at "assets/fonts/my-font.json" expect 512x512. Check that both files come from the same generated atlas.', + ); + expect(() => fontAtlasCache.get(urls.metricsUrl)).toThrow( + /not found in store/, + ); + }); + + it('should load again after a failed load', async () => { + mockFetchJsonResponse({}, false); + + const fontAtlasCache = new FontAtlasCache(createImageCache()); + + await expect(fontAtlasCache.getOrLoad(urls)).rejects.toThrow( + /Failed to load font atlas JSON/, + ); + + mockFetchJsonResponse(buildValidJson()); + + await expect(fontAtlasCache.getOrLoad(urls)).resolves.toBeDefined(); }); it('should throw a descriptive error when the JSON fetch fails', async () => { mockFetchJsonResponse({}, false); - const fontAtlasCache = new FontAtlasCache(); + const fontAtlasCache = new FontAtlasCache(createImageCache()); await expect( - fontAtlasCache.getOrLoad('assets/fonts/missing-font.json'), + fontAtlasCache.getOrLoad({ + metricsUrl: 'assets/fonts/missing-font.json', + imageUrl: 'assets/fonts/missing-font.png', + }), ).rejects.toThrow( 'Failed to load font atlas JSON at "assets/fonts/missing-font.json": 404 Not Found', ); @@ -122,10 +220,10 @@ describe('FontAtlasCache', () => { it('should throw a descriptive error when the JSON is malformed', async () => { mockFetchJsonResponse({ formatVersion: 999 }); - const fontAtlasCache = new FontAtlasCache(); + const fontAtlasCache = new FontAtlasCache(createImageCache()); - await expect( - fontAtlasCache.getOrLoad('assets/fonts/broken-font.json'), - ).rejects.toThrow(/unsupported formatVersion "999"/); + await expect(fontAtlasCache.getOrLoad(urls)).rejects.toThrow( + /unsupported formatVersion "999"/, + ); }); }); diff --git a/src/text/font-atlas/font-atlas-cache.ts b/src/text/font-atlas/font-atlas-cache.ts index 08549c067..6c346d1cc 100644 --- a/src/text/font-atlas/font-atlas-cache.ts +++ b/src/text/font-atlas/font-atlas-cache.ts @@ -1,89 +1,135 @@ -import { type AssetCache, ImageCache } from '../../asset-loading/index.js'; +import { ImageCache } from '../../asset-loading/index.js'; import type { FontAtlas } from './font-atlas.js'; +import type { FontAtlasData } from './font-atlas-data.js'; import { toFontAtlasData } from './font-atlas-file-data.js'; import { validateFontAtlasFileData } from './validate-font-atlas-data.js'; /** - * Loads and caches `FontAtlas`es: a metrics JSON file plus its atlas image, - * keyed by the JSON file's path (e.g. `getOrLoad('assets/fonts/my-font.json')` - * loads that file, which in turn points at its own atlas image, resolved - * relative to the JSON file). + * The two files a font atlas is loaded from. Pass the URLs your bundler + * gives you for each file (e.g. Vite's `import url from './font.json?url'` + * and `import url from './font.png'`), so content-hashed file names work. */ -export class FontAtlasCache implements AssetCache { - public assets = new Map(); +export interface FontAtlasUrls { + /** The URL of the atlas's metrics JSON file. */ + metricsUrl: string; + /** The URL of the atlas's image, generated together with the metrics. */ + imageUrl: string; +} + +interface FontAtlasLoad { + readonly imageUrl: string; + readonly promise: Promise; +} + +/** + * Loads and caches `FontAtlas`es from a metrics JSON file and its atlas + * image, keyed by the metrics file's URL. + */ +export class FontAtlasCache { private readonly _imageCache: ImageCache; + private readonly _loads = new Map(); + private readonly _fontAtlases = new Map(); constructor(imageCache: ImageCache = new ImageCache()) { this._imageCache = imageCache; } /** - * Retrieves a font atlas from the cache. - * @param key - The path of the font atlas's JSON file. + * Retrieves a loaded font atlas from the cache. + * @param metricsUrl - The URL of the font atlas's metrics JSON file. * @returns The cached font atlas. - * @throws Will throw an error if the font atlas is not found in the cache. + * @throws Will throw an error if the font atlas hasn't finished loading. */ - public get(key: string): FontAtlas { - const fontAtlas = this.assets.get(key); + public get(metricsUrl: string): FontAtlas { + const fontAtlas = this._fontAtlases.get(metricsUrl); if (!fontAtlas) { - throw new Error(`Font atlas with key "${key}" not found in store.`); + throw new Error( + `Font atlas with metrics URL "${metricsUrl}" not found in store.`, + ); } return fontAtlas; } /** - * Loads a font atlas's JSON metrics and image from the specified key and - * caches it. - * @param key - The path of the font atlas's JSON file. - * @returns A promise that resolves when the font atlas is loaded and cached. - * @throws Will throw an error if the JSON fails to fetch, is malformed, or - * is an unsupported/incompatible atlas format. + * Retrieves a font atlas from the cache, loading it the first time it's + * requested. Concurrent calls for the same atlas share one load. + * @param urls - The URLs of the atlas's metrics JSON file and image. + * @returns A promise that resolves to the font atlas. + * @throws Will throw an error if `urls.metricsUrl` was already requested + * with a different image URL, if the JSON fails to fetch, is malformed or + * is an unsupported atlas format, if the image fails to load, or if the + * image's size doesn't match the metrics' `atlasSize`. */ - public async load(key: string): Promise { - const response = await fetch(key); + public async getOrLoad(urls: FontAtlasUrls): Promise { + const { metricsUrl, imageUrl } = urls; + const existingLoad = this._loads.get(metricsUrl); - if (!response.ok) { - throw new Error( - `Failed to load font atlas JSON at "${key}": ${response.status} ${response.statusText}`, - ); + if (existingLoad) { + if (existingLoad.imageUrl !== imageUrl) { + throw new Error( + `Font atlas "${metricsUrl}" was already requested with image "${existingLoad.imageUrl}", so it can't also be loaded with image "${imageUrl}".`, + ); + } + + return existingLoad.promise; } - const rawJson: unknown = await response.json(); - const fileData = validateFontAtlasFileData(rawJson, key); - const data = toFontAtlasData(fileData); - const imagePath = resolveRelativeToKey(key, data.atlasImage); - const image = await this._imageCache.getOrLoad(imagePath); + const promise = this._load(metricsUrl, imageUrl); + + this._loads.set(metricsUrl, { imageUrl, promise }); + + try { + return await promise; + } catch (error) { + if (this._loads.get(metricsUrl)?.promise === promise) { + this._loads.delete(metricsUrl); + } - this.assets.set(key, { data, image }); + throw error; + } } - /** - * Retrieves a font atlas from the cache if it exists, otherwise loads and - * caches it. - * @param key - The key of the font atlas to retrieve or load. - * @returns A promise that resolves to the font atlas. - */ - public async getOrLoad(key: string): Promise { - if (!this.assets.has(key)) { - await this.load(key); + private async _load( + metricsUrl: string, + imageUrl: string, + ): Promise { + const [data, image] = await Promise.all([ + loadFontAtlasData(metricsUrl), + this._imageCache.getOrLoad(imageUrl), + ]); + + const { width, height } = data.atlasSize; + + if (image.naturalWidth !== width || image.naturalHeight !== height) { + throw new Error( + `Font atlas image "${imageUrl}" is ${image.naturalWidth}x${image.naturalHeight}, but the metrics at "${metricsUrl}" expect ${width}x${height}. Check that both files come from the same generated atlas.`, + ); } - return this.get(key); + const fontAtlas: FontAtlas = { data, image }; + + this._fontAtlases.set(metricsUrl, fontAtlas); + + return fontAtlas; } } /** - * Resolves `relativePath` against the directory `key` sits in, the same way - * `FontAtlasData.atlasImage` is documented as being relative to its own - * JSON file. + * Fetches, validates and converts the metrics JSON at `metricsUrl`. */ -function resolveRelativeToKey(key: string, relativePath: string): string { - const lastSlashIndex = key.lastIndexOf('/'); - const directory = - lastSlashIndex === -1 ? '' : key.slice(0, lastSlashIndex + 1); +async function loadFontAtlasData(metricsUrl: string): Promise { + const response = await fetch(metricsUrl); + + if (!response.ok) { + throw new Error( + `Failed to load font atlas JSON at "${metricsUrl}": ${response.status} ${response.statusText}`, + ); + } + + const rawJson: unknown = await response.json(); - return `${directory}${relativePath}`; + return toFontAtlasData(validateFontAtlasFileData(rawJson, metricsUrl)); } diff --git a/src/text/font-atlas/font-atlas-data.ts b/src/text/font-atlas/font-atlas-data.ts index fd9a606a8..8bb67d62e 100644 --- a/src/text/font-atlas/font-atlas-data.ts +++ b/src/text/font-atlas/font-atlas-data.ts @@ -90,9 +90,6 @@ export interface FontAtlasData { /** `'msdf'` for v1; reserved for `'mtsdf'` later. */ type: 'msdf'; - /** Atlas texture path, relative to this atlas's JSON file. */ - atlasImage: string; - /** Atlas texture pixel dimensions. */ atlasSize: AtlasSize; diff --git a/src/text/font-atlas/font-atlas-file-data.test.ts b/src/text/font-atlas/font-atlas-file-data.test.ts index 4ea3a74bd..8718feb70 100644 --- a/src/text/font-atlas/font-atlas-file-data.test.ts +++ b/src/text/font-atlas/font-atlas-file-data.test.ts @@ -11,7 +11,6 @@ import { const buildFileData = (): FontAtlasFileData => ({ formatVersion: CURRENT_FONT_ATLAS_FORMAT_VERSION, type: 'msdf', - atlasImage: 'my-font.png', atlasSize: { width: 512, height: 512 }, distanceRange: 4, metrics: { lineHeight: 1.2, ascender: 0.9, descender: -0.2, capHeight: 0.7 }, @@ -40,7 +39,6 @@ describe('toFontAtlasData', () => { expect(result.formatVersion).toBe(CURRENT_FONT_ATLAS_FORMAT_VERSION); expect(result.type).toBe('msdf'); - expect(result.atlasImage).toBe('my-font.png'); expect(result.atlasSize).toEqual({ width: 512, height: 512 }); expect(result.distanceRange).toBe(4); expect(result.metrics).toEqual({ diff --git a/src/text/font-atlas/font-atlas-file-data.ts b/src/text/font-atlas/font-atlas-file-data.ts index e61f21b06..4da631420 100644 --- a/src/text/font-atlas/font-atlas-file-data.ts +++ b/src/text/font-atlas/font-atlas-file-data.ts @@ -7,8 +7,8 @@ import { } from './font-atlas-data.js'; /** - * The on-disk, JSON-serializable shape of a committed font atlas asset - * (`.json`, sitting alongside `.png`). This is the same data as + * The on-disk, JSON-serializable shape of a font atlas's metrics file + * (`.json`, generated together with its `.png` image). This is the same data as * `FontAtlasData`, except `glyphs`/`kerning` use JSON-safe arrays/records * instead of `Map`s. `toFontAtlasData` converts one into the other. */ @@ -19,9 +19,6 @@ export interface FontAtlasFileData { /** `'msdf'` for v1; reserved for `'mtsdf'` later. */ type: 'msdf'; - /** Atlas texture path, relative to this JSON file. */ - atlasImage: string; - /** Atlas texture pixel dimensions. */ atlasSize: AtlasSize; @@ -48,7 +45,6 @@ export function toFontAtlasData(fileData: FontAtlasFileData): FontAtlasData { return { formatVersion: fileData.formatVersion, type: fileData.type, - atlasImage: fileData.atlasImage, atlasSize: fileData.atlasSize, distanceRange: fileData.distanceRange, metrics: fileData.metrics, diff --git a/src/text/font-atlas/validate-font-atlas-data.test.ts b/src/text/font-atlas/validate-font-atlas-data.test.ts index 1830592a9..66220976f 100644 --- a/src/text/font-atlas/validate-font-atlas-data.test.ts +++ b/src/text/font-atlas/validate-font-atlas-data.test.ts @@ -9,7 +9,6 @@ import { validateFontAtlasFileData } from './validate-font-atlas-data.js'; const buildValidJson = (): FontAtlasFileData => ({ formatVersion: CURRENT_FONT_ATLAS_FORMAT_VERSION, type: 'msdf', - atlasImage: 'my-font.png', atlasSize: { width: 512, height: 512 }, distanceRange: 4, metrics: { lineHeight: 1.2, ascender: 0.9, descender: -0.2, capHeight: 0.7 }, @@ -64,12 +63,10 @@ describe('validateFontAtlasFileData', () => { ); }); - it('should throw when atlasImage is missing or empty', () => { - const json = { ...buildValidJson(), atlasImage: '' }; + it('should accept a file that still names its atlas image', () => { + const json = { ...buildValidJson(), atlasImage: 'my-font.png' }; - expect(() => validateFontAtlasFileData(json, 'fixture.json')).toThrow( - /atlasImage must be a non-empty string/, - ); + expect(validateFontAtlasFileData(json, 'fixture.json')).toBe(json); }); it('should throw when atlasSize is non-positive', () => { diff --git a/src/text/font-atlas/validate-font-atlas-data.ts b/src/text/font-atlas/validate-font-atlas-data.ts index 58890766f..934af52b4 100644 --- a/src/text/font-atlas/validate-font-atlas-data.ts +++ b/src/text/font-atlas/validate-font-atlas-data.ts @@ -43,10 +43,6 @@ export function validateFontAtlasFileData( fail(`unsupported type "${String(data.type)}", expected "msdf".`); } - if (typeof data.atlasImage !== 'string' || data.atlasImage.length === 0) { - fail('atlasImage must be a non-empty string.'); - } - validateAtlasSize(data.atlasSize, fail); validateDistanceRange(data.distanceRange, fail); validateMetrics(data.metrics, fail); diff --git a/src/text/rendering/create-text-renderable.test.ts b/src/text/rendering/create-text-renderable.test.ts index 93eacc10a..a745f2a08 100644 --- a/src/text/rendering/create-text-renderable.test.ts +++ b/src/text/rendering/create-text-renderable.test.ts @@ -41,7 +41,6 @@ describe('createTextRenderable', () => { data: { formatVersion: 2, type: 'msdf', - atlasImage: 'fixture.png', atlasSize: { width: 512, height: 512 }, distanceRange: 4, metrics: { diff --git a/src/text/systems/text-shaping-system.test.ts b/src/text/systems/text-shaping-system.test.ts index 7da10fc85..54d686b8d 100644 --- a/src/text/systems/text-shaping-system.test.ts +++ b/src/text/systems/text-shaping-system.test.ts @@ -30,7 +30,6 @@ function buildFontAtlas(): FontAtlas { data: { formatVersion: 2, type: 'msdf', - atlasImage: 'fixture.png', atlasSize: { width: 256, height: 256 }, distanceRange: 4, metrics: { diff --git a/src/text/utilities/shape-text.test.ts b/src/text/utilities/shape-text.test.ts index 7d71db149..475167cc7 100644 --- a/src/text/utilities/shape-text.test.ts +++ b/src/text/utilities/shape-text.test.ts @@ -11,7 +11,6 @@ function buildFixtureFontAtlasData(): FontAtlasData { return { formatVersion: 2, type: 'msdf', - atlasImage: 'fixture.png', atlasSize: { width: 256, height: 256 }, distanceRange: 4, // `capHeight: 0.7` intentionally matches "A"/"V"'s own `planeBounds.top`