@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 +21 -0
- package/README.md +3 -0
- package/dist/build.d.ts +43 -0
- package/dist/build.js +106 -0
- package/dist/charset.d.ts +8 -0
- package/dist/charset.js +43 -0
- package/dist/components.d.ts +54 -0
- package/dist/components.js +118 -0
- package/dist/font.d.ts +193 -0
- package/dist/font.js +377 -0
- package/dist/importer.d.ts +40 -0
- package/dist/importer.js +193 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +140 -0
- package/dist/kerning.d.ts +8 -0
- package/dist/kerning.js +168 -0
- package/dist/layout.d.ts +84 -0
- package/dist/layout.js +297 -0
- package/dist/locale.d.ts +133 -0
- package/dist/locale.js +574 -0
- package/dist/msdf.d.ts +52 -0
- package/dist/msdf.js +1147 -0
- package/dist/opentype.d.ts +36 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +43 -0
- package/dist/preview.d.ts +2 -0
- package/dist/preview.js +76 -0
- package/dist/render.d.ts +114 -0
- package/dist/render.js +544 -0
- package/dist/shaders.d.ts +2 -0
- package/dist/shaders.js +158 -0
- package/dist/shape.d.ts +77 -0
- package/dist/shape.js +404 -0
- package/dist/source.d.ts +44 -0
- package/dist/source.js +108 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { packRects, SkylinePacker } from "@aethervtt/shard-texture";
|
|
2
|
+
import { buildFont, fontFromBytes } from "./build.js";
|
|
3
|
+
import { charsetCodepoints } from "./charset.js";
|
|
4
|
+
import { Localized, SCREEN_CORNERS, ScreenText, TEXT_ALIGNS, Text } from "./components.js";
|
|
5
|
+
import {
|
|
6
|
+
Font,
|
|
7
|
+
FontPage,
|
|
8
|
+
GLYPH_PADDING,
|
|
9
|
+
Glyph,
|
|
10
|
+
RUNTIME_PAGE_SIZE
|
|
11
|
+
} from "./font.js";
|
|
12
|
+
import {
|
|
13
|
+
atlasLabel,
|
|
14
|
+
FontAssetType,
|
|
15
|
+
FontImporter,
|
|
16
|
+
FontImportSettings,
|
|
17
|
+
FontStore,
|
|
18
|
+
Fonts
|
|
19
|
+
} from "./importer.js";
|
|
20
|
+
import { hasGposKerning, readGposKerning } from "./kerning.js";
|
|
21
|
+
import {
|
|
22
|
+
isCjk,
|
|
23
|
+
layoutText,
|
|
24
|
+
measureText,
|
|
25
|
+
TextLayout
|
|
26
|
+
} from "./layout.js";
|
|
27
|
+
import {
|
|
28
|
+
installLocalization,
|
|
29
|
+
KEYED_COMPONENTS,
|
|
30
|
+
Locale,
|
|
31
|
+
LocaleState,
|
|
32
|
+
LocaleStore,
|
|
33
|
+
loadStringTables,
|
|
34
|
+
localeChain,
|
|
35
|
+
localeMethods,
|
|
36
|
+
localeOfPath,
|
|
37
|
+
localizationKeysIn,
|
|
38
|
+
localizeComponent,
|
|
39
|
+
localizeText,
|
|
40
|
+
PLURAL_CATEGORIES,
|
|
41
|
+
placeholdersOf,
|
|
42
|
+
StringTable,
|
|
43
|
+
StringTableAssetType,
|
|
44
|
+
StringTableImporter,
|
|
45
|
+
StringTables,
|
|
46
|
+
setLocale,
|
|
47
|
+
tr,
|
|
48
|
+
validateLocalization,
|
|
49
|
+
validateStringTable
|
|
50
|
+
} from "./locale.js";
|
|
51
|
+
import {
|
|
52
|
+
blitMsdf,
|
|
53
|
+
generateMsdf,
|
|
54
|
+
msdfBox,
|
|
55
|
+
prepareShape
|
|
56
|
+
} from "./msdf.js";
|
|
57
|
+
import { textPlugin } from "./plugin.js";
|
|
58
|
+
import { describeText, prepareTexts, TextRenderer, TextStore } from "./render.js";
|
|
59
|
+
import { TEXT_SHADERS } from "./shaders.js";
|
|
60
|
+
import {
|
|
61
|
+
colorEdgesSimple,
|
|
62
|
+
EdgeColor,
|
|
63
|
+
shapeBounds,
|
|
64
|
+
shapeFromCommands
|
|
65
|
+
} from "./shape.js";
|
|
66
|
+
import {
|
|
67
|
+
FontSource,
|
|
68
|
+
rasterizeGlyph,
|
|
69
|
+
TOFU_ADVANCE,
|
|
70
|
+
tofuShape
|
|
71
|
+
} from "./source.js";
|
|
72
|
+
export {
|
|
73
|
+
EdgeColor,
|
|
74
|
+
Font,
|
|
75
|
+
FontAssetType,
|
|
76
|
+
FontImportSettings,
|
|
77
|
+
FontImporter,
|
|
78
|
+
FontPage,
|
|
79
|
+
FontSource,
|
|
80
|
+
FontStore,
|
|
81
|
+
Fonts,
|
|
82
|
+
GLYPH_PADDING,
|
|
83
|
+
Glyph,
|
|
84
|
+
KEYED_COMPONENTS,
|
|
85
|
+
Locale,
|
|
86
|
+
LocaleState,
|
|
87
|
+
LocaleStore,
|
|
88
|
+
Localized,
|
|
89
|
+
PLURAL_CATEGORIES,
|
|
90
|
+
RUNTIME_PAGE_SIZE,
|
|
91
|
+
SCREEN_CORNERS,
|
|
92
|
+
ScreenText,
|
|
93
|
+
SkylinePacker,
|
|
94
|
+
StringTable,
|
|
95
|
+
StringTableAssetType,
|
|
96
|
+
StringTableImporter,
|
|
97
|
+
StringTables,
|
|
98
|
+
TEXT_ALIGNS,
|
|
99
|
+
TEXT_SHADERS,
|
|
100
|
+
TOFU_ADVANCE,
|
|
101
|
+
Text,
|
|
102
|
+
TextLayout,
|
|
103
|
+
TextRenderer,
|
|
104
|
+
TextStore,
|
|
105
|
+
atlasLabel,
|
|
106
|
+
blitMsdf,
|
|
107
|
+
buildFont,
|
|
108
|
+
charsetCodepoints,
|
|
109
|
+
colorEdgesSimple,
|
|
110
|
+
describeText,
|
|
111
|
+
fontFromBytes,
|
|
112
|
+
generateMsdf,
|
|
113
|
+
hasGposKerning,
|
|
114
|
+
installLocalization,
|
|
115
|
+
isCjk,
|
|
116
|
+
layoutText,
|
|
117
|
+
loadStringTables,
|
|
118
|
+
localeChain,
|
|
119
|
+
localeMethods,
|
|
120
|
+
localeOfPath,
|
|
121
|
+
localizationKeysIn,
|
|
122
|
+
localizeComponent,
|
|
123
|
+
localizeText,
|
|
124
|
+
measureText,
|
|
125
|
+
msdfBox,
|
|
126
|
+
packRects,
|
|
127
|
+
placeholdersOf,
|
|
128
|
+
prepareShape,
|
|
129
|
+
prepareTexts,
|
|
130
|
+
rasterizeGlyph,
|
|
131
|
+
readGposKerning,
|
|
132
|
+
setLocale,
|
|
133
|
+
shapeBounds,
|
|
134
|
+
shapeFromCommands,
|
|
135
|
+
textPlugin,
|
|
136
|
+
tofuShape,
|
|
137
|
+
tr,
|
|
138
|
+
validateLocalization,
|
|
139
|
+
validateStringTable
|
|
140
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kerning pairs straight from the font's bytes: GPOS pair adjustment (lookup type 2, also inside
|
|
3
|
+
* type 9 extension lookups, which opentype.js doesn't read) under the `kern` feature. Only pairs
|
|
4
|
+
* between the given glyphs are collected. Values are in font units.
|
|
5
|
+
*/
|
|
6
|
+
export declare function readGposKerning(bytes: Uint8Array, glyphs: Iterable<number>): Map<number, number>;
|
|
7
|
+
/** Whether the font has a GPOS `kern` feature (then the legacy `kern` table is ignored). */
|
|
8
|
+
export declare function hasGposKerning(bytes: Uint8Array): boolean;
|
package/dist/kerning.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
function tableOffset(view, tag) {
|
|
2
|
+
if (view.byteLength < 12) return -1;
|
|
3
|
+
const count = view.getUint16(4);
|
|
4
|
+
for (let i = 0; i < count; i++) {
|
|
5
|
+
const rec = 12 + i * 16;
|
|
6
|
+
if (rec + 16 > view.byteLength) return -1;
|
|
7
|
+
let name = "";
|
|
8
|
+
for (let c = 0; c < 4; c++) name += String.fromCharCode(view.getUint8(rec + c));
|
|
9
|
+
if (name === tag) return view.getUint32(rec + 8);
|
|
10
|
+
}
|
|
11
|
+
return -1;
|
|
12
|
+
}
|
|
13
|
+
function readCoverage(view, offset) {
|
|
14
|
+
const out = /* @__PURE__ */ new Map();
|
|
15
|
+
const format = view.getUint16(offset);
|
|
16
|
+
if (format === 1) {
|
|
17
|
+
const count = view.getUint16(offset + 2);
|
|
18
|
+
for (let i = 0; i < count; i++) out.set(view.getUint16(offset + 4 + i * 2), i);
|
|
19
|
+
} else if (format === 2) {
|
|
20
|
+
const count = view.getUint16(offset + 2);
|
|
21
|
+
for (let i = 0; i < count; i++) {
|
|
22
|
+
const rec = offset + 4 + i * 6;
|
|
23
|
+
const start = view.getUint16(rec);
|
|
24
|
+
const end = view.getUint16(rec + 2);
|
|
25
|
+
const index = view.getUint16(rec + 4);
|
|
26
|
+
for (let g = start; g <= end; g++) out.set(g, index + g - start);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
function readClassDef(view, offset) {
|
|
32
|
+
const out = /* @__PURE__ */ new Map();
|
|
33
|
+
const format = view.getUint16(offset);
|
|
34
|
+
if (format === 1) {
|
|
35
|
+
const start = view.getUint16(offset + 2);
|
|
36
|
+
const count = view.getUint16(offset + 4);
|
|
37
|
+
for (let i = 0; i < count; i++) out.set(start + i, view.getUint16(offset + 6 + i * 2));
|
|
38
|
+
} else if (format === 2) {
|
|
39
|
+
const count = view.getUint16(offset + 2);
|
|
40
|
+
for (let i = 0; i < count; i++) {
|
|
41
|
+
const rec = offset + 4 + i * 6;
|
|
42
|
+
const start = view.getUint16(rec);
|
|
43
|
+
const end = view.getUint16(rec + 2);
|
|
44
|
+
const cls = view.getUint16(rec + 4);
|
|
45
|
+
for (let g = start; g <= end; g++) out.set(g, cls);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function popcount(v) {
|
|
51
|
+
let n = 0;
|
|
52
|
+
for (let x = v; x; x >>= 1) n += x & 1;
|
|
53
|
+
return n;
|
|
54
|
+
}
|
|
55
|
+
function pairValue(view, rec, format1, format2) {
|
|
56
|
+
let v = 0;
|
|
57
|
+
if (format1 & 4) v += view.getInt16(rec + 2 * popcount(format1 & 3));
|
|
58
|
+
if (format2 & 1) v += view.getInt16(rec + 2 * popcount(format1));
|
|
59
|
+
return v;
|
|
60
|
+
}
|
|
61
|
+
const pairKey = (a, b) => a * 65536 + b;
|
|
62
|
+
function readGposKerning(bytes, glyphs) {
|
|
63
|
+
const out = /* @__PURE__ */ new Map();
|
|
64
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
65
|
+
const gpos = tableOffset(view, "GPOS");
|
|
66
|
+
if (gpos < 0) return out;
|
|
67
|
+
const wanted = [...new Set(glyphs)];
|
|
68
|
+
const featureList = gpos + view.getUint16(gpos + 6);
|
|
69
|
+
const lookupList = gpos + view.getUint16(gpos + 8);
|
|
70
|
+
const lookupIndices = /* @__PURE__ */ new Set();
|
|
71
|
+
const featureCount = view.getUint16(featureList);
|
|
72
|
+
for (let i = 0; i < featureCount; i++) {
|
|
73
|
+
const rec = featureList + 2 + i * 6;
|
|
74
|
+
let tag = "";
|
|
75
|
+
for (let c = 0; c < 4; c++) tag += String.fromCharCode(view.getUint8(rec + c));
|
|
76
|
+
if (tag !== "kern") continue;
|
|
77
|
+
const feature = featureList + view.getUint16(rec + 4);
|
|
78
|
+
const count = view.getUint16(feature + 2);
|
|
79
|
+
for (let j = 0; j < count; j++) lookupIndices.add(view.getUint16(feature + 4 + j * 2));
|
|
80
|
+
}
|
|
81
|
+
const lookupCount = view.getUint16(lookupList);
|
|
82
|
+
for (const index of [...lookupIndices].sort((a, b) => a - b)) {
|
|
83
|
+
if (index >= lookupCount) continue;
|
|
84
|
+
const lookup = lookupList + view.getUint16(lookupList + 2 + index * 2);
|
|
85
|
+
const type = view.getUint16(lookup);
|
|
86
|
+
const subCount = view.getUint16(lookup + 4);
|
|
87
|
+
const matched = /* @__PURE__ */ new Set();
|
|
88
|
+
for (let s = 0; s < subCount; s++) {
|
|
89
|
+
let sub = lookup + view.getUint16(lookup + 6 + s * 2);
|
|
90
|
+
let subType = type;
|
|
91
|
+
if (type === 9) {
|
|
92
|
+
subType = view.getUint16(sub + 2);
|
|
93
|
+
sub += view.getUint32(sub + 4);
|
|
94
|
+
}
|
|
95
|
+
if (subType !== 2) continue;
|
|
96
|
+
readPairPos(view, sub, wanted, matched, out);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const [k, v] of out) if (v === 0) out.delete(k);
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
function readPairPos(view, sub, wanted, matched, out) {
|
|
103
|
+
const format = view.getUint16(sub);
|
|
104
|
+
const coverage = readCoverage(view, sub + view.getUint16(sub + 2));
|
|
105
|
+
const format1 = view.getUint16(sub + 4);
|
|
106
|
+
const format2 = view.getUint16(sub + 6);
|
|
107
|
+
const recordSize = 2 * (popcount(format1) + popcount(format2));
|
|
108
|
+
if (format === 1) {
|
|
109
|
+
const setOffsets = sub + 10;
|
|
110
|
+
for (const g1 of wanted) {
|
|
111
|
+
const ci = coverage.get(g1);
|
|
112
|
+
if (ci === void 0) continue;
|
|
113
|
+
const set = sub + view.getUint16(setOffsets + ci * 2);
|
|
114
|
+
const count = view.getUint16(set);
|
|
115
|
+
const seconds = /* @__PURE__ */ new Map();
|
|
116
|
+
for (let i = 0; i < count; i++) {
|
|
117
|
+
const rec = set + 2 + i * (2 + recordSize);
|
|
118
|
+
seconds.set(view.getUint16(rec), rec + 2);
|
|
119
|
+
}
|
|
120
|
+
for (const g2 of wanted) {
|
|
121
|
+
const rec = seconds.get(g2);
|
|
122
|
+
const key = pairKey(g1, g2);
|
|
123
|
+
if (rec === void 0 || matched.has(key)) continue;
|
|
124
|
+
matched.add(key);
|
|
125
|
+
out.set(key, (out.get(key) ?? 0) + pairValue(view, rec, format1, format2));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} else if (format === 2) {
|
|
129
|
+
const class1 = readClassDef(view, sub + view.getUint16(sub + 8));
|
|
130
|
+
const class2 = readClassDef(view, sub + view.getUint16(sub + 10));
|
|
131
|
+
const class1Count = view.getUint16(sub + 12);
|
|
132
|
+
const class2Count = view.getUint16(sub + 14);
|
|
133
|
+
const records = sub + 16;
|
|
134
|
+
for (const g1 of wanted) {
|
|
135
|
+
if (!coverage.has(g1)) continue;
|
|
136
|
+
const c1 = class1.get(g1) ?? 0;
|
|
137
|
+
if (c1 >= class1Count) continue;
|
|
138
|
+
for (const g2 of wanted) {
|
|
139
|
+
const key = pairKey(g1, g2);
|
|
140
|
+
if (matched.has(key)) continue;
|
|
141
|
+
const c2 = class2.get(g2) ?? 0;
|
|
142
|
+
if (c2 >= class2Count) continue;
|
|
143
|
+
matched.add(key);
|
|
144
|
+
const rec = records + (c1 * class2Count + c2) * recordSize;
|
|
145
|
+
const v = pairValue(view, rec, format1, format2);
|
|
146
|
+
if (v !== 0) out.set(key, (out.get(key) ?? 0) + v);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function hasGposKerning(bytes) {
|
|
152
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
153
|
+
const gpos = tableOffset(view, "GPOS");
|
|
154
|
+
if (gpos < 0) return false;
|
|
155
|
+
const featureList = gpos + view.getUint16(gpos + 6);
|
|
156
|
+
const count = view.getUint16(featureList);
|
|
157
|
+
for (let i = 0; i < count; i++) {
|
|
158
|
+
const rec = featureList + 2 + i * 6;
|
|
159
|
+
let tag = "";
|
|
160
|
+
for (let c = 0; c < 4; c++) tag += String.fromCharCode(view.getUint8(rec + c));
|
|
161
|
+
if (tag === "kern") return true;
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
export {
|
|
166
|
+
hasGposKerning,
|
|
167
|
+
readGposKerning
|
|
168
|
+
};
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Font, FontPage, Glyph } from './font.js';
|
|
2
|
+
export type TextAlign = 'left' | 'center' | 'right';
|
|
3
|
+
export interface TextLayoutOptions {
|
|
4
|
+
/** Layout units per em (world units, or pixels for screen text). Default 1. */
|
|
5
|
+
size?: number;
|
|
6
|
+
/** Line alignment within the block. Default 'left'. */
|
|
7
|
+
align?: TextAlign;
|
|
8
|
+
/**
|
|
9
|
+
* Pivot of the text block, normalized, y up: [0, 0] is the bottom-left corner, [1, 1] the
|
|
10
|
+
* top-right, [0.5, 0.5] (default) the center. The pivot sits at the layout origin.
|
|
11
|
+
*/
|
|
12
|
+
anchor?: readonly [number, number];
|
|
13
|
+
/** Wrap lines longer than this (layout units); 0 (default) never wraps. */
|
|
14
|
+
maxWidth?: number;
|
|
15
|
+
/** Distance between baselines, as a multiple of the size. Default 1.2. */
|
|
16
|
+
lineHeight?: number;
|
|
17
|
+
/** Extra space between glyphs, in em. Default 0. */
|
|
18
|
+
letterSpacing?: number;
|
|
19
|
+
/** Apply kerning pairs. Default true. */
|
|
20
|
+
kerning?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface TextLine {
|
|
23
|
+
/** UTF-16 range in the value: [start, end). Excludes the break's collapsed space or '\n'. */
|
|
24
|
+
start: number;
|
|
25
|
+
end: number;
|
|
26
|
+
/** Advance width, trailing spaces excluded (layout units). */
|
|
27
|
+
width: number;
|
|
28
|
+
}
|
|
29
|
+
export interface TextMetrics {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
lines: TextLine[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Glyph instances of a laid-out string, in reusable typed arrays: laying out into the same object
|
|
36
|
+
* again doesn't allocate once its arrays are big enough. Positions are layout units, y up, with
|
|
37
|
+
* the block's anchor at the origin.
|
|
38
|
+
*/
|
|
39
|
+
export declare class TextLayout {
|
|
40
|
+
/** Glyph quads drawn (spaces and other blank glyphs aren't). */
|
|
41
|
+
count: number;
|
|
42
|
+
/** Per quad: x, y (bottom-left corner), width, height. */
|
|
43
|
+
quads: Float32Array<ArrayBuffer>;
|
|
44
|
+
/** Per quad: u0, v0, u1, v1 in texture space (v down); (u0, v0) is the quad's top-left. */
|
|
45
|
+
uvs: Float32Array<ArrayBuffer>;
|
|
46
|
+
/** Per quad: index into `pages`. */
|
|
47
|
+
page: Uint16Array<ArrayBuffer>;
|
|
48
|
+
/** Per quad: UTF-16 index of its character in the value. */
|
|
49
|
+
source: Uint32Array<ArrayBuffer>;
|
|
50
|
+
/** Per quad: the glyph (font, codepoint, runtime or fallback). Entries past `count` are stale. */
|
|
51
|
+
glyphs: Glyph[];
|
|
52
|
+
/** Atlas pages the quads sample, in first-use order. Entries past `pageCount` are stale. */
|
|
53
|
+
pages: FontPage[];
|
|
54
|
+
pageCount: number;
|
|
55
|
+
/** Block size (layout units): widest line, and ascent to descent over all lines. */
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
/** Block bounds relative to the origin (after the anchor). */
|
|
59
|
+
left: number;
|
|
60
|
+
bottom: number;
|
|
61
|
+
lineCount: number;
|
|
62
|
+
/** Per line: UTF-16 [start, end), width, first quad. */
|
|
63
|
+
lineStart: Uint32Array<ArrayBuffer>;
|
|
64
|
+
lineEnd: Uint32Array<ArrayBuffer>;
|
|
65
|
+
lineWidth: Float32Array<ArrayBuffer>;
|
|
66
|
+
lineQuad: Uint32Array<ArrayBuffer>;
|
|
67
|
+
/** Characters that fell back to the missing-glyph box. */
|
|
68
|
+
missing: number;
|
|
69
|
+
/** Bumps on every layout. */
|
|
70
|
+
version: number;
|
|
71
|
+
/** Lines as objects (allocates). */
|
|
72
|
+
lines(): TextLine[];
|
|
73
|
+
}
|
|
74
|
+
/** CJK ideographs, kana, Hangul, and fullwidth forms: a line may break between any two. */
|
|
75
|
+
export declare function isCjk(cp: number): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Lays out `value` into `out` (reused; its arrays grow as needed): advances and kerning, line
|
|
78
|
+
* breaks at spaces (the breaking space collapses), at '\n', between CJK characters, and inside a
|
|
79
|
+
* word wider than `maxWidth`; then alignment and the anchor. Doesn't allocate once `out` is big
|
|
80
|
+
* enough, except when a new glyph has to be generated.
|
|
81
|
+
*/
|
|
82
|
+
export declare function layoutText(font: Font, value: string, options?: TextLayoutOptions, out?: TextLayout): TextLayout;
|
|
83
|
+
/** Measures a string without keeping glyph instances: its block size and line breaks. */
|
|
84
|
+
export declare function measureText(font: Font, value: string, options?: TextLayoutOptions): TextMetrics;
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
function grow(a, n) {
|
|
2
|
+
if (a.length >= n) return a;
|
|
3
|
+
let len = Math.max(16, a.length * 2);
|
|
4
|
+
while (len < n) len *= 2;
|
|
5
|
+
const out = new a.constructor(len);
|
|
6
|
+
out.set(a);
|
|
7
|
+
return out;
|
|
8
|
+
}
|
|
9
|
+
class TextLayout {
|
|
10
|
+
/** Glyph quads drawn (spaces and other blank glyphs aren't). */
|
|
11
|
+
count = 0;
|
|
12
|
+
/** Per quad: x, y (bottom-left corner), width, height. */
|
|
13
|
+
quads = new Float32Array(64);
|
|
14
|
+
/** Per quad: u0, v0, u1, v1 in texture space (v down); (u0, v0) is the quad's top-left. */
|
|
15
|
+
uvs = new Float32Array(64);
|
|
16
|
+
/** Per quad: index into `pages`. */
|
|
17
|
+
page = new Uint16Array(16);
|
|
18
|
+
/** Per quad: UTF-16 index of its character in the value. */
|
|
19
|
+
source = new Uint32Array(16);
|
|
20
|
+
/** Per quad: the glyph (font, codepoint, runtime or fallback). Entries past `count` are stale. */
|
|
21
|
+
glyphs = [];
|
|
22
|
+
/** Atlas pages the quads sample, in first-use order. Entries past `pageCount` are stale. */
|
|
23
|
+
pages = [];
|
|
24
|
+
pageCount = 0;
|
|
25
|
+
/** Block size (layout units): widest line, and ascent to descent over all lines. */
|
|
26
|
+
width = 0;
|
|
27
|
+
height = 0;
|
|
28
|
+
/** Block bounds relative to the origin (after the anchor). */
|
|
29
|
+
left = 0;
|
|
30
|
+
bottom = 0;
|
|
31
|
+
lineCount = 0;
|
|
32
|
+
/** Per line: UTF-16 [start, end), width, first quad. */
|
|
33
|
+
lineStart = new Uint32Array(8);
|
|
34
|
+
lineEnd = new Uint32Array(8);
|
|
35
|
+
lineWidth = new Float32Array(8);
|
|
36
|
+
lineQuad = new Uint32Array(8);
|
|
37
|
+
/** Characters that fell back to the missing-glyph box. */
|
|
38
|
+
missing = 0;
|
|
39
|
+
/** Bumps on every layout. */
|
|
40
|
+
version = 0;
|
|
41
|
+
/** Lines as objects (allocates). */
|
|
42
|
+
lines() {
|
|
43
|
+
const out = [];
|
|
44
|
+
for (let i = 0; i < this.lineCount; i++) {
|
|
45
|
+
out.push({ start: this.lineStart[i], end: this.lineEnd[i], width: this.lineWidth[i] });
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function isCjk(cp) {
|
|
51
|
+
return cp >= 11904 && cp <= 40959 || cp >= 4352 && cp <= 4607 || cp >= 43360 && cp <= 43391 || cp >= 44032 && cp <= 55215 || cp >= 63744 && cp <= 64255 || cp >= 65072 && cp <= 65103 || cp >= 65280 && cp <= 65519 || cp >= 131072 && cp <= 262143;
|
|
52
|
+
}
|
|
53
|
+
const NO_BREAK_BEFORE = /* @__PURE__ */ new Set();
|
|
54
|
+
for (const ch of "\u3001\u3002\uFF0C\uFF0E\u30FB\uFF1A\uFF1B\uFF1F\uFF01\uFF09\u300D\u300F\u3011\u3015\u3009\u300B\u3019\u3017\u309D\u309E\u30FD\u30FE\u30FC\u3041\u3043\u3045\u3047\u3049\u3063\u3083\u3085\u3087\u308E\u3095\u3096\u30A1\u30A3\u30A5\u30A7\u30A9\u30C3\u30E3\u30E5\u30E7\u30EE\u30F5\u30F6\u3005\u303B\u2010\u30A0\u301C\u2026\u2025\uFF01\uFF09\uFF0C\uFF0E\uFF1A\uFF1B\uFF1F\uFF3D\uFF5D\uFF61\uFF63\uFF64\uFF65\uFF70") {
|
|
55
|
+
NO_BREAK_BEFORE.add(ch.codePointAt(0));
|
|
56
|
+
}
|
|
57
|
+
const NO_BREAK_AFTER = /* @__PURE__ */ new Set();
|
|
58
|
+
for (const ch of "\uFF08\u300C\u300E\u3010\u3014\u3008\u300A\u3018\u3016\uFF3B\uFF5B\uFF62") NO_BREAK_AFTER.add(ch.codePointAt(0));
|
|
59
|
+
const isSpace = (cp) => cp === 32 || cp === 9 || cp === 12288;
|
|
60
|
+
const scratch = new TextLayout();
|
|
61
|
+
function layoutText(font, value, options = {}, out = new TextLayout()) {
|
|
62
|
+
const size = options.size ?? 1;
|
|
63
|
+
const maxWidth = options.maxWidth ?? 0;
|
|
64
|
+
const lineAdvance = (options.lineHeight ?? 1.2) * size;
|
|
65
|
+
const spacing = (options.letterSpacing ?? 0) * size;
|
|
66
|
+
const kerning = options.kerning !== false;
|
|
67
|
+
const alignFactor = options.align === "center" ? 0.5 : options.align === "right" ? 1 : 0;
|
|
68
|
+
const anchor = options.anchor;
|
|
69
|
+
const anchorX = anchor ? anchor[0] : 0.5;
|
|
70
|
+
const anchorY = anchor ? anchor[1] : 0.5;
|
|
71
|
+
const limit = maxWidth > 0 ? maxWidth + size * 1e-6 : Infinity;
|
|
72
|
+
out.count = 0;
|
|
73
|
+
out.lineCount = 0;
|
|
74
|
+
out.missing = 0;
|
|
75
|
+
out.version++;
|
|
76
|
+
const notdef = font.notdef;
|
|
77
|
+
let count = 0;
|
|
78
|
+
let quads = out.quads;
|
|
79
|
+
let uvs = out.uvs;
|
|
80
|
+
let pageOf = out.page;
|
|
81
|
+
let sourceOf = out.source;
|
|
82
|
+
const glyphs = out.glyphs;
|
|
83
|
+
const pages = out.pages;
|
|
84
|
+
let lastPage;
|
|
85
|
+
let lastPageIndex = 0;
|
|
86
|
+
let pageCount = 0;
|
|
87
|
+
let lineStartChar = 0;
|
|
88
|
+
let lineQuad = 0;
|
|
89
|
+
let pen = 0;
|
|
90
|
+
let contentEnd = 0;
|
|
91
|
+
let contentEndChar = 0;
|
|
92
|
+
let prev = null;
|
|
93
|
+
let prevCp = -1;
|
|
94
|
+
let prevSpace = false;
|
|
95
|
+
let opp = false;
|
|
96
|
+
let oppChar = 0;
|
|
97
|
+
let oppQuad = 0;
|
|
98
|
+
let oppPen = 0;
|
|
99
|
+
let oppWidth = 0;
|
|
100
|
+
let oppEnd = 0;
|
|
101
|
+
const n = value.length;
|
|
102
|
+
let i = 0;
|
|
103
|
+
while (i <= n) {
|
|
104
|
+
let cp = -1;
|
|
105
|
+
let next = i + 1;
|
|
106
|
+
if (i < n) {
|
|
107
|
+
cp = value.charCodeAt(i);
|
|
108
|
+
if (cp >= 55296 && cp <= 56319 && i + 1 < n) {
|
|
109
|
+
const lo = value.charCodeAt(i + 1);
|
|
110
|
+
if (lo >= 56320 && lo <= 57343) {
|
|
111
|
+
cp = (cp - 55296 << 10) + (lo - 56320) + 65536;
|
|
112
|
+
next = i + 2;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (cp === -1 || cp === 10) {
|
|
117
|
+
out.lineCount = pushLine(out, lineStartChar, i, contentEnd, lineQuad);
|
|
118
|
+
if (cp === -1) break;
|
|
119
|
+
lineStartChar = next;
|
|
120
|
+
lineQuad = count;
|
|
121
|
+
pen = 0;
|
|
122
|
+
contentEnd = 0;
|
|
123
|
+
contentEndChar = next;
|
|
124
|
+
prev = null;
|
|
125
|
+
prevCp = -1;
|
|
126
|
+
prevSpace = false;
|
|
127
|
+
opp = false;
|
|
128
|
+
i = next;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (cp === 13) {
|
|
132
|
+
i = next;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const space = isSpace(cp);
|
|
136
|
+
let g = font.resolve(cp === 9 ? 32 : cp);
|
|
137
|
+
if (g === notdef) {
|
|
138
|
+
if (space) g = font.resolve(32);
|
|
139
|
+
else out.missing++;
|
|
140
|
+
}
|
|
141
|
+
let kern = 0;
|
|
142
|
+
if (prev !== null) {
|
|
143
|
+
kern = spacing;
|
|
144
|
+
if (kerning && prev.kernCount > 0 && prev.font === g.font) {
|
|
145
|
+
const f = prev.font;
|
|
146
|
+
const keys = f.kernRight;
|
|
147
|
+
const target = g.codepoint;
|
|
148
|
+
let lo = prev.kernStart;
|
|
149
|
+
let hi = lo + prev.kernCount - 1;
|
|
150
|
+
while (lo <= hi) {
|
|
151
|
+
const mid = lo + hi >> 1;
|
|
152
|
+
const k = keys[mid];
|
|
153
|
+
if (k === target) {
|
|
154
|
+
kern += f.kernValue[mid] * size;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
if (k < target) lo = mid + 1;
|
|
158
|
+
else hi = mid - 1;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!space && prev !== null) {
|
|
163
|
+
if (prevSpace) {
|
|
164
|
+
if (contentEnd > 0 || count > lineQuad) {
|
|
165
|
+
opp = true;
|
|
166
|
+
oppChar = i;
|
|
167
|
+
oppQuad = count;
|
|
168
|
+
oppPen = pen + kern;
|
|
169
|
+
oppWidth = contentEnd;
|
|
170
|
+
oppEnd = contentEndChar;
|
|
171
|
+
}
|
|
172
|
+
} else if ((isCjk(cp) || isCjk(prevCp)) && !NO_BREAK_BEFORE.has(cp) && !NO_BREAK_AFTER.has(prevCp)) {
|
|
173
|
+
opp = true;
|
|
174
|
+
oppChar = i;
|
|
175
|
+
oppQuad = count;
|
|
176
|
+
oppPen = pen + kern;
|
|
177
|
+
oppWidth = contentEnd;
|
|
178
|
+
oppEnd = i;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
let x = pen + kern;
|
|
182
|
+
let advanceEnd = x + g.advance * size;
|
|
183
|
+
if (!space && advanceEnd > limit && (count > lineQuad || contentEnd > 0)) {
|
|
184
|
+
if (opp) {
|
|
185
|
+
out.lineCount = pushLine(out, lineStartChar, oppEnd, oppWidth, lineQuad);
|
|
186
|
+
for (let q = oppQuad; q < count; q++) quads[q * 4] = quads[q * 4] - oppPen;
|
|
187
|
+
lineStartChar = oppChar;
|
|
188
|
+
lineQuad = oppQuad;
|
|
189
|
+
pen -= oppPen;
|
|
190
|
+
contentEnd -= oppPen;
|
|
191
|
+
if (contentEnd < 0) contentEnd = 0;
|
|
192
|
+
x -= oppPen;
|
|
193
|
+
advanceEnd -= oppPen;
|
|
194
|
+
opp = false;
|
|
195
|
+
}
|
|
196
|
+
if (advanceEnd > limit && (count > lineQuad || contentEnd > 0)) {
|
|
197
|
+
out.lineCount = pushLine(out, lineStartChar, i, contentEnd, lineQuad);
|
|
198
|
+
lineStartChar = i;
|
|
199
|
+
lineQuad = count;
|
|
200
|
+
advanceEnd -= x;
|
|
201
|
+
x = 0;
|
|
202
|
+
pen = 0;
|
|
203
|
+
contentEnd = 0;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (g.visible) {
|
|
207
|
+
if (count * 4 + 4 > quads.length) {
|
|
208
|
+
quads = out.quads = grow(quads, count * 4 + 4);
|
|
209
|
+
uvs = out.uvs = grow(uvs, count * 4 + 4);
|
|
210
|
+
pageOf = out.page = grow(pageOf, count + 1);
|
|
211
|
+
sourceOf = out.source = grow(sourceOf, count + 1);
|
|
212
|
+
}
|
|
213
|
+
const o = count * 4;
|
|
214
|
+
quads[o] = x + g.left * size;
|
|
215
|
+
quads[o + 1] = g.bottom * size;
|
|
216
|
+
quads[o + 2] = (g.right - g.left) * size;
|
|
217
|
+
quads[o + 3] = (g.top - g.bottom) * size;
|
|
218
|
+
uvs[o] = g.u0;
|
|
219
|
+
uvs[o + 1] = g.v0;
|
|
220
|
+
uvs[o + 2] = g.u1;
|
|
221
|
+
uvs[o + 3] = g.v1;
|
|
222
|
+
const page = g.font.pages[g.page];
|
|
223
|
+
if (page !== lastPage) {
|
|
224
|
+
let p = 0;
|
|
225
|
+
while (p < pageCount && pages[p] !== page) p++;
|
|
226
|
+
if (p === pageCount) {
|
|
227
|
+
if (p < pages.length) pages[p] = page;
|
|
228
|
+
else pages.push(page);
|
|
229
|
+
pageCount++;
|
|
230
|
+
}
|
|
231
|
+
lastPage = page;
|
|
232
|
+
lastPageIndex = p;
|
|
233
|
+
}
|
|
234
|
+
pageOf[count] = lastPageIndex;
|
|
235
|
+
sourceOf[count] = i;
|
|
236
|
+
if (count < glyphs.length) glyphs[count] = g;
|
|
237
|
+
else glyphs.push(g);
|
|
238
|
+
count++;
|
|
239
|
+
}
|
|
240
|
+
pen = advanceEnd;
|
|
241
|
+
if (!space) {
|
|
242
|
+
contentEnd = pen;
|
|
243
|
+
contentEndChar = next;
|
|
244
|
+
}
|
|
245
|
+
prev = g;
|
|
246
|
+
prevCp = cp;
|
|
247
|
+
prevSpace = space;
|
|
248
|
+
i = next;
|
|
249
|
+
}
|
|
250
|
+
out.count = count;
|
|
251
|
+
out.pageCount = pageCount;
|
|
252
|
+
let width = 0;
|
|
253
|
+
const lines = out.lineCount;
|
|
254
|
+
for (let l = 0; l < lines; l++) if (out.lineWidth[l] > width) width = out.lineWidth[l];
|
|
255
|
+
const height = lines > 0 ? (font.ascent - font.descent) * size + (lines - 1) * lineAdvance : 0;
|
|
256
|
+
const left = -anchorX * width;
|
|
257
|
+
const bottom = -anchorY * height;
|
|
258
|
+
const top = bottom + height;
|
|
259
|
+
for (let l = 0; l < lines; l++) {
|
|
260
|
+
const dx = left + (width - out.lineWidth[l]) * alignFactor;
|
|
261
|
+
const baseline = top - font.ascent * size - l * lineAdvance;
|
|
262
|
+
const end = l + 1 < lines ? out.lineQuad[l + 1] : count;
|
|
263
|
+
for (let q = out.lineQuad[l]; q < end; q++) {
|
|
264
|
+
quads[q * 4] = quads[q * 4] + dx;
|
|
265
|
+
quads[q * 4 + 1] = quads[q * 4 + 1] + baseline;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
out.width = width;
|
|
269
|
+
out.height = height;
|
|
270
|
+
out.left = left;
|
|
271
|
+
out.bottom = bottom;
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
function pushLine(out, start, end, width, quad) {
|
|
275
|
+
const l = out.lineCount;
|
|
276
|
+
if (l >= out.lineStart.length) {
|
|
277
|
+
out.lineStart = grow(out.lineStart, l + 1);
|
|
278
|
+
out.lineEnd = grow(out.lineEnd, l + 1);
|
|
279
|
+
out.lineWidth = grow(out.lineWidth, l + 1);
|
|
280
|
+
out.lineQuad = grow(out.lineQuad, l + 1);
|
|
281
|
+
}
|
|
282
|
+
out.lineStart[l] = start;
|
|
283
|
+
out.lineEnd[l] = end;
|
|
284
|
+
out.lineWidth[l] = width;
|
|
285
|
+
out.lineQuad[l] = quad;
|
|
286
|
+
return l + 1;
|
|
287
|
+
}
|
|
288
|
+
function measureText(font, value, options = {}) {
|
|
289
|
+
const layout = layoutText(font, value, options, scratch);
|
|
290
|
+
return { width: layout.width, height: layout.height, lines: layout.lines() };
|
|
291
|
+
}
|
|
292
|
+
export {
|
|
293
|
+
TextLayout,
|
|
294
|
+
isCjk,
|
|
295
|
+
layoutText,
|
|
296
|
+
measureText
|
|
297
|
+
};
|