ntk 5.4.0 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/app.js +46 -0
- package/lib/fontconfig.js +70 -13
- package/lib/index.js +36 -18
- package/lib/renderingcontext_2d.js +901 -266
- package/lib/text/fontmanager.js +52 -14
- package/lib/text/fontsource.js +12 -1
- package/lib/text/layout.js +194 -33
- package/lib/text/shape.js +17 -0
- package/lib/window.js +18 -3
- package/package.json +1 -1
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import parseFontStyle from
|
|
2
|
-
import extrudePolyline from
|
|
3
|
-
|
|
4
|
-
import { safeRelease } from
|
|
5
|
-
import { cssColor } from
|
|
6
|
-
import Drawable from
|
|
7
|
-
import { Image } from
|
|
8
|
-
import {
|
|
1
|
+
import parseFontStyle from "canvas-fontstyle";
|
|
2
|
+
import extrudePolyline from "extrude-polyline";
|
|
3
|
+
|
|
4
|
+
import { safeRelease } from "./cleanup.js";
|
|
5
|
+
import { cssColor } from "./color.js";
|
|
6
|
+
import Drawable from "./drawable.js";
|
|
7
|
+
import { Image } from "./image.js";
|
|
8
|
+
import {
|
|
9
|
+
ImageData,
|
|
10
|
+
fromStraightRgba,
|
|
11
|
+
pixelLayout,
|
|
12
|
+
toStraightRgba,
|
|
13
|
+
} from "./imagedata.js";
|
|
9
14
|
import {
|
|
10
15
|
Path2D,
|
|
11
16
|
flattenPath,
|
|
@@ -15,17 +20,27 @@ import {
|
|
|
15
20
|
matApply,
|
|
16
21
|
matInvert,
|
|
17
22
|
matIsIdentity,
|
|
18
|
-
matMultiply
|
|
19
|
-
} from
|
|
20
|
-
import Picture from
|
|
21
|
-
import Pixmap from
|
|
22
|
-
import { routeRaster } from
|
|
23
|
-
import { compositeTraps, drawGlyphRuns } from
|
|
24
|
-
import { TextLayout } from
|
|
25
|
-
import { reorderRuns } from
|
|
26
|
-
import { trapezoidize } from
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
matMultiply,
|
|
24
|
+
} from "./path.js";
|
|
25
|
+
import Picture from "./picture.js";
|
|
26
|
+
import Pixmap from "./pixmap.js";
|
|
27
|
+
import { routeRaster } from "./rasterize.js";
|
|
28
|
+
import { compositeTraps, drawGlyphRuns } from "./text/glyphs.js";
|
|
29
|
+
import { TextLayout } from "./text/layout.js";
|
|
30
|
+
import { reorderRuns } from "./text/shape.js";
|
|
31
|
+
import { trapezoidize } from "./trapezoid.js";
|
|
32
|
+
|
|
33
|
+
/** The overlap of two {x, y, w, h} boxes, or null when they have none. */
|
|
34
|
+
function intersectBox(a, b) {
|
|
35
|
+
const x = Math.max(a.x, b.x);
|
|
36
|
+
const y = Math.max(a.y, b.y);
|
|
37
|
+
const right = Math.min(a.x + a.w, b.x + b.w);
|
|
38
|
+
const bottom = Math.min(a.y + a.h, b.y + b.h);
|
|
39
|
+
if (right <= x || bottom <= y) return null;
|
|
40
|
+
return { x, y, w: right - x, h: bottom - y };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const DEFAULT_FONT = "20px sans-serif";
|
|
29
44
|
|
|
30
45
|
/**
|
|
31
46
|
* Fallback for a context dropped without `destroy()`, matching Pixmap,
|
|
@@ -56,14 +71,14 @@ const gcRegistry = new FinalizationRegistry(({ X, gcs }) => {
|
|
|
56
71
|
/** a fillStyle with a single colour behind it, as opposed to a gradient or a
|
|
57
72
|
* caller-supplied Picture */
|
|
58
73
|
function isPlainColor(style) {
|
|
59
|
-
return typeof style ===
|
|
74
|
+
return typeof style === "string" || Array.isArray(style);
|
|
60
75
|
}
|
|
61
76
|
|
|
62
77
|
function isPictureSource(image) {
|
|
63
78
|
return (
|
|
64
79
|
image instanceof Image ||
|
|
65
80
|
(image != null &&
|
|
66
|
-
typeof image.picture ===
|
|
81
|
+
typeof image.picture === "function" &&
|
|
67
82
|
Number.isFinite(image.width) &&
|
|
68
83
|
Number.isFinite(image.height))
|
|
69
84
|
);
|
|
@@ -73,24 +88,24 @@ function isPictureSource(image) {
|
|
|
73
88
|
// map directly; with a clip/shape mask active the op only applies inside
|
|
74
89
|
// the mask coverage (outside pixels are left untouched).
|
|
75
90
|
const GCO_TO_PICTOP = {
|
|
76
|
-
|
|
77
|
-
copy:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
xor:
|
|
86
|
-
lighter:
|
|
91
|
+
"source-over": "Over",
|
|
92
|
+
copy: "Src",
|
|
93
|
+
"destination-over": "OverReverse",
|
|
94
|
+
"source-in": "In",
|
|
95
|
+
"destination-in": "InReverse",
|
|
96
|
+
"source-out": "Out",
|
|
97
|
+
"destination-out": "OutReverse",
|
|
98
|
+
"source-atop": "Atop",
|
|
99
|
+
"destination-atop": "AtopReverse",
|
|
100
|
+
xor: "Xor",
|
|
101
|
+
lighter: "Add",
|
|
87
102
|
};
|
|
88
103
|
|
|
89
104
|
// extrude-polyline has no round caps/joins: 'round' extrudes as butt/bevel
|
|
90
105
|
// and the missing coverage is unioned in afterwards as triangle-fan disks
|
|
91
106
|
// (see _strokePolys)
|
|
92
|
-
const LINE_CAP = { butt:
|
|
93
|
-
const LINE_JOIN = { miter:
|
|
107
|
+
const LINE_CAP = { butt: "butt", square: "square", round: "butt" };
|
|
108
|
+
const LINE_JOIN = { miter: "miter", bevel: "bevel", round: "bevel" };
|
|
94
109
|
|
|
95
110
|
/**
|
|
96
111
|
* Split a device-space polyline into dash "on" runs by arc length.
|
|
@@ -206,12 +221,12 @@ class RenderingContext2d {
|
|
|
206
221
|
this._gc = null;
|
|
207
222
|
this.picture = null;
|
|
208
223
|
this._bindTarget();
|
|
209
|
-
if (typeof window.on ===
|
|
210
|
-
window.on(
|
|
224
|
+
if (typeof window.on === "function") {
|
|
225
|
+
window.on("_backing", () => this._bindTarget());
|
|
211
226
|
// masks are sized to the drawable — recreate them after a resize
|
|
212
|
-
window.on(
|
|
227
|
+
window.on("resize", () => this._dropMasks());
|
|
213
228
|
// window-backed pictures are freed server-side with the window
|
|
214
|
-
window.on(
|
|
229
|
+
window.on("_destroyed", () => {
|
|
215
230
|
if (this.picture && this._target === this.window) {
|
|
216
231
|
this.picture.forget();
|
|
217
232
|
this.picture = null;
|
|
@@ -219,30 +234,29 @@ class RenderingContext2d {
|
|
|
219
234
|
});
|
|
220
235
|
}
|
|
221
236
|
|
|
222
|
-
this._solidPictures = new Map();
|
|
223
237
|
this.fillMask = null;
|
|
224
238
|
this.fillMaskDrawable = null;
|
|
225
239
|
this.clipMask = null;
|
|
226
240
|
this.clipMaskDrawable = null;
|
|
227
241
|
this._textStyle = null;
|
|
228
242
|
this._lastFontString = null;
|
|
229
|
-
this.textAlign =
|
|
230
|
-
this.textBaseline =
|
|
243
|
+
this.textAlign = "start";
|
|
244
|
+
this.textBaseline = "alphabetic";
|
|
231
245
|
|
|
232
246
|
this._path = new Path2D();
|
|
233
247
|
this._m = [1, 0, 0, 1, 0, 0];
|
|
234
248
|
this._stack = [];
|
|
235
249
|
this._clips = []; // [{ polys, rule }] in device space, already stacked
|
|
236
|
-
this._gco =
|
|
250
|
+
this._gco = "source-over";
|
|
237
251
|
this.globalAlpha = 1;
|
|
238
|
-
this.lineCap =
|
|
239
|
-
this.lineJoin =
|
|
252
|
+
this.lineCap = "butt";
|
|
253
|
+
this.lineJoin = "miter";
|
|
240
254
|
this.miterLimit = 10;
|
|
241
255
|
this._lineDash = [];
|
|
242
256
|
this._lineDashOffset = 0;
|
|
243
257
|
|
|
244
|
-
this.fillStyle =
|
|
245
|
-
this.strokeStyle =
|
|
258
|
+
this.fillStyle = "white";
|
|
259
|
+
this.strokeStyle = "black";
|
|
246
260
|
this.lineWidth = 1;
|
|
247
261
|
}
|
|
248
262
|
|
|
@@ -274,11 +288,20 @@ class RenderingContext2d {
|
|
|
274
288
|
drawable: target,
|
|
275
289
|
format,
|
|
276
290
|
polyEdge: 1,
|
|
277
|
-
polyMode: 1
|
|
291
|
+
polyMode: 1,
|
|
278
292
|
});
|
|
279
293
|
this._dropMasks();
|
|
280
294
|
}
|
|
281
295
|
|
|
296
|
+
// Undo a rectangular server-side clip. "No clip" has to be stamped as a
|
|
297
|
+
// rectangle that outlives window growth: the backing pixmap has headroom
|
|
298
|
+
// past the window and growing into it does not rebind the picture, so a
|
|
299
|
+
// restore at today's window size would keep clipping tomorrow's pixels.
|
|
300
|
+
// Coordinates are INT16 — one rect at their maximum covers any drawable.
|
|
301
|
+
_resetPictureClip() {
|
|
302
|
+
this.Render.SetPictureClipRectangles(this.picture.id, 0, 0, [0, 0, 0x7fff, 0x7fff]);
|
|
303
|
+
}
|
|
304
|
+
|
|
282
305
|
/**
|
|
283
306
|
* Release everything this context allocated server-side. Idempotent, and
|
|
284
307
|
* the context must not be drawn with afterwards.
|
|
@@ -287,10 +310,11 @@ class RenderingContext2d {
|
|
|
287
310
|
* connection outlives both, which is why this went missing for so long (see
|
|
288
311
|
* issue #156). It matters as soon as contexts are created *dynamically* —
|
|
289
312
|
* one per offscreen `Surface`, say — because without it each one
|
|
290
|
-
* permanently costs a GC
|
|
313
|
+
* permanently costs a GC and a Picture.
|
|
291
314
|
*
|
|
292
315
|
* `_backgroundPicture` and `_glyphSource` are deliberately not freed here:
|
|
293
|
-
* both are either
|
|
316
|
+
* both are either solids owned by the app (`App#solidPicture` — shared
|
|
317
|
+
* with every other context, freed with the connection) or a
|
|
294
318
|
* `Picture`/`CanvasGradient` the caller passed in through `fillStyle`,
|
|
295
319
|
* which is not ours to free.
|
|
296
320
|
*/
|
|
@@ -311,11 +335,6 @@ class RenderingContext2d {
|
|
|
311
335
|
this._gc = this._fillMaskGC = null;
|
|
312
336
|
this._gcs.length = 0;
|
|
313
337
|
|
|
314
|
-
for (const picture of this._solidPictures.values()) {
|
|
315
|
-
picture.destroy();
|
|
316
|
-
picture._sourcePixmap?.destroy();
|
|
317
|
-
}
|
|
318
|
-
this._solidPictures.clear();
|
|
319
338
|
this._backgroundPicture = this._glyphSource = null;
|
|
320
339
|
|
|
321
340
|
if (this.picture) {
|
|
@@ -351,7 +370,7 @@ class RenderingContext2d {
|
|
|
351
370
|
|
|
352
371
|
// notify a double-buffered window that its backing content changed
|
|
353
372
|
_markDirty() {
|
|
354
|
-
if (typeof this.window._markDirty !==
|
|
373
|
+
if (typeof this.window._markDirty !== "function") return;
|
|
355
374
|
// The clip bounds everything this operation could have touched, so it is
|
|
356
375
|
// also the region the window has to blit. Reporting it lets the present
|
|
357
376
|
// copy the part of the backing store that changed instead of all of it —
|
|
@@ -375,7 +394,7 @@ class RenderingContext2d {
|
|
|
375
394
|
}
|
|
376
395
|
|
|
377
396
|
_op() {
|
|
378
|
-
return this.Render.PictOp[GCO_TO_PICTOP[this._gco] ||
|
|
397
|
+
return this.Render.PictOp[GCO_TO_PICTOP[this._gco] || "Over"];
|
|
379
398
|
}
|
|
380
399
|
|
|
381
400
|
set globalCompositeOperation(value) {
|
|
@@ -386,31 +405,21 @@ class RenderingContext2d {
|
|
|
386
405
|
return this._gco;
|
|
387
406
|
}
|
|
388
407
|
|
|
408
|
+
// solids live on the app, not the context: contexts can be as short-lived
|
|
409
|
+
// as one Surface.render call, and the colours outlive all of them
|
|
389
410
|
createSolidPicture(r, g, b, a) {
|
|
390
|
-
|
|
391
|
-
let p = this._solidPictures.get(key);
|
|
392
|
-
if (p) return p;
|
|
393
|
-
|
|
394
|
-
// TODO: use window.createPixmap
|
|
395
|
-
const pixmap = new Pixmap(this.window.app, { depth: 32, width: 1, height: 1 });
|
|
396
|
-
const pictSolidPix = this.window.X.AllocID();
|
|
397
|
-
this.Render.CreatePicture(pictSolidPix, pixmap.id, this.Render.rgba32, { repeat: 1 });
|
|
398
|
-
this.Render.FillRectangles(1, pictSolidPix, [r, g, b, a], [0, 0, 100, 100]);
|
|
399
|
-
p = new Picture(this.window.app, { id: pictSolidPix });
|
|
400
|
-
p._sourcePixmap = pixmap; // keep the 1x1 pixmap alive alongside the picture
|
|
401
|
-
this._solidPictures.set(key, p);
|
|
402
|
-
return p;
|
|
411
|
+
return this.window.app.solidPicture(r, g, b, a);
|
|
403
412
|
}
|
|
404
413
|
|
|
405
414
|
_stylePicture(value) {
|
|
406
|
-
if (typeof value ===
|
|
415
|
+
if (typeof value === "string" || Array.isArray(value)) {
|
|
407
416
|
const c = parseColor(value);
|
|
408
417
|
return this.createSolidPicture(c[0], c[1], c[2], c[3]);
|
|
409
418
|
}
|
|
410
419
|
if (value instanceof Picture || value instanceof CanvasGradient) {
|
|
411
420
|
return value;
|
|
412
421
|
}
|
|
413
|
-
throw new Error(
|
|
422
|
+
throw new Error("Unknown fill style");
|
|
414
423
|
}
|
|
415
424
|
|
|
416
425
|
set fillStyle(value) {
|
|
@@ -476,7 +485,7 @@ class RenderingContext2d {
|
|
|
476
485
|
textAlign: this.textAlign,
|
|
477
486
|
textBaseline: this.textBaseline,
|
|
478
487
|
m: this._m.slice(),
|
|
479
|
-
clips: this._clips
|
|
488
|
+
clips: this._clips,
|
|
480
489
|
});
|
|
481
490
|
}
|
|
482
491
|
|
|
@@ -526,9 +535,11 @@ class RenderingContext2d {
|
|
|
526
535
|
}
|
|
527
536
|
|
|
528
537
|
setTransform(a, b, c, d, e, f) {
|
|
529
|
-
if (typeof a ===
|
|
538
|
+
if (typeof a === "object" && a !== null) {
|
|
530
539
|
const m = a;
|
|
531
|
-
this._m = Array.isArray(m)
|
|
540
|
+
this._m = Array.isArray(m)
|
|
541
|
+
? m.slice(0, 6)
|
|
542
|
+
: [m.a, m.b, m.c, m.d, m.e, m.f];
|
|
532
543
|
return;
|
|
533
544
|
}
|
|
534
545
|
if (a === undefined) return this.resetTransform();
|
|
@@ -587,13 +598,35 @@ class RenderingContext2d {
|
|
|
587
598
|
}
|
|
588
599
|
|
|
589
600
|
arc(x, y, r, startAngle, endAngle, counterclockwise = false) {
|
|
590
|
-
if (r < 0) throw new RangeError(
|
|
591
|
-
this._appendUserSegments(
|
|
601
|
+
if (r < 0) throw new RangeError("arc: negative radius");
|
|
602
|
+
this._appendUserSegments(
|
|
603
|
+
ellipseSegments(x, y, r, r, 0, startAngle, endAngle, counterclockwise),
|
|
604
|
+
);
|
|
592
605
|
}
|
|
593
606
|
|
|
594
|
-
ellipse(
|
|
595
|
-
|
|
596
|
-
|
|
607
|
+
ellipse(
|
|
608
|
+
x,
|
|
609
|
+
y,
|
|
610
|
+
rx,
|
|
611
|
+
ry,
|
|
612
|
+
rotation,
|
|
613
|
+
startAngle,
|
|
614
|
+
endAngle,
|
|
615
|
+
counterclockwise = false,
|
|
616
|
+
) {
|
|
617
|
+
if (rx < 0 || ry < 0) throw new RangeError("ellipse: negative radius");
|
|
618
|
+
this._appendUserSegments(
|
|
619
|
+
ellipseSegments(
|
|
620
|
+
x,
|
|
621
|
+
y,
|
|
622
|
+
rx,
|
|
623
|
+
ry,
|
|
624
|
+
rotation,
|
|
625
|
+
startAngle,
|
|
626
|
+
endAngle,
|
|
627
|
+
counterclockwise,
|
|
628
|
+
),
|
|
629
|
+
);
|
|
597
630
|
}
|
|
598
631
|
|
|
599
632
|
arcTo(x1, y1, x2, y2, r) {
|
|
@@ -625,12 +658,12 @@ class RenderingContext2d {
|
|
|
625
658
|
if (args[0] instanceof Path2D) {
|
|
626
659
|
return {
|
|
627
660
|
polys: flattenPath(args[0]._cmds, this._m),
|
|
628
|
-
rule: args[1] ===
|
|
661
|
+
rule: args[1] === "evenodd" ? "evenodd" : "nonzero",
|
|
629
662
|
};
|
|
630
663
|
}
|
|
631
664
|
return {
|
|
632
665
|
polys: flattenPath(this._path._cmds, null),
|
|
633
|
-
rule: args[0] ===
|
|
666
|
+
rule: args[0] === "evenodd" ? "evenodd" : "nonzero",
|
|
634
667
|
};
|
|
635
668
|
}
|
|
636
669
|
|
|
@@ -642,11 +675,11 @@ class RenderingContext2d {
|
|
|
642
675
|
this.fillMaskDrawable = new Pixmap(this.window.app, {
|
|
643
676
|
depth: 8,
|
|
644
677
|
width: this.width,
|
|
645
|
-
height: this.height
|
|
678
|
+
height: this.height,
|
|
646
679
|
});
|
|
647
680
|
this.fillMask = new Picture(this.window.app, {
|
|
648
681
|
drawable: this.fillMaskDrawable,
|
|
649
|
-
format: this.Render.a8
|
|
682
|
+
format: this.Render.a8,
|
|
650
683
|
});
|
|
651
684
|
}
|
|
652
685
|
|
|
@@ -655,19 +688,19 @@ class RenderingContext2d {
|
|
|
655
688
|
this.clipMaskDrawable = new Pixmap(this.window.app, {
|
|
656
689
|
depth: 8,
|
|
657
690
|
width: this.width,
|
|
658
|
-
height: this.height
|
|
691
|
+
height: this.height,
|
|
659
692
|
});
|
|
660
693
|
this.clipMask = new Picture(this.window.app, {
|
|
661
694
|
drawable: this.clipMaskDrawable,
|
|
662
|
-
format: this.Render.a8
|
|
695
|
+
format: this.Render.a8,
|
|
663
696
|
});
|
|
664
697
|
}
|
|
665
698
|
|
|
666
|
-
_rasterizePolys(picture, polys, rule) {
|
|
699
|
+
_rasterizePolys(picture, polys, rule, dx = 0, dy = 0) {
|
|
667
700
|
const flat = [];
|
|
668
701
|
for (const p of polys) if (p.pts.length >= 6) flat.push(p.pts);
|
|
669
702
|
if (!flat.length) return;
|
|
670
|
-
const traps = trapezoidize(flat,
|
|
703
|
+
const traps = trapezoidize(flat, dx, dy, [], rule);
|
|
671
704
|
// stay under the server's maximum request size
|
|
672
705
|
const chunk = 4000 * 6;
|
|
673
706
|
for (let i = 0; i < traps.length; i += chunk) {
|
|
@@ -699,7 +732,8 @@ class RenderingContext2d {
|
|
|
699
732
|
_uploadCoverage(job, b, edges) {
|
|
700
733
|
const rasterizer = this.window.app.rasterizer;
|
|
701
734
|
if (!rasterizer) return false;
|
|
702
|
-
if (routeRaster(b.w, b.h, edges, this.window.app.rasterPolicy) !==
|
|
735
|
+
if (routeRaster(b.w, b.h, edges, this.window.app.rasterPolicy) !== "local")
|
|
736
|
+
return false;
|
|
703
737
|
|
|
704
738
|
const coverage = rasterizer.rasterize({ ...job, width: b.w, height: b.h });
|
|
705
739
|
if (!coverage) return false;
|
|
@@ -717,10 +751,24 @@ class RenderingContext2d {
|
|
|
717
751
|
} else {
|
|
718
752
|
data = Buffer.alloc(stride * b.h);
|
|
719
753
|
for (let y = 0; y < b.h; ++y) {
|
|
720
|
-
Buffer.from(coverage.buffer, coverage.byteOffset + y * b.w, b.w).copy(
|
|
754
|
+
Buffer.from(coverage.buffer, coverage.byteOffset + y * b.w, b.w).copy(
|
|
755
|
+
data,
|
|
756
|
+
y * stride,
|
|
757
|
+
);
|
|
721
758
|
}
|
|
722
759
|
}
|
|
723
|
-
this.X.PutImage(
|
|
760
|
+
this.X.PutImage(
|
|
761
|
+
2,
|
|
762
|
+
this.fillMaskDrawable.id,
|
|
763
|
+
this._maskGC(),
|
|
764
|
+
b.w,
|
|
765
|
+
b.h,
|
|
766
|
+
b.x,
|
|
767
|
+
b.y,
|
|
768
|
+
0,
|
|
769
|
+
8,
|
|
770
|
+
data,
|
|
771
|
+
);
|
|
724
772
|
return true;
|
|
725
773
|
}
|
|
726
774
|
|
|
@@ -802,22 +850,74 @@ class RenderingContext2d {
|
|
|
802
850
|
flat.push(p.pts);
|
|
803
851
|
edges += p.pts.length / 2;
|
|
804
852
|
}
|
|
805
|
-
if (
|
|
806
|
-
|
|
853
|
+
if (
|
|
854
|
+
!this._uploadCoverage({ polys: flat, rule, dx: -b.x, dy: -b.y }, b, edges)
|
|
855
|
+
) {
|
|
856
|
+
R.FillRectangles(
|
|
857
|
+
R.PictOp.Src,
|
|
858
|
+
this.fillMask.id,
|
|
859
|
+
[0, 0, 0, 0],
|
|
860
|
+
[b.x, b.y, b.w, b.h],
|
|
861
|
+
);
|
|
807
862
|
this._rasterizePolys(this.fillMask, polys, rule);
|
|
808
863
|
}
|
|
809
864
|
|
|
865
|
+
// a rectangular clip narrows where the coverage is *composited*; the
|
|
866
|
+
// mask content outside it is stale by the same argument as outside `b`.
|
|
867
|
+
// A stack whose mask was dropped by restore() may still hold a poly —
|
|
868
|
+
// _clipRect says null then, and the mask comes back on demand.
|
|
869
|
+
let out = b;
|
|
870
|
+
if (this._clips.length && !this.clipMask) {
|
|
871
|
+
const cr = this._clipRect();
|
|
872
|
+
if (cr) {
|
|
873
|
+
out = intersectBox(b, cr);
|
|
874
|
+
if (!out) return;
|
|
875
|
+
} else {
|
|
876
|
+
this._requireClipMask();
|
|
877
|
+
}
|
|
878
|
+
}
|
|
810
879
|
if (alpha < 1) {
|
|
811
880
|
// In with a constant color scales the a8 coverage by that alpha
|
|
812
|
-
R.FillRectangles(
|
|
881
|
+
R.FillRectangles(
|
|
882
|
+
R.PictOp.In,
|
|
883
|
+
this.fillMask.id,
|
|
884
|
+
[0, 0, 0, alpha],
|
|
885
|
+
[out.x, out.y, out.w, out.h],
|
|
886
|
+
);
|
|
813
887
|
}
|
|
814
888
|
if (this.clipMask) {
|
|
815
889
|
// clipMask is surface-aligned, so it is sampled at the same offset
|
|
816
|
-
R.Composite(
|
|
890
|
+
R.Composite(
|
|
891
|
+
R.PictOp.In,
|
|
892
|
+
this.clipMask.id,
|
|
893
|
+
0,
|
|
894
|
+
this.fillMask.id,
|
|
895
|
+
out.x,
|
|
896
|
+
out.y,
|
|
897
|
+
0,
|
|
898
|
+
0,
|
|
899
|
+
out.x,
|
|
900
|
+
out.y,
|
|
901
|
+
out.w,
|
|
902
|
+
out.h,
|
|
903
|
+
);
|
|
817
904
|
}
|
|
818
905
|
// src is either a 1x1 repeating solid (offset irrelevant) or a
|
|
819
906
|
// surface-aligned gradient, so it is sampled at the same offset too
|
|
820
|
-
R.Composite(
|
|
907
|
+
R.Composite(
|
|
908
|
+
op,
|
|
909
|
+
src.id,
|
|
910
|
+
this.fillMask.id,
|
|
911
|
+
this.picture.id,
|
|
912
|
+
out.x,
|
|
913
|
+
out.y,
|
|
914
|
+
out.x,
|
|
915
|
+
out.y,
|
|
916
|
+
out.x,
|
|
917
|
+
out.y,
|
|
918
|
+
out.w,
|
|
919
|
+
out.h,
|
|
920
|
+
);
|
|
821
921
|
this._markDirty();
|
|
822
922
|
}
|
|
823
923
|
|
|
@@ -828,16 +928,18 @@ class RenderingContext2d {
|
|
|
828
928
|
const det = this._m[0] * this._m[3] - this._m[1] * this._m[2];
|
|
829
929
|
const scale = Math.sqrt(Math.abs(det)) || 1;
|
|
830
930
|
const thickness = this.lineWidth * scale;
|
|
831
|
-
const roundCap = this.lineCap ===
|
|
832
|
-
const roundJoin = this.lineJoin ===
|
|
931
|
+
const roundCap = this.lineCap === "round";
|
|
932
|
+
const roundJoin = this.lineJoin === "round";
|
|
833
933
|
const stroke = extrudePolyline({
|
|
834
934
|
thickness,
|
|
835
|
-
cap: LINE_CAP[this.lineCap] ||
|
|
836
|
-
join: LINE_JOIN[this.lineJoin] ||
|
|
837
|
-
miterLimit: this.miterLimit
|
|
935
|
+
cap: LINE_CAP[this.lineCap] || "butt",
|
|
936
|
+
join: LINE_JOIN[this.lineJoin] || "miter",
|
|
937
|
+
miterLimit: this.miterLimit,
|
|
838
938
|
});
|
|
839
939
|
// dash distances are user-space lengths; scale them like the line width
|
|
840
|
-
const dash = this._lineDash.length
|
|
940
|
+
const dash = this._lineDash.length
|
|
941
|
+
? this._lineDash.map((d) => d * scale)
|
|
942
|
+
: null;
|
|
841
943
|
const dashOffset = this._lineDashOffset * scale;
|
|
842
944
|
|
|
843
945
|
const tris = [];
|
|
@@ -867,7 +969,9 @@ class RenderingContext2d {
|
|
|
867
969
|
const l1 = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
868
970
|
const l2 = Math.hypot(c[0] - b[0], c[1] - b[1]);
|
|
869
971
|
if (!l1 || !l2) return;
|
|
870
|
-
let dot =
|
|
972
|
+
let dot =
|
|
973
|
+
((b[0] - a[0]) * (c[0] - b[0]) + (b[1] - a[1]) * (c[1] - b[1])) /
|
|
974
|
+
(l1 * l2);
|
|
871
975
|
dot = Math.max(-1, Math.min(1, dot));
|
|
872
976
|
// bevel-to-arc gap depth for turn angle θ: r * (1 - cos(θ/2))
|
|
873
977
|
if (r * (1 - Math.sqrt((1 + dot) / 2)) > 0.05) addDisk(b[0], b[1]);
|
|
@@ -888,9 +992,11 @@ class RenderingContext2d {
|
|
|
888
992
|
}
|
|
889
993
|
}
|
|
890
994
|
if (roundJoin) {
|
|
891
|
-
for (let i = 1; i < pts.length - 1; i++)
|
|
995
|
+
for (let i = 1; i < pts.length - 1; i++)
|
|
996
|
+
maybeJoinDisk(pts[i - 1], pts[i], pts[i + 1]);
|
|
892
997
|
// the seam of a closed loop is a join too (pts[0] === pts[last])
|
|
893
|
-
if (closed && pts.length > 2)
|
|
998
|
+
if (closed && pts.length > 2)
|
|
999
|
+
maybeJoinDisk(pts[pts.length - 2], pts[0], pts[1]);
|
|
894
1000
|
}
|
|
895
1001
|
if (roundCap && !closed) {
|
|
896
1002
|
addDisk(pts[0][0], pts[0][1]);
|
|
@@ -903,7 +1009,8 @@ class RenderingContext2d {
|
|
|
903
1009
|
for (let i = 1; i < run.length; i++) {
|
|
904
1010
|
const p = run[i];
|
|
905
1011
|
const q = out[out.length - 1];
|
|
906
|
-
if (Math.abs(p[0] - q[0]) > 1e-6 || Math.abs(p[1] - q[1]) > 1e-6)
|
|
1012
|
+
if (Math.abs(p[0] - q[0]) > 1e-6 || Math.abs(p[1] - q[1]) > 1e-6)
|
|
1013
|
+
out.push(p);
|
|
907
1014
|
}
|
|
908
1015
|
return out;
|
|
909
1016
|
};
|
|
@@ -924,7 +1031,8 @@ class RenderingContext2d {
|
|
|
924
1031
|
}
|
|
925
1032
|
if (pts.length >= 2 && poly.closed) {
|
|
926
1033
|
const [fx, fy] = pts[0];
|
|
927
|
-
if (Math.abs(fx - lx) > 1e-6 || Math.abs(fy - ly) > 1e-6)
|
|
1034
|
+
if (Math.abs(fx - lx) > 1e-6 || Math.abs(fy - ly) > 1e-6)
|
|
1035
|
+
pts.push([fx, fy]);
|
|
928
1036
|
}
|
|
929
1037
|
if (pts.length < 2) continue;
|
|
930
1038
|
|
|
@@ -947,11 +1055,23 @@ class RenderingContext2d {
|
|
|
947
1055
|
// round-cap/join disks overlap the stroke body; overlapping coverage
|
|
948
1056
|
// must accumulate in the clamped a8 mask (single composite) or a
|
|
949
1057
|
// semi-transparent stroke style would double-blend at the overlaps
|
|
950
|
-
const direct =
|
|
1058
|
+
const direct =
|
|
1059
|
+
!hasRound &&
|
|
1060
|
+
this.globalAlpha >= 1 &&
|
|
1061
|
+
!this._clips.length &&
|
|
1062
|
+
op === this.Render.PictOp.Over;
|
|
951
1063
|
const chunk = 4000 * 6;
|
|
952
1064
|
if (direct) {
|
|
953
1065
|
for (let i = 0; i < tris.length; i += chunk) {
|
|
954
|
-
this.Render.Triangles(
|
|
1066
|
+
this.Render.Triangles(
|
|
1067
|
+
op,
|
|
1068
|
+
src.id,
|
|
1069
|
+
0,
|
|
1070
|
+
0,
|
|
1071
|
+
this.picture.id,
|
|
1072
|
+
this.Render.a8,
|
|
1073
|
+
tris.slice(i, i + chunk),
|
|
1074
|
+
);
|
|
955
1075
|
}
|
|
956
1076
|
this._markDirty();
|
|
957
1077
|
return;
|
|
@@ -970,20 +1090,80 @@ class RenderingContext2d {
|
|
|
970
1090
|
this._ensureFillMask();
|
|
971
1091
|
const b = this._trisBBox(tris);
|
|
972
1092
|
if (!b) return;
|
|
973
|
-
if (
|
|
974
|
-
this.
|
|
1093
|
+
if (
|
|
1094
|
+
!this._uploadCoverage(
|
|
1095
|
+
{ triangles: tris, dx: -b.x, dy: -b.y },
|
|
1096
|
+
b,
|
|
1097
|
+
tris.length / 2,
|
|
1098
|
+
)
|
|
1099
|
+
) {
|
|
1100
|
+
this.Render.FillRectangles(
|
|
1101
|
+
this.Render.PictOp.Src,
|
|
1102
|
+
this.fillMask.id,
|
|
1103
|
+
[0, 0, 0, 0],
|
|
1104
|
+
[b.x, b.y, b.w, b.h],
|
|
1105
|
+
);
|
|
975
1106
|
const opaque = this.createSolidPicture(0, 0, 0, 1);
|
|
976
1107
|
for (let i = 0; i < tris.length; i += chunk) {
|
|
977
|
-
this.Render.Triangles(
|
|
1108
|
+
this.Render.Triangles(
|
|
1109
|
+
this.Render.PictOp.Add,
|
|
1110
|
+
opaque.id,
|
|
1111
|
+
0,
|
|
1112
|
+
0,
|
|
1113
|
+
this.fillMask.id,
|
|
1114
|
+
this.Render.a8,
|
|
1115
|
+
tris.slice(i, i + chunk),
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
let out = b;
|
|
1120
|
+
if (this._clips.length && !this.clipMask) {
|
|
1121
|
+
const cr = this._clipRect();
|
|
1122
|
+
if (cr) {
|
|
1123
|
+
out = intersectBox(b, cr);
|
|
1124
|
+
if (!out) return;
|
|
1125
|
+
} else {
|
|
1126
|
+
this._requireClipMask();
|
|
978
1127
|
}
|
|
979
1128
|
}
|
|
980
1129
|
if (this.globalAlpha < 1) {
|
|
981
|
-
this.Render.FillRectangles(
|
|
1130
|
+
this.Render.FillRectangles(
|
|
1131
|
+
this.Render.PictOp.In,
|
|
1132
|
+
this.fillMask.id,
|
|
1133
|
+
[0, 0, 0, this.globalAlpha],
|
|
1134
|
+
[out.x, out.y, out.w, out.h],
|
|
1135
|
+
);
|
|
982
1136
|
}
|
|
983
1137
|
if (this.clipMask) {
|
|
984
|
-
this.Render.Composite(
|
|
1138
|
+
this.Render.Composite(
|
|
1139
|
+
this.Render.PictOp.In,
|
|
1140
|
+
this.clipMask.id,
|
|
1141
|
+
0,
|
|
1142
|
+
this.fillMask.id,
|
|
1143
|
+
out.x,
|
|
1144
|
+
out.y,
|
|
1145
|
+
0,
|
|
1146
|
+
0,
|
|
1147
|
+
out.x,
|
|
1148
|
+
out.y,
|
|
1149
|
+
out.w,
|
|
1150
|
+
out.h,
|
|
1151
|
+
);
|
|
985
1152
|
}
|
|
986
|
-
this.Render.Composite(
|
|
1153
|
+
this.Render.Composite(
|
|
1154
|
+
op,
|
|
1155
|
+
src.id,
|
|
1156
|
+
this.fillMask.id,
|
|
1157
|
+
this.picture.id,
|
|
1158
|
+
out.x,
|
|
1159
|
+
out.y,
|
|
1160
|
+
out.x,
|
|
1161
|
+
out.y,
|
|
1162
|
+
out.x,
|
|
1163
|
+
out.y,
|
|
1164
|
+
out.w,
|
|
1165
|
+
out.h,
|
|
1166
|
+
);
|
|
987
1167
|
this._markDirty();
|
|
988
1168
|
}
|
|
989
1169
|
|
|
@@ -1000,7 +1180,7 @@ class RenderingContext2d {
|
|
|
1000
1180
|
drawGlyphs(op, src, positioned) {
|
|
1001
1181
|
const app = this.window.app;
|
|
1002
1182
|
const R = this.Render;
|
|
1003
|
-
if (!this.
|
|
1183
|
+
if (!this._clips.length) {
|
|
1004
1184
|
drawGlyphRuns(app, op, src.id, this.picture.id, positioned);
|
|
1005
1185
|
this._markDirty();
|
|
1006
1186
|
return;
|
|
@@ -1012,19 +1192,62 @@ class RenderingContext2d {
|
|
|
1012
1192
|
const rect = this._clipRect();
|
|
1013
1193
|
if (rect) {
|
|
1014
1194
|
if (rect.w === 0 || rect.h === 0) return;
|
|
1015
|
-
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
1195
|
+
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
1196
|
+
rect.x,
|
|
1197
|
+
rect.y,
|
|
1198
|
+
rect.w,
|
|
1199
|
+
rect.h,
|
|
1200
|
+
]);
|
|
1016
1201
|
drawGlyphRuns(app, op, src.id, this.picture.id, positioned);
|
|
1017
|
-
|
|
1202
|
+
this._resetPictureClip();
|
|
1018
1203
|
this._markDirty();
|
|
1019
1204
|
return;
|
|
1020
1205
|
}
|
|
1206
|
+
const clipMask = this._requireClipMask();
|
|
1021
1207
|
this._ensureFillMask();
|
|
1022
1208
|
this._glyphSource ??= this.createSolidPicture(1, 1, 1, 1);
|
|
1023
|
-
R.FillRectangles(
|
|
1209
|
+
R.FillRectangles(
|
|
1210
|
+
R.PictOp.Src,
|
|
1211
|
+
this.fillMask.id,
|
|
1212
|
+
[0, 0, 0, 0],
|
|
1213
|
+
[0, 0, this.width, this.height],
|
|
1214
|
+
);
|
|
1024
1215
|
// solid white through the glyphs leaves their coverage in the a8 mask
|
|
1025
|
-
drawGlyphRuns(
|
|
1026
|
-
|
|
1027
|
-
|
|
1216
|
+
drawGlyphRuns(
|
|
1217
|
+
app,
|
|
1218
|
+
R.PictOp.Over,
|
|
1219
|
+
this._glyphSource.id,
|
|
1220
|
+
this.fillMask.id,
|
|
1221
|
+
positioned,
|
|
1222
|
+
);
|
|
1223
|
+
R.Composite(
|
|
1224
|
+
R.PictOp.In,
|
|
1225
|
+
clipMask.id,
|
|
1226
|
+
0,
|
|
1227
|
+
this.fillMask.id,
|
|
1228
|
+
0,
|
|
1229
|
+
0,
|
|
1230
|
+
0,
|
|
1231
|
+
0,
|
|
1232
|
+
0,
|
|
1233
|
+
0,
|
|
1234
|
+
this.width,
|
|
1235
|
+
this.height,
|
|
1236
|
+
);
|
|
1237
|
+
R.Composite(
|
|
1238
|
+
op,
|
|
1239
|
+
src.id,
|
|
1240
|
+
this.fillMask.id,
|
|
1241
|
+
this.picture.id,
|
|
1242
|
+
0,
|
|
1243
|
+
0,
|
|
1244
|
+
0,
|
|
1245
|
+
0,
|
|
1246
|
+
0,
|
|
1247
|
+
0,
|
|
1248
|
+
this.width,
|
|
1249
|
+
this.height,
|
|
1250
|
+
);
|
|
1028
1251
|
this._markDirty();
|
|
1029
1252
|
}
|
|
1030
1253
|
|
|
@@ -1038,7 +1261,7 @@ class RenderingContext2d {
|
|
|
1038
1261
|
if (!traps.length) return;
|
|
1039
1262
|
const app = this.window.app;
|
|
1040
1263
|
const R = this.Render;
|
|
1041
|
-
if (!this.
|
|
1264
|
+
if (!this._clips.length) {
|
|
1042
1265
|
compositeTraps(app, op, src.id, this.picture.id, traps);
|
|
1043
1266
|
this._markDirty();
|
|
1044
1267
|
return;
|
|
@@ -1046,29 +1269,93 @@ class RenderingContext2d {
|
|
|
1046
1269
|
const rect = this._clipRect();
|
|
1047
1270
|
if (rect) {
|
|
1048
1271
|
if (rect.w === 0 || rect.h === 0) return;
|
|
1049
|
-
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
1272
|
+
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
1273
|
+
rect.x,
|
|
1274
|
+
rect.y,
|
|
1275
|
+
rect.w,
|
|
1276
|
+
rect.h,
|
|
1277
|
+
]);
|
|
1050
1278
|
compositeTraps(app, op, src.id, this.picture.id, traps);
|
|
1051
|
-
|
|
1279
|
+
this._resetPictureClip();
|
|
1052
1280
|
this._markDirty();
|
|
1053
1281
|
return;
|
|
1054
1282
|
}
|
|
1283
|
+
const clipMask = this._requireClipMask();
|
|
1055
1284
|
this._ensureFillMask();
|
|
1056
1285
|
this._glyphSource ??= this.createSolidPicture(1, 1, 1, 1);
|
|
1057
|
-
R.FillRectangles(
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1286
|
+
R.FillRectangles(
|
|
1287
|
+
R.PictOp.Src,
|
|
1288
|
+
this.fillMask.id,
|
|
1289
|
+
[0, 0, 0, 0],
|
|
1290
|
+
[0, 0, this.width, this.height],
|
|
1291
|
+
);
|
|
1292
|
+
compositeTraps(
|
|
1293
|
+
app,
|
|
1294
|
+
R.PictOp.Over,
|
|
1295
|
+
this._glyphSource.id,
|
|
1296
|
+
this.fillMask.id,
|
|
1297
|
+
traps,
|
|
1298
|
+
);
|
|
1299
|
+
R.Composite(
|
|
1300
|
+
R.PictOp.In,
|
|
1301
|
+
clipMask.id,
|
|
1302
|
+
0,
|
|
1303
|
+
this.fillMask.id,
|
|
1304
|
+
0,
|
|
1305
|
+
0,
|
|
1306
|
+
0,
|
|
1307
|
+
0,
|
|
1308
|
+
0,
|
|
1309
|
+
0,
|
|
1310
|
+
this.width,
|
|
1311
|
+
this.height,
|
|
1312
|
+
);
|
|
1313
|
+
R.Composite(
|
|
1314
|
+
op,
|
|
1315
|
+
src.id,
|
|
1316
|
+
this.fillMask.id,
|
|
1317
|
+
this.picture.id,
|
|
1318
|
+
0,
|
|
1319
|
+
0,
|
|
1320
|
+
0,
|
|
1321
|
+
0,
|
|
1322
|
+
0,
|
|
1323
|
+
0,
|
|
1324
|
+
this.width,
|
|
1325
|
+
this.height,
|
|
1326
|
+
);
|
|
1061
1327
|
this._markDirty();
|
|
1062
1328
|
}
|
|
1063
1329
|
|
|
1064
1330
|
// combined clip ∩ globalAlpha mask for direct composites (rect/image
|
|
1065
1331
|
// fast paths); returns a picture id or 0. Reuses the fill scratch mask.
|
|
1066
1332
|
_compositeMask() {
|
|
1067
|
-
|
|
1333
|
+
// reached only when the clip is not a rectangle (those went through the
|
|
1334
|
+
// SetPictureClipRectangles path), so a clip here always means the mask
|
|
1335
|
+
const clipMask = this._clips.length ? this._requireClipMask() : null;
|
|
1336
|
+
if (this.globalAlpha >= 1) return clipMask ? clipMask.id : 0;
|
|
1068
1337
|
this._ensureFillMask();
|
|
1069
|
-
this.Render.FillRectangles(
|
|
1070
|
-
|
|
1071
|
-
this.
|
|
1338
|
+
this.Render.FillRectangles(
|
|
1339
|
+
this.Render.PictOp.Src,
|
|
1340
|
+
this.fillMask.id,
|
|
1341
|
+
[0, 0, 0, this.globalAlpha],
|
|
1342
|
+
[0, 0, this.width, this.height],
|
|
1343
|
+
);
|
|
1344
|
+
if (clipMask) {
|
|
1345
|
+
this.Render.Composite(
|
|
1346
|
+
this.Render.PictOp.In,
|
|
1347
|
+
clipMask.id,
|
|
1348
|
+
0,
|
|
1349
|
+
this.fillMask.id,
|
|
1350
|
+
0,
|
|
1351
|
+
0,
|
|
1352
|
+
0,
|
|
1353
|
+
0,
|
|
1354
|
+
0,
|
|
1355
|
+
0,
|
|
1356
|
+
this.width,
|
|
1357
|
+
this.height,
|
|
1358
|
+
);
|
|
1072
1359
|
}
|
|
1073
1360
|
return this.fillMask.id;
|
|
1074
1361
|
}
|
|
@@ -1077,8 +1364,13 @@ class RenderingContext2d {
|
|
|
1077
1364
|
// drawing
|
|
1078
1365
|
|
|
1079
1366
|
clearRect(x, y, w, h) {
|
|
1080
|
-
if (matIsIdentity(this._m) && !this.
|
|
1081
|
-
this.Render.FillRectangles(
|
|
1367
|
+
if (matIsIdentity(this._m) && !this._clips.length) {
|
|
1368
|
+
this.Render.FillRectangles(
|
|
1369
|
+
this.Render.PictOp.Src,
|
|
1370
|
+
this.picture.id,
|
|
1371
|
+
[1, 1, 1, 1],
|
|
1372
|
+
[x, y, w, h],
|
|
1373
|
+
);
|
|
1082
1374
|
this._markDirty();
|
|
1083
1375
|
return;
|
|
1084
1376
|
}
|
|
@@ -1086,10 +1378,10 @@ class RenderingContext2d {
|
|
|
1086
1378
|
tmp.rect(x, y, w, h);
|
|
1087
1379
|
// clear = opaque white, ignoring alpha and the composite op (but
|
|
1088
1380
|
// honoring the clip), matching the identity fast path above
|
|
1089
|
-
this._fillPolys(flattenPath(tmp._cmds, this._m),
|
|
1381
|
+
this._fillPolys(flattenPath(tmp._cmds, this._m), "nonzero", {
|
|
1090
1382
|
src: this.createSolidPicture(1, 1, 1, 1),
|
|
1091
1383
|
op: this.Render.PictOp.Over,
|
|
1092
|
-
alpha: 1
|
|
1384
|
+
alpha: 1,
|
|
1093
1385
|
});
|
|
1094
1386
|
}
|
|
1095
1387
|
|
|
@@ -1101,17 +1393,21 @@ class RenderingContext2d {
|
|
|
1101
1393
|
this._backgroundPicture.id,
|
|
1102
1394
|
mask,
|
|
1103
1395
|
this.picture.id,
|
|
1104
|
-
x,
|
|
1105
|
-
|
|
1106
|
-
x,
|
|
1107
|
-
|
|
1396
|
+
x,
|
|
1397
|
+
y,
|
|
1398
|
+
x,
|
|
1399
|
+
y,
|
|
1400
|
+
x,
|
|
1401
|
+
y,
|
|
1402
|
+
w,
|
|
1403
|
+
h,
|
|
1108
1404
|
);
|
|
1109
1405
|
this._markDirty();
|
|
1110
1406
|
return;
|
|
1111
1407
|
}
|
|
1112
1408
|
const tmp = new Path2D();
|
|
1113
1409
|
tmp.rect(x, y, w, h);
|
|
1114
|
-
this._fillPolys(flattenPath(tmp._cmds, this._m),
|
|
1410
|
+
this._fillPolys(flattenPath(tmp._cmds, this._m), "nonzero");
|
|
1115
1411
|
}
|
|
1116
1412
|
|
|
1117
1413
|
strokeRect(x, y, w, h) {
|
|
@@ -1166,7 +1462,12 @@ class RenderingContext2d {
|
|
|
1166
1462
|
// rather than quietly changing what they look like.
|
|
1167
1463
|
const integral = (v) => Math.abs(v - Math.round(v)) < 1e-6;
|
|
1168
1464
|
if (![x, y, w, h].every(integral)) return null;
|
|
1169
|
-
return {
|
|
1465
|
+
return {
|
|
1466
|
+
x: Math.round(x),
|
|
1467
|
+
y: Math.round(y),
|
|
1468
|
+
w: Math.round(w),
|
|
1469
|
+
h: Math.round(h),
|
|
1470
|
+
};
|
|
1170
1471
|
}
|
|
1171
1472
|
|
|
1172
1473
|
/** Intersection of the whole clip stack, or null if any part is not a
|
|
@@ -1201,15 +1502,66 @@ class RenderingContext2d {
|
|
|
1201
1502
|
// copy-on-write: earlier save() snapshots keep their own clip list
|
|
1202
1503
|
this._clips = this._clips.concat([entry]);
|
|
1203
1504
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1505
|
+
// A stack of rectangles stays virtual: consumers apply the intersected
|
|
1506
|
+
// rectangle server-side (SetPictureClipRectangles, bounded composites),
|
|
1507
|
+
// and no a8 mask — no window-sized pixmap, no AddTraps — ever exists.
|
|
1508
|
+
// That is the common case by a wide margin: a renderer clipping to a
|
|
1509
|
+
// damage rect, a viewport and a cell nests rectangles three deep before
|
|
1510
|
+
// the first rounded corner appears. The mask materializes on the first
|
|
1511
|
+
// entry that is not a rectangle, and stays in sync from then on.
|
|
1512
|
+
if (!this.clipMask) {
|
|
1513
|
+
if (entry.rect) return;
|
|
1514
|
+
this._materializeClipMask();
|
|
1208
1515
|
return;
|
|
1209
1516
|
}
|
|
1210
1517
|
this._intersectClip(entry);
|
|
1211
1518
|
}
|
|
1212
1519
|
|
|
1520
|
+
/**
|
|
1521
|
+
* Build the a8 mask from the whole clip stack, on first demand. Rect
|
|
1522
|
+
* entries are plain fills; only genuinely non-rectangular entries
|
|
1523
|
+
* rasterize, and those through `_applyPolyClip`, which keeps the
|
|
1524
|
+
* trapezoid work off the window-sized mask itself.
|
|
1525
|
+
*/
|
|
1526
|
+
_materializeClipMask() {
|
|
1527
|
+
const R = this.Render;
|
|
1528
|
+
this._ensureClipMask();
|
|
1529
|
+
R.FillRectangles(
|
|
1530
|
+
R.PictOp.Src,
|
|
1531
|
+
this.clipMask.id,
|
|
1532
|
+
[0, 0, 0, 0],
|
|
1533
|
+
[0, 0, this.width, this.height],
|
|
1534
|
+
);
|
|
1535
|
+
const first = this._clips[0];
|
|
1536
|
+
if (first.rect) {
|
|
1537
|
+
const r = first.rect;
|
|
1538
|
+
const x = Math.max(0, Math.min(r.x, this.width));
|
|
1539
|
+
const y = Math.max(0, Math.min(r.y, this.height));
|
|
1540
|
+
const right = Math.max(x, Math.min(r.x + r.w, this.width));
|
|
1541
|
+
const bottom = Math.max(y, Math.min(r.y + r.h, this.height));
|
|
1542
|
+
if (right > x && bottom > y) {
|
|
1543
|
+
R.FillRectangles(
|
|
1544
|
+
R.PictOp.Src,
|
|
1545
|
+
this.clipMask.id,
|
|
1546
|
+
[0, 0, 0, 1],
|
|
1547
|
+
[x, y, right - x, bottom - y],
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
} else {
|
|
1551
|
+
this._applyPolyClip(first, R.PictOp.Src);
|
|
1552
|
+
}
|
|
1553
|
+
for (let i = 1; i < this._clips.length; i++)
|
|
1554
|
+
this._intersectClip(this._clips[i]);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/** The live mask, built if the stack has not needed one yet. Callers on
|
|
1558
|
+
* the rectangle fast paths never get here; a rect-only stack only
|
|
1559
|
+
* materializes when a consumer genuinely has no cheaper way in. */
|
|
1560
|
+
_requireClipMask() {
|
|
1561
|
+
if (!this.clipMask) this._materializeClipMask();
|
|
1562
|
+
return this.clipMask;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1213
1565
|
// rasterize one clip entry into a temp a8 and In-composite it onto the mask
|
|
1214
1566
|
_intersectClip(entry) {
|
|
1215
1567
|
// Intersecting with a *rectangle* is just "keep the inside, clear the
|
|
@@ -1236,32 +1588,97 @@ class RenderingContext2d {
|
|
|
1236
1588
|
outside.push(right, y, this.width - right, bottom - y);
|
|
1237
1589
|
}
|
|
1238
1590
|
if (outside.length) {
|
|
1239
|
-
this.Render.FillRectangles(
|
|
1591
|
+
this.Render.FillRectangles(
|
|
1592
|
+
this.Render.PictOp.Src,
|
|
1593
|
+
this.clipMask.id,
|
|
1594
|
+
[0, 0, 0, 0],
|
|
1595
|
+
outside,
|
|
1596
|
+
);
|
|
1240
1597
|
}
|
|
1241
1598
|
return;
|
|
1242
1599
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1600
|
+
this._applyPolyClip(entry, this.Render.PictOp.In);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* Rasterize one non-rectangular clip entry and combine it onto the mask:
|
|
1605
|
+
* `Src` writes a fresh mask (everything outside was just cleared), `In`
|
|
1606
|
+
* intersects a live one. The trapezoids land in a temp sized to the
|
|
1607
|
+
* entry's bounding box, never on the window-sized mask — on glamor an
|
|
1608
|
+
* AddTraps is a software fallback that maps its whole target pixmap, so
|
|
1609
|
+
* the target's size *is* the cost. Everything outside the box is cleared
|
|
1610
|
+
* with plain fills: outside the entry the mask is zero by definition.
|
|
1611
|
+
*/
|
|
1612
|
+
_applyPolyClip(entry, op) {
|
|
1613
|
+
const R = this.Render;
|
|
1614
|
+
const bb = this._polysBBox(entry.polys);
|
|
1615
|
+
if (!bb || bb.w <= 0 || bb.h <= 0) {
|
|
1616
|
+
R.FillRectangles(
|
|
1617
|
+
R.PictOp.Src,
|
|
1618
|
+
this.clipMask.id,
|
|
1619
|
+
[0, 0, 0, 0],
|
|
1620
|
+
[0, 0, this.width, this.height],
|
|
1621
|
+
);
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
if (op === R.PictOp.In) {
|
|
1625
|
+
const outside = [];
|
|
1626
|
+
if (bb.y > 0) outside.push(0, 0, this.width, bb.y);
|
|
1627
|
+
if (bb.y + bb.h < this.height) {
|
|
1628
|
+
outside.push(0, bb.y + bb.h, this.width, this.height - (bb.y + bb.h));
|
|
1629
|
+
}
|
|
1630
|
+
if (bb.x > 0) outside.push(0, bb.y, bb.x, bb.h);
|
|
1631
|
+
if (bb.x + bb.w < this.width) {
|
|
1632
|
+
outside.push(bb.x + bb.w, bb.y, this.width - (bb.x + bb.w), bb.h);
|
|
1633
|
+
}
|
|
1634
|
+
if (outside.length) {
|
|
1635
|
+
R.FillRectangles(R.PictOp.Src, this.clipMask.id, [0, 0, 0, 0], outside);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
const tmpPixmap = new Pixmap(this.window.app, {
|
|
1639
|
+
depth: 8,
|
|
1640
|
+
width: bb.w,
|
|
1641
|
+
height: bb.h,
|
|
1642
|
+
});
|
|
1643
|
+
const tmpMask = new Picture(this.window.app, {
|
|
1644
|
+
drawable: tmpPixmap,
|
|
1645
|
+
format: R.a8,
|
|
1646
|
+
});
|
|
1647
|
+
R.FillRectangles(
|
|
1648
|
+
R.PictOp.Src,
|
|
1649
|
+
tmpMask.id,
|
|
1650
|
+
[0, 0, 0, 0],
|
|
1651
|
+
[0, 0, bb.w, bb.h],
|
|
1652
|
+
);
|
|
1653
|
+
this._rasterizePolys(tmpMask, entry.polys, entry.rule, -bb.x, -bb.y);
|
|
1654
|
+
R.Composite(
|
|
1655
|
+
op,
|
|
1656
|
+
tmpMask.id,
|
|
1657
|
+
0,
|
|
1658
|
+
this.clipMask.id,
|
|
1659
|
+
0,
|
|
1660
|
+
0,
|
|
1661
|
+
0,
|
|
1662
|
+
0,
|
|
1663
|
+
bb.x,
|
|
1664
|
+
bb.y,
|
|
1665
|
+
bb.w,
|
|
1666
|
+
bb.h,
|
|
1667
|
+
);
|
|
1248
1668
|
tmpMask.destroy();
|
|
1249
1669
|
tmpPixmap.destroy();
|
|
1250
1670
|
}
|
|
1251
1671
|
|
|
1252
1672
|
_rebuildClipMask() {
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1673
|
+
// The stack changed shape (restore, resize): drop the mask rather than
|
|
1674
|
+
// rebuild it eagerly. A stack that went back to rectangles never needs
|
|
1675
|
+
// one again, and one that still holds a poly rebuilds on first demand
|
|
1676
|
+
// through _requireClipMask — same work, paid only if something draws.
|
|
1677
|
+
if (this.clipMask) {
|
|
1678
|
+
this.clipMask.destroy();
|
|
1679
|
+
this.clipMaskDrawable.destroy();
|
|
1680
|
+
this.clipMask = this.clipMaskDrawable = null;
|
|
1260
1681
|
}
|
|
1261
|
-
this._ensureClipMask();
|
|
1262
|
-
this.Render.FillRectangles(this.Render.PictOp.Src, this.clipMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
|
|
1263
|
-
this._rasterizePolys(this.clipMask, this._clips[0].polys, this._clips[0].rule);
|
|
1264
|
-
for (let i = 1; i < this._clips.length; i++) this._intersectClip(this._clips[i]);
|
|
1265
1682
|
}
|
|
1266
1683
|
|
|
1267
1684
|
isPointInPath(...args) {
|
|
@@ -1272,8 +1689,15 @@ class RenderingContext2d {
|
|
|
1272
1689
|
rest = args.slice(1);
|
|
1273
1690
|
}
|
|
1274
1691
|
const [x, y, rule] = rest;
|
|
1275
|
-
const polys = path
|
|
1276
|
-
|
|
1692
|
+
const polys = path
|
|
1693
|
+
? flattenPath(path._cmds, this._m)
|
|
1694
|
+
: flattenPath(this._path._cmds, null);
|
|
1695
|
+
return polysContain(
|
|
1696
|
+
polys,
|
|
1697
|
+
x,
|
|
1698
|
+
y,
|
|
1699
|
+
rule === "evenodd" ? "evenodd" : "nonzero",
|
|
1700
|
+
);
|
|
1277
1701
|
}
|
|
1278
1702
|
|
|
1279
1703
|
// ------------------------------------------------------------------
|
|
@@ -1287,24 +1711,24 @@ class RenderingContext2d {
|
|
|
1287
1711
|
// horizontal offset of the alignment point, given the shaped width
|
|
1288
1712
|
_alignOffset(shaped) {
|
|
1289
1713
|
let align = this.textAlign;
|
|
1290
|
-
if (align ===
|
|
1291
|
-
if (align ===
|
|
1292
|
-
if (align ===
|
|
1293
|
-
if (align ===
|
|
1714
|
+
if (align === "start") align = shaped.baseLevel & 1 ? "right" : "left";
|
|
1715
|
+
if (align === "end") align = shaped.baseLevel & 1 ? "left" : "right";
|
|
1716
|
+
if (align === "center") return -shaped.width / 2;
|
|
1717
|
+
if (align === "right") return -shaped.width;
|
|
1294
1718
|
return 0;
|
|
1295
1719
|
}
|
|
1296
1720
|
|
|
1297
1721
|
// vertical distance from the requested y to the baseline
|
|
1298
1722
|
_baselineOffset(metrics) {
|
|
1299
1723
|
switch (this.textBaseline) {
|
|
1300
|
-
case
|
|
1724
|
+
case "top":
|
|
1301
1725
|
return metrics.ascent;
|
|
1302
|
-
case
|
|
1726
|
+
case "hanging":
|
|
1303
1727
|
return metrics.ascent * 0.8;
|
|
1304
|
-
case
|
|
1728
|
+
case "middle":
|
|
1305
1729
|
return (metrics.ascent - metrics.descent) / 2;
|
|
1306
|
-
case
|
|
1307
|
-
case
|
|
1730
|
+
case "bottom":
|
|
1731
|
+
case "ideographic":
|
|
1308
1732
|
return -metrics.descent;
|
|
1309
1733
|
default:
|
|
1310
1734
|
// 'alphabetic'
|
|
@@ -1323,11 +1747,13 @@ class RenderingContext2d {
|
|
|
1323
1747
|
* font via `ctx.font` instead).
|
|
1324
1748
|
*/
|
|
1325
1749
|
fillText(text, x, y) {
|
|
1326
|
-
text = String(text ??
|
|
1750
|
+
text = String(text ?? "");
|
|
1327
1751
|
if (!text) return;
|
|
1328
1752
|
const style = this._resolvedTextStyle();
|
|
1329
1753
|
const app = this.window.app;
|
|
1330
|
-
|
|
1754
|
+
// through the shaping memo TextLayout uses: a label repainted every
|
|
1755
|
+
// frame shapes once, not once per frame
|
|
1756
|
+
const shaped = app.fonts._shapeCachedWhole(text, style);
|
|
1331
1757
|
const [tx, ty] = matApply(this._m, x, y);
|
|
1332
1758
|
const ox = tx + this._alignOffset(shaped);
|
|
1333
1759
|
const oy = ty + this._baselineOffset(style.font.metrics(style.size));
|
|
@@ -1338,7 +1764,11 @@ class RenderingContext2d {
|
|
|
1338
1764
|
positioned.push({ run, x: cursor, y: oy });
|
|
1339
1765
|
cursor += run.width;
|
|
1340
1766
|
}
|
|
1341
|
-
this.drawGlyphs(
|
|
1767
|
+
this.drawGlyphs(
|
|
1768
|
+
this.Render.PictOp.Over,
|
|
1769
|
+
this._backgroundPicture,
|
|
1770
|
+
positioned,
|
|
1771
|
+
);
|
|
1342
1772
|
}
|
|
1343
1773
|
|
|
1344
1774
|
/**
|
|
@@ -1348,7 +1778,7 @@ class RenderingContext2d {
|
|
|
1348
1778
|
*/
|
|
1349
1779
|
measureText(text) {
|
|
1350
1780
|
const style = this._resolvedTextStyle();
|
|
1351
|
-
const shaped = this.window.app.fonts.shape(String(text ??
|
|
1781
|
+
const shaped = this.window.app.fonts.shape(String(text ?? ""), style);
|
|
1352
1782
|
let minX = 0;
|
|
1353
1783
|
let maxX = 0;
|
|
1354
1784
|
let minY = 0;
|
|
@@ -1378,7 +1808,7 @@ class RenderingContext2d {
|
|
|
1378
1808
|
emHeightAscent: m.ascent,
|
|
1379
1809
|
emHeightDescent: m.descent,
|
|
1380
1810
|
// legacy ntk field: ink height
|
|
1381
|
-
height: maxY - minY
|
|
1811
|
+
height: maxY - minY,
|
|
1382
1812
|
};
|
|
1383
1813
|
}
|
|
1384
1814
|
|
|
@@ -1402,14 +1832,14 @@ class RenderingContext2d {
|
|
|
1402
1832
|
* `app.fonts.load()` win over system (fontconfig) lookup.
|
|
1403
1833
|
*/
|
|
1404
1834
|
set font(val) {
|
|
1405
|
-
if (!val || typeof val !==
|
|
1835
|
+
if (!val || typeof val !== "string") return;
|
|
1406
1836
|
const parsed = parseFontStyle(val);
|
|
1407
1837
|
if (!parsed) return;
|
|
1408
1838
|
const style = {
|
|
1409
1839
|
family: parsed.family,
|
|
1410
1840
|
weight: parsed.weight,
|
|
1411
1841
|
style: parsed.style,
|
|
1412
|
-
size: parsed.size
|
|
1842
|
+
size: parsed.size,
|
|
1413
1843
|
};
|
|
1414
1844
|
style.font = this.window.app.fonts.match(style.family, style);
|
|
1415
1845
|
this._lastFontString = val;
|
|
@@ -1424,20 +1854,23 @@ class RenderingContext2d {
|
|
|
1424
1854
|
// gradients / images
|
|
1425
1855
|
|
|
1426
1856
|
createLinearGradient(x0, y0, x1, y1) {
|
|
1427
|
-
return new CanvasGradient(
|
|
1857
|
+
return new CanvasGradient("linear", this, x0, y0, x1, y1);
|
|
1428
1858
|
}
|
|
1429
1859
|
|
|
1430
1860
|
createRadialGradient(x0, y0, r0, x1, y1, r1) {
|
|
1431
|
-
return new CanvasGradient(
|
|
1861
|
+
return new CanvasGradient("radial", this, x0, y0, x1, y1, r0, r1);
|
|
1432
1862
|
}
|
|
1433
1863
|
|
|
1434
1864
|
createConicalGradient(x0, y0, angle) {
|
|
1435
|
-
return new CanvasGradient(
|
|
1865
|
+
return new CanvasGradient("conical", this, x0, y0, angle);
|
|
1436
1866
|
}
|
|
1437
1867
|
|
|
1438
1868
|
/** the pixel layout of whatever this context currently draws into */
|
|
1439
1869
|
get _layout() {
|
|
1440
|
-
const depth =
|
|
1870
|
+
const depth =
|
|
1871
|
+
this._target.depth ??
|
|
1872
|
+
this.window.depth ??
|
|
1873
|
+
this.display.screen[0].root_depth;
|
|
1441
1874
|
if (!this._layoutCache || this._layoutCache.depth !== depth) {
|
|
1442
1875
|
this._layoutCache = pixelLayout(this.display, depth);
|
|
1443
1876
|
}
|
|
@@ -1451,9 +1884,12 @@ class RenderingContext2d {
|
|
|
1451
1884
|
* createImageData(imagedata)
|
|
1452
1885
|
*/
|
|
1453
1886
|
createImageData(a, b) {
|
|
1454
|
-
if (typeof a ===
|
|
1455
|
-
if (a && typeof a.width ===
|
|
1456
|
-
|
|
1887
|
+
if (typeof a === "number") return new ImageData(a, b);
|
|
1888
|
+
if (a && typeof a.width === "number" && a.data)
|
|
1889
|
+
return new ImageData(a.width, a.height);
|
|
1890
|
+
throw new TypeError(
|
|
1891
|
+
"createImageData: pass (width, height) or an ImageData",
|
|
1892
|
+
);
|
|
1457
1893
|
}
|
|
1458
1894
|
|
|
1459
1895
|
/**
|
|
@@ -1469,14 +1905,20 @@ class RenderingContext2d {
|
|
|
1469
1905
|
const src = data.data;
|
|
1470
1906
|
if (!src || src.length !== width * height * 4) {
|
|
1471
1907
|
throw new Error(
|
|
1472
|
-
`putImageData: data must be ${width * height * 4} RGBA bytes for ${width}x${height}
|
|
1908
|
+
`putImageData: data must be ${width * height * 4} RGBA bytes for ${width}x${height}`,
|
|
1473
1909
|
);
|
|
1474
1910
|
}
|
|
1475
1911
|
dirtyWidth ??= width;
|
|
1476
1912
|
dirtyHeight ??= height;
|
|
1477
1913
|
// the spec normalises negative extents by moving the origin
|
|
1478
|
-
if (dirtyWidth < 0) {
|
|
1479
|
-
|
|
1914
|
+
if (dirtyWidth < 0) {
|
|
1915
|
+
dirtyX += dirtyWidth;
|
|
1916
|
+
dirtyWidth = -dirtyWidth;
|
|
1917
|
+
}
|
|
1918
|
+
if (dirtyHeight < 0) {
|
|
1919
|
+
dirtyY += dirtyHeight;
|
|
1920
|
+
dirtyHeight = -dirtyHeight;
|
|
1921
|
+
}
|
|
1480
1922
|
const sx = Math.max(0, dirtyX);
|
|
1481
1923
|
const sy = Math.max(0, dirtyY);
|
|
1482
1924
|
const sw = Math.min(width, dirtyX + dirtyWidth) - sx;
|
|
@@ -1488,8 +1930,11 @@ class RenderingContext2d {
|
|
|
1488
1930
|
const cropped = new Uint8ClampedArray(sw * sh * 4);
|
|
1489
1931
|
for (let row = 0; row < sh; row++) {
|
|
1490
1932
|
cropped.set(
|
|
1491
|
-
src.subarray(
|
|
1492
|
-
|
|
1933
|
+
src.subarray(
|
|
1934
|
+
(sy + row) * width * 4 + sx * 4,
|
|
1935
|
+
(sy + row) * width * 4 + (sx + sw) * 4,
|
|
1936
|
+
),
|
|
1937
|
+
row * sw * 4,
|
|
1493
1938
|
);
|
|
1494
1939
|
}
|
|
1495
1940
|
rgba = cropped;
|
|
@@ -1507,11 +1952,13 @@ class RenderingContext2d {
|
|
|
1507
1952
|
2, // ZPixmap
|
|
1508
1953
|
this._target.id,
|
|
1509
1954
|
this._gc,
|
|
1510
|
-
sw,
|
|
1511
|
-
|
|
1955
|
+
sw,
|
|
1956
|
+
rows,
|
|
1957
|
+
x + sx,
|
|
1958
|
+
y + sy + row,
|
|
1512
1959
|
0,
|
|
1513
1960
|
layout.depth,
|
|
1514
|
-
bytes.subarray(row * stride, (row + rows) * stride)
|
|
1961
|
+
bytes.subarray(row * stride, (row + rows) * stride),
|
|
1515
1962
|
);
|
|
1516
1963
|
}
|
|
1517
1964
|
this._markDirty();
|
|
@@ -1531,9 +1978,9 @@ class RenderingContext2d {
|
|
|
1531
1978
|
*/
|
|
1532
1979
|
getImageData(x, y, w, h, cb) {
|
|
1533
1980
|
const promise = this.readPixels(x, y, w, h).then(
|
|
1534
|
-
(raw) => new ImageData(toStraightRgba(raw.data, raw.layout, w, h), w, h)
|
|
1981
|
+
(raw) => new ImageData(toStraightRgba(raw.data, raw.layout, w, h), w, h),
|
|
1535
1982
|
);
|
|
1536
|
-
if (typeof cb ===
|
|
1983
|
+
if (typeof cb === "function") {
|
|
1537
1984
|
promise.then((data) => cb(null, data), cb);
|
|
1538
1985
|
return undefined;
|
|
1539
1986
|
}
|
|
@@ -1561,10 +2008,19 @@ class RenderingContext2d {
|
|
|
1561
2008
|
readPixels(x, y, w, h) {
|
|
1562
2009
|
const layout = this._layout;
|
|
1563
2010
|
return new Promise((resolve, reject) => {
|
|
1564
|
-
this.X.GetImage(
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
2011
|
+
this.X.GetImage(
|
|
2012
|
+
2,
|
|
2013
|
+
this._target.id,
|
|
2014
|
+
x,
|
|
2015
|
+
y,
|
|
2016
|
+
w,
|
|
2017
|
+
h,
|
|
2018
|
+
0xffffffff,
|
|
2019
|
+
(err, img) => {
|
|
2020
|
+
if (err) return reject(err);
|
|
2021
|
+
resolve({ width: w, height: h, data: img.data, layout, ...layout });
|
|
2022
|
+
},
|
|
2023
|
+
);
|
|
1568
2024
|
});
|
|
1569
2025
|
}
|
|
1570
2026
|
|
|
@@ -1594,18 +2050,27 @@ class RenderingContext2d {
|
|
|
1594
2050
|
* picture is the same thing to the server, with no pixel work at all.
|
|
1595
2051
|
*/
|
|
1596
2052
|
_beginDirectComposite() {
|
|
1597
|
-
const rect = this.
|
|
1598
|
-
if (!rect)
|
|
1599
|
-
|
|
1600
|
-
|
|
2053
|
+
const rect = this._clips.length ? this._clipRect() : null;
|
|
2054
|
+
if (!rect)
|
|
2055
|
+
return { mask: this._compositeMask(), clipped: false, empty: false };
|
|
2056
|
+
if (rect.w === 0 || rect.h === 0)
|
|
2057
|
+
return { mask: 0, clipped: false, empty: true };
|
|
2058
|
+
this.Render.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
2059
|
+
rect.x,
|
|
2060
|
+
rect.y,
|
|
2061
|
+
rect.w,
|
|
2062
|
+
rect.h,
|
|
2063
|
+
]);
|
|
1601
2064
|
const mask =
|
|
1602
|
-
this.globalAlpha >= 1
|
|
2065
|
+
this.globalAlpha >= 1
|
|
2066
|
+
? 0
|
|
2067
|
+
: this.createSolidPicture(0, 0, 0, this.globalAlpha).id;
|
|
1603
2068
|
return { mask, clipped: true, empty: false };
|
|
1604
2069
|
}
|
|
1605
2070
|
|
|
1606
2071
|
_endDirectComposite(state) {
|
|
1607
2072
|
if (!state.clipped) return;
|
|
1608
|
-
this.
|
|
2073
|
+
this._resetPictureClip();
|
|
1609
2074
|
}
|
|
1610
2075
|
|
|
1611
2076
|
/** The source colour for a coverage composite, with `globalAlpha` folded
|
|
@@ -1615,7 +2080,8 @@ class RenderingContext2d {
|
|
|
1615
2080
|
* colour to fold into; `_drawCoverage` routes those through the scratch. */
|
|
1616
2081
|
_coverageSource() {
|
|
1617
2082
|
const style = this._fillStyle;
|
|
1618
|
-
if (this.globalAlpha >= 1 || !isPlainColor(style))
|
|
2083
|
+
if (this.globalAlpha >= 1 || !isPlainColor(style))
|
|
2084
|
+
return this._backgroundPicture;
|
|
1619
2085
|
const c = parseColor(style);
|
|
1620
2086
|
return this.createSolidPicture(c[0], c[1], c[2], c[3] * this.globalAlpha);
|
|
1621
2087
|
}
|
|
@@ -1633,69 +2099,149 @@ class RenderingContext2d {
|
|
|
1633
2099
|
const R = this.Render;
|
|
1634
2100
|
const scaled = dw !== sw || dh !== sh;
|
|
1635
2101
|
if (scaled) {
|
|
1636
|
-
R.SetPictureTransform(picture.id, [
|
|
1637
|
-
|
|
2102
|
+
R.SetPictureTransform(picture.id, [
|
|
2103
|
+
sw / dw,
|
|
2104
|
+
0,
|
|
2105
|
+
sx,
|
|
2106
|
+
0,
|
|
2107
|
+
sh / dh,
|
|
2108
|
+
sy,
|
|
2109
|
+
0,
|
|
2110
|
+
0,
|
|
2111
|
+
1,
|
|
2112
|
+
]);
|
|
2113
|
+
picture.setFilter("bilinear");
|
|
1638
2114
|
}
|
|
1639
2115
|
// with a transform in place the mask is already sampled from (sx, sy)
|
|
1640
2116
|
const mx = scaled ? 0 : sx;
|
|
1641
2117
|
const my = scaled ? 0 : sy;
|
|
1642
2118
|
|
|
1643
|
-
const rect = this.
|
|
2119
|
+
const rect = this._clips.length ? this._clipRect() : null;
|
|
1644
2120
|
// the mask slot can hold exactly one picture, so anything that cannot be
|
|
1645
2121
|
// folded into the colour or handed to the server as a clip rectangle has
|
|
1646
2122
|
// to be intersected into the scratch mask first
|
|
1647
2123
|
const scratch =
|
|
1648
|
-
(this.
|
|
2124
|
+
(this._clips.length && !rect) ||
|
|
2125
|
+
(this.globalAlpha < 1 && !isPlainColor(this._fillStyle));
|
|
1649
2126
|
|
|
1650
2127
|
if (scratch) {
|
|
1651
2128
|
this._ensureFillMask();
|
|
1652
|
-
R.FillRectangles(
|
|
1653
|
-
|
|
2129
|
+
R.FillRectangles(
|
|
2130
|
+
R.PictOp.Src,
|
|
2131
|
+
this.fillMask.id,
|
|
2132
|
+
[0, 0, 0, 0],
|
|
2133
|
+
[0, 0, this.width, this.height],
|
|
2134
|
+
);
|
|
2135
|
+
R.Composite(
|
|
2136
|
+
R.PictOp.Src,
|
|
2137
|
+
picture.id,
|
|
2138
|
+
0,
|
|
2139
|
+
this.fillMask.id,
|
|
2140
|
+
mx,
|
|
2141
|
+
my,
|
|
2142
|
+
0,
|
|
2143
|
+
0,
|
|
2144
|
+
dx,
|
|
2145
|
+
dy,
|
|
2146
|
+
dw,
|
|
2147
|
+
dh,
|
|
2148
|
+
);
|
|
1654
2149
|
if (this.globalAlpha < 1) {
|
|
1655
2150
|
R.Composite(
|
|
1656
2151
|
R.PictOp.InReverse,
|
|
1657
2152
|
this.createSolidPicture(0, 0, 0, this.globalAlpha).id,
|
|
1658
2153
|
0,
|
|
1659
2154
|
this.fillMask.id,
|
|
1660
|
-
0,
|
|
1661
|
-
|
|
2155
|
+
0,
|
|
2156
|
+
0,
|
|
2157
|
+
0,
|
|
2158
|
+
0,
|
|
2159
|
+
0,
|
|
2160
|
+
0,
|
|
2161
|
+
this.width,
|
|
2162
|
+
this.height,
|
|
1662
2163
|
);
|
|
1663
2164
|
}
|
|
1664
|
-
if (this.
|
|
1665
|
-
|
|
2165
|
+
if (this._clips.length && !rect) {
|
|
2166
|
+
const clipMask = this._requireClipMask();
|
|
2167
|
+
R.Composite(
|
|
2168
|
+
R.PictOp.In,
|
|
2169
|
+
clipMask.id,
|
|
2170
|
+
0,
|
|
2171
|
+
this.fillMask.id,
|
|
2172
|
+
0,
|
|
2173
|
+
0,
|
|
2174
|
+
0,
|
|
2175
|
+
0,
|
|
2176
|
+
0,
|
|
2177
|
+
0,
|
|
2178
|
+
this.width,
|
|
2179
|
+
this.height,
|
|
2180
|
+
);
|
|
2181
|
+
} else if (rect) {
|
|
2182
|
+
// scratch was forced by alpha-over-gradient, not by the clip: the
|
|
2183
|
+
// rectangle still applies, server-side, around the final composite
|
|
2184
|
+
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
2185
|
+
rect.x,
|
|
2186
|
+
rect.y,
|
|
2187
|
+
rect.w,
|
|
2188
|
+
rect.h,
|
|
2189
|
+
]);
|
|
1666
2190
|
}
|
|
1667
2191
|
R.Composite(
|
|
1668
2192
|
op,
|
|
1669
2193
|
this._backgroundPicture.id,
|
|
1670
2194
|
this.fillMask.id,
|
|
1671
2195
|
this.picture.id,
|
|
1672
|
-
0,
|
|
1673
|
-
|
|
1674
|
-
|
|
2196
|
+
0,
|
|
2197
|
+
0,
|
|
2198
|
+
0,
|
|
2199
|
+
0,
|
|
2200
|
+
dx,
|
|
2201
|
+
dy,
|
|
2202
|
+
dw,
|
|
2203
|
+
dh,
|
|
1675
2204
|
);
|
|
2205
|
+
if (rect) {
|
|
2206
|
+
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
2207
|
+
0,
|
|
2208
|
+
0,
|
|
2209
|
+
this.width,
|
|
2210
|
+
this.height,
|
|
2211
|
+
]);
|
|
2212
|
+
}
|
|
1676
2213
|
} else if (!rect || (rect.w > 0 && rect.h > 0)) {
|
|
1677
2214
|
if (rect) {
|
|
1678
|
-
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
2215
|
+
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
2216
|
+
rect.x,
|
|
2217
|
+
rect.y,
|
|
2218
|
+
rect.w,
|
|
2219
|
+
rect.h,
|
|
2220
|
+
]);
|
|
1679
2221
|
}
|
|
1680
2222
|
R.Composite(
|
|
1681
2223
|
op,
|
|
1682
2224
|
this._coverageSource().id,
|
|
1683
2225
|
picture.id,
|
|
1684
2226
|
this.picture.id,
|
|
1685
|
-
0,
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
2227
|
+
0,
|
|
2228
|
+
0,
|
|
2229
|
+
mx,
|
|
2230
|
+
my,
|
|
2231
|
+
dx,
|
|
2232
|
+
dy,
|
|
2233
|
+
dw,
|
|
2234
|
+
dh,
|
|
1689
2235
|
);
|
|
1690
2236
|
if (rect) {
|
|
1691
|
-
|
|
2237
|
+
this._resetPictureClip();
|
|
1692
2238
|
}
|
|
1693
2239
|
}
|
|
1694
2240
|
|
|
1695
2241
|
if (scaled) {
|
|
1696
2242
|
// restore defaults so the cached surface stays reusable as-is
|
|
1697
2243
|
R.SetPictureTransform(picture.id, [1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
1698
|
-
picture.setFilter(
|
|
2244
|
+
picture.setFilter("nearest");
|
|
1699
2245
|
}
|
|
1700
2246
|
this._markDirty();
|
|
1701
2247
|
}
|
|
@@ -1729,7 +2275,7 @@ class RenderingContext2d {
|
|
|
1729
2275
|
return;
|
|
1730
2276
|
}
|
|
1731
2277
|
|
|
1732
|
-
if (image.format ===
|
|
2278
|
+
if (image.format === "a8") {
|
|
1733
2279
|
this._drawCoverage(picture, sx, sy, sw, sh, dx, dy, dw, dh, op);
|
|
1734
2280
|
return;
|
|
1735
2281
|
}
|
|
@@ -1741,31 +2287,52 @@ class RenderingContext2d {
|
|
|
1741
2287
|
if (scaled) {
|
|
1742
2288
|
// the picture transform maps composite coordinates into source
|
|
1743
2289
|
// samples: dest pixel (i, j) reads (sx + i*sw/dw, sy + j*sh/dh)
|
|
1744
|
-
this.Render.SetPictureTransform(picture.id, [
|
|
1745
|
-
|
|
2290
|
+
this.Render.SetPictureTransform(picture.id, [
|
|
2291
|
+
sw / dw,
|
|
2292
|
+
0,
|
|
2293
|
+
sx,
|
|
2294
|
+
0,
|
|
2295
|
+
sh / dh,
|
|
2296
|
+
sy,
|
|
2297
|
+
0,
|
|
2298
|
+
0,
|
|
2299
|
+
1,
|
|
2300
|
+
]);
|
|
2301
|
+
picture.setFilter("bilinear");
|
|
1746
2302
|
this.Render.Composite(
|
|
1747
2303
|
op,
|
|
1748
2304
|
picture.id,
|
|
1749
2305
|
mask,
|
|
1750
2306
|
this.picture.id,
|
|
1751
|
-
0,
|
|
1752
|
-
|
|
1753
|
-
dx,
|
|
1754
|
-
|
|
2307
|
+
0,
|
|
2308
|
+
0,
|
|
2309
|
+
dx,
|
|
2310
|
+
dy,
|
|
2311
|
+
dx,
|
|
2312
|
+
dy,
|
|
2313
|
+
dw,
|
|
2314
|
+
dh,
|
|
1755
2315
|
);
|
|
1756
2316
|
// restore defaults so the cached upload can be reused as-is
|
|
1757
|
-
this.Render.SetPictureTransform(
|
|
1758
|
-
|
|
2317
|
+
this.Render.SetPictureTransform(
|
|
2318
|
+
picture.id,
|
|
2319
|
+
[1, 0, 0, 0, 1, 0, 0, 0, 1],
|
|
2320
|
+
);
|
|
2321
|
+
picture.setFilter("nearest");
|
|
1759
2322
|
} else {
|
|
1760
2323
|
this.Render.Composite(
|
|
1761
2324
|
op,
|
|
1762
2325
|
picture.id,
|
|
1763
2326
|
mask,
|
|
1764
2327
|
this.picture.id,
|
|
1765
|
-
sx,
|
|
1766
|
-
|
|
1767
|
-
dx,
|
|
1768
|
-
|
|
2328
|
+
sx,
|
|
2329
|
+
sy,
|
|
2330
|
+
dx,
|
|
2331
|
+
dy,
|
|
2332
|
+
dx,
|
|
2333
|
+
dy,
|
|
2334
|
+
dw,
|
|
2335
|
+
dh,
|
|
1769
2336
|
);
|
|
1770
2337
|
}
|
|
1771
2338
|
this._endDirectComposite(state);
|
|
@@ -1788,12 +2355,21 @@ class RenderingContext2d {
|
|
|
1788
2355
|
image.picture.id,
|
|
1789
2356
|
this._compositeMask(),
|
|
1790
2357
|
this.picture.id,
|
|
1791
|
-
0,
|
|
2358
|
+
0,
|
|
2359
|
+
0,
|
|
2360
|
+
0,
|
|
2361
|
+
0,
|
|
2362
|
+
0,
|
|
2363
|
+
0,
|
|
1792
2364
|
this.width,
|
|
1793
|
-
this.height
|
|
2365
|
+
this.height,
|
|
1794
2366
|
);
|
|
1795
2367
|
this._markDirty();
|
|
1796
|
-
} else if (
|
|
2368
|
+
} else if (
|
|
2369
|
+
image &&
|
|
2370
|
+
image.context &&
|
|
2371
|
+
typeof image.context.getImageData === "function"
|
|
2372
|
+
) {
|
|
1797
2373
|
// node-canvas Canvas ( or any ctx with ctx.canvas.getImageData returning pixels)
|
|
1798
2374
|
const imageData = image.context.getImageData(sx, sy, sWidth, sHeight);
|
|
1799
2375
|
// node-canvas hands over straight RGBA, and the rgba32 picture below is
|
|
@@ -1803,7 +2379,7 @@ class RenderingContext2d {
|
|
|
1803
2379
|
imageData.data,
|
|
1804
2380
|
pixelLayout(this.display, 32),
|
|
1805
2381
|
sWidth,
|
|
1806
|
-
sHeight
|
|
2382
|
+
sHeight,
|
|
1807
2383
|
);
|
|
1808
2384
|
|
|
1809
2385
|
// One upload GC per app rather than one per call, the same sharing
|
|
@@ -1812,8 +2388,15 @@ class RenderingContext2d {
|
|
|
1812
2388
|
// time and free none of them, so drawing a node-canvas source in an
|
|
1813
2389
|
// animation loop leaked three server resources per frame.
|
|
1814
2390
|
const app = this.window.app;
|
|
1815
|
-
const pixmap = new Pixmap(app, {
|
|
1816
|
-
|
|
2391
|
+
const pixmap = new Pixmap(app, {
|
|
2392
|
+
depth: 32,
|
|
2393
|
+
width: sWidth,
|
|
2394
|
+
height: sHeight,
|
|
2395
|
+
});
|
|
2396
|
+
const picture = new Picture(app, {
|
|
2397
|
+
drawable: pixmap,
|
|
2398
|
+
format: this.Render.rgba32,
|
|
2399
|
+
});
|
|
1817
2400
|
try {
|
|
1818
2401
|
let gc = app._imageUploadGC;
|
|
1819
2402
|
if (!gc) {
|
|
@@ -1829,9 +2412,14 @@ class RenderingContext2d {
|
|
|
1829
2412
|
picture.id,
|
|
1830
2413
|
this._compositeMask(),
|
|
1831
2414
|
this.picture.id,
|
|
1832
|
-
0,
|
|
2415
|
+
0,
|
|
2416
|
+
0,
|
|
2417
|
+
0,
|
|
2418
|
+
0,
|
|
2419
|
+
0,
|
|
2420
|
+
0,
|
|
1833
2421
|
this.width,
|
|
1834
|
-
this.height
|
|
2422
|
+
this.height,
|
|
1835
2423
|
);
|
|
1836
2424
|
this._markDirty();
|
|
1837
2425
|
} finally {
|
|
@@ -1856,22 +2444,51 @@ class RenderingContext2d {
|
|
|
1856
2444
|
matApply(this._m, dx, dy),
|
|
1857
2445
|
matApply(this._m, dx + dw, dy),
|
|
1858
2446
|
matApply(this._m, dx, dy + dh),
|
|
1859
|
-
matApply(this._m, dx + dw, dy + dh)
|
|
2447
|
+
matApply(this._m, dx + dw, dy + dh),
|
|
1860
2448
|
];
|
|
1861
2449
|
const x0 = Math.max(0, Math.floor(Math.min(...corners.map((c) => c[0]))));
|
|
1862
2450
|
const y0 = Math.max(0, Math.floor(Math.min(...corners.map((c) => c[1]))));
|
|
1863
|
-
const x1 = Math.min(
|
|
1864
|
-
|
|
2451
|
+
const x1 = Math.min(
|
|
2452
|
+
this.width,
|
|
2453
|
+
Math.ceil(Math.max(...corners.map((c) => c[0]))),
|
|
2454
|
+
);
|
|
2455
|
+
const y1 = Math.min(
|
|
2456
|
+
this.height,
|
|
2457
|
+
Math.ceil(Math.max(...corners.map((c) => c[1]))),
|
|
2458
|
+
);
|
|
1865
2459
|
if (x1 <= x0 || y1 <= y0) return;
|
|
1866
2460
|
|
|
1867
2461
|
// device -> source sample (adding the source-rect origin)
|
|
1868
2462
|
const srcM = [inv[0], inv[1], inv[2], inv[3], inv[4] + sx, inv[5] + sy];
|
|
1869
|
-
this.Render.SetPictureTransform(picture.id, [
|
|
1870
|
-
|
|
2463
|
+
this.Render.SetPictureTransform(picture.id, [
|
|
2464
|
+
srcM[0],
|
|
2465
|
+
srcM[2],
|
|
2466
|
+
srcM[4],
|
|
2467
|
+
srcM[1],
|
|
2468
|
+
srcM[3],
|
|
2469
|
+
srcM[5],
|
|
2470
|
+
0,
|
|
2471
|
+
0,
|
|
2472
|
+
1,
|
|
2473
|
+
]);
|
|
2474
|
+
picture.setFilter("bilinear");
|
|
1871
2475
|
const mask = this._compositeMask();
|
|
1872
|
-
this.Render.Composite(
|
|
2476
|
+
this.Render.Composite(
|
|
2477
|
+
op,
|
|
2478
|
+
picture.id,
|
|
2479
|
+
mask,
|
|
2480
|
+
this.picture.id,
|
|
2481
|
+
x0,
|
|
2482
|
+
y0,
|
|
2483
|
+
x0,
|
|
2484
|
+
y0,
|
|
2485
|
+
x0,
|
|
2486
|
+
y0,
|
|
2487
|
+
x1 - x0,
|
|
2488
|
+
y1 - y0,
|
|
2489
|
+
);
|
|
1873
2490
|
this.Render.SetPictureTransform(picture.id, [1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
1874
|
-
picture.setFilter(
|
|
2491
|
+
picture.setFilter("nearest");
|
|
1875
2492
|
this._markDirty();
|
|
1876
2493
|
}
|
|
1877
2494
|
}
|
|
@@ -1886,7 +2503,7 @@ class CanvasGradient {
|
|
|
1886
2503
|
|
|
1887
2504
|
this.x0 = p0;
|
|
1888
2505
|
this.y0 = p1;
|
|
1889
|
-
if (type ===
|
|
2506
|
+
if (type === "linear" || type === "radial") {
|
|
1890
2507
|
this.x1 = p2;
|
|
1891
2508
|
this.y1 = p3;
|
|
1892
2509
|
this.r0 = p4;
|
|
@@ -1911,17 +2528,34 @@ class CanvasGradient {
|
|
|
1911
2528
|
|
|
1912
2529
|
this._id = X.AllocID();
|
|
1913
2530
|
switch (this.type) {
|
|
1914
|
-
case
|
|
1915
|
-
Render.LinearGradient(
|
|
2531
|
+
case "linear":
|
|
2532
|
+
Render.LinearGradient(
|
|
2533
|
+
this._id,
|
|
2534
|
+
[this.x0, this.y0],
|
|
2535
|
+
[this.x1, this.y1],
|
|
2536
|
+
this.stops,
|
|
2537
|
+
);
|
|
1916
2538
|
break;
|
|
1917
|
-
case
|
|
1918
|
-
Render.RadialGradient(
|
|
2539
|
+
case "radial":
|
|
2540
|
+
Render.RadialGradient(
|
|
2541
|
+
this._id,
|
|
2542
|
+
[this.x0, this.y0],
|
|
2543
|
+
[this.x1, this.y1],
|
|
2544
|
+
this.r0,
|
|
2545
|
+
this.r1,
|
|
2546
|
+
this.stops,
|
|
2547
|
+
);
|
|
1919
2548
|
break;
|
|
1920
|
-
case
|
|
1921
|
-
Render.ConicalGradient(
|
|
2549
|
+
case "conical":
|
|
2550
|
+
Render.ConicalGradient(
|
|
2551
|
+
this._id,
|
|
2552
|
+
[this.x0, this.y0],
|
|
2553
|
+
this.angle,
|
|
2554
|
+
this.stops,
|
|
2555
|
+
);
|
|
1922
2556
|
break;
|
|
1923
2557
|
default:
|
|
1924
|
-
throw new Error(
|
|
2558
|
+
throw new Error("unknown gradient type");
|
|
1925
2559
|
}
|
|
1926
2560
|
// wrap with picture so FreePicture is invoked on gc via FinalizationRegistry
|
|
1927
2561
|
this._picture = new Picture(this.ctx.window.app, { id: this._id });
|
|
@@ -1930,7 +2564,8 @@ class CanvasGradient {
|
|
|
1930
2564
|
}
|
|
1931
2565
|
|
|
1932
2566
|
// register context
|
|
1933
|
-
Drawable.renderingContextFactory[
|
|
2567
|
+
Drawable.renderingContextFactory["2d"] = (window) =>
|
|
2568
|
+
new RenderingContext2d(window);
|
|
1934
2569
|
|
|
1935
2570
|
export default RenderingContext2d;
|
|
1936
2571
|
export { CanvasGradient };
|