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.
@@ -1,11 +1,16 @@
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 { ImageData, fromStraightRgba, pixelLayout, toStraightRgba } from './imagedata.js';
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 './path.js';
20
- import Picture from './picture.js';
21
- import Pixmap from './pixmap.js';
22
- import { routeRaster } from './rasterize.js';
23
- import { compositeTraps, drawGlyphRuns } from './text/glyphs.js';
24
- import { TextLayout } from './text/layout.js';
25
- import { reorderRuns } from './text/shape.js';
26
- import { trapezoidize } from './trapezoid.js';
27
-
28
- const DEFAULT_FONT = '20px sans-serif';
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 === 'string' || Array.isArray(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 === 'function' &&
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
- 'source-over': 'Over',
77
- copy: 'Src',
78
- 'destination-over': 'OverReverse',
79
- 'source-in': 'In',
80
- 'destination-in': 'InReverse',
81
- 'source-out': 'Out',
82
- 'destination-out': 'OutReverse',
83
- 'source-atop': 'Atop',
84
- 'destination-atop': 'AtopReverse',
85
- xor: 'Xor',
86
- lighter: 'Add'
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: 'butt', square: 'square', round: 'butt' };
93
- const LINE_JOIN = { miter: 'miter', bevel: 'bevel', round: 'bevel' };
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 === 'function') {
210
- window.on('_backing', () => this._bindTarget());
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('resize', () => this._dropMasks());
227
+ window.on("resize", () => this._dropMasks());
213
228
  // window-backed pictures are freed server-side with the window
214
- window.on('_destroyed', () => {
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 = 'start';
230
- this.textBaseline = 'alphabetic';
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 = 'source-over';
250
+ this._gco = "source-over";
237
251
  this.globalAlpha = 1;
238
- this.lineCap = 'butt';
239
- this.lineJoin = 'miter';
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 = 'white';
245
- this.strokeStyle = 'black';
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, a Picture, and a 1x1 pixmap per fill colour used.
313
+ * permanently costs a GC and a Picture.
291
314
  *
292
315
  * `_backgroundPicture` and `_glyphSource` are deliberately not freed here:
293
- * both are either aliases into `_solidPictures`, already freed below, or a
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 !== 'function') return;
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] || 'Over'];
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
- const key = [r, g, b, a].join('|');
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 === 'string' || Array.isArray(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('Unknown fill style');
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 === 'object' && a !== null) {
538
+ if (typeof a === "object" && a !== null) {
530
539
  const m = a;
531
- this._m = Array.isArray(m) ? m.slice(0, 6) : [m.a, m.b, m.c, m.d, m.e, m.f];
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('arc: negative radius');
591
- this._appendUserSegments(ellipseSegments(x, y, r, r, 0, startAngle, endAngle, counterclockwise));
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(x, y, rx, ry, rotation, startAngle, endAngle, counterclockwise = false) {
595
- if (rx < 0 || ry < 0) throw new RangeError('ellipse: negative radius');
596
- this._appendUserSegments(ellipseSegments(x, y, rx, ry, rotation, startAngle, endAngle, counterclockwise));
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] === 'evenodd' ? 'evenodd' : 'nonzero'
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] === 'evenodd' ? 'evenodd' : 'nonzero'
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, 0, 0, [], rule);
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) !== 'local') return false;
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(data, y * stride);
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(2, this.fillMaskDrawable.id, this._maskGC(), b.w, b.h, b.x, b.y, 0, 8, data);
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 (!this._uploadCoverage({ polys: flat, rule, dx: -b.x, dy: -b.y }, b, edges)) {
806
- R.FillRectangles(R.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], [b.x, b.y, b.w, b.h]);
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(R.PictOp.In, this.fillMask.id, [0, 0, 0, alpha], [b.x, b.y, b.w, b.h]);
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(R.PictOp.In, this.clipMask.id, 0, this.fillMask.id, b.x, b.y, 0, 0, b.x, b.y, b.w, b.h);
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(op, src.id, this.fillMask.id, this.picture.id, b.x, b.y, b.x, b.y, b.x, b.y, b.w, b.h);
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 === 'round';
832
- const roundJoin = this.lineJoin === 'round';
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] || 'butt',
836
- join: LINE_JOIN[this.lineJoin] || 'miter',
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 ? this._lineDash.map((d) => d * scale) : null;
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 = ((b[0] - a[0]) * (c[0] - b[0]) + (b[1] - a[1]) * (c[1] - b[1])) / (l1 * l2);
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++) maybeJoinDisk(pts[i - 1], pts[i], pts[i + 1]);
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) maybeJoinDisk(pts[pts.length - 2], pts[0], pts[1]);
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) out.push(p);
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) pts.push([fx, fy]);
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 = !hasRound && this.globalAlpha >= 1 && !this.clipMask && op === this.Render.PictOp.Over;
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(op, src.id, 0, 0, this.picture.id, this.Render.a8, tris.slice(i, i + chunk));
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 (!this._uploadCoverage({ triangles: tris, dx: -b.x, dy: -b.y }, b, tris.length / 2)) {
974
- this.Render.FillRectangles(this.Render.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], [b.x, b.y, b.w, b.h]);
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(this.Render.PictOp.Add, opaque.id, 0, 0, this.fillMask.id, this.Render.a8, tris.slice(i, i + chunk));
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(this.Render.PictOp.In, this.fillMask.id, [0, 0, 0, this.globalAlpha], [b.x, b.y, b.w, b.h]);
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(this.Render.PictOp.In, this.clipMask.id, 0, this.fillMask.id, b.x, b.y, 0, 0, b.x, b.y, b.w, b.h);
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(op, src.id, this.fillMask.id, this.picture.id, b.x, b.y, b.x, b.y, b.x, b.y, b.w, b.h);
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.clipMask) {
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, [rect.x, rect.y, rect.w, rect.h]);
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
- R.SetPictureClipRectangles(this.picture.id, 0, 0, [0, 0, this.width, this.height]);
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(R.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
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(app, R.PictOp.Over, this._glyphSource.id, this.fillMask.id, positioned);
1026
- R.Composite(R.PictOp.In, this.clipMask.id, 0, this.fillMask.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
1027
- R.Composite(op, src.id, this.fillMask.id, this.picture.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
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.clipMask) {
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, [rect.x, rect.y, rect.w, rect.h]);
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
- R.SetPictureClipRectangles(this.picture.id, 0, 0, [0, 0, this.width, this.height]);
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(R.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
1058
- compositeTraps(app, R.PictOp.Over, this._glyphSource.id, this.fillMask.id, traps);
1059
- R.Composite(R.PictOp.In, this.clipMask.id, 0, this.fillMask.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
1060
- R.Composite(op, src.id, this.fillMask.id, this.picture.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
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
- if (this.globalAlpha >= 1) return this.clipMask ? this.clipMask.id : 0;
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(this.Render.PictOp.Src, this.fillMask.id, [0, 0, 0, this.globalAlpha], [0, 0, this.width, this.height]);
1070
- if (this.clipMask) {
1071
- this.Render.Composite(this.Render.PictOp.In, this.clipMask.id, 0, this.fillMask.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
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.clipMask) {
1081
- this.Render.FillRectangles(this.Render.PictOp.Src, this.picture.id, [1, 1, 1, 1], [x, y, w, h]);
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), 'nonzero', {
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, y,
1105
- x, y,
1106
- x, y,
1107
- w, h
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), 'nonzero');
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 { x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h) };
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
- if (this._clips.length === 1) {
1205
- this._ensureClipMask();
1206
- this.Render.FillRectangles(this.Render.PictOp.Src, this.clipMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
1207
- this._rasterizePolys(this.clipMask, polys, rule);
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(this.Render.PictOp.Src, this.clipMask.id, [0, 0, 0, 0], outside);
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
- const tmpPixmap = new Pixmap(this.window.app, { depth: 8, width: this.width, height: this.height });
1244
- const tmpMask = new Picture(this.window.app, { drawable: tmpPixmap, format: this.Render.a8 });
1245
- this.Render.FillRectangles(this.Render.PictOp.Src, tmpMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
1246
- this._rasterizePolys(tmpMask, entry.polys, entry.rule);
1247
- this.Render.Composite(this.Render.PictOp.In, tmpMask.id, 0, this.clipMask.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
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
- if (!this._clips.length) {
1254
- if (this.clipMask) {
1255
- this.clipMask.destroy();
1256
- this.clipMaskDrawable.destroy();
1257
- this.clipMask = this.clipMaskDrawable = null;
1258
- }
1259
- return;
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 ? flattenPath(path._cmds, this._m) : flattenPath(this._path._cmds, null);
1276
- return polysContain(polys, x, y, rule === 'evenodd' ? 'evenodd' : 'nonzero');
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 === 'start') align = shaped.baseLevel & 1 ? 'right' : 'left';
1291
- if (align === 'end') align = shaped.baseLevel & 1 ? 'left' : 'right';
1292
- if (align === 'center') return -shaped.width / 2;
1293
- if (align === 'right') return -shaped.width;
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 'top':
1724
+ case "top":
1301
1725
  return metrics.ascent;
1302
- case 'hanging':
1726
+ case "hanging":
1303
1727
  return metrics.ascent * 0.8;
1304
- case 'middle':
1728
+ case "middle":
1305
1729
  return (metrics.ascent - metrics.descent) / 2;
1306
- case 'bottom':
1307
- case 'ideographic':
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
- const shaped = app.fonts.shape(text, style);
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(this.Render.PictOp.Over, this._backgroundPicture, positioned);
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 ?? ''), style);
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 !== 'string') return;
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('linear', this, x0, y0, x1, y1);
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('radial', this, x0, y0, x1, y1, r0, r1);
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('conical', this, x0, y0, angle);
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 = this._target.depth ?? this.window.depth ?? this.display.screen[0].root_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 === 'number') return new ImageData(a, b);
1455
- if (a && typeof a.width === 'number' && a.data) return new ImageData(a.width, a.height);
1456
- throw new TypeError('createImageData: pass (width, height) or an ImageData');
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) { dirtyX += dirtyWidth; dirtyWidth = -dirtyWidth; }
1479
- if (dirtyHeight < 0) { dirtyY += dirtyHeight; dirtyHeight = -dirtyHeight; }
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((sy + row) * width * 4 + sx * 4, (sy + row) * width * 4 + (sx + sw) * 4),
1492
- row * sw * 4
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, rows,
1511
- x + sx, y + sy + row,
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 === 'function') {
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(2, this._target.id, x, y, w, h, 0xffffffff, (err, img) => {
1565
- if (err) return reject(err);
1566
- resolve({ width: w, height: h, data: img.data, layout, ...layout });
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.clipMask ? this._clipRect() : null;
1598
- if (!rect) return { mask: this._compositeMask(), clipped: false, empty: false };
1599
- if (rect.w === 0 || rect.h === 0) return { mask: 0, clipped: false, empty: true };
1600
- this.Render.SetPictureClipRectangles(this.picture.id, 0, 0, [rect.x, rect.y, rect.w, rect.h]);
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 ? 0 : this.createSolidPicture(0, 0, 0, this.globalAlpha).id;
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.Render.SetPictureClipRectangles(this.picture.id, 0, 0, [0, 0, this.width, this.height]);
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)) return this._backgroundPicture;
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, [sw / dw, 0, sx, 0, sh / dh, sy, 0, 0, 1]);
1637
- picture.setFilter('bilinear');
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.clipMask ? this._clipRect() : null;
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.clipMask && !rect) || (this.globalAlpha < 1 && !isPlainColor(this._fillStyle));
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(R.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
1653
- R.Composite(R.PictOp.Src, picture.id, 0, this.fillMask.id, mx, my, 0, 0, dx, dy, dw, dh);
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, 0, 0, 0, 0, 0,
1661
- this.width, this.height
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.clipMask) {
1665
- R.Composite(R.PictOp.In, this.clipMask.id, 0, this.fillMask.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
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, 0, 0, 0,
1673
- dx, dy,
1674
- dw, dh
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, [rect.x, rect.y, rect.w, rect.h]);
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, 0,
1686
- mx, my,
1687
- dx, dy,
1688
- dw, dh
2227
+ 0,
2228
+ 0,
2229
+ mx,
2230
+ my,
2231
+ dx,
2232
+ dy,
2233
+ dw,
2234
+ dh,
1689
2235
  );
1690
2236
  if (rect) {
1691
- R.SetPictureClipRectangles(this.picture.id, 0, 0, [0, 0, this.width, this.height]);
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('nearest');
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 === 'a8') {
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, [sw / dw, 0, sx, 0, sh / dh, sy, 0, 0, 1]);
1745
- picture.setFilter('bilinear');
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, 0,
1752
- dx, dy,
1753
- dx, dy,
1754
- dw, dh
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(picture.id, [1, 0, 0, 0, 1, 0, 0, 0, 1]);
1758
- picture.setFilter('nearest');
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, sy,
1766
- dx, dy,
1767
- dx, dy,
1768
- dw, dh
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, 0, 0, 0, 0, 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 (image && image.context && typeof image.context.getImageData === 'function') {
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, { depth: 32, width: sWidth, height: sHeight });
1816
- const picture = new Picture(app, { drawable: pixmap, format: this.Render.rgba32 });
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, 0, 0, 0, 0, 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(this.width, Math.ceil(Math.max(...corners.map((c) => c[0]))));
1864
- const y1 = Math.min(this.height, Math.ceil(Math.max(...corners.map((c) => c[1]))));
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, [srcM[0], srcM[2], srcM[4], srcM[1], srcM[3], srcM[5], 0, 0, 1]);
1870
- picture.setFilter('bilinear');
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(op, picture.id, mask, this.picture.id, x0, y0, x0, y0, x0, y0, x1 - x0, y1 - y0);
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('nearest');
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 === 'linear' || type === 'radial') {
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 'linear':
1915
- Render.LinearGradient(this._id, [this.x0, this.y0], [this.x1, this.y1], this.stops);
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 'radial':
1918
- Render.RadialGradient(this._id, [this.x0, this.y0], [this.x1, this.y1], this.r0, this.r1, this.stops);
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 'conical':
1921
- Render.ConicalGradient(this._id, [this.x0, this.y0], this.angle, this.stops);
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('unknown gradient type');
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['2d'] = (window) => new RenderingContext2d(window);
2567
+ Drawable.renderingContextFactory["2d"] = (window) =>
2568
+ new RenderingContext2d(window);
1934
2569
 
1935
2570
  export default RenderingContext2d;
1936
2571
  export { CanvasGradient };