ntk 8.18.0 → 8.19.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/lib/app.js +217 -35
- package/lib/precise.js +90 -36
- package/lib/renderingcontext_2d.js +1105 -442
- package/lib/text/layout.js +12 -6
- package/lib/trapezoid.js +48 -26
- package/package.json +1 -1
|
@@ -100,6 +100,25 @@ function isIntegral(v) {
|
|
|
100
100
|
return Math.abs(v - Math.round(v)) < 1e-6;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* The extent of AddTraps data — `[top-left x, top-right x, top, bottom-left
|
|
105
|
+
* x, bottom-right x, bottom, …]`, as `trapezoidize` makes it — or null for
|
|
106
|
+
* none.
|
|
107
|
+
*/
|
|
108
|
+
function trapsInk(traps) {
|
|
109
|
+
let minX = Infinity;
|
|
110
|
+
let minY = Infinity;
|
|
111
|
+
let maxX = -Infinity;
|
|
112
|
+
let maxY = -Infinity;
|
|
113
|
+
for (let i = 0; i < traps.length; i += 6) {
|
|
114
|
+
minX = Math.min(minX, traps[i], traps[i + 3]);
|
|
115
|
+
maxX = Math.max(maxX, traps[i + 1], traps[i + 4]);
|
|
116
|
+
if (traps[i + 2] < minY) minY = traps[i + 2];
|
|
117
|
+
if (traps[i + 5] > maxY) maxY = traps[i + 5];
|
|
118
|
+
}
|
|
119
|
+
return maxX >= minX && maxY >= minY ? { minX, minY, maxX, maxY } : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
103
122
|
/** The overlap of two {x, y, w, h} boxes, or null when they have none. */
|
|
104
123
|
function intersectBox(a, b) {
|
|
105
124
|
const x = Math.max(a.x, b.x);
|
|
@@ -236,16 +255,6 @@ const gcRegistry = new FinalizationRegistry(({ X, gcs, regions }) => {
|
|
|
236
255
|
});
|
|
237
256
|
});
|
|
238
257
|
|
|
239
|
-
/**
|
|
240
|
-
* What `drawImage` takes as a server-side source: anything that knows its own
|
|
241
|
-
* size and can hand over a `Picture` for this connection.
|
|
242
|
-
*
|
|
243
|
-
* `Image` is client pixels uploaded once; `Surface` is pixels the server drew
|
|
244
|
-
* itself. Neither is special — the contract is the two members, so a caller
|
|
245
|
-
* with its own cache of rendered things can satisfy it without ntk knowing
|
|
246
|
-
* the type. A `RenderingContext2d` deliberately does not match: its `picture`
|
|
247
|
-
* is a property rather than a method, and it keeps its own branch below.
|
|
248
|
-
*/
|
|
249
258
|
/** a fillStyle with a single colour behind it, as opposed to a gradient, a
|
|
250
259
|
* pattern or a caller-supplied Picture */
|
|
251
260
|
function isPlainColor(style) {
|
|
@@ -445,6 +454,54 @@ function fitsFixed(values) {
|
|
|
445
454
|
return values.every((v) => Math.abs(v) < 32768);
|
|
446
455
|
}
|
|
447
456
|
|
|
457
|
+
/** How far from a whole pixel a coordinate may be and still count as on it:
|
|
458
|
+
* the rounding a transform adds, as `isIntegral` allows. */
|
|
459
|
+
const SNAP = 1e-6;
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* The rectangle (x, y, w, h) under `m`, as the whole-pixel device box a
|
|
463
|
+
* transformed `drawImage` composites over: `x0..x1` by `y0..y1`, unclamped.
|
|
464
|
+
*
|
|
465
|
+
* A corner a rounding error past a whole pixel stays on it. `rotate(Math.PI /
|
|
466
|
+
* 2)` leaves cos(π/2) at 6e-17 rather than 0, so a corner meant for y 30
|
|
467
|
+
* lands on 30.000000000000004, and rounding that out gave the box a row the
|
|
468
|
+
* rectangle does not reach.
|
|
469
|
+
*
|
|
470
|
+
* `fills` is whether the rectangle is that box exactly — an axis-aligned
|
|
471
|
+
* transform that puts every corner on a whole pixel — so that every pixel the
|
|
472
|
+
* composite touches has its centre inside the rectangle. Turned, skewed or
|
|
473
|
+
* off the pixel grid, the box holds pixels whose centres are outside it.
|
|
474
|
+
*/
|
|
475
|
+
function turnedBox(m, x, y, w, h) {
|
|
476
|
+
const corners = [
|
|
477
|
+
matApply(m, x, y),
|
|
478
|
+
matApply(m, x + w, y),
|
|
479
|
+
matApply(m, x, y + h),
|
|
480
|
+
matApply(m, x + w, y + h),
|
|
481
|
+
];
|
|
482
|
+
const xs = corners.map((c) => c[0]);
|
|
483
|
+
const ys = corners.map((c) => c[1]);
|
|
484
|
+
const x0 = Math.floor(Math.min(...xs) + SNAP);
|
|
485
|
+
const y0 = Math.floor(Math.min(...ys) + SNAP);
|
|
486
|
+
const x1 = Math.ceil(Math.max(...xs) - SNAP);
|
|
487
|
+
const y1 = Math.ceil(Math.max(...ys) - SNAP);
|
|
488
|
+
const on = (v, a, b) => Math.abs(v - a) < SNAP || Math.abs(v - b) < SNAP;
|
|
489
|
+
const fills = corners.every(([cx, cy]) => on(cx, x0, x1) && on(cy, y0, y1));
|
|
490
|
+
return { x0, y0, x1, y1, fills };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* What `drawImage` takes as a server-side source: anything that knows its own
|
|
495
|
+
* size and can hand over a `Picture` for this connection.
|
|
496
|
+
*
|
|
497
|
+
* `Image` is client pixels uploaded once; `Surface` is pixels the server drew
|
|
498
|
+
* itself. Neither is special — the contract is the two members, so a caller
|
|
499
|
+
* with its own cache of rendered things can satisfy it without ntk knowing
|
|
500
|
+
* the type. A `RenderingContext2d` does not match — its `picture` is a
|
|
501
|
+
* property rather than a method — and neither does a node-canvas; `drawImage`
|
|
502
|
+
* wraps both in one of these (`contextSource`, `_canvasSource`), so every
|
|
503
|
+
* form of the call treats them as it treats a Surface.
|
|
504
|
+
*/
|
|
448
505
|
function isPictureSource(image) {
|
|
449
506
|
return (
|
|
450
507
|
image instanceof Image ||
|
|
@@ -455,9 +512,83 @@ function isPictureSource(image) {
|
|
|
455
512
|
);
|
|
456
513
|
}
|
|
457
514
|
|
|
515
|
+
/** a node-canvas, or anything else whose pixels come from
|
|
516
|
+
* `image.context.getImageData()` */
|
|
517
|
+
function isCanvasLike(image) {
|
|
518
|
+
return typeof image?.context?.getImageData === "function";
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* `drawImage`'s arguments as the eight numbers of its longest form — the
|
|
523
|
+
* source rectangle, then the destination one — whichever form they came in.
|
|
524
|
+
*/
|
|
525
|
+
function imageRects(image, args) {
|
|
526
|
+
let sx = 0;
|
|
527
|
+
let sy = 0;
|
|
528
|
+
let sw = image.width;
|
|
529
|
+
let sh = image.height;
|
|
530
|
+
let dx = 0;
|
|
531
|
+
let dy = 0;
|
|
532
|
+
let dw;
|
|
533
|
+
let dh;
|
|
534
|
+
if (args.length >= 8) {
|
|
535
|
+
[sx, sy, sw, sh, dx, dy, dw, dh] = args;
|
|
536
|
+
} else if (args.length >= 4) {
|
|
537
|
+
[dx, dy, dw, dh] = args;
|
|
538
|
+
} else {
|
|
539
|
+
[dx = 0, dy = 0] = args;
|
|
540
|
+
dw = sw;
|
|
541
|
+
dh = sh;
|
|
542
|
+
}
|
|
543
|
+
return [sx, sy, sw, sh, dx, dy, dw, dh];
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* A 2d context as a `drawImage` source: the picture-source contract over the
|
|
548
|
+
* pixels it has drawn, so that it is drawn exactly as a Surface is.
|
|
549
|
+
*
|
|
550
|
+
* Its picture comes with nothing in the clip slot (`_sourcePicture`). A
|
|
551
|
+
* context drawing into an a8 target is coverage, painted in the current
|
|
552
|
+
* `fillStyle`, as the a8 Surface it is drawing into would be. `_drawable` is
|
|
553
|
+
* for `_drawsFromCopy`.
|
|
554
|
+
*/
|
|
555
|
+
function contextSource(ctx, app) {
|
|
556
|
+
if (!ctx._picture) throw destroyedSourceError();
|
|
557
|
+
if (ctx.window.app !== app) throw otherConnectionError();
|
|
558
|
+
return {
|
|
559
|
+
width: ctx.width,
|
|
560
|
+
height: ctx.height,
|
|
561
|
+
format: ctx._picture.format === app.display.Render.a8 ? "a8" : undefined,
|
|
562
|
+
picture: () => ctx._sourcePicture(),
|
|
563
|
+
_drawable: ctx._target,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function destroyedSourceError() {
|
|
568
|
+
return new Error(
|
|
569
|
+
"drawImage: the 2d context passed as the image has been destroyed — by\n" +
|
|
570
|
+
"ctx.destroy(), along with its window, or at the end of the\n" +
|
|
571
|
+
"Surface.render() call that lent it — so there is nothing to read.\n" +
|
|
572
|
+
"\n" +
|
|
573
|
+
"Draw the Surface itself, or keep a context from getContext('2d') for\n" +
|
|
574
|
+
"as long as you draw from it.",
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function otherConnectionError() {
|
|
579
|
+
return new Error(
|
|
580
|
+
"drawImage: the 2d context passed as the image draws on another X\n" +
|
|
581
|
+
"connection, and a picture means nothing on a connection that did not\n" +
|
|
582
|
+
"make it. Bring the pixels over through the client:\n" +
|
|
583
|
+
"\n" +
|
|
584
|
+
" const pixels = await source.getImageData(0, 0, source.width, source.height);\n" +
|
|
585
|
+
" ctx.drawImage(new Image(pixels), x, y);",
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
458
589
|
// canvas globalCompositeOperation -> XRender PictOp name. Porter-Duff ops
|
|
459
|
-
// map directly
|
|
460
|
-
// the
|
|
590
|
+
// map directly, and a clip bounds every one of them: nothing outside it
|
|
591
|
+
// changes, whatever the op (`_clipAfter`).
|
|
461
592
|
const GCO_TO_PICTOP = {
|
|
462
593
|
"source-over": "Over",
|
|
463
594
|
copy: "Src",
|
|
@@ -477,7 +608,9 @@ const GCO_TO_PICTOP = {
|
|
|
477
608
|
// what the split gives up is the gaps between the boxes, and for these ops
|
|
478
609
|
// the single-box version would not have changed those pixels either. `copy`,
|
|
479
610
|
// `source-in`, `destination-in`, `source-out` and `destination-atop` write
|
|
480
|
-
// the source — or nothing — across the whole box, so they keep one.
|
|
611
|
+
// the source — or nothing — across the whole box, so they keep one. Nor can
|
|
612
|
+
// a path clip be folded into their mask: it is applied after them instead
|
|
613
|
+
// (`_clipAfter`).
|
|
481
614
|
const MASK_BOUNDED_OPS = [
|
|
482
615
|
"Over",
|
|
483
616
|
"OverReverse",
|
|
@@ -856,6 +989,11 @@ class RenderingContext2d {
|
|
|
856
989
|
return;
|
|
857
990
|
}
|
|
858
991
|
if (!this._pictureClipped) return; // nothing of ours is in the slot
|
|
992
|
+
this._clearPictureClip();
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** Empty the picture's clip slot. */
|
|
996
|
+
_clearPictureClip() {
|
|
859
997
|
if (this._fixes) {
|
|
860
998
|
// None is the real "no clip"; a rectangle would only be a wider one
|
|
861
999
|
this._fixes.SetPictureClipRegion(this._picture.id, 0, 0, 0);
|
|
@@ -898,6 +1036,26 @@ class RenderingContext2d {
|
|
|
898
1036
|
return this._picture;
|
|
899
1037
|
}
|
|
900
1038
|
|
|
1039
|
+
/**
|
|
1040
|
+
* The picture for a composite that reads this context's pixels — a
|
|
1041
|
+
* `drawImage` of the context — with nothing in its clip slot.
|
|
1042
|
+
*
|
|
1043
|
+
* A clip is drawing state, not part of the pixels: a canvas drawn
|
|
1044
|
+
* somewhere gives up all of them, whatever its own context's clip. But
|
|
1045
|
+
* glamor — Xorg's modesetting driver, Xwayland — cuts a composite down to
|
|
1046
|
+
* its source's clip, where fb (Xvfb, XQuartz) reads past it, and `picture`
|
|
1047
|
+
* leaves the slot holding what the clip stack says, a region clip included.
|
|
1048
|
+
* So the slot is emptied here, and what it held is owed back: it goes back
|
|
1049
|
+
* on before this context next draws, as any reset does.
|
|
1050
|
+
*/
|
|
1051
|
+
_sourcePicture() {
|
|
1052
|
+
if (this._pictureClipped) {
|
|
1053
|
+
this._clearPictureClip();
|
|
1054
|
+
this._clipStale = true;
|
|
1055
|
+
}
|
|
1056
|
+
return this._picture;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
901
1059
|
/**
|
|
902
1060
|
* The region the clip stack currently means, or null when it holds no
|
|
903
1061
|
* region entry. One entry is used as it is; several are intersected into a
|
|
@@ -946,9 +1104,10 @@ class RenderingContext2d {
|
|
|
946
1104
|
* one per offscreen `Surface`, say — because without it each one
|
|
947
1105
|
* permanently costs a GC and a Picture.
|
|
948
1106
|
*
|
|
949
|
-
*
|
|
950
|
-
*
|
|
951
|
-
* with every other context, freed
|
|
1107
|
+
* What fills, strokes and glyphs paint with is deliberately not freed
|
|
1108
|
+
* here: either solids owned by the app (`App#styleSolid`,
|
|
1109
|
+
* `App#solidPicture` — shared with every other context, and freed by the
|
|
1110
|
+
* app, as it lets them go or with the connection) or a
|
|
952
1111
|
* `Picture`/`CanvasGradient` the caller passed in through `fillStyle`,
|
|
953
1112
|
* which is not ours to free.
|
|
954
1113
|
*/
|
|
@@ -984,7 +1143,7 @@ class RenderingContext2d {
|
|
|
984
1143
|
this._gc = this._fillMaskGC = this._clipMaskGC = null;
|
|
985
1144
|
this._gcs.length = 0;
|
|
986
1145
|
|
|
987
|
-
this.
|
|
1146
|
+
this._fillSource = this._strokeSource = this._glyphSource = null;
|
|
988
1147
|
|
|
989
1148
|
if (this._picture) {
|
|
990
1149
|
// a picture on a *window* is freed by the server with the window;
|
|
@@ -1042,6 +1201,34 @@ class RenderingContext2d {
|
|
|
1042
1201
|
return this.window.height;
|
|
1043
1202
|
}
|
|
1044
1203
|
|
|
1204
|
+
/**
|
|
1205
|
+
* Whether text drawn here — `fillText`, `drawGlyphs` and so
|
|
1206
|
+
* `TextLayout.draw` — honours `globalAlpha`. Always true: it is here to
|
|
1207
|
+
* be asked. ntk used to draw glyphs at full opacity whatever the alpha,
|
|
1208
|
+
* and a renderer handed contexts it did not make (another backend's, an
|
|
1209
|
+
* older ntk's) has to fade their text some dearer way, such as drawing it
|
|
1210
|
+
* on a surface of its own and fading that. Undefined on a context that
|
|
1211
|
+
* has never heard of it, which is the answer that keeps it safe.
|
|
1212
|
+
*/
|
|
1213
|
+
get fadesGlyphs() {
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Whether `drawGlyphs` and `drawTraps` take a colour for their source as
|
|
1219
|
+
* well as a picture: a CSS colour string or a premultiplied
|
|
1220
|
+
* `[r, g, b, a]`, the two forms `fillStyle` takes, or `null` for the fill
|
|
1221
|
+
* style itself (`_sourceOf`). Always true, and here to be asked, as
|
|
1222
|
+
* `fadesGlyphs` is. Older ntk took a picture only, and a colour drew
|
|
1223
|
+
* nothing: the server refused it as a picture after the call had
|
|
1224
|
+
* returned, so nothing a caller could catch said so. Code handed a
|
|
1225
|
+
* context it did not make asks this first, and makes its solids with
|
|
1226
|
+
* `createSolidPicture` where it is not true.
|
|
1227
|
+
*/
|
|
1228
|
+
get takesColorSources() {
|
|
1229
|
+
return true;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1045
1232
|
_op() {
|
|
1046
1233
|
return this.Render.PictOp[GCO_TO_PICTOP[this._gco] || "Over"];
|
|
1047
1234
|
}
|
|
@@ -1060,15 +1247,28 @@ class RenderingContext2d {
|
|
|
1060
1247
|
return this.window.app.solidPicture(r, g, b, a);
|
|
1061
1248
|
}
|
|
1062
1249
|
|
|
1250
|
+
/**
|
|
1251
|
+
* A solid for this call's own requests: a colour with `globalAlpha`
|
|
1252
|
+
* folded into it, or the alpha alone in a mask slot. A fade makes a new
|
|
1253
|
+
* one every frame, so the app keeps only the most recent and frees the
|
|
1254
|
+
* rest (App#transientSolid) — use it in the requests of the call that
|
|
1255
|
+
* asked, and never keep it or hand it out. An app that only answers
|
|
1256
|
+
* solidPicture — a host's own, a test's stand-in — keeps them all.
|
|
1257
|
+
*/
|
|
1258
|
+
_transientSolid(r, g, b, a) {
|
|
1259
|
+
const app = this.window.app;
|
|
1260
|
+
return app.transientSolid
|
|
1261
|
+
? app.transientSolid(r, g, b, a)
|
|
1262
|
+
: app.solidPicture(r, g, b, a);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1063
1265
|
_stylePicture(value) {
|
|
1064
|
-
if (typeof value === "string") {
|
|
1065
|
-
// by the string, where the app keeps that index (App#solidPictureOf);
|
|
1066
|
-
// an app that only answers solidPicture — a host's own, a test's
|
|
1067
|
-
// stand-in — takes the long way below
|
|
1068
|
-
const app = this.window.app;
|
|
1069
|
-
if (app.solidPictureOf) return app.solidPictureOf(value);
|
|
1070
|
-
}
|
|
1071
1266
|
if (typeof value === "string" || Array.isArray(value)) {
|
|
1267
|
+
// the app's, which it lets go as colours animate (App#styleSolid) —
|
|
1268
|
+
// the style stays here, to ask by again. An app that only answers
|
|
1269
|
+
// solidPicture — a host's own, a test's stand-in — takes the long way
|
|
1270
|
+
const app = this.window.app;
|
|
1271
|
+
if (app.styleSolid) return app.styleSolid(value);
|
|
1072
1272
|
const c = parseColor(value);
|
|
1073
1273
|
return this.createSolidPicture(c[0], c[1], c[2], c[3]);
|
|
1074
1274
|
}
|
|
@@ -1086,7 +1286,7 @@ class RenderingContext2d {
|
|
|
1086
1286
|
// the picture first: a value that throws leaves the style as it was
|
|
1087
1287
|
const picture = this._stylePicture(value);
|
|
1088
1288
|
this._fillStyle = value;
|
|
1089
|
-
this.
|
|
1289
|
+
this._fillSource = picture;
|
|
1090
1290
|
}
|
|
1091
1291
|
|
|
1092
1292
|
get fillStyle() {
|
|
@@ -1097,13 +1297,35 @@ class RenderingContext2d {
|
|
|
1097
1297
|
// the picture first: a value that throws leaves the style as it was
|
|
1098
1298
|
const picture = this._stylePicture(value);
|
|
1099
1299
|
this._strokeStyle = value;
|
|
1100
|
-
this.
|
|
1300
|
+
this._strokeSource = picture;
|
|
1101
1301
|
}
|
|
1102
1302
|
|
|
1103
1303
|
get strokeStyle() {
|
|
1104
1304
|
return this._strokeStyle;
|
|
1105
1305
|
}
|
|
1106
1306
|
|
|
1307
|
+
/**
|
|
1308
|
+
* What a fill paints with: the gradient, pattern or picture `fillStyle`
|
|
1309
|
+
* is, or the app's solid of its colour. The app lets a solid go once more
|
|
1310
|
+
* colours than it keeps have been asked for since (App#styleSolid), and
|
|
1311
|
+
* once it is freed its id can come back in another colour — so one found
|
|
1312
|
+
* let go is asked for again, by the style. Read it for the drawing at
|
|
1313
|
+
* hand: what it returns is good for the rest of the job, and past that
|
|
1314
|
+
* only through here.
|
|
1315
|
+
*/
|
|
1316
|
+
get _backgroundPicture() {
|
|
1317
|
+
const p = this._fillSource;
|
|
1318
|
+
if (!p?._evicted) return p;
|
|
1319
|
+
return (this._fillSource = this._stylePicture(this._fillStyle));
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/** What a stroke paints with, from `strokeStyle`, as `_backgroundPicture`. */
|
|
1323
|
+
get _strokePicture() {
|
|
1324
|
+
const p = this._strokeSource;
|
|
1325
|
+
if (!p?._evicted) return p;
|
|
1326
|
+
return (this._strokeSource = this._stylePicture(this._strokeStyle));
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1107
1329
|
/**
|
|
1108
1330
|
* Canvas-spec dash list: values are distances (user-space units) of
|
|
1109
1331
|
* alternating dashes and gaps. An empty list means solid; an odd-length
|
|
@@ -1358,8 +1580,10 @@ class RenderingContext2d {
|
|
|
1358
1580
|
* as they do to the drawing it belongs to.
|
|
1359
1581
|
*/
|
|
1360
1582
|
_paintShadow(surface, dx, dy) {
|
|
1583
|
+
// put back as it was: a solid the app let go of meanwhile is asked for
|
|
1584
|
+
// again when next read (_backgroundPicture)
|
|
1361
1585
|
const style = this._fillStyle;
|
|
1362
|
-
const
|
|
1586
|
+
const source = this._fillSource;
|
|
1363
1587
|
this.fillStyle = this._shadowColor;
|
|
1364
1588
|
try {
|
|
1365
1589
|
this._drawCoverage(
|
|
@@ -1376,7 +1600,7 @@ class RenderingContext2d {
|
|
|
1376
1600
|
);
|
|
1377
1601
|
} finally {
|
|
1378
1602
|
this._fillStyle = style;
|
|
1379
|
-
this.
|
|
1603
|
+
this._fillSource = source;
|
|
1380
1604
|
}
|
|
1381
1605
|
}
|
|
1382
1606
|
|
|
@@ -1769,8 +1993,9 @@ class RenderingContext2d {
|
|
|
1769
1993
|
);
|
|
1770
1994
|
if (!tile) return false;
|
|
1771
1995
|
const picture = tile.picture(app);
|
|
1996
|
+
// put back as it was, as _paintShadow does
|
|
1772
1997
|
const style = this._fillStyle;
|
|
1773
|
-
const
|
|
1998
|
+
const source = this._fillSource;
|
|
1774
1999
|
this.fillStyle = this._shadowColor;
|
|
1775
2000
|
try {
|
|
1776
2001
|
const op = this._op();
|
|
@@ -1791,7 +2016,7 @@ class RenderingContext2d {
|
|
|
1791
2016
|
}
|
|
1792
2017
|
} finally {
|
|
1793
2018
|
this._fillStyle = style;
|
|
1794
|
-
this.
|
|
2019
|
+
this._fillSource = source;
|
|
1795
2020
|
}
|
|
1796
2021
|
return true;
|
|
1797
2022
|
}
|
|
@@ -2379,10 +2604,116 @@ class RenderingContext2d {
|
|
|
2379
2604
|
return this._boundedOps.has(op);
|
|
2380
2605
|
}
|
|
2381
2606
|
|
|
2607
|
+
/**
|
|
2608
|
+
* Whether a drawing with `op` has to have the clip applied after the op
|
|
2609
|
+
* (`_compositeClipAfter`) rather than folded into its mask.
|
|
2610
|
+
*
|
|
2611
|
+
* The canvas never changes a pixel outside the clip, and at an
|
|
2612
|
+
* antialiased clip edge takes a pixel as far from what it was towards
|
|
2613
|
+
* what the op makes of it as the clip covers it. Folded into the mask,
|
|
2614
|
+
* the clip scales the source instead, which for an op that leaves the
|
|
2615
|
+
* destination alone where the mask is zero is that same pixel: `Over`
|
|
2616
|
+
* through a mask of a half is half of the way to `Over`. The rest write
|
|
2617
|
+
* where the mask is zero — through it, `copy` is the source *times* the
|
|
2618
|
+
* mask — so folded in, a clip would clear every pixel of the drawing's
|
|
2619
|
+
* box it leaves out. A rectangular clip never needs this: the box is cut
|
|
2620
|
+
* to it, or the server clips to it, and neither touches a pixel outside.
|
|
2621
|
+
*/
|
|
2622
|
+
_clipAfter(op) {
|
|
2623
|
+
return this._hasPolyClip && !this._maskBounded(op);
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
/**
|
|
2627
|
+
* `Composite` of `src` through `mask` onto this context: straight onto its
|
|
2628
|
+
* picture, or, `after`, with the clip applied after the op
|
|
2629
|
+
* (`_compositeClipAfter`), when `mask` does not hold the clip. The rest
|
|
2630
|
+
* of the arguments are Composite's own, from the source's coordinates on.
|
|
2631
|
+
*/
|
|
2632
|
+
_compositeOnto(after, op, src, mask, ...coords) {
|
|
2633
|
+
if (after) {
|
|
2634
|
+
this._compositeClipAfter(op, src, mask, ...coords);
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
this.Render.Composite(op, src, mask, this._dst(), ...coords);
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
/**
|
|
2641
|
+
* A `Composite` onto this context, with the clip the a8 clip mask holds
|
|
2642
|
+
* applied after the op, as the canvas applies one: each pixel goes from
|
|
2643
|
+
* what it was towards what the op makes of it by as much as the clip
|
|
2644
|
+
* covers it. Inside the clip that is the op, outside it nothing at all,
|
|
2645
|
+
* and along an antialiased edge the blend between the two.
|
|
2646
|
+
*
|
|
2647
|
+
* RENDER has no operator for that. The op is done on a copy of the box,
|
|
2648
|
+
* in the destination's own format, and the copy comes back through the
|
|
2649
|
+
* clip's coverage `c` in two composites: `OutReverse` leaves `d·(1 − c)`
|
|
2650
|
+
* of the destination and `Add` puts `t·c` of the copy into it. A pixel
|
|
2651
|
+
* the clip covers wholly, or not at all, comes out exact.
|
|
2652
|
+
*
|
|
2653
|
+
* The arguments are the ones the Composite request would have carried with
|
|
2654
|
+
* no clip, and `mask` holds none. The box is cut to the clip's extents —
|
|
2655
|
+
* outside them the clip covers nothing, so nothing there is read or
|
|
2656
|
+
* written — on the whole pixels the wire takes, so it samples what the
|
|
2657
|
+
* one composite would have.
|
|
2658
|
+
*/
|
|
2659
|
+
_compositeClipAfter(op, src, mask, srcX, srcY, maskX, maskY, dstX, dstY, w, h) {
|
|
2660
|
+
const R = this.Render;
|
|
2661
|
+
// each coordinate truncated on its own, as the 16-bit wire fields are
|
|
2662
|
+
const left = Math.trunc(dstX);
|
|
2663
|
+
const top = Math.trunc(dstY);
|
|
2664
|
+
const ext = this._clipExtents() ?? {
|
|
2665
|
+
x: 0,
|
|
2666
|
+
y: 0,
|
|
2667
|
+
w: this.width,
|
|
2668
|
+
h: this.height,
|
|
2669
|
+
};
|
|
2670
|
+
const x = Math.max(left, ext.x);
|
|
2671
|
+
const y = Math.max(top, ext.y);
|
|
2672
|
+
const bw = Math.min(left + Math.trunc(w), ext.x + ext.w) - x;
|
|
2673
|
+
const bh = Math.min(top + Math.trunc(h), ext.y + ext.h) - y;
|
|
2674
|
+
if (!(bw > 0 && bh > 0)) return;
|
|
2675
|
+
const clipMask = this._requireClipMask().id;
|
|
2676
|
+
const dst = this._dst();
|
|
2677
|
+
const app = this.window.app;
|
|
2678
|
+
const pixmap = new Pixmap(app, {
|
|
2679
|
+
depth: this._targetPixels().depth,
|
|
2680
|
+
width: bw,
|
|
2681
|
+
height: bh,
|
|
2682
|
+
});
|
|
2683
|
+
const copy = new Picture(app, {
|
|
2684
|
+
drawable: pixmap,
|
|
2685
|
+
format: this._picture.format,
|
|
2686
|
+
});
|
|
2687
|
+
try {
|
|
2688
|
+
R.Composite(R.PictOp.Src, dst, 0, copy.id, x, y, 0, 0, 0, 0, bw, bh);
|
|
2689
|
+
R.Composite(
|
|
2690
|
+
op,
|
|
2691
|
+
src,
|
|
2692
|
+
mask,
|
|
2693
|
+
copy.id,
|
|
2694
|
+
Math.trunc(srcX) + x - left,
|
|
2695
|
+
Math.trunc(srcY) + y - top,
|
|
2696
|
+
Math.trunc(maskX) + x - left,
|
|
2697
|
+
Math.trunc(maskY) + y - top,
|
|
2698
|
+
0,
|
|
2699
|
+
0,
|
|
2700
|
+
bw,
|
|
2701
|
+
bh,
|
|
2702
|
+
);
|
|
2703
|
+
R.Composite(R.PictOp.OutReverse, clipMask, 0, dst, x, y, 0, 0, x, y, bw, bh);
|
|
2704
|
+
R.Composite(R.PictOp.Add, copy.id, clipMask, dst, 0, 0, x, y, x, y, bw, bh);
|
|
2705
|
+
} finally {
|
|
2706
|
+
// queued behind the composites that read it
|
|
2707
|
+
copy.destroy();
|
|
2708
|
+
pixmap.destroy();
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2382
2712
|
/**
|
|
2383
2713
|
* The tail every masked fill and stroke ends in: coverage for one box into
|
|
2384
2714
|
* the scratch a8 mask, scaled by `alpha`, intersected with the clip, and
|
|
2385
|
-
* the source composited through it
|
|
2715
|
+
* the source composited through it — or, for an op that writes where the
|
|
2716
|
+
* mask is zero, the clip applied after it (`_clipAfter`).
|
|
2386
2717
|
*
|
|
2387
2718
|
* `job` is what a Rasterizer takes minus the box (docs/context-2d.md) —
|
|
2388
2719
|
* `{ polys, rule }` or `{ triangles }`; `server` rasterizes the same
|
|
@@ -2430,7 +2761,8 @@ class RenderingContext2d {
|
|
|
2430
2761
|
[out.x, out.y, out.w, out.h],
|
|
2431
2762
|
);
|
|
2432
2763
|
}
|
|
2433
|
-
|
|
2764
|
+
const after = this._clipAfter(op);
|
|
2765
|
+
if (this.clipMask && !after) {
|
|
2434
2766
|
// clipMask is surface-aligned, so it is sampled at the same offset
|
|
2435
2767
|
R.Composite(
|
|
2436
2768
|
R.PictOp.In,
|
|
@@ -2450,11 +2782,11 @@ class RenderingContext2d {
|
|
|
2450
2782
|
// src is either a 1x1 repeating solid (offset irrelevant) or a
|
|
2451
2783
|
// gradient/pattern, sampled at the same offset from its own origin
|
|
2452
2784
|
const [ox, oy] = sourceOrigin(src);
|
|
2453
|
-
|
|
2785
|
+
this._compositeOnto(
|
|
2786
|
+
after,
|
|
2454
2787
|
op,
|
|
2455
2788
|
src.id,
|
|
2456
2789
|
this.fillMask.id,
|
|
2457
|
-
this._dst(),
|
|
2458
2790
|
out.x - ox,
|
|
2459
2791
|
out.y - oy,
|
|
2460
2792
|
out.x,
|
|
@@ -3139,8 +3471,12 @@ class RenderingContext2d {
|
|
|
3139
3471
|
* instead of shaping.
|
|
3140
3472
|
*
|
|
3141
3473
|
* @param {number} op Render.PictOp (`ctx.Render.PictOp.Over` for normal text)
|
|
3142
|
-
* @param {Picture} src
|
|
3143
|
-
*
|
|
3474
|
+
* @param {string|number[]|null|Picture|CanvasGradient} src what the glyphs
|
|
3475
|
+
* paint with: a colour — a CSS colour string, or a premultiplied
|
|
3476
|
+
* `[r, g, b, a]` in 0..1 — or `null` for the fill style; or a picture —
|
|
3477
|
+
* a solid (`ctx.createSolidPicture(r, g, b, a)`) or a gradient. A colour
|
|
3478
|
+
* paints with a solid the app lets go of as colours change, where each
|
|
3479
|
+
* of `createSolidPicture`'s is held until the app closes (`_sourceOf`)
|
|
3144
3480
|
* @param {Array<{run, x, y, textRendering?}>} positioned runs in visual
|
|
3145
3481
|
* order; `x`/`y` is the run's baseline origin in **user space** — the
|
|
3146
3482
|
* current transform applies to it, as it does to every other drawing
|
|
@@ -3169,8 +3505,14 @@ class RenderingContext2d {
|
|
|
3169
3505
|
* relative positions, painted under the glyphs (issue #283). A paragraph
|
|
3170
3506
|
* whose spans change colour draws as several calls, and — as several
|
|
3171
3507
|
* `fillText`s would — casts a shadow per call.
|
|
3508
|
+
*
|
|
3509
|
+
* So does `globalAlpha`, to the glyphs and their shadow both, as it does
|
|
3510
|
+
* to every fill: at 0 nothing is drawn, not even the shadow.
|
|
3172
3511
|
*/
|
|
3173
3512
|
drawGlyphs(op, src, positioned) {
|
|
3513
|
+
if (this.globalAlpha <= 0) return;
|
|
3514
|
+
// before the shadow: a colour that is not one throws with nothing drawn
|
|
3515
|
+
src = this._sourceOf(src, "drawGlyphs");
|
|
3174
3516
|
const m = this._m;
|
|
3175
3517
|
if (!matIsIdentity(m)) {
|
|
3176
3518
|
positioned = positioned.map((p) => {
|
|
@@ -3195,12 +3537,29 @@ class RenderingContext2d {
|
|
|
3195
3537
|
* instead, intersect that with the clip, and paint the real source
|
|
3196
3538
|
* through the result — the same shape as _fillPolys. A rectangular clip
|
|
3197
3539
|
* takes the server-side fast path below instead of the mask.
|
|
3540
|
+
*
|
|
3541
|
+
* Nor has it a mask slot for `globalAlpha`, which is why text used to be
|
|
3542
|
+
* drawn at full opacity whatever it was. A solid source takes the alpha
|
|
3543
|
+
* in its colour, as `fillRect`'s does, and the glyphs go the way they
|
|
3544
|
+
* always went; a gradient or a pattern has no one colour to fold it
|
|
3545
|
+
* into, and paints through the scratch mask with the alpha in there.
|
|
3546
|
+
*
|
|
3547
|
+
* And it composites each glyph on its own, over that glyph's box. For an
|
|
3548
|
+
* op that leaves the destination alone where the mask is zero, that draws
|
|
3549
|
+
* what one composite through all of them would, but where two glyphs'
|
|
3550
|
+
* ink overlaps and takes the op twice. For `copy`, `source-in`,
|
|
3551
|
+
* `destination-in`, `source-out` and `destination-atop` it is nothing
|
|
3552
|
+
* like: each glyph's box is cleared outside its ink, a neighbour's ink
|
|
3553
|
+
* with it, and the rest of the drawing's box is left alone. Those paint
|
|
3554
|
+
* through the scratch mask too, once, over the box round every glyph's
|
|
3555
|
+
* ink — the box a fill of the same coverage clears (`_paintThroughMask`).
|
|
3198
3556
|
*/
|
|
3199
3557
|
_drawGlyphsDevice(op, src, positioned) {
|
|
3558
|
+
if (this.globalAlpha <= 0) return;
|
|
3200
3559
|
const app = this.window.app;
|
|
3201
|
-
const R = this.Render;
|
|
3202
3560
|
if (!prepareStyle(src, this._m, this.width, this.height)) return;
|
|
3203
|
-
|
|
3561
|
+
const faded = this._fadedSource(src);
|
|
3562
|
+
if (faded && !this._hasPolyClip && this._maskBounded(op)) {
|
|
3204
3563
|
// Fast path: a rectangular clip is something the server can do itself.
|
|
3205
3564
|
// Two small requests around the ordinary glyph composite, instead of
|
|
3206
3565
|
// clearing and compositing a full-surface a8 mask three times — which
|
|
@@ -3215,77 +3574,50 @@ class RenderingContext2d {
|
|
|
3215
3574
|
drawGlyphRuns(
|
|
3216
3575
|
app,
|
|
3217
3576
|
op,
|
|
3218
|
-
|
|
3577
|
+
faded.id,
|
|
3219
3578
|
this._dst(),
|
|
3220
3579
|
positioned,
|
|
3221
3580
|
rect ?? { x: 0, y: 0, w: this.width, h: this.height },
|
|
3222
|
-
sourceOrigin(
|
|
3581
|
+
sourceOrigin(faded),
|
|
3223
3582
|
);
|
|
3224
3583
|
if (rect) this._invalidatePictureClip();
|
|
3225
3584
|
this._markDirty();
|
|
3226
3585
|
return;
|
|
3227
3586
|
}
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
[0, 0, this.width, this.height],
|
|
3236
|
-
);
|
|
3237
|
-
// solid white through the glyphs leaves their coverage in the a8 mask
|
|
3238
|
-
drawGlyphRuns(
|
|
3239
|
-
app,
|
|
3240
|
-
R.PictOp.Over,
|
|
3241
|
-
this._glyphSource.id,
|
|
3242
|
-
this.fillMask.id,
|
|
3243
|
-
positioned,
|
|
3244
|
-
{ x: 0, y: 0, w: this.width, h: this.height },
|
|
3245
|
-
);
|
|
3246
|
-
R.Composite(
|
|
3247
|
-
R.PictOp.In,
|
|
3248
|
-
clipMask.id,
|
|
3249
|
-
0,
|
|
3250
|
-
this.fillMask.id,
|
|
3251
|
-
0,
|
|
3252
|
-
0,
|
|
3253
|
-
0,
|
|
3254
|
-
0,
|
|
3255
|
-
0,
|
|
3256
|
-
0,
|
|
3257
|
-
this.width,
|
|
3258
|
-
this.height,
|
|
3259
|
-
);
|
|
3260
|
-
const [ox, oy] = sourceOrigin(src);
|
|
3261
|
-
R.Composite(
|
|
3587
|
+
// every glyph's, whether or not it is sent: a glyph too far outside the
|
|
3588
|
+
// clip to be drawn still widens the box an op like `copy` clears
|
|
3589
|
+
const ink = positionedRunsInk(positioned);
|
|
3590
|
+
if (!ink) return; // a run of spaces inks nothing
|
|
3591
|
+
const R = this.Render;
|
|
3592
|
+
const alpha = faded ? 1 : this.globalAlpha;
|
|
3593
|
+
this._paintThroughScratch(
|
|
3262
3594
|
op,
|
|
3263
|
-
src
|
|
3264
|
-
|
|
3265
|
-
this.
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
0,
|
|
3269
|
-
0,
|
|
3270
|
-
0,
|
|
3271
|
-
0,
|
|
3272
|
-
this.width,
|
|
3273
|
-
this.height,
|
|
3595
|
+
faded ?? src,
|
|
3596
|
+
alpha,
|
|
3597
|
+
this._rawBox(ink.minX, ink.minY, ink.maxX, ink.maxY),
|
|
3598
|
+
(white, mask, bounds) =>
|
|
3599
|
+
drawGlyphRuns(app, R.PictOp.Over, white, mask, positioned, bounds),
|
|
3274
3600
|
);
|
|
3275
|
-
this._markDirty();
|
|
3276
3601
|
}
|
|
3277
3602
|
|
|
3278
3603
|
/**
|
|
3279
3604
|
* Trapezoid coverage under the clip — `drawGlyphs` for vector shapes that
|
|
3280
3605
|
* are already trapezoidized (KaTeX radicals and rules go this way).
|
|
3281
3606
|
* Without it they composite straight to the destination picture and spill
|
|
3282
|
-
* out of clipped boxes, which is what glyphs used to do.
|
|
3607
|
+
* out of clipped boxes, which is what glyphs used to do. `globalAlpha`
|
|
3608
|
+
* applies as it does to glyphs, so a radical fades with the formula, and
|
|
3609
|
+
* so does `op`: one that writes where the mask is zero clears the box
|
|
3610
|
+
* round the trapezoids, with the same pixel of slack, on either route.
|
|
3611
|
+
* `src` is what `drawGlyphs` takes: a colour, `null` for the fill style,
|
|
3612
|
+
* or a picture.
|
|
3283
3613
|
*/
|
|
3284
3614
|
drawTraps(op, src, traps) {
|
|
3285
|
-
if (!traps.length) return;
|
|
3615
|
+
if (!traps.length || this.globalAlpha <= 0) return;
|
|
3616
|
+
src = this._sourceOf(src, "drawTraps");
|
|
3286
3617
|
const app = this.window.app;
|
|
3287
|
-
|
|
3288
|
-
|
|
3618
|
+
if (!prepareStyle(src, this._m, this.width, this.height)) return;
|
|
3619
|
+
const faded = this._fadedSource(src);
|
|
3620
|
+
if (faded && !this._hasPolyClip && this._maskBounded(op)) {
|
|
3289
3621
|
const rect = this._clipRect();
|
|
3290
3622
|
if (rect) {
|
|
3291
3623
|
if (rect.w === 0 || rect.h === 0) return;
|
|
@@ -3294,61 +3626,152 @@ class RenderingContext2d {
|
|
|
3294
3626
|
compositeTraps(
|
|
3295
3627
|
app,
|
|
3296
3628
|
op,
|
|
3297
|
-
|
|
3629
|
+
faded.id,
|
|
3298
3630
|
this._dst(),
|
|
3299
3631
|
traps,
|
|
3300
3632
|
undefined,
|
|
3301
3633
|
undefined,
|
|
3302
|
-
sourceOrigin(
|
|
3634
|
+
sourceOrigin(faded),
|
|
3303
3635
|
);
|
|
3304
3636
|
if (rect) this._invalidatePictureClip();
|
|
3305
3637
|
this._markDirty();
|
|
3306
3638
|
return;
|
|
3307
3639
|
}
|
|
3308
|
-
const
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
app,
|
|
3319
|
-
R.PictOp.Over,
|
|
3320
|
-
this._glyphSource.id,
|
|
3321
|
-
this.fillMask.id,
|
|
3322
|
-
traps,
|
|
3640
|
+
const ink = trapsInk(traps);
|
|
3641
|
+
if (!ink) return;
|
|
3642
|
+
const alpha = faded ? 1 : this.globalAlpha;
|
|
3643
|
+
this._paintThroughScratch(
|
|
3644
|
+
op,
|
|
3645
|
+
faded ?? src,
|
|
3646
|
+
alpha,
|
|
3647
|
+
this._rawBox(ink.minX, ink.minY, ink.maxX, ink.maxY),
|
|
3648
|
+
(white, mask) =>
|
|
3649
|
+
compositeTraps(app, this.Render.PictOp.Over, white, mask, traps),
|
|
3323
3650
|
);
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
/**
|
|
3654
|
+
* The picture a `drawGlyphs` or `drawTraps` source paints with. A colour
|
|
3655
|
+
* — a CSS colour string or a premultiplied `[r, g, b, a]` — is the app's
|
|
3656
|
+
* solid of it, from the LRU the colour styles share (App#styleSolid):
|
|
3657
|
+
* good for the rest of the job, then let go of once enough other colours
|
|
3658
|
+
* have been asked for. That is why a caller passes one rather than a
|
|
3659
|
+
* solid from `createSolidPicture`, which is held until the app closes:
|
|
3660
|
+
* a terminal showing truecolor output, or a colour animated through a
|
|
3661
|
+
* tween, made one of those for every colour it ever drew. `null` is the
|
|
3662
|
+
* fill style's. Anything else is a picture or a style of the caller's,
|
|
3663
|
+
* and goes as it came.
|
|
3664
|
+
*/
|
|
3665
|
+
_sourceOf(src, call) {
|
|
3666
|
+
if (src == null) return this._backgroundPicture;
|
|
3667
|
+
if (typeof src === "string" || Array.isArray(src)) {
|
|
3668
|
+
return this._stylePicture(src);
|
|
3669
|
+
}
|
|
3670
|
+
if (typeof src === "object") return src;
|
|
3671
|
+
// a grid keeps its colours as 0xRRGGBB numbers, and one handed over as
|
|
3672
|
+
// it is went to the server as a picture id, which it refused later
|
|
3673
|
+
const hex =
|
|
3674
|
+
Number.isInteger(src) && src >= 0 && src <= 0xffffff
|
|
3675
|
+
? ` ('#${src.toString(16).padStart(6, "0")}' is that number read as 0xRRGGBB)`
|
|
3676
|
+
: "";
|
|
3677
|
+
throw new TypeError(
|
|
3678
|
+
`${call}: src is ${typeof src === "number" ? src : `a ${typeof src}`}, neither a ` +
|
|
3679
|
+
`colour nor a picture. Pass a CSS colour string${hex}, a premultiplied ` +
|
|
3680
|
+
"[r, g, b, a] in 0..1, null for the fillStyle, or a picture such as " +
|
|
3681
|
+
"createSolidPicture makes",
|
|
3337
3682
|
);
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
/**
|
|
3686
|
+
* `src` with `globalAlpha` folded into it: the source itself at full
|
|
3687
|
+
* opacity, and at less, a solid of its colour with all four premultiplied
|
|
3688
|
+
* components scaled — which is compositing it at that opacity, for any op
|
|
3689
|
+
* (`_foldedColor`). Null when there is an alpha to apply and no colour to
|
|
3690
|
+
* fold it into: a gradient, a pattern, a picture of the caller's, or a
|
|
3691
|
+
* solid from a host whose `solidPicture` does not say what it paints with
|
|
3692
|
+
* (App#solidPicture does).
|
|
3693
|
+
*/
|
|
3694
|
+
_fadedSource(src) {
|
|
3695
|
+
const a = this.globalAlpha;
|
|
3696
|
+
if (a >= 1) return src;
|
|
3697
|
+
const c = src._rgba;
|
|
3698
|
+
if (!c) return null;
|
|
3699
|
+
return this._transientSolid(c[0] * a, c[1] * a, c[2] * a, c[3] * a);
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
/**
|
|
3703
|
+
* Paint `src` through coverage the server draws into the scratch a8 mask:
|
|
3704
|
+
* how glyphs and trapezoids reach the surface when their own composite
|
|
3705
|
+
* cannot carry what applies to it — a clip that is not a rectangle, a
|
|
3706
|
+
* `globalAlpha` with no colour to fold into, or an op that writes where
|
|
3707
|
+
* they have no ink.
|
|
3708
|
+
*
|
|
3709
|
+
* `box` is the drawing's own box in device pixels, its ink with a pixel
|
|
3710
|
+
* of slack (`_rawBox`), and everything here is bounded to the part of it
|
|
3711
|
+
* the surface and the clip let through, as a fill's mask is
|
|
3712
|
+
* (`_paintThroughMask`): the clip's rectangle, or the extents of a stack
|
|
3713
|
+
* with a path in it. For most ops that only saves pixel work. For the
|
|
3714
|
+
* ones that clear where the mask is zero it is what they clear — the
|
|
3715
|
+
* rest of the drawing's box inside the clip, not the rest of the surface.
|
|
3716
|
+
*
|
|
3717
|
+
* `drawCoverage(white, mask, bounds)` composites the coverage into the
|
|
3718
|
+
* mask, with a solid white source: what that leaves in an a8 picture is
|
|
3719
|
+
* exactly the coverage. Only `bounds` of it is read, so a glyph that
|
|
3720
|
+
* cannot reach it need not be sent. `alpha` scales the coverage, the clip
|
|
3721
|
+
* mask cuts it, and `src` is composited through the result — or, for an
|
|
3722
|
+
* op that writes where the mask is zero, the clip is applied after it
|
|
3723
|
+
* (`_clipAfter`).
|
|
3724
|
+
*/
|
|
3725
|
+
_paintThroughScratch(op, src, alpha, box, drawCoverage) {
|
|
3726
|
+
const R = this.Render;
|
|
3727
|
+
const clip = this._hasPolyClip ? this._clipExtents() : this._clipRect();
|
|
3728
|
+
let out = this._onSurface(box);
|
|
3729
|
+
if (out && clip) out = intersectBox(out, clip);
|
|
3730
|
+
// off the surface, clipped away, or not a number at all — the box of a
|
|
3731
|
+
// glyph whose origin is at Infinity
|
|
3732
|
+
if (!(out?.w > 0 && out.h > 0)) return;
|
|
3733
|
+
const after = this._clipAfter(op);
|
|
3734
|
+
const clipMask = this._hasPolyClip && !after ? this._requireClipMask() : null;
|
|
3735
|
+
this._ensureFillMask();
|
|
3736
|
+
this._glyphSource ??= this.createSolidPicture(1, 1, 1, 1);
|
|
3737
|
+
const rect = [out.x, out.y, out.w, out.h];
|
|
3738
|
+
R.FillRectangles(R.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], rect);
|
|
3739
|
+
drawCoverage(this._glyphSource.id, this.fillMask.id, out);
|
|
3740
|
+
if (alpha < 1) {
|
|
3741
|
+
// In with a constant color scales the a8 coverage by that alpha
|
|
3742
|
+
R.FillRectangles(R.PictOp.In, this.fillMask.id, [0, 0, 0, alpha], rect);
|
|
3743
|
+
}
|
|
3744
|
+
if (clipMask) {
|
|
3745
|
+
// clipMask is surface-aligned, so it is sampled at the same offset
|
|
3746
|
+
R.Composite(
|
|
3747
|
+
R.PictOp.In,
|
|
3748
|
+
clipMask.id,
|
|
3749
|
+
0,
|
|
3750
|
+
this.fillMask.id,
|
|
3751
|
+
out.x,
|
|
3752
|
+
out.y,
|
|
3753
|
+
0,
|
|
3754
|
+
0,
|
|
3755
|
+
out.x,
|
|
3756
|
+
out.y,
|
|
3757
|
+
out.w,
|
|
3758
|
+
out.h,
|
|
3759
|
+
);
|
|
3760
|
+
}
|
|
3338
3761
|
const [ox, oy] = sourceOrigin(src);
|
|
3339
|
-
|
|
3762
|
+
this._compositeOnto(
|
|
3763
|
+
after,
|
|
3340
3764
|
op,
|
|
3341
3765
|
src.id,
|
|
3342
3766
|
this.fillMask.id,
|
|
3343
|
-
|
|
3344
|
-
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
this.height,
|
|
3767
|
+
out.x - ox,
|
|
3768
|
+
out.y - oy,
|
|
3769
|
+
out.x,
|
|
3770
|
+
out.y,
|
|
3771
|
+
out.x,
|
|
3772
|
+
out.y,
|
|
3773
|
+
out.w,
|
|
3774
|
+
out.h,
|
|
3352
3775
|
);
|
|
3353
3776
|
this._markDirty();
|
|
3354
3777
|
}
|
|
@@ -3359,20 +3782,18 @@ class RenderingContext2d {
|
|
|
3359
3782
|
* a8 mask anywhere (issue #307).
|
|
3360
3783
|
*
|
|
3361
3784
|
* Neither thing `_compositeMask()` puts in a mask is really mask work.
|
|
3362
|
-
* A **rectangular clip** is a smaller composite box:
|
|
3363
|
-
* the
|
|
3364
|
-
*
|
|
3365
|
-
*
|
|
3366
|
-
*
|
|
3367
|
-
*
|
|
3368
|
-
*
|
|
3369
|
-
*
|
|
3785
|
+
* A **rectangular clip** is a smaller composite box: the canvas changes
|
|
3786
|
+
* nothing outside the clip, and never compositing there is exactly that,
|
|
3787
|
+
* for every op — `copy` and the other ops that write where the mask is
|
|
3788
|
+
* zero included, which through a mask cleared the box outside the clip.
|
|
3789
|
+
* Inside it a rect clip is binary coverage of 1, so the two agree pixel
|
|
3790
|
+
* for pixel rather than approximately. **`globalAlpha`** is a 1x1 solid,
|
|
3791
|
+
* which is that alpha everywhere the server samples it, for the price of
|
|
3792
|
+
* no pixels at all. A **region** clip needs neither: the picture carries
|
|
3793
|
+
* it, and it applies to the composite like any other.
|
|
3370
3794
|
*
|
|
3371
3795
|
* What is left needing a mask is what genuinely needs one: a
|
|
3372
|
-
* non-rectangular clip entry
|
|
3373
|
-
* — `copy`, `source-in`, `destination-in`, `source-out` and
|
|
3374
|
-
* `destination-atop` cover the whole composite box, so a shrunk box would
|
|
3375
|
-
* leave behind the pixels the mask path clears.
|
|
3796
|
+
* non-rectangular clip entry (`_compositeMasked`).
|
|
3376
3797
|
*
|
|
3377
3798
|
* `box` is the composite's destination box in device coordinates. Callers
|
|
3378
3799
|
* pass their own arguments through, so it is only intersected when it is
|
|
@@ -3383,14 +3804,13 @@ class RenderingContext2d {
|
|
|
3383
3804
|
* @returns {{box: ?object, mask: number}|null} the box to composite over —
|
|
3384
3805
|
* null when the clip rejects it whole and nothing should be drawn — and
|
|
3385
3806
|
* the mask slot for it; or null when this route does not apply and the
|
|
3386
|
-
* caller has to fall back to `
|
|
3807
|
+
* caller has to fall back to `_compositeMasked()`.
|
|
3387
3808
|
*/
|
|
3388
|
-
_boxedComposite(box
|
|
3809
|
+
_boxedComposite(box) {
|
|
3389
3810
|
if (this._hasPolyClip) return null;
|
|
3390
3811
|
let out = box;
|
|
3391
3812
|
const clip = this._clipRect();
|
|
3392
3813
|
if (clip) {
|
|
3393
|
-
if (!this._maskBounded(op)) return null;
|
|
3394
3814
|
if (![box.x, box.y, box.w, box.h].every(isIntegral)) return null;
|
|
3395
3815
|
out = intersectBox(box, clip);
|
|
3396
3816
|
}
|
|
@@ -3405,7 +3825,7 @@ class RenderingContext2d {
|
|
|
3405
3825
|
*/
|
|
3406
3826
|
_alphaMask() {
|
|
3407
3827
|
if (this.globalAlpha >= 1) return 0;
|
|
3408
|
-
return this.
|
|
3828
|
+
return this._transientSolid(0, 0, 0, this.globalAlpha).id;
|
|
3409
3829
|
}
|
|
3410
3830
|
|
|
3411
3831
|
/**
|
|
@@ -3436,7 +3856,9 @@ class RenderingContext2d {
|
|
|
3436
3856
|
// The fallback, not the common case: `_boxedComposite` above answers for a
|
|
3437
3857
|
// rect-only clip stack without materializing anything, and every caller
|
|
3438
3858
|
// here asks it first. A **region** clip needs nothing either — the picture
|
|
3439
|
-
// carries it, and the mask must not try to hold it.
|
|
3859
|
+
// carries it, and the mask must not try to hold it. Nor is it for an op
|
|
3860
|
+
// that writes where the mask is zero: that takes the clip after the op
|
|
3861
|
+
// (`_clipAfter`).
|
|
3440
3862
|
//
|
|
3441
3863
|
// `box` is the destination box of the composite the mask is for, in device
|
|
3442
3864
|
// coordinates. The composite samples the mask over that box and nowhere
|
|
@@ -3478,6 +3900,33 @@ class RenderingContext2d {
|
|
|
3478
3900
|
return this.fillMask.id;
|
|
3479
3901
|
}
|
|
3480
3902
|
|
|
3903
|
+
/**
|
|
3904
|
+
* The direct composite `_boxedComposite` has no cheap form for: `src`
|
|
3905
|
+
* over `box`, sampled from (`srcX`, `srcY`), under a clip the a8 clip mask
|
|
3906
|
+
* holds — a path, or a rectangle a box off the pixel grid cannot be cut
|
|
3907
|
+
* to. For an op that leaves the destination alone where the mask is zero
|
|
3908
|
+
* the clip and `globalAlpha` are one mask (`_compositeMask`). The rest
|
|
3909
|
+
* would clear every pixel of the box the clip leaves out, so for them the
|
|
3910
|
+
* mask is the alpha alone and the clip goes after the op (`_clipAfter`).
|
|
3911
|
+
*/
|
|
3912
|
+
_compositeMasked(op, src, srcX, srcY, box) {
|
|
3913
|
+
const after = !this._maskBounded(op);
|
|
3914
|
+
this._compositeOnto(
|
|
3915
|
+
after,
|
|
3916
|
+
op,
|
|
3917
|
+
src,
|
|
3918
|
+
after ? this._alphaMask() : this._compositeMask(box),
|
|
3919
|
+
srcX,
|
|
3920
|
+
srcY,
|
|
3921
|
+
box.x,
|
|
3922
|
+
box.y,
|
|
3923
|
+
box.x,
|
|
3924
|
+
box.y,
|
|
3925
|
+
box.w,
|
|
3926
|
+
box.h,
|
|
3927
|
+
);
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3481
3930
|
// ------------------------------------------------------------------
|
|
3482
3931
|
// drawing
|
|
3483
3932
|
|
|
@@ -3590,24 +4039,28 @@ class RenderingContext2d {
|
|
|
3590
4039
|
// coordinates all shift with the box, so a gradient or a pattern stays
|
|
3591
4040
|
// where it was and only the ink outside the clip goes away.
|
|
3592
4041
|
const direct =
|
|
3593
|
-
w > 0 && h > 0 ? this._boxedComposite({ x, y, w, h }
|
|
4042
|
+
w > 0 && h > 0 ? this._boxedComposite({ x, y, w, h }) : null;
|
|
3594
4043
|
if (direct && !direct.box) return; // the clip rejects the fill whole
|
|
3595
|
-
const box = direct ? direct.box : { x, y, w, h };
|
|
3596
4044
|
const [ox, oy] = sourceOrigin(style);
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
4045
|
+
if (direct) {
|
|
4046
|
+
const box = direct.box;
|
|
4047
|
+
this.Render.Composite(
|
|
4048
|
+
op,
|
|
4049
|
+
style.id,
|
|
4050
|
+
direct.mask,
|
|
4051
|
+
this._dst(),
|
|
4052
|
+
box.x - ox,
|
|
4053
|
+
box.y - oy,
|
|
4054
|
+
box.x,
|
|
4055
|
+
box.y,
|
|
4056
|
+
box.x,
|
|
4057
|
+
box.y,
|
|
4058
|
+
box.w,
|
|
4059
|
+
box.h,
|
|
4060
|
+
);
|
|
4061
|
+
} else {
|
|
4062
|
+
this._compositeMasked(op, style.id, x - ox, y - oy, { x, y, w, h });
|
|
4063
|
+
}
|
|
3611
4064
|
this._markDirty();
|
|
3612
4065
|
return;
|
|
3613
4066
|
}
|
|
@@ -4178,7 +4631,9 @@ class RenderingContext2d {
|
|
|
4178
4631
|
});
|
|
4179
4632
|
}
|
|
4180
4633
|
const encoded = encodeGlyphItems(items, page.bits);
|
|
4181
|
-
|
|
4634
|
+
// the style's own solid at full opacity; under a fade, one with the
|
|
4635
|
+
// alpha folded in, made for this composite
|
|
4636
|
+
const src = this._transientSolid(color[0], color[1], color[2], color[3]);
|
|
4182
4637
|
R.CompositeGlyphs(
|
|
4183
4638
|
encoded.bits,
|
|
4184
4639
|
R.PictOp.Over,
|
|
@@ -4720,10 +5175,16 @@ class RenderingContext2d {
|
|
|
4720
5175
|
* The current transform's translation applies to the anchor point;
|
|
4721
5176
|
* glyphs themselves are not rotated/scaled by the transform (size the
|
|
4722
5177
|
* font via `ctx.font` instead).
|
|
5178
|
+
*
|
|
5179
|
+
* The glyphs composite with `globalCompositeOperation`, as a fill does
|
|
5180
|
+
* and as their shadow always did: drawn source-over whatever it was, a
|
|
5181
|
+
* shadowed `destination-out` label erased with its shadow and then
|
|
5182
|
+
* painted its letters back in.
|
|
4723
5183
|
*/
|
|
4724
5184
|
fillText(text, x, y) {
|
|
4725
5185
|
text = String(text ?? "");
|
|
4726
|
-
|
|
5186
|
+
// nothing to draw, and at globalAlpha 0 nothing to shape it for
|
|
5187
|
+
if (!text || this.globalAlpha <= 0) return;
|
|
4727
5188
|
if (this._shadowed()) this._shadowOfText(text, x, y);
|
|
4728
5189
|
const style = this._resolvedTextStyle();
|
|
4729
5190
|
const app = this.window.app;
|
|
@@ -4747,11 +5208,7 @@ class RenderingContext2d {
|
|
|
4747
5208
|
}
|
|
4748
5209
|
// already device space: the anchor went through the matrix above, and
|
|
4749
5210
|
// the offsets and advances added to it are device pixels
|
|
4750
|
-
this._drawGlyphsDevice(
|
|
4751
|
-
this.Render.PictOp.Over,
|
|
4752
|
-
this._backgroundPicture,
|
|
4753
|
-
positioned,
|
|
4754
|
-
);
|
|
5211
|
+
this._drawGlyphsDevice(this._op(), this._backgroundPicture, positioned);
|
|
4755
5212
|
}
|
|
4756
5213
|
|
|
4757
5214
|
/**
|
|
@@ -5164,19 +5621,6 @@ class RenderingContext2d {
|
|
|
5164
5621
|
});
|
|
5165
5622
|
}
|
|
5166
5623
|
|
|
5167
|
-
/**
|
|
5168
|
-
* Draw an image, with the three canvas signatures:
|
|
5169
|
-
*
|
|
5170
|
-
* ctx.drawImage(image, dx, dy)
|
|
5171
|
-
* ctx.drawImage(image, dx, dy, dWidth, dHeight)
|
|
5172
|
-
* ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
|
|
5173
|
-
*
|
|
5174
|
-
* `image` is an ntk `Image` (see `loadImage()` — PNG/JPEG decoded
|
|
5175
|
-
* client-side, uploaded to the server once and composited from there;
|
|
5176
|
-
* scaling is server-side with bilinear filtering), another
|
|
5177
|
-
* RenderingContext2d, or a node-canvas-like object. Images honor the
|
|
5178
|
-
* current transform (server-side, via the picture transform).
|
|
5179
|
-
*/
|
|
5180
5624
|
/**
|
|
5181
5625
|
* Clip and mask for a direct composite, as a bracket around it.
|
|
5182
5626
|
*
|
|
@@ -5188,23 +5632,29 @@ class RenderingContext2d {
|
|
|
5188
5632
|
*
|
|
5189
5633
|
* Uniform alpha needs no surface-sized mask either: a 1x1 repeating
|
|
5190
5634
|
* picture is the same thing to the server, with no pixel work at all.
|
|
5635
|
+
*
|
|
5636
|
+
* `after` says the composite takes the clip after the op rather than in
|
|
5637
|
+
* its mask (`_clipAfter`), through `_compositeOnto`.
|
|
5191
5638
|
*/
|
|
5192
|
-
_beginDirectComposite(box) {
|
|
5639
|
+
_beginDirectComposite(box, op) {
|
|
5193
5640
|
const rect = this._clipRect();
|
|
5194
5641
|
if (!rect) {
|
|
5195
5642
|
// a poly clip is the only thing left that needs a mask; without one
|
|
5196
5643
|
// there is nothing to apply here but the alpha. The mask it does need
|
|
5197
5644
|
// is bounded to `box`, the destination box of the composite it is for
|
|
5198
|
-
// — the only pixels of it the server samples (issue #313).
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5645
|
+
// — the only pixels of it the server samples (issue #313). An op that
|
|
5646
|
+
// writes where the mask is zero takes the clip after it instead.
|
|
5647
|
+
const after = this._clipAfter(op);
|
|
5648
|
+
const mask =
|
|
5649
|
+
this._hasPolyClip && !after
|
|
5650
|
+
? this._compositeMask(box)
|
|
5651
|
+
: this._alphaMask();
|
|
5652
|
+
return { mask, after, clipped: false, empty: false };
|
|
5203
5653
|
}
|
|
5204
5654
|
if (rect.w === 0 || rect.h === 0)
|
|
5205
|
-
return { mask: 0, clipped: false, empty: true };
|
|
5655
|
+
return { mask: 0, after: false, clipped: false, empty: true };
|
|
5206
5656
|
this._setPictureClip(rect);
|
|
5207
|
-
return { mask: this._alphaMask(), clipped: true, empty: false };
|
|
5657
|
+
return { mask: this._alphaMask(), after: false, clipped: true, empty: false };
|
|
5208
5658
|
}
|
|
5209
5659
|
|
|
5210
5660
|
_endDirectComposite(state) {
|
|
@@ -5216,14 +5666,19 @@ class RenderingContext2d {
|
|
|
5216
5666
|
* into it. The mask slot is taken by the coverage, so the alpha has to go
|
|
5217
5667
|
* somewhere — and a 1x1 solid is free, where a surface-sized alpha mask is
|
|
5218
5668
|
* not. Falls back to the fill picture for gradients, which have no single
|
|
5219
|
-
* colour to fold into; `_drawCoverage` routes those through the scratch.
|
|
5669
|
+
* colour to fold into; `_drawCoverage` routes those through the scratch.
|
|
5670
|
+
*
|
|
5671
|
+
* All four components are scaled, not the alpha alone: the colour is
|
|
5672
|
+
* premultiplied, and one whose alpha shrank under unchanged components is
|
|
5673
|
+
* brighter than it can be — `#0000ff` at 0.5 over black came out
|
|
5674
|
+
* `#0000ff`, where `fillRect` draws `#000080`. */
|
|
5220
5675
|
_coverageSource() {
|
|
5221
5676
|
const style = this._fillStyle;
|
|
5222
5677
|
if (this.globalAlpha >= 1 || !isPlainColor(style)) {
|
|
5223
5678
|
return this._backgroundPicture;
|
|
5224
5679
|
}
|
|
5225
|
-
const c =
|
|
5226
|
-
return this.
|
|
5680
|
+
const c = this._foldedColor(style);
|
|
5681
|
+
return this._transientSolid(c[0], c[1], c[2], c[3]);
|
|
5227
5682
|
}
|
|
5228
5683
|
|
|
5229
5684
|
/**
|
|
@@ -5234,6 +5689,10 @@ class RenderingContext2d {
|
|
|
5234
5689
|
* monochrome drawing rendered once as coverage can then be painted in any
|
|
5235
5690
|
* colour, so a hover, a disabled state and a theme change all reuse the one
|
|
5236
5691
|
* rendered copy instead of each needing their own.
|
|
5692
|
+
*
|
|
5693
|
+
* The rectangles are device pixels, whatever the transform: a shadow's
|
|
5694
|
+
* coverage is composited through here as it is. Under a transform,
|
|
5695
|
+
* `drawImage` places coverage with `_drawCoverageTurned` instead.
|
|
5237
5696
|
*/
|
|
5238
5697
|
_drawCoverage(picture, sx, sy, sw, sh, dx, dy, dw, dh, op) {
|
|
5239
5698
|
// Coverage the surface has none of is drawn as nothing. X takes the
|
|
@@ -5242,10 +5701,9 @@ class RenderingContext2d {
|
|
|
5242
5701
|
// threw out of the paint. A NaN destination fails the test too.
|
|
5243
5702
|
if (!(dx < this.width && dy < this.height && dx + dw > 0 && dy + dh > 0)) return;
|
|
5244
5703
|
const R = this.Render;
|
|
5245
|
-
// the coverage is painted in the current fillStyle
|
|
5246
|
-
//
|
|
5247
|
-
|
|
5248
|
-
if (!prepareStyle(style, this._m, this.width, this.height)) return;
|
|
5704
|
+
// the coverage is painted in the current fillStyle — a gradient/pattern
|
|
5705
|
+
// whose transform collapsed paints nothing
|
|
5706
|
+
if (!prepareStyle(this._backgroundPicture, this._m, this.width, this.height)) return;
|
|
5249
5707
|
const scaled = dw !== sw || dh !== sh;
|
|
5250
5708
|
if (scaled) {
|
|
5251
5709
|
R.SetPictureTransform(picture.id, [
|
|
@@ -5264,15 +5722,36 @@ class RenderingContext2d {
|
|
|
5264
5722
|
R.ChangePicture(picture.id, { repeat: 2 }); // Repeat.Pad
|
|
5265
5723
|
}
|
|
5266
5724
|
// with a transform in place the mask is already sampled from (sx, sy)
|
|
5267
|
-
|
|
5268
|
-
|
|
5725
|
+
this._paintCoverage(picture, scaled ? 0 : sx, scaled ? 0 : sy, dx, dy, dw, dh, op);
|
|
5726
|
+
|
|
5727
|
+
if (scaled) {
|
|
5728
|
+
// restore defaults so the cached surface stays reusable as-is
|
|
5729
|
+
R.SetPictureTransform(picture.id, [1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
5730
|
+
picture.setFilter("nearest");
|
|
5731
|
+
R.ChangePicture(picture.id, { repeat: 0 }); // Repeat.None
|
|
5732
|
+
}
|
|
5733
|
+
}
|
|
5269
5734
|
|
|
5735
|
+
/**
|
|
5736
|
+
* The current `fillStyle` painted through coverage over the destination
|
|
5737
|
+
* box (dx, dy, dw, dh): `picture`, an a8 picture, is the mask, sampled
|
|
5738
|
+
* from (mx, my) at the box's corner through whatever transform its caller
|
|
5739
|
+
* has put on it — a scale (`_drawCoverage`) or a turn
|
|
5740
|
+
* (`_drawCoverageTurned`). The caller has prepared the style for this
|
|
5741
|
+
* paint (`prepareStyle`).
|
|
5742
|
+
*/
|
|
5743
|
+
_paintCoverage(picture, mx, my, dx, dy, dw, dh, op) {
|
|
5744
|
+
const R = this.Render;
|
|
5745
|
+
const style = this._backgroundPicture;
|
|
5270
5746
|
const rect = this._clipRect();
|
|
5271
5747
|
// the mask slot can hold exactly one picture, so anything that cannot be
|
|
5272
5748
|
// folded into the colour or handed to the server as a clip rectangle has
|
|
5273
|
-
// to be intersected into the scratch mask first
|
|
5749
|
+
// to be intersected into the scratch mask first — a path clip, unless
|
|
5750
|
+
// it is applied after the op (`_clipAfter`), and an alpha a gradient
|
|
5751
|
+
// has no colour to take
|
|
5752
|
+
const after = this._clipAfter(op);
|
|
5274
5753
|
const scratch =
|
|
5275
|
-
this._hasPolyClip ||
|
|
5754
|
+
(this._hasPolyClip && !after) ||
|
|
5276
5755
|
(this.globalAlpha < 1 && !isPlainColor(this._fillStyle));
|
|
5277
5756
|
|
|
5278
5757
|
if (scratch) {
|
|
@@ -5300,7 +5779,7 @@ class RenderingContext2d {
|
|
|
5300
5779
|
if (this.globalAlpha < 1) {
|
|
5301
5780
|
R.Composite(
|
|
5302
5781
|
R.PictOp.InReverse,
|
|
5303
|
-
this.
|
|
5782
|
+
this._transientSolid(0, 0, 0, this.globalAlpha).id,
|
|
5304
5783
|
0,
|
|
5305
5784
|
this.fillMask.id,
|
|
5306
5785
|
0,
|
|
@@ -5313,7 +5792,7 @@ class RenderingContext2d {
|
|
|
5313
5792
|
this.height,
|
|
5314
5793
|
);
|
|
5315
5794
|
}
|
|
5316
|
-
if (this._hasPolyClip) {
|
|
5795
|
+
if (this._hasPolyClip && !after) {
|
|
5317
5796
|
const clipMask = this._requireClipMask();
|
|
5318
5797
|
R.Composite(
|
|
5319
5798
|
R.PictOp.In,
|
|
@@ -5341,11 +5820,11 @@ class RenderingContext2d {
|
|
|
5341
5820
|
// part of a full-surface picture: the region just cleared to zero, so
|
|
5342
5821
|
// nothing paints at all unless dx and dy are both zero.
|
|
5343
5822
|
const [ox, oy] = sourceOrigin(style);
|
|
5344
|
-
|
|
5823
|
+
this._compositeOnto(
|
|
5824
|
+
after,
|
|
5345
5825
|
op,
|
|
5346
5826
|
style.id,
|
|
5347
5827
|
this.fillMask.id,
|
|
5348
|
-
this._dst(),
|
|
5349
5828
|
dx - ox,
|
|
5350
5829
|
dy - oy,
|
|
5351
5830
|
dx,
|
|
@@ -5363,11 +5842,11 @@ class RenderingContext2d {
|
|
|
5363
5842
|
// exactly as in _fillPolys
|
|
5364
5843
|
const src = this._coverageSource();
|
|
5365
5844
|
const [ox, oy] = sourceOrigin(src);
|
|
5366
|
-
|
|
5845
|
+
this._compositeOnto(
|
|
5846
|
+
after,
|
|
5367
5847
|
op,
|
|
5368
5848
|
src.id,
|
|
5369
5849
|
picture.id,
|
|
5370
|
-
this._dst(),
|
|
5371
5850
|
dx - ox,
|
|
5372
5851
|
dy - oy,
|
|
5373
5852
|
mx,
|
|
@@ -5379,229 +5858,410 @@ class RenderingContext2d {
|
|
|
5379
5858
|
);
|
|
5380
5859
|
if (rect) this._invalidatePictureClip();
|
|
5381
5860
|
}
|
|
5382
|
-
|
|
5383
|
-
if (scaled) {
|
|
5384
|
-
// restore defaults so the cached surface stays reusable as-is
|
|
5385
|
-
R.SetPictureTransform(picture.id, [1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
5386
|
-
picture.setFilter("nearest");
|
|
5387
|
-
R.ChangePicture(picture.id, { repeat: 0 }); // Repeat.None
|
|
5388
|
-
}
|
|
5389
5861
|
this._markDirty();
|
|
5390
5862
|
}
|
|
5391
5863
|
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5864
|
+
/**
|
|
5865
|
+
* Coverage under a transform, where `_drawImageTransformed` puts it: over
|
|
5866
|
+
* `box`, the part of the whole-pixel box round the turned rectangle that
|
|
5867
|
+
* the surface has, with `transform` taking that box's pixels back into the
|
|
5868
|
+
* image from its corner. The turned coverage is the mask, and the fill
|
|
5869
|
+
* style is painted through it as `_drawCoverage` paints it: clip,
|
|
5870
|
+
* `globalAlpha` and the composite op included, and a gradient or a
|
|
5871
|
+
* pattern under the transform in force, as for a fill.
|
|
5872
|
+
*
|
|
5873
|
+
* It used to be composited as the source, as an image is, and RENDER reads
|
|
5874
|
+
* an a8 source as alpha over black: a mask drawn under any transform, a
|
|
5875
|
+
* translate included, came out black whatever the fill style was.
|
|
5876
|
+
*/
|
|
5877
|
+
_drawCoverageTurned(image, box, transform, op) {
|
|
5878
|
+
// Nothing to paint: a clip rectangle the box misses, which clips every
|
|
5879
|
+
// op on the server, or a style that paints nothing (`_drawCoverage`).
|
|
5880
|
+
// Decided before the transform is installed, so there is none to put
|
|
5881
|
+
// back.
|
|
5882
|
+
const rect = this._clipRect();
|
|
5883
|
+
if (rect && !intersectBox(box, rect)) return;
|
|
5884
|
+
if (!prepareStyle(this._backgroundPicture, this._m, this.width, this.height)) return;
|
|
5885
|
+
const R = this.Render;
|
|
5886
|
+
const picture = image.picture(this.window.app);
|
|
5887
|
+
R.SetPictureTransform(picture.id, transform);
|
|
5888
|
+
picture.setFilter("bilinear");
|
|
5889
|
+
this._paintCoverage(picture, 0, 0, box.x, box.y, box.w, box.h, op);
|
|
5890
|
+
R.SetPictureTransform(picture.id, IDENTITY_TRANSFORM);
|
|
5891
|
+
picture.setFilter("nearest");
|
|
5892
|
+
}
|
|
5416
5893
|
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5894
|
+
/**
|
|
5895
|
+
* Draw an image, with the three canvas signatures:
|
|
5896
|
+
*
|
|
5897
|
+
* ctx.drawImage(image, dx, dy)
|
|
5898
|
+
* ctx.drawImage(image, dx, dy, dWidth, dHeight)
|
|
5899
|
+
* ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
|
|
5900
|
+
*
|
|
5901
|
+
* `image` is an ntk `Image` (see `loadImage()` — PNG/JPEG decoded
|
|
5902
|
+
* client-side, uploaded to the server once and composited from there), a
|
|
5903
|
+
* `Surface`, anything else with a size and a `picture(app)`, another
|
|
5904
|
+
* RenderingContext2d, or a node-canvas-like object. Every one of them is
|
|
5905
|
+
* drawn the same way: cropped, scaled server-side with bilinear filtering,
|
|
5906
|
+
* under the current transform, clipped, faded by `globalAlpha` and
|
|
5907
|
+
* composited with `globalCompositeOperation`.
|
|
5908
|
+
*/
|
|
5909
|
+
drawImage(image, ...args) {
|
|
5910
|
+
// A context and a node-canvas are drawn from a picture of their pixels,
|
|
5911
|
+
// as a Surface is. A context used to be composited whole at (0, 0), with
|
|
5912
|
+
// Over, whatever the call asked for.
|
|
5913
|
+
if (image instanceof RenderingContext2d) {
|
|
5914
|
+
image = contextSource(image, this.window.app);
|
|
5915
|
+
} else if (!isPictureSource(image) && isCanvasLike(image)) {
|
|
5916
|
+
const source = this._canvasSource(image);
|
|
5917
|
+
try {
|
|
5918
|
+
this.drawImage(source, ...args);
|
|
5919
|
+
} finally {
|
|
5920
|
+
source.destroy();
|
|
5420
5921
|
}
|
|
5922
|
+
return;
|
|
5923
|
+
}
|
|
5924
|
+
if (!isPictureSource(image)) return;
|
|
5925
|
+
const rects = imageRects(image, args);
|
|
5926
|
+
const cut = this._spilledCrop(image, rects);
|
|
5927
|
+
if (cut || this._drawsFromCopy(image, rects)) {
|
|
5928
|
+
this._drawFromCopy(image, rects, cut);
|
|
5929
|
+
return;
|
|
5930
|
+
}
|
|
5931
|
+
if (this._shadowed()) this._shadowOfImage(image, args);
|
|
5421
5932
|
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5933
|
+
const [sx, sy, sw, sh, dx, dy, dw, dh] = rects;
|
|
5934
|
+
if (!(dw > 0) || !(dh > 0) || !(sw > 0) || !(sh > 0)) return;
|
|
5935
|
+
const app = this.window.app;
|
|
5936
|
+
const op = this._op();
|
|
5426
5937
|
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
const mask = state.mask;
|
|
5430
|
-
const scaled = dw !== sw || dh !== sh;
|
|
5431
|
-
if (scaled) {
|
|
5432
|
-
// the picture transform maps composite coordinates into source
|
|
5433
|
-
// samples: dest pixel (i, j) reads (sx + i*sw/dw, sy + j*sh/dh)
|
|
5434
|
-
this.Render.SetPictureTransform(picture.id, [
|
|
5435
|
-
sw / dw,
|
|
5436
|
-
0,
|
|
5437
|
-
sx,
|
|
5438
|
-
0,
|
|
5439
|
-
sh / dh,
|
|
5440
|
-
sy,
|
|
5441
|
-
0,
|
|
5442
|
-
0,
|
|
5443
|
-
1,
|
|
5444
|
-
]);
|
|
5445
|
-
picture.setFilter("bilinear");
|
|
5446
|
-
// Bilinear sampling at the rim reads half a source pixel past the
|
|
5447
|
-
// image, and a picture's default repeat reads transparent there: an
|
|
5448
|
-
// upscaled image faded into whatever it was drawn over across its
|
|
5449
|
-
// outer pixels — three of them for a 15-pixel swatch drawn at 96.
|
|
5450
|
-
// The canvas spec's drawImage clamps to the image's edge, and so do
|
|
5451
|
-
// browsers; RepeatPad is that. Only on this axis-aligned path, whose
|
|
5452
|
-
// composite box is the destination rectangle exactly: under a
|
|
5453
|
-
// rotation the box is the bounding box, and pad would paint its
|
|
5454
|
-
// corners.
|
|
5455
|
-
this.Render.ChangePicture(picture.id, { repeat: 2 }); // Repeat.Pad
|
|
5456
|
-
this.Render.Composite(
|
|
5457
|
-
op,
|
|
5458
|
-
picture.id,
|
|
5459
|
-
mask,
|
|
5460
|
-
this._dst(),
|
|
5461
|
-
0,
|
|
5462
|
-
0,
|
|
5463
|
-
dx,
|
|
5464
|
-
dy,
|
|
5465
|
-
dx,
|
|
5466
|
-
dy,
|
|
5467
|
-
dw,
|
|
5468
|
-
dh,
|
|
5469
|
-
);
|
|
5470
|
-
// restore defaults so the cached upload can be reused as-is
|
|
5471
|
-
this.Render.SetPictureTransform(
|
|
5472
|
-
picture.id,
|
|
5473
|
-
[1, 0, 0, 0, 1, 0, 0, 0, 1],
|
|
5474
|
-
);
|
|
5475
|
-
picture.setFilter("nearest");
|
|
5476
|
-
this.Render.ChangePicture(picture.id, { repeat: 0 }); // Repeat.None
|
|
5477
|
-
} else {
|
|
5478
|
-
this.Render.Composite(
|
|
5479
|
-
op,
|
|
5480
|
-
picture.id,
|
|
5481
|
-
mask,
|
|
5482
|
-
this._dst(),
|
|
5483
|
-
sx,
|
|
5484
|
-
sy,
|
|
5485
|
-
dx,
|
|
5486
|
-
dy,
|
|
5487
|
-
dx,
|
|
5488
|
-
dy,
|
|
5489
|
-
dw,
|
|
5490
|
-
dh,
|
|
5491
|
-
);
|
|
5492
|
-
}
|
|
5493
|
-
this._endDirectComposite(state);
|
|
5494
|
-
this._markDirty();
|
|
5938
|
+
if (!matIsIdentity(this._m)) {
|
|
5939
|
+
this._drawImageTransformed(image, sx, sy, sw, sh, dx, dy, dw, dh, op);
|
|
5495
5940
|
return;
|
|
5496
5941
|
}
|
|
5497
5942
|
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5943
|
+
if (image.format === "a8") {
|
|
5944
|
+
const picture = image.picture(app);
|
|
5945
|
+
this._drawCoverage(picture, sx, sy, sw, sh, dx, dy, dw, dh, op);
|
|
5946
|
+
return;
|
|
5947
|
+
}
|
|
5502
5948
|
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
//
|
|
5510
|
-
//
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5949
|
+
const state = this._beginDirectComposite({ x: dx, y: dy, w: dw, h: dh }, op);
|
|
5950
|
+
if (state.empty) return;
|
|
5951
|
+
// only now that the clip has had its say: a node-canvas uploads here
|
|
5952
|
+
const picture = image.picture(app);
|
|
5953
|
+
const scaled = dw !== sw || dh !== sh;
|
|
5954
|
+
if (scaled) {
|
|
5955
|
+
// the picture transform maps composite coordinates into source
|
|
5956
|
+
// samples: dest pixel (i, j) reads (sx + i*sw/dw, sy + j*sh/dh)
|
|
5957
|
+
this.Render.SetPictureTransform(picture.id, [
|
|
5958
|
+
sw / dw,
|
|
5959
|
+
0,
|
|
5960
|
+
sx,
|
|
5961
|
+
0,
|
|
5962
|
+
sh / dh,
|
|
5963
|
+
sy,
|
|
5964
|
+
0,
|
|
5965
|
+
0,
|
|
5966
|
+
1,
|
|
5967
|
+
]);
|
|
5968
|
+
picture.setFilter("bilinear");
|
|
5969
|
+
// Bilinear sampling at the rim reads half a source pixel past the
|
|
5970
|
+
// image, and a picture's default repeat reads transparent there: an
|
|
5971
|
+
// upscaled image faded into whatever it was drawn over across its
|
|
5972
|
+
// outer pixels — three of them for a 15-pixel swatch drawn at 96.
|
|
5973
|
+
// The canvas spec's drawImage clamps to the image's edge, and so do
|
|
5974
|
+
// browsers; RepeatPad is that. Only on this axis-aligned path, whose
|
|
5975
|
+
// composite box is the destination rectangle exactly: under a
|
|
5976
|
+
// rotation the box is the bounding box, and pad would paint its
|
|
5977
|
+
// corners.
|
|
5978
|
+
this.Render.ChangePicture(picture.id, { repeat: 2 }); // Repeat.Pad
|
|
5979
|
+
this._compositeOnto(
|
|
5980
|
+
state.after,
|
|
5981
|
+
op,
|
|
5982
|
+
picture.id,
|
|
5983
|
+
state.mask,
|
|
5984
|
+
0,
|
|
5985
|
+
0,
|
|
5986
|
+
dx,
|
|
5987
|
+
dy,
|
|
5988
|
+
dx,
|
|
5989
|
+
dy,
|
|
5990
|
+
dw,
|
|
5991
|
+
dh,
|
|
5527
5992
|
);
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
typeof image.context.getImageData === "function"
|
|
5533
|
-
) {
|
|
5534
|
-
// node-canvas Canvas ( or any ctx with ctx.canvas.getImageData returning pixels)
|
|
5535
|
-
// What the clip leaves of the composite, decided before anything is
|
|
5536
|
-
// uploaded: a clip that rejects it whole makes the upload below dead
|
|
5537
|
-
// work (issue #307)
|
|
5538
|
-
const full = { x: 0, y: 0, w: this.width, h: this.height };
|
|
5539
|
-
const direct = this._boxedComposite(full, this.Render.PictOp.Over);
|
|
5540
|
-
if (direct && !direct.box) return;
|
|
5541
|
-
const box = direct ? direct.box : full;
|
|
5542
|
-
const imageData = image.context.getImageData(sx, sy, sWidth, sHeight);
|
|
5543
|
-
// node-canvas hands over straight RGBA, and the rgba32 picture below is
|
|
5544
|
-
// premultiplied — this used to swap the channels and stop there, which
|
|
5545
|
-
// composited translucent images at full brightness
|
|
5546
|
-
const data = fromStraightRgba(
|
|
5547
|
-
imageData.data,
|
|
5548
|
-
pixelLayout(this.display, 32),
|
|
5549
|
-
sWidth,
|
|
5550
|
-
sHeight,
|
|
5993
|
+
// restore defaults so the cached upload can be reused as-is
|
|
5994
|
+
this.Render.SetPictureTransform(
|
|
5995
|
+
picture.id,
|
|
5996
|
+
[1, 0, 0, 0, 1, 0, 0, 0, 1],
|
|
5551
5997
|
);
|
|
5998
|
+
picture.setFilter("nearest");
|
|
5999
|
+
this.Render.ChangePicture(picture.id, { repeat: 0 }); // Repeat.None
|
|
6000
|
+
} else {
|
|
6001
|
+
this._compositeOnto(
|
|
6002
|
+
state.after,
|
|
6003
|
+
op,
|
|
6004
|
+
picture.id,
|
|
6005
|
+
state.mask,
|
|
6006
|
+
sx,
|
|
6007
|
+
sy,
|
|
6008
|
+
dx,
|
|
6009
|
+
dy,
|
|
6010
|
+
dx,
|
|
6011
|
+
dy,
|
|
6012
|
+
dw,
|
|
6013
|
+
dh,
|
|
6014
|
+
);
|
|
6015
|
+
}
|
|
6016
|
+
this._endDirectComposite(state);
|
|
6017
|
+
this._markDirty();
|
|
6018
|
+
}
|
|
5552
6019
|
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
6020
|
+
/**
|
|
6021
|
+
* A node-canvas-like source as a picture source: its pixels are uploaded
|
|
6022
|
+
* on first use, which is after the clip has had its say, so a draw the clip
|
|
6023
|
+
* rejects uploads nothing (issue #307). `destroy()` frees the upload once
|
|
6024
|
+
* the composites that read it are queued — X executes requests in order, so
|
|
6025
|
+
* the server is done with it by the time it reads the frees.
|
|
6026
|
+
*/
|
|
6027
|
+
_canvasSource(image) {
|
|
6028
|
+
const app = this.window.app;
|
|
6029
|
+
const { width, height } = image;
|
|
6030
|
+
let pixmap = null;
|
|
6031
|
+
let picture = null;
|
|
6032
|
+
return {
|
|
6033
|
+
width,
|
|
6034
|
+
height,
|
|
6035
|
+
picture: () => {
|
|
6036
|
+
if (picture) return picture;
|
|
6037
|
+
const imageData = image.context.getImageData(0, 0, width, height);
|
|
6038
|
+
// node-canvas hands over straight RGBA, and the rgba32 picture below
|
|
6039
|
+
// is premultiplied — this used to swap the channels and stop there,
|
|
6040
|
+
// which composited translucent images at full brightness
|
|
6041
|
+
const data = fromStraightRgba(
|
|
6042
|
+
imageData.data,
|
|
6043
|
+
pixelLayout(this.display, 32),
|
|
6044
|
+
width,
|
|
6045
|
+
height,
|
|
6046
|
+
);
|
|
6047
|
+
pixmap = new Pixmap(app, { depth: 32, width, height });
|
|
6048
|
+
picture = new Picture(app, {
|
|
6049
|
+
drawable: pixmap,
|
|
6050
|
+
format: this.Render.rgba32,
|
|
6051
|
+
});
|
|
6052
|
+
// One upload GC per app rather than one per call, the same sharing
|
|
6053
|
+
// Image.picture does: a GC is valid for any drawable of the same
|
|
6054
|
+
// screen and depth. This used to allocate a GC, a pixmap and a
|
|
6055
|
+
// picture every time and free none of them, so drawing a node-canvas
|
|
6056
|
+
// source in an animation loop leaked three server resources a frame.
|
|
5569
6057
|
let gc = app._imageUploadGC;
|
|
5570
6058
|
if (!gc) {
|
|
5571
6059
|
gc = app._imageUploadGC = this.X.AllocID();
|
|
5572
6060
|
this.X.CreateGC(gc, pixmap.id);
|
|
5573
6061
|
}
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
6062
|
+
this.X.PutImage(2, pixmap.id, gc, width, height, 0, 0, 0, 32, data);
|
|
6063
|
+
return picture;
|
|
6064
|
+
},
|
|
6065
|
+
destroy: () => {
|
|
6066
|
+
picture?.destroy();
|
|
6067
|
+
pixmap?.destroy();
|
|
6068
|
+
},
|
|
6069
|
+
};
|
|
6070
|
+
}
|
|
5577
6071
|
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
6072
|
+
/**
|
|
6073
|
+
* Whether `drawImage` has to read a copy of the image rather than its
|
|
6074
|
+
* picture, because the picture holds pixels the draw must not read.
|
|
6075
|
+
*
|
|
6076
|
+
* Its own, first: the drawable this context draws into — the context
|
|
6077
|
+
* itself, another context on the same drawable, or a Surface drawn from
|
|
6078
|
+
* inside its own `render()`. A composite that reads the pixels it writes
|
|
6079
|
+
* sees its own output on the servers that work row by row — pixman, so
|
|
6080
|
+
* Xvfb and XQuartz, and glamor too, which hands a picture drawn onto its
|
|
6081
|
+
* own pixmap to pixman — and anything drawn lower than it was read from
|
|
6082
|
+
* came out smeared, each row read after a row above it had been written
|
|
6083
|
+
* there. A canvas draws from the pixels as they were before the call.
|
|
6084
|
+
*
|
|
6085
|
+
* Then a window's, past its edge: the backing pixmap is larger than the
|
|
6086
|
+
* window, and a draw that scales the image up, or puts it under a
|
|
6087
|
+
* transform, filters across the edge into the background colour there,
|
|
6088
|
+
* where the canvas clamps to the edge pixel (see the scaled path above).
|
|
6089
|
+
*/
|
|
6090
|
+
_drawsFromCopy(image, rects) {
|
|
6091
|
+
const drawable = image instanceof Surface ? image.pixmap : image._drawable;
|
|
6092
|
+
if (!drawable) return false;
|
|
6093
|
+
if (drawable.id === this._target?.id) return true;
|
|
6094
|
+
const [, , sw, sh, , , dw, dh] = rects;
|
|
6095
|
+
const acrossEdge = dw > sw || dh > sh || !matIsIdentity(this._m);
|
|
6096
|
+
return (
|
|
6097
|
+
acrossEdge &&
|
|
6098
|
+
(drawable.width > image.width || drawable.height > image.height)
|
|
6099
|
+
);
|
|
6100
|
+
}
|
|
6101
|
+
|
|
6102
|
+
/**
|
|
6103
|
+
* The part of the image `drawImage` has to draw from a copy of, because
|
|
6104
|
+
* drawn from the image itself it would show pixels beyond the crop; null
|
|
6105
|
+
* when it would not.
|
|
6106
|
+
*
|
|
6107
|
+
* Under a transform the composite covers the whole-pixel box around the
|
|
6108
|
+
* turned destination rectangle (`turnedBox`), and samples the image through
|
|
6109
|
+
* the inverse transform across all of it. Turned or skewed, the box's
|
|
6110
|
+
* corners are outside the rectangle, and so are the pixels along a side
|
|
6111
|
+
* that falls between two pixels; they sample the image outside the source
|
|
6112
|
+
* rectangle. Past the image's own edge that reads transparent, so a whole
|
|
6113
|
+
* image is drawn right — but past a crop it reads the rest of the image,
|
|
6114
|
+
* which came out at the box's corners: a crop of the left half of an image
|
|
6115
|
+
* turned 30° drew a wedge of its right half beside it, and one translated
|
|
6116
|
+
* by a tenth of a pixel drew a column of it.
|
|
6117
|
+
*
|
|
6118
|
+
* Only a crop that leaves a whole pixel of the image out needs the copy,
|
|
6119
|
+
* and only under a transform whose box is wider than its rectangle: the
|
|
6120
|
+
* axis-aligned paths composite over the destination rectangle and nowhere
|
|
6121
|
+
* else, and so does an axis-aligned transform that puts its corners on
|
|
6122
|
+
* whole pixels.
|
|
6123
|
+
*
|
|
6124
|
+
* The copy is of the crop's own pixels and of no more (`_drawFromCopy`),
|
|
6125
|
+
* and of them only the ones the composite can sample: the device box the
|
|
6126
|
+
* surface and a clip leave of it, mapped back into the image, with two
|
|
6127
|
+
* pixels to spare for the bilinear filter and the 16.16 transform. A draw
|
|
6128
|
+
* that lands nowhere copies nothing; one mostly off the surface, or
|
|
6129
|
+
* repainted through a small clip, copies the part it shows. A shadow
|
|
6130
|
+
* samples the image somewhere else, so a shadowed draw copies the crop
|
|
6131
|
+
* whole.
|
|
6132
|
+
*/
|
|
6133
|
+
_spilledCrop(image, rects) {
|
|
6134
|
+
if (matIsIdentity(this._m)) return null;
|
|
6135
|
+
const [sx, sy, sw, sh, dx, dy, dw, dh] = rects;
|
|
6136
|
+
if (!(dw > 0) || !(dh > 0) || !(sw > 0) || !(sh > 0)) return null;
|
|
6137
|
+
// the pixels the crop overlaps by more than a rounding error
|
|
6138
|
+
const crop = {
|
|
6139
|
+
x0: Math.max(0, Math.floor(sx + SNAP)),
|
|
6140
|
+
y0: Math.max(0, Math.floor(sy + SNAP)),
|
|
6141
|
+
x1: Math.min(image.width, Math.ceil(sx + sw - SNAP)),
|
|
6142
|
+
y1: Math.min(image.height, Math.ceil(sy + sh - SNAP)),
|
|
6143
|
+
};
|
|
6144
|
+
const cropped =
|
|
6145
|
+
crop.x0 > 0 ||
|
|
6146
|
+
crop.y0 > 0 ||
|
|
6147
|
+
crop.x1 < image.width ||
|
|
6148
|
+
crop.y1 < image.height;
|
|
6149
|
+
if (!cropped) return null;
|
|
6150
|
+
const turned = turnedBox(this._m, dx, dy, dw, dh);
|
|
6151
|
+
if (turned.fills) return null;
|
|
6152
|
+
// Too small for the transform to say, or nowhere the draw can show:
|
|
6153
|
+
// `_drawImageTransformed` draws nothing, so nothing is copied for it.
|
|
6154
|
+
const none = { x0: 0, y0: 0, x1: 0, y1: 0 };
|
|
6155
|
+
const inv = matInvert(
|
|
6156
|
+
matMultiply(this._m, [dw / sw, 0, 0, dh / sh, dx, dy]),
|
|
6157
|
+
);
|
|
6158
|
+
if (!inv || !fitsFixed(inv.slice(0, 4))) return none;
|
|
6159
|
+
if (this._shadowed()) return crop;
|
|
6160
|
+
|
|
6161
|
+
// Nothing outside the clip is written, whatever the op, so nothing there
|
|
6162
|
+
// needs reading (`_boxedComposite`, `_clipAfter`).
|
|
6163
|
+
let box = intersectBox(
|
|
6164
|
+
{
|
|
6165
|
+
x: turned.x0,
|
|
6166
|
+
y: turned.y0,
|
|
6167
|
+
w: turned.x1 - turned.x0,
|
|
6168
|
+
h: turned.y1 - turned.y0,
|
|
6169
|
+
},
|
|
6170
|
+
{ x: 0, y: 0, w: this.width, h: this.height },
|
|
6171
|
+
);
|
|
6172
|
+
const clip = this._clipExtents();
|
|
6173
|
+
if (box && clip) box = intersectBox(box, clip);
|
|
6174
|
+
if (!box) return none;
|
|
6175
|
+
const us = [];
|
|
6176
|
+
const vs = [];
|
|
6177
|
+
for (const [x, y] of [
|
|
6178
|
+
[box.x, box.y],
|
|
6179
|
+
[box.x + box.w, box.y],
|
|
6180
|
+
[box.x, box.y + box.h],
|
|
6181
|
+
[box.x + box.w, box.y + box.h],
|
|
6182
|
+
]) {
|
|
6183
|
+
const [u, v] = matApply(inv, x, y);
|
|
6184
|
+
us.push(sx + u);
|
|
6185
|
+
vs.push(sy + v);
|
|
6186
|
+
}
|
|
6187
|
+
return {
|
|
6188
|
+
x0: Math.max(crop.x0, Math.floor(Math.min(...us)) - 2),
|
|
6189
|
+
y0: Math.max(crop.y0, Math.floor(Math.min(...vs)) - 2),
|
|
6190
|
+
x1: Math.min(crop.x1, Math.ceil(Math.max(...us)) + 2),
|
|
6191
|
+
y1: Math.min(crop.y1, Math.ceil(Math.max(...vs)) + 2),
|
|
6192
|
+
};
|
|
6193
|
+
}
|
|
6194
|
+
|
|
6195
|
+
/**
|
|
6196
|
+
* `drawImage` from a copy of the part of the image it reads, `cut`: by
|
|
6197
|
+
* default the source rectangle and the pixel around it bilinear filtering
|
|
6198
|
+
* reads inside the destination, cut at the image's edge, so the copy draws
|
|
6199
|
+
* what the image itself would have. The copy is taken before a shadow is
|
|
6200
|
+
* drawn, which would otherwise land on the pixels it is a shadow of. It is
|
|
6201
|
+
* argb32, which holds any source of 8 bits a channel exactly, or a8 for
|
|
6202
|
+
* coverage.
|
|
6203
|
+
*
|
|
6204
|
+
* A crop that spills (`_spilledCrop`) is cut with no pixel around it:
|
|
6205
|
+
* beyond the copy's edge the transformed composite reads transparent, so a
|
|
6206
|
+
* crop turned or skewed is drawn as the same pixels cut out into an image
|
|
6207
|
+
* of their own would be, its edges fading out over a pixel as the image's
|
|
6208
|
+
* own edges do. A pixel around it would come back as a band of what is
|
|
6209
|
+
* beside the crop, all along the outside of its edge.
|
|
6210
|
+
*/
|
|
6211
|
+
_drawFromCopy(image, rects, cut = null) {
|
|
6212
|
+
const [sx, sy, sw, sh, dx, dy, dw, dh] = rects;
|
|
6213
|
+
if (!(dw > 0) || !(dh > 0) || !(sw > 0) || !(sh > 0)) return;
|
|
6214
|
+
const { x0, y0, x1, y1 } = cut ?? {
|
|
6215
|
+
x0: Math.max(0, Math.floor(sx) - 1),
|
|
6216
|
+
y0: Math.max(0, Math.floor(sy) - 1),
|
|
6217
|
+
x1: Math.min(image.width, Math.ceil(sx + sw) + 1),
|
|
6218
|
+
y1: Math.min(image.height, Math.ceil(sy + sh) + 1),
|
|
6219
|
+
};
|
|
6220
|
+
// the crop holds none of the image, or none the draw can show
|
|
6221
|
+
if (!(x1 > x0 && y1 > y0)) return;
|
|
6222
|
+
const width = x1 - x0;
|
|
6223
|
+
const height = y1 - y0;
|
|
6224
|
+
const app = this.window.app;
|
|
6225
|
+
const R = this.Render;
|
|
6226
|
+
const coverage = image.format === "a8";
|
|
6227
|
+
const pixmap = new Pixmap(app, { depth: coverage ? 8 : 32, width, height });
|
|
6228
|
+
const picture = new Picture(app, {
|
|
6229
|
+
drawable: pixmap,
|
|
6230
|
+
format: coverage ? R.a8 : R.rgba32,
|
|
6231
|
+
});
|
|
6232
|
+
try {
|
|
6233
|
+
R.Composite(
|
|
6234
|
+
R.PictOp.Src,
|
|
6235
|
+
image.picture(app).id,
|
|
6236
|
+
0,
|
|
6237
|
+
picture.id,
|
|
6238
|
+
x0,
|
|
6239
|
+
y0,
|
|
6240
|
+
0,
|
|
6241
|
+
0,
|
|
6242
|
+
0,
|
|
6243
|
+
0,
|
|
6244
|
+
width,
|
|
6245
|
+
height,
|
|
6246
|
+
);
|
|
6247
|
+
const copy = {
|
|
6248
|
+
width,
|
|
6249
|
+
height,
|
|
6250
|
+
format: image.format,
|
|
6251
|
+
picture: () => picture,
|
|
6252
|
+
};
|
|
6253
|
+
this.drawImage(copy, sx - x0, sy - y0, sw, sh, dx, dy, dw, dh);
|
|
6254
|
+
} finally {
|
|
6255
|
+
// queued behind the composites that read it
|
|
6256
|
+
picture.destroy();
|
|
6257
|
+
pixmap.destroy();
|
|
5599
6258
|
}
|
|
5600
6259
|
}
|
|
5601
6260
|
|
|
5602
6261
|
// general-affine drawImage: fold CTM + dest rect + source offset into the
|
|
5603
|
-
// source picture transform and composite over the transformed bbox
|
|
5604
|
-
|
|
6262
|
+
// source picture transform and composite over the transformed bbox — or,
|
|
6263
|
+
// for coverage, into the mask's (`_drawCoverageTurned`)
|
|
6264
|
+
_drawImageTransformed(image, sx, sy, sw, sh, dx, dy, dw, dh, op) {
|
|
5605
6265
|
// maps source-local (u, v) in [0..sw)x[0..sh) to device coordinates
|
|
5606
6266
|
const M = matMultiply(this._m, [dw / sw, 0, 0, dh / sh, dx, dy]);
|
|
5607
6267
|
const inv = matInvert(M);
|
|
@@ -5612,23 +6272,14 @@ class RenderingContext2d {
|
|
|
5612
6272
|
// pixel across for any image X can hold, and nothing to see.
|
|
5613
6273
|
if (!fitsFixed(inv.slice(0, 4))) return;
|
|
5614
6274
|
|
|
5615
|
-
// device bbox of the transformed dest rect, clamped to the canvas
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
const
|
|
5623
|
-
const y0 = Math.max(0, Math.floor(Math.min(...corners.map((c) => c[1]))));
|
|
5624
|
-
const x1 = Math.min(
|
|
5625
|
-
this.width,
|
|
5626
|
-
Math.ceil(Math.max(...corners.map((c) => c[0]))),
|
|
5627
|
-
);
|
|
5628
|
-
const y1 = Math.min(
|
|
5629
|
-
this.height,
|
|
5630
|
-
Math.ceil(Math.max(...corners.map((c) => c[1]))),
|
|
5631
|
-
);
|
|
6275
|
+
// device bbox of the transformed dest rect, clamped to the canvas. A crop
|
|
6276
|
+
// has been cut out of the image by now wherever this box is wider than
|
|
6277
|
+
// the rectangle (`_spilledCrop`).
|
|
6278
|
+
const turned = turnedBox(this._m, dx, dy, dw, dh);
|
|
6279
|
+
const x0 = Math.max(0, turned.x0);
|
|
6280
|
+
const y0 = Math.max(0, turned.y0);
|
|
6281
|
+
const x1 = Math.min(this.width, turned.x1);
|
|
6282
|
+
const y1 = Math.min(this.height, turned.y1);
|
|
5632
6283
|
if (x1 <= x0 || y1 <= y0) return;
|
|
5633
6284
|
|
|
5634
6285
|
// Device -> source sample, measured from the bounding box's corner: the
|
|
@@ -5649,30 +6300,42 @@ class RenderingContext2d {
|
|
|
5649
6300
|
// rather than thrown out of the paint.
|
|
5650
6301
|
if (!fitsFixed([e, f])) return;
|
|
5651
6302
|
|
|
6303
|
+
const bbox = { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
6304
|
+
const transform = [a, c, e, b, d, f, 0, 0, 1];
|
|
6305
|
+
// coverage is the mask, and the fill style the source
|
|
6306
|
+
if (image.format === "a8") {
|
|
6307
|
+
this._drawCoverageTurned(image, bbox, transform, op);
|
|
6308
|
+
return;
|
|
6309
|
+
}
|
|
6310
|
+
|
|
5652
6311
|
// A rectangular clip shrinks the box instead of becoming a mask (issue
|
|
5653
6312
|
// #307). Decided before the picture transform is installed, so a draw
|
|
5654
6313
|
// the clip rejects does not have to put it back.
|
|
5655
|
-
const
|
|
5656
|
-
const direct = this._boxedComposite(bbox, op);
|
|
6314
|
+
const direct = this._boxedComposite(bbox);
|
|
5657
6315
|
if (direct && !direct.box) return;
|
|
5658
|
-
const box = direct ? direct.box : bbox;
|
|
5659
6316
|
|
|
5660
|
-
this.
|
|
6317
|
+
const picture = image.picture(this.window.app);
|
|
6318
|
+
this.Render.SetPictureTransform(picture.id, transform);
|
|
5661
6319
|
picture.setFilter("bilinear");
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
6320
|
+
if (direct) {
|
|
6321
|
+
const box = direct.box;
|
|
6322
|
+
this.Render.Composite(
|
|
6323
|
+
op,
|
|
6324
|
+
picture.id,
|
|
6325
|
+
direct.mask,
|
|
6326
|
+
this._dst(),
|
|
6327
|
+
box.x - x0,
|
|
6328
|
+
box.y - y0,
|
|
6329
|
+
box.x,
|
|
6330
|
+
box.y,
|
|
6331
|
+
box.x,
|
|
6332
|
+
box.y,
|
|
6333
|
+
box.w,
|
|
6334
|
+
box.h,
|
|
6335
|
+
);
|
|
6336
|
+
} else {
|
|
6337
|
+
this._compositeMasked(op, picture.id, 0, 0, bbox);
|
|
6338
|
+
}
|
|
5676
6339
|
this.Render.SetPictureTransform(picture.id, [1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
5677
6340
|
picture.setFilter("nearest");
|
|
5678
6341
|
this._markDirty();
|