@aethervtt/shard-text 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick Dias
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @aethervtt/shard-text
2
+
3
+ Part of [Shard](https://github.com/aethervtt/shard), a WebGPU-first 2D/3D game engine in TypeScript. See the repository for documentation.
@@ -0,0 +1,43 @@
1
+ import { type CharsetName } from './charset.js';
2
+ import { Font, type FontMetricsJson } from './font.js';
3
+ import { FontSource } from './source.js';
4
+ export interface FontBuildOptions {
5
+ /** `latin`, `latin-extended`, or a string of the characters to include. Default `latin`. */
6
+ charset?: CharsetName | (string & {});
7
+ /** Atlas pixels per em. Default 48. */
8
+ size?: number;
9
+ /** Distance range in atlas pixels. Default 4. */
10
+ range?: number;
11
+ /** Include kerning pairs. Default true. */
12
+ kerning?: boolean;
13
+ /** Largest atlas page side (a power of two). Default 2048. */
14
+ maxPageSize?: number;
15
+ /** Padding around each glyph in pixels. Default 1. */
16
+ padding?: number;
17
+ /** Fallback font project paths, recorded in the metrics. */
18
+ fallback?: string[];
19
+ }
20
+ export interface FontBuild {
21
+ metrics: FontMetricsJson;
22
+ /** RGBA8 atlas pages (MSDF in RGB, alpha 255), row 0 at the top. */
23
+ pages: {
24
+ width: number;
25
+ height: number;
26
+ pixels: Uint8Array;
27
+ }[];
28
+ /** Charset codepoints the font doesn't have. */
29
+ missing: number[];
30
+ source: FontSource;
31
+ }
32
+ /**
33
+ * Generates a font's MSDF atlas and metrics: every charset glyph the font has, packed onto
34
+ * power-of-two pages, plus kerning between them and a missing-glyph box.
35
+ */
36
+ export declare function buildFont(input: Uint8Array | FontSource, options?: FontBuildOptions): FontBuild;
37
+ /**
38
+ * A runtime font straight from a font file (no asset import): builds the atlas in code and keeps
39
+ * the outlines for on-demand glyphs (unless `outlines` is false).
40
+ */
41
+ export declare function fontFromBytes(bytes: Uint8Array, options?: FontBuildOptions & {
42
+ outlines?: boolean;
43
+ }): Font;
package/dist/build.js ADDED
@@ -0,0 +1,106 @@
1
+ import { packRects } from "@aethervtt/shard-texture";
2
+ import { charsetCodepoints } from "./charset.js";
3
+ import {
4
+ Font,
5
+ GLYPH_PADDING
6
+ } from "./font.js";
7
+ import { FontSource, rasterizeGlyph, TOFU_ADVANCE, tofuShape } from "./source.js";
8
+ const round6 = (v) => Math.round(v * 1e6) / 1e6;
9
+ function buildFont(input, options = {}) {
10
+ const source = input instanceof FontSource ? input : new FontSource(input);
11
+ const size = options.size ?? 48;
12
+ const range = options.range ?? 4;
13
+ const raster = { size, range };
14
+ const upem = source.unitsPerEm;
15
+ const charset = options.charset ?? "latin";
16
+ const codepoints = charsetCodepoints(charset);
17
+ const missing = [];
18
+ const entries = [];
19
+ for (const cp of codepoints) {
20
+ const index = source.glyphIndex(cp);
21
+ if (index === 0) {
22
+ missing.push(cp);
23
+ continue;
24
+ }
25
+ const bitmap = rasterizeGlyph(source.shape(index), upem, raster);
26
+ entries.push({ json: { u: cp, a: round6(source.advance(index) / upem) }, bitmap, index });
27
+ }
28
+ const notdefShape = source.shape(0);
29
+ const hasNotdef = notdefShape.contours.length > 0;
30
+ const notdef = {
31
+ json: { u: -1, a: hasNotdef ? round6(source.advance(0) / upem) : TOFU_ADVANCE },
32
+ bitmap: hasNotdef ? rasterizeGlyph(notdefShape, upem, raster) : rasterizeGlyph(tofuShape(), 1, raster),
33
+ index: 0
34
+ };
35
+ const all = [...entries, notdef];
36
+ const visible = all.filter((e) => e.bitmap.plane);
37
+ const packing = packRects(
38
+ visible.map((e) => ({ width: e.bitmap.width, height: e.bitmap.height })),
39
+ options.maxPageSize ?? 2048,
40
+ options.padding ?? GLYPH_PADDING
41
+ );
42
+ const pages = packing.pages.map((p) => {
43
+ const pixels = new Uint8Array(p.width * p.height * 4);
44
+ for (let i = 3; i < pixels.length; i += 4) pixels[i] = 255;
45
+ return { width: p.width, height: p.height, pixels };
46
+ });
47
+ visible.forEach((e, i) => {
48
+ const place = packing.placements[i];
49
+ const page = pages[place.page];
50
+ const bmp = e.bitmap;
51
+ for (let row = 0; row < bmp.height; row++) {
52
+ page.pixels.set(
53
+ bmp.pixels.subarray(row * bmp.width * 4, (row + 1) * bmp.width * 4),
54
+ ((place.y + row) * page.width + place.x) * 4
55
+ );
56
+ }
57
+ e.json.b = bmp.plane.map(round6);
58
+ e.json.r = [place.x, place.y, bmp.width, bmp.height];
59
+ if (place.page !== 0) e.json.p = place.page;
60
+ });
61
+ const kerning = [];
62
+ if (options.kerning !== false) {
63
+ const byGlyph = /* @__PURE__ */ new Map();
64
+ for (const e of entries) {
65
+ const list = byGlyph.get(e.index);
66
+ if (list) list.push(e.json.u);
67
+ else byGlyph.set(e.index, [e.json.u]);
68
+ }
69
+ const pairs = source.kerning(byGlyph.keys());
70
+ const sorted = [...pairs].sort((a, b) => a[0] - b[0]);
71
+ for (const [key, value] of sorted) {
72
+ const lefts = byGlyph.get(Math.floor(key / 65536));
73
+ const rights = byGlyph.get(key % 65536);
74
+ for (const l of lefts) for (const r of rights) kerning.push(l, r, value);
75
+ }
76
+ }
77
+ const metrics = {
78
+ version: 1,
79
+ name: source.name,
80
+ size,
81
+ range,
82
+ unitsPerEm: upem,
83
+ ascent: round6(source.ascender / upem),
84
+ descent: round6(source.descender / upem),
85
+ lineGap: round6(source.lineGap / upem),
86
+ pages: packing.pages,
87
+ glyphs: entries.map((e) => e.json),
88
+ notdef: notdef.json,
89
+ kerning,
90
+ charset: charset === "latin" || charset === "latin-extended" ? charset : "custom"
91
+ };
92
+ if (options.fallback?.length) metrics.fallback = [...options.fallback];
93
+ return { metrics, pages, missing, source };
94
+ }
95
+ function fontFromBytes(bytes, options = {}) {
96
+ const build = buildFont(bytes, options);
97
+ const load = {
98
+ pixels: build.pages.map((p) => p.pixels),
99
+ ...options.outlines === false ? {} : { source: build.source }
100
+ };
101
+ return Font.fromMetrics(build.metrics, load);
102
+ }
103
+ export {
104
+ buildFont,
105
+ fontFromBytes
106
+ };
@@ -0,0 +1,8 @@
1
+ export type CharsetName = 'latin' | 'latin-extended';
2
+ /**
3
+ * Codepoints of a charset, sorted and unique:
4
+ * - `latin`: printable ASCII, Latin-1 Supplement, and common typographic punctuation.
5
+ * - `latin-extended`: `latin` plus Latin Extended-A.
6
+ * - any other string: exactly its characters (plus the space).
7
+ */
8
+ export declare function charsetCodepoints(charset: CharsetName | (string & {})): number[];
@@ -0,0 +1,43 @@
1
+ const PUNCTUATION = [
2
+ 8211,
3
+ 8212,
4
+ 8216,
5
+ 8217,
6
+ 8218,
7
+ 8220,
8
+ 8221,
9
+ 8222,
10
+ 8224,
11
+ 8225,
12
+ 8226,
13
+ 8230,
14
+ 8240,
15
+ 8249,
16
+ 8250,
17
+ 8364,
18
+ 8482
19
+ ];
20
+ function range(from, to) {
21
+ const out = [];
22
+ for (let c = from; c <= to; c++) out.push(c);
23
+ return out;
24
+ }
25
+ function charsetCodepoints(charset) {
26
+ const set = /* @__PURE__ */ new Set();
27
+ if (charset === "latin" || charset === "latin-extended") {
28
+ for (const c of range(32, 126)) set.add(c);
29
+ for (const c of range(160, 255)) set.add(c);
30
+ for (const c of PUNCTUATION) set.add(c);
31
+ if (charset === "latin-extended") for (const c of range(256, 383)) set.add(c);
32
+ } else {
33
+ set.add(32);
34
+ for (const ch of charset) {
35
+ const cp = ch.codePointAt(0);
36
+ if (cp >= 32 && cp !== 127) set.add(cp);
37
+ }
38
+ }
39
+ return [...set].sort((a, b) => a - b);
40
+ }
41
+ export {
42
+ charsetCodepoints
43
+ };
@@ -0,0 +1,54 @@
1
+ export declare const TEXT_ALIGNS: readonly ['left', 'center', 'right'];
2
+ export declare const Text: import("@aethervtt/shard-core").ComponentDef<{
3
+ readonly value: import("@aethervtt/shard-core").FieldType<string, "object">;
4
+ readonly key: import("@aethervtt/shard-core").FieldType<string, "object">;
5
+ readonly params: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").JsonValue, "object">;
6
+ readonly font: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"Font"> | null, "object">;
7
+ readonly color: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
8
+ readonly align: import("@aethervtt/shard-core").FieldType<"center" | "left" | "right", "u8">;
9
+ readonly anchor: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec2, "f32">;
10
+ readonly maxWidth: import("@aethervtt/shard-core").FieldType<number, "f32">;
11
+ readonly lineHeight: import("@aethervtt/shard-core").FieldType<number, "f32">;
12
+ readonly weight: import("@aethervtt/shard-core").FieldType<number, "f32">;
13
+ readonly outline: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
14
+ width: import("@aethervtt/shard-core").FieldType<number, "f32">;
15
+ color: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
16
+ }>, "object">;
17
+ readonly shadow: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
18
+ offset: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec2, "f32">;
19
+ softness: import("@aethervtt/shard-core").FieldType<number, "f32">;
20
+ color: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
21
+ }>, "object">;
22
+ readonly size: import("@aethervtt/shard-core").FieldType<number, "f32">;
23
+ readonly billboard: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
24
+ }>;
25
+ export declare const SCREEN_CORNERS: readonly ['top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right'];
26
+ export declare const ScreenText: import("@aethervtt/shard-core").ComponentDef<{
27
+ readonly value: import("@aethervtt/shard-core").FieldType<string, "object">;
28
+ readonly key: import("@aethervtt/shard-core").FieldType<string, "object">;
29
+ readonly params: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").JsonValue, "object">;
30
+ readonly font: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"Font"> | null, "object">;
31
+ readonly color: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
32
+ readonly align: import("@aethervtt/shard-core").FieldType<"center" | "left" | "right", "u8">;
33
+ readonly anchor: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec2, "f32">;
34
+ readonly maxWidth: import("@aethervtt/shard-core").FieldType<number, "f32">;
35
+ readonly lineHeight: import("@aethervtt/shard-core").FieldType<number, "f32">;
36
+ readonly weight: import("@aethervtt/shard-core").FieldType<number, "f32">;
37
+ readonly outline: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
38
+ width: import("@aethervtt/shard-core").FieldType<number, "f32">;
39
+ color: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
40
+ }>, "object">;
41
+ readonly shadow: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
42
+ offset: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec2, "f32">;
43
+ softness: import("@aethervtt/shard-core").FieldType<number, "f32">;
44
+ color: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
45
+ }>, "object">;
46
+ readonly size: import("@aethervtt/shard-core").FieldType<number, "f32">;
47
+ readonly corner: import("@aethervtt/shard-core").FieldType<"bottom" | "bottom-left" | "bottom-right" | "center" | "left" | "right" | "top" | "top-left" | "top-right", "u8">;
48
+ readonly position: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec2, "f32">;
49
+ }>;
50
+ /** The string a localized text shows: its key resolved in the current locale (0038). */
51
+ export declare const Localized: import("@aethervtt/shard-core").ComponentDef<{
52
+ readonly value: import("@aethervtt/shard-core").FieldType<string, "object">;
53
+ readonly locale: import("@aethervtt/shard-core").FieldType<string, "object">;
54
+ }>;
@@ -0,0 +1,118 @@
1
+ import { defineComponent, t } from "@aethervtt/shard-core";
2
+ import { Visibility } from "@aethervtt/shard-render";
3
+ import { Transform } from "@aethervtt/shard-transform";
4
+ const TEXT_ALIGNS = ["left", "center", "right"];
5
+ const textFields = {
6
+ value: t.string({
7
+ description: 'The text. "\\n" breaks a line. With a key, it shows until the key resolves.'
8
+ }),
9
+ key: t.string({
10
+ description: "Localization key in the locales/*.strings.json tables: the text shows the current locale's string for it, and follows locale switches."
11
+ }),
12
+ params: t.json({
13
+ default: {},
14
+ description: `Values for the key's {name} placeholders: { "amount": 42 }. Numbers format for the locale; "n" (or "count") picks the plural form.`
15
+ }),
16
+ font: t.handle("Font", { description: "An imported font (.ttf, .otf)." }),
17
+ color: t.color({ default: [1, 1, 1, 1], description: "Fill color (linear)." }),
18
+ align: t.enum(TEXT_ALIGNS, { description: "Line alignment within the block." }),
19
+ anchor: t.vec2({
20
+ default: [0.5, 0.5],
21
+ description: "Pivot of the text block, y up: [0, 0] bottom left, [1, 1] top right."
22
+ }),
23
+ maxWidth: t.f32({ min: 0, description: "Wrap lines longer than this (0: never wrap)." }),
24
+ lineHeight: t.f32({
25
+ default: 1.2,
26
+ min: 0.1,
27
+ description: "Distance between baselines, as a multiple of the size."
28
+ }),
29
+ weight: t.f32({
30
+ min: -0.25,
31
+ max: 0.25,
32
+ description: "Thickens (+) or thins (\u2212) the strokes, in em fractions of the distance range."
33
+ }),
34
+ outline: t.struct(
35
+ {
36
+ width: t.f32({
37
+ min: 0,
38
+ max: 0.25,
39
+ description: "Outline thickness, in em (0: none). At most about half the font's distance range: range \xF7 (2 \xD7 atlas size), 0.04 em at the defaults."
40
+ }),
41
+ color: t.color({ default: [0, 0, 0, 1], description: "Outline color (linear)." })
42
+ },
43
+ { description: "An outline around the glyphs, from the distance field." }
44
+ ),
45
+ shadow: t.struct(
46
+ {
47
+ offset: t.vec2({ description: "Shadow offset in em, y up (0, 0 with softness: a glow)." }),
48
+ softness: t.f32({
49
+ min: 0,
50
+ max: 1,
51
+ description: "Blur of the shadow (0: none unless offset)."
52
+ }),
53
+ color: t.color({ default: [0, 0, 0, 0.6], description: "Shadow color (linear, alpha)." })
54
+ },
55
+ { description: "A soft shadow or glow behind the glyphs." }
56
+ )
57
+ };
58
+ const Text = defineComponent(
59
+ "text/Text",
60
+ {
61
+ ...textFields,
62
+ size: t.f32({
63
+ default: 1,
64
+ min: 0,
65
+ unit: "m",
66
+ description: "Height of one em, in world units."
67
+ }),
68
+ billboard: t.bool({ description: "Always face the camera (labels, markers)." })
69
+ },
70
+ {
71
+ description: "Text in the world, in the entity's XY plane (or facing the camera): MSDF glyphs, sharp at any size and angle.",
72
+ requires: [Transform, Visibility]
73
+ }
74
+ );
75
+ const SCREEN_CORNERS = [
76
+ "top-left",
77
+ "top",
78
+ "top-right",
79
+ "left",
80
+ "center",
81
+ "right",
82
+ "bottom-left",
83
+ "bottom",
84
+ "bottom-right"
85
+ ];
86
+ const ScreenText = defineComponent(
87
+ "text/ScreenText",
88
+ {
89
+ ...textFields,
90
+ size: t.f32({ default: 24, min: 0, description: "Height of one em, in pixels." }),
91
+ corner: t.enum(SCREEN_CORNERS, {
92
+ description: "The view point it's positioned from."
93
+ }),
94
+ position: t.vec2({ description: "Offset from the corner in pixels, x right, y down." })
95
+ },
96
+ {
97
+ description: "Text over the finished image (after tonemapping), placed in pixels from a view corner.",
98
+ requires: [Visibility]
99
+ }
100
+ );
101
+ const Localized = defineComponent(
102
+ "text/Localized",
103
+ {
104
+ value: t.string({ readonly: true, description: "The resolved string." }),
105
+ locale: t.string({ readonly: true, description: 'The locale it came from ("" if missing).' })
106
+ },
107
+ {
108
+ description: "Set by localization on entities whose UiText, Text, or ScreenText has a key: the translated string, shown instead of the text. Derived: never written to scenes or saves.",
109
+ serialize: false
110
+ }
111
+ );
112
+ export {
113
+ Localized,
114
+ SCREEN_CORNERS,
115
+ ScreenText,
116
+ TEXT_ALIGNS,
117
+ Text
118
+ };
package/dist/font.d.ts ADDED
@@ -0,0 +1,193 @@
1
+ import type { AssetRef } from '@aethervtt/shard-core';
2
+ import { SkylinePacker, Texture } from '@aethervtt/shard-texture';
3
+ import { FontSource } from './source.js';
4
+ /** One glyph in the metrics JSON. Lengths are em units; the atlas box is in pixels. */
5
+ export interface GlyphJson {
6
+ /** Codepoint; -1 for the missing-glyph box. */
7
+ u: number;
8
+ /** Advance width. */
9
+ a: number;
10
+ /** Quad bounds relative to the pen on the baseline (y up): left, bottom, right, top. */
11
+ b?: [number, number, number, number];
12
+ /** Atlas box: x, y (top-left), width, height, in pixels. The quad maps to its texel centers. */
13
+ r?: [number, number, number, number];
14
+ /** Atlas page (default 0). */
15
+ p?: number;
16
+ }
17
+ /**
18
+ * A font's imported metrics: the `Font` asset's JSON artifact. Everything a layout needs, plus
19
+ * where each glyph sits in the atlas pages (the `#Atlas` texture sub-assets).
20
+ */
21
+ export interface FontMetricsJson {
22
+ version: 1;
23
+ name: string;
24
+ /** Atlas pixels per em. */
25
+ size: number;
26
+ /** Distance range in atlas pixels. */
27
+ range: number;
28
+ /** The source font's units per em (kerning values are in these units). */
29
+ unitsPerEm: number;
30
+ /** Em units: ascent above the baseline (positive), descent below it (negative), line gap. */
31
+ ascent: number;
32
+ descent: number;
33
+ lineGap: number;
34
+ pages: {
35
+ width: number;
36
+ height: number;
37
+ }[];
38
+ glyphs: GlyphJson[];
39
+ notdef: GlyphJson;
40
+ /** Kerning pairs as flat triples: left codepoint, right codepoint, adjustment in font units. */
41
+ kerning: number[];
42
+ /** Project paths of fallback fonts, tried in order for missing glyphs. */
43
+ fallback?: string[];
44
+ /** What the atlas covers: `latin`, `latin-extended`, or `custom`. */
45
+ charset?: string;
46
+ }
47
+ /** An atlas page. Imported pages point at their texture asset; runtime pages own a Texture. */
48
+ export declare class FontPage {
49
+ readonly font: Font;
50
+ readonly index: number;
51
+ readonly width: number;
52
+ readonly height: number;
53
+ /** The page's texture asset (`font.ttf#Atlas`), for imported fonts. */
54
+ ref: AssetRef | undefined;
55
+ /**
56
+ * The page's pixels, for runtime pages and fonts built in code. Its `version` bumps when glyphs
57
+ * are added, so the renderer re-uploads it.
58
+ */
59
+ texture: Texture | undefined;
60
+ /** Made at runtime for glyphs outside the imported set. */
61
+ readonly runtime: boolean;
62
+ /** @internal Free space, for runtime pages. */
63
+ packer: SkylinePacker | undefined;
64
+ constructor(font: Font, index: number, init: {
65
+ width: number;
66
+ height: number;
67
+ ref?: AssetRef;
68
+ texture?: Texture;
69
+ runtime?: boolean;
70
+ });
71
+ }
72
+ /** A glyph ready for layout. Plane bounds in em units; uv in texture space (v down). */
73
+ export declare class Glyph {
74
+ readonly codepoint: number;
75
+ /** The font whose atlas holds the glyph (a fallback's, for fallback glyphs). */
76
+ readonly font: Font;
77
+ readonly advance: number;
78
+ readonly left: number;
79
+ readonly bottom: number;
80
+ readonly right: number;
81
+ readonly top: number;
82
+ /** Index into `font.pages`. */
83
+ readonly page: number;
84
+ /** Atlas box in pixels. */
85
+ readonly x: number;
86
+ readonly y: number;
87
+ readonly w: number;
88
+ readonly h: number;
89
+ /** Texture coordinates of the quad: (u0, v0) is its top-left. */
90
+ readonly u0: number;
91
+ readonly v0: number;
92
+ readonly u1: number;
93
+ readonly v1: number;
94
+ /** Generated at runtime rather than imported. */
95
+ readonly runtime: boolean;
96
+ /** Whether it draws anything (spaces don't). */
97
+ readonly visible: boolean;
98
+ /** @internal Range of this glyph's kerning pairs (as the left glyph) in its font's tables. */
99
+ kernStart: number;
100
+ kernCount: number;
101
+ constructor(font: Font, json: GlyphJson, runtime: boolean);
102
+ }
103
+ export interface FontLoadOptions {
104
+ /** RGBA8 pixels per page, for fonts built in code (each becomes a Texture). */
105
+ pixels?: readonly Uint8Array[];
106
+ /** Texture asset refs per page, for imported fonts. */
107
+ pageRefs?: readonly (AssetRef | undefined)[];
108
+ /** The font file, for on-demand glyphs outside the imported set. */
109
+ source?: Uint8Array | FontSource;
110
+ /** Fallback font refs, linked by the `Fonts` store. */
111
+ fallbackRefs?: readonly AssetRef[];
112
+ }
113
+ /** Side of new runtime atlas pages (pixels). */
114
+ export declare const RUNTIME_PAGE_SIZE = 512;
115
+ /** Padding around glyphs in atlas pages (pixels). */
116
+ export declare const GLYPH_PADDING = 1;
117
+ /**
118
+ * A runtime font: glyph metrics and atlas pages. `resolve` finds a glyph for any codepoint: this
119
+ * font's atlas, then an on-demand glyph from its own outlines, then each fallback (atlas, then
120
+ * on-demand), then the missing-glyph box. Resolved codepoints are cached.
121
+ */
122
+ export declare class Font {
123
+ name: string;
124
+ size: number;
125
+ range: number;
126
+ unitsPerEm: number;
127
+ ascent: number;
128
+ descent: number;
129
+ lineGap: number;
130
+ charset: string;
131
+ pages: FontPage[];
132
+ /** Glyphs this font holds (imported and generated), by codepoint. */
133
+ glyphs: Map<number, Glyph>;
134
+ notdef: Glyph;
135
+ /** Bumps when glyphs or pages are added (on-demand glyphs), or on hot reload. */
136
+ version: number;
137
+ /** Glyphs generated at runtime so far. */
138
+ runtimeGlyphs: number;
139
+ /** Codepoints nothing could draw (they show the missing-glyph box). */
140
+ readonly missing: Set<number>;
141
+ /** Fallback fonts from the import settings, before the `Fonts` store links them. */
142
+ fallbackRefs: AssetRef[];
143
+ /** The metrics' fallback paths (project paths). */
144
+ fallbackPaths: string[];
145
+ /** @internal The store that links this font's fallbacks (set by `FontStore`). */
146
+ linker: {
147
+ link(): void;
148
+ } | undefined;
149
+ /** @internal Kerning tables: right codepoints and em adjustments, grouped by left glyph. */
150
+ kernRight: Int32Array;
151
+ kernValue: Float32Array;
152
+ private _fallbacks;
153
+ private resolved;
154
+ private sourceBytes;
155
+ private _source;
156
+ private json;
157
+ private constructor();
158
+ /** Builds a font from its metrics JSON and its atlas pages (pixels or texture refs). */
159
+ static fromMetrics(json: FontMetricsJson, options?: FontLoadOptions): Font;
160
+ /** The metrics this font was loaded from. */
161
+ get metrics(): FontMetricsJson;
162
+ /** Line height in em: ascent - descent + line gap. */
163
+ get lineHeight(): number;
164
+ get fallbacks(): readonly Font[];
165
+ /** Sets the fallback fonts, tried in order. Clears the resolved-glyph cache. */
166
+ set fallbacks(fonts: readonly Font[]);
167
+ /** Forgets resolved fallback and missing glyphs (after fallbacks change). */
168
+ invalidate(): void;
169
+ /** Whether on-demand glyphs can be generated (the font's outlines are available). */
170
+ get hasOutlines(): boolean;
171
+ /** The parsed font file, parsed on first use; undefined without outlines. */
172
+ get source(): FontSource | undefined;
173
+ /** This font's own glyph for a codepoint (imported or already generated), no fallbacks. */
174
+ glyph(codepoint: number): Glyph | undefined;
175
+ /** A glyph for any codepoint; never undefined (the missing-glyph box at worst). */
176
+ resolve(codepoint: number): Glyph;
177
+ /** Whether a codepoint resolves to a real glyph (here, on demand, or in a fallback). */
178
+ has(codepoint: number): boolean;
179
+ /** Kerning between two glyphs, in em (0 across fonts). */
180
+ kerning(left: Glyph, right: Glyph): number;
181
+ private resolveSlow;
182
+ private find;
183
+ /**
184
+ * Generates a glyph from this font's outlines into a runtime atlas page, if the font has the
185
+ * codepoint. Bumps `version` and the page texture's version.
186
+ */
187
+ generate(codepoint: number): Glyph | undefined;
188
+ /** Finds room for a w×h box on a runtime page, adding a page when the last one is full. */
189
+ private allocate;
190
+ private setKerning;
191
+ /** Hot reload: takes over another font's data, keeping this object (and its fallbacks). */
192
+ copyFrom(next: Font): void;
193
+ }