@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.
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Glyph outlines as msdfgen shapes: contours of linear, quadratic, and cubic edge segments, plus
3
+ * msdfgen's simple edge coloring. Built once per glyph (not a per-pixel path), so plain objects are
4
+ * fine here; the generator flattens them into typed arrays.
5
+ */
6
+ /** Edge colors are channel bit masks (red 1, green 2, blue 4). */
7
+ export declare const EdgeColor: {
8
+ readonly black: 0;
9
+ readonly red: 1;
10
+ readonly green: 2;
11
+ readonly yellow: 3;
12
+ readonly blue: 4;
13
+ readonly magenta: 5;
14
+ readonly cyan: 6;
15
+ readonly white: 7;
16
+ };
17
+ export type EdgeKind = 1 | 2 | 3;
18
+ export interface Edge {
19
+ /** 1 linear, 2 quadratic, 3 cubic: the curve's degree. */
20
+ kind: EdgeKind;
21
+ /** Control points x0, y0, x1, y1, …: 2, 3, or 4 points. */
22
+ p: number[];
23
+ color: number;
24
+ }
25
+ export interface Contour {
26
+ edges: Edge[];
27
+ }
28
+ export interface Shape {
29
+ contours: Contour[];
30
+ }
31
+ /** Outline commands in font units, y up (opentype.js `path.commands`). */
32
+ export type OutlineCommand = {
33
+ type: 'M';
34
+ x: number;
35
+ y: number;
36
+ } | {
37
+ type: 'L';
38
+ x: number;
39
+ y: number;
40
+ } | {
41
+ type: 'Q';
42
+ x1: number;
43
+ y1: number;
44
+ x: number;
45
+ y: number;
46
+ } | {
47
+ type: 'C';
48
+ x1: number;
49
+ y1: number;
50
+ x2: number;
51
+ y2: number;
52
+ x: number;
53
+ y: number;
54
+ } | {
55
+ type: 'Z';
56
+ };
57
+ export declare function linear(x0: number, y0: number, x1: number, y1: number): Edge;
58
+ /** Builds a shape from path commands, closing open contours and dropping zero-length edges. */
59
+ export declare function shapeFromCommands(commands: readonly OutlineCommand[]): Shape;
60
+ /** A point on the edge at `t`, into `out`. */
61
+ export declare function edgePoint(e: Edge, t: number, out: number[]): void;
62
+ /** The (unnormalized) tangent at `t`, with msdfgen's fallbacks for degenerate control points. */
63
+ export declare function edgeDirection(e: Edge, t: number, out: number[]): void;
64
+ export declare function splitInThirds(e: Edge): [Edge, Edge, Edge];
65
+ export declare function reverseContour(c: Contour): void;
66
+ /** +1 or -1 by orientation (msdfgen's `Contour::winding`), 0 if empty. */
67
+ export declare function contourWinding(c: Contour): number;
68
+ /** Exact bounds [left, bottom, right, top] of the shape, or undefined when it has no edges. */
69
+ export declare function shapeBounds(shape: Shape): [number, number, number, number] | undefined;
70
+ /** Splits single-edge contours in thirds so they can take three colors (msdfgen `normalize`). */
71
+ export declare function normalizeShape(shape: Shape): void;
72
+ /**
73
+ * msdfgen's `edgeColoringSimple`: corners are where the direction turns by more than
74
+ * `angleThreshold` radians; edges between corners share a color, and neighbors across a corner
75
+ * differ, so each corner is where two channels meet.
76
+ */
77
+ export declare function colorEdgesSimple(shape: Shape, angleThreshold?: number, seedValue?: number): void;
package/dist/shape.js ADDED
@@ -0,0 +1,404 @@
1
+ const EdgeColor = {
2
+ black: 0,
3
+ red: 1,
4
+ green: 2,
5
+ yellow: 3,
6
+ blue: 4,
7
+ magenta: 5,
8
+ cyan: 6,
9
+ white: 7
10
+ };
11
+ function linear(x0, y0, x1, y1) {
12
+ return { kind: 1, p: [x0, y0, x1, y1], color: EdgeColor.white };
13
+ }
14
+ function shapeFromCommands(commands) {
15
+ const contours = [];
16
+ let edges = [];
17
+ let sx = 0;
18
+ let sy = 0;
19
+ let x = 0;
20
+ let y = 0;
21
+ const close = () => {
22
+ if (edges.length > 0 && (x !== sx || y !== sy)) edges.push(linear(x, y, sx, sy));
23
+ if (edges.length > 0) contours.push({ edges });
24
+ edges = [];
25
+ x = sx;
26
+ y = sy;
27
+ };
28
+ for (const c of commands) {
29
+ switch (c.type) {
30
+ case "M":
31
+ close();
32
+ sx = x = c.x;
33
+ sy = y = c.y;
34
+ break;
35
+ case "L":
36
+ if (c.x !== x || c.y !== y) edges.push(linear(x, y, c.x, c.y));
37
+ x = c.x;
38
+ y = c.y;
39
+ break;
40
+ case "Q":
41
+ if (c.x !== x || c.y !== y || c.x1 !== x || c.y1 !== y) {
42
+ edges.push({ kind: 2, p: [x, y, c.x1, c.y1, c.x, c.y], color: EdgeColor.white });
43
+ }
44
+ x = c.x;
45
+ y = c.y;
46
+ break;
47
+ case "C":
48
+ if (c.x !== x || c.y !== y || c.x1 !== x || c.y1 !== y || c.x2 !== x || c.y2 !== y) {
49
+ edges.push({
50
+ kind: 3,
51
+ p: [x, y, c.x1, c.y1, c.x2, c.y2, c.x, c.y],
52
+ color: EdgeColor.white
53
+ });
54
+ }
55
+ x = c.x;
56
+ y = c.y;
57
+ break;
58
+ case "Z":
59
+ close();
60
+ break;
61
+ }
62
+ }
63
+ close();
64
+ return { contours };
65
+ }
66
+ function edgePoint(e, t, out) {
67
+ const p = e.p;
68
+ const s = 1 - t;
69
+ if (e.kind === 1) {
70
+ out[0] = s * p[0] + t * p[2];
71
+ out[1] = s * p[1] + t * p[3];
72
+ } else if (e.kind === 2) {
73
+ const a = s * s;
74
+ const b = 2 * s * t;
75
+ const c = t * t;
76
+ out[0] = a * p[0] + b * p[2] + c * p[4];
77
+ out[1] = a * p[1] + b * p[3] + c * p[5];
78
+ } else {
79
+ const a = s * s * s;
80
+ const b = 3 * s * s * t;
81
+ const c = 3 * s * t * t;
82
+ const d = t * t * t;
83
+ out[0] = a * p[0] + b * p[2] + c * p[4] + d * p[6];
84
+ out[1] = a * p[1] + b * p[3] + c * p[5] + d * p[7];
85
+ }
86
+ }
87
+ function edgeDirection(e, t, out) {
88
+ const p = e.p;
89
+ if (e.kind === 1) {
90
+ out[0] = p[2] - p[0];
91
+ out[1] = p[3] - p[1];
92
+ } else if (e.kind === 2) {
93
+ const ax = p[2] - p[0];
94
+ const ay = p[3] - p[1];
95
+ const bx = p[4] - p[2];
96
+ const by = p[5] - p[3];
97
+ out[0] = ax + (bx - ax) * t;
98
+ out[1] = ay + (by - ay) * t;
99
+ if (out[0] === 0 && out[1] === 0) {
100
+ out[0] = p[4] - p[0];
101
+ out[1] = p[5] - p[1];
102
+ }
103
+ } else {
104
+ const ax = p[2] - p[0];
105
+ const ay = p[3] - p[1];
106
+ const bx = p[4] - p[2];
107
+ const by = p[5] - p[3];
108
+ const cx = p[6] - p[4];
109
+ const cy = p[7] - p[5];
110
+ const abx = ax + (bx - ax) * t;
111
+ const aby = ay + (by - ay) * t;
112
+ const bcx = bx + (cx - bx) * t;
113
+ const bcy = by + (cy - by) * t;
114
+ out[0] = abx + (bcx - abx) * t;
115
+ out[1] = aby + (bcy - aby) * t;
116
+ if (out[0] === 0 && out[1] === 0) {
117
+ if (t === 0) {
118
+ out[0] = p[4] - p[0];
119
+ out[1] = p[5] - p[1];
120
+ } else if (t === 1) {
121
+ out[0] = p[6] - p[2];
122
+ out[1] = p[7] - p[3];
123
+ }
124
+ }
125
+ }
126
+ }
127
+ function splitAt(e, t) {
128
+ const p = e.p;
129
+ const lerp = (a, b) => a + (b - a) * t;
130
+ if (e.kind === 1) {
131
+ const mx2 = lerp(p[0], p[2]);
132
+ const my2 = lerp(p[1], p[3]);
133
+ return [
134
+ { kind: 1, p: [p[0], p[1], mx2, my2], color: e.color },
135
+ { kind: 1, p: [mx2, my2, p[2], p[3]], color: e.color }
136
+ ];
137
+ }
138
+ if (e.kind === 2) {
139
+ const ax2 = lerp(p[0], p[2]);
140
+ const ay2 = lerp(p[1], p[3]);
141
+ const bx2 = lerp(p[2], p[4]);
142
+ const by2 = lerp(p[3], p[5]);
143
+ const mx2 = lerp(ax2, bx2);
144
+ const my2 = lerp(ay2, by2);
145
+ return [
146
+ { kind: 2, p: [p[0], p[1], ax2, ay2, mx2, my2], color: e.color },
147
+ { kind: 2, p: [mx2, my2, bx2, by2, p[4], p[5]], color: e.color }
148
+ ];
149
+ }
150
+ const ax = lerp(p[0], p[2]);
151
+ const ay = lerp(p[1], p[3]);
152
+ const bx = lerp(p[2], p[4]);
153
+ const by = lerp(p[3], p[5]);
154
+ const cx = lerp(p[4], p[6]);
155
+ const cy = lerp(p[5], p[7]);
156
+ const abx = lerp(ax, bx);
157
+ const aby = lerp(ay, by);
158
+ const bcx = lerp(bx, cx);
159
+ const bcy = lerp(by, cy);
160
+ const mx = lerp(abx, bcx);
161
+ const my = lerp(aby, bcy);
162
+ return [
163
+ { kind: 3, p: [p[0], p[1], ax, ay, abx, aby, mx, my], color: e.color },
164
+ { kind: 3, p: [mx, my, bcx, bcy, cx, cy, p[6], p[7]], color: e.color }
165
+ ];
166
+ }
167
+ function splitInThirds(e) {
168
+ const [a, rest] = splitAt(e, 1 / 3);
169
+ const [b, c] = splitAt(rest, 0.5);
170
+ return [a, b, c];
171
+ }
172
+ function reverseEdge(e) {
173
+ const p = e.p;
174
+ const n = p.length;
175
+ for (let i = 0; i < n / 2; i += 2) {
176
+ const j = n - 2 - i;
177
+ const x = p[i];
178
+ const y = p[i + 1];
179
+ p[i] = p[j];
180
+ p[i + 1] = p[j + 1];
181
+ p[j] = x;
182
+ p[j + 1] = y;
183
+ }
184
+ }
185
+ function reverseContour(c) {
186
+ c.edges.reverse();
187
+ for (const e of c.edges) reverseEdge(e);
188
+ }
189
+ const shoelace = (ax, ay, bx, by) => (bx - ax) * (ay + by);
190
+ function contourWinding(c) {
191
+ const edges = c.edges;
192
+ if (edges.length === 0) return 0;
193
+ let total = 0;
194
+ const a = [0, 0];
195
+ const b = [0, 0];
196
+ const cc = [0, 0];
197
+ const d = [0, 0];
198
+ if (edges.length === 1) {
199
+ edgePoint(edges[0], 0, a);
200
+ edgePoint(edges[0], 1 / 3, b);
201
+ edgePoint(edges[0], 2 / 3, cc);
202
+ total += shoelace(a[0], a[1], b[0], b[1]);
203
+ total += shoelace(b[0], b[1], cc[0], cc[1]);
204
+ total += shoelace(cc[0], cc[1], a[0], a[1]);
205
+ } else if (edges.length === 2) {
206
+ edgePoint(edges[0], 0, a);
207
+ edgePoint(edges[0], 0.5, b);
208
+ edgePoint(edges[1], 0, cc);
209
+ edgePoint(edges[1], 0.5, d);
210
+ total += shoelace(a[0], a[1], b[0], b[1]);
211
+ total += shoelace(b[0], b[1], cc[0], cc[1]);
212
+ total += shoelace(cc[0], cc[1], d[0], d[1]);
213
+ total += shoelace(d[0], d[1], a[0], a[1]);
214
+ } else {
215
+ const last = edges[edges.length - 1];
216
+ let px = last.p[0];
217
+ let py = last.p[1];
218
+ for (const e of edges) {
219
+ total += shoelace(px, py, e.p[0], e.p[1]);
220
+ px = e.p[0];
221
+ py = e.p[1];
222
+ }
223
+ }
224
+ return total > 0 ? 1 : total < 0 ? -1 : 0;
225
+ }
226
+ function shapeBounds(shape) {
227
+ let l = Infinity;
228
+ let b = Infinity;
229
+ let r = -Infinity;
230
+ let t = -Infinity;
231
+ const pt = [0, 0];
232
+ const add = (x, y) => {
233
+ if (x < l) l = x;
234
+ if (x > r) r = x;
235
+ if (y < b) b = y;
236
+ if (y > t) t = y;
237
+ };
238
+ const extremum = (e, param) => {
239
+ if (param > 0 && param < 1) {
240
+ edgePoint(e, param, pt);
241
+ add(pt[0], pt[1]);
242
+ }
243
+ };
244
+ for (const c of shape.contours) {
245
+ for (const e of c.edges) {
246
+ const p = e.p;
247
+ add(p[0], p[1]);
248
+ add(p[p.length - 2], p[p.length - 1]);
249
+ if (e.kind === 2) {
250
+ for (let k = 0; k < 2; k++) {
251
+ const den = p[k] - 2 * p[2 + k] + p[4 + k];
252
+ if (den !== 0) extremum(e, (p[k] - p[2 + k]) / den);
253
+ }
254
+ } else if (e.kind === 3) {
255
+ for (let k = 0; k < 2; k++) {
256
+ const p0 = p[k];
257
+ const p1 = p[2 + k];
258
+ const p2 = p[4 + k];
259
+ const p3 = p[6 + k];
260
+ const a = -p0 + 3 * p1 - 3 * p2 + p3;
261
+ const bb = 2 * (p0 - 2 * p1 + p2);
262
+ const cc = p1 - p0;
263
+ if (Math.abs(a) < 1e-12) {
264
+ if (bb !== 0) extremum(e, -cc / bb);
265
+ } else {
266
+ const disc = bb * bb - 4 * a * cc;
267
+ if (disc >= 0) {
268
+ const s = Math.sqrt(disc);
269
+ extremum(e, (-bb + s) / (2 * a));
270
+ extremum(e, (-bb - s) / (2 * a));
271
+ }
272
+ }
273
+ }
274
+ }
275
+ }
276
+ }
277
+ return l <= r ? [l, b, r, t] : void 0;
278
+ }
279
+ function normalizeShape(shape) {
280
+ for (const c of shape.contours) {
281
+ if (c.edges.length === 1) c.edges = splitInThirds(c.edges[0]);
282
+ }
283
+ }
284
+ function isCorner(ax, ay, bx, by, crossThreshold) {
285
+ return ax * bx + ay * by <= 0 || Math.abs(ax * by - ay * bx) > crossThreshold;
286
+ }
287
+ function normalizeInPlace(v) {
288
+ const len = Math.sqrt(v[0] * v[0] + v[1] * v[1]);
289
+ if (len === 0) {
290
+ v[0] = 0;
291
+ v[1] = 1;
292
+ } else {
293
+ v[0] = v[0] / len;
294
+ v[1] = v[1] / len;
295
+ }
296
+ }
297
+ function switchColor(color, seed, banned = EdgeColor.black) {
298
+ const combined = color & banned;
299
+ if (combined === EdgeColor.red || combined === EdgeColor.green || combined === EdgeColor.blue) {
300
+ return combined ^ EdgeColor.white;
301
+ }
302
+ if (color === EdgeColor.black || color === EdgeColor.white) {
303
+ const start = [EdgeColor.cyan, EdgeColor.magenta, EdgeColor.yellow];
304
+ const next = start[seed[0] % 3];
305
+ seed[0] = Math.floor(seed[0] / 3);
306
+ return next;
307
+ }
308
+ const shifted = color << 1 + (seed[0] & 1);
309
+ seed[0] = Math.floor(seed[0] / 2);
310
+ return (shifted | shifted >> 3) & EdgeColor.white;
311
+ }
312
+ function symmetricalTrichotomy(position, n) {
313
+ return Math.trunc(3 + 2.875 * position / (n - 1) - 1.4375 + 0.5) - 3;
314
+ }
315
+ function colorEdgesSimple(shape, angleThreshold = 3, seedValue = 0) {
316
+ const crossThreshold = Math.sin(angleThreshold);
317
+ const seed = [seedValue];
318
+ const initial = [EdgeColor.cyan, EdgeColor.magenta, EdgeColor.yellow];
319
+ let color = initial[seed[0] % 3];
320
+ seed[0] = Math.floor(seed[0] / 3);
321
+ const prev = [0, 0];
322
+ const dir = [0, 0];
323
+ for (const contour of shape.contours) {
324
+ const edges = contour.edges;
325
+ if (edges.length === 0) continue;
326
+ const corners = [];
327
+ edgeDirection(edges[edges.length - 1], 1, prev);
328
+ for (let i = 0; i < edges.length; i++) {
329
+ edgeDirection(edges[i], 0, dir);
330
+ normalizeInPlace(prev);
331
+ normalizeInPlace(dir);
332
+ if (isCorner(prev[0], prev[1], dir[0], dir[1], crossThreshold)) corners.push(i);
333
+ edgeDirection(edges[i], 1, prev);
334
+ }
335
+ if (corners.length === 0) {
336
+ color = switchColor(color, seed);
337
+ for (const e of edges) e.color = color;
338
+ } else if (corners.length === 1) {
339
+ color = switchColor(color, seed);
340
+ const c0 = color;
341
+ color = switchColor(color, seed);
342
+ const colors = [c0, EdgeColor.white, color];
343
+ const corner = corners[0];
344
+ if (edges.length >= 3) {
345
+ const m = edges.length;
346
+ for (let i = 0; i < m; i++) {
347
+ edges[(corner + i) % m].color = colors[1 + symmetricalTrichotomy(i, m)];
348
+ }
349
+ } else {
350
+ const parts = new Array(7);
351
+ const first = splitInThirds(edges[0]);
352
+ parts[0 + 3 * corner] = first[0];
353
+ parts[1 + 3 * corner] = first[1];
354
+ parts[2 + 3 * corner] = first[2];
355
+ if (edges.length >= 2) {
356
+ const second = splitInThirds(edges[1]);
357
+ parts[3 - 3 * corner] = second[0];
358
+ parts[4 - 3 * corner] = second[1];
359
+ parts[5 - 3 * corner] = second[2];
360
+ parts[0].color = parts[1].color = colors[0];
361
+ parts[2].color = parts[3].color = colors[1];
362
+ parts[4].color = parts[5].color = colors[2];
363
+ } else {
364
+ parts[0].color = colors[0];
365
+ parts[1].color = colors[1];
366
+ parts[2].color = colors[2];
367
+ }
368
+ contour.edges = parts.filter((p) => p !== void 0);
369
+ }
370
+ } else {
371
+ const cornerCount = corners.length;
372
+ let spline = 0;
373
+ const start = corners[0];
374
+ const m = edges.length;
375
+ color = switchColor(color, seed);
376
+ const initialColor = color;
377
+ for (let i = 0; i < m; i++) {
378
+ const index = (start + i) % m;
379
+ if (spline + 1 < cornerCount && corners[spline + 1] === index) {
380
+ spline++;
381
+ color = switchColor(
382
+ color,
383
+ seed,
384
+ spline === cornerCount - 1 ? initialColor : EdgeColor.black
385
+ );
386
+ }
387
+ edges[index].color = color;
388
+ }
389
+ }
390
+ }
391
+ }
392
+ export {
393
+ EdgeColor,
394
+ colorEdgesSimple,
395
+ contourWinding,
396
+ edgeDirection,
397
+ edgePoint,
398
+ linear,
399
+ normalizeShape,
400
+ reverseContour,
401
+ shapeBounds,
402
+ shapeFromCommands,
403
+ splitInThirds
404
+ };
@@ -0,0 +1,44 @@
1
+ import type { Font as OpentypeFont } from 'opentype.js';
2
+ import { type Shape } from './shape.js';
3
+ /** A parsed font file: outlines, metrics, and kerning, in font units. */
4
+ export declare class FontSource {
5
+ readonly bytes: Uint8Array;
6
+ readonly font: OpentypeFont;
7
+ readonly unitsPerEm: number;
8
+ readonly ascender: number;
9
+ readonly descender: number;
10
+ readonly lineGap: number;
11
+ readonly name: string;
12
+ private readonly cmap;
13
+ constructor(bytes: Uint8Array, path?: string);
14
+ /** The glyph index for a codepoint; 0 (.notdef) when the font doesn't have it. */
15
+ glyphIndex(codepoint: number): number;
16
+ advance(glyph: number): number;
17
+ /** The glyph's outline as a shape, in font units (y up). */
18
+ shape(glyph: number): Shape;
19
+ /** Kerning between the given glyphs, pair key `left * 65536 + right`, in font units. */
20
+ kerning(glyphs: Iterable<number>): Map<number, number>;
21
+ }
22
+ /** A box glyph for codepoints no font has: an outlined rectangle, in em units. */
23
+ export declare function tofuShape(): Shape;
24
+ export declare const TOFU_ADVANCE = 0.6;
25
+ /** One glyph's MSDF: RGBA pixels (row 0 at the top) and its quad in em units. */
26
+ export interface GlyphBitmap {
27
+ width: number;
28
+ height: number;
29
+ /** RGBA8, alpha 255. Empty for blank glyphs. */
30
+ pixels: Uint8Array;
31
+ /** Quad bounds in em units relative to the pen and baseline: left, bottom, right, top. */
32
+ plane: [number, number, number, number] | undefined;
33
+ }
34
+ export interface GlyphRaster {
35
+ /** Pixels per em. */
36
+ size: number;
37
+ /** Distance range in pixels. */
38
+ range: number;
39
+ }
40
+ /**
41
+ * Generates a glyph's MSDF. `shape` is in units where one em is `unitsPerEm` (font units, or 1 for
42
+ * shapes already in em).
43
+ */
44
+ export declare function rasterizeGlyph(shape: Shape, unitsPerEm: number, raster: GlyphRaster): GlyphBitmap;
package/dist/source.js ADDED
@@ -0,0 +1,108 @@
1
+ import { ShardError } from "@aethervtt/shard-core";
2
+ import * as opentype from "opentype.js";
3
+ const parse = opentype.parse ?? opentype.default.parse;
4
+ import { hasGposKerning, readGposKerning } from "./kerning.js";
5
+ import { blitMsdf, generateMsdf, msdfBox, prepareShape } from "./msdf.js";
6
+ import { linear, shapeBounds, shapeFromCommands } from "./shape.js";
7
+ function fontName(font) {
8
+ const names = font.names;
9
+ for (const table of [
10
+ names.windows,
11
+ names.macintosh,
12
+ names.unicode,
13
+ names
14
+ ]) {
15
+ const full = table?.fullName ?? table?.fontFamily;
16
+ if (full && typeof full === "object") {
17
+ const value = full.en ?? Object.values(full)[0];
18
+ if (typeof value === "string") return value;
19
+ }
20
+ }
21
+ return void 0;
22
+ }
23
+ class FontSource {
24
+ bytes;
25
+ font;
26
+ unitsPerEm;
27
+ ascender;
28
+ descender;
29
+ lineGap;
30
+ name;
31
+ cmap;
32
+ constructor(bytes, path = "font") {
33
+ this.bytes = bytes;
34
+ try {
35
+ const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
36
+ this.font = parse(buffer, { lowMemory: true });
37
+ } catch (cause) {
38
+ throw new ShardError(
39
+ "text/font-parse-failed",
40
+ `${path}: not a font opentype.js can read (${cause.message})`,
41
+ { path, cause, hint: "Fonts import from .ttf and .otf files (TrueType or CFF outlines)." }
42
+ );
43
+ }
44
+ const f = this.font;
45
+ this.unitsPerEm = f.unitsPerEm;
46
+ const hhea = f.tables.hhea;
47
+ this.ascender = hhea.ascender;
48
+ this.descender = hhea.descender;
49
+ this.lineGap = hhea.lineGap;
50
+ this.cmap = f.tables.cmap.glyphIndexMap;
51
+ this.name = fontName(f) ?? path;
52
+ }
53
+ /** The glyph index for a codepoint; 0 (.notdef) when the font doesn't have it. */
54
+ glyphIndex(codepoint) {
55
+ return this.cmap[codepoint] ?? 0;
56
+ }
57
+ advance(glyph) {
58
+ return this.font.glyphs.get(glyph).advanceWidth ?? 0;
59
+ }
60
+ /** The glyph's outline as a shape, in font units (y up). */
61
+ shape(glyph) {
62
+ return shapeFromCommands(this.font.glyphs.get(glyph).path.commands);
63
+ }
64
+ /** Kerning between the given glyphs, pair key `left * 65536 + right`, in font units. */
65
+ kerning(glyphs) {
66
+ if (hasGposKerning(this.bytes)) return readGposKerning(this.bytes, glyphs);
67
+ const out = /* @__PURE__ */ new Map();
68
+ const wanted = new Set(glyphs);
69
+ for (const [key, value] of Object.entries(this.font.kerningPairs ?? {})) {
70
+ const [l, r] = key.split(",").map(Number);
71
+ if (value !== 0 && wanted.has(l) && wanted.has(r)) out.set(l * 65536 + r, value);
72
+ }
73
+ return out;
74
+ }
75
+ }
76
+ function tofuShape() {
77
+ const rect = (l, b, r, t, clockwise) => clockwise ? [linear(l, b, l, t), linear(l, t, r, t), linear(r, t, r, b), linear(r, b, l, b)] : [linear(l, b, r, b), linear(r, b, r, t), linear(r, t, l, t), linear(l, t, l, b)];
78
+ return {
79
+ contours: [
80
+ { edges: rect(0.08, 0, 0.52, 0.7, true) },
81
+ { edges: rect(0.14, 0.06, 0.46, 0.64, false) }
82
+ ]
83
+ };
84
+ }
85
+ const TOFU_ADVANCE = 0.6;
86
+ function rasterizeGlyph(shape, unitsPerEm, raster) {
87
+ const bounds = shapeBounds(shape);
88
+ if (!bounds) return { width: 0, height: 0, pixels: new Uint8Array(0), plane: void 0 };
89
+ const scale = raster.size / unitsPerEm;
90
+ prepareShape(shape);
91
+ const box = msdfBox(bounds, scale, raster.range);
92
+ const sdf = generateMsdf(shape, box, { scale, range: raster.range });
93
+ const pixels = new Uint8Array(box.width * box.height * 4);
94
+ blitMsdf(sdf, box.width, box.height, pixels, box.width, 0, 0);
95
+ const inv = 1 / unitsPerEm;
96
+ return {
97
+ width: box.width,
98
+ height: box.height,
99
+ pixels,
100
+ plane: [box.plane[0] * inv, box.plane[1] * inv, box.plane[2] * inv, box.plane[3] * inv]
101
+ };
102
+ }
103
+ export {
104
+ FontSource,
105
+ TOFU_ADVANCE,
106
+ rasterizeGlyph,
107
+ tofuShape
108
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@aethervtt/shard-text",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "LICENSE",
16
+ "README.md"
17
+ ],
18
+ "dependencies": {
19
+ "@aethervtt/shard-assets": "0.0.1",
20
+ "@aethervtt/shard-core": "0.0.1",
21
+ "@aethervtt/shard-gpu": "0.0.1",
22
+ "@aethervtt/shard-render": "0.0.1",
23
+ "@aethervtt/shard-runtime": "0.0.1",
24
+ "@aethervtt/shard-texture": "0.0.1",
25
+ "@aethervtt/shard-transform": "0.0.1",
26
+ "opentype.js": "^2.0.0"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/aethervtt/shard.git",
31
+ "directory": "packages/text"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "provenance": true
36
+ },
37
+ "shard": {
38
+ "buildSha": "d6c5c6aef4675575c878b02cf2c8f738b4190229"
39
+ }
40
+ }