ntk 8.14.5 → 8.14.7
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/renderingcontext_2d.js +211 -68
- package/lib/text/font.js +14 -0
- package/lib/text/fontmanager.js +24 -0
- package/lib/text/layout.js +62 -8
- package/lib/text/marks.js +3 -3
- package/lib/text/pairs.js +88 -0
- package/package.json +1 -1
|
@@ -1899,11 +1899,12 @@ class RenderingContext2d {
|
|
|
1899
1899
|
* well as the AddTraps: two requests become one, and the one that is left
|
|
1900
1900
|
* touches only the bounding box.
|
|
1901
1901
|
*/
|
|
1902
|
-
_uploadCoverage(job, b, out, edges) {
|
|
1902
|
+
_uploadCoverage(job, b, out, edges, route) {
|
|
1903
1903
|
const rasterizer = this.window.app.rasterizer;
|
|
1904
1904
|
if (!rasterizer) return false;
|
|
1905
|
-
|
|
1906
|
-
|
|
1905
|
+
const where =
|
|
1906
|
+
route ?? routeRaster(b.w, b.h, edges, this.window.app.rasterPolicy);
|
|
1907
|
+
if (where !== "local") return false;
|
|
1907
1908
|
|
|
1908
1909
|
// Rasterized over the whole box, as always, so a drawing that clips
|
|
1909
1910
|
// split across passes — a pan's strips, a full repaint — gets the same
|
|
@@ -1978,6 +1979,84 @@ class RenderingContext2d {
|
|
|
1978
1979
|
return out;
|
|
1979
1980
|
}
|
|
1980
1981
|
|
|
1982
|
+
/**
|
|
1983
|
+
* A drawing's own extent: its box with the same pixel of slack as
|
|
1984
|
+
* `_clampBBox`, and *not* clamped to the surface — what its masks are
|
|
1985
|
+
* clustered and routed by (`_fillPolys`, the stroke's mask path).
|
|
1986
|
+
*
|
|
1987
|
+
* Which rasterizer draws a mask — ours or the server's — has to be a
|
|
1988
|
+
* property of the drawing, never of where it lands. The two antialias
|
|
1989
|
+
* differently, so a route taken from the part the surface shows changed
|
|
1990
|
+
* as the drawing moved: a stroke whose cap had just left the surface had
|
|
1991
|
+
* three triangles fewer to count, went to the other rasterizer, and came
|
|
1992
|
+
* out up to ten levels different along its whole length — a pan's copy
|
|
1993
|
+
* of it one way, the strip beside it the other. Clamping and culling
|
|
1994
|
+
* decide how much work a mask is, never how it is drawn.
|
|
1995
|
+
*/
|
|
1996
|
+
_rawBox(minX, minY, maxX, maxY) {
|
|
1997
|
+
const x = Math.floor(minX) - 1;
|
|
1998
|
+
const y = Math.floor(minY) - 1;
|
|
1999
|
+
return { x, y, w: Math.ceil(maxX) + 1 - x, h: Math.ceil(maxY) + 1 - y };
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
/** `_rawBox` of one flat `[x0, y0, …]` point list; null for none. */
|
|
2003
|
+
_pointsExtent(pts) {
|
|
2004
|
+
let minX = Infinity;
|
|
2005
|
+
let minY = Infinity;
|
|
2006
|
+
let maxX = -Infinity;
|
|
2007
|
+
let maxY = -Infinity;
|
|
2008
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
2009
|
+
if (pts[i] < minX) minX = pts[i];
|
|
2010
|
+
if (pts[i] > maxX) maxX = pts[i];
|
|
2011
|
+
if (pts[i + 1] < minY) minY = pts[i + 1];
|
|
2012
|
+
if (pts[i + 1] > maxY) maxY = pts[i + 1];
|
|
2013
|
+
}
|
|
2014
|
+
if (!(maxX >= minX && maxY >= minY)) return null;
|
|
2015
|
+
return this._rawBox(minX, minY, maxX, maxY);
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
/** `_rawBox` over every subpath of a flattened path; null for none. */
|
|
2019
|
+
_polysExtent(polys) {
|
|
2020
|
+
let out = null;
|
|
2021
|
+
for (const poly of polys) {
|
|
2022
|
+
const b = this._pointsExtent(poly.pts);
|
|
2023
|
+
if (!b) continue;
|
|
2024
|
+
out = out ? unionBox(out, b) : b;
|
|
2025
|
+
}
|
|
2026
|
+
return out;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
/**
|
|
2030
|
+
* Where a drawing whose own box is `box` is rasterized (`routeRaster`),
|
|
2031
|
+
* sized by as much of it as the surface could ever show — each side no
|
|
2032
|
+
* longer than the surface's, and its edges in the same proportion —
|
|
2033
|
+
* rather than by the part it shows now: a route that depends only on the
|
|
2034
|
+
* drawing's size and complexity is the same wherever the drawing lands
|
|
2035
|
+
* (`_rawBox`). Counting every edge of a stroke that runs far past the
|
|
2036
|
+
* surface sent it through the mask more often than the part it shows
|
|
2037
|
+
* earned: a fifth more bytes a frame for a hundred such strokes.
|
|
2038
|
+
*/
|
|
2039
|
+
_routeFor(box, edges) {
|
|
2040
|
+
const w = Math.min(box.w, this.width);
|
|
2041
|
+
const h = Math.min(box.h, this.height);
|
|
2042
|
+
return routeRaster(
|
|
2043
|
+
w,
|
|
2044
|
+
h,
|
|
2045
|
+
(edges * w * h) / (box.w * box.h),
|
|
2046
|
+
this.window.app.rasterPolicy,
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
/** the part of a box on the surface; null where there is none */
|
|
2051
|
+
_onSurface(b) {
|
|
2052
|
+
const x = Math.max(0, b.x);
|
|
2053
|
+
const y = Math.max(0, b.y);
|
|
2054
|
+
const w = Math.min(this.width, b.x + b.w) - x;
|
|
2055
|
+
const h = Math.min(this.height, b.y + b.h) - y;
|
|
2056
|
+
if (w <= 0 || h <= 0) return null;
|
|
2057
|
+
return { x, y, w, h };
|
|
2058
|
+
}
|
|
2059
|
+
|
|
1981
2060
|
// a pixel of slack for the antialiased edge, clamped to the surface
|
|
1982
2061
|
_clampBBox(minX, minY, maxX, maxY) {
|
|
1983
2062
|
const x = Math.max(0, Math.floor(minX) - 1);
|
|
@@ -2030,7 +2109,7 @@ class RenderingContext2d {
|
|
|
2030
2109
|
*
|
|
2031
2110
|
* @returns {boolean} whether anything was composited
|
|
2032
2111
|
*/
|
|
2033
|
-
_paintThroughMask(b, job, { edges, src, op, alpha, server }) {
|
|
2112
|
+
_paintThroughMask(b, job, { edges, route, src, op, alpha, server }) {
|
|
2034
2113
|
const R = this.Render;
|
|
2035
2114
|
// Everything is bounded to the part of the box the clip lets through:
|
|
2036
2115
|
// the clip's rectangle, or the extents of a stack with a path in it —
|
|
@@ -2049,7 +2128,9 @@ class RenderingContext2d {
|
|
|
2049
2128
|
if (!out) return false;
|
|
2050
2129
|
}
|
|
2051
2130
|
if (this._hasPolyClip) this._requireClipMask();
|
|
2052
|
-
if (
|
|
2131
|
+
if (
|
|
2132
|
+
!this._uploadCoverage({ ...job, dx: -b.x, dy: -b.y }, b, out, edges, route)
|
|
2133
|
+
) {
|
|
2053
2134
|
R.FillRectangles(
|
|
2054
2135
|
R.PictOp.Src,
|
|
2055
2136
|
this.fillMask.id,
|
|
@@ -2118,12 +2199,13 @@ class RenderingContext2d {
|
|
|
2118
2199
|
if (!prepareStyle(src, this._m)) return;
|
|
2119
2200
|
|
|
2120
2201
|
// one box per subpath, so a path holding disjoint ones can be masked as
|
|
2121
|
-
// the pieces it is rather than as the box around all of them
|
|
2202
|
+
// the pieces it is rather than as the box around all of them — each the
|
|
2203
|
+
// subpath's own box, on the surface or not (`_rawBox`)
|
|
2122
2204
|
const shapes = [];
|
|
2123
2205
|
const pieces = [];
|
|
2124
2206
|
for (const p of polys) {
|
|
2125
2207
|
if (p.pts.length < 6) continue;
|
|
2126
|
-
const b = this.
|
|
2208
|
+
const b = this._pointsExtent(p.pts);
|
|
2127
2209
|
if (!b) continue;
|
|
2128
2210
|
shapes.push(p.pts);
|
|
2129
2211
|
pieces.push(b);
|
|
@@ -2133,18 +2215,30 @@ class RenderingContext2d {
|
|
|
2133
2215
|
this._ensureFillMask();
|
|
2134
2216
|
let painted = false;
|
|
2135
2217
|
for (const cluster of this._maskClusters(pieces, op)) {
|
|
2218
|
+
// Routed by every subpath of the cluster, and rasterized from the
|
|
2219
|
+
// ones the surface shows, over the part of them it shows: a subpath
|
|
2220
|
+
// wholly off it adds nothing to the winding of a pixel outside its
|
|
2221
|
+
// own box.
|
|
2136
2222
|
const flat = [];
|
|
2223
|
+
let box = null;
|
|
2137
2224
|
let edges = 0;
|
|
2225
|
+
let drawn = 0;
|
|
2138
2226
|
for (const i of cluster.items) {
|
|
2139
|
-
flat.push(shapes[i]);
|
|
2140
2227
|
edges += shapes[i].length / 2;
|
|
2228
|
+
const shown = this._onSurface(pieces[i]);
|
|
2229
|
+
if (!shown) continue;
|
|
2230
|
+
flat.push(shapes[i]);
|
|
2231
|
+
drawn += shapes[i].length / 2;
|
|
2232
|
+
box = box ? unionBox(box, shown) : shown;
|
|
2141
2233
|
}
|
|
2234
|
+
if (!box) continue;
|
|
2142
2235
|
painted =
|
|
2143
2236
|
this._paintThroughMask(
|
|
2144
|
-
|
|
2237
|
+
box,
|
|
2145
2238
|
{ polys: flat, rule },
|
|
2146
2239
|
{
|
|
2147
|
-
edges,
|
|
2240
|
+
edges: drawn,
|
|
2241
|
+
route: this._routeFor(cluster, edges),
|
|
2148
2242
|
src,
|
|
2149
2243
|
op,
|
|
2150
2244
|
alpha,
|
|
@@ -2497,28 +2591,12 @@ class RenderingContext2d {
|
|
|
2497
2591
|
// cuts above are meant to keep the extruder off that path; this keeps
|
|
2498
2592
|
// a future miss from being catastrophic. A sum is finite iff every
|
|
2499
2593
|
// term is.
|
|
2500
|
-
//
|
|
2501
|
-
// A triangle wholly off the surface is left out as well: it adds
|
|
2502
|
-
// nothing on either route — the rasterizer would walk its rows only for
|
|
2503
|
-
// its edges to cancel on the border column, and the server would be
|
|
2504
|
-
// sent it only to clip it away — and a zoomed graph has hundreds of
|
|
2505
|
-
// edges past the window every frame.
|
|
2506
|
-
const right = this.width + 1;
|
|
2507
|
-
const bottom = this.height + 1;
|
|
2508
2594
|
let w = 0;
|
|
2509
2595
|
for (let i = 0; i < tris.length; i += 6) {
|
|
2510
|
-
const ax = tris[i];
|
|
2511
|
-
const ay = tris[i + 1];
|
|
2512
|
-
const bx = tris[i + 2];
|
|
2513
|
-
const by = tris[i + 3];
|
|
2514
|
-
const cx = tris[i + 4];
|
|
2515
|
-
const cy = tris[i + 5];
|
|
2516
|
-
if (!Number.isFinite(ax + ay + bx + by + cx + cy)) continue;
|
|
2517
2596
|
if (
|
|
2518
|
-
(
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
(ay > bottom && by > bottom && cy > bottom)
|
|
2597
|
+
!Number.isFinite(
|
|
2598
|
+
tris[i] + tris[i + 1] + tris[i + 2] + tris[i + 3] + tris[i + 4] + tris[i + 5],
|
|
2599
|
+
)
|
|
2522
2600
|
) {
|
|
2523
2601
|
continue;
|
|
2524
2602
|
}
|
|
@@ -2527,19 +2605,6 @@ class RenderingContext2d {
|
|
|
2527
2605
|
}
|
|
2528
2606
|
if (w < tris.length) tris.length = w;
|
|
2529
2607
|
if (!tris.length) return;
|
|
2530
|
-
// Triangles are 16.16 fixed point, and the source origin a direct stroke
|
|
2531
|
-
// hands the server is 16 bits: a stroke that reaches that far is cut to
|
|
2532
|
-
// the surface first (lib/cliprect.js), a pixel to spare on every side, so
|
|
2533
|
-
// what the surface shows is drawn exactly as before. One that does not
|
|
2534
|
-
// goes as it came, for the server to clip.
|
|
2535
|
-
const edgeX = this.width + 1;
|
|
2536
|
-
const edgeY = this.height + 1;
|
|
2537
|
-
if (!withinRect(tris, -WIRE_REACH, -WIRE_REACH, WIRE_REACH, WIRE_REACH)) {
|
|
2538
|
-
const cut = clipTrianglesToRect(tris, -1, -1, edgeX, edgeY);
|
|
2539
|
-
tris.length = 0;
|
|
2540
|
-
for (let i = 0; i < cut.length; i++) tris.push(cut[i]);
|
|
2541
|
-
if (!tris.length) return;
|
|
2542
|
-
}
|
|
2543
2608
|
|
|
2544
2609
|
const op = this._op();
|
|
2545
2610
|
// round-cap/join disks overlap the stroke body; overlapping coverage
|
|
@@ -2555,6 +2620,14 @@ class RenderingContext2d {
|
|
|
2555
2620
|
op === this.Render.PictOp.Over;
|
|
2556
2621
|
const chunk = 4000 * 6;
|
|
2557
2622
|
if (direct) {
|
|
2623
|
+
this._cullTris(tris);
|
|
2624
|
+
if (!tris.length) return;
|
|
2625
|
+
const cut = this._wireCut(tris);
|
|
2626
|
+
if (cut !== tris) {
|
|
2627
|
+
tris.length = 0;
|
|
2628
|
+
for (let i = 0; i < cut.length; i++) tris.push(cut[i]);
|
|
2629
|
+
if (!tris.length) return;
|
|
2630
|
+
}
|
|
2558
2631
|
for (let i = 0; i < tris.length; i += chunk) {
|
|
2559
2632
|
const batch = tris.slice(i, i + chunk);
|
|
2560
2633
|
// RENDER aligns the source with the *first triangle's first vertex*,
|
|
@@ -2588,24 +2661,31 @@ class RenderingContext2d {
|
|
|
2588
2661
|
// and took 1.9 s on XQuartz where the bounded version takes 37 ms
|
|
2589
2662
|
// (react-x11#148). Stale mask content outside the boxes is never
|
|
2590
2663
|
// composited, so clearing only them is safe.
|
|
2664
|
+
//
|
|
2665
|
+
// The islands are the whole stroke's, on the surface or not, and so are
|
|
2666
|
+
// the clusters and the route each is drawn by (`_rawBox`); only the
|
|
2667
|
+
// triangles the surface shows are rasterized.
|
|
2591
2668
|
this._ensureFillMask();
|
|
2592
2669
|
const pieces = this._trisPieces(tris);
|
|
2593
2670
|
if (!pieces.length) return;
|
|
2594
|
-
// the soup itself when it is one piece holding every triangle — the
|
|
2595
|
-
// common case (one polyline is one piece), and worth not copying
|
|
2596
|
-
const whole =
|
|
2597
|
-
pieces.length === 1 &&
|
|
2598
|
-
pieces[0].start === 0 &&
|
|
2599
|
-
pieces[0].end === tris.length;
|
|
2600
2671
|
let painted = false;
|
|
2601
2672
|
for (const cluster of this._maskClusters(pieces, op)) {
|
|
2602
|
-
|
|
2673
|
+
let edges = 0;
|
|
2674
|
+
for (const i of cluster.items) {
|
|
2675
|
+
edges += (pieces[i].end - pieces[i].start) / 2;
|
|
2676
|
+
}
|
|
2677
|
+
const batch = this._trisBatch(tris, pieces, cluster);
|
|
2678
|
+
if (!batch.length) continue;
|
|
2679
|
+
// the mask over what is drawn, as tight as it always was
|
|
2680
|
+
const box = this._pointsBBox(batch);
|
|
2681
|
+
if (!box) continue;
|
|
2603
2682
|
painted =
|
|
2604
2683
|
this._paintThroughMask(
|
|
2605
|
-
|
|
2684
|
+
box,
|
|
2606
2685
|
{ triangles: batch },
|
|
2607
2686
|
{
|
|
2608
2687
|
edges: batch.length / 2,
|
|
2688
|
+
route: this._routeFor(cluster, edges),
|
|
2609
2689
|
src,
|
|
2610
2690
|
op,
|
|
2611
2691
|
alpha: this.globalAlpha,
|
|
@@ -2642,17 +2722,17 @@ class RenderingContext2d {
|
|
|
2642
2722
|
* linear pre-pass whose only job is to hand the clustering a handful of
|
|
2643
2723
|
* boxes instead of thousands of triangles.
|
|
2644
2724
|
*
|
|
2645
|
-
*
|
|
2646
|
-
*
|
|
2647
|
-
*
|
|
2725
|
+
* Each piece is its own box (`_rawBox`), on the surface or not: the
|
|
2726
|
+
* clusters and the routes are the stroke's, and the surface only bounds
|
|
2727
|
+
* the work (`_trisBatch`).
|
|
2648
2728
|
*/
|
|
2649
2729
|
_trisPieces(tris) {
|
|
2650
2730
|
const pieces = [];
|
|
2651
2731
|
let open = null;
|
|
2652
2732
|
const close = () => {
|
|
2653
2733
|
if (!open) return;
|
|
2654
|
-
const b = this.
|
|
2655
|
-
|
|
2734
|
+
const b = this._rawBox(open.minX, open.minY, open.maxX, open.maxY);
|
|
2735
|
+
pieces.push({ ...b, start: open.start, end: open.end });
|
|
2656
2736
|
open = null;
|
|
2657
2737
|
};
|
|
2658
2738
|
for (let i = 0; i < tris.length; i += 6) {
|
|
@@ -2681,17 +2761,81 @@ class RenderingContext2d {
|
|
|
2681
2761
|
return pieces;
|
|
2682
2762
|
}
|
|
2683
2763
|
|
|
2684
|
-
/**
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2764
|
+
/**
|
|
2765
|
+
* One cluster's triangles, gathered from its pieces: less those wholly
|
|
2766
|
+
* off the surface, which add nothing on either route — the rasterizer
|
|
2767
|
+
* would walk their rows only for their edges to cancel on the border
|
|
2768
|
+
* column, and the server would be sent them only to clip them away — and
|
|
2769
|
+
* cut to the wire's reach (`_wireCut`). The soup itself when the cluster
|
|
2770
|
+
* is all of it and all of it is on the surface: the common case, one
|
|
2771
|
+
* polyline in view, and worth not copying.
|
|
2772
|
+
*/
|
|
2773
|
+
_trisBatch(tris, pieces, cluster) {
|
|
2774
|
+
const { items } = cluster;
|
|
2775
|
+
const inside =
|
|
2776
|
+
cluster.x >= -1 &&
|
|
2777
|
+
cluster.y >= -1 &&
|
|
2778
|
+
cluster.x + cluster.w <= this.width + 1 &&
|
|
2779
|
+
cluster.y + cluster.h <= this.height + 1;
|
|
2780
|
+
if (
|
|
2781
|
+
inside &&
|
|
2782
|
+
items.length === 1 &&
|
|
2783
|
+
pieces[items[0]].start === 0 &&
|
|
2784
|
+
pieces[items[0]].end === tris.length
|
|
2785
|
+
) {
|
|
2786
|
+
return this._wireCut(tris);
|
|
2787
|
+
}
|
|
2788
|
+
const out = [];
|
|
2690
2789
|
for (const i of items) {
|
|
2691
|
-
|
|
2692
|
-
for (let k = piece.start; k < piece.end; ++k) out[w++] = tris[k];
|
|
2790
|
+
for (let k = pieces[i].start; k < pieces[i].end; ++k) out.push(tris[k]);
|
|
2693
2791
|
}
|
|
2694
|
-
|
|
2792
|
+
if (!inside) this._cullTris(out);
|
|
2793
|
+
return this._wireCut(out);
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
/**
|
|
2797
|
+
* Leave out, in place, every triangle wholly off the surface: nothing of
|
|
2798
|
+
* it can land on it, and a zoomed graph has hundreds of edges past the
|
|
2799
|
+
* window every frame.
|
|
2800
|
+
*/
|
|
2801
|
+
_cullTris(tris) {
|
|
2802
|
+
const right = this.width + 1;
|
|
2803
|
+
const bottom = this.height + 1;
|
|
2804
|
+
let w = 0;
|
|
2805
|
+
for (let i = 0; i < tris.length; i += 6) {
|
|
2806
|
+
const ax = tris[i];
|
|
2807
|
+
const ay = tris[i + 1];
|
|
2808
|
+
const bx = tris[i + 2];
|
|
2809
|
+
const by = tris[i + 3];
|
|
2810
|
+
const cx = tris[i + 4];
|
|
2811
|
+
const cy = tris[i + 5];
|
|
2812
|
+
if (
|
|
2813
|
+
(ax < -1 && bx < -1 && cx < -1) ||
|
|
2814
|
+
(ay < -1 && by < -1 && cy < -1) ||
|
|
2815
|
+
(ax > right && bx > right && cx > right) ||
|
|
2816
|
+
(ay > bottom && by > bottom && cy > bottom)
|
|
2817
|
+
) {
|
|
2818
|
+
continue;
|
|
2819
|
+
}
|
|
2820
|
+
if (w < i) for (let k = 0; k < 6; k++) tris[w + k] = tris[i + k];
|
|
2821
|
+
w += 6;
|
|
2822
|
+
}
|
|
2823
|
+
if (w < tris.length) tris.length = w;
|
|
2824
|
+
return tris;
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
/**
|
|
2828
|
+
* Triangles are 16.16 fixed point, and the source origin a direct stroke
|
|
2829
|
+
* hands the server is 16 bits: a stroke that reaches that far is cut to
|
|
2830
|
+
* the surface first (lib/cliprect.js), a pixel to spare on every side, so
|
|
2831
|
+
* what the surface shows is drawn exactly as before. One that does not
|
|
2832
|
+
* goes as it came, for the server to clip — the same array.
|
|
2833
|
+
*/
|
|
2834
|
+
_wireCut(tris) {
|
|
2835
|
+
if (withinRect(tris, -WIRE_REACH, -WIRE_REACH, WIRE_REACH, WIRE_REACH)) {
|
|
2836
|
+
return tris;
|
|
2837
|
+
}
|
|
2838
|
+
return clipTrianglesToRect(tris, -1, -1, this.width + 1, this.height + 1);
|
|
2695
2839
|
}
|
|
2696
2840
|
|
|
2697
2841
|
/**
|
|
@@ -4129,11 +4273,10 @@ class RenderingContext2d {
|
|
|
4129
4273
|
if (!rasterizer) return false;
|
|
4130
4274
|
const { shapes, edges } = fillableShapes(entry.polys);
|
|
4131
4275
|
if (!shapes.length) return false;
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
)
|
|
4276
|
+
// routed by the clip's own extent, as a fill is (`_rawBox`), and
|
|
4277
|
+
// rasterized over the part of it on the surface
|
|
4278
|
+
if (this._routeFor(this._polysExtent(entry.polys) ?? bb, edges) !== "local")
|
|
4135
4279
|
return false;
|
|
4136
|
-
}
|
|
4137
4280
|
|
|
4138
4281
|
const coverage = rasterizer.rasterize({
|
|
4139
4282
|
polys: shapes,
|
package/lib/text/font.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as fontkit from 'fontkit';
|
|
|
3
3
|
import { flatten, rasterizePath } from '../rasterize.js';
|
|
4
4
|
import { NO_ANCHOR, tolerateAnchors, watchAnchors } from './anchors.js';
|
|
5
5
|
import { marksCover, standInMarks } from './marks.js';
|
|
6
|
+
import { pairGlyphs, pairsCover } from './pairs.js';
|
|
6
7
|
|
|
7
8
|
// Axis coordinates are rounded to this many decimals before anything is
|
|
8
9
|
// instantiated or cached. Every distinct coordinate is a font in its own
|
|
@@ -154,6 +155,8 @@ export default class Font {
|
|
|
154
155
|
// whether the face's mark attachment is watched for a NULL anchor
|
|
155
156
|
// (./anchors.js): set at its first shaping, false once one has been met
|
|
156
157
|
this._anchors = false;
|
|
158
|
+
// the glyphs its pairs can move (./pairs.js), read when first asked
|
|
159
|
+
this._pairs = undefined;
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
static loadSync(path, postscriptName) {
|
|
@@ -356,6 +359,17 @@ export default class Font {
|
|
|
356
359
|
return this._drawable;
|
|
357
360
|
}
|
|
358
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Whether two glyphs side by side can be set otherwise than apart: the
|
|
364
|
+
* left one moved against what follows it, or the right one against what
|
|
365
|
+
* comes before it (./pairs.js). True where the tables cannot tell.
|
|
366
|
+
*/
|
|
367
|
+
mayKern(left, right) {
|
|
368
|
+
if (this._pairs === undefined) this._pairs = pairGlyphs(this.fk);
|
|
369
|
+
const pairs = this._pairs;
|
|
370
|
+
return pairs === null || pairsCover(pairs.left, left) || pairsCover(pairs.right, right);
|
|
371
|
+
}
|
|
372
|
+
|
|
359
373
|
hasGlyph(codepoint) {
|
|
360
374
|
return this.drawable && this.fk.hasGlyphForCodePoint(codepoint);
|
|
361
375
|
}
|
package/lib/text/fontmanager.js
CHANGED
|
@@ -235,6 +235,8 @@ export default class FontManager {
|
|
|
235
235
|
this._shapeCount = 0;
|
|
236
236
|
// whether two letters shape otherwise together than apart (`_meets`)
|
|
237
237
|
this._meetings = new Map();
|
|
238
|
+
// how much two letters kern across a break opportunity (`_kernAcross`)
|
|
239
|
+
this._kerns = new Map();
|
|
238
240
|
// a paragraph's layout up to the line fill, for its next width
|
|
239
241
|
// (text/paragraphs.js, TextLayout)
|
|
240
242
|
this._paragraphs = new ParagraphCache();
|
|
@@ -580,6 +582,28 @@ export default class FontManager {
|
|
|
580
582
|
return meets;
|
|
581
583
|
}
|
|
582
584
|
|
|
585
|
+
/**
|
|
586
|
+
* How much wider the two letters of `pair`, cut at `cut`, are set
|
|
587
|
+
* together than apart in this style: their kerning, negative for a pair
|
|
588
|
+
* set closer. 0 where they do not kern, and where shaping them together
|
|
589
|
+
* changes a glyph — a ligature, a contextual form — which no offset
|
|
590
|
+
* between the two stands for. Kept, as `_meets` keeps its answers.
|
|
591
|
+
*/
|
|
592
|
+
_kernAcross(pair, cut, style, levelsKey = '0') {
|
|
593
|
+
const key = `${shapeGroupOf(style, levelsKey)}|${cut}|${pair}`;
|
|
594
|
+
let kern = this._kerns.get(key);
|
|
595
|
+
if (kern !== undefined) return kern;
|
|
596
|
+
const whole = this._shapeCached(pair, style, levelsKey);
|
|
597
|
+
const left = this._shapeCached(pair.slice(0, cut), style, sliceKey(levelsKey, 0, cut));
|
|
598
|
+
const right = this._shapeCached(pair.slice(cut), style, sliceKey(levelsKey, cut, pair.length));
|
|
599
|
+
const ids = (...shaped) => shaped.flatMap((s) => s.runs.flatMap((r) => r.glyphs.map((g) => g.id))).join(',');
|
|
600
|
+
kern = whole.width - left.width - right.width;
|
|
601
|
+
if (Math.abs(kern) <= 1e-6 || ids(whole) !== ids(left, right)) kern = 0;
|
|
602
|
+
if (this._kerns.size >= SHAPE_GENERATION) this._kerns.clear();
|
|
603
|
+
this._kerns.set(key, kern);
|
|
604
|
+
return kern;
|
|
605
|
+
}
|
|
606
|
+
|
|
583
607
|
_shapeCached(text, style, levelsKey = '0') {
|
|
584
608
|
// A hit in this generation is the whole cost of a word a paragraph has
|
|
585
609
|
// shaped before — a relayout at a new width asks for every word again,
|
package/lib/text/layout.js
CHANGED
|
@@ -21,10 +21,16 @@ const UNSPACED =
|
|
|
21
21
|
/** How wide a line's tokens are, the white space it ends on left out. */
|
|
22
22
|
function tokensWidth(toks) {
|
|
23
23
|
let width = 0;
|
|
24
|
-
for (
|
|
24
|
+
for (let i = 0; i < toks.length; i++) width += toks[i].width + (i ? kernBefore(toks[i]) : 0);
|
|
25
25
|
return toks.length ? width - toks[toks.length - 1].wsWidth : 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** The kerning between a token and the one before it on its line
|
|
29
|
+
* (`TextLayout._kernBefore`); none for a token a line starts with. */
|
|
30
|
+
function kernBefore(token) {
|
|
31
|
+
return token.kernBefore ?? 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
function isWsGlyph(g) {
|
|
29
35
|
return g.codePoints.length > 0 && g.codePoints.every((cp) => WS.has(cp));
|
|
30
36
|
}
|
|
@@ -121,7 +127,7 @@ export class TextLayout {
|
|
|
121
127
|
curWidth = 0;
|
|
122
128
|
};
|
|
123
129
|
for (let token of tokens) {
|
|
124
|
-
while (wraps && curWidth + token.width - token.wsWidth > maxWidth) {
|
|
130
|
+
while (wraps && curWidth + (cur.length ? kernBefore(token) : 0) + token.width - token.wsWidth > maxWidth) {
|
|
125
131
|
if (cur.length > 0) {
|
|
126
132
|
flush();
|
|
127
133
|
} else if (whole && !UNSPACED.test(text.slice(token.start, token.end))) {
|
|
@@ -135,8 +141,8 @@ export class TextLayout {
|
|
|
135
141
|
token = rest;
|
|
136
142
|
}
|
|
137
143
|
}
|
|
144
|
+
curWidth += token.width + (cur.length ? kernBefore(token) : 0);
|
|
138
145
|
cur.push(token);
|
|
139
|
-
curWidth += token.width;
|
|
140
146
|
if (token.required) flush();
|
|
141
147
|
}
|
|
142
148
|
if (cur.length) lineTokens.push(cur);
|
|
@@ -185,16 +191,24 @@ export class TextLayout {
|
|
|
185
191
|
// .start/.end are absolute code-unit ranges into the full text, kept
|
|
186
192
|
// for caret positioning (caretPosition / indexAt)
|
|
187
193
|
let entries = [];
|
|
188
|
-
for (
|
|
194
|
+
for (let t = 0; t < toks.length; t++) {
|
|
195
|
+
const token = toks[t];
|
|
196
|
+
// the kerning with the token before it goes before its first run
|
|
197
|
+
let kern = t ? kernBefore(token) : 0;
|
|
189
198
|
for (const frag of token.fragments) {
|
|
190
199
|
for (const run of frag.shaped.runs) {
|
|
191
|
-
|
|
200
|
+
const entry = {
|
|
192
201
|
run,
|
|
193
202
|
span: frag.span,
|
|
194
203
|
level: run.level,
|
|
195
204
|
start: frag.start + run.start,
|
|
196
205
|
end: frag.start + run.end
|
|
197
|
-
}
|
|
206
|
+
};
|
|
207
|
+
if (kern) {
|
|
208
|
+
entry.kern = kern;
|
|
209
|
+
kern = 0;
|
|
210
|
+
}
|
|
211
|
+
entries.push(entry);
|
|
198
212
|
}
|
|
199
213
|
}
|
|
200
214
|
}
|
|
@@ -234,6 +248,7 @@ export class TextLayout {
|
|
|
234
248
|
const runs = [];
|
|
235
249
|
let x = 0;
|
|
236
250
|
for (const e of entries) {
|
|
251
|
+
if (e.kern) x += e.kern;
|
|
237
252
|
const positioned = { x, width: e.run.width, run: e.run, span: e.span, start: e.start, end: e.end };
|
|
238
253
|
if (e.ellipsis) positioned.ellipsis = true;
|
|
239
254
|
runs.push(positioned);
|
|
@@ -396,10 +411,48 @@ export class TextLayout {
|
|
|
396
411
|
tokens.push(this._makeToken(text, prev, text.length, levels, spanAt, false));
|
|
397
412
|
}
|
|
398
413
|
}
|
|
414
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
415
|
+
const kern = this._kernBefore(text, tokens[i - 1], tokens[i], levels);
|
|
416
|
+
if (kern) tokens[i].kernBefore = kern;
|
|
417
|
+
}
|
|
399
418
|
|
|
400
419
|
return { spans, text, baseLevel, tokens };
|
|
401
420
|
}
|
|
402
421
|
|
|
422
|
+
/**
|
|
423
|
+
* The kerning between the last letter of one token and the first of the
|
|
424
|
+
* next, where the two are on one line. Each token is shaped apart, so a
|
|
425
|
+
* pair either side of a break opportunity was never kerned — Trebuchet MS
|
|
426
|
+
* sets a space closer to an A, a T or a Y — and a line of it came out
|
|
427
|
+
* wider than a browser's, which shapes a line whole: shaping breaks only
|
|
428
|
+
* at an inline box's margin, border or padding (CSS Text 3, 7.3). Only
|
|
429
|
+
* between letters shaped alike at one left-to-right level; a line that
|
|
430
|
+
* breaks there never meets it.
|
|
431
|
+
*/
|
|
432
|
+
_kernBefore(text, prev, next, levels) {
|
|
433
|
+
if (prev.required) return 0;
|
|
434
|
+
const a = prev.fragments[prev.fragments.length - 1];
|
|
435
|
+
const b = next.fragments[0];
|
|
436
|
+
if (!a || !b || a.shaping !== b.shaping || a.span.shapeApart || b.span.shapeApart) return 0;
|
|
437
|
+
const at = a.start + a.text.length;
|
|
438
|
+
if (at !== b.start) return 0;
|
|
439
|
+
// two glyphs the face's tables cannot move against each other are set
|
|
440
|
+
// together as apart, and asked about no further: most faces kern no
|
|
441
|
+
// space, and a paragraph meets one at every word
|
|
442
|
+
const l = a.shaped.runs[a.shaped.runs.length - 1];
|
|
443
|
+
const r = b.shaped.runs[0];
|
|
444
|
+
if (!l || !r || l.font !== r.font) return 0;
|
|
445
|
+
const lg = l.glyphs[l.glyphs.length - 1];
|
|
446
|
+
const rg = r.glyphs[0];
|
|
447
|
+
if (!lg || !rg || (l.font.mayKern && !l.font.mayKern(lg.id, rg.id))) return 0;
|
|
448
|
+
const low = text.charCodeAt(at - 1);
|
|
449
|
+
const before = low >= 0xdc00 && low < 0xe000 && at >= 2 ? 2 : 1;
|
|
450
|
+
const after = text.codePointAt(at) > 0xffff ? 2 : 1;
|
|
451
|
+
const key = normalizedLevels(levels, at - before, at + after);
|
|
452
|
+
if (key.includes(',') || Number(key) & 1) return 0;
|
|
453
|
+
return this.fonts._kernAcross(text.slice(at - before, at + after), before, a.shaping, key);
|
|
454
|
+
}
|
|
455
|
+
|
|
403
456
|
/**
|
|
404
457
|
* Whether the letters that meet at an edge between two spans of a group
|
|
405
458
|
* shape otherwise together than apart: their glyphs or their advances
|
|
@@ -537,9 +590,10 @@ export class TextLayout {
|
|
|
537
590
|
for (const token of toks) {
|
|
538
591
|
// trailing whitespace does not count against the budget, exactly as it
|
|
539
592
|
// does not count during the greedy fill
|
|
540
|
-
|
|
593
|
+
const kern = kept.length ? kernBefore(token) : 0;
|
|
594
|
+
if (used + kern + token.width - token.wsWidth <= budget) {
|
|
541
595
|
kept.push(token);
|
|
542
|
-
used += token.width;
|
|
596
|
+
used += kern + token.width;
|
|
543
597
|
continue;
|
|
544
598
|
}
|
|
545
599
|
const [head] = this._forceBreak(token, budget - used);
|
package/lib/text/marks.js
CHANGED
|
@@ -102,7 +102,7 @@ export function marksCover(bits, id) {
|
|
|
102
102
|
* base)` reads the 16-bit offset stored at `pos` and answers where it
|
|
103
103
|
* points, `base` being what the format measures it from.
|
|
104
104
|
*/
|
|
105
|
-
function table(fk, tag) {
|
|
105
|
+
export function table(fk, tag) {
|
|
106
106
|
const entry = fk.directory?.tables?.[tag];
|
|
107
107
|
if (!entry) return null;
|
|
108
108
|
const stream = fk._getTableStream(tag);
|
|
@@ -163,7 +163,7 @@ function coverLookup(t, lookupList, index, bits) {
|
|
|
163
163
|
* Where the coverage a subtable is entered from starts, or -1 for a
|
|
164
164
|
* subtable this does not know.
|
|
165
165
|
*/
|
|
166
|
-
function firstCoverage(t, type, sub) {
|
|
166
|
+
export function firstCoverage(t, type, sub) {
|
|
167
167
|
const format = t.u16(sub);
|
|
168
168
|
switch (type) {
|
|
169
169
|
case 1: // single adjustment
|
|
@@ -191,7 +191,7 @@ function firstCoverage(t, type, sub) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
function addCoverage(t, coverage, bits) {
|
|
194
|
+
export function addCoverage(t, coverage, bits) {
|
|
195
195
|
const format = t.u16(coverage);
|
|
196
196
|
const count = t.u16(coverage + 2);
|
|
197
197
|
if (format === 1) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The glyphs a face can set closer or further from the glyph beside them,
|
|
2
|
+
// read from its tables' bytes the way ./marks.js reads its marks: a pair of
|
|
3
|
+
// letters that neither can be is set together as it is set apart, and the
|
|
4
|
+
// layout need not shape the two to find that out (`TextLayout._kernBefore`).
|
|
5
|
+
//
|
|
6
|
+
// A pair's left glyph can be moved by a pair adjustment it enters, by a
|
|
7
|
+
// cursive attachment, by a context it is the first input of, or by the
|
|
8
|
+
// pairs of a legacy `kern` table; its right glyph only by a context it is
|
|
9
|
+
// the first input of, with the left glyph behind it. Every GPOS lookup is
|
|
10
|
+
// read, whatever feature names it — a superset of what fontkit applies,
|
|
11
|
+
// which only widens what counts.
|
|
12
|
+
//
|
|
13
|
+
// Null — ask the shaper — for a face shaped through AAT's morx, a `kern`
|
|
14
|
+
// table in a format this does not read, or tables it cannot read to the end.
|
|
15
|
+
|
|
16
|
+
import { addCoverage, firstCoverage, table } from './marks.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} fk a fontkit font
|
|
20
|
+
* @returns {{ left: Uint8Array, right: Uint8Array }|null} a bit per glyph id,
|
|
21
|
+
* set for every glyph that can be moved against the glyph after it
|
|
22
|
+
* (`left`) or before it (`right`); null where this cannot tell
|
|
23
|
+
*/
|
|
24
|
+
export function pairGlyphs(fk) {
|
|
25
|
+
try {
|
|
26
|
+
if (fk.directory?.tables?.morx) return null;
|
|
27
|
+
const left = new Uint8Array(8192);
|
|
28
|
+
const right = new Uint8Array(8192);
|
|
29
|
+
const gpos = table(fk, 'GPOS');
|
|
30
|
+
if (gpos) {
|
|
31
|
+
const lookupList = gpos.offset(8, 0);
|
|
32
|
+
for (let index = 0, n = gpos.u16(lookupList); index < n; index++) {
|
|
33
|
+
if (!coverPairs(gpos, lookupList, index, left, right)) return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const kern = table(fk, 'kern');
|
|
37
|
+
if (kern && !kernLefts(kern, left)) return null;
|
|
38
|
+
return { left, right };
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether `bits` (from `pairGlyphs`) has glyph `id`. */
|
|
45
|
+
export function pairsCover(bits, id) {
|
|
46
|
+
return (bits[id >> 3] & (1 << (id & 7))) !== 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Adds what a lookup can move against a neighbour; false for one it cannot read. */
|
|
50
|
+
function coverPairs(t, lookupList, index, left, right) {
|
|
51
|
+
const lookup = t.offset(lookupList + 2 + index * 2, lookupList);
|
|
52
|
+
const type = t.u16(lookup);
|
|
53
|
+
for (let i = 0, n = t.u16(lookup + 4); i < n; i++) {
|
|
54
|
+
let sub = t.offset(lookup + 6 + i * 2, lookup);
|
|
55
|
+
let subType = type;
|
|
56
|
+
if (subType === 9) {
|
|
57
|
+
if (t.u16(sub) !== 1) return false;
|
|
58
|
+
subType = t.u16(sub + 2);
|
|
59
|
+
sub += t.u32(sub + 4);
|
|
60
|
+
}
|
|
61
|
+
// a single adjustment moves a glyph whatever is beside it, and a mark
|
|
62
|
+
// attaches with no advance of its own
|
|
63
|
+
if (subType !== 2 && subType !== 3 && subType !== 7 && subType !== 8) continue;
|
|
64
|
+
const coverage = firstCoverage(t, subType, sub);
|
|
65
|
+
if (coverage < 0) return false;
|
|
66
|
+
addCoverage(t, coverage, left);
|
|
67
|
+
if (subType === 7 || subType === 8) addCoverage(t, coverage, right);
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Adds the left glyphs of a Microsoft `kern` table's pairs; false for a
|
|
73
|
+
* table or subtable format this does not read. */
|
|
74
|
+
function kernLefts(t, left) {
|
|
75
|
+
if (t.u16(0) !== 0) return false;
|
|
76
|
+
let at = 4;
|
|
77
|
+
for (let i = 0, n = t.u16(2); i < n; i++) {
|
|
78
|
+
const length = t.u16(at + 2);
|
|
79
|
+
const format = t.u16(at + 4) >> 8;
|
|
80
|
+
if (format !== 0) return false;
|
|
81
|
+
for (let j = 0, pairs = t.u16(at + 6); j < pairs; j++) {
|
|
82
|
+
const id = t.u16(at + 14 + j * 6);
|
|
83
|
+
left[id >> 3] |= 1 << (id & 7);
|
|
84
|
+
}
|
|
85
|
+
at += length;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|