ntk 8.17.4 → 8.17.6

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,52 @@ 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
+
399
436
  /**
400
437
  * Whether every entry can be written as XRender's 16.16 fixed point, whose
401
438
  * whole part is a signed 16-bit number. A picture transform is written that
@@ -2387,14 +2424,15 @@ class RenderingContext2d {
2387
2424
  );
2388
2425
  }
2389
2426
  // src is either a 1x1 repeating solid (offset irrelevant) or a
2390
- // 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);
2391
2429
  R.Composite(
2392
2430
  op,
2393
2431
  src.id,
2394
2432
  this.fillMask.id,
2395
2433
  this._dst(),
2396
- out.x,
2397
- out.y,
2434
+ out.x - ox,
2435
+ out.y - oy,
2398
2436
  out.x,
2399
2437
  out.y,
2400
2438
  out.x,
@@ -2417,7 +2455,7 @@ class RenderingContext2d {
2417
2455
  op = op ?? this._op();
2418
2456
  alpha = alpha ?? this.globalAlpha;
2419
2457
  if (alpha <= 0) return;
2420
- if (!prepareStyle(src, this._m)) return;
2458
+ if (!prepareStyle(src, this._m, this.width, this.height)) return;
2421
2459
 
2422
2460
  // one box per subpath, so a path holding disjoint ones can be masked as
2423
2461
  // the pieces it is rather than as the box around all of them — each the
@@ -2473,7 +2511,7 @@ class RenderingContext2d {
2473
2511
  _strokePolys(polys, { src = null } = {}) {
2474
2512
  src = src ?? this._strokePicture;
2475
2513
  if (this.globalAlpha <= 0) return;
2476
- if (!prepareStyle(src, this._m)) return;
2514
+ if (!prepareStyle(src, this._m, this.width, this.height)) return;
2477
2515
  // approximate transform-aware line width by the average scale factor
2478
2516
  const det = this._m[0] * this._m[3] - this._m[1] * this._m[2];
2479
2517
  const scale = Math.sqrt(Math.abs(det)) || 1;
@@ -2841,6 +2879,7 @@ class RenderingContext2d {
2841
2879
  op === this.Render.PictOp.Over;
2842
2880
  const chunk = 4000 * 6;
2843
2881
  if (direct) {
2882
+ const [ox, oy] = sourceOrigin(src);
2844
2883
  this._cullTris(tris);
2845
2884
  if (!tris.length) return;
2846
2885
  const cut = this._wireCut(tris);
@@ -2858,11 +2897,12 @@ class RenderingContext2d {
2858
2897
  // convention drawGlyphRuns and compositeTraps use — without it every
2859
2898
  // non-constant stroke style (a gradient, a pattern) is offset by
2860
2899
  // wherever the stroke happens to start, and shifts as it moves.
2900
+ // Less the style's own origin, as every fill samples it.
2861
2901
  this.Render.Triangles(
2862
2902
  op,
2863
2903
  src.id,
2864
- Math.floor(batch[0]),
2865
- Math.floor(batch[1]),
2904
+ Math.floor(batch[0]) - ox,
2905
+ Math.floor(batch[1]) - oy,
2866
2906
  this._dst(),
2867
2907
  this.Render.a8,
2868
2908
  batch,
@@ -3128,7 +3168,7 @@ class RenderingContext2d {
3128
3168
  _drawGlyphsDevice(op, src, positioned) {
3129
3169
  const app = this.window.app;
3130
3170
  const R = this.Render;
3131
- if (!prepareStyle(src, this._m)) return;
3171
+ if (!prepareStyle(src, this._m, this.width, this.height)) return;
3132
3172
  if (!this._hasPolyClip) {
3133
3173
  // Fast path: a rectangular clip is something the server can do itself.
3134
3174
  // Two small requests around the ordinary glyph composite, instead of
@@ -3148,6 +3188,7 @@ class RenderingContext2d {
3148
3188
  this._dst(),
3149
3189
  positioned,
3150
3190
  rect ?? { x: 0, y: 0, w: this.width, h: this.height },
3191
+ sourceOrigin(src),
3151
3192
  );
3152
3193
  if (rect) this._invalidatePictureClip();
3153
3194
  this._markDirty();
@@ -3185,13 +3226,14 @@ class RenderingContext2d {
3185
3226
  this.width,
3186
3227
  this.height,
3187
3228
  );
3229
+ const [ox, oy] = sourceOrigin(src);
3188
3230
  R.Composite(
3189
3231
  op,
3190
3232
  src.id,
3191
3233
  this.fillMask.id,
3192
3234
  this._dst(),
3193
- 0,
3194
- 0,
3235
+ -ox,
3236
+ -oy,
3195
3237
  0,
3196
3238
  0,
3197
3239
  0,
@@ -3218,7 +3260,16 @@ class RenderingContext2d {
3218
3260
  if (rect.w === 0 || rect.h === 0) return;
3219
3261
  this._setPictureClip(rect);
3220
3262
  }
3221
- 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
+ );
3222
3273
  if (rect) this._invalidatePictureClip();
3223
3274
  this._markDirty();
3224
3275
  return;
@@ -3253,13 +3304,14 @@ class RenderingContext2d {
3253
3304
  this.width,
3254
3305
  this.height,
3255
3306
  );
3307
+ const [ox, oy] = sourceOrigin(src);
3256
3308
  R.Composite(
3257
3309
  op,
3258
3310
  src.id,
3259
3311
  this.fillMask.id,
3260
3312
  this._dst(),
3261
- 0,
3262
- 0,
3313
+ -ox,
3314
+ -oy,
3263
3315
  0,
3264
3316
  0,
3265
3317
  0,
@@ -3497,7 +3549,8 @@ class RenderingContext2d {
3497
3549
  w = x1 - x0;
3498
3550
  h = y1 - y0;
3499
3551
  }
3500
- if (!prepareStyle(this._backgroundPicture, this._m)) return;
3552
+ const style = this._backgroundPicture;
3553
+ if (!prepareStyle(style, this._m, this.width, this.height)) return;
3501
3554
  const op = this._op();
3502
3555
  // A rectangular clip is a smaller rectangle to fill, not a mask — this
3503
3556
  // was the one drawing path that still built a surface-sized a8 for one
@@ -3509,13 +3562,14 @@ class RenderingContext2d {
3509
3562
  w > 0 && h > 0 ? this._boxedComposite({ x, y, w, h }, op) : null;
3510
3563
  if (direct && !direct.box) return; // the clip rejects the fill whole
3511
3564
  const box = direct ? direct.box : { x, y, w, h };
3565
+ const [ox, oy] = sourceOrigin(style);
3512
3566
  this.Render.Composite(
3513
3567
  op,
3514
- this._backgroundPicture.id,
3568
+ style.id,
3515
3569
  direct ? direct.mask : this._compositeMask(box),
3516
3570
  this._dst(),
3517
- box.x,
3518
- box.y,
3571
+ box.x - ox,
3572
+ box.y - oy,
3519
3573
  box.x,
3520
3574
  box.y,
3521
3575
  box.x,
@@ -3667,7 +3721,8 @@ class RenderingContext2d {
3667
3721
  if (y + h > y1) y1 = y + h;
3668
3722
  }
3669
3723
  if (!disjointRects(rects)) return false;
3670
- 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;
3671
3726
  // the box they cover, on the surface: nothing outside it is sampled
3672
3727
  const bx = Math.max(0, x0);
3673
3728
  const by = Math.max(0, y0);
@@ -3701,13 +3756,14 @@ class RenderingContext2d {
3701
3756
  bw,
3702
3757
  bh,
3703
3758
  );
3759
+ const [ox, oy] = sourceOrigin(style);
3704
3760
  R.Composite(
3705
3761
  op,
3706
- this._backgroundPicture.id,
3762
+ style.id,
3707
3763
  mask,
3708
3764
  this._dst(),
3709
- bx,
3710
- by,
3765
+ bx - ox,
3766
+ by - oy,
3711
3767
  bx,
3712
3768
  by,
3713
3769
  bx,
@@ -5156,7 +5212,8 @@ class RenderingContext2d {
5156
5212
  const R = this.Render;
5157
5213
  // the coverage is painted in the current fillStyle, on both branches
5158
5214
  // below — a gradient/pattern whose transform collapsed paints nothing
5159
- if (!prepareStyle(this._backgroundPicture, this._m)) return;
5215
+ const style = this._backgroundPicture;
5216
+ if (!prepareStyle(style, this._m, this.width, this.height)) return;
5160
5217
  const scaled = dw !== sw || dh !== sh;
5161
5218
  if (scaled) {
5162
5219
  R.SetPictureTransform(picture.id, [
@@ -5246,18 +5303,19 @@ class RenderingContext2d {
5246
5303
  this._setPictureClip(rect);
5247
5304
  }
5248
5305
  // The scratch mask is surface-sized and surface-aligned — the coverage
5249
- // went into it at (dx, dy) — and so is a gradient source, so both are
5250
- // sampled at the destination offset. Reading either from (0, 0) draws a
5251
- // dw x dh window out of the wrong part of a full-surface picture: for
5252
- // the mask that is the region just cleared to zero, so nothing paints at
5253
- // 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);
5254
5312
  R.Composite(
5255
5313
  op,
5256
- this._backgroundPicture.id,
5314
+ style.id,
5257
5315
  this.fillMask.id,
5258
5316
  this._dst(),
5259
- dx,
5260
- dy,
5317
+ dx - ox,
5318
+ dy - oy,
5261
5319
  dx,
5262
5320
  dy,
5263
5321
  dx,
@@ -5269,15 +5327,17 @@ class RenderingContext2d {
5269
5327
  } else if (!rect || (rect.w > 0 && rect.h > 0)) {
5270
5328
  if (rect) this._setPictureClip(rect);
5271
5329
  // the source is a 1x1 repeating solid (offset irrelevant) or the fill
5272
- // picture, which for a gradient is surface-aligned: same offset as the
5273
- // 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);
5274
5334
  R.Composite(
5275
5335
  op,
5276
- this._coverageSource().id,
5336
+ src.id,
5277
5337
  picture.id,
5278
5338
  this._dst(),
5279
- dx,
5280
- dy,
5339
+ dx - ox,
5340
+ dy - oy,
5281
5341
  mx,
5282
5342
  my,
5283
5343
  dx,
@@ -5618,6 +5678,9 @@ class CanvasGradient {
5618
5678
  // what the server currently holds: a fresh gradient picture is
5619
5679
  // untransformed, so an untransformed fill costs no extra request
5620
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;
5621
5684
 
5622
5685
  this.x0 = p0;
5623
5686
  this.y0 = p1;
@@ -5707,13 +5770,14 @@ class CanvasGradient {
5707
5770
  /**
5708
5771
  * Make the server-side mapping match the CTM this paint runs under. The
5709
5772
  * gradient's own coordinates are user space and every fill samples the
5710
- * source at device coordinates, so the picture transform — which takes a
5711
- * 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.
5712
5776
  *
5713
5777
  * Returns false when the CTM collapses (a zero scale), which paints
5714
5778
  * nothing, exactly as the canvas spec says.
5715
5779
  */
