ntk 8.18.0 → 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/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/precise.js CHANGED
@@ -144,22 +144,27 @@ function edgeInit(edge, yStart, xTop, yTop, xBot, yBot) {
144
144
  let signdx = 0;
145
145
  let rem = 0;
146
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.
147
152
  if (dx >= 0) {
148
153
  signdx = 1;
149
- stepx = idiv(dx, dy);
150
- rem = dx % dy;
154
+ stepx = Math.trunc(dx / dy);
155
+ rem = dx - stepx * dy;
151
156
  e = -dy;
152
157
  } else {
153
158
  signdx = -1;
154
- stepx = -idiv(-dx, dy);
155
- rem = -dx % dy;
159
+ stepx = -Math.trunc(-dx / dy);
160
+ rem = -dx + stepx * dy;
156
161
  }
157
162
  // _pixman_edge_multi_init for the small and the big step; n is at most
158
163
  // 4370, so every product here is exact in a double
159
164
  let ne = STEP_Y_SMALL * rem;
160
165
  let sx = (STEP_Y_SMALL * stepx) | 0;
161
166
  if (ne > 0) {
162
- const nx = idiv(ne, dy) | 0;
167
+ const nx = Math.trunc(ne / dy);
163
168
  ne -= nx * dy;
164
169
  sx = (sx + nx * signdx) | 0;
165
170
  }
@@ -168,7 +173,7 @@ function edgeInit(edge, yStart, xTop, yTop, xBot, yBot) {
168
173
  ne = STEP_Y_BIG * rem;
169
174
  sx = (STEP_Y_BIG * stepx) | 0;
170
175
  if (ne > 0) {
171
- const nx = idiv(ne, dy) | 0;
176
+ const nx = Math.trunc(ne / dy);
172
177
  ne -= nx * dy;
173
178
  sx = (sx + nx * signdx) | 0;
174
179
  }
@@ -219,9 +224,20 @@ function edgeInit(edge, yStart, xTop, yTop, xBot, yBot) {
219
224
  edge[SIGNDX] = signdx;
220
225
  }
221
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
+
222
232
  /**
223
233
  * rasterize_edges_8 between `left` and `right`, from sample row `t` to `b`
224
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.
225
241
  */
226
242
  function addSpans(acc, stride, width, t, b) {
227
243
  let y = t;
@@ -245,54 +261,92 @@ function addSpans(acc, stride, width, t, b) {
245
261
  // pixman clips the span to the image: from 0, and to the last pixel of
246
262
  // the row taken whole. Both are what an unclipped span would add there.
247
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;
248
276
  for (;;) {
249
277
  const l = lx < 0 ? 0 : lx;
250
278
  const r = rx >> 16 >= width ? lastX : rx;
251
279
  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
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;
256
293
  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;
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;
263
306
  }
264
- if (y === b) return;
307
+ if (y === b) break;
265
308
  if ((y & 0xffff) !== Y_FRAC_LAST) {
266
- lx = (lx + lsxs) | 0;
267
309
  le = (le + ldxs) | 0;
268
- if (le > 0) {
269
- le = (le - ldy) | 0;
270
- lx = (lx + lsign) | 0;
271
- }
272
- rx = (rx + rsxs) | 0;
310
+ c = -le >>> 31;
311
+ le = (le - c * ldy) | 0;
312
+ lx = (lx + lsxs + c * lsign) | 0;
273
313
  re = (re + rdxs) | 0;
274
- if (re > 0) {
275
- re = (re - rdy) | 0;
276
- rx = (rx + rsign) | 0;
277
- }
314
+ c = -re >>> 31;
315
+ re = (re - c * rdy) | 0;
316
+ rx = (rx + rsxs + c * rsign) | 0;
278
317
  y = (y + STEP_Y_SMALL) | 0;
279
318
  } else {
280
- lx = (lx + lsxb) | 0;
281
319
  le = (le + ldxb) | 0;
282
- if (le > 0) {
283
- le = (le - ldy) | 0;
284
- lx = (lx + lsign) | 0;
285
- }
286
- rx = (rx + rsxb) | 0;
320
+ c = -le >>> 31;
321
+ le = (le - c * ldy) | 0;
322
+ lx = (lx + lsxb + c * lsign) | 0;
287
323
  re = (re + rdxb) | 0;
288
- if (re > 0) {
289
- re = (re - rdy) | 0;
290
- rx = (rx + rsign) | 0;
291
- }
324
+ c = -re >>> 31;
325
+ re = (re - c * rdy) | 0;
326
+ rx = (rx + rsxb + c * rsign) | 0;
292
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
+ }
293
339
  row += stride;
294
340
  }
295
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
+ }
296
350
  }
297
351
 
298
352
  /** the first sample row of a band from `top`, inside the grid */
@@ -3169,8 +3169,12 @@ class RenderingContext2d {
3169
3169
  * relative positions, painted under the glyphs (issue #283). A paragraph
3170
3170
  * whose spans change colour draws as several calls, and — as several
3171
3171
  * `fillText`s would — casts a shadow per call.
3172
+ *
3173
+ * So does `globalAlpha`, to the glyphs and their shadow both, as it does
3174
+ * to every fill: at 0 nothing is drawn, not even the shadow.
3172
3175
  */
3173
3176
  drawGlyphs(op, src, positioned) {
3177
+ if (this.globalAlpha <= 0) return;
3174
3178
  const m = this._m;
3175
3179
  if (!matIsIdentity(m)) {
3176
3180
  positioned = positioned.map((p) => {
@@ -3195,12 +3199,19 @@ class RenderingContext2d {
3195
3199
  * instead, intersect that with the clip, and paint the real source
3196
3200
  * through the result — the same shape as _fillPolys. A rectangular clip
3197
3201
  * takes the server-side fast path below instead of the mask.
3202
+ *
3203
+ * Nor has it a mask slot for `globalAlpha`, which is why text used to be
3204
+ * drawn at full opacity whatever it was. A solid source takes the alpha
3205
+ * in its colour, as `fillRect`'s does, and the glyphs go the way they
3206
+ * always went; a gradient or a pattern has no one colour to fold it
3207
+ * into, and paints through the scratch mask with the alpha in there.
3198
3208
  */
3199
3209
  _drawGlyphsDevice(op, src, positioned) {
3210
+ if (this.globalAlpha <= 0) return;
3200
3211
  const app = this.window.app;
3201
- const R = this.Render;
3202
3212
  if (!prepareStyle(src, this._m, this.width, this.height)) return;
3203
- if (!this._hasPolyClip) {
3213
+ const faded = this._fadedSource(src);
3214
+ if (faded && !this._hasPolyClip) {
3204
3215
  // Fast path: a rectangular clip is something the server can do itself.
3205
3216
  // Two small requests around the ordinary glyph composite, instead of
3206
3217
  // clearing and compositing a full-surface a8 mask three times — which
@@ -3215,77 +3226,37 @@ class RenderingContext2d {
3215
3226
  drawGlyphRuns(
3216
3227
  app,
3217
3228
  op,
3218
- src.id,
3229
+ faded.id,
3219
3230
  this._dst(),
3220
3231
  positioned,
3221
3232
  rect ?? { x: 0, y: 0, w: this.width, h: this.height },
3222
- sourceOrigin(src),
3233
+ sourceOrigin(faded),
3223
3234
  );
3224
3235
  if (rect) this._invalidatePictureClip();
3225
3236
  this._markDirty();
3226
3237
  return;
3227
3238
  }
3228
- const clipMask = this._requireClipMask();
3229
- this._ensureFillMask();
3230
- this._glyphSource ??= this.createSolidPicture(1, 1, 1, 1);
3231
- R.FillRectangles(
3232
- R.PictOp.Src,
3233
- this.fillMask.id,
3234
- [0, 0, 0, 0],
3235
- [0, 0, this.width, this.height],
3236
- );
3237
- // solid white through the glyphs leaves their coverage in the a8 mask
3238
- drawGlyphRuns(
3239
- app,
3240
- R.PictOp.Over,
3241
- this._glyphSource.id,
3242
- this.fillMask.id,
3243
- positioned,
3244
- { x: 0, y: 0, w: this.width, h: this.height },
3245
- );
3246
- R.Composite(
3247
- R.PictOp.In,
3248
- clipMask.id,
3249
- 0,
3250
- this.fillMask.id,
3251
- 0,
3252
- 0,
3253
- 0,
3254
- 0,
3255
- 0,
3256
- 0,
3257
- this.width,
3258
- this.height,
3259
- );
3260
- const [ox, oy] = sourceOrigin(src);
3261
- R.Composite(
3262
- op,
3263
- src.id,
3264
- this.fillMask.id,
3265
- this._dst(),
3266
- -ox,
3267
- -oy,
3268
- 0,
3269
- 0,
3270
- 0,
3271
- 0,
3272
- this.width,
3273
- this.height,
3239
+ const R = this.Render;
3240
+ const alpha = faded ? 1 : this.globalAlpha;
3241
+ const surface = { x: 0, y: 0, w: this.width, h: this.height };
3242
+ this._paintThroughScratch(op, faded ?? src, alpha, (white, mask) =>
3243
+ drawGlyphRuns(app, R.PictOp.Over, white, mask, positioned, surface),
3274
3244
  );
3275
- this._markDirty();
3276
3245
  }
3277
3246
 
3278
3247
  /**
3279
3248
  * Trapezoid coverage under the clip — `drawGlyphs` for vector shapes that
3280
3249
  * are already trapezoidized (KaTeX radicals and rules go this way).
3281
3250
  * Without it they composite straight to the destination picture and spill
3282
- * out of clipped boxes, which is what glyphs used to do.
3251
+ * out of clipped boxes, which is what glyphs used to do. `globalAlpha`
3252
+ * applies as it does to glyphs, so a radical fades with the formula.
3283
3253
  */
3284
3254
  drawTraps(op, src, traps) {
3285
- if (!traps.length) return;
3255
+ if (!traps.length || this.globalAlpha <= 0) return;
3286
3256
  const app = this.window.app;
3287
- const R = this.Render;
3288
- if (!this._hasPolyClip) {
3257
+ if (!prepareStyle(src, this._m, this.width, this.height)) return;
3258
+ const faded = this._fadedSource(src);
3259
+ if (faded && !this._hasPolyClip) {
3289
3260
  const rect = this._clipRect();
3290
3261
  if (rect) {
3291
3262
  if (rect.w === 0 || rect.h === 0) return;
@@ -3294,18 +3265,57 @@ class RenderingContext2d {
3294
3265
  compositeTraps(
3295
3266
  app,
3296
3267
  op,
3297
- src.id,
3268
+ faded.id,
3298
3269
  this._dst(),
3299
3270
  traps,
3300
3271
  undefined,
3301
3272
  undefined,
3302
- sourceOrigin(src),
3273
+ sourceOrigin(faded),
3303
3274
  );
3304
3275
  if (rect) this._invalidatePictureClip();
3305
3276
  this._markDirty();
3306
3277
  return;
3307
3278
  }
3308
- const clipMask = this._requireClipMask();
3279
+ const alpha = faded ? 1 : this.globalAlpha;
3280
+ this._paintThroughScratch(op, faded ?? src, alpha, (white, mask) =>
3281
+ compositeTraps(app, this.Render.PictOp.Over, white, mask, traps),
3282
+ );
3283
+ }
3284
+
3285
+ /**
3286
+ * `src` with `globalAlpha` folded into it: the source itself at full
3287
+ * opacity, and at less, a solid of its colour with all four premultiplied
3288
+ * components scaled — which is compositing it at that opacity, for any op
3289
+ * (`_foldedColor`). Null when there is an alpha to apply and no colour to
3290
+ * fold it into: a gradient, a pattern, a picture of the caller's, or a
3291
+ * solid from a host whose `solidPicture` does not say what it paints with
3292
+ * (App#solidPicture does).
3293
+ */
3294
+ _fadedSource(src) {
3295
+ const a = this.globalAlpha;
3296
+ if (a >= 1) return src;
3297
+ const c = src._rgba;
3298
+ if (!c) return null;
3299
+ return this.createSolidPicture(c[0] * a, c[1] * a, c[2] * a, c[3] * a);
3300
+ }
3301
+
3302
+ /**
3303
+ * Paint `src` through coverage the server draws into the scratch a8 mask:
3304
+ * how glyphs and trapezoids reach the surface when their own composite
3305
+ * cannot carry what applies to it — a clip that is not a rectangle, or a
3306
+ * `globalAlpha` with no colour to fold into.
3307
+ *
3308
+ * `drawCoverage(white, mask)` composites the coverage into the mask, with
3309
+ * a solid white source: what that leaves in an a8 picture is exactly the
3310
+ * coverage. `alpha` scales it, the clip cuts it — the clip mask for a
3311
+ * stack with a path in it, the picture's clip rectangle for one of
3312
+ * rectangles — and `src` is composited through the result.
3313
+ */
3314
+ _paintThroughScratch(op, src, alpha, drawCoverage) {
3315
+ const R = this.Render;
3316
+ const rect = this._clipRect();
3317
+ if (rect && (rect.w === 0 || rect.h === 0)) return;
3318
+ const clipMask = this._hasPolyClip ? this._requireClipMask() : null;
3309
3319
  this._ensureFillMask();
3310
3320
  this._glyphSource ??= this.createSolidPicture(1, 1, 1, 1);
3311
3321
  R.FillRectangles(
@@ -3314,27 +3324,34 @@ class RenderingContext2d {
3314
3324
  [0, 0, 0, 0],
3315
3325
  [0, 0, this.width, this.height],
3316
3326
  );
3317
- compositeTraps(
3318
- app,
3319
- R.PictOp.Over,
3320
- this._glyphSource.id,
3321
- this.fillMask.id,
3322
- traps,
3323
- );
3324
- R.Composite(
3325
- R.PictOp.In,
3326
- clipMask.id,
3327
- 0,
3328
- this.fillMask.id,
3329
- 0,
3330
- 0,
3331
- 0,
3332
- 0,
3333
- 0,
3334
- 0,
3335
- this.width,
3336
- this.height,
3337
- );
3327
+ drawCoverage(this._glyphSource.id, this.fillMask.id);
3328
+ if (alpha < 1) {
3329
+ // In with a constant color scales the a8 coverage by that alpha
3330
+ R.FillRectangles(
3331
+ R.PictOp.In,
3332
+ this.fillMask.id,
3333
+ [0, 0, 0, alpha],
3334
+ [0, 0, this.width, this.height],
3335
+ );
3336
+ }
3337
+ if (clipMask) {
3338
+ R.Composite(
3339
+ R.PictOp.In,
3340
+ clipMask.id,
3341
+ 0,
3342
+ this.fillMask.id,
3343
+ 0,
3344
+ 0,
3345
+ 0,
3346
+ 0,
3347
+ 0,
3348
+ 0,
3349
+ this.width,
3350
+ this.height,
3351
+ );
3352
+ } else if (rect) {
3353
+ this._setPictureClip(rect);
3354
+ }
3338
3355
  const [ox, oy] = sourceOrigin(src);
3339
3356
  R.Composite(
3340
3357
  op,
@@ -3350,6 +3367,7 @@ class RenderingContext2d {
3350
3367
  this.width,
3351
3368
  this.height,
3352
3369
  );
3370
+ if (rect) this._invalidatePictureClip();
3353
3371
  this._markDirty();
3354
3372
  }
3355
3373
 
@@ -4723,7 +4741,8 @@ class RenderingContext2d {
4723
4741
  */
4724
4742
  fillText(text, x, y) {
4725
4743
  text = String(text ?? "");
4726
- if (!text) return;
4744
+ // nothing to draw, and at globalAlpha 0 nothing to shape it for
4745
+ if (!text || this.globalAlpha <= 0) return;
4727
4746
  if (this._shadowed()) this._shadowOfText(text, x, y);
4728
4747
  const style = this._resolvedTextStyle();
4729
4748
  const app = this.window.app;
@@ -5216,14 +5235,19 @@ class RenderingContext2d {
5216
5235
  * into it. The mask slot is taken by the coverage, so the alpha has to go
5217
5236
  * somewhere — and a 1x1 solid is free, where a surface-sized alpha mask is
5218
5237
  * not. Falls back to the fill picture for gradients, which have no single
5219
- * colour to fold into; `_drawCoverage` routes those through the scratch. */
5238
+ * colour to fold into; `_drawCoverage` routes those through the scratch.
5239
+ *
5240
+ * All four components are scaled, not the alpha alone: the colour is
5241
+ * premultiplied, and one whose alpha shrank under unchanged components is
5242
+ * brighter than it can be — `#0000ff` at 0.5 over black came out
5243
+ * `#0000ff`, where `fillRect` draws `#000080`. */
5220
5244
  _coverageSource() {
5221
5245
  const style = this._fillStyle;
5222
5246
  if (this.globalAlpha >= 1 || !isPlainColor(style)) {
5223
5247
  return this._backgroundPicture;
5224
5248
  }
5225
- const c = parseColor(style);
5226
- return this.createSolidPicture(c[0], c[1], c[2], c[3] * this.globalAlpha);
5249
+ const c = this._foldedColor(style);
5250
+ return this.createSolidPicture(c[0], c[1], c[2], c[3]);
5227
5251
  }
5228
5252
 
5229
5253
  /**
package/lib/trapezoid.js CHANGED
@@ -37,7 +37,7 @@ export function trapezoidize(polys, dx = 0, dy = 0, out = [], rule = 'nonzero')
37
37
  const evenodd = rule === 'evenodd';
38
38
  // collect non-horizontal edges, normalized to y0 < y1 with winding dir
39
39
  const edges = [];
40
- const ys = [];
40
+ let ys = [];
41
41
  // the largest coordinate magnitude, which sets the rounding noise
42
42
  let reach = 1;
43
43
  for (const poly of polys) {
@@ -66,14 +66,17 @@ export function trapezoidize(polys, dx = 0, dy = 0, out = [], rule = 'nonzero')
66
66
  // coordinate that fits one
67
67
  const eps = reach * 2 ** -44;
68
68
 
69
- ys.sort((a, b) => a - b);
69
+ // a typed array sorts numerically without a comparator call per step
70
+ ys = Float64Array.from(ys).sort();
70
71
  edges.sort((a, b) => a.y0 - b.y0);
71
72
 
72
73
  const active = [];
73
74
  let nextEdge = 0;
74
- // spans open from the previous slab, for vertical merging:
75
- // { l: leftEdge, r: rightEdge, at: index of the trap in `out` }
75
+ // spans open from the previous slab, for vertical merging, and the ones
76
+ // this slab opens: flat [leftEdge, rightEdge, index of the trap in `out`]
77
+ // triples, two buffers that trade places every slab
76
78
  let prevSpans = [];
79
+ let spans = [];
77
80
 
78
81
  for (let yi = 0; yi < ys.length - 1; ++yi) {
79
82
  const ya = ys[yi];
@@ -81,25 +84,38 @@ export function trapezoidize(polys, dx = 0, dy = 0, out = [], rule = 'nonzero')
81
84
  if (yb === ya) continue;
82
85
 
83
86
  // update the active edge list for this slab
84
- for (let i = active.length - 1; i >= 0; --i) {
85
- if (active[i].y1 <= ya) active.splice(i, 1);
87
+ let kept = 0;
88
+ for (let i = 0; i < active.length; ++i) {
89
+ if (active[i].y1 > ya) active[kept++] = active[i];
86
90
  }
91
+ active.length = kept;
87
92
  while (nextEdge < edges.length && edges[nextEdge].y0 <= ya) {
88
93
  if (edges[nextEdge].y1 > ya) active.push(edges[nextEdge]);
89
94
  nextEdge++;
90
95
  }
91
96
  if (active.length === 0) {
92
- prevSpans = [];
97
+ prevSpans.length = 0;
93
98
  continue;
94
99
  }
95
100
 
96
101
  // x at slab top/bottom for each active edge; sort left-to-right just
97
102
  // below the top (edges leaving one point go by where they are headed)
98
- for (const e of active) {
103
+ for (let i = 0; i < active.length; ++i) {
104
+ const e = active[i];
99
105
  e.xt = e.x0 + (ya - e.y0) * e.slope;
100
106
  e.xb = e.x0 + (yb - e.y0) * e.slope;
101
107
  }
102
- active.sort((a, b) => a.xt - b.xt || a.xb - b.xb);
108
+ // a slab holds a handful of edges, mostly in last slab's order: an
109
+ // insertion sort, stable as Array#sort is, so the order is the same
110
+ for (let i = 1; i < active.length; ++i) {
111
+ const e = active[i];
112
+ let j = i - 1;
113
+ while (j >= 0 && (active[j].xt - e.xt || active[j].xb - e.xb) > 0) {
114
+ active[j + 1] = active[j];
115
+ --j;
116
+ }
117
+ active[j + 1] = e;
118
+ }
103
119
 
104
120
  // cut the slab at each crossing, top to bottom; `y` is where the part
105
121
  // still to emit starts, and `xt` each edge's x there
@@ -121,13 +137,18 @@ export function trapezoidize(polys, dx = 0, dy = 0, out = [], rule = 'nonzero')
121
137
  }
122
138
  }
123
139
  if (k < 0 || yc >= yb - eps) {
124
- prevSpans = emitSpans(active, y, yb, true, evenodd, prevSpans, out);
140
+ emitSpans(active, y, yb, true, evenodd, prevSpans, spans, out);
141
+ [prevSpans, spans] = [spans, prevSpans];
125
142
  break;
126
143
  }
127
144
  if (yc > y + eps) {
128
- for (const e of active) e.xc = e.x0 + (yc - e.y0) * e.slope;
129
- prevSpans = emitSpans(active, y, yc, false, evenodd, prevSpans, out);
130
- for (const e of active) e.xt = e.xc;
145
+ for (let i = 0; i < active.length; ++i) {
146
+ const e = active[i];
147
+ e.xc = e.x0 + (yc - e.y0) * e.slope;
148
+ }
149
+ emitSpans(active, y, yc, false, evenodd, prevSpans, spans, out);
150
+ [prevSpans, spans] = [spans, prevSpans];
151
+ for (let i = 0; i < active.length; ++i) active[i].xt = active[i].xc;
131
152
  y = yc;
132
153
  }
133
154
  const a = active[k];
@@ -144,15 +165,16 @@ export function trapezoidize(polys, dx = 0, dy = 0, out = [], rule = 'nonzero')
144
165
  * parity for even-odd). Each edge is at `xt` at the top, and at the bottom
145
166
  * at `xb` when this is the `last` part of its vertex slab, else at `xc`. A
146
167
  * span bounded by the same two edges as one ending at `ya` extends that
147
- * trapezoid instead.
148
- * @returns the spans, for the next slab to merge with
168
+ * trapezoid instead. The spans go into `spans`, emptied first, for the next
169
+ * slab to merge with.
149
170
  */
150
- function emitSpans(active, ya, yb, last, evenodd, prevSpans, out) {
151
- const spans = [];
171
+ function emitSpans(active, ya, yb, last, evenodd, prevSpans, spans, out) {
172
+ spans.length = 0;
152
173
  let winding = 0;
153
174
  let crossings = 0;
154
175
  let left = null;
155
- for (const e of active) {
176
+ for (let i = 0; i < active.length; ++i) {
177
+ const e = active[i];
156
178
  const wasInside = evenodd ? (crossings & 1) === 1 : winding !== 0;
157
179
  winding += e.dir;
158
180
  crossings++;
@@ -164,24 +186,24 @@ function emitSpans(active, ya, yb, last, evenodd, prevSpans, out) {
164
186
  const rb = last ? e.xb : e.xc;
165
187
  // merge with a span from the previous slab bounded by the same edges
166
188
  let merged = false;
167
- for (const p of prevSpans) {
168
- if (p.l === left && p.r === e && out[p.at + 5] === ya) {
169
- out[p.at + 3] = lb;
170
- out[p.at + 4] = rb;
171
- out[p.at + 5] = yb;
172
- spans.push(p);
189
+ for (let p = 0; p < prevSpans.length; p += 3) {
190
+ const at = prevSpans[p + 2];
191
+ if (prevSpans[p] === left && prevSpans[p + 1] === e && out[at + 5] === ya) {
192
+ out[at + 3] = lb;
193
+ out[at + 4] = rb;
194
+ out[at + 5] = yb;
195
+ spans.push(left, e, at);
173
196
  merged = true;
174
197
  break;
175
198
  }
176
199
  }
177
200
  if (!merged && (e.xt > left.xt || rb > lb)) {
178
- spans.push({ l: left, r: e, at: out.length });
201
+ spans.push(left, e, out.length);
179
202
  out.push(left.xt, e.xt, ya, lb, rb, yb);
180
203
  }
181
204
  left = null;
182
205
  }
183
206
  }
184
- return spans;
185
207
  }
186
208
 
187
209
  /** area covered by a trapezoid list (useful for tests/diagnostics) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.18.0",
3
+ "version": "8.18.1",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",