ntk 8.17.8 → 8.18.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/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/app.js CHANGED
@@ -991,6 +991,9 @@ export default class App {
991
991
  p = new Picture(this, { id: pid });
992
992
  p._sourcePixmap = pixmap; // keep the 1x1 pixmap alive alongside the picture
993
993
  }
994
+ // what it paints with, so a drawing can fold `globalAlpha` into it: glyphs
995
+ // composite straight from their source, with no mask slot to carry one
996
+ p._rgba = Object.freeze([r, g, b, a]);
994
997
  this._solidPictures.set(key, p);
995
998
  return p;
996
999
  }
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,506 @@
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
+ // Plain float divisions truncate exactly here. A quotient of integers
148
+ // whose dividend is below 2^45 sits at least 1/dy from any integer it
149
+ // is not, and a double holds it to within (dividend / dy) · 2^-52, far
150
+ // closer than that. The step from the line's top below, whose dividend
151
+ // can pass 2^53, keeps idiv.
152
+ if (dx >= 0) {
153
+ signdx = 1;
154
+ stepx = Math.trunc(dx / dy);
155
+ rem = dx - stepx * dy;
156
+ e = -dy;
157
+ } else {
158
+ signdx = -1;
159
+ stepx = -Math.trunc(-dx / dy);
160
+ rem = -dx + stepx * dy;
161
+ }
162
+ // _pixman_edge_multi_init for the small and the big step; n is at most
163
+ // 4370, so every product here is exact in a double
164
+ let ne = STEP_Y_SMALL * rem;
165
+ let sx = (STEP_Y_SMALL * stepx) | 0;
166
+ if (ne > 0) {
167
+ const nx = Math.trunc(ne / dy);
168
+ ne -= nx * dy;
169
+ sx = (sx + nx * signdx) | 0;
170
+ }
171
+ edge[STEPX_SMALL] = sx;
172
+ edge[DX_SMALL] = ne;
173
+ ne = STEP_Y_BIG * rem;
174
+ sx = (STEP_Y_BIG * stepx) | 0;
175
+ if (ne > 0) {
176
+ const nx = Math.trunc(ne / dy);
177
+ ne -= nx * dy;
178
+ sx = (sx + nx * signdx) | 0;
179
+ }
180
+ edge[STEPX_BIG] = sx;
181
+ edge[DX_BIG] = ne;
182
+ }
183
+
184
+ // pixman_edge_step(e, yStart - yTop). Its products are 64-bit in C and
185
+ // pass 2^53 for a tall edge entering the grid far below its top, so past
186
+ // that they are done in BigInt.
187
+ const n = (yStart - yTop) | 0;
188
+ const step = n * stepx;
189
+ x = Number.isSafeInteger(step)
190
+ ? (x + step) | 0
191
+ : Number(BigInt.asIntN(32, BigInt(x) + BigInt(n) * BigInt(stepx)));
192
+ const carry = n * rem;
193
+ if (Number.isSafeInteger(carry) && Number.isSafeInteger(e + carry + dy)) {
194
+ const ne = e + carry;
195
+ if (n >= 0) {
196
+ if (ne > 0) {
197
+ const nx = idiv(ne + dy - 1, dy) | 0;
198
+ e = (ne - nx * dy) | 0;
199
+ x = (x + nx * signdx) | 0;
200
+ }
201
+ } else if (ne <= -dy) {
202
+ const nx = idiv(-ne, dy) | 0;
203
+ e = (ne + nx * dy) | 0;
204
+ x = (x - nx * signdx) | 0;
205
+ }
206
+ } else {
207
+ const ne = BigInt(e) + BigInt(n) * BigInt(rem);
208
+ const bdy = BigInt(dy);
209
+ if (n >= 0) {
210
+ if (ne > 0n) {
211
+ const nx = BigInt.asIntN(32, (ne + bdy - 1n) / bdy);
212
+ e = Number(BigInt.asIntN(32, ne - nx * bdy));
213
+ x = (x + Number(nx) * signdx) | 0;
214
+ }
215
+ } else if (ne <= -bdy) {
216
+ const nx = BigInt.asIntN(32, -ne / bdy);
217
+ e = Number(BigInt.asIntN(32, ne + nx * bdy));
218
+ x = (x - Number(nx) * signdx) | 0;
219
+ }
220
+ }
221
+ edge[X] = x;
222
+ edge[E] = e;
223
+ edge[DY] = dy;
224
+ edge[SIGNDX] = signdx;
225
+ }
226
+
227
+ // RENDER_SAMPLES_X for every fraction of a pixel: how many of a pixel's 17
228
+ // sample columns lie left of an edge that crosses it there
229
+ const SAMPLES_X = new Uint8Array(65536);
230
+ for (let f = 0; f < 65536; f++) SAMPLES_X[f] = ((f + X_FRAC_FIRST) / STEP_X_SMALL) | 0;
231
+
232
+ /**
233
+ * rasterize_edges_8 between `left` and `right`, from sample row `t` to `b`
234
+ * inclusive, into the difference array `acc` (rows `stride` apart).
235
+ *
236
+ * A span adds 17 - ls to the pixel it starts in, 17 to each pixel after, rs
237
+ * to the pixel it ends in and nothing past it: the difference of two steps,
238
+ * each two cells of `acc`. While an edge stays in one pixel from sample row
239
+ * to sample row, as a steep one does for all fifteen, its two cells are
240
+ * summed here and written once.
241
+ */
242
+ function addSpans(acc, stride, width, t, b) {
243
+ let y = t;
244
+ let row = (y >> 16) * stride;
245
+ let lx = left[X];
246
+ let le = left[E];
247
+ const ldy = left[DY];
248
+ const lsign = left[SIGNDX];
249
+ const lsxs = left[STEPX_SMALL];
250
+ const ldxs = left[DX_SMALL];
251
+ const lsxb = left[STEPX_BIG];
252
+ const ldxb = left[DX_BIG];
253
+ let rx = right[X];
254
+ let re = right[E];
255
+ const rdy = right[DY];
256
+ const rsign = right[SIGNDX];
257
+ const rsxs = right[STEPX_SMALL];
258
+ const rdxs = right[DX_SMALL];
259
+ const rsxb = right[STEPX_BIG];
260
+ const rdxb = right[DX_BIG];
261
+ // pixman clips the span to the image: from 0, and to the last pixel of
262
+ // the row taken whole. Both are what an unclipped span would add there.
263
+ const lastX = (width << 16) - 1;
264
+ // each edge's step not yet written: the pixel it is in (-1 for none), and
265
+ // what it adds to that cell and to the next
266
+ let lPixel = -1;
267
+ let lAt = 0;
268
+ let lPast = 0;
269
+ let rPixel = -1;
270
+ let rAt = 0;
271
+ let rPast = 0;
272
+ // an edge's step carries one more unit of x when its error term passes
273
+ // zero: computed, not branched on, since a slope's carries come in no
274
+ // pattern a branch predictor can learn
275
+ let c = 0;
276
+ for (;;) {
277
+ const l = lx < 0 ? 0 : lx;
278
+ const r = rx >> 16 >= width ? lastX : rx;
279
+ if (r > l) {
280
+ const li = l >> 16;
281
+ const ls = SAMPLES_X[l & 0xffff];
282
+ if (li !== lPixel) {
283
+ if (lPixel >= 0) {
284
+ acc[row + lPixel] += lAt;
285
+ acc[row + lPixel + 1] += lPast;
286
+ }
287
+ lPixel = li;
288
+ lAt = 0;
289
+ lPast = 0;
290
+ }
291
+ lAt += N_X_FRAC - ls;
292
+ lPast += ls;
293
+ const ri = r >> 16;
294
+ const rs = SAMPLES_X[r & 0xffff];
295
+ if (ri !== rPixel) {
296
+ if (rPixel >= 0) {
297
+ acc[row + rPixel] -= rAt;
298
+ acc[row + rPixel + 1] -= rPast;
299
+ }
300
+ rPixel = ri;
301
+ rAt = 0;
302
+ rPast = 0;
303
+ }
304
+ rAt += N_X_FRAC - rs;
305
+ rPast += rs;
306
+ }
307
+ if (y === b) break;
308
+ if ((y & 0xffff) !== Y_FRAC_LAST) {
309
+ le = (le + ldxs) | 0;
310
+ c = -le >>> 31;
311
+ le = (le - c * ldy) | 0;
312
+ lx = (lx + lsxs + c * lsign) | 0;
313
+ re = (re + rdxs) | 0;
314
+ c = -re >>> 31;
315
+ re = (re - c * rdy) | 0;
316
+ rx = (rx + rsxs + c * rsign) | 0;
317
+ y = (y + STEP_Y_SMALL) | 0;
318
+ } else {
319
+ le = (le + ldxb) | 0;
320
+ c = -le >>> 31;
321
+ le = (le - c * ldy) | 0;
322
+ lx = (lx + lsxb + c * lsign) | 0;
323
+ re = (re + rdxb) | 0;
324
+ c = -re >>> 31;
325
+ re = (re - c * rdy) | 0;
326
+ rx = (rx + rsxb + c * rsign) | 0;
327
+ y = (y + STEP_Y_BIG) | 0;
328
+ // the pixel row is done: write what it left pending
329
+ if (lPixel >= 0) {
330
+ acc[row + lPixel] += lAt;
331
+ acc[row + lPixel + 1] += lPast;
332
+ lPixel = -1;
333
+ }
334
+ if (rPixel >= 0) {
335
+ acc[row + rPixel] -= rAt;
336
+ acc[row + rPixel + 1] -= rPast;
337
+ rPixel = -1;
338
+ }
339
+ row += stride;
340
+ }
341
+ }
342
+ if (lPixel >= 0) {
343
+ acc[row + lPixel] += lAt;
344
+ acc[row + lPixel + 1] += lPast;
345
+ }
346
+ if (rPixel >= 0) {
347
+ acc[row + rPixel] -= rAt;
348
+ acc[row + rPixel + 1] -= rPast;
349
+ }
350
+ }
351
+
352
+ /** the first sample row of a band from `top`, inside the grid */
353
+ function firstRow(top) {
354
+ return sampleCeilY(top < 0 ? 0 : top);
355
+ }
356
+
357
+ /** the last sample row of a band to `bottom`, inside a grid `height` high */
358
+ function lastRow(bottom, height) {
359
+ return sampleFloorY(bottom >> 16 >= height ? (height << 16) - 1 : bottom);
360
+ }
361
+
362
+ /**
363
+ * pixman_rasterize_trapezoid: a pixman_trapezoid_t — top and bottom, each
364
+ * side a line through two points — already offset into the grid
365
+ */
366
+ function addTrapezoid(acc, stride, width, height, top, bottom, l1x, l1y, l2x, l2y, r1x, r1y, r2x, r2y) {
367
+ if (l1y === l2y || r1y === r2y || !(bottom > top)) return;
368
+ const t = firstRow(top);
369
+ const b = lastRow(bottom, height);
370
+ if (b < t) return;
371
+ if (l1y <= l2y) edgeInit(left, t, l1x, l1y, l2x, l2y);
372
+ else edgeInit(left, t, l2x, l2y, l1x, l1y);
373
+ if (r1y <= r2y) edgeInit(right, t, r1x, r1y, r2x, r2y);
374
+ else edgeInit(right, t, r2x, r2y, r1x, r1y);
375
+ addSpans(acc, stride, width, t, b);
376
+ }
377
+
378
+ // whether b is clockwise of a about ref in y-down space, as pixman asks it
379
+ function clockwise(refX, refY, ax, ay, bx, by) {
380
+ const adx = ax - refX;
381
+ const ady = ay - refY;
382
+ const bdx = bx - refX;
383
+ const bdy = by - refY;
384
+ const p = bdy * adx;
385
+ const q = ady * bdx;
386
+ if (Number.isSafeInteger(p) && Number.isSafeInteger(q)) return p - q < 0;
387
+ return BigInt(bdy) * BigInt(adx) - BigInt(ady) * BigInt(bdx) < 0n;
388
+ }
389
+
390
+ /**
391
+ * Add a Triangles request's triangles — float device coordinates, six per
392
+ * triangle — to the grid, each split into two trapezoids as
393
+ * triangle_to_trapezoids does.
394
+ */
395
+ function addTriangles(acc, stride, width, height, tris, dx, dy) {
396
+ const xo = dx * 65536;
397
+ const yo = dy * 65536;
398
+ for (let i = 0; i + 5 < tris.length; i += 6) {
399
+ let tx = (fixed16(tris[i]) + xo) | 0;
400
+ let ty = (fixed16(tris[i + 1]) + yo) | 0;
401
+ let lx = (fixed16(tris[i + 2]) + xo) | 0;
402
+ let ly = (fixed16(tris[i + 3]) + yo) | 0;
403
+ let rx = (fixed16(tris[i + 4]) + xo) | 0;
404
+ let ry = (fixed16(tris[i + 5]) + yo) | 0;
405
+ let s;
406
+ // the topmost point first, ties to the left; then left before right
407
+ if (ty === ly ? tx > lx : ty > ly) {
408
+ s = lx; lx = tx; tx = s;
409
+ s = ly; ly = ty; ty = s;
410
+ }
411
+ if (ty === ry ? tx > rx : ty > ry) {
412
+ s = rx; rx = tx; tx = s;
413
+ s = ry; ry = ty; ty = s;
414
+ }
415
+ if (clockwise(tx, ty, rx, ry, lx, ly)) {
416
+ s = rx; rx = lx; lx = s;
417
+ s = ry; ry = ly; ly = s;
418
+ }
419
+ // down from the top to the higher of the other two, then on to the
420
+ // lower, with the side that turned there continuing along the third edge
421
+ const mid = ry < ly ? ry : ly;
422
+ addTrapezoid(acc, stride, width, height, ty, mid, tx, ty, lx, ly, tx, ty, rx, ry);
423
+ if (ry < ly) addTrapezoid(acc, stride, width, height, ry, ly, tx, ty, lx, ly, rx, ry, lx, ly);
424
+ else addTrapezoid(acc, stride, width, height, ly, ry, lx, ly, rx, ry, tx, ty, rx, ry);
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Add an AddTraps request's trapezoids — float device coordinates, six per
430
+ * trapezoid: top left, right, y, then bottom left, right, y — to the grid,
431
+ * as pixman_add_traps does.
432
+ */
433
+ function addTraps(acc, stride, width, height, traps, dx, dy) {
434
+ const xo = dx * 65536;
435
+ const yo = dy * 65536;
436
+ for (let i = 0; i + 5 < traps.length; i += 6) {
437
+ const topY = (fixed16(traps[i + 2]) + yo) | 0;
438
+ const botY = (fixed16(traps[i + 5]) + yo) | 0;
439
+ const t = firstRow(topY);
440
+ const b = lastRow(botY, height);
441
+ if (b < t) continue;
442
+ const tl = (fixed16(traps[i]) + xo) | 0;
443
+ const tr = (fixed16(traps[i + 1]) + xo) | 0;
444
+ const bl = (fixed16(traps[i + 3]) + xo) | 0;
445
+ const br = (fixed16(traps[i + 4]) + xo) | 0;
446
+ edgeInit(left, t, tl, topY, bl, botY);
447
+ edgeInit(right, t, tr, topY, br, botY);
448
+ addSpans(acc, stride, width, t, b);
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Each row's running sum, clamped: the saturating adds, all at once. The
454
+ * cells are zeroed as they are read, so the accumulator is ready for the
455
+ * next mask without a pass of its own.
456
+ */
457
+ function integrate(acc, stride, width, height) {
458
+ const out = new Uint8Array(width * height);
459
+ for (let y = 0; y < height; y++) {
460
+ let sum = 0;
461
+ const from = y * stride;
462
+ const to = y * width;
463
+ for (let x = 0; x < width; x++) {
464
+ sum += acc[from + x];
465
+ acc[from + x] = 0;
466
+ out[to + x] = sum > 255 ? 255 : sum;
467
+ }
468
+ // the slack cells past the row, which a span ending at its last pixel
469
+ // writes and nothing reads
470
+ acc[from + width] = 0;
471
+ acc[from + width + 1] = 0;
472
+ }
473
+ return out;
474
+ }
475
+
476
+ // One accumulator for every mask, grown to the largest asked for: masks are
477
+ // rasterized one at a time, and allocating a fresh one each time was a
478
+ // tenth of the cost of a wall of small icons, in the garbage collector.
479
+ let scratch = new Int32Array(0);
480
+
481
+ function accumulator(cells) {
482
+ if (scratch.length < cells) scratch = new Int32Array(Math.max(cells, scratch.length * 2));
483
+ return scratch;
484
+ }
485
+
486
+ /**
487
+ * A Rasterizer (docs/context-2d.md) whose masks are the ones the X server
488
+ * draws from the same geometry: `triangles` exactly as the 2d context sends
489
+ * them with Triangles, `polys` trapezoidized exactly as it sends them with
490
+ * AddTraps. A drawing gets the same bytes whichever side rasterizes it.
491
+ *
492
+ * `dx`/`dy` are whole pixels, which is all the 2d context passes; anything
493
+ * else is declined, and goes to the server.
494
+ */
495
+ export class PreciseRasterizer {
496
+ rasterize({ polys, triangles, width, height, rule, dx = 0, dy = 0 }) {
497
+ if (!Number.isInteger(dx) || !Number.isInteger(dy)) return null;
498
+ // one cell of slack: a span ending in the last column clears the cell
499
+ // past it
500
+ const stride = width + 2;
501
+ const acc = accumulator(stride * height);
502
+ if (triangles) addTriangles(acc, stride, width, height, triangles, dx, dy);
503
+ else addTraps(acc, stride, width, height, trapezoidize(polys, 0, 0, [], rule), dx, dy);
504
+ return integrate(acc, stride, width, height);
505
+ }
506
+ }
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 path/stroke geometry -> a8 coverage mask,
7
- // uploaded by the 2d context instead of asking the server
8
- // to rasterize trapezoids (see routeRaster below)
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
- /** the in-repo rasterizer: CoverageAccumulator, no dependencies */
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
- /** the process-wide default Rasterizer */
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 ScanlineRasterizer();
451
+ if (!_defaultRasterizer) _defaultRasterizer = new PreciseRasterizer();
439
452
  return _defaultRasterizer;
440
453
  }
441
454