ntk 8.12.0 → 8.12.1
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 +221 -38
- 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
|
}
|
|
@@ -3009,9 +3058,12 @@ class RenderingContext2d {
|
|
|
3009
3058
|
* A solid-colour fillStyle under an identity transform and a rectangular
|
|
3010
3059
|
* (or absent) clip compiles the whole list into a single
|
|
3011
3060
|
* `Render.FillRectangles`, where N `fillRect` calls cost N composites.
|
|
3012
|
-
*
|
|
3013
|
-
*
|
|
3014
|
-
*
|
|
3061
|
+
* Under a clip that is not a rectangle, rectangles that do not overlap
|
|
3062
|
+
* still cost three requests however many there are
|
|
3063
|
+
* (`_fillRectsThroughClip`). Gradient/Picture styles, transforms, and
|
|
3064
|
+
* overlapping rectangles or `copy` under a non-rectangular clip fall back
|
|
3065
|
+
* to that `fillRect` loop, so the answer is always right and only the
|
|
3066
|
+
* request count varies.
|
|
3015
3067
|
*/
|
|
3016
3068
|
fillRects(rects) {
|
|
3017
3069
|
if (!rects || !rects.length || this.globalAlpha <= 0) return;
|
|
@@ -3045,12 +3097,13 @@ class RenderingContext2d {
|
|
|
3045
3097
|
if (
|
|
3046
3098
|
!matIsIdentity(this._m) ||
|
|
3047
3099
|
!isPlainColor(this._fillStyle) ||
|
|
3048
|
-
this._hasPolyClip
|
|
3100
|
+
(this._hasPolyClip && !this._fillRectsThroughClip(flat))
|
|
3049
3101
|
) {
|
|
3050
3102
|
for (let i = 0; i < flat.length; i += 4)
|
|
3051
3103
|
this._fillRect(flat[i], flat[i + 1], flat[i + 2], flat[i + 3]);
|
|
3052
3104
|
return;
|
|
3053
3105
|
}
|
|
3106
|
+
if (this._hasPolyClip) return;
|
|
3054
3107
|
if (clip && (clip.w === 0 || clip.h === 0)) return; // clipped away
|
|
3055
3108
|
const R = this.Render;
|
|
3056
3109
|
if (clip) this._setPictureClip(clip);
|
|
@@ -3073,6 +3126,102 @@ class RenderingContext2d {
|
|
|
3073
3126
|
this._markDirty();
|
|
3074
3127
|
}
|
|
3075
3128
|
|
|
3129
|
+
/**
|
|
3130
|
+
* `fillRects` under a clip that is not a rectangle — a rounded card, a
|
|
3131
|
+
* path: every rectangle's coverage into the scratch a8 mask with one
|
|
3132
|
+
* `FillRectangles`, `globalAlpha` and the clip applied to it once each,
|
|
3133
|
+
* and the style composited through it once. One rectangle at a time is a
|
|
3134
|
+
* composite through the clip each, and a chart's marks inside a rounded
|
|
3135
|
+
* box a zoom had put off the pixel grid were sixty thousand of them a
|
|
3136
|
+
* frame (issue #374).
|
|
3137
|
+
*
|
|
3138
|
+
* Only where that is the loop's picture. The rectangles must not overlap:
|
|
3139
|
+
* the loop blends a pixel once per rectangle over it, at the clip's
|
|
3140
|
+
* coverage each time, so where two overlap on the clip's antialiased edge
|
|
3141
|
+
* it is darker than one blend through the mask — and where they do not,
|
|
3142
|
+
* every pixel is blended once either way, whatever the colour's alpha.
|
|
3143
|
+
* And the operator must leave the destination alone where the mask is
|
|
3144
|
+
* zero: `copy` clears every pixel of its box the mask misses, the gaps
|
|
3145
|
+
* between the rectangles here. The rectangles reach the mask as the wire
|
|
3146
|
+
* carries every box, in whole pixels truncated toward zero, which is what
|
|
3147
|
+
* `_fillRect`'s composites are cut to as well.
|
|
3148
|
+
*
|
|
3149
|
+
* @returns {boolean} whether the rectangles were drawn here
|
|
3150
|
+
*/
|
|
3151
|
+
_fillRectsThroughClip(flat) {
|
|
3152
|
+
const R = this.Render;
|
|
3153
|
+
const op = this._op();
|
|
3154
|
+
if (!this._maskBounded(op)) return false;
|
|
3155
|
+
const rects = [];
|
|
3156
|
+
let x0 = Infinity;
|
|
3157
|
+
let y0 = Infinity;
|
|
3158
|
+
let x1 = -Infinity;
|
|
3159
|
+
let y1 = -Infinity;
|
|
3160
|
+
for (let i = 0; i < flat.length; i += 4) {
|
|
3161
|
+
const x = Math.trunc(flat[i]);
|
|
3162
|
+
const y = Math.trunc(flat[i + 1]);
|
|
3163
|
+
const w = Math.trunc(flat[i + 2]);
|
|
3164
|
+
const h = Math.trunc(flat[i + 3]);
|
|
3165
|
+
if (w <= 0 || h <= 0) continue;
|
|
3166
|
+
rects.push(x, y, w, h);
|
|
3167
|
+
if (x < x0) x0 = x;
|
|
3168
|
+
if (y < y0) y0 = y;
|
|
3169
|
+
if (x + w > x1) x1 = x + w;
|
|
3170
|
+
if (y + h > y1) y1 = y + h;
|
|
3171
|
+
}
|
|
3172
|
+
if (!disjointRects(rects)) return false;
|
|
3173
|
+
if (!prepareStyle(this._backgroundPicture, this._m)) return true;
|
|
3174
|
+
// the box they cover, on the surface: nothing outside it is sampled
|
|
3175
|
+
const bx = Math.max(0, x0);
|
|
3176
|
+
const by = Math.max(0, y0);
|
|
3177
|
+
const bw = Math.min(this.width, x1) - bx;
|
|
3178
|
+
const bh = Math.min(this.height, y1) - by;
|
|
3179
|
+
if (!(bw > 0 && bh > 0)) return true;
|
|
3180
|
+
this._ensureFillMask();
|
|
3181
|
+
const mask = this.fillMask.id;
|
|
3182
|
+
R.FillRectangles(R.PictOp.Src, mask, [0, 0, 0, 0], [bx, by, bw, bh]);
|
|
3183
|
+
// stay under the server's maximum request size
|
|
3184
|
+
const chunk = 10000 * 4;
|
|
3185
|
+
for (let i = 0; i < rects.length; i += chunk) {
|
|
3186
|
+
R.FillRectangles(
|
|
3187
|
+
R.PictOp.Src,
|
|
3188
|
+
mask,
|
|
3189
|
+
[0, 0, 0, this.globalAlpha < 1 ? this.globalAlpha : 1],
|
|
3190
|
+
i === 0 && rects.length <= chunk ? rects : rects.slice(i, i + chunk),
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
R.Composite(
|
|
3194
|
+
R.PictOp.In,
|
|
3195
|
+
this._requireClipMask().id,
|
|
3196
|
+
0,
|
|
3197
|
+
mask,
|
|
3198
|
+
bx,
|
|
3199
|
+
by,
|
|
3200
|
+
0,
|
|
3201
|
+
0,
|
|
3202
|
+
bx,
|
|
3203
|
+
by,
|
|
3204
|
+
bw,
|
|
3205
|
+
bh,
|
|
3206
|
+
);
|
|
3207
|
+
R.Composite(
|
|
3208
|
+
op,
|
|
3209
|
+
this._backgroundPicture.id,
|
|
3210
|
+
mask,
|
|
3211
|
+
this._dst(),
|
|
3212
|
+
bx,
|
|
3213
|
+
by,
|
|
3214
|
+
bx,
|
|
3215
|
+
by,
|
|
3216
|
+
bx,
|
|
3217
|
+
by,
|
|
3218
|
+
bw,
|
|
3219
|
+
bh,
|
|
3220
|
+
);
|
|
3221
|
+
this._markDirty();
|
|
3222
|
+
return true;
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3076
3225
|
strokeRect(x, y, w, h) {
|
|
3077
3226
|
if (this._shadowed()) this._shadowOfRect(x, y, w, h, true);
|
|
3078
3227
|
const m = this._m;
|
|
@@ -3531,6 +3680,40 @@ class RenderingContext2d {
|
|
|
3531
3680
|
return { x, y, w, h };
|
|
3532
3681
|
}
|
|
3533
3682
|
|
|
3683
|
+
/**
|
|
3684
|
+
* The box the clip stack lets anything through, in device pixels on the
|
|
3685
|
+
* surface: the intersection of its rectangles and of its paths' bounding
|
|
3686
|
+
* boxes. Null when the stack holds no shape — region entries are the
|
|
3687
|
+
* picture's, as in `_clipRect` — and an empty box when it holds a path
|
|
3688
|
+
* that lands nowhere. What a drawing through the a8 mask is bounded to
|
|
3689
|
+
* (issue #372). Cached against the stack, which is replaced on every
|
|
3690
|
+
* change rather than edited.
|
|
3691
|
+
*/
|
|
3692
|
+
_clipExtents() {
|
|
3693
|
+
if (this._extentsOf === this._clips) return this._extents;
|
|
3694
|
+
let out = null;
|
|
3695
|
+
for (const entry of this._clips) {
|
|
3696
|
+
if (entry.region) continue;
|
|
3697
|
+
const box = entry.rect ?? (entry.bbox ??= this._polysBBox(entry.polys));
|
|
3698
|
+
const next = box && (out ? intersectBox(out, box) : box);
|
|
3699
|
+
if (!next) {
|
|
3700
|
+
out = { x: 0, y: 0, w: 0, h: 0 };
|
|
3701
|
+
break;
|
|
3702
|
+
}
|
|
3703
|
+
out = next;
|
|
3704
|
+
}
|
|
3705
|
+
if (out) {
|
|
3706
|
+
const x = Math.max(0, Math.min(out.x, this.width));
|
|
3707
|
+
const y = Math.max(0, Math.min(out.y, this.height));
|
|
3708
|
+
const w = Math.max(0, Math.min(out.x + out.w, this.width) - x);
|
|
3709
|
+
const h = Math.max(0, Math.min(out.y + out.h, this.height) - y);
|
|
3710
|
+
out = { x, y, w, h };
|
|
3711
|
+
}
|
|
3712
|
+
this._extentsOf = this._clips;
|
|
3713
|
+
this._extents = out;
|
|
3714
|
+
return out;
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3534
3717
|
/**
|
|
3535
3718
|
* Replace the clip stack and re-derive what kinds of entry are in it.
|
|
3536
3719
|
*
|