5716
- _sync(ctm) {
5780
+ _sync(ctm, width = 0, height = 0) {
5717
5781
  let inv = matInvert(ctm);
5718
5782
  if (!inv) return false;
5719
5783
  const id = this.id; // lazily creates the picture
@@ -5721,9 +5785,13 @@ class CanvasGradient {
5721
5785
  // much nearer its origin
5722
5786
  const k = this._scale;
5723
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]]);
5724
5791
  // the transform is 16.16 fixed point too: one that cannot be carried
5725
5792
  // fills nothing, rather than throwing the rest of the paint away
5726
5793
  if (!fitsFixed(inv)) return false;
5794
+ this._origin = origin;
5727
5795
  const a = this._applied;
5728
5796
  if (
5729
5797
  inv[0] !== a[0] ||
@@ -5801,6 +5869,7 @@ class CanvasPattern {
5801
5869
  // what the server currently holds: a fresh picture is untransformed and
5802
5870
  // filtered nearest, so an untransformed fill costs no extra request
5803
5871
  this._applied = [1, 0, 0, 1, 0, 0];
5872
+ this._origin = NO_ORIGIN;
5804
5873
  this._filter = "nearest";
5805
5874
  }
5806
5875
 
@@ -5844,20 +5913,36 @@ class CanvasPattern {
5844
5913
  * Make the server-side mapping match `ctm ∘ patternMatrix`. XRender's
5845
5914
  * picture transform runs the other way — it takes a coordinate in the
5846
5915
  * composite's source space (which every fill here keeps equal to device
5847
- * 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.
5848
5918
  *
5849
5919
  * Returns false when that composition collapses (a zero scale), which
5850
5920
  * paints nothing, exactly as the canvas spec says.
5851
5921
  */
5852
- _sync(ctm) {
5922
+ _sync(ctm, width = 0, height = 0) {
5853
5923
  const m = matMultiply(ctm, this._m);
5854
- const inv = matInvert(m);
5924
+ let inv = matInvert(m);
5855
5925
  if (!inv) return false;
5856
- // 16.16 fixed point, as a gradient's is: a tile scaled down far from
5857
- // the origin puts its device position times the downscale into the
5858
- // translation, and one that cannot be carried fills nothing rather than
5859
- // throwing the rest of the paint away
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
5860
5944
  if (!fitsFixed(inv)) return false;
5945
+ this._origin = origin;
5861
5946
  const a = this._applied;
5862
5947
  if (
5863
5948
  inv[0] !== a[0] ||
@@ -54,7 +54,8 @@ import {
54
54
  noFontsError,
55
55
  prewarmFaces,
56
56
  prewarmPatterns,
57
- supported
57
+ supported,
58
+ weightRank
58
59
  } from '../fontconfig.js';
59
60
  import Font from './font.js';
60
61
 
@@ -178,14 +179,16 @@ export class FontconfigFontSource {
178
179
  * source.alias('sans-serif', 'DejaVu Sans');
179
180
  * const app = await createClient({ fontSource: source });
180
181
  *
181
- * Matching mirrors fontconfig semantics: every added face is a fallback
182
+ * Matching mirrors the system path: every added face is a fallback
182
183
  * candidate; faces matching a requested family (in list order) rank first,
183
- * then closest weight, then style. Coverage checks use the real font
184
- * tables, so per-codepoint fallback works exactly like the system path.
184
+ * then within a family the style, then the weight CSS's font matching picks
185
+ * (CSS Fonts 4 §5.2) — as the fontconfig source does, so a family answers
186
+ * alike from either. Coverage checks use the real font tables, so
187
+ * per-codepoint fallback works exactly like the system path.
185
188
  */
186
189
  export class StaticFontSource {
187
190
  constructor() {
188
- this._faces = []; // { font, family, weight, italic, candidate }
191
+ this._faces = []; // { font, family, weight: [lo, hi], italic, candidate }
189
192
  this._aliases = new Map(); // 'sans-serif' -> 'dejavu sans'
190
193
  this._n = 0;
191
194
  /** files a font spec could not parse: [{ file, error }] */
@@ -196,7 +199,9 @@ export class StaticFontSource {
196
199
  * Register a font.
197
200
  * @param {Uint8Array|Buffer|Font} data font file bytes (or an open Font)
198
201
  * @param {object} [opts] { family (alias for matching; defaults to the
199
- * font's family name), weight, style, postscriptName (face of a .ttc) }
202
+ * font's family name), weight, style, postscriptName (face of a .ttc) }.
203
+ * A `weight` is the face's, as @font-face's descriptor is; without one a
204
+ * variable face holds every weight on its `wght` axis.
200
205
  * @returns {Font}
201
206
  */
202
207
  add(data, opts = {}) {
@@ -208,10 +213,12 @@ export class StaticFontSource {
208
213
  postscriptName: opts.postscriptName
209
214
  });
210
215
  const detected = detectStyle(font);
216
+ const axis = font.variationAxes.wght;
217
+ const weight = opts.weight !== undefined ? numericWeight(opts.weight) : detected.weight;
211
218
  this._faces.push({
212
219
  font,
213
220
  family: (opts.family || font.familyName || '').toLowerCase(),
214
- weight: opts.weight !== undefined ? numericWeight(opts.weight) : detected.weight,
221
+ weight: axis && opts.weight === undefined ? [axis.min, axis.max] : [weight, weight],
215
222
  italic:
216
223
  opts.style !== undefined ? String(opts.style).includes('italic') : detected.italic,
217
224
  candidate: null
@@ -234,8 +241,8 @@ export class StaticFontSource {
234
241
  *
235
242
  * This is not cosmetic. Every widget default in the toolkit is
236
243
  * `sans-serif`, and `code`/`pre` ask for `monospace`; with no alias for
237
- * them `matchSorted` ranks every face equally and breaks the tie on weight
238
- * distance and then insertion order, so `monospace` silently renders in a
244
+ * them `matchSorted` ranks every face equally and breaks the tie on style
245
+ * and weight and then insertion order, so `monospace` silently renders in a
239
246
  * proportional face. Inference happens automatically for a source built
240
247
  * from a font spec; a hand-built source calls this when it wants it.
241
248
  *
@@ -278,16 +285,19 @@ export class StaticFontSource {
278
285
  const weight = numericWeight(pattern.weight);
279
286
  const italic = !!(pattern.style && String(pattern.style).includes('italic'));
280
287
 
288
+ // family, then style, then weight in CSS's order; the sort is stable, so
289
+ // faces alike in all three stay in the order they were added
281
290
  const scored = this._faces.map((face) => {
282
291
  let rank = families.indexOf(face.family);
283
292
  if (rank === -1) rank = families.length; // non-matching faces trail
284
293
  return {
285
294
  face,
286
- score:
287
- rank * 1e6 + (face.italic !== italic ? 1e4 : 0) + Math.abs(face.weight - weight)
295
+ ranks: [rank, face.italic !== italic ? 1 : 0, weightRank(weight, face.weight, 400, 500)]
288
296
  };
289
297
  });
290
- scored.sort((a, b) => a.score - b.score);
298
+ scored.sort(
299
+ (a, b) => a.ranks[0] - b.ranks[0] || a.ranks[1] - b.ranks[1] || a.ranks[2] - b.ranks[2]
300
+ );
291
301
  return scored.map(({ face }) => {
292
302
  if (!face.candidate) {
293
303
  // the font's own family name, not the (lowercased, possibly aliased)
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.17.4",
3
+ "version": "8.17.6",
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",