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.
- package/lib/fontconfig.js +429 -97
- package/lib/renderingcontext_2d.js +134 -49
- package/lib/text/fontsource.js +22 -12
- package/lib/text/glyphs.js +34 -13
- package/package.json +1 -1
|
@@ -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
|
|
391
|
-
* anything else, including every
|
|
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
|
-
//
|
|
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
|
-
|
|
3194
|
-
|
|
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(
|
|
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
|
-
|
|
3262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) —
|
|
5250
|
-
//
|
|
5251
|
-
// dw x dh window out of the wrong
|
|
5252
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
5273
|
-
//
|
|
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
|
-
|
|
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
|
|
5711
|
-
* source coordinate to a gradient one — is the CTM's
|
|
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
|
-
|
|
5924
|
+
let inv = matInvert(m);
|
|
5855
5925
|
if (!inv) return false;
|
|
5856
|
-
//
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
//
|
|
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] ||
|
package/lib/text/fontsource.js
CHANGED
|
@@ -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
|
|
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
|
|
184
|
-
*
|
|
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
|
|
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
|
|
238
|
-
*
|
|
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
|
-
|
|
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(
|
|
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)
|
package/lib/text/glyphs.js
CHANGED
|
@@ -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
|
|
656
|
-
// coordinates, so gradient fill styles
|
|
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
|
|
804
|
-
|
|
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
|
|