ntk 8.17.8 → 8.18.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/README.md +4 -0
- package/lib/index.js +2 -0
- package/lib/precise.js +452 -0
- package/lib/rasterize.js +21 -8
- package/lib/renderingcontext_2d.js +53 -21
- package/lib/text/brotlidictionary.js +99 -0
- package/lib/text/font.js +16 -6
- package/lib/text/woff2.js +102 -0
- package/lib/trapezoid.js +111 -45
- package/lib/vendor/fontkit.js +14379 -0
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -144,6 +144,10 @@ parser, say — can import just that piece, and loads only what it needs:
|
|
|
144
144
|
Each is the same module the root re-exports, so importing a name both ways
|
|
145
145
|
gives the same function or class: nothing is loaded, or instantiated, twice.
|
|
146
146
|
|
|
147
|
+
`ntk/package.json` is exported too, for the tools that read a dependency's
|
|
148
|
+
version or metadata that way — bundlers, React Native's resolver, version
|
|
149
|
+
reporters. It loads nothing.
|
|
150
|
+
|
|
147
151
|
## 3d graphics
|
|
148
152
|
|
|
149
153
|
Two backends, chosen by `glPolicy`
|
package/lib/index.js
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
defaultRasterizer,
|
|
48
48
|
setDefaultRasterizer
|
|
49
49
|
} from './rasterize.js';
|
|
50
|
+
import { PreciseRasterizer } from './precise.js';
|
|
50
51
|
import { DEFAULT_MASK_POLICY } from './maskcluster.js';
|
|
51
52
|
import {
|
|
52
53
|
DEFAULT_SHADOW_POLICY,
|
|
@@ -284,6 +285,7 @@ export {
|
|
|
284
285
|
setDefaultFontSource,
|
|
285
286
|
CoverageAccumulator,
|
|
286
287
|
ScanlineRasterizer,
|
|
288
|
+
PreciseRasterizer,
|
|
287
289
|
defaultRasterizer,
|
|
288
290
|
setDefaultRasterizer,
|
|
289
291
|
DEFAULT_RASTER_POLICY,
|
package/lib/precise.js
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
// RENDER's Precise rasterization, done here: the a8 coverage an X server
|
|
2
|
+
// draws for a Triangles or AddTraps request, computed on this side of the
|
|
3
|
+
// wire from the same numbers.
|
|
4
|
+
//
|
|
5
|
+
// A mask is rasterized either here or by the server (routeRaster in
|
|
6
|
+
// rasterize.js). The analytic ScanlineRasterizer and the server's point
|
|
7
|
+
// sampling come out a few levels apart at every edge, so whenever the same
|
|
8
|
+
// pixels were drawn by different routes in different passes — a stroke cut
|
|
9
|
+
// to the runs inside a clip against the whole of it, a stroke drawn straight
|
|
10
|
+
// to the destination against the same stroke through the mask — a partial
|
|
11
|
+
// repaint and a full one disagreed (issue #462). With this as the local
|
|
12
|
+
// rasterizer they cannot: both routes are one function of the wire geometry,
|
|
13
|
+
// and the route is a question of cost alone.
|
|
14
|
+
//
|
|
15
|
+
// RENDER's default poly-mode is Precise, and the protocol defines it
|
|
16
|
+
// (renderproto, "Polygon Rasterization"): for an 8-bit alpha, a regular grid
|
|
17
|
+
// 17 samples wide and 15 high, centred in the pixel; each trapezoid or
|
|
18
|
+
// triangle is Add-combined into the mask. 17 × 15 = 255, so a pixel's alpha
|
|
19
|
+
// is its count of covered samples. pixman implements it, and pixman is what
|
|
20
|
+
// rasterizes Triangles, Trapezoids and AddTraps in fb (Xvfb, XQuartz) and in
|
|
21
|
+
// glamor (Xorg's modesetting driver, Xwayland). This is a port of pixman's
|
|
22
|
+
// code — pixman_edge_init and _step, pixman_sample_ceil_y and _floor_y,
|
|
23
|
+
// rasterize_edges_8, triangle_to_trapezoids — down to its rounding. pixman
|
|
24
|
+
// sits up to one 16.16 unit off the spec's sample positions, and its edge
|
|
25
|
+
// step leaves the error term alone when a step carries nothing; matching it
|
|
26
|
+
// is matching the servers, so both are kept. Measured byte-identical against
|
|
27
|
+
// XQuartz on 300 random drawings, 24.5 M pixels.
|
|
28
|
+
//
|
|
29
|
+
// One liberty, which changes no byte: pixman adds each trapezoid into the
|
|
30
|
+
// mask a sample row at a time with a saturating add. Every count it adds is
|
|
31
|
+
// non-negative, so the saturating sum is the plain sum clamped once. Each
|
|
32
|
+
// sample row's span goes into a difference array instead — four cells,
|
|
33
|
+
// however wide the span — and each pixel row is integrated and clamped at
|
|
34
|
+
// the end.
|
|
35
|
+
//
|
|
36
|
+
// The input is the wire's integers. `fixed16` is what a coordinate becomes
|
|
37
|
+
// in a request, and the 2d context sends coordinates already on that grid
|
|
38
|
+
// (`snapFixed16`), so that the conversion node-x11 applies has nothing left
|
|
39
|
+
// to round.
|
|
40
|
+
|
|
41
|
+
import { trapezoidize } from './trapezoid.js';
|
|
42
|
+
|
|
43
|
+
// the sample grid for an 8-bit alpha, as pixman-private.h derives it
|
|
44
|
+
const STEP_Y_SMALL = 4369; // 65536 / 15
|
|
45
|
+
const STEP_Y_BIG = 4370; // 65536 - 14 * STEP_Y_SMALL
|
|
46
|
+
const Y_FRAC_FIRST = 2185; // STEP_Y_BIG / 2
|
|
47
|
+
const Y_FRAC_LAST = 63351; // Y_FRAC_FIRST + 14 * STEP_Y_SMALL
|
|
48
|
+
const STEP_X_SMALL = 3855; // 65536 / 17
|
|
49
|
+
const X_FRAC_FIRST = 1928; // (65536 - 16 * STEP_X_SMALL) / 2
|
|
50
|
+
const N_X_FRAC = 17;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A coordinate as the 16.16 fixed-point int32 a request carries: truncated
|
|
54
|
+
* toward zero, as node-x11 converts. Every coordinate the 2d context sends
|
|
55
|
+
* is in reach of the int32 (lib/cliprect.js's WIRE_REACH).
|
|
56
|
+
*/
|
|
57
|
+
export function fixed16(v) {
|
|
58
|
+
return Math.trunc(v * 65536) | 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The nearest coordinate toward zero that 16.16 holds exactly. Sending this
|
|
63
|
+
* rather than `v` makes the request's integer `fixed16(v)` however the
|
|
64
|
+
* client library converts — node-x11's `parseInt(v * 65536)` reads a tiny
|
|
65
|
+
* product's exponent notation, so 4e-11 became 4 where it should be 0.
|
|
66
|
+
*/
|
|
67
|
+
export function snapFixed16(v) {
|
|
68
|
+
return Math.trunc(v * 65536) / 65536;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// C's integer division, which truncates toward zero, exact for |a| < 2^53.
|
|
72
|
+
// A float quotient can land on the wrong side of an integer, so the
|
|
73
|
+
// remainder settles it.
|
|
74
|
+
function idiv(a, b) {
|
|
75
|
+
let q = Math.trunc(a / b);
|
|
76
|
+
const r = a - q * b;
|
|
77
|
+
if (a >= 0) {
|
|
78
|
+
if (r < 0) q--;
|
|
79
|
+
else if (r >= b) q++;
|
|
80
|
+
} else if (r > 0) q++;
|
|
81
|
+
else if (r <= -b) q--;
|
|
82
|
+
return q;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// pixman's DIV: division rounding toward -infinity, for b > 0
|
|
86
|
+
function floorDiv(a, b) {
|
|
87
|
+
return a < 0 ? idiv(a - b + 1, b) : idiv(a, b);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** pixman_sample_ceil_y: the first sample row at or below y */
|
|
91
|
+
function sampleCeilY(y) {
|
|
92
|
+
let f = y & 0xffff;
|
|
93
|
+
let i = y - f;
|
|
94
|
+
f = floorDiv(f - Y_FRAC_FIRST + (STEP_Y_SMALL - 1), STEP_Y_SMALL) * STEP_Y_SMALL + Y_FRAC_FIRST;
|
|
95
|
+
if (f > Y_FRAC_LAST) {
|
|
96
|
+
if (i >> 16 === 0x7fff) {
|
|
97
|
+
f = 0xffff;
|
|
98
|
+
} else {
|
|
99
|
+
f = Y_FRAC_FIRST;
|
|
100
|
+
i += 65536;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return (i | f) | 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** pixman_sample_floor_y: the last sample row strictly above y */
|
|
107
|
+
function sampleFloorY(y) {
|
|
108
|
+
let f = y & 0xffff;
|
|
109
|
+
let i = y - f;
|
|
110
|
+
f = floorDiv(f - 1 - Y_FRAC_FIRST, STEP_Y_SMALL) * STEP_Y_SMALL + Y_FRAC_FIRST;
|
|
111
|
+
if (f < Y_FRAC_FIRST) {
|
|
112
|
+
if (i >> 16 === -32768) {
|
|
113
|
+
f = 0;
|
|
114
|
+
} else {
|
|
115
|
+
f = Y_FRAC_LAST;
|
|
116
|
+
i -= 65536;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return (i | f) | 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// pixman_edge_t, as eight int32 fields
|
|
123
|
+
const X = 0;
|
|
124
|
+
const E = 1;
|
|
125
|
+
const DY = 2;
|
|
126
|
+
const SIGNDX = 3;
|
|
127
|
+
const STEPX_SMALL = 4;
|
|
128
|
+
const DX_SMALL = 5;
|
|
129
|
+
const STEPX_BIG = 6;
|
|
130
|
+
const DX_BIG = 7;
|
|
131
|
+
const left = new Int32Array(8);
|
|
132
|
+
const right = new Int32Array(8);
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* pixman_edge_init followed by its pixman_edge_step to `yStart`: the edge
|
|
136
|
+
* from (xTop, yTop) down to (xBot, yBot), placed on its first sample row.
|
|
137
|
+
*/
|
|
138
|
+
function edgeInit(edge, yStart, xTop, yTop, xBot, yBot) {
|
|
139
|
+
const dx = (xBot - xTop) | 0;
|
|
140
|
+
const dy = (yBot - yTop) | 0;
|
|
141
|
+
let x = xTop;
|
|
142
|
+
let e = 0;
|
|
143
|
+
let stepx = 0;
|
|
144
|
+
let signdx = 0;
|
|
145
|
+
let rem = 0;
|
|
146
|
+
if (dy) {
|
|
147
|
+
if (dx >= 0) {
|
|
148
|
+
signdx = 1;
|
|
149
|
+
stepx = idiv(dx, dy);
|
|
150
|
+
rem = dx % dy;
|
|
151
|
+
e = -dy;
|
|
152
|
+
} else {
|
|
153
|
+
signdx = -1;
|
|
154
|
+
stepx = -idiv(-dx, dy);
|
|
155
|
+
rem = -dx % dy;
|
|
156
|
+
}
|
|
157
|
+
// _pixman_edge_multi_init for the small and the big step; n is at most
|
|
158
|
+
// 4370, so every product here is exact in a double
|
|
159
|
+
let ne = STEP_Y_SMALL * rem;
|
|
160
|
+
let sx = (STEP_Y_SMALL * stepx) | 0;
|
|
161
|
+
if (ne > 0) {
|
|
162
|
+
const nx = idiv(ne, dy) | 0;
|
|
163
|
+
ne -= nx * dy;
|
|
164
|
+
sx = (sx + nx * signdx) | 0;
|
|
165
|
+
}
|
|
166
|
+
edge[STEPX_SMALL] = sx;
|
|
167
|
+
edge[DX_SMALL] = ne;
|
|
168
|
+
ne = STEP_Y_BIG * rem;
|
|
169
|
+
sx = (STEP_Y_BIG * stepx) | 0;
|
|
170
|
+
if (ne > 0) {
|
|
171
|
+
const nx = idiv(ne, dy) | 0;
|
|
172
|
+
ne -= nx * dy;
|
|
173
|
+
sx = (sx + nx * signdx) | 0;
|
|
174
|
+
}
|
|
175
|
+
edge[STEPX_BIG] = sx;
|
|
176
|
+
edge[DX_BIG] = ne;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// pixman_edge_step(e, yStart - yTop). Its products are 64-bit in C and
|
|
180
|
+
// pass 2^53 for a tall edge entering the grid far below its top, so past
|
|
181
|
+
// that they are done in BigInt.
|
|
182
|
+
const n = (yStart - yTop) | 0;
|
|
183
|
+
const step = n * stepx;
|
|
184
|
+
x = Number.isSafeInteger(step)
|
|
185
|
+
? (x + step) | 0
|
|
186
|
+
: Number(BigInt.asIntN(32, BigInt(x) + BigInt(n) * BigInt(stepx)));
|
|
187
|
+
const carry = n * rem;
|
|
188
|
+
if (Number.isSafeInteger(carry) && Number.isSafeInteger(e + carry + dy)) {
|
|
189
|
+
const ne = e + carry;
|
|
190
|
+
if (n >= 0) {
|
|
191
|
+
if (ne > 0) {
|
|
192
|
+
const nx = idiv(ne + dy - 1, dy) | 0;
|
|
193
|
+
e = (ne - nx * dy) | 0;
|
|
194
|
+
x = (x + nx * signdx) | 0;
|
|
195
|
+
}
|
|
196
|
+
} else if (ne <= -dy) {
|
|
197
|
+
const nx = idiv(-ne, dy) | 0;
|
|
198
|
+
e = (ne + nx * dy) | 0;
|
|
199
|
+
x = (x - nx * signdx) | 0;
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
const ne = BigInt(e) + BigInt(n) * BigInt(rem);
|
|
203
|
+
const bdy = BigInt(dy);
|
|
204
|
+
if (n >= 0) {
|
|
205
|
+
if (ne > 0n) {
|
|
206
|
+
const nx = BigInt.asIntN(32, (ne + bdy - 1n) / bdy);
|
|
207
|
+
e = Number(BigInt.asIntN(32, ne - nx * bdy));
|
|
208
|
+
x = (x + Number(nx) * signdx) | 0;
|
|
209
|
+
}
|
|
210
|
+
} else if (ne <= -bdy) {
|
|
211
|
+
const nx = BigInt.asIntN(32, -ne / bdy);
|
|
212
|
+
e = Number(BigInt.asIntN(32, ne + nx * bdy));
|
|
213
|
+
x = (x - Number(nx) * signdx) | 0;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
edge[X] = x;
|
|
217
|
+
edge[E] = e;
|
|
218
|
+
edge[DY] = dy;
|
|
219
|
+
edge[SIGNDX] = signdx;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* rasterize_edges_8 between `left` and `right`, from sample row `t` to `b`
|
|
224
|
+
* inclusive, into the difference array `acc` (rows `stride` apart).
|
|
225
|
+
*/
|
|
226
|
+
function addSpans(acc, stride, width, t, b) {
|
|
227
|
+
let y = t;
|
|
228
|
+
let row = (y >> 16) * stride;
|
|
229
|
+
let lx = left[X];
|
|
230
|
+
let le = left[E];
|
|
231
|
+
const ldy = left[DY];
|
|
232
|
+
const lsign = left[SIGNDX];
|
|
233
|
+
const lsxs = left[STEPX_SMALL];
|
|
234
|
+
const ldxs = left[DX_SMALL];
|
|
235
|
+
const lsxb = left[STEPX_BIG];
|
|
236
|
+
const ldxb = left[DX_BIG];
|
|
237
|
+
let rx = right[X];
|
|
238
|
+
let re = right[E];
|
|
239
|
+
const rdy = right[DY];
|
|
240
|
+
const rsign = right[SIGNDX];
|
|
241
|
+
const rsxs = right[STEPX_SMALL];
|
|
242
|
+
const rdxs = right[DX_SMALL];
|
|
243
|
+
const rsxb = right[STEPX_BIG];
|
|
244
|
+
const rdxb = right[DX_BIG];
|
|
245
|
+
// pixman clips the span to the image: from 0, and to the last pixel of
|
|
246
|
+
// the row taken whole. Both are what an unclipped span would add there.
|
|
247
|
+
const lastX = (width << 16) - 1;
|
|
248
|
+
for (;;) {
|
|
249
|
+
const l = lx < 0 ? 0 : lx;
|
|
250
|
+
const r = rx >> 16 >= width ? lastX : rx;
|
|
251
|
+
if (r > l) {
|
|
252
|
+
// the span adds 17 - ls to the pixel it starts in, 17 to each pixel
|
|
253
|
+
// after, rs to the pixel it ends in and nothing past it: the
|
|
254
|
+
// difference of two steps, each two cells of `acc`
|
|
255
|
+
const li = l >> 16;
|
|
256
|
+
const ri = r >> 16;
|
|
257
|
+
const ls = (((l & 0xffff) + X_FRAC_FIRST) / STEP_X_SMALL) | 0;
|
|
258
|
+
const rs = (((r & 0xffff) + X_FRAC_FIRST) / STEP_X_SMALL) | 0;
|
|
259
|
+
acc[row + li] += N_X_FRAC - ls;
|
|
260
|
+
acc[row + li + 1] += ls;
|
|
261
|
+
acc[row + ri] -= N_X_FRAC - rs;
|
|
262
|
+
acc[row + ri + 1] -= rs;
|
|
263
|
+
}
|
|
264
|
+
if (y === b) return;
|
|
265
|
+
if ((y & 0xffff) !== Y_FRAC_LAST) {
|
|
266
|
+
lx = (lx + lsxs) | 0;
|
|
267
|
+
le = (le + ldxs) | 0;
|
|
268
|
+
if (le > 0) {
|
|
269
|
+
le = (le - ldy) | 0;
|
|
270
|
+
lx = (lx + lsign) | 0;
|
|
271
|
+
}
|
|
272
|
+
rx = (rx + rsxs) | 0;
|
|
273
|
+
re = (re + rdxs) | 0;
|
|
274
|
+
if (re > 0) {
|
|
275
|
+
re = (re - rdy) | 0;
|
|
276
|
+
rx = (rx + rsign) | 0;
|
|
277
|
+
}
|
|
278
|
+
y = (y + STEP_Y_SMALL) | 0;
|
|
279
|
+
} else {
|
|
280
|
+
lx = (lx + lsxb) | 0;
|
|
281
|
+
le = (le + ldxb) | 0;
|
|
282
|
+
if (le > 0) {
|
|
283
|
+
le = (le - ldy) | 0;
|
|
284
|
+
lx = (lx + lsign) | 0;
|
|
285
|
+
}
|
|
286
|
+
rx = (rx + rsxb) | 0;
|
|
287
|
+
re = (re + rdxb) | 0;
|
|
288
|
+
if (re > 0) {
|
|
289
|
+
re = (re - rdy) | 0;
|
|
290
|
+
rx = (rx + rsign) | 0;
|
|
291
|
+
}
|
|
292
|
+
y = (y + STEP_Y_BIG) | 0;
|
|
293
|
+
row += stride;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** the first sample row of a band from `top`, inside the grid */
|
|
299
|
+
function firstRow(top) {
|
|
300
|
+
return sampleCeilY(top < 0 ? 0 : top);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** the last sample row of a band to `bottom`, inside a grid `height` high */
|
|
304
|
+
function lastRow(bottom, height) {
|
|
305
|
+
return sampleFloorY(bottom >> 16 >= height ? (height << 16) - 1 : bottom);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* pixman_rasterize_trapezoid: a pixman_trapezoid_t — top and bottom, each
|
|
310
|
+
* side a line through two points — already offset into the grid
|
|
311
|
+
*/
|
|
312
|
+
function addTrapezoid(acc, stride, width, height, top, bottom, l1x, l1y, l2x, l2y, r1x, r1y, r2x, r2y) {
|
|
313
|
+
if (l1y === l2y || r1y === r2y || !(bottom > top)) return;
|
|
314
|
+
const t = firstRow(top);
|
|
315
|
+
const b = lastRow(bottom, height);
|
|
316
|
+
if (b < t) return;
|
|
317
|
+
if (l1y <= l2y) edgeInit(left, t, l1x, l1y, l2x, l2y);
|
|
318
|
+
else edgeInit(left, t, l2x, l2y, l1x, l1y);
|
|
319
|
+
if (r1y <= r2y) edgeInit(right, t, r1x, r1y, r2x, r2y);
|
|
320
|
+
else edgeInit(right, t, r2x, r2y, r1x, r1y);
|
|
321
|
+
addSpans(acc, stride, width, t, b);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// whether b is clockwise of a about ref in y-down space, as pixman asks it
|
|
325
|
+
function clockwise(refX, refY, ax, ay, bx, by) {
|
|
326
|
+
const adx = ax - refX;
|
|
327
|
+
const ady = ay - refY;
|
|
328
|
+
const bdx = bx - refX;
|
|
329
|
+
const bdy = by - refY;
|
|
330
|
+
const p = bdy * adx;
|
|
331
|
+
const q = ady * bdx;
|
|
332
|
+
if (Number.isSafeInteger(p) && Number.isSafeInteger(q)) return p - q < 0;
|
|
333
|
+
return BigInt(bdy) * BigInt(adx) - BigInt(ady) * BigInt(bdx) < 0n;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Add a Triangles request's triangles — float device coordinates, six per
|
|
338
|
+
* triangle — to the grid, each split into two trapezoids as
|
|
339
|
+
* triangle_to_trapezoids does.
|
|
340
|
+
*/
|
|
341
|
+
function addTriangles(acc, stride, width, height, tris, dx, dy) {
|
|
342
|
+
const xo = dx * 65536;
|
|
343
|
+
const yo = dy * 65536;
|
|
344
|
+
for (let i = 0; i + 5 < tris.length; i += 6) {
|
|
345
|
+
let tx = (fixed16(tris[i]) + xo) | 0;
|
|
346
|
+
let ty = (fixed16(tris[i + 1]) + yo) | 0;
|
|
347
|
+
let lx = (fixed16(tris[i + 2]) + xo) | 0;
|
|
348
|
+
let ly = (fixed16(tris[i + 3]) + yo) | 0;
|
|
349
|
+
let rx = (fixed16(tris[i + 4]) + xo) | 0;
|
|
350
|
+
let ry = (fixed16(tris[i + 5]) + yo) | 0;
|
|
351
|
+
let s;
|
|
352
|
+
// the topmost point first, ties to the left; then left before right
|
|
353
|
+
if (ty === ly ? tx > lx : ty > ly) {
|
|
354
|
+
s = lx; lx = tx; tx = s;
|
|
355
|
+
s = ly; ly = ty; ty = s;
|
|
356
|
+
}
|
|
357
|
+
if (ty === ry ? tx > rx : ty > ry) {
|
|
358
|
+
s = rx; rx = tx; tx = s;
|
|
359
|
+
s = ry; ry = ty; ty = s;
|
|
360
|
+
}
|
|
361
|
+
if (clockwise(tx, ty, rx, ry, lx, ly)) {
|
|
362
|
+
s = rx; rx = lx; lx = s;
|
|
363
|
+
s = ry; ry = ly; ly = s;
|
|
364
|
+
}
|
|
365
|
+
// down from the top to the higher of the other two, then on to the
|
|
366
|
+
// lower, with the side that turned there continuing along the third edge
|
|
367
|
+
const mid = ry < ly ? ry : ly;
|
|
368
|
+
addTrapezoid(acc, stride, width, height, ty, mid, tx, ty, lx, ly, tx, ty, rx, ry);
|
|
369
|
+
if (ry < ly) addTrapezoid(acc, stride, width, height, ry, ly, tx, ty, lx, ly, rx, ry, lx, ly);
|
|
370
|
+
else addTrapezoid(acc, stride, width, height, ly, ry, lx, ly, rx, ry, tx, ty, rx, ry);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Add an AddTraps request's trapezoids — float device coordinates, six per
|
|
376
|
+
* trapezoid: top left, right, y, then bottom left, right, y — to the grid,
|
|
377
|
+
* as pixman_add_traps does.
|
|
378
|
+
*/
|
|
379
|
+
function addTraps(acc, stride, width, height, traps, dx, dy) {
|
|
380
|
+
const xo = dx * 65536;
|
|
381
|
+
const yo = dy * 65536;
|
|
382
|
+
for (let i = 0; i + 5 < traps.length; i += 6) {
|
|
383
|
+
const topY = (fixed16(traps[i + 2]) + yo) | 0;
|
|
384
|
+
const botY = (fixed16(traps[i + 5]) + yo) | 0;
|
|
385
|
+
const t = firstRow(topY);
|
|
386
|
+
const b = lastRow(botY, height);
|
|
387
|
+
if (b < t) continue;
|
|
388
|
+
const tl = (fixed16(traps[i]) + xo) | 0;
|
|
389
|
+
const tr = (fixed16(traps[i + 1]) + xo) | 0;
|
|
390
|
+
const bl = (fixed16(traps[i + 3]) + xo) | 0;
|
|
391
|
+
const br = (fixed16(traps[i + 4]) + xo) | 0;
|
|
392
|
+
edgeInit(left, t, tl, topY, bl, botY);
|
|
393
|
+
edgeInit(right, t, tr, topY, br, botY);
|
|
394
|
+
addSpans(acc, stride, width, t, b);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Each row's running sum, clamped: the saturating adds, all at once. The
|
|
400
|
+
* cells are zeroed as they are read, so the accumulator is ready for the
|
|
401
|
+
* next mask without a pass of its own.
|
|
402
|
+
*/
|
|
403
|
+
function integrate(acc, stride, width, height) {
|
|
404
|
+
const out = new Uint8Array(width * height);
|
|
405
|
+
for (let y = 0; y < height; y++) {
|
|
406
|
+
let sum = 0;
|
|
407
|
+
const from = y * stride;
|
|
408
|
+
const to = y * width;
|
|
409
|
+
for (let x = 0; x < width; x++) {
|
|
410
|
+
sum += acc[from + x];
|
|
411
|
+
acc[from + x] = 0;
|
|
412
|
+
out[to + x] = sum > 255 ? 255 : sum;
|
|
413
|
+
}
|
|
414
|
+
// the slack cells past the row, which a span ending at its last pixel
|
|
415
|
+
// writes and nothing reads
|
|
416
|
+
acc[from + width] = 0;
|
|
417
|
+
acc[from + width + 1] = 0;
|
|
418
|
+
}
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// One accumulator for every mask, grown to the largest asked for: masks are
|
|
423
|
+
// rasterized one at a time, and allocating a fresh one each time was a
|
|
424
|
+
// tenth of the cost of a wall of small icons, in the garbage collector.
|
|
425
|
+
let scratch = new Int32Array(0);
|
|
426
|
+
|
|
427
|
+
function accumulator(cells) {
|
|
428
|
+
if (scratch.length < cells) scratch = new Int32Array(Math.max(cells, scratch.length * 2));
|
|
429
|
+
return scratch;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* A Rasterizer (docs/context-2d.md) whose masks are the ones the X server
|
|
434
|
+
* draws from the same geometry: `triangles` exactly as the 2d context sends
|
|
435
|
+
* them with Triangles, `polys` trapezoidized exactly as it sends them with
|
|
436
|
+
* AddTraps. A drawing gets the same bytes whichever side rasterizes it.
|
|
437
|
+
*
|
|
438
|
+
* `dx`/`dy` are whole pixels, which is all the 2d context passes; anything
|
|
439
|
+
* else is declined, and goes to the server.
|
|
440
|
+
*/
|
|
441
|
+
export class PreciseRasterizer {
|
|
442
|
+
rasterize({ polys, triangles, width, height, rule, dx = 0, dy = 0 }) {
|
|
443
|
+
if (!Number.isInteger(dx) || !Number.isInteger(dy)) return null;
|
|
444
|
+
// one cell of slack: a span ending in the last column clears the cell
|
|
445
|
+
// past it
|
|
446
|
+
const stride = width + 2;
|
|
447
|
+
const acc = accumulator(stride * height);
|
|
448
|
+
if (triangles) addTriangles(acc, stride, width, height, triangles, dx, dy);
|
|
449
|
+
else addTraps(acc, stride, width, height, trapezoidize(polys, 0, 0, [], rule), dx, dy);
|
|
450
|
+
return integrate(acc, stride, width, height);
|
|
451
|
+
}
|
|
452
|
+
}
|
package/lib/rasterize.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// Pure-JS coverage rasterization: outlines and polygons in, 8-bit alpha out.
|
|
2
2
|
//
|
|
3
|
-
// Two consumers, one algorithm:
|
|
4
|
-
//
|
|
5
3
|
// rasterizePath() glyph outlines -> a8 bitmap for XRender AddGlyphs
|
|
6
|
-
// rasterizePolys() device-space
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// rasterizePolys() device-space polygons -> an a8 coverage mask
|
|
5
|
+
//
|
|
6
|
+
// The 2d context's masks are rasterized locally or by the server
|
|
7
|
+
// (routeRaster below), locally by default with PreciseRasterizer
|
|
8
|
+
// (lib/precise.js), which samples as the server does so that the route
|
|
9
|
+
// never shows. The algorithm here is the analytic one: ScanlineRasterizer,
|
|
10
|
+
// a Rasterizer that can still be plugged in, and what glyph bitmaps use,
|
|
11
|
+
// since a glyph has no server route to agree with.
|
|
9
12
|
//
|
|
10
13
|
// The algorithm is signed-area accumulation, the one font-rs and
|
|
11
14
|
// stb_truetype v2 use. Each edge deposits the exact area it sweeps through
|
|
@@ -15,6 +18,8 @@
|
|
|
15
18
|
// the result is closer to a reference rasterizer than 4x4 supersampling was,
|
|
16
19
|
// at a fraction of the cost (~25 microseconds for a 20px icon against ~600).
|
|
17
20
|
|
|
21
|
+
import { PreciseRasterizer } from './precise.js';
|
|
22
|
+
|
|
18
23
|
/**
|
|
19
24
|
* Antialiased coverage accumulator over a width x height pixel grid.
|
|
20
25
|
*
|
|
@@ -421,7 +426,12 @@ function curveSteps(...coords) {
|
|
|
421
426
|
// A rasterizer that only handles, say, non-zero fills can return null
|
|
422
427
|
// for everything else and stay correct.
|
|
423
428
|
|
|
424
|
-
/**
|
|
429
|
+
/**
|
|
430
|
+
* The analytic rasterizer: CoverageAccumulator, exact area coverage. Its
|
|
431
|
+
* edges are a few levels away from the server's point sampling, so a drawing
|
|
432
|
+
* it rasterizes and one the server does can disagree where they meet — the
|
|
433
|
+
* reason the default is PreciseRasterizer (lib/precise.js).
|
|
434
|
+
*/
|
|
425
435
|
export class ScanlineRasterizer {
|
|
426
436
|
rasterize({ polys, triangles, width, height, rule, dx = 0, dy = 0 }) {
|
|
427
437
|
const acc = new CoverageAccumulator(width, height);
|
|
@@ -433,9 +443,12 @@ export class ScanlineRasterizer {
|
|
|
433
443
|
|
|
434
444
|
let _defaultRasterizer = null;
|
|
435
445
|
|
|
436
|
-
/**
|
|
446
|
+
/**
|
|
447
|
+
* The process-wide default Rasterizer: PreciseRasterizer, whose masks are
|
|
448
|
+
* byte-identical to the server's for the same geometry (lib/precise.js).
|
|
449
|
+
*/
|
|
437
450
|
export function defaultRasterizer() {
|
|
438
|
-
if (!_defaultRasterizer) _defaultRasterizer = new
|
|
451
|
+
if (!_defaultRasterizer) _defaultRasterizer = new PreciseRasterizer();
|
|
439
452
|
return _defaultRasterizer;
|
|
440
453
|
}
|
|
441
454
|
|
|
@@ -29,6 +29,7 @@ import Picture from "./picture.js";
|
|
|
29
29
|
import { formatForDepth } from "./pictformat.js";
|
|
30
30
|
import Pixmap from "./pixmap.js";
|
|
31
31
|
import { REGION_DOCS, regionId } from "./region.js";
|
|
32
|
+
import { snapFixed16 } from "./precise.js";
|
|
32
33
|
import { routeRaster } from "./rasterize.js";
|
|
33
34
|
import {
|
|
34
35
|
clipRingToRect,
|
|
@@ -2098,29 +2099,33 @@ class RenderingContext2d {
|
|
|
2098
2099
|
});
|
|
2099
2100
|
}
|
|
2100
2101
|
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2102
|
+
/**
|
|
2103
|
+
* The shapes a `w`×`h` picture whose pixel (0, 0) is device (-dx, -dy)
|
|
2104
|
+
* is sent, in device space: `_rasterizePolys` trapezoidizes them for the
|
|
2105
|
+
* server, and the local route hands the same ones to the rasterizer, so
|
|
2106
|
+
* that both rasterize one geometry (lib/precise.js).
|
|
2107
|
+
*
|
|
2108
|
+
* Trapezoids are 16.16 fixed point, and a coordinate past 32,767 throws
|
|
2109
|
+
* out of the paint (lib/cliprect.js). They go on the wire in device space
|
|
2110
|
+
* with the picture's offset beside them, which the server adds, so a
|
|
2111
|
+
* shape has to be in reach both in device space and in the picture's.
|
|
2112
|
+
* One that is not is cut to the picture first, a pixel to spare on every
|
|
2113
|
+
* side: nothing outside the picture is ever composited. One in reach
|
|
2114
|
+
* goes as it came, for the server to clip.
|
|
2115
|
+
*/
|
|
2116
|
+
_wireShapes(shapes, dx = 0, dy = 0, w = this.width, h = this.height) {
|
|
2116
2117
|
const x0 = -dx - 1;
|
|
2117
2118
|
const y0 = -dy - 1;
|
|
2118
2119
|
const x1 = w - dx + 1;
|
|
2119
2120
|
const y1 = h - dy + 1;
|
|
2121
|
+
const rx0 = Math.max(-WIRE_REACH, -WIRE_REACH - dx);
|
|
2122
|
+
const ry0 = Math.max(-WIRE_REACH, -WIRE_REACH - dy);
|
|
2123
|
+
const rx1 = Math.min(WIRE_REACH, WIRE_REACH - dx);
|
|
2124
|
+
const ry1 = Math.min(WIRE_REACH, WIRE_REACH - dy);
|
|
2120
2125
|
let fitted = null;
|
|
2121
2126
|
for (let i = 0; i < shapes.length; i++) {
|
|
2122
2127
|
const shape = shapes[i];
|
|
2123
|
-
if (withinRect(shape,
|
|
2128
|
+
if (withinRect(shape, rx0, ry0, rx1, ry1)) {
|
|
2124
2129
|
if (fitted) fitted.push(shape);
|
|
2125
2130
|
continue;
|
|
2126
2131
|
}
|
|
@@ -2128,11 +2133,30 @@ class RenderingContext2d {
|
|
|
2128
2133
|
const ring = clipRingToRect(shape, x0, y0, x1, y1);
|
|
2129
2134
|
if (ring.length >= 6) fitted.push(ring);
|
|
2130
2135
|
}
|
|
2131
|
-
|
|
2136
|
+
return fitted ?? shapes;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
_rasterizePolys(
|
|
2140
|
+
picture,
|
|
2141
|
+
shapes,
|
|
2142
|
+
rule,
|
|
2143
|
+
dx = 0,
|
|
2144
|
+
dy = 0,
|
|
2145
|
+
w = this.width,
|
|
2146
|
+
h = this.height,
|
|
2147
|
+
) {
|
|
2148
|
+
if (!shapes.length) return;
|
|
2149
|
+
// In device space and on the 16.16 grid, with the offset into the
|
|
2150
|
+
// picture left to the request: these are the numbers PreciseRasterizer
|
|
2151
|
+
// rasterizes when the same mask is drawn here, so the two routes agree
|
|
2152
|
+
// to the byte. An offset folded into the coordinates instead rounds
|
|
2153
|
+
// differently from one added to them on the wire.
|
|
2154
|
+
const traps = trapezoidize(this._wireShapes(shapes, dx, dy, w, h), 0, 0, [], rule);
|
|
2155
|
+
for (let i = 0; i < traps.length; i++) traps[i] = snapFixed16(traps[i]);
|
|
2132
2156
|
// stay under the server's maximum request size
|
|
2133
2157
|
const chunk = 4000 * 6;
|
|
2134
2158
|
for (let i = 0; i < traps.length; i += chunk) {
|
|
2135
|
-
this.Render.AddTraps(picture.id,
|
|
2159
|
+
this.Render.AddTraps(picture.id, dx, dy, traps.slice(i, i + chunk));
|
|
2136
2160
|
}
|
|
2137
2161
|
}
|
|
2138
2162
|
|
|
@@ -2491,17 +2515,20 @@ class RenderingContext2d {
|
|
|
2491
2515
|
box = box ? unionBox(box, shown) : shown;
|
|
2492
2516
|
}
|
|
2493
2517
|
if (!box) continue;
|
|
2518
|
+
// what the server would be sent for the surface-sized mask, so that
|
|
2519
|
+
// drawn here it comes out the same
|
|
2520
|
+
const wire = this._wireShapes(flat);
|
|
2494
2521
|
painted =
|
|
2495
2522
|
this._paintThroughMask(
|
|
2496
2523
|
box,
|
|
2497
|
-
{ polys:
|
|
2524
|
+
{ polys: wire, rule },
|
|
2498
2525
|
{
|
|
2499
2526
|
edges: drawn,
|
|
2500
2527
|
route: this._routeFor(cluster, edges),
|
|
2501
2528
|
src,
|
|
2502
2529
|
op,
|
|
2503
2530
|
alpha,
|
|
2504
|
-
server: () => this._rasterizePolys(this.fillMask,
|
|
2531
|
+
server: () => this._rasterizePolys(this.fillMask, wire, rule),
|
|
2505
2532
|
},
|
|
2506
2533
|
) || painted;
|
|
2507
2534
|
}
|
|
@@ -2864,6 +2891,10 @@ class RenderingContext2d {
|
|
|
2864
2891
|
}
|
|
2865
2892
|
if (w < tris.length) tris.length = w;
|
|
2866
2893
|
if (!tris.length) return;
|
|
2894
|
+
// On the 16.16 grid the wire carries, once, for every route below: the
|
|
2895
|
+
// server rasterizes these numbers, and so does PreciseRasterizer when
|
|
2896
|
+
// the stroke is drawn here (lib/precise.js)
|
|
2897
|
+
for (let i = 0; i < tris.length; i++) tris[i] = snapFixed16(tris[i]);
|
|
2867
2898
|
|
|
2868
2899
|
const op = this._op();
|
|
2869
2900
|
// round-cap/join disks overlap the stroke body; overlapping coverage
|
|
@@ -4556,7 +4587,8 @@ class RenderingContext2d {
|
|
|
4556
4587
|
return false;
|
|
4557
4588
|
|
|
4558
4589
|
const coverage = rasterizer.rasterize({
|
|
4559
|
-
|
|
4590
|
+
// what the AddTraps fallback in _applyPolyClip would send
|
|
4591
|
+
polys: this._wireShapes(shapes, -bb.x, -bb.y, bb.w, bb.h),
|
|
4560
4592
|
rule: entry.rule,
|
|
4561
4593
|
dx: -bb.x,
|
|
4562
4594
|
dy: -bb.y,
|