@react-x11/components 0.12.0 → 0.14.0
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/dist/html/controls.d.ts +41 -5
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +85 -57
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +134 -20
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +545 -126
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +18 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +27 -0
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/parse.d.ts +47 -6
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +336 -30
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +142 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +563 -147
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/transform.d.ts +85 -0
- package/dist/html/css/transform.d.ts.map +1 -0
- package/dist/html/css/transform.js +425 -0
- package/dist/html/css/transform.js.map +1 -0
- package/dist/html/css/ua.d.ts +11 -1
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +73 -9
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +4 -0
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +3 -1
- package/dist/html/dom.js.map +1 -1
- package/dist/html/fonts.d.ts +78 -4
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +373 -19
- package/dist/html/fonts.js.map +1 -1
- package/dist/html/form.d.ts +200 -0
- package/dist/html/form.d.ts.map +1 -0
- package/dist/html/form.js +801 -0
- package/dist/html/form.js.map +1 -0
- package/dist/html/index.d.ts +12 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +30 -301
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/axes.d.ts +14 -0
- package/dist/html/layout/axes.d.ts.map +1 -0
- package/dist/html/layout/axes.js +28 -0
- package/dist/html/layout/axes.js.map +1 -0
- package/dist/html/layout/block.d.ts +93 -12
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +393 -39
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +90 -2
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +74 -10
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +1 -1
- package/dist/html/layout/cache.d.ts.map +1 -1
- package/dist/html/layout/cache.js +6 -0
- package/dist/html/layout/cache.js.map +1 -1
- package/dist/html/layout/css-grid.d.ts.map +1 -1
- package/dist/html/layout/css-grid.js +90 -18
- package/dist/html/layout/css-grid.js.map +1 -1
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +574 -84
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/inline.d.ts +3 -0
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +154 -54
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/multicol.d.ts +42 -0
- package/dist/html/layout/multicol.d.ts.map +1 -0
- package/dist/html/layout/multicol.js +897 -0
- package/dist/html/layout/multicol.js.map +1 -0
- package/dist/html/layout/shaping.d.ts +7 -0
- package/dist/html/layout/shaping.d.ts.map +1 -1
- package/dist/html/layout/shaping.js +25 -20
- package/dist/html/layout/shaping.js.map +1 -1
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +62 -18
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/node.d.ts +90 -15
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +771 -186
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +101 -5
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +1502 -160
- package/dist/html/paint.js.map +1 -1
- package/dist/html/svg.d.ts +15 -2
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +249 -17
- package/dist/html/svg.js.map +1 -1
- package/dist/html/widgets.d.ts +29 -0
- package/dist/html/widgets.d.ts.map +1 -0
- package/dist/html/widgets.js +700 -0
- package/dist/html/widgets.js.map +1 -0
- package/dist/html/woff2.d.ts +14 -0
- package/dist/html/woff2.d.ts.map +1 -0
- package/dist/html/woff2.js +608 -0
- package/dist/html/woff2.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/maps/controller.d.ts +17 -1
- package/dist/maps/controller.d.ts.map +1 -1
- package/dist/maps/controller.js +27 -7
- package/dist/maps/controller.js.map +1 -1
- package/dist/richtext/node.d.ts +8 -1
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +5 -1
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +25 -6
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +240 -24
- package/dist/richtext/runs.js.map +1 -1
- package/package.json +2 -2
- package/src/html/controls.ts +114 -51
- package/src/html/css/cascade.ts +614 -140
- package/src/html/css/color.ts +29 -0
- package/src/html/css/parse.ts +388 -33
- package/src/html/css/style.ts +688 -150
- package/src/html/css/transform.ts +459 -0
- package/src/html/css/ua.ts +77 -10
- package/src/html/css/values.ts +4 -0
- package/src/html/dom.ts +3 -1
- package/src/html/fonts.ts +417 -24
- package/src/html/form.ts +962 -0
- package/src/html/index.ts +49 -338
- package/src/html/layout/axes.ts +62 -0
- package/src/html/layout/block.ts +447 -37
- package/src/html/layout/boxes.ts +143 -14
- package/src/html/layout/cache.ts +6 -0
- package/src/html/layout/css-grid.ts +95 -22
- package/src/html/layout/flex.ts +675 -78
- package/src/html/layout/inline.ts +159 -55
- package/src/html/layout/multicol.ts +977 -0
- package/src/html/layout/shaping.ts +27 -18
- package/src/html/layout/table.ts +62 -19
- package/src/html/node.ts +798 -172
- package/src/html/paint.ts +1742 -241
- package/src/html/svg.ts +277 -19
- package/src/html/widgets.ts +821 -0
- package/src/html/woff2.ts +612 -0
- package/src/index.ts +1 -0
- package/src/maps/controller.ts +28 -7
- package/src/richtext/node.ts +13 -2
- package/src/richtext/runs.ts +313 -35
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
// WOFF2 → sfnt: the font inside the container the web serves one in.
|
|
2
|
+
//
|
|
3
|
+
// A WOFF2 is an OpenType font with its tables in one Brotli stream and its
|
|
4
|
+
// outlines written another way, and nearly every `@font-face` on the web
|
|
5
|
+
// names nothing else. A text engine that reads the container needs none of
|
|
6
|
+
// this, as ntk's does, through fontkit. CoreText reads an sfnt and nothing
|
|
7
|
+
// else, so react-x11's `loadFont` refuses a WOFF2 on macOS, and a page set
|
|
8
|
+
// in a web font was set in its fallback there: nextjs.org in Arial, for
|
|
9
|
+
// Geist. What an engine cannot read as served it is handed as the font the
|
|
10
|
+
// file holds (`WebFonts._register`), which is what this rebuilds.
|
|
11
|
+
//
|
|
12
|
+
// The format is W3C's (WOFF File Format 2.0): a header, a directory whose
|
|
13
|
+
// lengths are variable-width integers, then the stream. Three tables may be
|
|
14
|
+
// stored another way than an sfnt has them: `glyf` as seven streams —
|
|
15
|
+
// contour counts, point counts, flags, coordinates as triplets of one to
|
|
16
|
+
// four bytes, composites, the bounding boxes that are not their points',
|
|
17
|
+
// instructions — with `loca` left out, since an outline's offset is where
|
|
18
|
+
// it lands, and `hmtx` without the left side bearings that equal a glyph's
|
|
19
|
+
// `xMin`. Each is put back as the table an sfnt has (5.1, 5.3 and 5.4), and
|
|
20
|
+
// everything else is copied. What comes out is the font, not the file it was made from: the
|
|
21
|
+
// outlines are packed as the reference decoder packs them, which is the
|
|
22
|
+
// tightest a `glyf` allows and not necessarily what the font's author wrote.
|
|
23
|
+
//
|
|
24
|
+
// Brotli is the caller's (`inflate`), and `sfntFromWoff2` brings node's: its
|
|
25
|
+
// `zlib` has one, and nothing in `src/` names a node module, so it is
|
|
26
|
+
// reached the way `src/embed/host.ts` reaches `child_process`. A collection
|
|
27
|
+
// (`ttcf`), a file that ends early and anything else that does not read as
|
|
28
|
+
// the format come back as null, a source that did not load — and so does
|
|
29
|
+
// every file on a runtime with no Brotli to read one with.
|
|
30
|
+
|
|
31
|
+
const SIGNATURE = 0x774f4632; // 'wOF2'
|
|
32
|
+
const TTCF = 0x74746366;
|
|
33
|
+
|
|
34
|
+
/** The tags a directory entry names by number (4.1, "Known Table Tags"). */
|
|
35
|
+
const KNOWN_TAGS = (
|
|
36
|
+
'cmap head hhea hmtx maxp name OS/2 post cvt_ fpgm glyf loca prep CFF_ ' +
|
|
37
|
+
'VORG EBDT EBLC gasp hdmx kern LTSH PCLT VDMX vhea vmtx BASE GDEF GPOS ' +
|
|
38
|
+
'GSUB EBSC JSTF MATH CBDT CBLC COLR CPAL SVG_ sbix acnt avar bdat bloc ' +
|
|
39
|
+
'bsln cvar fdsc feat fmtx fvar gvar hsty just lcar mort morx opbd prop ' +
|
|
40
|
+
'trak Zapf Silf Glat Gloc Feat Sill'
|
|
41
|
+
)
|
|
42
|
+
.split(' ')
|
|
43
|
+
.map((tag) => tag.replace('_', ' '));
|
|
44
|
+
|
|
45
|
+
/** Whether bytes are a WOFF2 file, by their signature. */
|
|
46
|
+
export function isWoff2(bytes: Uint8Array): boolean {
|
|
47
|
+
return (
|
|
48
|
+
bytes.length >= 4 &&
|
|
49
|
+
bytes[0] === 0x77 &&
|
|
50
|
+
bytes[1] === 0x4f &&
|
|
51
|
+
bytes[2] === 0x46 &&
|
|
52
|
+
bytes[3] === 0x32
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type Inflate = (data: Uint8Array) => Uint8Array;
|
|
57
|
+
|
|
58
|
+
/** More than any one font's tables come to: a CJK family's face is a
|
|
59
|
+
* quarter of it. */
|
|
60
|
+
const LARGEST_FONT = 1 << 26;
|
|
61
|
+
|
|
62
|
+
let brotli: Promise<Inflate | null> | null = null;
|
|
63
|
+
|
|
64
|
+
/** node's Brotli, or null where there is none. The specifier is a variable
|
|
65
|
+
* on purpose: a literal `import('node:zlib')` would need `@types/node` in
|
|
66
|
+
* the build program, and a bundler would try to resolve it. */
|
|
67
|
+
function nodeBrotli(): Promise<Inflate | null> {
|
|
68
|
+
brotli ??= (async () => {
|
|
69
|
+
try {
|
|
70
|
+
const specifier = 'node:zlib';
|
|
71
|
+
const zlib = (await import(/* @vite-ignore */ specifier)) as {
|
|
72
|
+
brotliDecompressSync?(
|
|
73
|
+
data: Uint8Array,
|
|
74
|
+
options: { maxOutputLength: number },
|
|
75
|
+
): Uint8Array;
|
|
76
|
+
};
|
|
77
|
+
const inflate = zlib.brotliDecompressSync;
|
|
78
|
+
// a few bytes of Brotli can stand for gigabytes, and the stream is a
|
|
79
|
+
// page's: past the largest font there is, it is not one
|
|
80
|
+
return typeof inflate === 'function'
|
|
81
|
+
? (data) => inflate(data, { maxOutputLength: LARGEST_FONT })
|
|
82
|
+
: null;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
return brotli;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The sfnt a WOFF2 file holds (`woff2ToSfnt`), read with node's Brotli;
|
|
91
|
+
* null for a file that is not one, and where there is no Brotli. */
|
|
92
|
+
export async function sfntFromWoff2(
|
|
93
|
+
bytes: Uint8Array,
|
|
94
|
+
): Promise<Uint8Array | null> {
|
|
95
|
+
if (!isWoff2(bytes)) return null;
|
|
96
|
+
const inflate = await nodeBrotli();
|
|
97
|
+
return inflate ? woff2ToSfnt(bytes, inflate) : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A cursor over bytes, big-endian as an sfnt is. A read past the end
|
|
101
|
+
* throws, which is how a truncated file is found. */
|
|
102
|
+
class Reader {
|
|
103
|
+
private _view: DataView;
|
|
104
|
+
pos = 0;
|
|
105
|
+
constructor(readonly bytes: Uint8Array) {
|
|
106
|
+
this._view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
107
|
+
}
|
|
108
|
+
u8(): number {
|
|
109
|
+
return this._view.getUint8(this.pos++);
|
|
110
|
+
}
|
|
111
|
+
u16(): number {
|
|
112
|
+
const v = this._view.getUint16(this.pos);
|
|
113
|
+
this.pos += 2;
|
|
114
|
+
return v;
|
|
115
|
+
}
|
|
116
|
+
i16(): number {
|
|
117
|
+
const v = this._view.getInt16(this.pos);
|
|
118
|
+
this.pos += 2;
|
|
119
|
+
return v;
|
|
120
|
+
}
|
|
121
|
+
u32(): number {
|
|
122
|
+
const v = this._view.getUint32(this.pos);
|
|
123
|
+
this.pos += 4;
|
|
124
|
+
return v;
|
|
125
|
+
}
|
|
126
|
+
/** The next `length` bytes, as a view. */
|
|
127
|
+
take(length: number): Uint8Array {
|
|
128
|
+
if (length < 0 || this.pos + length > this.bytes.length) {
|
|
129
|
+
throw new RangeError('woff2: a stream ends early');
|
|
130
|
+
}
|
|
131
|
+
const out = this.bytes.subarray(this.pos, this.pos + length);
|
|
132
|
+
this.pos += length;
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
/** `UIntBase128`: seven bits a byte, the high bit for "more" (3.1). */
|
|
136
|
+
base128(): number {
|
|
137
|
+
let value = 0;
|
|
138
|
+
for (let i = 0; i < 5; i += 1) {
|
|
139
|
+
const byte = this.u8();
|
|
140
|
+
if (i === 0 && byte === 0x80) break; // a leading zero
|
|
141
|
+
if (value > 0x1ffffff) break; // would overflow 32 bits
|
|
142
|
+
value = value * 128 + (byte & 0x7f);
|
|
143
|
+
if (!(byte & 0x80)) return value;
|
|
144
|
+
}
|
|
145
|
+
throw new RangeError('woff2: a length that is not a UIntBase128');
|
|
146
|
+
}
|
|
147
|
+
/** `255UInt16`: one to three bytes for a 16-bit number (3.1). */
|
|
148
|
+
u255(): number {
|
|
149
|
+
const code = this.u8();
|
|
150
|
+
if (code === 253) return this.u16();
|
|
151
|
+
if (code === 255) return this.u8() + 253;
|
|
152
|
+
if (code === 254) return this.u8() + 506;
|
|
153
|
+
return code;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Bytes being written, growing as they are. */
|
|
158
|
+
class Writer {
|
|
159
|
+
bytes = new Uint8Array(1024);
|
|
160
|
+
length = 0;
|
|
161
|
+
private _room(more: number): void {
|
|
162
|
+
if (this.length + more <= this.bytes.length) return;
|
|
163
|
+
const grown = new Uint8Array(
|
|
164
|
+
Math.max(this.bytes.length * 2, this.length + more),
|
|
165
|
+
);
|
|
166
|
+
grown.set(this.bytes.subarray(0, this.length));
|
|
167
|
+
this.bytes = grown;
|
|
168
|
+
}
|
|
169
|
+
u8(v: number): void {
|
|
170
|
+
this._room(1);
|
|
171
|
+
this.bytes[this.length++] = v & 0xff;
|
|
172
|
+
}
|
|
173
|
+
u16(v: number): void {
|
|
174
|
+
this._room(2);
|
|
175
|
+
this.bytes[this.length++] = (v >> 8) & 0xff;
|
|
176
|
+
this.bytes[this.length++] = v & 0xff;
|
|
177
|
+
}
|
|
178
|
+
put(data: Uint8Array): void {
|
|
179
|
+
this._room(data.length);
|
|
180
|
+
this.bytes.set(data, this.length);
|
|
181
|
+
this.length += data.length;
|
|
182
|
+
}
|
|
183
|
+
/** Zeros up to a multiple of four, which a glyph and a table start on. */
|
|
184
|
+
align(): void {
|
|
185
|
+
const pad = (4 - (this.length & 3)) & 3;
|
|
186
|
+
this._room(pad);
|
|
187
|
+
this.bytes.fill(0, this.length, this.length + pad);
|
|
188
|
+
this.length += pad;
|
|
189
|
+
}
|
|
190
|
+
done(): Uint8Array {
|
|
191
|
+
return this.bytes.subarray(0, this.length);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
interface Entry {
|
|
196
|
+
tag: string;
|
|
197
|
+
/** Stored another way than an sfnt has it, and to be put back. */
|
|
198
|
+
transformed: boolean;
|
|
199
|
+
/** Its length in the stream. */
|
|
200
|
+
length: number;
|
|
201
|
+
data: Uint8Array;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// `glyf` flags (OpenType, "Simple Glyph Description")
|
|
205
|
+
const ON_CURVE = 0x01;
|
|
206
|
+
const X_SHORT = 0x02;
|
|
207
|
+
const Y_SHORT = 0x04;
|
|
208
|
+
const REPEAT = 0x08;
|
|
209
|
+
const X_SAME = 0x10; // or, with X_SHORT, "positive"
|
|
210
|
+
const Y_SAME = 0x20;
|
|
211
|
+
const OVERLAP_SIMPLE = 0x40;
|
|
212
|
+
|
|
213
|
+
// composite glyph flags
|
|
214
|
+
const ARG_WORDS = 0x0001;
|
|
215
|
+
const HAVE_SCALE = 0x0008;
|
|
216
|
+
const MORE_COMPONENTS = 0x0020;
|
|
217
|
+
const HAVE_XY_SCALE = 0x0040;
|
|
218
|
+
const HAVE_TWO_BY_TWO = 0x0080;
|
|
219
|
+
const HAVE_INSTRUCTIONS = 0x0100;
|
|
220
|
+
|
|
221
|
+
interface Outlines {
|
|
222
|
+
glyf: Uint8Array;
|
|
223
|
+
loca: Uint8Array;
|
|
224
|
+
/** Whether `loca` holds 32-bit offsets, which `head` has to agree on. */
|
|
225
|
+
long: boolean;
|
|
226
|
+
/** Each glyph's `xMin`: the side bearing a transformed `hmtx` left out. */
|
|
227
|
+
xMins: Int16Array;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* `glyf` and `loca` from a transformed `glyf` (5.1): each glyph's record
|
|
232
|
+
* written as an sfnt has it, and the offsets they landed at.
|
|
233
|
+
*/
|
|
234
|
+
function outlines(data: Uint8Array): Outlines {
|
|
235
|
+
const head = new Reader(data);
|
|
236
|
+
head.u16(); // reserved
|
|
237
|
+
const options = head.u16();
|
|
238
|
+
const numGlyphs = head.u16();
|
|
239
|
+
const indexFormat = head.u16();
|
|
240
|
+
const sizes = [0, 0, 0, 0, 0, 0, 0].map(() => head.u32());
|
|
241
|
+
const [contours, points, flags, glyphs, composites, boxes, instructions] =
|
|
242
|
+
sizes.map((size) => new Reader(head.take(size)));
|
|
243
|
+
const overlaps =
|
|
244
|
+
options & 1 ? head.take((numGlyphs + 7) >> 3) : new Uint8Array(0);
|
|
245
|
+
// the boxes that are written out, a bit a glyph, then the boxes
|
|
246
|
+
const explicit = boxes.take(((numGlyphs + 31) >> 5) << 2);
|
|
247
|
+
const bit = (map: Uint8Array, i: number): boolean =>
|
|
248
|
+
i >> 3 < map.length && (map[i >> 3] & (0x80 >> (i & 7))) !== 0;
|
|
249
|
+
|
|
250
|
+
const out = new Writer();
|
|
251
|
+
const offsets = new Uint32Array(numGlyphs + 1);
|
|
252
|
+
const xMins = new Int16Array(numGlyphs);
|
|
253
|
+
const xs: number[] = [];
|
|
254
|
+
const ys: number[] = [];
|
|
255
|
+
const on: boolean[] = [];
|
|
256
|
+
const moves = new Writer();
|
|
257
|
+
const rises = new Writer();
|
|
258
|
+
for (let g = 0; g < numGlyphs; g += 1) {
|
|
259
|
+
offsets[g] = out.length;
|
|
260
|
+
const count = contours.i16();
|
|
261
|
+
if (count === 0) continue; // no outline, and no record
|
|
262
|
+
if (count < 0) {
|
|
263
|
+
// a composite: its components as they are, and a box always written
|
|
264
|
+
if (!bit(explicit, g)) throw new RangeError('woff2: a composite, no box');
|
|
265
|
+
const box = boxes.take(8);
|
|
266
|
+
const from = composites.pos;
|
|
267
|
+
let more = true;
|
|
268
|
+
let instructed = false;
|
|
269
|
+
while (more) {
|
|
270
|
+
const flag = composites.u16();
|
|
271
|
+
composites.pos += 2; // the component's glyph
|
|
272
|
+
composites.pos += flag & ARG_WORDS ? 4 : 2;
|
|
273
|
+
if (flag & HAVE_SCALE) composites.pos += 2;
|
|
274
|
+
else if (flag & HAVE_XY_SCALE) composites.pos += 4;
|
|
275
|
+
else if (flag & HAVE_TWO_BY_TWO) composites.pos += 8;
|
|
276
|
+
if (flag & HAVE_INSTRUCTIONS) instructed = true;
|
|
277
|
+
more = (flag & MORE_COMPONENTS) !== 0;
|
|
278
|
+
}
|
|
279
|
+
const to = composites.pos;
|
|
280
|
+
composites.pos = from;
|
|
281
|
+
out.u16(0xffff);
|
|
282
|
+
out.put(box);
|
|
283
|
+
out.put(composites.take(to - from));
|
|
284
|
+
if (instructed) {
|
|
285
|
+
const length = glyphs.u255();
|
|
286
|
+
out.u16(length);
|
|
287
|
+
out.put(instructions.take(length));
|
|
288
|
+
}
|
|
289
|
+
xMins[g] = (box[0] << 8) | box[1];
|
|
290
|
+
out.align();
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// a simple glyph: where each contour ends, then its points as the
|
|
295
|
+
// moves between them
|
|
296
|
+
const ends: number[] = [];
|
|
297
|
+
let total = 0;
|
|
298
|
+
for (let c = 0; c < count; c += 1) {
|
|
299
|
+
total += points.u255();
|
|
300
|
+
ends.push(total - 1);
|
|
301
|
+
}
|
|
302
|
+
xs.length = ys.length = on.length = total;
|
|
303
|
+
let x = 0;
|
|
304
|
+
let y = 0;
|
|
305
|
+
let xMin = 0;
|
|
306
|
+
let yMin = 0;
|
|
307
|
+
let xMax = 0;
|
|
308
|
+
let yMax = 0;
|
|
309
|
+
for (let p = 0; p < total; p += 1) {
|
|
310
|
+
// a triplet: a flag that says how many bytes the move is in, which
|
|
311
|
+
// way each half of it goes and whether the point is on the curve
|
|
312
|
+
// (5.2, table "Triplet Encoding")
|
|
313
|
+
const flag = flags.u8();
|
|
314
|
+
const f = flag & 0x7f;
|
|
315
|
+
let dx: number;
|
|
316
|
+
let dy: number;
|
|
317
|
+
if (f < 10) {
|
|
318
|
+
dx = 0;
|
|
319
|
+
dy = ((f & 14) << 7) + glyphs.u8();
|
|
320
|
+
if (!(f & 1)) dy = -dy;
|
|
321
|
+
} else if (f < 20) {
|
|
322
|
+
dx = (((f - 10) & 14) << 7) + glyphs.u8();
|
|
323
|
+
dy = 0;
|
|
324
|
+
if (!(f & 1)) dx = -dx;
|
|
325
|
+
} else if (f < 84) {
|
|
326
|
+
const b0 = f - 20;
|
|
327
|
+
const b1 = glyphs.u8();
|
|
328
|
+
dx = 1 + (b0 & 0x30) + (b1 >> 4);
|
|
329
|
+
dy = 1 + ((b0 & 0x0c) << 2) + (b1 & 0x0f);
|
|
330
|
+
if (!(f & 1)) dx = -dx;
|
|
331
|
+
if (!(f & 2)) dy = -dy;
|
|
332
|
+
} else if (f < 120) {
|
|
333
|
+
const b0 = f - 84;
|
|
334
|
+
dx = 1 + (Math.floor(b0 / 12) << 8) + glyphs.u8();
|
|
335
|
+
dy = 1 + (((b0 % 12) >> 2) << 8) + glyphs.u8();
|
|
336
|
+
if (!(f & 1)) dx = -dx;
|
|
337
|
+
if (!(f & 2)) dy = -dy;
|
|
338
|
+
} else if (f < 124) {
|
|
339
|
+
const b0 = glyphs.u8();
|
|
340
|
+
const b1 = glyphs.u8();
|
|
341
|
+
const b2 = glyphs.u8();
|
|
342
|
+
dx = (b0 << 4) + (b1 >> 4);
|
|
343
|
+
dy = ((b1 & 0x0f) << 8) + b2;
|
|
344
|
+
if (!(f & 1)) dx = -dx;
|
|
345
|
+
if (!(f & 2)) dy = -dy;
|
|
346
|
+
} else {
|
|
347
|
+
dx = glyphs.u16();
|
|
348
|
+
dy = glyphs.u16();
|
|
349
|
+
if (!(f & 1)) dx = -dx;
|
|
350
|
+
if (!(f & 2)) dy = -dy;
|
|
351
|
+
}
|
|
352
|
+
x += dx;
|
|
353
|
+
y += dy;
|
|
354
|
+
xs[p] = x;
|
|
355
|
+
ys[p] = y;
|
|
356
|
+
on[p] = !(flag & 0x80);
|
|
357
|
+
if (p === 0) {
|
|
358
|
+
xMin = xMax = x;
|
|
359
|
+
yMin = yMax = y;
|
|
360
|
+
} else {
|
|
361
|
+
if (x < xMin) xMin = x;
|
|
362
|
+
if (x > xMax) xMax = x;
|
|
363
|
+
if (y < yMin) yMin = y;
|
|
364
|
+
if (y > yMax) yMax = y;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const length = glyphs.u255();
|
|
368
|
+
|
|
369
|
+
out.u16(count);
|
|
370
|
+
if (bit(explicit, g)) {
|
|
371
|
+
const box = boxes.take(8);
|
|
372
|
+
out.put(box);
|
|
373
|
+
xMin = (box[0] << 8) | box[1];
|
|
374
|
+
} else {
|
|
375
|
+
// the box of its points, which is what an encoder leaves out
|
|
376
|
+
out.u16(xMin);
|
|
377
|
+
out.u16(yMin);
|
|
378
|
+
out.u16(xMax);
|
|
379
|
+
out.u16(yMax);
|
|
380
|
+
}
|
|
381
|
+
xMins[g] = xMin;
|
|
382
|
+
for (const end of ends) out.u16(end);
|
|
383
|
+
out.u16(length);
|
|
384
|
+
out.put(instructions.take(length));
|
|
385
|
+
|
|
386
|
+
// The flags, a run of equal ones written once with a count, then the
|
|
387
|
+
// horizontal moves and the vertical ones: a byte where a move fits
|
|
388
|
+
// one, nothing where there is none.
|
|
389
|
+
moves.length = rises.length = 0;
|
|
390
|
+
let last = -1;
|
|
391
|
+
let repeats = 0;
|
|
392
|
+
let px = 0;
|
|
393
|
+
let py = 0;
|
|
394
|
+
for (let p = 0; p < total; p += 1) {
|
|
395
|
+
let flag = on[p] ? ON_CURVE : 0;
|
|
396
|
+
if (p === 0 && bit(overlaps, g)) flag |= OVERLAP_SIMPLE;
|
|
397
|
+
const dx = xs[p] - px;
|
|
398
|
+
const dy = ys[p] - py;
|
|
399
|
+
if (dx === 0) flag |= X_SAME;
|
|
400
|
+
else if (dx > -256 && dx < 256) {
|
|
401
|
+
flag |= X_SHORT | (dx > 0 ? X_SAME : 0);
|
|
402
|
+
moves.u8(Math.abs(dx));
|
|
403
|
+
} else moves.u16(dx);
|
|
404
|
+
if (dy === 0) flag |= Y_SAME;
|
|
405
|
+
else if (dy > -256 && dy < 256) {
|
|
406
|
+
flag |= Y_SHORT | (dy > 0 ? Y_SAME : 0);
|
|
407
|
+
rises.u8(Math.abs(dy));
|
|
408
|
+
} else rises.u16(dy);
|
|
409
|
+
if (flag === last && repeats !== 255) {
|
|
410
|
+
out.bytes[out.length - 1] |= REPEAT;
|
|
411
|
+
repeats += 1;
|
|
412
|
+
} else {
|
|
413
|
+
if (repeats) out.u8(repeats);
|
|
414
|
+
out.u8(flag);
|
|
415
|
+
repeats = 0;
|
|
416
|
+
}
|
|
417
|
+
last = flag;
|
|
418
|
+
px = xs[p];
|
|
419
|
+
py = ys[p];
|
|
420
|
+
}
|
|
421
|
+
if (repeats) out.u8(repeats);
|
|
422
|
+
out.put(moves.done());
|
|
423
|
+
out.put(rises.done());
|
|
424
|
+
out.align();
|
|
425
|
+
}
|
|
426
|
+
offsets[numGlyphs] = out.length;
|
|
427
|
+
|
|
428
|
+
// `loca` in the width the font asked for, or the wider one where the
|
|
429
|
+
// outlines, packed again, no longer fit it
|
|
430
|
+
const long = indexFormat !== 0 || out.length > 0x1fffe;
|
|
431
|
+
const loca = new Uint8Array((numGlyphs + 1) * (long ? 4 : 2));
|
|
432
|
+
const view = new DataView(loca.buffer);
|
|
433
|
+
for (let g = 0; g <= numGlyphs; g += 1) {
|
|
434
|
+
if (long) view.setUint32(g * 4, offsets[g]);
|
|
435
|
+
else view.setUint16(g * 2, offsets[g] >> 1);
|
|
436
|
+
}
|
|
437
|
+
return { glyf: out.done(), loca, long, xMins };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* `hmtx` from a transformed one (5.4): the advances as they were, and each
|
|
442
|
+
* side bearing the encoder left out put back as its glyph's `xMin`, which
|
|
443
|
+
* is what it left them out for equalling.
|
|
444
|
+
*/
|
|
445
|
+
function metrics(
|
|
446
|
+
data: Uint8Array,
|
|
447
|
+
numGlyphs: number,
|
|
448
|
+
numHMetrics: number,
|
|
449
|
+
xMins: Int16Array,
|
|
450
|
+
): Uint8Array {
|
|
451
|
+
const from = new Reader(data);
|
|
452
|
+
const flags = from.u8();
|
|
453
|
+
const advances: number[] = [];
|
|
454
|
+
for (let i = 0; i < numHMetrics; i += 1) advances.push(from.u16());
|
|
455
|
+
const out = new Writer();
|
|
456
|
+
const bearings: number[] = [];
|
|
457
|
+
for (let i = 0; i < numHMetrics; i += 1) {
|
|
458
|
+
bearings.push(flags & 1 ? (xMins[i] ?? 0) : from.i16());
|
|
459
|
+
}
|
|
460
|
+
for (let i = numHMetrics; i < numGlyphs; i += 1) {
|
|
461
|
+
bearings.push(flags & 2 ? (xMins[i] ?? 0) : from.i16());
|
|
462
|
+
}
|
|
463
|
+
for (let i = 0; i < numGlyphs; i += 1) {
|
|
464
|
+
if (i < numHMetrics) out.u16(advances[i]);
|
|
465
|
+
out.u16(bearings[i]);
|
|
466
|
+
}
|
|
467
|
+
return out.done();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function checksum(data: Uint8Array): number {
|
|
471
|
+
let sum = 0;
|
|
472
|
+
const whole = data.length & ~3;
|
|
473
|
+
for (let i = 0; i < whole; i += 4) {
|
|
474
|
+
sum =
|
|
475
|
+
(sum +
|
|
476
|
+
((data[i] << 24) |
|
|
477
|
+
(data[i + 1] << 16) |
|
|
478
|
+
(data[i + 2] << 8) |
|
|
479
|
+
data[i + 3])) >>>
|
|
480
|
+
0;
|
|
481
|
+
}
|
|
482
|
+
if (whole < data.length) {
|
|
483
|
+
let last = 0;
|
|
484
|
+
for (let i = whole; i < data.length; i += 1) {
|
|
485
|
+
last |= data[i] << (24 - 8 * (i - whole));
|
|
486
|
+
}
|
|
487
|
+
sum = (sum + last) >>> 0;
|
|
488
|
+
}
|
|
489
|
+
return sum;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* The sfnt — TrueType or OpenType — a WOFF2 file holds, or null when the
|
|
494
|
+
* bytes are not one this reads. `inflate` is Brotli: the compressed stream
|
|
495
|
+
* in, its bytes out.
|
|
496
|
+
*/
|
|
497
|
+
export function woff2ToSfnt(
|
|
498
|
+
bytes: Uint8Array,
|
|
499
|
+
inflate: Inflate,
|
|
500
|
+
): Uint8Array | null {
|
|
501
|
+
try {
|
|
502
|
+
return rebuild(bytes, inflate);
|
|
503
|
+
} catch {
|
|
504
|
+
// truncated, or not the format: a source that did not load
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function rebuild(bytes: Uint8Array, inflate: Inflate): Uint8Array | null {
|
|
510
|
+
const file = new Reader(bytes);
|
|
511
|
+
if (file.u32() !== SIGNATURE) return null;
|
|
512
|
+
const flavor = file.u32();
|
|
513
|
+
// a collection shares tables between its fonts, and a face is one font
|
|
514
|
+
if (flavor === TTCF) return null;
|
|
515
|
+
file.u32(); // length
|
|
516
|
+
const numTables = file.u16();
|
|
517
|
+
file.u16(); // reserved
|
|
518
|
+
file.u32(); // totalSfntSize
|
|
519
|
+
const compressed = file.u32();
|
|
520
|
+
file.pos = 48;
|
|
521
|
+
|
|
522
|
+
const entries: Entry[] = [];
|
|
523
|
+
for (let i = 0; i < numTables; i += 1) {
|
|
524
|
+
const flags = file.u8();
|
|
525
|
+
const known = flags & 0x3f;
|
|
526
|
+
const tag =
|
|
527
|
+
known === 0x3f ? String.fromCharCode(...file.take(4)) : KNOWN_TAGS[known];
|
|
528
|
+
const version = flags >> 6;
|
|
529
|
+
const original = file.base128();
|
|
530
|
+
// `glyf` and `loca` are transformed at version 0 and plain at 3; every
|
|
531
|
+
// other table is plain at 0 (4.1, the flags' transformation version)
|
|
532
|
+
const transformed =
|
|
533
|
+
tag === 'glyf' || tag === 'loca' ? version === 0 : version !== 0;
|
|
534
|
+
const length = transformed ? file.base128() : original;
|
|
535
|
+
entries.push({ tag, transformed, length, data: bytes });
|
|
536
|
+
}
|
|
537
|
+
if (new Set(entries.map((e) => e.tag)).size !== entries.length) return null;
|
|
538
|
+
|
|
539
|
+
const stream = new Reader(inflate(file.take(compressed)));
|
|
540
|
+
for (const entry of entries) entry.data = stream.take(entry.length);
|
|
541
|
+
|
|
542
|
+
const tables = new Map<string, Uint8Array>();
|
|
543
|
+
for (const entry of entries) {
|
|
544
|
+
if (!entry.transformed) tables.set(entry.tag, entry.data);
|
|
545
|
+
}
|
|
546
|
+
const glyf = entries.find((e) => e.tag === 'glyf');
|
|
547
|
+
const loca = entries.find((e) => e.tag === 'loca');
|
|
548
|
+
let made: Outlines | null = null;
|
|
549
|
+
if (glyf?.transformed) {
|
|
550
|
+
// the two are transformed together, and `loca` is then no bytes at all
|
|
551
|
+
if (!loca?.transformed || loca.length !== 0) return null;
|
|
552
|
+
made = outlines(glyf.data);
|
|
553
|
+
tables.set('glyf', made.glyf);
|
|
554
|
+
tables.set('loca', made.loca);
|
|
555
|
+
} else if (loca?.transformed) return null;
|
|
556
|
+
|
|
557
|
+
const hmtx = entries.find((e) => e.tag === 'hmtx');
|
|
558
|
+
if (hmtx?.transformed) {
|
|
559
|
+
const hhea = tables.get('hhea');
|
|
560
|
+
const maxp = tables.get('maxp');
|
|
561
|
+
if (!made || !hhea || !maxp) return null;
|
|
562
|
+
const numHMetrics = new Reader(hhea.subarray(34)).u16();
|
|
563
|
+
const numGlyphs = new Reader(maxp.subarray(4)).u16();
|
|
564
|
+
tables.set('hmtx', metrics(hmtx.data, numGlyphs, numHMetrics, made.xMins));
|
|
565
|
+
}
|
|
566
|
+
for (const entry of entries) {
|
|
567
|
+
// any other transform is one this format has not defined
|
|
568
|
+
if (entry.transformed && !tables.has(entry.tag)) return null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const head = tables.get('head');
|
|
572
|
+
if (head && head.length >= 54) {
|
|
573
|
+
// `head` is written again: its checksum of the whole file is this
|
|
574
|
+
// file's, and its `loca` width is the one the offsets were written in
|
|
575
|
+
const copy = head.slice();
|
|
576
|
+
copy.fill(0, 8, 12);
|
|
577
|
+
if (made) copy[51] = made.long ? 1 : 0;
|
|
578
|
+
tables.set('head', copy);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// The sfnt: a header, a directory in tag order, then each table on a
|
|
582
|
+
// four-byte boundary (OpenType, "Organization of an OpenType Font").
|
|
583
|
+
const tags = [...tables.keys()].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
584
|
+
const count = tags.length;
|
|
585
|
+
let size = 12 + 16 * count;
|
|
586
|
+
for (const tag of tags) size += (tables.get(tag)!.length + 3) & ~3;
|
|
587
|
+
const out = new Uint8Array(size);
|
|
588
|
+
const view = new DataView(out.buffer);
|
|
589
|
+
const log = Math.floor(Math.log2(count || 1));
|
|
590
|
+
view.setUint32(0, flavor);
|
|
591
|
+
view.setUint16(4, count);
|
|
592
|
+
view.setUint16(6, 16 << log);
|
|
593
|
+
view.setUint16(8, log);
|
|
594
|
+
view.setUint16(10, 16 * count - (16 << log));
|
|
595
|
+
let at = 12 + 16 * count;
|
|
596
|
+
let headAt = -1;
|
|
597
|
+
tags.forEach((tag, i) => {
|
|
598
|
+
const data = tables.get(tag)!;
|
|
599
|
+
const record = 12 + 16 * i;
|
|
600
|
+
for (let c = 0; c < 4; c += 1) out[record + c] = tag.charCodeAt(c);
|
|
601
|
+
view.setUint32(record + 4, checksum(data));
|
|
602
|
+
view.setUint32(record + 8, at);
|
|
603
|
+
view.setUint32(record + 12, data.length);
|
|
604
|
+
out.set(data, at);
|
|
605
|
+
if (tag === 'head') headAt = at;
|
|
606
|
+
at += (data.length + 3) & ~3;
|
|
607
|
+
});
|
|
608
|
+
if (headAt >= 0 && tables.get('head')!.length >= 12) {
|
|
609
|
+
view.setUint32(headAt + 8, (0xb1b0afba - checksum(out)) >>> 0);
|
|
610
|
+
}
|
|
611
|
+
return out;
|
|
612
|
+
}
|
package/src/index.ts
CHANGED
package/src/maps/controller.ts
CHANGED
|
@@ -277,6 +277,26 @@ export const glideClock = {
|
|
|
277
277
|
},
|
|
278
278
|
};
|
|
279
279
|
|
|
280
|
+
/**
|
|
281
|
+
* The clock the settle window is measured on, and the timer that closes it.
|
|
282
|
+
*
|
|
283
|
+
* The shape of {@link glideClock}, and exported as it is — from this module
|
|
284
|
+
* and not from the package — so that a test can hold the window open. It is
|
|
285
|
+
* {@link SETTLE_MS} of wall-clock time, and a test that asks what the map
|
|
286
|
+
* does *while it is moving* is asking inside it: a runner slow enough to
|
|
287
|
+
* spend the window between the wheel and the frame that looks finds a map
|
|
288
|
+
* that has settled, and a frame that is no longer asked where a glide stops.
|
|
289
|
+
*/
|
|
290
|
+
export const settleClock = {
|
|
291
|
+
now(): number {
|
|
292
|
+
return Date.now();
|
|
293
|
+
},
|
|
294
|
+
arm,
|
|
295
|
+
disarm(handle: unknown): void {
|
|
296
|
+
timers.clearTimeout?.(handle);
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
280
300
|
function clamp(value: number, low: number, high: number): number {
|
|
281
301
|
return value < low ? low : value > high ? high : value;
|
|
282
302
|
}
|
|
@@ -374,7 +394,7 @@ export class MapController {
|
|
|
374
394
|
* camera move arms the settle timer again, and the next notch a glide.
|
|
375
395
|
*/
|
|
376
396
|
dispose(): void {
|
|
377
|
-
if (this._settleTimer !== null)
|
|
397
|
+
if (this._settleTimer !== null) settleClock.disarm(this._settleTimer);
|
|
378
398
|
this._settleTimer = null;
|
|
379
399
|
if (this._glideTimer !== null) glideClock.disarm(this._glideTimer);
|
|
380
400
|
this._glideTimer = null;
|
|
@@ -811,7 +831,7 @@ export class MapController {
|
|
|
811
831
|
|
|
812
832
|
/** Whether the camera moved within the settle window. */
|
|
813
833
|
get moving(): boolean {
|
|
814
|
-
return
|
|
834
|
+
return settleClock.now() < this._settleAt;
|
|
815
835
|
}
|
|
816
836
|
|
|
817
837
|
/** …or a press is still down: what the retained renderer holds
|
|
@@ -832,22 +852,23 @@ export class MapController {
|
|
|
832
852
|
*
|
|
833
853
|
* Wall-clock time rather than a frame clock: this is a window a hundred
|
|
834
854
|
* and forty milliseconds long compared inside a timer callback, and a
|
|
835
|
-
* timer is the one thing that runs when no frame does.
|
|
855
|
+
* timer is the one thing that runs when no frame does. Read and armed
|
|
856
|
+
* through {@link settleClock}, which a test holds.
|
|
836
857
|
*/
|
|
837
858
|
touch(): void {
|
|
838
|
-
this._settleAt =
|
|
859
|
+
this._settleAt = settleClock.now() + SETTLE_MS;
|
|
839
860
|
if (this._settleTimer !== null) return;
|
|
840
861
|
const tick = (): void => {
|
|
841
862
|
this._settleTimer = null;
|
|
842
|
-
const left = this._settleAt -
|
|
863
|
+
const left = this._settleAt - settleClock.now();
|
|
843
864
|
if (left > 0) {
|
|
844
|
-
this._settleTimer = arm(tick, left);
|
|
865
|
+
this._settleTimer = settleClock.arm(tick, left);
|
|
845
866
|
return;
|
|
846
867
|
}
|
|
847
868
|
this._props.onMoveEnd?.(this.camera());
|
|
848
869
|
this._view?.settled();
|
|
849
870
|
};
|
|
850
|
-
this._settleTimer = arm(tick, SETTLE_MS);
|
|
871
|
+
this._settleTimer = settleClock.arm(tick, SETTLE_MS);
|
|
851
872
|
}
|
|
852
873
|
|
|
853
874
|
// --- input ---------------------------------------------------------------
|