ntk 8.12.0 → 8.12.2
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 +230 -39
- package/lib/text/glyphs.js +34 -5
- package/package.json +1 -1
|
@@ -94,6 +94,51 @@ function intersectBox(a, b) {
|
|
|
94
94
|
return { x, y, w: right - x, h: bottom - y };
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Whether no two of a flat `[x, y, w, h, …]` list of whole-pixel rectangles
|
|
99
|
+
* share a pixel — touching edges do not. A sweep along x holding the
|
|
100
|
+
* rectangles it is inside as disjoint y spans in order, so a list that is
|
|
101
|
+
* disjoint, which is the list this is asked about, costs a binary search
|
|
102
|
+
* per rectangle: a chart's per-column spans, an occupancy grid's cells.
|
|
103
|
+
*/
|
|
104
|
+
function disjointRects(rects) {
|
|
105
|
+
const n = rects.length / 4;
|
|
106
|
+
if (n < 2) return true;
|
|
107
|
+
// events: where a rectangle starts and where it ends, ends first at a tie
|
|
108
|
+
const events = [];
|
|
109
|
+
for (let i = 0; i < n; i++) {
|
|
110
|
+
events.push([rects[i * 4], 1, i], [rects[i * 4] + rects[i * 4 + 2], 0, i]);
|
|
111
|
+
}
|
|
112
|
+
events.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
113
|
+
// the y spans the sweep is inside, sorted by their top, as rectangle ids
|
|
114
|
+
const active = [];
|
|
115
|
+
const top = (i) => rects[i * 4 + 1];
|
|
116
|
+
const bottom = (i) => rects[i * 4 + 1] + rects[i * 4 + 3];
|
|
117
|
+
const find = (y) => {
|
|
118
|
+
let lo = 0;
|
|
119
|
+
let hi = active.length;
|
|
120
|
+
while (lo < hi) {
|
|
121
|
+
const mid = (lo + hi) >> 1;
|
|
122
|
+
if (top(active[mid]) < y) lo = mid + 1;
|
|
123
|
+
else hi = mid;
|
|
124
|
+
}
|
|
125
|
+
return lo;
|
|
126
|
+
};
|
|
127
|
+
for (const [, starts, i] of events) {
|
|
128
|
+
if (!starts) {
|
|
129
|
+
let at = find(top(i));
|
|
130
|
+
while (active[at] !== i) at++;
|
|
131
|
+
active.splice(at, 1);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const at = find(top(i));
|
|
135
|
+
if (at > 0 && bottom(active[at - 1]) > top(i)) return false;
|
|
136
|
+
if (at < active.length && top(active[at]) < bottom(i)) return false;
|
|
137
|
+
active.splice(at, 0, i);
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
97
142
|
/** The device-space ink bounds of a flattened path, unclamped. */
|
|
98
143
|
function polysInk(polys) {
|
|
99
144
|
let minX = Infinity;
|
|
@@ -1854,42 +1899,47 @@ class RenderingContext2d {
|
|
|
1854
1899
|
* well as the AddTraps: two requests become one, and the one that is left
|
|
1855
1900
|
* touches only the bounding box.
|
|
1856
1901
|
*/
|
|
1857
|
-
_uploadCoverage(job, b, edges) {
|
|
1902
|
+
_uploadCoverage(job, b, out, edges) {
|
|
1858
1903
|
const rasterizer = this.window.app.rasterizer;
|
|
1859
1904
|
if (!rasterizer) return false;
|
|
1860
1905
|
if (routeRaster(b.w, b.h, edges, this.window.app.rasterPolicy) !== "local")
|
|
1861
1906
|
return false;
|
|
1862
1907
|
|
|
1908
|
+
// Rasterized over the whole box, as always, so a drawing that clips
|
|
1909
|
+
// split across passes — a pan's strips, a full repaint — gets the same
|
|
1910
|
+
// coverage in every one of them, byte for byte. Only `out`, the part the
|
|
1911
|
+
// clip lets through, goes on the wire (issue #372).
|
|
1863
1912
|
const coverage = rasterizer.rasterize({ ...job, width: b.w, height: b.h });
|
|
1864
1913
|
if (!coverage) return false;
|
|
1865
1914
|
|
|
1866
1915
|
// X wants scanlines padded to 4 bytes; the rasterizer contract is
|
|
1867
|
-
// unpadded rows. When the
|
|
1868
|
-
// power-of-two icon box is — the
|
|
1869
|
-
// rasterizer's own bytes, with no
|
|
1870
|
-
|
|
1916
|
+
// unpadded rows. When the part sent is the whole box and its width is
|
|
1917
|
+
// already a multiple of 4 — which every power-of-two icon box is — the
|
|
1918
|
+
// coverage goes out as a view over the rasterizer's own bytes, with no
|
|
1919
|
+
// copy at all.
|
|
1920
|
+
const stride = (out.w + 3) & ~3;
|
|
1871
1921
|
let data;
|
|
1872
|
-
if (stride === b.w) {
|
|
1922
|
+
if (out.w === b.w && out.h === b.h && stride === b.w) {
|
|
1873
1923
|
data = Buffer.isBuffer(coverage)
|
|
1874
1924
|
? coverage
|
|
1875
1925
|
: Buffer.from(coverage.buffer, coverage.byteOffset, coverage.length);
|
|
1876
1926
|
} else {
|
|
1877
|
-
data = Buffer.alloc(stride *
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
);
|
|
1927
|
+
data = Buffer.alloc(stride * out.h);
|
|
1928
|
+
const left = out.x - b.x;
|
|
1929
|
+
const top = out.y - b.y;
|
|
1930
|
+
for (let y = 0; y < out.h; ++y) {
|
|
1931
|
+
const from = coverage.byteOffset + (top + y) * b.w + left;
|
|
1932
|
+
Buffer.from(coverage.buffer, from, out.w).copy(data, y * stride);
|
|
1883
1933
|
}
|
|
1884
1934
|
}
|
|
1885
1935
|
this.X.PutImage(
|
|
1886
1936
|
2,
|
|
1887
1937
|
this.fillMaskDrawable.id,
|
|
1888
1938
|
this._maskGC(),
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1939
|
+
out.w,
|
|
1940
|
+
out.h,
|
|
1941
|
+
out.x,
|
|
1942
|
+
out.y,
|
|
1893
1943
|
0,
|
|
1894
1944
|
8,
|
|
1895
1945
|
data,
|
|
@@ -1982,30 +2032,29 @@ class RenderingContext2d {
|
|
|
1982
2032
|
*/
|
|
1983
2033
|
_paintThroughMask(b, job, { edges, src, op, alpha, server }) {
|
|
1984
2034
|
const R = this.Render;
|
|
1985
|
-
//
|
|
1986
|
-
//
|
|
1987
|
-
// and
|
|
1988
|
-
//
|
|
1989
|
-
//
|
|
1990
|
-
//
|
|
2035
|
+
// Everything is bounded to the part of the box the clip lets through:
|
|
2036
|
+
// the clip's rectangle, or the extents of a stack with a path in it —
|
|
2037
|
+
// the coverage rasterized and uploaded, the clip applied, the composite.
|
|
2038
|
+
// A stroke across a rounded pane used to upload its whole box through
|
|
2039
|
+
// the mask to paint a few rows of it (issue #372). The mask content
|
|
2040
|
+
// outside `out` is stale by the same argument as outside `b`, and a box
|
|
2041
|
+
// the clip rejects outright needs no coverage at all. A stack whose mask
|
|
2042
|
+
// was dropped by restore() may still hold a poly, and the mask comes
|
|
2043
|
+
// back on demand. A region clip needs nothing here: the picture carries
|
|
2044
|
+
// it, and it applies to the composite below like any other.
|
|
2045
|
+
const clip = this._hasPolyClip ? this._clipExtents() : this._clipRect();
|
|
1991
2046
|
let out = b;
|
|
1992
|
-
if (
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
} else {
|
|
1996
|
-
const cr = this._clipRect();
|
|
1997
|
-
if (cr) {
|
|
1998
|
-
out = intersectBox(b, cr);
|
|
1999
|
-
if (!out) return false;
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2047
|
+
if (clip) {
|
|
2048
|
+
out = intersectBox(b, clip);
|
|
2049
|
+
if (!out) return false;
|
|
2002
2050
|
}
|
|
2003
|
-
if (
|
|
2051
|
+
if (this._hasPolyClip) this._requireClipMask();
|
|
2052
|
+
if (!this._uploadCoverage({ ...job, dx: -b.x, dy: -b.y }, b, out, edges)) {
|
|
2004
2053
|
R.FillRectangles(
|
|
2005
2054
|
R.PictOp.Src,
|
|
2006
2055
|
this.fillMask.id,
|
|
2007
2056
|
[0, 0, 0, 0],
|
|
2008
|
-
[
|
|
2057
|
+
[out.x, out.y, out.w, out.h],
|
|
2009
2058
|
);
|
|
2010
2059
|
server();
|
|
2011
2060
|
}
|
|
@@ -2667,7 +2716,14 @@ class RenderingContext2d {
|
|
|
2667
2716
|
if (rect.w === 0 || rect.h === 0) return;
|
|
2668
2717
|
this._setPictureClip(rect);
|
|
2669
2718
|
}
|
|
2670
|
-
drawGlyphRuns(
|
|
2719
|
+
drawGlyphRuns(
|
|
2720
|
+
app,
|
|
2721
|
+
op,
|
|
2722
|
+
src.id,
|
|
2723
|
+
this._dst(),
|
|
2724
|
+
positioned,
|
|
2725
|
+
rect ?? { x: 0, y: 0, w: this.width, h: this.height },
|
|
2726
|
+
);
|
|
2671
2727
|
if (rect) this._invalidatePictureClip();
|
|
2672
2728
|
this._markDirty();
|
|
2673
2729
|
return;
|
|
@@ -2688,6 +2744,7 @@ class RenderingContext2d {
|
|
|
2688
2744
|
this._glyphSource.id,
|
|
2689
2745
|
this.fillMask.id,
|
|
2690
2746
|
positioned,
|
|
2747
|
+
{ x: 0, y: 0, w: this.width, h: this.height },
|
|
2691
2748
|
);
|
|
2692
2749
|
R.Composite(
|
|
2693
2750
|
R.PictOp.In,
|
|
@@ -3009,9 +3066,12 @@ class RenderingContext2d {
|
|
|
3009
3066
|
* A solid-colour fillStyle under an identity transform and a rectangular
|
|
3010
3067
|
* (or absent) clip compiles the whole list into a single
|
|
3011
3068
|
* `Render.FillRectangles`, where N `fillRect` calls cost N composites.
|
|
3012
|
-
*
|
|
3013
|
-
*
|
|
3014
|
-
*
|
|
3069
|
+
* Under a clip that is not a rectangle, rectangles that do not overlap
|
|
3070
|
+
* still cost three requests however many there are
|
|
3071
|
+
* (`_fillRectsThroughClip`). Gradient/Picture styles, transforms, and
|
|
3072
|
+
* overlapping rectangles or `copy` under a non-rectangular clip fall back
|
|
3073
|
+
* to that `fillRect` loop, so the answer is always right and only the
|
|
3074
|
+
* request count varies.
|
|
3015
3075
|
*/
|
|
3016
3076
|
fillRects(rects) {
|
|
3017
3077
|
if (!rects || !rects.length || this.globalAlpha <= 0) return;
|
|
@@ -3045,12 +3105,13 @@ class RenderingContext2d {
|
|
|
3045
3105
|
if (
|
|
3046
3106
|
!matIsIdentity(this._m) ||
|
|
3047
3107
|
!isPlainColor(this._fillStyle) ||
|
|
3048
|
-
this._hasPolyClip
|
|
3108
|
+
(this._hasPolyClip && !this._fillRectsThroughClip(flat))
|
|
3049
3109
|
) {
|
|
3050
3110
|
for (let i = 0; i < flat.length; i += 4)
|
|
3051
3111
|
this._fillRect(flat[i], flat[i + 1], flat[i + 2], flat[i + 3]);
|
|
3052
3112
|
return;
|
|
3053
3113
|
}
|
|
3114
|
+
if (this._hasPolyClip) return;
|
|
3054
3115
|
if (clip && (clip.w === 0 || clip.h === 0)) return; // clipped away
|
|
3055
3116
|
const R = this.Render;
|
|
3056
3117
|
if (clip) this._setPictureClip(clip);
|
|
@@ -3073,6 +3134,102 @@ class RenderingContext2d {
|
|
|
3073
3134
|
this._markDirty();
|
|
3074
3135
|
}
|
|
3075
3136
|
|
|
3137
|
+
/**
|
|
3138
|
+
* `fillRects` under a clip that is not a rectangle — a rounded card, a
|
|
3139
|
+
* path: every rectangle's coverage into the scratch a8 mask with one
|
|
3140
|
+
* `FillRectangles`, `globalAlpha` and the clip applied to it once each,
|
|
3141
|
+
* and the style composited through it once. One rectangle at a time is a
|
|
3142
|
+
* composite through the clip each, and a chart's marks inside a rounded
|
|
3143
|
+
* box a zoom had put off the pixel grid were sixty thousand of them a
|
|
3144
|
+
* frame (issue #374).
|
|
3145
|
+
*
|
|
3146
|
+
* Only where that is the loop's picture. The rectangles must not overlap:
|
|
3147
|
+
* the loop blends a pixel once per rectangle over it, at the clip's
|
|
3148
|
+
* coverage each time, so where two overlap on the clip's antialiased edge
|
|
3149
|
+
* it is darker than one blend through the mask — and where they do not,
|
|
3150
|
+
* every pixel is blended once either way, whatever the colour's alpha.
|
|
3151
|
+
* And the operator must leave the destination alone where the mask is
|
|
3152
|
+
* zero: `copy` clears every pixel of its box the mask misses, the gaps
|
|
3153
|
+
* between the rectangles here. The rectangles reach the mask as the wire
|
|
3154
|
+
* carries every box, in whole pixels truncated toward zero, which is what
|
|
3155
|
+
* `_fillRect`'s composites are cut to as well.
|
|
3156
|
+
*
|
|
3157
|
+
* @returns {boolean} whether the rectangles were drawn here
|
|
3158
|
+
*/
|
|
3159
|
+
_fillRectsThroughClip(flat) {
|
|
3160
|
+
const R = this.Render;
|
|
3161
|
+
const op = this._op();
|
|
3162
|
+
if (!this._maskBounded(op)) return false;
|
|
3163
|
+
const rects = [];
|
|
3164
|
+
let x0 = Infinity;
|
|
3165
|
+
let y0 = Infinity;
|
|
3166
|
+
let x1 = -Infinity;
|
|
3167
|
+
let y1 = -Infinity;
|
|
3168
|
+
for (let i = 0; i < flat.length; i += 4) {
|
|
3169
|
+
const x = Math.trunc(flat[i]);
|
|
3170
|
+
const y = Math.trunc(flat[i + 1]);
|
|
3171
|
+
const w = Math.trunc(flat[i + 2]);
|
|
3172
|
+
const h = Math.trunc(flat[i + 3]);
|
|
3173
|
+
if (w <= 0 || h <= 0) continue;
|
|
3174
|
+
rects.push(x, y, w, h);
|
|
3175
|
+
if (x < x0) x0 = x;
|
|
3176
|
+
if (y < y0) y0 = y;
|
|
3177
|
+
if (x + w > x1) x1 = x + w;
|
|
3178
|
+
if (y + h > y1) y1 = y + h;
|
|
3179
|
+
}
|
|
3180
|
+
if (!disjointRects(rects)) return false;
|
|
3181
|
+
if (!prepareStyle(this._backgroundPicture, this._m)) return true;
|
|
3182
|
+
// the box they cover, on the surface: nothing outside it is sampled
|
|
3183
|
+
const bx = Math.max(0, x0);
|
|
3184
|
+
const by = Math.max(0, y0);
|
|
3185
|
+
const bw = Math.min(this.width, x1) - bx;
|
|
3186
|
+
const bh = Math.min(this.height, y1) - by;
|
|
3187
|
+
if (!(bw > 0 && bh > 0)) return true;
|
|
3188
|
+
this._ensureFillMask();
|
|
3189
|
+
const mask = this.fillMask.id;
|
|
3190
|
+
R.FillRectangles(R.PictOp.Src, mask, [0, 0, 0, 0], [bx, by, bw, bh]);
|
|
3191
|
+
// stay under the server's maximum request size
|
|
3192
|
+
const chunk = 10000 * 4;
|
|
3193
|
+
for (let i = 0; i < rects.length; i += chunk) {
|
|
3194
|
+
R.FillRectangles(
|
|
3195
|
+
R.PictOp.Src,
|
|
3196
|
+
mask,
|
|
3197
|
+
[0, 0, 0, this.globalAlpha < 1 ? this.globalAlpha : 1],
|
|
3198
|
+
i === 0 && rects.length <= chunk ? rects : rects.slice(i, i + chunk),
|
|
3199
|
+
);
|
|
3200
|
+
}
|
|
3201
|
+
R.Composite(
|
|
3202
|
+
R.PictOp.In,
|
|
3203
|
+
this._requireClipMask().id,
|
|
3204
|
+
0,
|
|
3205
|
+
mask,
|
|
3206
|
+
bx,
|
|
3207
|
+
by,
|
|
3208
|
+
0,
|
|
3209
|
+
0,
|
|
3210
|
+
bx,
|
|
3211
|
+
by,
|
|
3212
|
+
bw,
|
|
3213
|
+
bh,
|
|
3214
|
+
);
|
|
3215
|
+
R.Composite(
|
|
3216
|
+
op,
|
|
3217
|
+
this._backgroundPicture.id,
|
|
3218
|
+
mask,
|
|
3219
|
+
this._dst(),
|
|
3220
|
+
bx,
|
|
3221
|
+
by,
|
|
3222
|
+
bx,
|
|
3223
|
+
by,
|
|
3224
|
+
bx,
|
|
3225
|
+
by,
|
|
3226
|
+
bw,
|
|
3227
|
+
bh,
|
|
3228
|
+
);
|
|
3229
|
+
this._markDirty();
|
|
3230
|
+
return true;
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3076
3233
|
strokeRect(x, y, w, h) {
|
|
3077
3234
|
if (this._shadowed()) this._shadowOfRect(x, y, w, h, true);
|
|
3078
3235
|
const m = this._m;
|
|
@@ -3531,6 +3688,40 @@ class RenderingContext2d {
|
|
|
3531
3688
|
return { x, y, w, h };
|
|
3532
3689
|
}
|
|
3533
3690
|
|
|
3691
|
+
/**
|
|
3692
|
+
* The box the clip stack lets anything through, in device pixels on the
|
|
3693
|
+
* surface: the intersection of its rectangles and of its paths' bounding
|
|
3694
|
+
* boxes. Null when the stack holds no shape — region entries are the
|
|
3695
|
+
* picture's, as in `_clipRect` — and an empty box when it holds a path
|
|
3696
|
+
* that lands nowhere. What a drawing through the a8 mask is bounded to
|
|
3697
|
+
* (issue #372). Cached against the stack, which is replaced on every
|
|
3698
|
+
* change rather than edited.
|
|
3699
|
+
*/
|
|
3700
|
+
_clipExtents() {
|
|
3701
|
+
if (this._extentsOf === this._clips) return this._extents;
|
|
3702
|
+
let out = null;
|
|
3703
|
+
for (const entry of this._clips) {
|
|
3704
|
+
if (entry.region) continue;
|
|
3705
|
+
const box = entry.rect ?? (entry.bbox ??= this._polysBBox(entry.polys));
|
|
3706
|
+
const next = box && (out ? intersectBox(out, box) : box);
|
|
3707
|
+
if (!next) {
|
|
3708
|
+
out = { x: 0, y: 0, w: 0, h: 0 };
|
|
3709
|
+
break;
|
|
3710
|
+
}
|
|
3711
|
+
out = next;
|
|
3712
|
+
}
|
|
3713
|
+
if (out) {
|
|
3714
|
+
const x = Math.max(0, Math.min(out.x, this.width));
|
|
3715
|
+
const y = Math.max(0, Math.min(out.y, this.height));
|
|
3716
|
+
const w = Math.max(0, Math.min(out.x + out.w, this.width) - x);
|
|
3717
|
+
const h = Math.max(0, Math.min(out.y + out.h, this.height) - y);
|
|
3718
|
+
out = { x, y, w, h };
|
|
3719
|
+
}
|
|
3720
|
+
this._extentsOf = this._clips;
|
|
3721
|
+
this._extents = out;
|
|
3722
|
+
return out;
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3534
3725
|
/**
|
|
3535
3726
|
* Replace the clip stack and re-derive what kinds of entry are in it.
|
|
3536
3727
|
*
|
package/lib/text/glyphs.js
CHANGED
|
@@ -571,8 +571,11 @@ export function routeGlyphSize(app, font, size, policy = policyOf(app), textRend
|
|
|
571
571
|
* @param {number} srcId source picture (fill color/gradient)
|
|
572
572
|
* @param {number} dstId destination picture
|
|
573
573
|
* @param {Array<{run, x, y}>} positioned visual-order runs with baseline origins
|
|
574
|
+
* @param {{x: number, y: number, w: number, h: number}} [bounds] the part of
|
|
575
|
+
* the destination that can show anything — the clip, or the surface. A
|
|
576
|
+
* glyph wholly outside it is not sent (`visibleGlyph`).
|
|
574
577
|
*/
|
|
575
|
-
export function drawGlyphRuns(app, op, srcId, dstId, positioned) {
|
|
578
|
+
export function drawGlyphRuns(app, op, srcId, dstId, positioned, bounds = null) {
|
|
576
579
|
const policy = policyOf(app);
|
|
577
580
|
let bitmap = positioned;
|
|
578
581
|
let vector = null;
|
|
@@ -590,11 +593,35 @@ export function drawGlyphRuns(app, op, srcId, dstId, positioned) {
|
|
|
590
593
|
bitmap.push(positioned[i]);
|
|
591
594
|
}
|
|
592
595
|
}
|
|
593
|
-
if (bitmap.length) drawBitmapGlyphRuns(app, op, srcId, dstId, bitmap, policy);
|
|
594
|
-
if (vector) drawVectorGlyphRuns(app, op, srcId, dstId, vector);
|
|
596
|
+
if (bitmap.length) drawBitmapGlyphRuns(app, op, srcId, dstId, bitmap, policy, bounds);
|
|
597
|
+
if (vector) drawVectorGlyphRuns(app, op, srcId, dstId, vector, bounds);
|
|
595
598
|
}
|
|
596
599
|
|
|
597
|
-
|
|
600
|
+
/**
|
|
601
|
+
* Whether a glyph whose origin is at (x, y) can put any ink inside `bounds`,
|
|
602
|
+
* give or take two ems — no glyph reaches further from its origin than that,
|
|
603
|
+
* swashes and all.
|
|
604
|
+
*
|
|
605
|
+
* Not only a saving: the glyphs of a line wider than 32767 device pixels —
|
|
606
|
+
* a minified file in a code editor, a long label in a narrow cell — have
|
|
607
|
+
* origins no request can carry. CompositeGlyphs places its first glyph and
|
|
608
|
+
* every element after it in 16-bit coordinates, and the vector path's trap
|
|
609
|
+
* mask would span the whole line, so drawing such a line threw a RangeError
|
|
610
|
+
* out of the paint however little of it was on screen. What is left after
|
|
611
|
+
* the cull sits inside a surface, and every coordinate in it fits.
|
|
612
|
+
*/
|
|
613
|
+
function visibleGlyph(bounds, x, y, size) {
|
|
614
|
+
if (bounds === null) return true;
|
|
615
|
+
const reach = size * 2;
|
|
616
|
+
return (
|
|
617
|
+
x + reach > bounds.x &&
|
|
618
|
+
x - reach < bounds.x + bounds.w &&
|
|
619
|
+
y + reach > bounds.y &&
|
|
620
|
+
y - reach < bounds.y + bounds.h
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function drawBitmapGlyphRuns(app, op, srcId, dstId, positioned, policy, bounds) {
|
|
598
625
|
const Render = app.display.Render;
|
|
599
626
|
const items = [];
|
|
600
627
|
let bits = 8;
|
|
@@ -608,6 +635,7 @@ function drawBitmapGlyphRuns(app, op, srcId, dstId, positioned, policy) {
|
|
|
608
635
|
if (page.bits > bits) bits = page.bits;
|
|
609
636
|
}
|
|
610
637
|
for (const pos of positionGlyphs(positioned)) {
|
|
638
|
+
if (!visibleGlyph(bounds, pos.x, pos.y, pos.run.size)) continue;
|
|
611
639
|
const page = pages.get(pos.run);
|
|
612
640
|
const e = page.entry(pos.glyph.id);
|
|
613
641
|
items.push({ gs: e.gs, lid: e.lid, adv: e.adv, x: pos.x, y: pos.y });
|
|
@@ -646,7 +674,7 @@ const MAX_TRAPS_PER_REQUEST = 6000;
|
|
|
646
674
|
* Positions are intentionally NOT rounded to whole pixels — fractional
|
|
647
675
|
* advances and origins keep zoom animations smooth.
|
|
648
676
|
*/
|
|
649
|
-
function drawVectorGlyphRuns(app, op, srcId, dstId, positioned) {
|
|
677
|
+
function drawVectorGlyphRuns(app, op, srcId, dstId, positioned, bounds = null) {
|
|
650
678
|
// unrounded glyph origins + ink bounding box
|
|
651
679
|
const placed = [];
|
|
652
680
|
let minX = Infinity;
|
|
@@ -657,6 +685,7 @@ function drawVectorGlyphRuns(app, op, srcId, dstId, positioned) {
|
|
|
657
685
|
const gx = cursor + g.dx;
|
|
658
686
|
const gy = y - g.dy;
|
|
659
687
|
cursor += g.ax;
|
|
688
|
+
if (!visibleGlyph(bounds, gx, gy, run.size)) continue;
|
|
660
689
|
const e = run.font.glyphExtents(g.id, run.size);
|
|
661
690
|
if (!Number.isFinite(e.minX) || e.maxX <= e.minX) continue; // blank glyph
|
|
662
691
|
placed.push({ run, glyph: g, x: gx, y: gy });
|