ntk 8.17.3 → 8.17.5

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.
@@ -387,15 +387,63 @@ function patternSourceOf(app, source) {
387
387
  * after a `translate` moves with the fill, and one created under a translate
388
388
  * and filled without it does not).
389
389
  *
390
- * Returns false when nothing would be painted (a singular matrix), true for
391
- * anything else, including every plain-colour style.
390
+ * Returns false when nothing would be painted (a singular matrix, or one
391
+ * the wire cannot carry), true for anything else, including every
392
+ * plain-colour style. `width` and `height` are the surface's: the style's
393
+ * source origin is kept on it (`sourceOrigin`).
392
394
  */
393
- function prepareStyle(src, m) {
395
+ function prepareStyle(src, m, width, height) {
394
396
  return src instanceof CanvasPattern || src instanceof CanvasGradient
395
- ? src._sync(m)
397
+ ? src._sync(m, width, height)
396
398
  : true;
397
399
  }
398
400
 
401
+ const NO_ORIGIN = Object.freeze([0, 0]);
402
+
403
+ /**
404
+ * Where a composite samples `src` from, as a device point: each composite
405
+ * that reads a style at its destination's own coordinates passes those
406
+ * coordinates less this as its source offset.
407
+ *
408
+ * A gradient's or a pattern's picture transform is the inverse of the CTM,
409
+ * and sampling it at device coordinates put the CTM's translation, times
410
+ * its downscale, into the transform: a box a fortieth of its size at x
411
+ * 2,200 asked for 88,000, past what 16.16 fixed point carries. `_sync`
412
+ * folds this origin into the transform instead, and puts it where the
413
+ * style's own origin lands, so the translation is small wherever on the
414
+ * surface the style is painted. Everything else — a solid, a picture —
415
+ * samples at device coordinates, from (0, 0).
416
+ */
417
+ function sourceOrigin(src) {
418
+ return src instanceof CanvasPattern || src instanceof CanvasGradient
419
+ ? src._origin
420
+ : NO_ORIGIN;
421
+ }
422
+
423
+ /**
424
+ * The origin `_sync` picks for a style whose own origin lands at device
425
+ * (x, y): the nearest whole pixel, kept on the surface. On it, a composite's
426
+ * source offset — its destination's, less this — is within the surface's
427
+ * size of 0, which the 16-bit field carries whatever the transform; and the
428
+ * transform's translation is the style's coordinate there, which for a
429
+ * style painted on the surface is one the fill samples anyway.
430
+ */
431
+ function originOn(x, y, width, height) {
432
+ const on = (v, size) => (v > 0 ? Math.min(Math.round(v), size) : 0);
433
+ return [on(x, width), on(y, height)];
434
+ }
435
+
436
+ /**
437
+ * Whether every entry can be written as XRender's 16.16 fixed point, whose
438
+ * whole part is a signed 16-bit number. A picture transform is written that
439
+ * way, and the request encoder throws on an entry past it — out of the paint
440
+ * that asked for it, and out of everything drawn after it in the same frame.
441
+ * Not finite fails too.
442
+ */
443
+ function fitsFixed(values) {
444
+ return values.every((v) => Math.abs(v) < 32768);
445
+ }
446
+
399
447
  function isPictureSource(image) {
400
448
  return (
401
449
  image instanceof Image ||
@@ -2376,14 +2424,15 @@ class RenderingContext2d {
2376
2424
  );
2377
2425
  }
2378
2426
  // src is either a 1x1 repeating solid (offset irrelevant) or a
2379
- // surface-aligned gradient, so it is sampled at the same offset too
2427
+ // gradient/pattern, sampled at the same offset from its own origin
2428
+ const [ox, oy] = sourceOrigin(src);
2380
2429
  R.Composite(
2381
2430
  op,
2382
2431
  src.id,
2383
2432
  this.fillMask.id,
2384
2433
  this._dst(),
2385
- out.x,
2386
- out.y,
2434
+ out.x - ox,
2435
+ out.y - oy,
2387
2436
  out.x,
2388
2437
  out.y,
2389
2438
  out.x,
@@ -2406,7 +2455,7 @@ class RenderingContext2d {
2406
2455
  op = op ?? this._op();
2407
2456
  alpha = alpha ?? this.globalAlpha;
2408
2457
  if (alpha <= 0) return;
2409
- if (!prepareStyle(src, this._m)) return;
2458
+ if (!prepareStyle(src, this._m, this.width, this.height)) return;
2410
2459
 
2411
2460
  // one box per subpath, so a path holding disjoint ones can be masked as
2412
2461
  // the pieces it is rather than as the box around all of them — each the
@@ -2462,7 +2511,7 @@ class RenderingContext2d {
2462
2511
  _strokePolys(polys, { src = null } = {}) {
2463
2512
  src = src ?? this._strokePicture;
2464
2513
  if (this.globalAlpha <= 0) return;
2465
- if (!prepareStyle(src, this._m)) return;
2514
+ if (!prepareStyle(src, this._m, this.width, this.height)) return;
2466
2515
  // approximate transform-aware line width by the average scale factor
2467
2516
  const det = this._m[0] * this._m[3] - this._m[1] * this._m[2];
2468
2517
  const scale = Math.sqrt(Math.abs(det)) || 1;
@@ -2830,6 +2879,7 @@ class RenderingContext2d {
2830
2879
  op === this.Render.PictOp.Over;
2831
2880
  const chunk = 4000 * 6;
2832
2881
  if (direct) {
2882
+ const [ox, oy] = sourceOrigin(src);
2833
2883
  this._cullTris(tris);
2834
2884
  if (!tris.length) return;
2835
2885
  const cut = this._wireCut(tris);
@@ -2847,11 +2897,12 @@ class RenderingContext2d {
2847
2897
  // convention drawGlyphRuns and compositeTraps use — without it every
2848
2898
  // non-constant stroke style (a gradient, a pattern) is offset by
2849
2899
  // wherever the stroke happens to start, and shifts as it moves.
2900
+ // Less the style's own origin, as every fill samples it.
2850
2901
  this.Render.Triangles(
2851
2902
  op,
2852
2903
  src.id,
2853
- Math.floor(batch[0]),
2854
- Math.floor(batch[1]),
2904
+ Math.floor(batch[0]) - ox,
2905
+ Math.floor(batch[1]) - oy,
2855
2906
  this._dst(),
2856
2907
  this.Render.a8,
2857
2908
  batch,
@@ -3117,7 +3168,7 @@ class RenderingContext2d {
3117
3168
  _drawGlyphsDevice(op, src, positioned) {
3118
3169
  const app = this.window.app;
3119
3170
  const R = this.Render;
3120
- if (!prepareStyle(src, this._m)) return;
3171
+ if (!prepareStyle(src, this._m, this.width, this.height)) return;
3121
3172
  if (!this._hasPolyClip) {
3122
3173
  // Fast path: a rectangular clip is something the server can do itself.
3123
3174
  // Two small requests around the ordinary glyph composite, instead of
@@ -3137,6 +3188,7 @@ class RenderingContext2d {
3137
3188
  this._dst(),
3138
3189
  positioned,
3139
3190
  rect ?? { x: 0, y: 0, w: this.width, h: this.height },
3191
+ sourceOrigin(src),
3140
3192
  );
3141
3193
  if (rect) this._invalidatePictureClip();
3142
3194
  this._markDirty();
@@ -3174,13 +3226,14 @@ class RenderingContext2d {
3174
3226
  this.width,
3175
3227
  this.height,
3176
3228
  );
3229
+ const [ox, oy] = sourceOrigin(src);
3177
3230
  R.Composite(
3178
3231
  op,
3179
3232
  src.id,
3180
3233
  this.fillMask.id,
3181
3234
  this._dst(),
3182
- 0,
3183
- 0,
3235
+ -ox,
3236
+ -oy,
3184
3237
  0,
3185
3238
  0,
3186
3239
  0,
@@ -3207,7 +3260,16 @@ class RenderingContext2d {
3207
3260
  if (rect.w === 0 || rect.h === 0) return;
3208
3261
  this._setPictureClip(rect);
3209
3262
  }
3210
- compositeTraps(app, op, src.id, this._dst(), traps);
3263
+ compositeTraps(
3264
+ app,
3265
+ op,
3266
+ src.id,
3267
+ this._dst(),
3268
+ traps,
3269
+ undefined,
3270
+ undefined,
3271
+ sourceOrigin(src),
3272
+ );
3211
3273
  if (rect) this._invalidatePictureClip();
3212
3274
  this._markDirty();
3213
3275
  return;
@@ -3242,13 +3304,14 @@ class RenderingContext2d {
3242
3304
  this.width,
3243
3305
  this.height,
3244
3306
  );
3307
+ const [ox, oy] = sourceOrigin(src);
3245
3308
  R.Composite(
3246
3309
  op,
3247
3310
  src.id,
3248
3311
  this.fillMask.id,
3249
3312
  this._dst(),
3250
- 0,
3251
- 0,
3313
+ -ox,
3314
+ -oy,
3252
3315
  0,
3253
3316
  0,
3254
3317
  0,
@@ -3486,7 +3549,8 @@ class RenderingContext2d {
3486
3549
  w = x1 - x0;
3487
3550
  h = y1 - y0;
3488
3551
  }
3489
- if (!prepareStyle(this._backgroundPicture, this._m)) return;
3552
+ const style = this._backgroundPicture;
3553
+ if (!prepareStyle(style, this._m, this.width, this.height)) return;
3490
3554
  const op = this._op();
3491
3555
  // A rectangular clip is a smaller rectangle to fill, not a mask — this
3492
3556
  // was the one drawing path that still built a surface-sized a8 for one
@@ -3498,13 +3562,14 @@ class RenderingContext2d {
3498
3562
  w > 0 && h > 0 ? this._boxedComposite({ x, y, w, h }, op) : null;
3499
3563
  if (direct && !direct.box) return; // the clip rejects the fill whole
3500
3564
  const box = direct ? direct.box : { x, y, w, h };
3565
+ const [ox, oy] = sourceOrigin(style);
3501
3566
  this.Render.Composite(
3502
3567
  op,
3503
- this._backgroundPicture.id,
3568
+ style.id,
3504
3569
  direct ? direct.mask : this._compositeMask(box),
3505
3570
  this._dst(),
3506
- box.x,
3507
- box.y,
3571
+ box.x - ox,
3572
+ box.y - oy,
3508
3573
  box.x,
3509
3574
  box.y,
3510
3575
  box.x,
@@ -3656,7 +3721,8 @@ class RenderingContext2d {
3656
3721
  if (y + h > y1) y1 = y + h;
3657
3722
  }
3658
3723
  if (!disjointRects(rects)) return false;
3659
- if (!prepareStyle(this._backgroundPicture, this._m)) return true;
3724
+ const style = this._backgroundPicture;
3725
+ if (!prepareStyle(style, this._m, this.width, this.height)) return true;
3660
3726
  // the box they cover, on the surface: nothing outside it is sampled
3661
3727
  const bx = Math.max(0, x0);
3662
3728
  const by = Math.max(0, y0);
@@ -3690,13 +3756,14 @@ class RenderingContext2d {
3690
3756
  bw,
3691
3757
  bh,
3692
3758
  );
3759
+ const [ox, oy] = sourceOrigin(style);
3693
3760
  R.Composite(
3694
3761
  op,
3695
- this._backgroundPicture.id,
3762
+ style.id,
3696
3763
  mask,
3697
3764
  this._dst(),
3698
- bx,
3699
- by,
3765
+ bx - ox,
3766
+ by - oy,
3700
3767
  bx,
3701
3768
  by,
3702
3769
  bx,
@@ -5145,7 +5212,8 @@ class RenderingContext2d {
5145
5212
  const R = this.Render;
5146
5213
  // the coverage is painted in the current fillStyle, on both branches
5147
5214
  // below — a gradient/pattern whose transform collapsed paints nothing
5148
- if (!prepareStyle(this._backgroundPicture, this._m)) return;
5215
+ const style = this._backgroundPicture;
5216
+ if (!prepareStyle(style, this._m, this.width, this.height)) return;
5149
5217
  const scaled = dw !== sw || dh !== sh;
5150
5218
  if (scaled) {
5151
5219
  R.SetPictureTransform(picture.id, [
@@ -5235,18 +5303,19 @@ class RenderingContext2d {
5235
5303
  this._setPictureClip(rect);
5236
5304
  }
5237
5305
  // The scratch mask is surface-sized and surface-aligned — the coverage
5238
- // went into it at (dx, dy) — and so is a gradient source, so both are
5239
- // sampled at the destination offset. Reading either from (0, 0) draws a
5240
- // dw x dh window out of the wrong part of a full-surface picture: for
5241
- // the mask that is the region just cleared to zero, so nothing paints at
5242
- // all unless dx and dy are both zero.
5306
+ // went into it at (dx, dy) — so it is sampled at the destination
5307
+ // offset, and a gradient or pattern at that offset from its origin.
5308
+ // Reading the mask from (0, 0) draws a dw x dh window out of the wrong
5309
+ // part of a full-surface picture: the region just cleared to zero, so
5310
+ // nothing paints at all unless dx and dy are both zero.
5311
+ const [ox, oy] = sourceOrigin(style);
5243
5312
  R.Composite(
5244
5313
  op,
5245
- this._backgroundPicture.id,
5314
+ style.id,
5246
5315
  this.fillMask.id,
5247
5316
  this._dst(),
5248
- dx,
5249
- dy,
5317
+ dx - ox,
5318
+ dy - oy,
5250
5319
  dx,
5251
5320
  dy,
5252
5321
  dx,
@@ -5258,15 +5327,17 @@ class RenderingContext2d {
5258
5327
  } else if (!rect || (rect.w > 0 && rect.h > 0)) {
5259
5328
  if (rect) this._setPictureClip(rect);
5260
5329
  // the source is a 1x1 repeating solid (offset irrelevant) or the fill
5261
- // picture, which for a gradient is surface-aligned: same offset as the
5262
- // destination, exactly as in _fillPolys
5330
+ // style, sampled at the destination's offset from its own origin,
5331
+ // exactly as in _fillPolys
5332
+ const src = this._coverageSource();
5333
+ const [ox, oy] = sourceOrigin(src);
5263
5334
  R.Composite(
5264
5335
  op,
5265
- this._coverageSource().id,
5336
+ src.id,
5266
5337
  picture.id,
5267
5338
  this._dst(),
5268
- dx,
5269
- dy,
5339
+ dx - ox,
5340
+ dy - oy,
5270
5341
  mx,
5271
5342
  my,
5272
5343
  dx,
@@ -5503,6 +5574,11 @@ class RenderingContext2d {
5503
5574
  const M = matMultiply(this._m, [dw / sw, 0, 0, dh / sh, dx, dy]);
5504
5575
  const inv = matInvert(M);
5505
5576
  if (!inv) return;
5577
+ // The inverse's linear part is how many source pixels one device pixel
5578
+ // steps across, and it goes on the wire as 16.16 fixed point. Past
5579
+ // 32,767 the image is drawn at under 1/32,768 of its size: less than a
5580
+ // pixel across for any image X can hold, and nothing to see.
5581
+ if (!fitsFixed(inv.slice(0, 4))) return;
5506
5582
 
5507
5583
  // device bbox of the transformed dest rect, clamped to the canvas
5508
5584
  const corners = [
@@ -5523,6 +5599,24 @@ class RenderingContext2d {
5523
5599
  );
5524
5600
  if (x1 <= x0 || y1 <= y0) return;
5525
5601
 
5602
+ // Device -> source sample, measured from the bounding box's corner: the
5603
+ // corner's device position is folded into the transform, and the
5604
+ // composite's source origin is its box's offset from that corner. With
5605
+ // the source origin at device coordinates instead, the translation was
5606
+ // the device position times the downscale — a 50x30 thumbnail of a
5607
+ // 2000x1200 image at x 2,200 is 88,000, past what 16.16 carries, and
5608
+ // the encoder threw. From the corner it is the source coordinate the
5609
+ // corner samples: the image's own size or so, wherever on the surface it
5610
+ // is drawn. The corner is the unclipped one, so a clip that narrows the
5611
+ // box below samples the same points the mask route does.
5612
+ const [a, b, c, d] = inv;
5613
+ const e = a * x0 + c * y0 + inv[4] + sx;
5614
+ const f = b * x0 + d * y0 + inv[5] + sy;
5615
+ // A corner out of reach even so is a draw near the limit above, or an
5616
+ // image over 20,000 pixels across under a rotation; it is not drawn,
5617
+ // rather than thrown out of the paint.
5618
+ if (!fitsFixed([e, f])) return;
5619
+
5526
5620
  // A rectangular clip shrinks the box instead of becoming a mask (issue
5527
5621
  // #307). Decided before the picture transform is installed, so a draw
5528
5622
  // the clip rejects does not have to put it back.
@@ -5531,27 +5625,15 @@ class RenderingContext2d {
5531
5625
  if (direct && !direct.box) return;
5532
5626
  const box = direct ? direct.box : bbox;
5533
5627
 
5534
- // device -> source sample (adding the source-rect origin)
5535
- const srcM = [inv[0], inv[1], inv[2], inv[3], inv[4] + sx, inv[5] + sy];
5536
- this.Render.SetPictureTransform(picture.id, [
5537
- srcM[0],
5538
- srcM[2],
5539
- srcM[4],
5540
- srcM[1],
5541
- srcM[3],
5542
- srcM[5],
5543
- 0,
5544
- 0,
5545
- 1,
5546
- ]);
5628
+ this.Render.SetPictureTransform(picture.id, [a, c, e, b, d, f, 0, 0, 1]);
5547
5629
  picture.setFilter("bilinear");
5548
5630
  this.Render.Composite(
5549
5631
  op,
5550
5632
  picture.id,
5551
5633
  direct ? direct.mask : this._compositeMask(box),
5552
5634
  this._dst(),
5553
- box.x,
5554
- box.y,
5635
+ box.x - x0,
5636
+ box.y - y0,
5555
5637
  box.x,
5556
5638
  box.y,
5557
5639
  box.x,
@@ -5596,6 +5678,9 @@ class CanvasGradient {
5596
5678
  // what the server currently holds: a fresh gradient picture is
5597
5679
  // untransformed, so an untransformed fill costs no extra request
5598
5680
  this._applied = [1, 0, 0, 1, 0, 0];
5681
+ // the device point fills sample it from (`sourceOrigin`), set with the
5682
+ // transform it belongs with
5683
+ this._origin = NO_ORIGIN;
5599
5684
 
5600
5685
  this.x0 = p0;
5601
5686
  this.y0 = p1;
@@ -5685,13 +5770,14 @@ class CanvasGradient {
5685
5770
  /**
5686
5771
  * Make the server-side mapping match the CTM this paint runs under. The
5687
5772
  * gradient's own coordinates are user space and every fill samples the
5688
- * source at device coordinates, so the picture transform — which takes a
5689
- * source coordinate to a gradient one — is the CTM's inverse.
5773
+ * source at device coordinates less `_origin`, so the picture transform —
5774
+ * which takes a source coordinate to a gradient one — is the CTM's
5775
+ * inverse, from that origin.
5690
5776
  *
5691
5777
  * Returns false when the CTM collapses (a zero scale), which paints
5692
5778
  * nothing, exactly as the canvas spec says.
5693
5779
  */
5694
- _sync(ctm) {
5780
+ _sync(ctm, width = 0, height = 0) {
5695
5781
  let inv = matInvert(ctm);
5696
5782
  if (!inv) return false;
5697
5783
  const id = this.id; // lazily creates the picture
@@ -5699,9 +5785,13 @@ class CanvasGradient {
5699
5785
  // much nearer its origin
5700
5786
  const k = this._scale;
5701
5787
  if (k !== 1) inv = inv.map((v) => v / k);
5788
+ // sampled from where user space's origin lands (`sourceOrigin`)
5789
+ const origin = originOn(ctm[4], ctm[5], width, height);
5790
+ inv = matMultiply(inv, [1, 0, 0, 1, origin[0], origin[1]]);
5702
5791
  // the transform is 16.16 fixed point too: one that cannot be carried
5703
5792
  // fills nothing, rather than throwing the rest of the paint away
5704
- if (!inv.every((v) => Math.abs(v) < 32767)) return false;
5793
+ if (!fitsFixed(inv)) return false;
5794
+ this._origin = origin;
5705
5795
  const a = this._applied;
5706
5796
  if (
5707
5797
  inv[0] !== a[0] ||
@@ -5779,6 +5869,7 @@ class CanvasPattern {
5779
5869
  // what the server currently holds: a fresh picture is untransformed and
5780
5870
  // filtered nearest, so an untransformed fill costs no extra request
5781
5871
  this._applied = [1, 0, 0, 1, 0, 0];
5872
+ this._origin = NO_ORIGIN;
5782
5873
  this._filter = "nearest";
5783
5874
  }
5784
5875
 
@@ -5822,15 +5913,36 @@ class CanvasPattern {
5822
5913
  * Make the server-side mapping match `ctm ∘ patternMatrix`. XRender's
5823
5914
  * picture transform runs the other way — it takes a coordinate in the
5824
5915
  * composite's source space (which every fill here keeps equal to device
5825
- * space) to a texel — so it is the inverse.
5916
+ * space less `_origin`) to a texel — so it is the inverse, from that
5917
+ * origin.
5826
5918
  *
5827
5919
  * Returns false when that composition collapses (a zero scale), which
5828
5920
  * paints nothing, exactly as the canvas spec says.
5829
5921
  */
5830
- _sync(ctm) {
5922
+ _sync(ctm, width = 0, height = 0) {
5831
5923
  const m = matMultiply(ctm, this._m);
5832
- const inv = matInvert(m);
5924
+ let inv = matInvert(m);
5833
5925
  if (!inv) return false;
5926
+ // sampled from where the tile's origin lands (`sourceOrigin`)
5927
+ const origin = originOn(m[4], m[5], width, height);
5928
+ inv = matMultiply(inv, [1, 0, 0, 1, origin[0], origin[1]]);
5929
+ // A tile that repeats is the same a whole number of tiles along, so the
5930
+ // translation — the texel the origin samples — is taken to the first
5931
+ // tile. A grid scrolled 100,000 pixels has its origin far off the
5932
+ // surface and samples texels 100,000 along; from the first tile they
5933
+ // are in reach of the wire.
5934
+ const period =
5935
+ this._repeat === 1 ? 1 : this._repeat === 3 ? 2 : 0; // Normal, Reflect
5936
+ if (period && this.width > 0 && this.height > 0) {
5937
+ const pw = this.width * period;
5938
+ const ph = this.height * period;
5939
+ inv[4] -= pw * Math.floor(inv[4] / pw);
5940
+ inv[5] -= ph * Math.floor(inv[5] / ph);
5941
+ }
5942
+ // 16.16 fixed point, as a gradient's is: one that cannot be carried
5943
+ // fills nothing rather than throwing the rest of the paint away
5944
+ if (!fitsFixed(inv)) return false;
5945
+ this._origin = origin;
5834
5946
  const a = this._applied;
5835
5947
  if (
5836
5948
  inv[0] !== a[0] ||
@@ -583,8 +583,12 @@ export function routeGlyphSize(app, font, size, policy = policyOf(app), textRend
583
583
  * @param {{x: number, y: number, w: number, h: number}} [bounds] the part of
584
584
  * the destination that can show anything — the clip, or the surface. A
585
585
  * glyph wholly outside it is not sent (`visibleGlyph`).
586
+ * @param {[number, number]} [srcOrigin] the destination point the source's
587
+ * (0, 0) is aligned with. A gradient or pattern the 2d context has
588
+ * transformed samples from its own origin; anything else is aligned with
589
+ * the destination, from (0, 0).
586
590
  */
587
- export function drawGlyphRuns(app, op, srcId, dstId, positioned, bounds = null) {
591
+ export function drawGlyphRuns(app, op, srcId, dstId, positioned, bounds = null, srcOrigin = [0, 0]) {
588
592
  const policy = policyOf(app);
589
593
  let bitmap = positioned;
590
594
  let vector = null;
@@ -602,8 +606,8 @@ export function drawGlyphRuns(app, op, srcId, dstId, positioned, bounds = null)
602
606
  bitmap.push(positioned[i]);
603
607
  }
604
608
  }
605
- if (bitmap.length) drawBitmapGlyphRuns(app, op, srcId, dstId, bitmap, policy, bounds);
606
- if (vector) drawVectorGlyphRuns(app, op, srcId, dstId, vector, bounds);
609
+ if (bitmap.length) drawBitmapGlyphRuns(app, op, srcId, dstId, bitmap, policy, bounds, srcOrigin);
610
+ if (vector) drawVectorGlyphRuns(app, op, srcId, dstId, vector, bounds, srcOrigin);
607
611
  }
608
612
 
609
613
  /**
@@ -630,7 +634,7 @@ function visibleGlyph(bounds, x, y, size) {
630
634
  );
631
635
  }
632
636
 
633
- function drawBitmapGlyphRuns(app, op, srcId, dstId, positioned, policy, bounds) {
637
+ function drawBitmapGlyphRuns(app, op, srcId, dstId, positioned, policy, bounds, srcOrigin) {
634
638
  const Render = app.display.Render;
635
639
  const items = [];
636
640
  let bits = 8;
@@ -652,8 +656,9 @@ function drawBitmapGlyphRuns(app, op, srcId, dstId, positioned, policy, bounds)
652
656
  const encoded = encodeGlyphItems(items, bits);
653
657
  if (!encoded) return;
654
658
  // srcX/srcY align the source with the FIRST glyph's origin (RENDER spec);
655
- // passing that origin itself makes source coordinates equal destination
656
- // coordinates, so gradient fill styles line up with canvas space
659
+ // passing that origin itself, less the source's own, makes source
660
+ // coordinates destination coordinates from there, so gradient fill styles
661
+ // line up with canvas space
657
662
  Render.CompositeGlyphs(
658
663
  encoded.bits,
659
664
  op,
@@ -661,8 +666,8 @@ function drawBitmapGlyphRuns(app, op, srcId, dstId, positioned, policy, bounds)
661
666
  dstId,
662
667
  0,
663
668
  encoded.gsid,
664
- items[0].x,
665
- items[0].y,
669
+ items[0].x - srcOrigin[0],
670
+ items[0].y - srcOrigin[1],
666
671
  encoded.elts
667
672
  );
668
673
  trimGlyphPages(app, policy, new Set(pages.values()));
@@ -683,7 +688,7 @@ const MAX_TRAPS_PER_REQUEST = 6000;
683
688
  * Positions are intentionally NOT rounded to whole pixels — fractional
684
689
  * advances and origins keep zoom animations smooth.
685
690
  */
686
- function drawVectorGlyphRuns(app, op, srcId, dstId, positioned, bounds = null) {
691
+ function drawVectorGlyphRuns(app, op, srcId, dstId, positioned, bounds = null, srcOrigin = [0, 0]) {
687
692
  // unrounded glyph origins + ink bounding box
688
693
  const placed = [];
689
694
  let minX = Infinity;
@@ -742,7 +747,7 @@ function drawVectorGlyphRuns(app, op, srcId, dstId, positioned, bounds = null) {
742
747
  bx = x0;
743
748
  by = y0;
744
749
  }
745
- compositeTraps(app, op, srcId, dstId, traps, bx, by);
750
+ compositeTraps(app, op, srcId, dstId, traps, bx, by, srcOrigin);
746
751
  }
747
752
 
748
753
  /**
@@ -753,8 +758,10 @@ function drawVectorGlyphRuns(app, op, srcId, dstId, positioned, bounds = null) {
753
758
  *
754
759
  * When bx/by are omitted they are derived from the trapezoid bounds (the
755
760
  * trap coordinates are then treated as absolute device coordinates).
761
+ * `srcOrigin` is the destination point the source's (0, 0) is aligned with,
762
+ * as for `drawGlyphRuns`.
756
763
  */
757
- export function compositeTraps(app, op, srcId, dstId, traps, bx, by) {
764
+ export function compositeTraps(app, op, srcId, dstId, traps, bx, by, srcOrigin = [0, 0]) {
758
765
  if (traps.length === 0) return;
759
766
  const Render = app.display.Render;
760
767
 
@@ -800,8 +807,22 @@ export function compositeTraps(app, op, srcId, dstId, traps, bx, by) {
800
807
  for (let i = 0; i < traps.length; i += MAX_TRAPS_PER_REQUEST * 6) {
801
808
  Render.AddTraps(mask.picture.id, 0, 0, traps.slice(i, i + MAX_TRAPS_PER_REQUEST * 6));
802
809
  }
803
- // src coords = dst coords (see drawBitmapGlyphRuns) so gradients line up
804
- Render.Composite(op, srcId, mask.picture.id, dstId, bx, by, 0, 0, bx, by, bw, bh);
810
+ // src coords = dst coords from the source's origin (see
811
+ // drawBitmapGlyphRuns) so gradients line up
812
+ Render.Composite(
813
+ op,
814
+ srcId,
815
+ mask.picture.id,
816
+ dstId,
817
+ bx - srcOrigin[0],
818
+ by - srcOrigin[1],
819
+ 0,
820
+ 0,
821
+ bx,
822
+ by,
823
+ bw,
824
+ bh
825
+ );
805
826
  releaseScratchMask(app);
806
827
  }
807
828