ntk 7.6.1 → 8.0.0
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/README.md +9 -10
- package/lib/image.js +12 -0
- package/lib/index.js +9 -30
- package/lib/maskcluster.js +191 -0
- package/lib/renderingcontext_2d.js +590 -175
- package/lib/text/layout.js +4 -4
- package/lib/widgets/svgview.js +5 -4
- package/package.json +4 -9
- package/lib/widgets/css.js +0 -673
- package/lib/widgets/highlight.js +0 -97
- package/lib/widgets/htmlview.js +0 -893
- package/lib/widgets/markdown.js +0 -148
- package/lib/widgets/markdownview.js +0 -492
- package/lib/widgets/tex.js +0 -679
- package/lib/yoga.js +0 -119
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
pixelLayout,
|
|
12
12
|
toStraightRgba,
|
|
13
13
|
} from "./imagedata.js";
|
|
14
|
+
import { clusterBoxes, maskPolicyOf, unionBox } from "./maskcluster.js";
|
|
14
15
|
import {
|
|
15
16
|
Path2D,
|
|
16
17
|
arcSegmentCount,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
import Picture from "./picture.js";
|
|
27
28
|
import Pixmap from "./pixmap.js";
|
|
28
29
|
import { routeRaster } from "./rasterize.js";
|
|
30
|
+
import { Surface } from "./surface.js";
|
|
29
31
|
import {
|
|
30
32
|
BL,
|
|
31
33
|
BR,
|
|
@@ -48,6 +50,22 @@ import { TextLayout } from "./text/layout.js";
|
|
|
48
50
|
import { reorderRuns } from "./text/shape.js";
|
|
49
51
|
import { trapezoidize } from "./trapezoid.js";
|
|
50
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The subpaths of a flattened path that can enclose area, as the flat
|
|
55
|
+
* `[x0, y0, …]` lists both the local rasterizer and the trapezoidizer take,
|
|
56
|
+
* with the edge count the routing policy asks for.
|
|
57
|
+
*/
|
|
58
|
+
function fillableShapes(polys) {
|
|
59
|
+
const shapes = [];
|
|
60
|
+
let edges = 0;
|
|
61
|
+
for (const p of polys) {
|
|
62
|
+
if (p.pts.length < 6) continue;
|
|
63
|
+
shapes.push(p.pts);
|
|
64
|
+
edges += p.pts.length / 2;
|
|
65
|
+
}
|
|
66
|
+
return { shapes, edges };
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
/** The overlap of two {x, y, w, h} boxes, or null when they have none. */
|
|
52
70
|
function intersectBox(a, b) {
|
|
53
71
|
const x = Math.max(a.x, b.x);
|
|
@@ -103,12 +121,92 @@ const gcRegistry = new FinalizationRegistry(({ X, gcs }) => {
|
|
|
103
121
|
* the type. A `RenderingContext2d` deliberately does not match: its `picture`
|
|
104
122
|
* is a property rather than a method, and it keeps its own branch below.
|
|
105
123
|
*/
|
|
106
|
-
/** a fillStyle with a single colour behind it, as opposed to a gradient
|
|
107
|
-
* caller-supplied Picture */
|
|
124
|
+
/** a fillStyle with a single colour behind it, as opposed to a gradient, a
|
|
125
|
+
* pattern or a caller-supplied Picture */
|
|
108
126
|
function isPlainColor(style) {
|
|
109
127
|
return typeof style === "string" || Array.isArray(style);
|
|
110
128
|
}
|
|
111
129
|
|
|
130
|
+
// `createPattern` repetitions -> XRender Repeat modes. The canvas spec's
|
|
131
|
+
// per-axis 'repeat-x'/'repeat-y' have no mode here (see createPattern);
|
|
132
|
+
// 'pad' and 'reflect' are the two XRender modes the spec has no name for.
|
|
133
|
+
const REPEAT_MODES = {
|
|
134
|
+
repeat: 1, // Repeat.Normal
|
|
135
|
+
"no-repeat": 0, // Repeat.None
|
|
136
|
+
pad: 2, // clamp to the edge pixels
|
|
137
|
+
reflect: 3, // mirror every other tile
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const PATTERN_DOCS =
|
|
141
|
+
"https://github.com/sidorares/ntk/blob/master/docs/context-2d.md#patterns";
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* What a pattern tiles: a drawable holding the tile plus the picture format
|
|
145
|
+
* to read it through. A repeating source Picture is created over it rather
|
|
146
|
+
* than the source's own picture being changed, so tiling a `Surface` leaves
|
|
147
|
+
* `drawImage` of that same surface exactly as it was.
|
|
148
|
+
*/
|
|
149
|
+
function patternSourceOf(app, source) {
|
|
150
|
+
const Render = app.display.Render;
|
|
151
|
+
const formatFor = (depth) =>
|
|
152
|
+
depth === 32 ? Render.rgba32 : depth === 8 ? "a8" : Render.rgb24;
|
|
153
|
+
|
|
154
|
+
let drawable = null;
|
|
155
|
+
let width;
|
|
156
|
+
let height;
|
|
157
|
+
let format;
|
|
158
|
+
if (source instanceof Surface) {
|
|
159
|
+
if (source.app !== app) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
"createPattern: the Surface belongs to a different X connection",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
drawable = source.pixmap;
|
|
165
|
+
format = source.format === "a8" ? "a8" : Render.rgba32;
|
|
166
|
+
({ width, height } = source);
|
|
167
|
+
} else if (source instanceof Image) {
|
|
168
|
+
drawable = source.pixmap(app);
|
|
169
|
+
format = Render.rgba32;
|
|
170
|
+
({ width, height } = source);
|
|
171
|
+
} else if (source && typeof source.id === "number") {
|
|
172
|
+
// a Drawable: a Pixmap, or a Window (through its backing pixmap, which
|
|
173
|
+
// is where a double-buffered window's current pixels actually are)
|
|
174
|
+
drawable = source._backing || source;
|
|
175
|
+
const depth = drawable.depth ?? source.depth;
|
|
176
|
+
if (!depth) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
"createPattern: the drawable's depth is not known yet — await the window's geometry, or pass a Surface",
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
format = formatFor(depth);
|
|
182
|
+
width = drawable.width ?? source.width;
|
|
183
|
+
height = drawable.height ?? source.height;
|
|
184
|
+
} else {
|
|
185
|
+
throw new Error(
|
|
186
|
+
"createPattern: expected a Surface, an Image, a Pixmap or a Window as the tile, got " +
|
|
187
|
+
(source === null ? "null" : typeof source),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if (format === "a8") {
|
|
191
|
+
throw new Error(
|
|
192
|
+
"createPattern: a coverage (a8) tile has no colour to paint with — XRender would " +
|
|
193
|
+
"sample it as black. Draw the tile into an argb32 Surface and tile that, or keep " +
|
|
194
|
+
`the a8 one and use ctx.drawImage, which paints it in the current fillStyle. ${PATTERN_DOCS}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return { drawable, format, width, height };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Point a pattern's picture transform at the transform in force for this
|
|
202
|
+
* fill — patterns are painted in user space, so the CTM is part of the
|
|
203
|
+
* mapping. Returns false when nothing would be painted (a singular matrix),
|
|
204
|
+
* true for anything else, including every non-pattern style.
|
|
205
|
+
*/
|
|
206
|
+
function preparePattern(src, m) {
|
|
207
|
+
return src instanceof CanvasPattern ? src._sync(m) : true;
|
|
208
|
+
}
|
|
209
|
+
|
|
112
210
|
function isPictureSource(image) {
|
|
113
211
|
return (
|
|
114
212
|
image instanceof Image ||
|
|
@@ -136,6 +234,30 @@ const GCO_TO_PICTOP = {
|
|
|
136
234
|
lighter: "Add",
|
|
137
235
|
};
|
|
138
236
|
|
|
237
|
+
// The ops whose result is the destination wherever the mask is zero. Only
|
|
238
|
+
// those may have a drawing's mask split into several boxes (maskcluster.js):
|
|
239
|
+
// what the split gives up is the gaps between the boxes, and for these ops
|
|
240
|
+
// the single-box version would not have changed those pixels either. `copy`,
|
|
241
|
+
// `source-in`, `destination-in`, `source-out` and `destination-atop` write
|
|
242
|
+
// the source — or nothing — across the whole box, so they keep one.
|
|
243
|
+
const MASK_BOUNDED_OPS = [
|
|
244
|
+
"Over",
|
|
245
|
+
"OverReverse",
|
|
246
|
+
"OutReverse",
|
|
247
|
+
"Atop",
|
|
248
|
+
"Xor",
|
|
249
|
+
"Add",
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
// and what they are clustered with instead: one mask, exactly as before
|
|
253
|
+
const ONE_MASK = { minSaving: Infinity, maxMasks: 1 };
|
|
254
|
+
|
|
255
|
+
// How far apart two of a stroke's triangles have to be to start a new piece
|
|
256
|
+
// (_trisPieces). Two mask boxes a pixel or two apart are not worth
|
|
257
|
+
// splitting — each carries a pixel of antialiasing slack of its own — so
|
|
258
|
+
// this only has to be small next to anything `minSaving` would pay for.
|
|
259
|
+
const PIECE_SLACK = 2;
|
|
260
|
+
|
|
139
261
|
// extrude-polyline has no round caps/joins: 'round' extrudes as butt/bevel
|
|
140
262
|
// and the missing coverage is unioned in afterwards as triangle-fan disks
|
|
141
263
|
// (see _strokePolys)
|
|
@@ -284,6 +406,12 @@ class RenderingContext2d {
|
|
|
284
406
|
// a silent perf cliff, so hits and misses-by-reason are always counted.
|
|
285
407
|
// NTK_DEBUG_SHAPES=1 prints the process-wide aggregate at exit.
|
|
286
408
|
this.shapeStats = { hits: 0, misses: {} };
|
|
409
|
+
// Mask cost, for the same reason (issue #264): a drawing whose pieces
|
|
410
|
+
// are scattered pays for the box around all of them unless the mask is
|
|
411
|
+
// split, and neither the pixels nor the split show up anywhere else.
|
|
412
|
+
// `masks` counts mask passes, `pixels` their total area, `split` the
|
|
413
|
+
// drawings that took more than one.
|
|
414
|
+
this.maskStats = { masks: 0, pixels: 0, split: 0 };
|
|
287
415
|
this._stack = [];
|
|
288
416
|
this._clips = []; // [{ polys, rule }] in device space, already stacked
|
|
289
417
|
this._gco = "source-over";
|
|
@@ -458,7 +586,11 @@ class RenderingContext2d {
|
|
|
458
586
|
const c = parseColor(value);
|
|
459
587
|
return this.createSolidPicture(c[0], c[1], c[2], c[3]);
|
|
460
588
|
}
|
|
461
|
-
if (
|
|
589
|
+
if (
|
|
590
|
+
value instanceof Picture ||
|
|
591
|
+
value instanceof CanvasGradient ||
|
|
592
|
+
value instanceof CanvasPattern
|
|
593
|
+
) {
|
|
462
594
|
return value;
|
|
463
595
|
}
|
|
464
596
|
throw new Error("Unknown fill style");
|
|
@@ -763,11 +895,9 @@ class RenderingContext2d {
|
|
|
763
895
|
});
|
|
764
896
|
}
|
|
765
897
|
|
|
766
|
-
_rasterizePolys(picture,
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
if (!flat.length) return;
|
|
770
|
-
const traps = trapezoidize(flat, dx, dy, [], rule);
|
|
898
|
+
_rasterizePolys(picture, shapes, rule, dx = 0, dy = 0) {
|
|
899
|
+
if (!shapes.length) return;
|
|
900
|
+
const traps = trapezoidize(shapes, dx, dy, [], rule);
|
|
771
901
|
// stay under the server's maximum request size
|
|
772
902
|
const chunk = 4000 * 6;
|
|
773
903
|
for (let i = 0; i < traps.length; i += chunk) {
|
|
@@ -840,46 +970,34 @@ class RenderingContext2d {
|
|
|
840
970
|
}
|
|
841
971
|
|
|
842
972
|
/**
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
|
|
846
|
-
/**
|
|
847
|
-
* Device-space bounding box of flattened polys, with a pixel of slack for
|
|
848
|
-
* the antialiased edge, clamped to the surface. Null when nothing lands
|
|
849
|
-
* on it.
|
|
973
|
+
* Device-space bounding box of one flat `[x0, y0, …]` point list, with a
|
|
974
|
+
* pixel of slack for the antialiased edge, clamped to the surface. Null
|
|
975
|
+
* when nothing lands on it.
|
|
850
976
|
*/
|
|
851
|
-
|
|
977
|
+
_pointsBBox(pts) {
|
|
852
978
|
let minX = Infinity;
|
|
853
979
|
let minY = Infinity;
|
|
854
980
|
let maxX = -Infinity;
|
|
855
981
|
let maxY = -Infinity;
|
|
856
|
-
for (
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
if (pts[i + 1] < minY) minY = pts[i + 1];
|
|
862
|
-
if (pts[i + 1] > maxY) maxY = pts[i + 1];
|
|
863
|
-
}
|
|
982
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
983
|
+
if (pts[i] < minX) minX = pts[i];
|
|
984
|
+
if (pts[i] > maxX) maxX = pts[i];
|
|
985
|
+
if (pts[i + 1] < minY) minY = pts[i + 1];
|
|
986
|
+
if (pts[i + 1] > maxY) maxY = pts[i + 1];
|
|
864
987
|
}
|
|
865
988
|
if (maxX === -Infinity) return null;
|
|
866
989
|
return this._clampBBox(minX, minY, maxX, maxY);
|
|
867
990
|
}
|
|
868
991
|
|
|
869
|
-
/** the same
|
|
870
|
-
|
|
871
|
-
let
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
if (tris[i] < minX) minX = tris[i];
|
|
877
|
-
if (tris[i] > maxX) maxX = tris[i];
|
|
878
|
-
if (tris[i + 1] < minY) minY = tris[i + 1];
|
|
879
|
-
if (tris[i + 1] > maxY) maxY = tris[i + 1];
|
|
992
|
+
/** the same over every subpath of a flattened path, as one box */
|
|
993
|
+
_polysBBox(polys) {
|
|
994
|
+
let out = null;
|
|
995
|
+
for (const poly of polys) {
|
|
996
|
+
const b = this._pointsBBox(poly.pts);
|
|
997
|
+
if (!b) continue;
|
|
998
|
+
out = out ? unionBox(out, b) : b;
|
|
880
999
|
}
|
|
881
|
-
|
|
882
|
-
return this._clampBBox(minX, minY, maxX, maxY);
|
|
1000
|
+
return out;
|
|
883
1001
|
}
|
|
884
1002
|
|
|
885
1003
|
// a pixel of slack for the antialiased edge, clamped to the surface
|
|
@@ -892,57 +1010,74 @@ class RenderingContext2d {
|
|
|
892
1010
|
return { x, y, w, h };
|
|
893
1011
|
}
|
|
894
1012
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1013
|
+
/**
|
|
1014
|
+
* The mask boxes one drawing's pieces are painted through: one per cluster
|
|
1015
|
+
* of them, whose union is the drawing's bounding box.
|
|
1016
|
+
*
|
|
1017
|
+
* Everything the mask does is bounded to those boxes rather than to the
|
|
1018
|
+
* whole surface. On the wire it makes no difference — a Composite request
|
|
1019
|
+
* is the same size either way — but it is the difference between the
|
|
1020
|
+
* server touching a 34x34 box and a 400x400 one per fill. Where the pieces
|
|
1021
|
+
* are *scattered*, their union is a poor bound in the same way, and
|
|
1022
|
+
* `clusterBoxes` cuts it into the few boxes the ink is actually in (see
|
|
1023
|
+
* maskcluster.js). Stale mask content outside a box is never composited,
|
|
1024
|
+
* so clearing only the boxes is safe.
|
|
1025
|
+
*/
|
|
1026
|
+
_maskClusters(pieces, op) {
|
|
1027
|
+
const clusters = clusterBoxes(
|
|
1028
|
+
pieces,
|
|
1029
|
+
this._maskBounded(op) ? maskPolicyOf(this.window.app) : ONE_MASK,
|
|
1030
|
+
);
|
|
1031
|
+
if (clusters.length > 1) this.maskStats.split++;
|
|
1032
|
+
return clusters;
|
|
1033
|
+
}
|
|
911
1034
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
}
|
|
920
|
-
if (
|
|
921
|
-
!this._uploadCoverage({ polys: flat, rule, dx: -b.x, dy: -b.y }, b, edges)
|
|
922
|
-
) {
|
|
923
|
-
R.FillRectangles(
|
|
924
|
-
R.PictOp.Src,
|
|
925
|
-
this.fillMask.id,
|
|
926
|
-
[0, 0, 0, 0],
|
|
927
|
-
[b.x, b.y, b.w, b.h],
|
|
928
|
-
);
|
|
929
|
-
this._rasterizePolys(this.fillMask, polys, rule);
|
|
930
|
-
}
|
|
1035
|
+
/** whether `op` leaves the destination alone where the mask is zero */
|
|
1036
|
+
_maskBounded(op) {
|
|
1037
|
+
this._boundedOps ??= new Set(
|
|
1038
|
+
MASK_BOUNDED_OPS.map((name) => this.Render.PictOp[name]),
|
|
1039
|
+
);
|
|
1040
|
+
return this._boundedOps.has(op);
|
|
1041
|
+
}
|
|
931
1042
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1043
|
+
/**
|
|
1044
|
+
* The tail every masked fill and stroke ends in: coverage for one box into
|
|
1045
|
+
* the scratch a8 mask, scaled by `alpha`, intersected with the clip, and
|
|
1046
|
+
* the source composited through it.
|
|
1047
|
+
*
|
|
1048
|
+
* `job` is what a Rasterizer takes minus the box (docs/context-2d.md) —
|
|
1049
|
+
* `{ polys, rule }` or `{ triangles }`; `server` rasterizes the same
|
|
1050
|
+
* geometry into the mask server-side, for when the local rasterizer is not
|
|
1051
|
+
* the cheaper route or declines.
|
|
1052
|
+
*
|
|
1053
|
+
* @returns {boolean} whether anything was composited
|
|
1054
|
+
*/
|
|
1055
|
+
_paintThroughMask(b, job, { edges, src, op, alpha, server }) {
|
|
1056
|
+
const R = this.Render;
|
|
1057
|
+
// A rectangular clip narrows where the coverage is *composited*; the
|
|
1058
|
+
// mask content outside it is stale by the same argument as outside `b`,
|
|
1059
|
+
// and a box the clip rejects outright needs no coverage at all. A stack
|
|
1060
|
+
// whose mask was dropped by restore() may still hold a poly — _clipRect
|
|
1061
|
+
// says null then, and the mask comes back on demand.
|
|
936
1062
|
let out = b;
|
|
937
1063
|
if (this._clips.length && !this.clipMask) {
|
|
938
1064
|
const cr = this._clipRect();
|
|
939
1065
|
if (cr) {
|
|
940
1066
|
out = intersectBox(b, cr);
|
|
941
|
-
if (!out) return;
|
|
1067
|
+
if (!out) return false;
|
|
942
1068
|
} else {
|
|
943
1069
|
this._requireClipMask();
|
|
944
1070
|
}
|
|
945
1071
|
}
|
|
1072
|
+
if (!this._uploadCoverage({ ...job, dx: -b.x, dy: -b.y }, b, edges)) {
|
|
1073
|
+
R.FillRectangles(
|
|
1074
|
+
R.PictOp.Src,
|
|
1075
|
+
this.fillMask.id,
|
|
1076
|
+
[0, 0, 0, 0],
|
|
1077
|
+
[b.x, b.y, b.w, b.h],
|
|
1078
|
+
);
|
|
1079
|
+
server();
|
|
1080
|
+
}
|
|
946
1081
|
if (alpha < 1) {
|
|
947
1082
|
// In with a constant color scales the a8 coverage by that alpha
|
|
948
1083
|
R.FillRectangles(
|
|
@@ -985,7 +1120,59 @@ class RenderingContext2d {
|
|
|
985
1120
|
out.w,
|
|
986
1121
|
out.h,
|
|
987
1122
|
);
|
|
988
|
-
this.
|
|
1123
|
+
this.maskStats.masks++;
|
|
1124
|
+
this.maskStats.pixels += out.w * out.h;
|
|
1125
|
+
return true;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Core fill: rasterize device-space polys into the scratch a8 mask,
|
|
1130
|
+
* scale by globalAlpha, intersect with the clip, composite the source.
|
|
1131
|
+
*/
|
|
1132
|
+
_fillPolys(polys, rule, { src = null, op = null, alpha = null } = {}) {
|
|
1133
|
+
if (!polys.length) return;
|
|
1134
|
+
src = src ?? this._backgroundPicture;
|
|
1135
|
+
op = op ?? this._op();
|
|
1136
|
+
alpha = alpha ?? this.globalAlpha;
|
|
1137
|
+
if (alpha <= 0) return;
|
|
1138
|
+
if (!preparePattern(src, this._m)) return;
|
|
1139
|
+
|
|
1140
|
+
// one box per subpath, so a path holding disjoint ones can be masked as
|
|
1141
|
+
// the pieces it is rather than as the box around all of them
|
|
1142
|
+
const shapes = [];
|
|
1143
|
+
const pieces = [];
|
|
1144
|
+
for (const p of polys) {
|
|
1145
|
+
if (p.pts.length < 6) continue;
|
|
1146
|
+
const b = this._pointsBBox(p.pts);
|
|
1147
|
+
if (!b) continue;
|
|
1148
|
+
shapes.push(p.pts);
|
|
1149
|
+
pieces.push(b);
|
|
1150
|
+
}
|
|
1151
|
+
if (!shapes.length) return;
|
|
1152
|
+
|
|
1153
|
+
this._ensureFillMask();
|
|
1154
|
+
let painted = false;
|
|
1155
|
+
for (const cluster of this._maskClusters(pieces, op)) {
|
|
1156
|
+
const flat = [];
|
|
1157
|
+
let edges = 0;
|
|
1158
|
+
for (const i of cluster.items) {
|
|
1159
|
+
flat.push(shapes[i]);
|
|
1160
|
+
edges += shapes[i].length / 2;
|
|
1161
|
+
}
|
|
1162
|
+
painted =
|
|
1163
|
+
this._paintThroughMask(
|
|
1164
|
+
cluster,
|
|
1165
|
+
{ polys: flat, rule },
|
|
1166
|
+
{
|
|
1167
|
+
edges,
|
|
1168
|
+
src,
|
|
1169
|
+
op,
|
|
1170
|
+
alpha,
|
|
1171
|
+
server: () => this._rasterizePolys(this.fillMask, flat, rule),
|
|
1172
|
+
},
|
|
1173
|
+
) || painted;
|
|
1174
|
+
}
|
|
1175
|
+
if (painted) this._markDirty();
|
|
989
1176
|
}
|
|
990
1177
|
|
|
991
1178
|
_strokePolys(polys, { src = null } = {}) {
|
|
@@ -1325,14 +1512,22 @@ class RenderingContext2d {
|
|
|
1325
1512
|
const chunk = 4000 * 6;
|
|
1326
1513
|
if (direct) {
|
|
1327
1514
|
for (let i = 0; i < tris.length; i += chunk) {
|
|
1515
|
+
const batch = tris.slice(i, i + chunk);
|
|
1516
|
+
// RENDER aligns the source with the *first triangle's first vertex*,
|
|
1517
|
+
// not with the destination: the source is sampled at
|
|
1518
|
+
// (srcX + x - floor(tris[0].x)). Passing that vertex back is what
|
|
1519
|
+
// makes source coordinates equal destination coordinates, the same
|
|
1520
|
+
// convention drawGlyphRuns and compositeTraps use — without it every
|
|
1521
|
+
// non-constant stroke style (a gradient, a pattern) is offset by
|
|
1522
|
+
// wherever the stroke happens to start, and shifts as it moves.
|
|
1328
1523
|
this.Render.Triangles(
|
|
1329
1524
|
op,
|
|
1330
1525
|
src.id,
|
|
1331
|
-
0,
|
|
1332
|
-
|
|
1526
|
+
Math.floor(batch[0]),
|
|
1527
|
+
Math.floor(batch[1]),
|
|
1333
1528
|
this.picture.id,
|
|
1334
1529
|
this.Render.a8,
|
|
1335
|
-
|
|
1530
|
+
batch,
|
|
1336
1531
|
);
|
|
1337
1532
|
}
|
|
1338
1533
|
this._markDirty();
|
|
@@ -1342,91 +1537,117 @@ class RenderingContext2d {
|
|
|
1342
1537
|
// Render coverage into the scratch mask, then composite through it so
|
|
1343
1538
|
// the stroke honors clip / globalAlpha / composite op.
|
|
1344
1539
|
//
|
|
1345
|
-
// Bounded to the stroke's
|
|
1346
|
-
//
|
|
1347
|
-
//
|
|
1348
|
-
//
|
|
1349
|
-
//
|
|
1350
|
-
//
|
|
1351
|
-
//
|
|
1540
|
+
// Bounded to the stroke's islands, for the same reason _fillPolys is: on
|
|
1541
|
+
// the wire a Composite is the same size either way, but this branch runs
|
|
1542
|
+
// once per stroke, and a wall of 400 round-capped icons is 3200 of them.
|
|
1543
|
+
// Clearing and compositing the whole surface each time was ~6 Gpx a frame
|
|
1544
|
+
// and took 1.9 s on XQuartz where the bounded version takes 37 ms
|
|
1545
|
+
// (react-x11#148). Stale mask content outside the boxes is never
|
|
1546
|
+
// composited, so clearing only them is safe.
|
|
1352
1547
|
this._ensureFillMask();
|
|
1353
|
-
const
|
|
1354
|
-
if (!
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1548
|
+
const pieces = this._trisPieces(tris);
|
|
1549
|
+
if (!pieces.length) return;
|
|
1550
|
+
// the soup itself when it is one piece holding every triangle — the
|
|
1551
|
+
// common case (one polyline is one piece), and worth not copying
|
|
1552
|
+
const whole =
|
|
1553
|
+
pieces.length === 1 &&
|
|
1554
|
+
pieces[0].start === 0 &&
|
|
1555
|
+
pieces[0].end === tris.length;
|
|
1556
|
+
let painted = false;
|
|
1557
|
+
for (const cluster of this._maskClusters(pieces, op)) {
|
|
1558
|
+
const batch = whole ? tris : this._trisOf(tris, pieces, cluster.items);
|
|
1559
|
+
painted =
|
|
1560
|
+
this._paintThroughMask(
|
|
1561
|
+
cluster,
|
|
1562
|
+
{ triangles: batch },
|
|
1563
|
+
{
|
|
1564
|
+
edges: batch.length / 2,
|
|
1565
|
+
src,
|
|
1566
|
+
op,
|
|
1567
|
+
alpha: this.globalAlpha,
|
|
1568
|
+
server: () => {
|
|
1569
|
+
const opaque = this.createSolidPicture(0, 0, 0, 1);
|
|
1570
|
+
for (let i = 0; i < batch.length; i += chunk) {
|
|
1571
|
+
this.Render.Triangles(
|
|
1572
|
+
this.Render.PictOp.Add,
|
|
1573
|
+
opaque.id,
|
|
1574
|
+
0,
|
|
1575
|
+
0,
|
|
1576
|
+
this.fillMask.id,
|
|
1577
|
+
this.Render.a8,
|
|
1578
|
+
batch.slice(i, i + chunk),
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
},
|
|
1582
|
+
},
|
|
1583
|
+
) || painted;
|
|
1584
|
+
}
|
|
1585
|
+
if (painted) this._markDirty();
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
/**
|
|
1589
|
+
* A stroke's triangle soup as the islands it is made of: consecutive
|
|
1590
|
+
* triangles are coalesced while they stay within `PIECE_SLACK` of each
|
|
1591
|
+
* other, which collapses a polyline — body, caps, join disks and all —
|
|
1592
|
+
* into one piece and leaves a batch of separate strokes as one piece each.
|
|
1593
|
+
* Each piece carries the `[start, end)` span of `tris` it owns.
|
|
1594
|
+
*
|
|
1595
|
+
* Coalescing is always safe: merging boxes can only widen a mask, while
|
|
1596
|
+
* splitting overlapping coverage apart is what would double-blend it —
|
|
1597
|
+
* and `clusterBoxes` cannot split overlapping boxes anyway. So this is a
|
|
1598
|
+
* linear pre-pass whose only job is to hand the clustering a handful of
|
|
1599
|
+
* boxes instead of thousands of triangles.
|
|
1600
|
+
*
|
|
1601
|
+
* A piece that misses the surface entirely is dropped, leaving a hole in
|
|
1602
|
+
* the spans — which is why the caller gathers a batch from the spans it
|
|
1603
|
+
* has rather than assuming they tile `tris`.
|
|
1604
|
+
*/
|
|
1605
|
+
_trisPieces(tris) {
|
|
1606
|
+
const pieces = [];
|
|
1607
|
+
let open = null;
|
|
1608
|
+
const close = () => {
|
|
1609
|
+
if (!open) return;
|
|
1610
|
+
const b = this._clampBBox(open.minX, open.minY, open.maxX, open.maxY);
|
|
1611
|
+
if (b) pieces.push({ ...b, start: open.start, end: open.end });
|
|
1612
|
+
open = null;
|
|
1613
|
+
};
|
|
1614
|
+
for (let i = 0; i < tris.length; i += 6) {
|
|
1615
|
+
const minX = Math.min(tris[i], tris[i + 2], tris[i + 4]);
|
|
1616
|
+
const maxX = Math.max(tris[i], tris[i + 2], tris[i + 4]);
|
|
1617
|
+
const minY = Math.min(tris[i + 1], tris[i + 3], tris[i + 5]);
|
|
1618
|
+
const maxY = Math.max(tris[i + 1], tris[i + 3], tris[i + 5]);
|
|
1619
|
+
if (
|
|
1620
|
+
open &&
|
|
1621
|
+
minX - PIECE_SLACK <= open.maxX &&
|
|
1622
|
+
open.minX - PIECE_SLACK <= maxX &&
|
|
1623
|
+
minY - PIECE_SLACK <= open.maxY &&
|
|
1624
|
+
open.minY - PIECE_SLACK <= maxY
|
|
1625
|
+
) {
|
|
1626
|
+
if (minX < open.minX) open.minX = minX;
|
|
1627
|
+
if (maxX > open.maxX) open.maxX = maxX;
|
|
1628
|
+
if (minY < open.minY) open.minY = minY;
|
|
1629
|
+
if (maxY > open.maxY) open.maxY = maxY;
|
|
1630
|
+
open.end = i + 6;
|
|
1631
|
+
continue;
|
|
1389
1632
|
}
|
|
1633
|
+
close();
|
|
1634
|
+
open = { minX, minY, maxX, maxY, start: i, end: i + 6 };
|
|
1390
1635
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
this.fillMask.id,
|
|
1405
|
-
out.x,
|
|
1406
|
-
out.y,
|
|
1407
|
-
0,
|
|
1408
|
-
0,
|
|
1409
|
-
out.x,
|
|
1410
|
-
out.y,
|
|
1411
|
-
out.w,
|
|
1412
|
-
out.h,
|
|
1413
|
-
);
|
|
1636
|
+
close();
|
|
1637
|
+
return pieces;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/** the triangles of the given pieces, gathered into one soup */
|
|
1641
|
+
_trisOf(tris, pieces, items) {
|
|
1642
|
+
let n = 0;
|
|
1643
|
+
for (const i of items) n += pieces[i].end - pieces[i].start;
|
|
1644
|
+
const out = new Array(n);
|
|
1645
|
+
let w = 0;
|
|
1646
|
+
for (const i of items) {
|
|
1647
|
+
const piece = pieces[i];
|
|
1648
|
+
for (let k = piece.start; k < piece.end; ++k) out[w++] = tris[k];
|
|
1414
1649
|
}
|
|
1415
|
-
|
|
1416
|
-
op,
|
|
1417
|
-
src.id,
|
|
1418
|
-
this.fillMask.id,
|
|
1419
|
-
this.picture.id,
|
|
1420
|
-
out.x,
|
|
1421
|
-
out.y,
|
|
1422
|
-
out.x,
|
|
1423
|
-
out.y,
|
|
1424
|
-
out.x,
|
|
1425
|
-
out.y,
|
|
1426
|
-
out.w,
|
|
1427
|
-
out.h,
|
|
1428
|
-
);
|
|
1429
|
-
this._markDirty();
|
|
1650
|
+
return out;
|
|
1430
1651
|
}
|
|
1431
1652
|
|
|
1432
1653
|
/**
|
|
@@ -1463,6 +1684,7 @@ class RenderingContext2d {
|
|
|
1463
1684
|
drawGlyphs(op, src, positioned) {
|
|
1464
1685
|
const app = this.window.app;
|
|
1465
1686
|
const R = this.Render;
|
|
1687
|
+
if (!preparePattern(src, this._m)) return;
|
|
1466
1688
|
if (!this._clips.length) {
|
|
1467
1689
|
drawGlyphRuns(app, op, src.id, this.picture.id, positioned);
|
|
1468
1690
|
this._markDirty();
|
|
@@ -1684,6 +1906,7 @@ class RenderingContext2d {
|
|
|
1684
1906
|
|
|
1685
1907
|
fillRect(x, y, w, h) {
|
|
1686
1908
|
if (matIsIdentity(this._m)) {
|
|
1909
|
+
if (!preparePattern(this._backgroundPicture, this._m)) return;
|
|
1687
1910
|
const mask = this._compositeMask();
|
|
1688
1911
|
this.Render.Composite(
|
|
1689
1912
|
this._op(),
|
|
@@ -2390,7 +2613,13 @@ class RenderingContext2d {
|
|
|
2390
2613
|
[0, 0, 0, 0],
|
|
2391
2614
|
[0, 0, bb.w, bb.h],
|
|
2392
2615
|
);
|
|
2393
|
-
this._rasterizePolys(
|
|
2616
|
+
this._rasterizePolys(
|
|
2617
|
+
tmpMask,
|
|
2618
|
+
fillableShapes(entry.polys).shapes,
|
|
2619
|
+
entry.rule,
|
|
2620
|
+
-bb.x,
|
|
2621
|
+
-bb.y,
|
|
2622
|
+
);
|
|
2394
2623
|
}
|
|
2395
2624
|
R.Composite(
|
|
2396
2625
|
op,
|
|
@@ -2429,14 +2658,8 @@ class RenderingContext2d {
|
|
|
2429
2658
|
_uploadClipCoverage(tmpPixmap, entry, bb) {
|
|
2430
2659
|
const rasterizer = this.window.app.rasterizer;
|
|
2431
2660
|
if (!rasterizer) return false;
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2434
|
-
for (const p of entry.polys) {
|
|
2435
|
-
if (p.pts.length < 6) continue;
|
|
2436
|
-
flat.push(p.pts);
|
|
2437
|
-
edges += p.pts.length / 2;
|
|
2438
|
-
}
|
|
2439
|
-
if (!flat.length) return false;
|
|
2661
|
+
const { shapes, edges } = fillableShapes(entry.polys);
|
|
2662
|
+
if (!shapes.length) return false;
|
|
2440
2663
|
if (
|
|
2441
2664
|
routeRaster(bb.w, bb.h, edges, this.window.app.rasterPolicy) !== "local"
|
|
2442
2665
|
) {
|
|
@@ -2444,7 +2667,7 @@ class RenderingContext2d {
|
|
|
2444
2667
|
}
|
|
2445
2668
|
|
|
2446
2669
|
const coverage = rasterizer.rasterize({
|
|
2447
|
-
polys:
|
|
2670
|
+
polys: shapes,
|
|
2448
2671
|
rule: entry.rule,
|
|
2449
2672
|
dx: -bb.x,
|
|
2450
2673
|
dy: -bb.y,
|
|
@@ -2748,6 +2971,35 @@ class RenderingContext2d {
|
|
|
2748
2971
|
return new CanvasGradient("conical", this, x0, y0, angle);
|
|
2749
2972
|
}
|
|
2750
2973
|
|
|
2974
|
+
/**
|
|
2975
|
+
* A tiled paint: `source` repeated across whatever it fills, by the server,
|
|
2976
|
+
* in the one composite the fill already costs (issue #263).
|
|
2977
|
+
*
|
|
2978
|
+
* const tile = new Surface(app, { width: 24, height: 24 });
|
|
2979
|
+
* tile.render((c) => { c.fillStyle = '#333'; c.fillRect(0, 0, 1, 1); });
|
|
2980
|
+
* ctx.fillStyle = ctx.createPattern(tile, 'repeat');
|
|
2981
|
+
* ctx.fillRect(0, 0, ctx.width, ctx.height); // one request, no mask
|
|
2982
|
+
*
|
|
2983
|
+
* That is the difference between a background grid costing one repeating
|
|
2984
|
+
* picture and costing a pane-sized coverage mask: drawn as thousands of
|
|
2985
|
+
* tiny subpaths, a dot grid rasterizes client-side into an a8 mask the
|
|
2986
|
+
* size of its own bounding box — which for a background *is* the pane —
|
|
2987
|
+
* then uploads and composites it, every frame.
|
|
2988
|
+
*
|
|
2989
|
+
* `source` is a `Surface` (pixels the server drew), an `Image` (pixels
|
|
2990
|
+
* uploaded from the client), a `Pixmap` or a `Window`.
|
|
2991
|
+
*
|
|
2992
|
+
* `repetition` is `'repeat'` (the default), `'no-repeat'`, or the two
|
|
2993
|
+
* XRender modes the canvas spec has no name for: `'pad'` (clamp to the
|
|
2994
|
+
* edge pixels) and `'reflect'` (mirror every other tile). The spec's
|
|
2995
|
+
* per-axis `'repeat-x'`/`'repeat-y'` are not among them — XRender repeats
|
|
2996
|
+
* a source picture on both axes or on neither — and asking for one throws
|
|
2997
|
+
* with the clip-to-a-strip equivalent.
|
|
2998
|
+
*/
|
|
2999
|
+
createPattern(source, repetition = "repeat") {
|
|
3000
|
+
return new CanvasPattern(this.window.app, source, repetition);
|
|
3001
|
+
}
|
|
3002
|
+
|
|
2751
3003
|
/** the pixel layout of whatever this context currently draws into */
|
|
2752
3004
|
get _layout() {
|
|
2753
3005
|
const depth =
|
|
@@ -2986,8 +3238,13 @@ class RenderingContext2d {
|
|
|
2986
3238
|
* colour to fold into; `_drawCoverage` routes those through the scratch. */
|
|
2987
3239
|
_coverageSource() {
|
|
2988
3240
|
const style = this._fillStyle;
|
|
2989
|
-
if (this.globalAlpha >= 1 || !isPlainColor(style))
|
|
2990
|
-
|
|
3241
|
+
if (this.globalAlpha >= 1 || !isPlainColor(style)) {
|
|
3242
|
+
// a pattern whose transform collapsed paints nothing, and "nothing" as
|
|
3243
|
+
// a source is transparent black — no call site here has to know
|
|
3244
|
+
return preparePattern(this._backgroundPicture, this._m)
|
|
3245
|
+
? this._backgroundPicture
|
|
3246
|
+
: this.createSolidPicture(0, 0, 0, 0);
|
|
3247
|
+
}
|
|
2991
3248
|
const c = parseColor(style);
|
|
2992
3249
|
return this.createSolidPicture(c[0], c[1], c[2], c[3] * this.globalAlpha);
|
|
2993
3250
|
}
|
|
@@ -3478,9 +3735,167 @@ class CanvasGradient {
|
|
|
3478
3735
|
}
|
|
3479
3736
|
}
|
|
3480
3737
|
|
|
3738
|
+
/**
|
|
3739
|
+
* The fill/stroke style `ctx.createPattern` returns: a tile and how it
|
|
3740
|
+
* repeats, backed by one repeating XRender source picture.
|
|
3741
|
+
*
|
|
3742
|
+
* The picture is created on first use and freed by `destroy()` (or by the
|
|
3743
|
+
* GC, through `Picture`'s finalizer). The tile it reads is *not* the
|
|
3744
|
+
* pattern's to free: destroying the `Surface`/`Image` it came from is safe
|
|
3745
|
+
* while the pattern lives — X keeps pixmap storage alive as long as a
|
|
3746
|
+
* picture references it — but the pixels stop tracking anything drawn after.
|
|
3747
|
+
*
|
|
3748
|
+
* A pattern is bound to the connection, not to the context that made it, so
|
|
3749
|
+
* one grid tile serves every window on the app.
|
|
3750
|
+
*/
|
|
3751
|
+
class CanvasPattern {
|
|
3752
|
+
constructor(app, source, repetition = "repeat") {
|
|
3753
|
+
const name = repetition ?? "repeat";
|
|
3754
|
+
const repeat = REPEAT_MODES[name];
|
|
3755
|
+
if (repeat === undefined) {
|
|
3756
|
+
const known = Object.keys(REPEAT_MODES)
|
|
3757
|
+
.map((k) => `'${k}'`)
|
|
3758
|
+
.join(", ");
|
|
3759
|
+
const axis = name === "repeat-x" || name === "repeat-y";
|
|
3760
|
+
throw new Error(
|
|
3761
|
+
`createPattern: unsupported repetition ${JSON.stringify(name)}` +
|
|
3762
|
+
(axis
|
|
3763
|
+
? " — XRender repeats a source picture on both axes or on neither, with no" +
|
|
3764
|
+
" per-axis mode to map this one to. Tile with 'repeat' and bound the fill" +
|
|
3765
|
+
" to the one row/column of tiles instead: ctx.fillRect(x, y, w, tile.height)" +
|
|
3766
|
+
" repeats horizontally and nowhere else."
|
|
3767
|
+
: "") +
|
|
3768
|
+
`. Supported: ${known}. ${PATTERN_DOCS}`,
|
|
3769
|
+
);
|
|
3770
|
+
}
|
|
3771
|
+
const { drawable, format, width, height } = patternSourceOf(app, source);
|
|
3772
|
+
|
|
3773
|
+
this.app = app;
|
|
3774
|
+
this.Render = app.display.Render;
|
|
3775
|
+
this.source = source;
|
|
3776
|
+
this.repetition = name;
|
|
3777
|
+
this.width = width;
|
|
3778
|
+
this.height = height;
|
|
3779
|
+
this._repeat = repeat;
|
|
3780
|
+
this._drawable = drawable;
|
|
3781
|
+
this._format = format;
|
|
3782
|
+
// pattern space -> user space, the canvas `CanvasPattern.setTransform`
|
|
3783
|
+
// matrix. The picture transform is its inverse, composed with the CTM.
|
|
3784
|
+
this._m = [1, 0, 0, 1, 0, 0];
|
|
3785
|
+
this._picture = null;
|
|
3786
|
+
// what the server currently holds: a fresh picture is untransformed and
|
|
3787
|
+
// filtered nearest, so an untransformed fill costs no extra request
|
|
3788
|
+
this._applied = [1, 0, 0, 1, 0, 0];
|
|
3789
|
+
this._filter = "nearest";
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
/**
|
|
3793
|
+
* Position/scale/rotate the tile, canvas-style: `matrix` maps pattern
|
|
3794
|
+
* space to user space, as `[a, b, c, d, e, f]` or a DOMMatrix-shaped
|
|
3795
|
+
* `{a, b, c, d, e, f}`. Translating by the scroll offset is what keeps a
|
|
3796
|
+
* grid glued to the content under it.
|
|
3797
|
+
*/
|
|
3798
|
+
setTransform(matrix) {
|
|
3799
|
+
const m = Array.isArray(matrix)
|
|
3800
|
+
? matrix
|
|
3801
|
+
: [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f];
|
|
3802
|
+
if (m.length < 6 || m.some((v) => !Number.isFinite(Number(v)))) {
|
|
3803
|
+
throw new Error(
|
|
3804
|
+
"CanvasPattern.setTransform: expected [a, b, c, d, e, f] or {a, b, c, d, e, f} of finite numbers",
|
|
3805
|
+
);
|
|
3806
|
+
}
|
|
3807
|
+
this._m = m.slice(0, 6).map(Number);
|
|
3808
|
+
return this;
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
/** the repeating source Picture, created on first use */
|
|
3812
|
+
get picture() {
|
|
3813
|
+
if (!this._picture) {
|
|
3814
|
+
this._picture = new Picture(this.app, {
|
|
3815
|
+
drawable: this._drawable,
|
|
3816
|
+
format: this._format,
|
|
3817
|
+
repeat: this._repeat,
|
|
3818
|
+
});
|
|
3819
|
+
}
|
|
3820
|
+
return this._picture;
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3823
|
+
/** the Picture id, which is all a fill needs of a style */
|
|
3824
|
+
get id() {
|
|
3825
|
+
return this.picture.id;
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
/**
|
|
3829
|
+
* Make the server-side mapping match `ctm ∘ patternMatrix`. XRender's
|
|
3830
|
+
* picture transform runs the other way — it takes a coordinate in the
|
|
3831
|
+
* composite's source space (which every fill here keeps equal to device
|
|
3832
|
+
* space) to a texel — so it is the inverse.
|
|
3833
|
+
*
|
|
3834
|
+
* Returns false when that composition collapses (a zero scale), which
|
|
3835
|
+
* paints nothing, exactly as the canvas spec says.
|
|
3836
|
+
*/
|
|
3837
|
+
_sync(ctm) {
|
|
3838
|
+
const m = matMultiply(ctm, this._m);
|
|
3839
|
+
const inv = matInvert(m);
|
|
3840
|
+
if (!inv) return false;
|
|
3841
|
+
const a = this._applied;
|
|
3842
|
+
if (
|
|
3843
|
+
inv[0] !== a[0] ||
|
|
3844
|
+
inv[1] !== a[1] ||
|
|
3845
|
+
inv[2] !== a[2] ||
|
|
3846
|
+
inv[3] !== a[3] ||
|
|
3847
|
+
inv[4] !== a[4] ||
|
|
3848
|
+
inv[5] !== a[5]
|
|
3849
|
+
) {
|
|
3850
|
+
this.Render.SetPictureTransform(this.id, [
|
|
3851
|
+
inv[0],
|
|
3852
|
+
inv[2],
|
|
3853
|
+
inv[4],
|
|
3854
|
+
inv[1],
|
|
3855
|
+
inv[3],
|
|
3856
|
+
inv[5],
|
|
3857
|
+
0,
|
|
3858
|
+
0,
|
|
3859
|
+
1,
|
|
3860
|
+
]);
|
|
3861
|
+
this._applied = inv;
|
|
3862
|
+
}
|
|
3863
|
+
// A tile landing on whole pixels wants its own pixels back, not a blend
|
|
3864
|
+
// of them: nearest is both exact and cheaper. Anything else is resampled.
|
|
3865
|
+
const filter =
|
|
3866
|
+
m[0] === 1 &&
|
|
3867
|
+
m[1] === 0 &&
|
|
3868
|
+
m[2] === 0 &&
|
|
3869
|
+
m[3] === 1 &&
|
|
3870
|
+
Number.isInteger(m[4]) &&
|
|
3871
|
+
Number.isInteger(m[5])
|
|
3872
|
+
? "nearest"
|
|
3873
|
+
: "bilinear";
|
|
3874
|
+
if (filter !== this._filter) {
|
|
3875
|
+
this.picture.setFilter(filter);
|
|
3876
|
+
this._filter = filter;
|
|
3877
|
+
}
|
|
3878
|
+
return true;
|
|
3879
|
+
}
|
|
3880
|
+
|
|
3881
|
+
/** free the repeating picture; the tile it read is the caller's */
|
|
3882
|
+
destroy() {
|
|
3883
|
+
if (this._picture) {
|
|
3884
|
+
this._picture.destroy();
|
|
3885
|
+
this._picture = null;
|
|
3886
|
+
}
|
|
3887
|
+
this._applied = [1, 0, 0, 1, 0, 0];
|
|
3888
|
+
this._filter = "nearest";
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
[Symbol.dispose]() {
|
|
3892
|
+
this.destroy();
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3481
3896
|
// register context
|
|
3482
3897
|
Drawable.renderingContextFactory["2d"] = (window) =>
|
|
3483
3898
|
new RenderingContext2d(window);
|
|
3484
3899
|
|
|
3485
3900
|
export default RenderingContext2d;
|
|
3486
|
-
export { CanvasGradient };
|
|
3901
|
+
export { CanvasGradient, CanvasPattern };
|