ntk 8.2.0 → 8.3.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 +346 -7
- package/lib/cleanup.js +23 -7
- package/lib/drawable.js +10 -0
- package/lib/events_map.js +100 -1
- package/lib/glyphdirectory.js +275 -0
- package/lib/glyphdwire.js +142 -0
- package/lib/glyphset.js +32 -5
- package/lib/index.js +25 -2
- package/lib/pictformat.js +196 -0
- package/lib/pixmap.js +166 -7
- package/lib/region.js +148 -0
- package/lib/renderingcontext_2d.js +717 -235
- package/lib/shapeglyphs.js +94 -26
- package/lib/sharedglyphs.js +433 -0
- package/lib/text/glyphs.js +143 -26
- package/lib/widgets/svgview.js +7 -5
- package/lib/window.js +308 -40
- package/lib/xembed.js +3 -1
- package/package.json +2 -2
|
@@ -25,7 +25,9 @@ import {
|
|
|
25
25
|
matMultiply,
|
|
26
26
|
} from "./path.js";
|
|
27
27
|
import Picture from "./picture.js";
|
|
28
|
+
import { formatForDepth } from "./pictformat.js";
|
|
28
29
|
import Pixmap from "./pixmap.js";
|
|
30
|
+
import { REGION_DOCS, regionId } from "./region.js";
|
|
29
31
|
import { routeRaster } from "./rasterize.js";
|
|
30
32
|
import {
|
|
31
33
|
blurCoverage,
|
|
@@ -75,6 +77,11 @@ function fillableShapes(polys) {
|
|
|
75
77
|
return { shapes, edges };
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
/** Whether `v` is a whole number, give or take the slack a transform adds. */
|
|
81
|
+
function isIntegral(v) {
|
|
82
|
+
return Math.abs(v - Math.round(v)) < 1e-6;
|
|
83
|
+
}
|
|
84
|
+
|
|
78
85
|
/** The overlap of two {x, y, w, h} boxes, or null when they have none. */
|
|
79
86
|
function intersectBox(a, b) {
|
|
80
87
|
const x = Math.max(a.x, b.x);
|
|
@@ -152,13 +159,17 @@ function variationsKey(variations) {
|
|
|
152
159
|
* context owns is a Pixmap or a Picture, which carry their own finalizers,
|
|
153
160
|
* and reaching into them from this one would race those.
|
|
154
161
|
*/
|
|
155
|
-
const gcRegistry = new FinalizationRegistry(({ X, gcs }) => {
|
|
162
|
+
const gcRegistry = new FinalizationRegistry(({ X, gcs, regions }) => {
|
|
156
163
|
safeRelease(X, () => {
|
|
157
164
|
for (const gc of gcs) {
|
|
158
165
|
if (!gc) continue;
|
|
159
166
|
X.FreeGC(gc);
|
|
160
167
|
X.ReleaseID(gc);
|
|
161
168
|
}
|
|
169
|
+
for (const { fixes, id } of regions) {
|
|
170
|
+
fixes.DestroyRegion(id);
|
|
171
|
+
X.ReleaseID(id);
|
|
172
|
+
}
|
|
162
173
|
});
|
|
163
174
|
});
|
|
164
175
|
|
|
@@ -191,6 +202,63 @@ const REPEAT_MODES = {
|
|
|
191
202
|
const PATTERN_DOCS =
|
|
192
203
|
"https://github.com/sidorares/ntk/blob/master/docs/context-2d.md#patterns";
|
|
193
204
|
|
|
205
|
+
/**
|
|
206
|
+
* `clipRegion()` before anything loaded XFIXES.
|
|
207
|
+
*
|
|
208
|
+
* Not the same failure as a server that *has* no XFIXES (`app.fixes()` throws
|
|
209
|
+
* that one, with a code): here the extension is simply not on the connection
|
|
210
|
+
* yet, and it cannot be fetched from under a synchronous call that has to land
|
|
211
|
+
* in request order with the drawing around it. One await fixes it, and the
|
|
212
|
+
* call that hands back a region is already that await.
|
|
213
|
+
*/
|
|
214
|
+
function needXFixesError() {
|
|
215
|
+
return new Error(
|
|
216
|
+
"ntk: ctx.clipRegion() needs XFIXES loaded on this connection first.\n" +
|
|
217
|
+
"\n" +
|
|
218
|
+
"Making the region through ntk is that step:\n" +
|
|
219
|
+
"\n" +
|
|
220
|
+
" const region = await app.createRegion([{ x: 0, y: 0, width: 100, height: 100 }]);\n" +
|
|
221
|
+
" ctx.clipRegion(region);\n" +
|
|
222
|
+
"\n" +
|
|
223
|
+
"For a region you built through node-x11 yourself, one `await app.fixes()`\n" +
|
|
224
|
+
"anywhere before the first clipRegion() call is enough.\n" +
|
|
225
|
+
"\n" +
|
|
226
|
+
REGION_DOCS,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* A drawable whose depth nothing has established yet, handed to
|
|
232
|
+
* createPattern.
|
|
233
|
+
*
|
|
234
|
+
* Three drawables are like this, and they want different answers. A window
|
|
235
|
+
* or a pixmap adopted by id knows nothing until the request ntk sent for it
|
|
236
|
+
* replies, and `ready` is the wait for exactly that — the visual a window
|
|
237
|
+
* tile's format really comes from among them. A window ntk created with the
|
|
238
|
+
* default `depth: 0` — CopyFromParent — has no reply pending at all, because
|
|
239
|
+
* only the server ever resolved that 0, so its depth has to be asked for.
|
|
240
|
+
* `getGeometry()` covers all of them, which is why it leads.
|
|
241
|
+
*/
|
|
242
|
+
function unknownDepthError() {
|
|
243
|
+
return new Error(
|
|
244
|
+
"createPattern: the tile's depth is not known yet, so there is no " +
|
|
245
|
+
"picture\nformat to read it through.\n" +
|
|
246
|
+
"\n" +
|
|
247
|
+
"Ask the server for it — the answer is written back to the tile:\n" +
|
|
248
|
+
"\n" +
|
|
249
|
+
" await tile.getGeometry();\n" +
|
|
250
|
+
" const pattern = ctx.createPattern(tile, 'repeat');\n" +
|
|
251
|
+
"\n" +
|
|
252
|
+
"For a window or a pixmap adopted by id — `new Window(app, { id })`,\n" +
|
|
253
|
+
"`new Pixmap(app, { id })`, or `ev.window` in a window manager — ntk\n" +
|
|
254
|
+
"has already sent that request: `await tile.ready` waits for the reply\n" +
|
|
255
|
+
"it is expecting rather than asking again, and resolves immediately on\n" +
|
|
256
|
+
"one ntk created. `Pixmap.adopt(app, id)` hands the pixmap over with\n" +
|
|
257
|
+
"this already done. A Surface carries its own depth and needs no wait.\n" +
|
|
258
|
+
`\n${PATTERN_DOCS}`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
194
262
|
/**
|
|
195
263
|
* What a pattern tiles: a drawable holding the tile plus the picture format
|
|
196
264
|
* to read it through. A repeating source Picture is created over it rather
|
|
@@ -199,13 +267,12 @@ const PATTERN_DOCS =
|
|
|
199
267
|
*/
|
|
200
268
|
function patternSourceOf(app, source) {
|
|
201
269
|
const Render = app.display.Render;
|
|
202
|
-
const formatFor = (depth) =>
|
|
203
|
-
depth === 32 ? Render.rgba32 : depth === 8 ? "a8" : Render.rgb24;
|
|
204
270
|
|
|
205
271
|
let drawable = null;
|
|
206
272
|
let width;
|
|
207
273
|
let height;
|
|
208
274
|
let format;
|
|
275
|
+
let coverage = false; // an a8 tile: no colour to paint with (see below)
|
|
209
276
|
if (source instanceof Surface) {
|
|
210
277
|
if (source.app !== app) {
|
|
211
278
|
throw new Error(
|
|
@@ -213,7 +280,8 @@ function patternSourceOf(app, source) {
|
|
|
213
280
|
);
|
|
214
281
|
}
|
|
215
282
|
drawable = source.pixmap;
|
|
216
|
-
|
|
283
|
+
coverage = source.format === "a8";
|
|
284
|
+
format = coverage ? Render.a8 : Render.rgba32;
|
|
217
285
|
({ width, height } = source);
|
|
218
286
|
} else if (source instanceof Image) {
|
|
219
287
|
drawable = source.pixmap(app);
|
|
@@ -224,12 +292,12 @@ function patternSourceOf(app, source) {
|
|
|
224
292
|
// is where a double-buffered window's current pixels actually are)
|
|
225
293
|
drawable = source._backing || source;
|
|
226
294
|
const depth = drawable.depth ?? source.depth;
|
|
227
|
-
if (!depth)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
format =
|
|
295
|
+
if (!depth) throw unknownDepthError();
|
|
296
|
+
// the tile's visual is what names its format; the depth is the fallback
|
|
297
|
+
// for a drawable that has none (issue #295)
|
|
298
|
+
const visual = drawable.visualId || source.visualId;
|
|
299
|
+
coverage = depth === 8;
|
|
300
|
+
format = app._knownPictFormat?.(visual, depth) ?? formatForDepth(Render, depth);
|
|
233
301
|
width = drawable.width ?? source.width;
|
|
234
302
|
height = drawable.height ?? source.height;
|
|
235
303
|
} else {
|
|
@@ -238,7 +306,7 @@ function patternSourceOf(app, source) {
|
|
|
238
306
|
(source === null ? "null" : typeof source),
|
|
239
307
|
);
|
|
240
308
|
}
|
|
241
|
-
if (
|
|
309
|
+
if (coverage) {
|
|
242
310
|
throw new Error(
|
|
243
311
|
"createPattern: a coverage (a8) tile has no colour to paint with — XRender would " +
|
|
244
312
|
"sample it as black. Draw the tile into an argb32 Surface and tile that, or keep " +
|
|
@@ -424,7 +492,10 @@ class RenderingContext2d {
|
|
|
424
492
|
// ids land here as they are allocated; the finalizer holds this array
|
|
425
493
|
// rather than the context, so registering cannot keep the context alive
|
|
426
494
|
this._gcs = [];
|
|
427
|
-
|
|
495
|
+
// same for the scratch regions the region-clip path allocates: the
|
|
496
|
+
// finalizer holds the arrays, never the context
|
|
497
|
+
this._regions = [];
|
|
498
|
+
gcRegistry.register(this, { X, gcs: this._gcs, regions: this._regions }, this);
|
|
428
499
|
|
|
429
500
|
this.window = window;
|
|
430
501
|
this.display = window.app.display;
|
|
@@ -435,17 +506,40 @@ class RenderingContext2d {
|
|
|
435
506
|
// buffering); re-bind when the backing pixmap is reallocated on resize
|
|
436
507
|
this._target = null;
|
|
437
508
|
this._gc = null;
|
|
438
|
-
this.
|
|
509
|
+
this._picture = null;
|
|
439
510
|
this._bindTarget();
|
|
511
|
+
// A window or pixmap adopted by id knows neither its depth nor its
|
|
512
|
+
// visual until the requests its constructor sent reply, and an unknown
|
|
513
|
+
// depth binds rgb24 — so a context taken on a depth-32 drawable before
|
|
514
|
+
// they land would silently drop the alpha channel, which is the ordinary
|
|
515
|
+
// case for a compositor taking the overlay window, its clients and their
|
|
516
|
+
// named pixmaps as bare ids (issues #293, #291). Awaiting `ready` first
|
|
517
|
+
// is the explicit fix; re-binding when the answer changes the format is
|
|
518
|
+
// the one that does not need to be known about.
|
|
519
|
+
if (!window.depth && typeof window.ready?.then === "function") {
|
|
520
|
+
window.ready.then(() => this._refreshFormat());
|
|
521
|
+
}
|
|
522
|
+
// The same, for the visual -> format table: it is one round trip on the
|
|
523
|
+
// connection (issue #295), taken while the connection is still being set
|
|
524
|
+
// up, so it is normally here already — this is the case where it is not,
|
|
525
|
+
// and the binding above took the depth's standard format. Where the
|
|
526
|
+
// visual names a different one — a 5:6:5 window, a 10-bit one, a BGR one
|
|
527
|
+
// — that binding is wrong and this is what puts it right.
|
|
528
|
+
if (window.app && !window.app._pictFormats) {
|
|
529
|
+
window.app.pictFormats?.().then(
|
|
530
|
+
() => this._refreshFormat(),
|
|
531
|
+
() => {},
|
|
532
|
+
);
|
|
533
|
+
}
|
|
440
534
|
if (typeof window.on === "function") {
|
|
441
535
|
window.on("_backing", () => this._bindTarget());
|
|
442
536
|
// masks are sized to the drawable — recreate them after a resize
|
|
443
537
|
window.on("resize", () => this._dropMasks());
|
|
444
538
|
// window-backed pictures are freed server-side with the window
|
|
445
539
|
window.on("_destroyed", () => {
|
|
446
|
-
if (this.
|
|
447
|
-
this.
|
|
448
|
-
this.
|
|
540
|
+
if (this._picture && this._target === this.window) {
|
|
541
|
+
this._picture.forget();
|
|
542
|
+
this._picture = null;
|
|
449
543
|
}
|
|
450
544
|
});
|
|
451
545
|
}
|
|
@@ -472,7 +566,26 @@ class RenderingContext2d {
|
|
|
472
566
|
// drawings that took more than one.
|
|
473
567
|
this.maskStats = { masks: 0, pixels: 0, split: 0 };
|
|
474
568
|
this._stack = [];
|
|
475
|
-
|
|
569
|
+
// [{ polys, rule, rect } | { region }] in device space, already stacked.
|
|
570
|
+
// Never mutated in place — save() shares the array with the snapshot and
|
|
571
|
+
// clip() concats a new one — so the two summary flags below only have to
|
|
572
|
+
// be recomputed where it is replaced (_setClips).
|
|
573
|
+
this._clips = [];
|
|
574
|
+
this._hasPolyClip = false; // any entry the a8 mask has to rasterize
|
|
575
|
+
this._hasRegionClip = false; // any XFIXES region entry
|
|
576
|
+
// XFIXES, once clipRegion() has been called on this context, and what
|
|
577
|
+
// the picture is currently clipped by. "No clip" is a state ntk tracks
|
|
578
|
+
// rather than a rectangle it stamps: a caller's region has to survive a
|
|
579
|
+
// drawing that only meant to narrow to a box (issue #292).
|
|
580
|
+
this._fixes = null;
|
|
581
|
+
this._pictureClipped = false;
|
|
582
|
+
// the rectangle the slot holds, when it is a plain one (no region in it),
|
|
583
|
+
// so a second drawing under the same clip can skip re-stamping it; and
|
|
584
|
+
// whether a reset is owed but not yet sent (issue #308)
|
|
585
|
+
this._pictureClipRect = null;
|
|
586
|
+
this._clipStale = false;
|
|
587
|
+
this._regionScratch = null; // region entries intersected, when >1
|
|
588
|
+
this._regionBox = null; // that ∩ the rectangular clip
|
|
476
589
|
this._gco = "source-over";
|
|
477
590
|
this.globalAlpha = 1;
|
|
478
591
|
this.lineCap = "butt";
|
|
@@ -495,9 +608,46 @@ class RenderingContext2d {
|
|
|
495
608
|
this.lineWidth = 1;
|
|
496
609
|
}
|
|
497
610
|
|
|
498
|
-
|
|
611
|
+
/**
|
|
612
|
+
* The picture format the target drawable is read and written through.
|
|
613
|
+
*
|
|
614
|
+
* The visual is what names it — depth alone does not (issue #295) — and
|
|
615
|
+
* the depth is the fallback for a drawable that has no visual to name
|
|
616
|
+
* (a pixmap, an `a8` coverage surface) or whose visual has not been
|
|
617
|
+
* answered for yet.
|
|
618
|
+
*/
|
|
619
|
+
_formatFor(depth, visual) {
|
|
620
|
+
return (
|
|
621
|
+
this.window.app?._knownPictFormat?.(visual, depth) ?? formatForDepth(this.Render, depth)
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** The depth and visual of whatever the context is currently bound to. */
|
|
626
|
+
_targetPixels() {
|
|
627
|
+
const target = this._target;
|
|
628
|
+
return {
|
|
629
|
+
depth: target?.depth ?? this.window.depth,
|
|
630
|
+
// a backing pixmap carries the window's visual; nothing else about a
|
|
631
|
+
// pixmap says what its pixels mean
|
|
632
|
+
visual: target?.visualId || this.window.visualId,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Re-bind if what is now known about the target's pixels names a different
|
|
638
|
+
* format than the picture was built with. Both answers arrive late on an
|
|
639
|
+
* adopted window, and neither costs anything when it changes nothing.
|
|
640
|
+
*/
|
|
641
|
+
_refreshFormat() {
|
|
642
|
+
const target = this.window._backing || this.window;
|
|
643
|
+
if (!this._picture || this._target !== target) return;
|
|
644
|
+
const { depth, visual } = this._targetPixels();
|
|
645
|
+
if (this._formatFor(depth, visual) !== this._picture.format) this._bindTarget(true);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
_bindTarget(force = false) {
|
|
499
649
|
const target = this.window._backing || this.window;
|
|
500
|
-
if (this._target === target) return;
|
|
650
|
+
if (this._target === target && !force) return;
|
|
501
651
|
this._target = target;
|
|
502
652
|
this._layoutCache = null; // the new target may be a different depth
|
|
503
653
|
|
|
@@ -508,36 +658,210 @@ class RenderingContext2d {
|
|
|
508
658
|
this._gcs.push(this._gc);
|
|
509
659
|
this.X.CreateGC(this._gc, target.id);
|
|
510
660
|
}
|
|
511
|
-
if (this.
|
|
661
|
+
if (this._picture) this._picture.destroy();
|
|
512
662
|
// a8 targets are coverage, not colour: drawing into one and using the
|
|
513
663
|
// result as a mask is how a monochrome drawing gets rendered once and
|
|
514
664
|
// recoloured on every use (see Surface).
|
|
515
|
-
const depth
|
|
516
|
-
const format =
|
|
517
|
-
depth === 32
|
|
518
|
-
? this.Render.rgba32
|
|
519
|
-
: depth === 8
|
|
520
|
-
? this.Render.a8
|
|
521
|
-
: this.Render.rgb24;
|
|
665
|
+
const { depth, visual } = this._targetPixels();
|
|
666
|
+
const format = this._formatFor(depth, visual);
|
|
522
667
|
// Does the target have a real alpha channel? Only then is "transparent"
|
|
523
668
|
// a colour it can hold, which is what clearRect turns on.
|
|
524
669
|
this._hasAlpha = depth === 32;
|
|
525
|
-
this.
|
|
670
|
+
this._picture = new Picture(this.window.app, {
|
|
526
671
|
drawable: target,
|
|
527
672
|
format,
|
|
528
673
|
polyEdge: 1,
|
|
529
674
|
polyMode: 1,
|
|
530
675
|
});
|
|
676
|
+
// a new picture carries no clip; a region one is context state and has to
|
|
677
|
+
// be re-installed on it (this runs on every backing-pixmap reallocation)
|
|
678
|
+
this._pictureClipped = false;
|
|
679
|
+
this._pictureClipRect = null;
|
|
680
|
+
this._invalidatePictureClip();
|
|
531
681
|
this._dropMasks();
|
|
532
682
|
}
|
|
533
683
|
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
|
|
684
|
+
// ------------------------------------------------------------------
|
|
685
|
+
// the picture's clip
|
|
686
|
+
//
|
|
687
|
+
// Two things narrow a drawing server-side: a rectangle from the clip stack
|
|
688
|
+
// (SetPictureClipRectangles, the fast path text and rounded boxes take) and
|
|
689
|
+
// an XFIXES region from clipRegion(). Both land on the same one slot — a
|
|
690
|
+
// Picture holds exactly one client clip — so they go through here, and here
|
|
691
|
+
// is also what puts the slot back afterwards.
|
|
692
|
+
//
|
|
693
|
+
// "Back" is the load-bearing word (issue #292). Undoing a narrow used to
|
|
694
|
+
// mean stamping a full-plane rectangle, which is a clip, not the absence of
|
|
695
|
+
// one: it overwrote whatever else was in the slot. Now the slot's contents
|
|
696
|
+
// are state the context tracks, so a region clip survives a fill or a glyph
|
|
697
|
+
// run that only meant to narrow to a box.
|
|
698
|
+
//
|
|
699
|
+
// Tracking the contents also makes the stamping *lazy* (issue #308). Every
|
|
700
|
+
// rect-clipped fast path brackets its own drawing — set, draw, reset — so
|
|
701
|
+
// two drawings under one clip used to emit a reset immediately followed by
|
|
702
|
+
// an identical set, with nothing in between that reads the slot. Instead
|
|
703
|
+
// the reset is only recorded as owed, `_dst()` flushes it before any
|
|
704
|
+
// drawing that does *not* set its own clip, and a set whose rectangle is
|
|
705
|
+
// already in the slot supersedes it and sends nothing. A rounded box under
|
|
706
|
+
// a damage clip goes from four stamps to one, and a repaint of the same
|
|
707
|
+
// rectangle next frame to none.
|
|
708
|
+
|
|
709
|
+
/** Narrow the picture to `rect`, intersected with any region clip. */
|
|
710
|
+
_setPictureClip(rect) {
|
|
711
|
+
const region = this._effectiveRegion();
|
|
712
|
+
if (region === null) {
|
|
713
|
+
const held = this._pictureClipRect;
|
|
714
|
+
// The owed reset (if any) is superseded either way: what follows draws
|
|
715
|
+
// under exactly this rectangle, which is what the slot is about to hold.
|
|
716
|
+
this._clipStale = false;
|
|
717
|
+
if (
|
|
718
|
+
held &&
|
|
719
|
+
held.x === rect.x &&
|
|
720
|
+
held.y === rect.y &&
|
|
721
|
+
held.w === rect.w &&
|
|
722
|
+
held.h === rect.h
|
|
723
|
+
) {
|
|
724
|
+
return; // already stamped, and nothing since has read the slot
|
|
725
|
+
}
|
|
726
|
+
this.Render.SetPictureClipRectangles(this._picture.id, 0, 0, [
|
|
727
|
+
rect.x,
|
|
728
|
+
rect.y,
|
|
729
|
+
rect.w,
|
|
730
|
+
rect.h,
|
|
731
|
+
]);
|
|
732
|
+
this._pictureClipped = true;
|
|
733
|
+
// a copy: the caller owns `rect` and _clipRect() hands out fresh ones,
|
|
734
|
+
// but nothing here should depend on that
|
|
735
|
+
this._pictureClipRect = { x: rect.x, y: rect.y, w: rect.w, h: rect.h };
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
// region ∩ rectangle, computed by the server: three requests and no round
|
|
739
|
+
// trip, against a full-surface a8 mask for the same answer.
|
|
740
|
+
//
|
|
741
|
+
// Always re-emitted, never skipped: the region is the caller's and they
|
|
742
|
+
// may have edited it — server-side, invisibly to us — since the last
|
|
743
|
+
// drawing, for the same reason _effectiveRegion() is recomputed.
|
|
744
|
+
const box = this._scratchRegion("_regionBox");
|
|
745
|
+
this._fixes.SetRegion(box, [
|
|
746
|
+
{ x: rect.x, y: rect.y, width: rect.w, height: rect.h },
|
|
747
|
+
]);
|
|
748
|
+
this._fixes.IntersectRegion(region, box, box);
|
|
749
|
+
this._fixes.SetPictureClipRegion(this._picture.id, box, 0, 0);
|
|
750
|
+
this._pictureClipped = true;
|
|
751
|
+
this._pictureClipRect = null; // a region is in there, not a plain rect
|
|
752
|
+
this._clipStale = false;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Note that the picture's clip no longer matches what the clip stack says,
|
|
757
|
+
* without sending anything: the next drawing either sets its own clip
|
|
758
|
+
* (which supersedes this) or goes through `_dst()`, which flushes it first.
|
|
759
|
+
*
|
|
760
|
+
* This is what every rect-clipped fast path calls after its drawing, and
|
|
761
|
+
* also what installs a region in the first place — `clipRegion()` and a
|
|
762
|
+
* `restore()` that changes the stack both come through here, so the region
|
|
763
|
+
* is on the picture for every route, including the ones that composite an
|
|
764
|
+
* a8 mask and never touch a clip rectangle.
|
|
765
|
+
*/
|
|
766
|
+
_invalidatePictureClip() {
|
|
767
|
+
this._clipStale = true;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Put the picture's clip back to what the clip stack says it is between
|
|
772
|
+
* drawings: the region clip if there is one, nothing otherwise. Sends
|
|
773
|
+
* nothing when the slot already says that.
|
|
774
|
+
*/
|
|
775
|
+
_flushPictureClip() {
|
|
776
|
+
if (!this._clipStale) return;
|
|
777
|
+
this._clipStale = false;
|
|
778
|
+
if (!this._picture) return; // destroyed, or a window that went away
|
|
779
|
+
const region = this._effectiveRegion();
|
|
780
|
+
if (region !== null) {
|
|
781
|
+
this._fixes.SetPictureClipRegion(this._picture.id, region, 0, 0);
|
|
782
|
+
this._pictureClipped = true;
|
|
783
|
+
this._pictureClipRect = null;
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (!this._pictureClipped) return; // nothing of ours is in the slot
|
|
787
|
+
if (this._fixes) {
|
|
788
|
+
// None is the real "no clip"; a rectangle would only be a wider one
|
|
789
|
+
this._fixes.SetPictureClipRegion(this._picture.id, 0, 0, 0);
|
|
790
|
+
} else {
|
|
791
|
+
// Without XFIXES the widest rectangle is all there is, and it has to
|
|
792
|
+
// outlive window growth: the backing pixmap has headroom past the
|
|
793
|
+
// window and growing into it does not rebind the picture, so a reset at
|
|
794
|
+
// today's window size would keep clipping tomorrow's pixels.
|
|
795
|
+
// Coordinates are INT16 — one rect at their maximum covers any drawable.
|
|
796
|
+
this.Render.SetPictureClipRectangles(this._picture.id, 0, 0, [0, 0, 0x7fff, 0x7fff]);
|
|
797
|
+
}
|
|
798
|
+
this._pictureClipped = false;
|
|
799
|
+
this._pictureClipRect = null;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* The destination picture id for a drawing request, with any owed clip
|
|
804
|
+
* reset flushed first.
|
|
805
|
+
*
|
|
806
|
+
* Everything that draws into this context's own picture goes through here
|
|
807
|
+
* — including the composites that bypass the set/reset bracket entirely
|
|
808
|
+
* (`_fillRect`, the `clearRect` fast path, the a8-mask routes) — because
|
|
809
|
+
* that is what keeps a lazily-owed reset from leaking a narrower clip into
|
|
810
|
+
* a drawing that never asked for one.
|
|
811
|
+
*/
|
|
812
|
+
_dst() {
|
|
813
|
+
this._flushPictureClip();
|
|
814
|
+
return this._picture.id;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* The RENDER Picture this context draws through — a real server-side
|
|
819
|
+
* picture a caller can composite from, or hang a clip region on
|
|
820
|
+
* (docs/context-2d.md). Reading it settles the clip slot first, so what an
|
|
821
|
+
* outside request sees is what the clip stack says, not whatever the last
|
|
822
|
+
* fast path left behind.
|
|
823
|
+
*/
|
|
824
|
+
get picture() {
|
|
825
|
+
this._flushPictureClip();
|
|
826
|
+
return this._picture;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* The region the clip stack currently means, or null when it holds no
|
|
831
|
+
* region entry. One entry is used as it is; several are intersected into a
|
|
832
|
+
* scratch region owned by this context.
|
|
833
|
+
*
|
|
834
|
+
* Recomputed rather than cached: the intersection is of regions the caller
|
|
835
|
+
* owns and may edit under us, and nesting region clips is rare enough that
|
|
836
|
+
* a few requests are the cheaper mistake to make.
|
|
837
|
+
*/
|
|
838
|
+
_effectiveRegion() {
|
|
839
|
+
if (!this._hasRegionClip) return null;
|
|
840
|
+
let first = null;
|
|
841
|
+
let dst = null;
|
|
842
|
+
for (const entry of this._clips) {
|
|
843
|
+
if (!entry.region) continue;
|
|
844
|
+
if (first === null) {
|
|
845
|
+
first = entry.region;
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
if (dst === null) {
|
|
849
|
+
dst = this._scratchRegion("_regionScratch");
|
|
850
|
+
this._fixes.CopyRegion(first, dst);
|
|
851
|
+
}
|
|
852
|
+
this._fixes.IntersectRegion(dst, entry.region, dst);
|
|
853
|
+
}
|
|
854
|
+
return dst === null ? first : dst;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/** One of this context's two scratch regions, allocated on first use. */
|
|
858
|
+
_scratchRegion(slot) {
|
|
859
|
+
if (this[slot] !== null) return this[slot];
|
|
860
|
+
const id = this.X.AllocID();
|
|
861
|
+
this._fixes.CreateRegion(id, []);
|
|
862
|
+
this[slot] = id;
|
|
863
|
+
this._regions.push({ fixes: this._fixes, id });
|
|
864
|
+
return id;
|
|
541
865
|
}
|
|
542
866
|
|
|
543
867
|
/**
|
|
@@ -561,8 +885,16 @@ class RenderingContext2d {
|
|
|
561
885
|
this._destroyed = true;
|
|
562
886
|
gcRegistry.unregister(this);
|
|
563
887
|
|
|
564
|
-
this.
|
|
888
|
+
this._setClips([]);
|
|
565
889
|
this._dropMasks();
|
|
890
|
+
for (const { fixes, id } of this._regions) {
|
|
891
|
+
safeRelease(this.X, () => {
|
|
892
|
+
fixes.DestroyRegion(id);
|
|
893
|
+
this.X.ReleaseID(id);
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
this._regions.length = 0;
|
|
897
|
+
this._regionScratch = this._regionBox = null;
|
|
566
898
|
for (const gc of [this._gc, this._fillMaskGC, this._clipMaskGC]) {
|
|
567
899
|
if (!gc) continue;
|
|
568
900
|
safeRelease(this.X, () => {
|
|
@@ -575,15 +907,15 @@ class RenderingContext2d {
|
|
|
575
907
|
|
|
576
908
|
this._backgroundPicture = this._glyphSource = null;
|
|
577
909
|
|
|
578
|
-
if (this.
|
|
910
|
+
if (this._picture) {
|
|
579
911
|
// a picture on a *window* is freed by the server with the window;
|
|
580
912
|
// asking again would raise BadPicture. Same rule as `_destroyed`.
|
|
581
913
|
if (this._target === this.window && !(this.window instanceof Pixmap)) {
|
|
582
|
-
this.
|
|
914
|
+
this._picture.forget();
|
|
583
915
|
} else {
|
|
584
|
-
this.
|
|
916
|
+
this._picture.destroy();
|
|
585
917
|
}
|
|
586
|
-
this.
|
|
918
|
+
this._picture = null;
|
|
587
919
|
}
|
|
588
920
|
}
|
|
589
921
|
|
|
@@ -615,7 +947,7 @@ class RenderingContext2d {
|
|
|
615
947
|
// a hover repaint of two tab headers used to blit the whole window. A
|
|
616
948
|
// context with no rectangular clip reports nothing, and the window falls
|
|
617
949
|
// back to a full blit, which is what any drawing outside a clip needs.
|
|
618
|
-
this.window._markDirty(this.
|
|
950
|
+
this.window._markDirty(this._clipRect());
|
|
619
951
|
}
|
|
620
952
|
|
|
621
953
|
// html context2d compatibility: canvas.getContext('2d').canvas == canvas
|
|
@@ -764,8 +1096,12 @@ class RenderingContext2d {
|
|
|
764
1096
|
this._shadowOffsetY = s.shadowOffsetY;
|
|
765
1097
|
this._m = s.m;
|
|
766
1098
|
if (s.clips !== this._clips) {
|
|
767
|
-
|
|
1099
|
+
const hadRegion = this._hasRegionClip;
|
|
1100
|
+
this._setClips(s.clips);
|
|
768
1101
|
this._rebuildClipMask();
|
|
1102
|
+
// a region clip going out of scope (or coming back into it) is a change
|
|
1103
|
+
// to the picture's clip, and nothing else will notice it
|
|
1104
|
+
if (hadRegion || this._hasRegionClip) this._invalidatePictureClip();
|
|
769
1105
|
}
|
|
770
1106
|
}
|
|
771
1107
|
|
|
@@ -1631,16 +1967,19 @@ class RenderingContext2d {
|
|
|
1631
1967
|
// A rectangular clip narrows where the coverage is *composited*; the
|
|
1632
1968
|
// mask content outside it is stale by the same argument as outside `b`,
|
|
1633
1969
|
// and a box the clip rejects outright needs no coverage at all. A stack
|
|
1634
|
-
// whose mask was dropped by restore() may still hold a poly
|
|
1635
|
-
//
|
|
1970
|
+
// whose mask was dropped by restore() may still hold a poly, and the mask
|
|
1971
|
+
// comes back on demand. A region clip needs nothing here: the picture
|
|
1972
|
+
// carries it, and it applies to the composite below like any other.
|
|
1636
1973
|
let out = b;
|
|
1637
|
-
if (
|
|
1638
|
-
|
|
1639
|
-
if (cr) {
|
|
1640
|
-
out = intersectBox(b, cr);
|
|
1641
|
-
if (!out) return false;
|
|
1642
|
-
} else {
|
|
1974
|
+
if (!this.clipMask) {
|
|
1975
|
+
if (this._hasPolyClip) {
|
|
1643
1976
|
this._requireClipMask();
|
|
1977
|
+
} else {
|
|
1978
|
+
const cr = this._clipRect();
|
|
1979
|
+
if (cr) {
|
|
1980
|
+
out = intersectBox(b, cr);
|
|
1981
|
+
if (!out) return false;
|
|
1982
|
+
}
|
|
1644
1983
|
}
|
|
1645
1984
|
}
|
|
1646
1985
|
if (!this._uploadCoverage({ ...job, dx: -b.x, dy: -b.y }, b, edges)) {
|
|
@@ -1684,7 +2023,7 @@ class RenderingContext2d {
|
|
|
1684
2023
|
op,
|
|
1685
2024
|
src.id,
|
|
1686
2025
|
this.fillMask.id,
|
|
1687
|
-
this.
|
|
2026
|
+
this._dst(),
|
|
1688
2027
|
out.x,
|
|
1689
2028
|
out.y,
|
|
1690
2029
|
out.x,
|
|
@@ -2082,7 +2421,10 @@ class RenderingContext2d {
|
|
|
2082
2421
|
const direct =
|
|
2083
2422
|
!hasRound &&
|
|
2084
2423
|
this.globalAlpha >= 1 &&
|
|
2085
|
-
|
|
2424
|
+
// a region clip stays on the picture and needs nothing here; a
|
|
2425
|
+
// rectangle or a mask does
|
|
2426
|
+
!this._hasPolyClip &&
|
|
2427
|
+
!this._clipRect() &&
|
|
2086
2428
|
op === this.Render.PictOp.Over;
|
|
2087
2429
|
const chunk = 4000 * 6;
|
|
2088
2430
|
if (direct) {
|
|
@@ -2100,7 +2442,7 @@ class RenderingContext2d {
|
|
|
2100
2442
|
src.id,
|
|
2101
2443
|
Math.floor(batch[0]),
|
|
2102
2444
|
Math.floor(batch[1]),
|
|
2103
|
-
this.
|
|
2445
|
+
this._dst(),
|
|
2104
2446
|
this.Render.a8,
|
|
2105
2447
|
batch,
|
|
2106
2448
|
);
|
|
@@ -2295,26 +2637,20 @@ class RenderingContext2d {
|
|
|
2295
2637
|
const app = this.window.app;
|
|
2296
2638
|
const R = this.Render;
|
|
2297
2639
|
if (!prepareStyle(src, this._m)) return;
|
|
2298
|
-
if (!this.
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
rect.y,
|
|
2313
|
-
rect.w,
|
|
2314
|
-
rect.h,
|
|
2315
|
-
]);
|
|
2316
|
-
drawGlyphRuns(app, op, src.id, this.picture.id, positioned);
|
|
2317
|
-
this._resetPictureClip();
|
|
2640
|
+
if (!this._hasPolyClip) {
|
|
2641
|
+
// Fast path: a rectangular clip is something the server can do itself.
|
|
2642
|
+
// Two small requests around the ordinary glyph composite, instead of
|
|
2643
|
+
// clearing and compositing a full-surface a8 mask three times — which
|
|
2644
|
+
// costs the same on the wire but many times the pixel work. No clip
|
|
2645
|
+
// stack at all, or only a region one, needs neither: the picture is
|
|
2646
|
+
// already carrying whatever applies.
|
|
2647
|
+
const rect = this._clipRect();
|
|
2648
|
+
if (rect) {
|
|
2649
|
+
if (rect.w === 0 || rect.h === 0) return;
|
|
2650
|
+
this._setPictureClip(rect);
|
|
2651
|
+
}
|
|
2652
|
+
drawGlyphRuns(app, op, src.id, this._dst(), positioned);
|
|
2653
|
+
if (rect) this._invalidatePictureClip();
|
|
2318
2654
|
this._markDirty();
|
|
2319
2655
|
return;
|
|
2320
2656
|
}
|
|
@@ -2353,7 +2689,7 @@ class RenderingContext2d {
|
|
|
2353
2689
|
op,
|
|
2354
2690
|
src.id,
|
|
2355
2691
|
this.fillMask.id,
|
|
2356
|
-
this.
|
|
2692
|
+
this._dst(),
|
|
2357
2693
|
0,
|
|
2358
2694
|
0,
|
|
2359
2695
|
0,
|
|
@@ -2376,22 +2712,14 @@ class RenderingContext2d {
|
|
|
2376
2712
|
if (!traps.length) return;
|
|
2377
2713
|
const app = this.window.app;
|
|
2378
2714
|
const R = this.Render;
|
|
2379
|
-
if (!this.
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
if (rect
|
|
2387
|
-
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
2388
|
-
rect.x,
|
|
2389
|
-
rect.y,
|
|
2390
|
-
rect.w,
|
|
2391
|
-
rect.h,
|
|
2392
|
-
]);
|
|
2393
|
-
compositeTraps(app, op, src.id, this.picture.id, traps);
|
|
2394
|
-
this._resetPictureClip();
|
|
2715
|
+
if (!this._hasPolyClip) {
|
|
2716
|
+
const rect = this._clipRect();
|
|
2717
|
+
if (rect) {
|
|
2718
|
+
if (rect.w === 0 || rect.h === 0) return;
|
|
2719
|
+
this._setPictureClip(rect);
|
|
2720
|
+
}
|
|
2721
|
+
compositeTraps(app, op, src.id, this._dst(), traps);
|
|
2722
|
+
if (rect) this._invalidatePictureClip();
|
|
2395
2723
|
this._markDirty();
|
|
2396
2724
|
return;
|
|
2397
2725
|
}
|
|
@@ -2429,7 +2757,7 @@ class RenderingContext2d {
|
|
|
2429
2757
|
op,
|
|
2430
2758
|
src.id,
|
|
2431
2759
|
this.fillMask.id,
|
|
2432
|
-
this.
|
|
2760
|
+
this._dst(),
|
|
2433
2761
|
0,
|
|
2434
2762
|
0,
|
|
2435
2763
|
0,
|
|
@@ -2442,19 +2770,111 @@ class RenderingContext2d {
|
|
|
2442
2770
|
this._markDirty();
|
|
2443
2771
|
}
|
|
2444
2772
|
|
|
2773
|
+
/**
|
|
2774
|
+
* The cheap form of a direct composite, when there is one: the box to
|
|
2775
|
+
* composite over and what belongs in its mask slot, with no surface-sized
|
|
2776
|
+
* a8 mask anywhere (issue #307).
|
|
2777
|
+
*
|
|
2778
|
+
* Neither thing `_compositeMask()` puts in a mask is really mask work.
|
|
2779
|
+
* A **rectangular clip** is a smaller composite box: for an op that leaves
|
|
2780
|
+
* the destination alone where the mask is zero, zeroing the mask outside
|
|
2781
|
+
* the rectangle and never compositing there are the same picture — and a
|
|
2782
|
+
* rect clip is an integer rectangle stamped as binary coverage, so the two
|
|
2783
|
+
* agree pixel for pixel rather than approximately. **`globalAlpha`** is a
|
|
2784
|
+
* 1x1 solid, which is that alpha everywhere the server samples it, for the
|
|
2785
|
+
* price of no pixels at all. A **region** clip needs neither: the picture
|
|
2786
|
+
* carries it, and it applies to the composite like any other.
|
|
2787
|
+
*
|
|
2788
|
+
* What is left needing a mask is what genuinely needs one: a
|
|
2789
|
+
* non-rectangular clip entry, or an op that writes where the mask is zero
|
|
2790
|
+
* — `copy`, `source-in`, `destination-in`, `source-out` and
|
|
2791
|
+
* `destination-atop` cover the whole composite box, so a shrunk box would
|
|
2792
|
+
* leave behind the pixels the mask path clears.
|
|
2793
|
+
*
|
|
2794
|
+
* `box` is the composite's destination box in device coordinates. Callers
|
|
2795
|
+
* pass their own arguments through, so it is only intersected when it is
|
|
2796
|
+
* integral: clip rectangles are integers and caller coordinates are not
|
|
2797
|
+
* necessarily, and rounding one here could land a pixel away from where
|
|
2798
|
+
* the mask path draws.
|
|
2799
|
+
*
|
|
2800
|
+
* @returns {{box: ?object, mask: number}|null} the box to composite over —
|
|
2801
|
+
* null when the clip rejects it whole and nothing should be drawn — and
|
|
2802
|
+
* the mask slot for it; or null when this route does not apply and the
|
|
2803
|
+
* caller has to fall back to `_compositeMask()`.
|
|
2804
|
+
*/
|
|
2805
|
+
_boxedComposite(box, op) {
|
|
2806
|
+
if (this._hasPolyClip) return null;
|
|
2807
|
+
let out = box;
|
|
2808
|
+
const clip = this._clipRect();
|
|
2809
|
+
if (clip) {
|
|
2810
|
+
if (!this._maskBounded(op)) return null;
|
|
2811
|
+
if (![box.x, box.y, box.w, box.h].every(isIntegral)) return null;
|
|
2812
|
+
out = intersectBox(box, clip);
|
|
2813
|
+
}
|
|
2814
|
+
return { box: out, mask: this._alphaMask() };
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
/**
|
|
2818
|
+
* `globalAlpha` as something to put in a mask slot: a 1x1 solid, which is
|
|
2819
|
+
* that alpha over any box the server samples it across, or 0 when there is
|
|
2820
|
+
* no alpha to apply. The surface-sized a8 `_compositeMask()` fills for the
|
|
2821
|
+
* same answer is pure pixel work.
|
|
2822
|
+
*/
|
|
2823
|
+
_alphaMask() {
|
|
2824
|
+
if (this.globalAlpha >= 1) return 0;
|
|
2825
|
+
return this.createSolidPicture(0, 0, 0, this.globalAlpha).id;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
/**
|
|
2829
|
+
* The composite's own box, as somewhere to put pixels: `box` grown to
|
|
2830
|
+
* whole pixels and clamped to the surface, or the whole surface when the
|
|
2831
|
+
* caller has no box to give.
|
|
2832
|
+
*
|
|
2833
|
+
* Growing outward rather than rounding is the safe direction — the mask
|
|
2834
|
+
* has to cover every pixel the composite samples, and a pixel of slack
|
|
2835
|
+
* outside it is never read.
|
|
2836
|
+
*/
|
|
2837
|
+
_maskBox(box) {
|
|
2838
|
+
// no box, or one the caller never normalized: the whole surface, which
|
|
2839
|
+
// is what this always used to be
|
|
2840
|
+
if (!box || !(box.w > 0) || !(box.h > 0))
|
|
2841
|
+
return { x: 0, y: 0, w: this.width, h: this.height };
|
|
2842
|
+
const x = Math.max(0, Math.floor(box.x));
|
|
2843
|
+
const y = Math.max(0, Math.floor(box.y));
|
|
2844
|
+
const right = Math.min(this.width, Math.ceil(box.x + box.w));
|
|
2845
|
+
const bottom = Math.min(this.height, Math.ceil(box.y + box.h));
|
|
2846
|
+
if (right <= x || bottom <= y) return { x: 0, y: 0, w: 0, h: 0 };
|
|
2847
|
+
return { x, y, w: right - x, h: bottom - y };
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2445
2850
|
// combined clip ∩ globalAlpha mask for direct composites (rect/image
|
|
2446
2851
|
// fast paths); returns a picture id or 0. Reuses the fill scratch mask.
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2852
|
+
//
|
|
2853
|
+
// The fallback, not the common case: `_boxedComposite` above answers for a
|
|
2854
|
+
// rect-only clip stack without materializing anything, and every caller
|
|
2855
|
+
// here asks it first. A **region** clip needs nothing either — the picture
|
|
2856
|
+
// carries it, and the mask must not try to hold it.
|
|
2857
|
+
//
|
|
2858
|
+
// `box` is the destination box of the composite the mask is for, in device
|
|
2859
|
+
// coordinates. The composite samples the mask over that box and nowhere
|
|
2860
|
+
// else, so both writes here take it instead of the whole surface (issue
|
|
2861
|
+
// #313): a translucent fill inside a rounded-corner clip used to stamp the
|
|
2862
|
+
// alpha across the full surface and intersect the clip across it again,
|
|
2863
|
+
// per fill, for pixels nothing reads. Stale content outside the box is
|
|
2864
|
+
// fine — every caller re-stamps the scratch unconditionally, so there is
|
|
2865
|
+
// no reuse to preserve.
|
|
2866
|
+
_compositeMask(box) {
|
|
2867
|
+
const clipMask =
|
|
2868
|
+
this._hasPolyClip || this._clipRect() ? this._requireClipMask() : null;
|
|
2451
2869
|
if (this.globalAlpha >= 1) return clipMask ? clipMask.id : 0;
|
|
2452
2870
|
this._ensureFillMask();
|
|
2871
|
+
const b = this._maskBox(box);
|
|
2872
|
+
if (b.w === 0 || b.h === 0) return this.fillMask.id; // nothing is sampled
|
|
2453
2873
|
this.Render.FillRectangles(
|
|
2454
2874
|
this.Render.PictOp.Src,
|
|
2455
2875
|
this.fillMask.id,
|
|
2456
2876
|
[0, 0, 0, this.globalAlpha],
|
|
2457
|
-
[
|
|
2877
|
+
[b.x, b.y, b.w, b.h],
|
|
2458
2878
|
);
|
|
2459
2879
|
if (clipMask) {
|
|
2460
2880
|
this.Render.Composite(
|
|
@@ -2462,14 +2882,14 @@ class RenderingContext2d {
|
|
|
2462
2882
|
clipMask.id,
|
|
2463
2883
|
0,
|
|
2464
2884
|
this.fillMask.id,
|
|
2885
|
+
b.x,
|
|
2886
|
+
b.y,
|
|
2465
2887
|
0,
|
|
2466
2888
|
0,
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
this.width,
|
|
2472
|
-
this.height,
|
|
2889
|
+
b.x,
|
|
2890
|
+
b.y,
|
|
2891
|
+
b.w,
|
|
2892
|
+
b.h,
|
|
2473
2893
|
);
|
|
2474
2894
|
}
|
|
2475
2895
|
return this.fillMask.id;
|
|
@@ -2486,10 +2906,10 @@ class RenderingContext2d {
|
|
|
2486
2906
|
// and what every caller predating ARGB windows expects.
|
|
2487
2907
|
clearRect(x, y, w, h) {
|
|
2488
2908
|
const alpha = this._hasAlpha;
|
|
2489
|
-
if (matIsIdentity(this._m) && !this.
|
|
2909
|
+
if (matIsIdentity(this._m) && !this._hasPolyClip && !this._clipRect()) {
|
|
2490
2910
|
this.Render.FillRectangles(
|
|
2491
2911
|
this.Render.PictOp.Src,
|
|
2492
|
-
this.
|
|
2912
|
+
this._dst(),
|
|
2493
2913
|
alpha ? [0, 0, 0, 0] : [1, 1, 1, 1],
|
|
2494
2914
|
[x, y, w, h],
|
|
2495
2915
|
);
|
|
@@ -2524,20 +2944,30 @@ class RenderingContext2d {
|
|
|
2524
2944
|
_fillRect(x, y, w, h) {
|
|
2525
2945
|
if (matIsIdentity(this._m)) {
|
|
2526
2946
|
if (!prepareStyle(this._backgroundPicture, this._m)) return;
|
|
2527
|
-
const
|
|
2947
|
+
const op = this._op();
|
|
2948
|
+
// A rectangular clip is a smaller rectangle to fill, not a mask — this
|
|
2949
|
+
// was the one drawing path that still built a surface-sized a8 for one
|
|
2950
|
+
// (issue #307), and a renderer filling a window background inside a
|
|
2951
|
+
// damage clip pays it every frame. Source, mask and destination
|
|
2952
|
+
// coordinates all shift with the box, so a gradient or a pattern stays
|
|
2953
|
+
// where it was and only the ink outside the clip goes away.
|
|
2954
|
+
const direct =
|
|
2955
|
+
w > 0 && h > 0 ? this._boxedComposite({ x, y, w, h }, op) : null;
|
|
2956
|
+
if (direct && !direct.box) return; // the clip rejects the fill whole
|
|
2957
|
+
const box = direct ? direct.box : { x, y, w, h };
|
|
2528
2958
|
this.Render.Composite(
|
|
2529
|
-
|
|
2959
|
+
op,
|
|
2530
2960
|
this._backgroundPicture.id,
|
|
2531
|
-
mask,
|
|
2532
|
-
this.
|
|
2533
|
-
x,
|
|
2534
|
-
y,
|
|
2535
|
-
x,
|
|
2536
|
-
y,
|
|
2537
|
-
x,
|
|
2538
|
-
y,
|
|
2539
|
-
w,
|
|
2540
|
-
h,
|
|
2961
|
+
direct ? direct.mask : this._compositeMask(box),
|
|
2962
|
+
this._dst(),
|
|
2963
|
+
box.x,
|
|
2964
|
+
box.y,
|
|
2965
|
+
box.x,
|
|
2966
|
+
box.y,
|
|
2967
|
+
box.x,
|
|
2968
|
+
box.y,
|
|
2969
|
+
box.w,
|
|
2970
|
+
box.h,
|
|
2541
2971
|
);
|
|
2542
2972
|
this._markDirty();
|
|
2543
2973
|
return;
|
|
@@ -2593,11 +3023,11 @@ class RenderingContext2d {
|
|
|
2593
3023
|
this._shadowOfPolys(flattenPath(rectPath._cmds, this._m), {});
|
|
2594
3024
|
}
|
|
2595
3025
|
|
|
2596
|
-
const clip = this.
|
|
3026
|
+
const clip = this._clipRect();
|
|
2597
3027
|
if (
|
|
2598
3028
|
!matIsIdentity(this._m) ||
|
|
2599
3029
|
!isPlainColor(this._fillStyle) ||
|
|
2600
|
-
|
|
3030
|
+
this._hasPolyClip
|
|
2601
3031
|
) {
|
|
2602
3032
|
for (let i = 0; i < flat.length; i += 4)
|
|
2603
3033
|
this._fillRect(flat[i], flat[i + 1], flat[i + 2], flat[i + 3]);
|
|
@@ -2605,14 +3035,7 @@ class RenderingContext2d {
|
|
|
2605
3035
|
}
|
|
2606
3036
|
if (clip && (clip.w === 0 || clip.h === 0)) return; // clipped away
|
|
2607
3037
|
const R = this.Render;
|
|
2608
|
-
if (clip)
|
|
2609
|
-
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
2610
|
-
clip.x,
|
|
2611
|
-
clip.y,
|
|
2612
|
-
clip.w,
|
|
2613
|
-
clip.h,
|
|
2614
|
-
]);
|
|
2615
|
-
}
|
|
3038
|
+
if (clip) this._setPictureClip(clip);
|
|
2616
3039
|
// globalAlpha folds into the premultiplied colour — scaling all four
|
|
2617
3040
|
// components is exactly what compositing at that opacity means, for
|
|
2618
3041
|
// any composite op (the mask slot would only scale the source the same
|
|
@@ -2623,12 +3046,12 @@ class RenderingContext2d {
|
|
|
2623
3046
|
for (let i = 0; i < flat.length; i += chunk) {
|
|
2624
3047
|
R.FillRectangles(
|
|
2625
3048
|
this._op(),
|
|
2626
|
-
this.
|
|
3049
|
+
this._dst(),
|
|
2627
3050
|
color,
|
|
2628
3051
|
i === 0 && flat.length <= chunk ? flat : flat.slice(i, i + chunk),
|
|
2629
3052
|
);
|
|
2630
3053
|
}
|
|
2631
|
-
if (clip) this.
|
|
3054
|
+
if (clip) this._invalidatePictureClip();
|
|
2632
3055
|
this._markDirty();
|
|
2633
3056
|
}
|
|
2634
3057
|
|
|
@@ -2741,8 +3164,8 @@ class RenderingContext2d {
|
|
|
2741
3164
|
// change what it covers — the rule argument needs no inspection)
|
|
2742
3165
|
if (!isPlainColor(this._fillStyle)) return this._shapeMiss("gradient");
|
|
2743
3166
|
if (this._gco !== "source-over") return this._shapeMiss("composite-op");
|
|
2744
|
-
|
|
2745
|
-
|
|
3167
|
+
if (this._hasPolyClip) return this._shapeMiss("clip-mask");
|
|
3168
|
+
const clip = this._clipRect();
|
|
2746
3169
|
|
|
2747
3170
|
const { x, y, w, h, radii } = box;
|
|
2748
3171
|
if (
|
|
@@ -2848,8 +3271,8 @@ class RenderingContext2d {
|
|
|
2848
3271
|
if (!isPlainColor(this._strokeStyle)) return this._shapeMiss("gradient");
|
|
2849
3272
|
if (this._gco !== "source-over") return this._shapeMiss("composite-op");
|
|
2850
3273
|
if (this._lineDash.length) return this._shapeMiss("dashes");
|
|
2851
|
-
|
|
2852
|
-
|
|
3274
|
+
if (this._hasPolyClip) return this._shapeMiss("clip-mask");
|
|
3275
|
+
const clip = this._clipRect();
|
|
2853
3276
|
|
|
2854
3277
|
const bw = this.lineWidth;
|
|
2855
3278
|
if (!Number.isFinite(bw) || bw <= 0) return this._shapeMiss("geometry");
|
|
@@ -2979,22 +3402,16 @@ class RenderingContext2d {
|
|
|
2979
3402
|
if (clip && (clip.w === 0 || clip.h === 0)) return; // clipped away
|
|
2980
3403
|
const R = this.Render;
|
|
2981
3404
|
const app = this.window.app;
|
|
2982
|
-
if (clip)
|
|
2983
|
-
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
2984
|
-
clip.x,
|
|
2985
|
-
clip.y,
|
|
2986
|
-
clip.w,
|
|
2987
|
-
clip.h,
|
|
2988
|
-
]);
|
|
2989
|
-
}
|
|
3405
|
+
if (clip) this._setPictureClip(clip);
|
|
2990
3406
|
if (corners.length) {
|
|
2991
3407
|
const page = getShapeGlyphPage(app);
|
|
2992
3408
|
page.ensure(corners);
|
|
2993
3409
|
const items = [];
|
|
2994
3410
|
for (const spec of corners) {
|
|
3411
|
+
const e = page.entry(spec.key);
|
|
2995
3412
|
items.push({
|
|
2996
|
-
gs:
|
|
2997
|
-
lid:
|
|
3413
|
+
gs: e.gs,
|
|
3414
|
+
lid: e.lid,
|
|
2998
3415
|
adv: 0,
|
|
2999
3416
|
x: spec.px,
|
|
3000
3417
|
y: spec.py,
|
|
@@ -3006,7 +3423,7 @@ class RenderingContext2d {
|
|
|
3006
3423
|
encoded.bits,
|
|
3007
3424
|
R.PictOp.Over,
|
|
3008
3425
|
src.id,
|
|
3009
|
-
this.
|
|
3426
|
+
this._dst(),
|
|
3010
3427
|
0,
|
|
3011
3428
|
encoded.gsid,
|
|
3012
3429
|
items[0].x,
|
|
@@ -3016,9 +3433,9 @@ class RenderingContext2d {
|
|
|
3016
3433
|
trimShapeGlyphs(app);
|
|
3017
3434
|
}
|
|
3018
3435
|
if (rects.length) {
|
|
3019
|
-
R.FillRectangles(R.PictOp.Over, this.
|
|
3436
|
+
R.FillRectangles(R.PictOp.Over, this._dst(), color, rects);
|
|
3020
3437
|
}
|
|
3021
|
-
if (clip) this.
|
|
3438
|
+
if (clip) this._invalidatePictureClip();
|
|
3022
3439
|
this._markDirty();
|
|
3023
3440
|
}
|
|
3024
3441
|
|
|
@@ -3053,8 +3470,7 @@ class RenderingContext2d {
|
|
|
3053
3470
|
// be honoured by rounding — which would differ from the mask path's
|
|
3054
3471
|
// antialiased edge by up to a pixel. Rare, so leave those to the mask
|
|
3055
3472
|
// rather than quietly changing what they look like.
|
|
3056
|
-
|
|
3057
|
-
if (![x, y, w, h].every(integral)) return null;
|
|
3473
|
+
if (![x, y, w, h].every(isIntegral)) return null;
|
|
3058
3474
|
return {
|
|
3059
3475
|
x: Math.round(x),
|
|
3060
3476
|
y: Math.round(y),
|
|
@@ -3063,14 +3479,21 @@ class RenderingContext2d {
|
|
|
3063
3479
|
};
|
|
3064
3480
|
}
|
|
3065
3481
|
|
|
3066
|
-
/**
|
|
3067
|
-
*
|
|
3482
|
+
/**
|
|
3483
|
+
* Intersection of the rectangular part of the clip stack, or null when
|
|
3484
|
+
* there is none — either because the stack is empty or because something
|
|
3485
|
+
* in it is not a rectangle, and then the a8 mask owns the whole stack.
|
|
3486
|
+
*
|
|
3487
|
+
* Region entries are transparent here. They are not rectangles and never
|
|
3488
|
+
* become one: the picture carries them, which is a different slot from
|
|
3489
|
+
* this and composes with it (see _setPictureClip).
|
|
3490
|
+
*/
|
|
3068
3491
|
_clipRect() {
|
|
3069
|
-
if (
|
|
3492
|
+
if (this._hasPolyClip) return null;
|
|
3070
3493
|
let out = null;
|
|
3071
3494
|
for (const entry of this._clips) {
|
|
3072
3495
|
const r = entry.rect;
|
|
3073
|
-
if (!r)
|
|
3496
|
+
if (!r) continue; // a region entry
|
|
3074
3497
|
if (!out) {
|
|
3075
3498
|
out = { ...r };
|
|
3076
3499
|
continue;
|
|
@@ -3081,6 +3504,7 @@ class RenderingContext2d {
|
|
|
3081
3504
|
const bottom = Math.min(out.y + out.h, r.y + r.h);
|
|
3082
3505
|
out = { x, y, w: Math.max(0, right - x), h: Math.max(0, bottom - y) };
|
|
3083
3506
|
}
|
|
3507
|
+
if (!out) return null;
|
|
3084
3508
|
// clamp into the surface: the server rejects out-of-range rectangles
|
|
3085
3509
|
const x = Math.max(0, Math.min(out.x, this.width));
|
|
3086
3510
|
const y = Math.max(0, Math.min(out.y, this.height));
|
|
@@ -3089,11 +3513,31 @@ class RenderingContext2d {
|
|
|
3089
3513
|
return { x, y, w, h };
|
|
3090
3514
|
}
|
|
3091
3515
|
|
|
3516
|
+
/**
|
|
3517
|
+
* Replace the clip stack and re-derive what kinds of entry are in it.
|
|
3518
|
+
*
|
|
3519
|
+
* The two flags are what the drawing paths branch on, so they are computed
|
|
3520
|
+
* once per change rather than scanned per drawing — and the stack is only
|
|
3521
|
+
* ever replaced, never appended to in place, because save() hands the same
|
|
3522
|
+
* array to its snapshot.
|
|
3523
|
+
*/
|
|
3524
|
+
_setClips(next) {
|
|
3525
|
+
this._clips = next;
|
|
3526
|
+
let poly = false;
|
|
3527
|
+
let region = false;
|
|
3528
|
+
for (const entry of next) {
|
|
3529
|
+
if (entry.region) region = true;
|
|
3530
|
+
else if (!entry.rect) poly = true;
|
|
3531
|
+
}
|
|
3532
|
+
this._hasPolyClip = poly;
|
|
3533
|
+
this._hasRegionClip = region;
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3092
3536
|
clip(...args) {
|
|
3093
3537
|
const { polys, rule } = this._polysFor(args);
|
|
3094
3538
|
const entry = { polys, rule, rect: RenderingContext2d._rectOfPolys(polys) };
|
|
3095
3539
|
// copy-on-write: earlier save() snapshots keep their own clip list
|
|
3096
|
-
this.
|
|
3540
|
+
this._setClips(this._clips.concat([entry]));
|
|
3097
3541
|
|
|
3098
3542
|
// A stack of rectangles stays virtual: consumers apply the intersected
|
|
3099
3543
|
// rectangle server-side (SetPictureClipRectangles, bounded composites),
|
|
@@ -3110,6 +3554,36 @@ class RenderingContext2d {
|
|
|
3110
3554
|
this._intersectClip(entry);
|
|
3111
3555
|
}
|
|
3112
3556
|
|
|
3557
|
+
/**
|
|
3558
|
+
* Intersect the clip with a server-side XFIXES region — a rectangle set the
|
|
3559
|
+
* X server owns, which is how X describes damage from an expose, a window's
|
|
3560
|
+
* SHAPE, or the area a compositor has left after subtracting the windows in
|
|
3561
|
+
* front of this one. `app.createRegion(rects)` makes one (lib/region.js).
|
|
3562
|
+
*
|
|
3563
|
+
* Scoped like `clip()`: `restore()` takes it off again, and nothing else
|
|
3564
|
+
* does. It intersects with rectangular and path clips in either order.
|
|
3565
|
+
*
|
|
3566
|
+
* The region is in **device pixels** and ignores the current transform —
|
|
3567
|
+
* unlike `clip()`, whose path goes through it. A region is a set of integer
|
|
3568
|
+
* rectangles, so there is no honest way to rotate or scale one; a caller who
|
|
3569
|
+
* wants it moved can `region.translate(dx, dy)`, which the server does.
|
|
3570
|
+
*
|
|
3571
|
+
* Unlike installing the region on `ctx.picture` yourself, this is a clip ntk
|
|
3572
|
+
* knows about: the fast paths that narrow the picture to a rectangle around
|
|
3573
|
+
* a glyph run or a fill restore *to* it afterwards, instead of overwriting
|
|
3574
|
+
* it with a full-plane rectangle (issue #292).
|
|
3575
|
+
*
|
|
3576
|
+
* @param {Region|number|{id: number}} region
|
|
3577
|
+
*/
|
|
3578
|
+
clipRegion(region) {
|
|
3579
|
+
const id = regionId(region);
|
|
3580
|
+
const fixes = this._fixes || this.window.app._fixes;
|
|
3581
|
+
if (!fixes) throw needXFixesError();
|
|
3582
|
+
this._fixes = fixes;
|
|
3583
|
+
this._setClips(this._clips.concat([{ region: id }]));
|
|
3584
|
+
this._invalidatePictureClip();
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3113
3587
|
/**
|
|
3114
3588
|
* Build the a8 mask from the whole clip stack, on first demand. Rect
|
|
3115
3589
|
* entries are plain fills; only genuinely non-rectangular entries
|
|
@@ -3125,7 +3599,18 @@ class RenderingContext2d {
|
|
|
3125
3599
|
[0, 0, 0, 0],
|
|
3126
3600
|
[0, 0, this.width, this.height],
|
|
3127
3601
|
);
|
|
3128
|
-
|
|
3602
|
+
// region entries are the picture's clip, not the mask's business
|
|
3603
|
+
const shapes = this._clips.filter((entry) => !entry.region);
|
|
3604
|
+
if (!shapes.length) {
|
|
3605
|
+
R.FillRectangles(
|
|
3606
|
+
R.PictOp.Src,
|
|
3607
|
+
this.clipMask.id,
|
|
3608
|
+
[0, 0, 0, 1],
|
|
3609
|
+
[0, 0, this.width, this.height],
|
|
3610
|
+
);
|
|
3611
|
+
return;
|
|
3612
|
+
}
|
|
3613
|
+
const first = shapes[0];
|
|
3129
3614
|
if (first.rect) {
|
|
3130
3615
|
const r = first.rect;
|
|
3131
3616
|
const x = Math.max(0, Math.min(r.x, this.width));
|
|
@@ -3143,8 +3628,7 @@ class RenderingContext2d {
|
|
|
3143
3628
|
} else {
|
|
3144
3629
|
this._applyPolyClip(first, R.PictOp.Src);
|
|
3145
3630
|
}
|
|
3146
|
-
for (let i = 1; i <
|
|
3147
|
-
this._intersectClip(this._clips[i]);
|
|
3631
|
+
for (let i = 1; i < shapes.length; i++) this._intersectClip(shapes[i]);
|
|
3148
3632
|
}
|
|
3149
3633
|
|
|
3150
3634
|
/** The live mask, built if the stack has not needed one yet. Callers on
|
|
@@ -3852,28 +4336,27 @@ class RenderingContext2d {
|
|
|
3852
4336
|
* Uniform alpha needs no surface-sized mask either: a 1x1 repeating
|
|
3853
4337
|
* picture is the same thing to the server, with no pixel work at all.
|
|
3854
4338
|
*/
|
|
3855
|
-
_beginDirectComposite() {
|
|
3856
|
-
const rect = this.
|
|
3857
|
-
if (!rect)
|
|
3858
|
-
|
|
4339
|
+
_beginDirectComposite(box) {
|
|
4340
|
+
const rect = this._clipRect();
|
|
4341
|
+
if (!rect) {
|
|
4342
|
+
// a poly clip is the only thing left that needs a mask; without one
|
|
4343
|
+
// there is nothing to apply here but the alpha. The mask it does need
|
|
4344
|
+
// is bounded to `box`, the destination box of the composite it is for
|
|
4345
|
+
// — the only pixels of it the server samples (issue #313).
|
|
4346
|
+
const mask = this._hasPolyClip
|
|
4347
|
+
? this._compositeMask(box)
|
|
4348
|
+
: this._alphaMask();
|
|
4349
|
+
return { mask, clipped: false, empty: false };
|
|
4350
|
+
}
|
|
3859
4351
|
if (rect.w === 0 || rect.h === 0)
|
|
3860
4352
|
return { mask: 0, clipped: false, empty: true };
|
|
3861
|
-
this.
|
|
3862
|
-
|
|
3863
|
-
rect.y,
|
|
3864
|
-
rect.w,
|
|
3865
|
-
rect.h,
|
|
3866
|
-
]);
|
|
3867
|
-
const mask =
|
|
3868
|
-
this.globalAlpha >= 1
|
|
3869
|
-
? 0
|
|
3870
|
-
: this.createSolidPicture(0, 0, 0, this.globalAlpha).id;
|
|
3871
|
-
return { mask, clipped: true, empty: false };
|
|
4353
|
+
this._setPictureClip(rect);
|
|
4354
|
+
return { mask: this._alphaMask(), clipped: true, empty: false };
|
|
3872
4355
|
}
|
|
3873
4356
|
|
|
3874
4357
|
_endDirectComposite(state) {
|
|
3875
4358
|
if (!state.clipped) return;
|
|
3876
|
-
this.
|
|
4359
|
+
this._invalidatePictureClip();
|
|
3877
4360
|
}
|
|
3878
4361
|
|
|
3879
4362
|
/** The source colour for a coverage composite, with `globalAlpha` folded
|
|
@@ -3923,12 +4406,12 @@ class RenderingContext2d {
|
|
|
3923
4406
|
const mx = scaled ? 0 : sx;
|
|
3924
4407
|
const my = scaled ? 0 : sy;
|
|
3925
4408
|
|
|
3926
|
-
const rect = this.
|
|
4409
|
+
const rect = this._clipRect();
|
|
3927
4410
|
// the mask slot can hold exactly one picture, so anything that cannot be
|
|
3928
4411
|
// folded into the colour or handed to the server as a clip rectangle has
|
|
3929
4412
|
// to be intersected into the scratch mask first
|
|
3930
4413
|
const scratch =
|
|
3931
|
-
|
|
4414
|
+
this._hasPolyClip ||
|
|
3932
4415
|
(this.globalAlpha < 1 && !isPlainColor(this._fillStyle));
|
|
3933
4416
|
|
|
3934
4417
|
if (scratch) {
|
|
@@ -3969,7 +4452,7 @@ class RenderingContext2d {
|
|
|
3969
4452
|
this.height,
|
|
3970
4453
|
);
|
|
3971
4454
|
}
|
|
3972
|
-
if (this.
|
|
4455
|
+
if (this._hasPolyClip) {
|
|
3973
4456
|
const clipMask = this._requireClipMask();
|
|
3974
4457
|
R.Composite(
|
|
3975
4458
|
R.PictOp.In,
|
|
@@ -3988,12 +4471,7 @@ class RenderingContext2d {
|
|
|
3988
4471
|
} else if (rect) {
|
|
3989
4472
|
// scratch was forced by alpha-over-gradient, not by the clip: the
|
|
3990
4473
|
// rectangle still applies, server-side, around the final composite
|
|
3991
|
-
|
|
3992
|
-
rect.x,
|
|
3993
|
-
rect.y,
|
|
3994
|
-
rect.w,
|
|
3995
|
-
rect.h,
|
|
3996
|
-
]);
|
|
4474
|
+
this._setPictureClip(rect);
|
|
3997
4475
|
}
|
|
3998
4476
|
// The scratch mask is surface-sized and surface-aligned — the coverage
|
|
3999
4477
|
// went into it at (dx, dy) — and so is a gradient source, so both are
|
|
@@ -4005,7 +4483,7 @@ class RenderingContext2d {
|
|
|
4005
4483
|
op,
|
|
4006
4484
|
this._backgroundPicture.id,
|
|
4007
4485
|
this.fillMask.id,
|
|
4008
|
-
this.
|
|
4486
|
+
this._dst(),
|
|
4009
4487
|
dx,
|
|
4010
4488
|
dy,
|
|
4011
4489
|
dx,
|
|
@@ -4015,23 +4493,9 @@ class RenderingContext2d {
|
|
|
4015
4493
|
dw,
|
|
4016
4494
|
dh,
|
|
4017
4495
|
);
|
|
4018
|
-
if (rect)
|
|
4019
|
-
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
4020
|
-
0,
|
|
4021
|
-
0,
|
|
4022
|
-
this.width,
|
|
4023
|
-
this.height,
|
|
4024
|
-
]);
|
|
4025
|
-
}
|
|
4496
|
+
if (rect) this._invalidatePictureClip();
|
|
4026
4497
|
} else if (!rect || (rect.w > 0 && rect.h > 0)) {
|
|
4027
|
-
if (rect)
|
|
4028
|
-
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
4029
|
-
rect.x,
|
|
4030
|
-
rect.y,
|
|
4031
|
-
rect.w,
|
|
4032
|
-
rect.h,
|
|
4033
|
-
]);
|
|
4034
|
-
}
|
|
4498
|
+
if (rect) this._setPictureClip(rect);
|
|
4035
4499
|
// the source is a 1x1 repeating solid (offset irrelevant) or the fill
|
|
4036
4500
|
// picture, which for a gradient is surface-aligned: same offset as the
|
|
4037
4501
|
// destination, exactly as in _fillPolys
|
|
@@ -4039,7 +4503,7 @@ class RenderingContext2d {
|
|
|
4039
4503
|
op,
|
|
4040
4504
|
this._coverageSource().id,
|
|
4041
4505
|
picture.id,
|
|
4042
|
-
this.
|
|
4506
|
+
this._dst(),
|
|
4043
4507
|
dx,
|
|
4044
4508
|
dy,
|
|
4045
4509
|
mx,
|
|
@@ -4049,9 +4513,7 @@ class RenderingContext2d {
|
|
|
4049
4513
|
dw,
|
|
4050
4514
|
dh,
|
|
4051
4515
|
);
|
|
4052
|
-
if (rect)
|
|
4053
|
-
this._resetPictureClip();
|
|
4054
|
-
}
|
|
4516
|
+
if (rect) this._invalidatePictureClip();
|
|
4055
4517
|
}
|
|
4056
4518
|
|
|
4057
4519
|
if (scaled) {
|
|
@@ -4097,7 +4559,7 @@ class RenderingContext2d {
|
|
|
4097
4559
|
return;
|
|
4098
4560
|
}
|
|
4099
4561
|
|
|
4100
|
-
const state = this._beginDirectComposite();
|
|
4562
|
+
const state = this._beginDirectComposite({ x: dx, y: dy, w: dw, h: dh });
|
|
4101
4563
|
if (state.empty) return;
|
|
4102
4564
|
const mask = state.mask;
|
|
4103
4565
|
const scaled = dw !== sw || dh !== sh;
|
|
@@ -4120,7 +4582,7 @@ class RenderingContext2d {
|
|
|
4120
4582
|
op,
|
|
4121
4583
|
picture.id,
|
|
4122
4584
|
mask,
|
|
4123
|
-
this.
|
|
4585
|
+
this._dst(),
|
|
4124
4586
|
0,
|
|
4125
4587
|
0,
|
|
4126
4588
|
dx,
|
|
@@ -4141,7 +4603,7 @@ class RenderingContext2d {
|
|
|
4141
4603
|
op,
|
|
4142
4604
|
picture.id,
|
|
4143
4605
|
mask,
|
|
4144
|
-
this.
|
|
4606
|
+
this._dst(),
|
|
4145
4607
|
sx,
|
|
4146
4608
|
sy,
|
|
4147
4609
|
dx,
|
|
@@ -4167,19 +4629,25 @@ class RenderingContext2d {
|
|
|
4167
4629
|
if (image instanceof RenderingContext2d) {
|
|
4168
4630
|
// TODO: if need to scale, set transform
|
|
4169
4631
|
// TODO: respect global compositing blend mode
|
|
4632
|
+
const full = { x: 0, y: 0, w: this.width, h: this.height };
|
|
4633
|
+
// source and destination are both surface-aligned here, so a clip
|
|
4634
|
+
// rectangle is just a smaller box to copy (issue #307)
|
|
4635
|
+
const direct = this._boxedComposite(full, this.Render.PictOp.Over);
|
|
4636
|
+
if (direct && !direct.box) return;
|
|
4637
|
+
const box = direct ? direct.box : full;
|
|
4170
4638
|
this.Render.Composite(
|
|
4171
4639
|
this.Render.PictOp.Over,
|
|
4172
4640
|
image.picture.id,
|
|
4173
|
-
this._compositeMask(),
|
|
4174
|
-
this.
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4641
|
+
direct ? direct.mask : this._compositeMask(box),
|
|
4642
|
+
this._dst(),
|
|
4643
|
+
box.x,
|
|
4644
|
+
box.y,
|
|
4645
|
+
box.x,
|
|
4646
|
+
box.y,
|
|
4647
|
+
box.x,
|
|
4648
|
+
box.y,
|
|
4649
|
+
box.w,
|
|
4650
|
+
box.h,
|
|
4183
4651
|
);
|
|
4184
4652
|
this._markDirty();
|
|
4185
4653
|
} else if (
|
|
@@ -4188,6 +4656,13 @@ class RenderingContext2d {
|
|
|
4188
4656
|
typeof image.context.getImageData === "function"
|
|
4189
4657
|
) {
|
|
4190
4658
|
// node-canvas Canvas ( or any ctx with ctx.canvas.getImageData returning pixels)
|
|
4659
|
+
// What the clip leaves of the composite, decided before anything is
|
|
4660
|
+
// uploaded: a clip that rejects it whole makes the upload below dead
|
|
4661
|
+
// work (issue #307)
|
|
4662
|
+
const full = { x: 0, y: 0, w: this.width, h: this.height };
|
|
4663
|
+
const direct = this._boxedComposite(full, this.Render.PictOp.Over);
|
|
4664
|
+
if (direct && !direct.box) return;
|
|
4665
|
+
const box = direct ? direct.box : full;
|
|
4191
4666
|
const imageData = image.context.getImageData(sx, sy, sWidth, sHeight);
|
|
4192
4667
|
// node-canvas hands over straight RGBA, and the rgba32 picture below is
|
|
4193
4668
|
// premultiplied — this used to swap the channels and stop there, which
|
|
@@ -4227,16 +4702,16 @@ class RenderingContext2d {
|
|
|
4227
4702
|
this.Render.Composite(
|
|
4228
4703
|
this.Render.PictOp.Over,
|
|
4229
4704
|
picture.id,
|
|
4230
|
-
this._compositeMask(),
|
|
4231
|
-
this.
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4705
|
+
direct ? direct.mask : this._compositeMask(box),
|
|
4706
|
+
this._dst(),
|
|
4707
|
+
box.x,
|
|
4708
|
+
box.y,
|
|
4709
|
+
box.x,
|
|
4710
|
+
box.y,
|
|
4711
|
+
box.x,
|
|
4712
|
+
box.y,
|
|
4713
|
+
box.w,
|
|
4714
|
+
box.h,
|
|
4240
4715
|
);
|
|
4241
4716
|
this._markDirty();
|
|
4242
4717
|
} finally {
|
|
@@ -4275,6 +4750,14 @@ class RenderingContext2d {
|
|
|
4275
4750
|
);
|
|
4276
4751
|
if (x1 <= x0 || y1 <= y0) return;
|
|
4277
4752
|
|
|
4753
|
+
// A rectangular clip shrinks the box instead of becoming a mask (issue
|
|
4754
|
+
// #307). Decided before the picture transform is installed, so a draw
|
|
4755
|
+
// the clip rejects does not have to put it back.
|
|
4756
|
+
const bbox = { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
4757
|
+
const direct = this._boxedComposite(bbox, op);
|
|
4758
|
+
if (direct && !direct.box) return;
|
|
4759
|
+
const box = direct ? direct.box : bbox;
|
|
4760
|
+
|
|
4278
4761
|
// device -> source sample (adding the source-rect origin)
|
|
4279
4762
|
const srcM = [inv[0], inv[1], inv[2], inv[3], inv[4] + sx, inv[5] + sy];
|
|
4280
4763
|
this.Render.SetPictureTransform(picture.id, [
|
|
@@ -4289,20 +4772,19 @@ class RenderingContext2d {
|
|
|
4289
4772
|
1,
|
|
4290
4773
|
]);
|
|
4291
4774
|
picture.setFilter("bilinear");
|
|
4292
|
-
const mask = this._compositeMask();
|
|
4293
4775
|
this.Render.Composite(
|
|
4294
4776
|
op,
|
|
4295
4777
|
picture.id,
|
|
4296
|
-
mask,
|
|
4297
|
-
this.
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4778
|
+
direct ? direct.mask : this._compositeMask(box),
|
|
4779
|
+
this._dst(),
|
|
4780
|
+
box.x,
|
|
4781
|
+
box.y,
|
|
4782
|
+
box.x,
|
|
4783
|
+
box.y,
|
|
4784
|
+
box.x,
|
|
4785
|
+
box.y,
|
|
4786
|
+
box.w,
|
|
4787
|
+
box.h,
|
|
4306
4788
|
);
|
|
4307
4789
|
this.Render.SetPictureTransform(picture.id, [1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
4308
4790
|
picture.setFilter("nearest");
|