ntk 8.1.1 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/app.js +339 -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 +830 -235
- package/lib/shapeglyphs.js +94 -26
- package/lib/sharedglyphs.js +433 -0
- package/lib/text/glyphs.js +204 -26
- package/lib/widgets/svgview.js +7 -5
- package/lib/window.js +218 -28
- 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,
|
|
@@ -52,6 +54,8 @@ import {
|
|
|
52
54
|
compositeTraps,
|
|
53
55
|
drawGlyphRuns,
|
|
54
56
|
encodeGlyphItems,
|
|
57
|
+
positionedRunsInk,
|
|
58
|
+
runId,
|
|
55
59
|
} from "./text/glyphs.js";
|
|
56
60
|
import { TextLayout } from "./text/layout.js";
|
|
57
61
|
import { reorderRuns } from "./text/shape.js";
|
|
@@ -73,6 +77,11 @@ function fillableShapes(polys) {
|
|
|
73
77
|
return { shapes, edges };
|
|
74
78
|
}
|
|
75
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
|
+
|
|
76
85
|
/** The overlap of two {x, y, w, h} boxes, or null when they have none. */
|
|
77
86
|
function intersectBox(a, b) {
|
|
78
87
|
const x = Math.max(a.x, b.x);
|
|
@@ -150,13 +159,17 @@ function variationsKey(variations) {
|
|
|
150
159
|
* context owns is a Pixmap or a Picture, which carry their own finalizers,
|
|
151
160
|
* and reaching into them from this one would race those.
|
|
152
161
|
*/
|
|
153
|
-
const gcRegistry = new FinalizationRegistry(({ X, gcs }) => {
|
|
162
|
+
const gcRegistry = new FinalizationRegistry(({ X, gcs, regions }) => {
|
|
154
163
|
safeRelease(X, () => {
|
|
155
164
|
for (const gc of gcs) {
|
|
156
165
|
if (!gc) continue;
|
|
157
166
|
X.FreeGC(gc);
|
|
158
167
|
X.ReleaseID(gc);
|
|
159
168
|
}
|
|
169
|
+
for (const { fixes, id } of regions) {
|
|
170
|
+
fixes.DestroyRegion(id);
|
|
171
|
+
X.ReleaseID(id);
|
|
172
|
+
}
|
|
160
173
|
});
|
|
161
174
|
});
|
|
162
175
|
|
|
@@ -189,6 +202,63 @@ const REPEAT_MODES = {
|
|
|
189
202
|
const PATTERN_DOCS =
|
|
190
203
|
"https://github.com/sidorares/ntk/blob/master/docs/context-2d.md#patterns";
|
|
191
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
|
+
|
|
192
262
|
/**
|
|
193
263
|
* What a pattern tiles: a drawable holding the tile plus the picture format
|
|
194
264
|
* to read it through. A repeating source Picture is created over it rather
|
|
@@ -197,13 +267,12 @@ const PATTERN_DOCS =
|
|
|
197
267
|
*/
|
|
198
268
|
function patternSourceOf(app, source) {
|
|
199
269
|
const Render = app.display.Render;
|
|
200
|
-
const formatFor = (depth) =>
|
|
201
|
-
depth === 32 ? Render.rgba32 : depth === 8 ? "a8" : Render.rgb24;
|
|
202
270
|
|
|
203
271
|
let drawable = null;
|
|
204
272
|
let width;
|
|
205
273
|
let height;
|
|
206
274
|
let format;
|
|
275
|
+
let coverage = false; // an a8 tile: no colour to paint with (see below)
|
|
207
276
|
if (source instanceof Surface) {
|
|
208
277
|
if (source.app !== app) {
|
|
209
278
|
throw new Error(
|
|
@@ -211,7 +280,8 @@ function patternSourceOf(app, source) {
|
|
|
211
280
|
);
|
|
212
281
|
}
|
|
213
282
|
drawable = source.pixmap;
|
|
214
|
-
|
|
283
|
+
coverage = source.format === "a8";
|
|
284
|
+
format = coverage ? Render.a8 : Render.rgba32;
|
|
215
285
|
({ width, height } = source);
|
|
216
286
|
} else if (source instanceof Image) {
|
|
217
287
|
drawable = source.pixmap(app);
|
|
@@ -222,12 +292,12 @@ function patternSourceOf(app, source) {
|
|
|
222
292
|
// is where a double-buffered window's current pixels actually are)
|
|
223
293
|
drawable = source._backing || source;
|
|
224
294
|
const depth = drawable.depth ?? source.depth;
|
|
225
|
-
if (!depth)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
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);
|
|
231
301
|
width = drawable.width ?? source.width;
|
|
232
302
|
height = drawable.height ?? source.height;
|
|
233
303
|
} else {
|
|
@@ -236,7 +306,7 @@ function patternSourceOf(app, source) {
|
|
|
236
306
|
(source === null ? "null" : typeof source),
|
|
237
307
|
);
|
|
238
308
|
}
|
|
239
|
-
if (
|
|
309
|
+
if (coverage) {
|
|
240
310
|
throw new Error(
|
|
241
311
|
"createPattern: a coverage (a8) tile has no colour to paint with — XRender would " +
|
|
242
312
|
"sample it as black. Draw the tile into an argb32 Surface and tile that, or keep " +
|
|
@@ -422,7 +492,10 @@ class RenderingContext2d {
|
|
|
422
492
|
// ids land here as they are allocated; the finalizer holds this array
|
|
423
493
|
// rather than the context, so registering cannot keep the context alive
|
|
424
494
|
this._gcs = [];
|
|
425
|
-
|
|
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);
|
|
426
499
|
|
|
427
500
|
this.window = window;
|
|
428
501
|
this.display = window.app.display;
|
|
@@ -433,17 +506,40 @@ class RenderingContext2d {
|
|
|
433
506
|
// buffering); re-bind when the backing pixmap is reallocated on resize
|
|
434
507
|
this._target = null;
|
|
435
508
|
this._gc = null;
|
|
436
|
-
this.
|
|
509
|
+
this._picture = null;
|
|
437
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
|
+
}
|
|
438
534
|
if (typeof window.on === "function") {
|
|
439
535
|
window.on("_backing", () => this._bindTarget());
|
|
440
536
|
// masks are sized to the drawable — recreate them after a resize
|
|
441
537
|
window.on("resize", () => this._dropMasks());
|
|
442
538
|
// window-backed pictures are freed server-side with the window
|
|
443
539
|
window.on("_destroyed", () => {
|
|
444
|
-
if (this.
|
|
445
|
-
this.
|
|
446
|
-
this.
|
|
540
|
+
if (this._picture && this._target === this.window) {
|
|
541
|
+
this._picture.forget();
|
|
542
|
+
this._picture = null;
|
|
447
543
|
}
|
|
448
544
|
});
|
|
449
545
|
}
|
|
@@ -470,7 +566,26 @@ class RenderingContext2d {
|
|
|
470
566
|
// drawings that took more than one.
|
|
471
567
|
this.maskStats = { masks: 0, pixels: 0, split: 0 };
|
|
472
568
|
this._stack = [];
|
|
473
|
-
|
|
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
|
|
474
589
|
this._gco = "source-over";
|
|
475
590
|
this.globalAlpha = 1;
|
|
476
591
|
this.lineCap = "butt";
|
|
@@ -493,9 +608,46 @@ class RenderingContext2d {
|
|
|
493
608
|
this.lineWidth = 1;
|
|
494
609
|
}
|
|
495
610
|
|
|
496
|
-
|
|
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) {
|
|
497
649
|
const target = this.window._backing || this.window;
|
|
498
|
-
if (this._target === target) return;
|
|
650
|
+
if (this._target === target && !force) return;
|
|
499
651
|
this._target = target;
|
|
500
652
|
this._layoutCache = null; // the new target may be a different depth
|
|
501
653
|
|
|
@@ -506,36 +658,210 @@ class RenderingContext2d {
|
|
|
506
658
|
this._gcs.push(this._gc);
|
|
507
659
|
this.X.CreateGC(this._gc, target.id);
|
|
508
660
|
}
|
|
509
|
-
if (this.
|
|
661
|
+
if (this._picture) this._picture.destroy();
|
|
510
662
|
// a8 targets are coverage, not colour: drawing into one and using the
|
|
511
663
|
// result as a mask is how a monochrome drawing gets rendered once and
|
|
512
664
|
// recoloured on every use (see Surface).
|
|
513
|
-
const depth
|
|
514
|
-
const format =
|
|
515
|
-
depth === 32
|
|
516
|
-
? this.Render.rgba32
|
|
517
|
-
: depth === 8
|
|
518
|
-
? this.Render.a8
|
|
519
|
-
: this.Render.rgb24;
|
|
665
|
+
const { depth, visual } = this._targetPixels();
|
|
666
|
+
const format = this._formatFor(depth, visual);
|
|
520
667
|
// Does the target have a real alpha channel? Only then is "transparent"
|
|
521
668
|
// a colour it can hold, which is what clearRect turns on.
|
|
522
669
|
this._hasAlpha = depth === 32;
|
|
523
|
-
this.
|
|
670
|
+
this._picture = new Picture(this.window.app, {
|
|
524
671
|
drawable: target,
|
|
525
672
|
format,
|
|
526
673
|
polyEdge: 1,
|
|
527
674
|
polyMode: 1,
|
|
528
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();
|
|
529
681
|
this._dropMasks();
|
|
530
682
|
}
|
|
531
683
|
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
|
|
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;
|
|
539
865
|
}
|
|
540
866
|
|
|
541
867
|
/**
|
|
@@ -559,8 +885,16 @@ class RenderingContext2d {
|
|
|
559
885
|
this._destroyed = true;
|
|
560
886
|
gcRegistry.unregister(this);
|
|
561
887
|
|
|
562
|
-
this.
|
|
888
|
+
this._setClips([]);
|
|
563
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;
|
|
564
898
|
for (const gc of [this._gc, this._fillMaskGC, this._clipMaskGC]) {
|
|
565
899
|
if (!gc) continue;
|
|
566
900
|
safeRelease(this.X, () => {
|
|
@@ -573,15 +907,15 @@ class RenderingContext2d {
|
|
|
573
907
|
|
|
574
908
|
this._backgroundPicture = this._glyphSource = null;
|
|
575
909
|
|
|
576
|
-
if (this.
|
|
910
|
+
if (this._picture) {
|
|
577
911
|
// a picture on a *window* is freed by the server with the window;
|
|
578
912
|
// asking again would raise BadPicture. Same rule as `_destroyed`.
|
|
579
913
|
if (this._target === this.window && !(this.window instanceof Pixmap)) {
|
|
580
|
-
this.
|
|
914
|
+
this._picture.forget();
|
|
581
915
|
} else {
|
|
582
|
-
this.
|
|
916
|
+
this._picture.destroy();
|
|
583
917
|
}
|
|
584
|
-
this.
|
|
918
|
+
this._picture = null;
|
|
585
919
|
}
|
|
586
920
|
}
|
|
587
921
|
|
|
@@ -613,7 +947,7 @@ class RenderingContext2d {
|
|
|
613
947
|
// a hover repaint of two tab headers used to blit the whole window. A
|
|
614
948
|
// context with no rectangular clip reports nothing, and the window falls
|
|
615
949
|
// back to a full blit, which is what any drawing outside a clip needs.
|
|
616
|
-
this.window._markDirty(this.
|
|
950
|
+
this.window._markDirty(this._clipRect());
|
|
617
951
|
}
|
|
618
952
|
|
|
619
953
|
// html context2d compatibility: canvas.getContext('2d').canvas == canvas
|
|
@@ -762,8 +1096,12 @@ class RenderingContext2d {
|
|
|
762
1096
|
this._shadowOffsetY = s.shadowOffsetY;
|
|
763
1097
|
this._m = s.m;
|
|
764
1098
|
if (s.clips !== this._clips) {
|
|
765
|
-
|
|
1099
|
+
const hadRegion = this._hasRegionClip;
|
|
1100
|
+
this._setClips(s.clips);
|
|
766
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();
|
|
767
1105
|
}
|
|
768
1106
|
}
|
|
769
1107
|
|
|
@@ -1099,6 +1437,110 @@ class RenderingContext2d {
|
|
|
1099
1437
|
);
|
|
1100
1438
|
}
|
|
1101
1439
|
|
|
1440
|
+
/**
|
|
1441
|
+
* The shadow of positioned glyph runs, cached — what `drawGlyphs`, and
|
|
1442
|
+
* therefore every `TextLayout.draw`, casts (issue #283).
|
|
1443
|
+
*
|
|
1444
|
+
* A paragraph gets **one** coverage surface, not one per line: the runs
|
|
1445
|
+
* already carry their own baselines, so they all go into the same surface
|
|
1446
|
+
* exactly as they all go into the same glyph composite. Nothing is
|
|
1447
|
+
* re-shaped — the caller handed us the runs, which is why this path is
|
|
1448
|
+
* cheaper than `_shadowOfText`, not dearer.
|
|
1449
|
+
*
|
|
1450
|
+
* The cached copy is position-independent, as `fillText`'s is: geometry is
|
|
1451
|
+
* stored relative to the first run's origin and the composite carries it
|
|
1452
|
+
* to wherever the text is drawn. The key is that relative geometry plus
|
|
1453
|
+
* the identity of each run, so the same string laid out to two widths —
|
|
1454
|
+
* same runs, different line origins — is two shadows, and re-drawing one
|
|
1455
|
+
* layout is one lookup no matter how many glyphs are in it.
|
|
1456
|
+
*
|
|
1457
|
+
* @param {Array<{run, x, y, textRendering?}>} positioned device-space runs
|
|
1458
|
+
*/
|
|
1459
|
+
_shadowOfGlyphs(positioned) {
|
|
1460
|
+
if (!positioned.length) return;
|
|
1461
|
+
const app = this.window.app;
|
|
1462
|
+
const policy = shadowPolicyOf(app);
|
|
1463
|
+
const sigma = shadowSigma(this._shadowBlur, policy);
|
|
1464
|
+
const reach = shadowReach(sigma);
|
|
1465
|
+
|
|
1466
|
+
// Run origins relative to the first, rounded: whole-pixel offsets are
|
|
1467
|
+
// what the bitmap glyph path draws at anyway, and they keep the key
|
|
1468
|
+
// stable as the paragraph moves — `(x + a) - (x + b)` is not exactly
|
|
1469
|
+
// `a - b` in floating point, and an origin-dependent key would miss the
|
|
1470
|
+
// cache on every scroll.
|
|
1471
|
+
const ax = positioned[0].x;
|
|
1472
|
+
const ay = positioned[0].y;
|
|
1473
|
+
const local = positioned.map((p) => ({
|
|
1474
|
+
run: p.run,
|
|
1475
|
+
x: Math.round(p.x - ax),
|
|
1476
|
+
y: Math.round(p.y - ay),
|
|
1477
|
+
textRendering: p.textRendering,
|
|
1478
|
+
}));
|
|
1479
|
+
const key = `${local
|
|
1480
|
+
.map((p) => `${runId(p.run)},${p.x},${p.y},${p.textRendering ?? ""}`)
|
|
1481
|
+
.join("\u0000")}\u0000${sigma}`;
|
|
1482
|
+
|
|
1483
|
+
let ink = null;
|
|
1484
|
+
const surface = cachedShadow(app, key, () => {
|
|
1485
|
+
ink = positionedRunsInk(local);
|
|
1486
|
+
if (!ink) return null; // a line of spaces inks nothing
|
|
1487
|
+
const box = {
|
|
1488
|
+
x: Math.floor(ink.minX) - 1 - reach,
|
|
1489
|
+
y: Math.floor(ink.minY) - 1 - reach,
|
|
1490
|
+
};
|
|
1491
|
+
box.w = Math.ceil(ink.maxX) + 1 + reach - box.x;
|
|
1492
|
+
box.h = Math.ceil(ink.maxY) + 1 + reach - box.y;
|
|
1493
|
+
if (box.w * box.h > policy.maxPixels) return null;
|
|
1494
|
+
let coverage = new Surface(app, {
|
|
1495
|
+
width: box.w,
|
|
1496
|
+
height: box.h,
|
|
1497
|
+
format: "a8",
|
|
1498
|
+
});
|
|
1499
|
+
coverage.render((sctx) => {
|
|
1500
|
+
this._loadShadowState(sctx, 0, 0);
|
|
1501
|
+
// the origins are already device-space and placed by hand
|
|
1502
|
+
sctx._m = [1, 0, 0, 1, 0, 0];
|
|
1503
|
+
sctx._drawGlyphsDevice(
|
|
1504
|
+
this.Render.PictOp.Over,
|
|
1505
|
+
sctx._backgroundPicture,
|
|
1506
|
+
local.map((p) => ({ ...p, x: p.x - box.x, y: p.y - box.y })),
|
|
1507
|
+
);
|
|
1508
|
+
});
|
|
1509
|
+
if (sigma > 0) coverage = blurCoverage(coverage, sigma);
|
|
1510
|
+
// where the anchor sits inside the surface — whole pixels, so the
|
|
1511
|
+
// composite below can carry it anywhere
|
|
1512
|
+
coverage._shadowOrigin = { x: -box.x, y: -box.y };
|
|
1513
|
+
return coverage;
|
|
1514
|
+
});
|
|
1515
|
+
if (surface) {
|
|
1516
|
+
const origin = surface._shadowOrigin;
|
|
1517
|
+
this._paintShadow(
|
|
1518
|
+
surface,
|
|
1519
|
+
Math.round(ax + this._shadowOffsetX) - origin.x,
|
|
1520
|
+
Math.round(ay + this._shadowOffsetY) - origin.y,
|
|
1521
|
+
);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
if (!ink) return;
|
|
1525
|
+
// Padded ink larger than a shadow surface may be: fall back to the
|
|
1526
|
+
// clipped, uncached path, which sizes itself to the part of the shadow
|
|
1527
|
+
// that can actually be seen — the same escape `_shadowOfText` takes.
|
|
1528
|
+
this._shadowOfDrawing(
|
|
1529
|
+
{
|
|
1530
|
+
minX: ax + ink.minX,
|
|
1531
|
+
maxX: ax + ink.maxX,
|
|
1532
|
+
minY: ay + ink.minY,
|
|
1533
|
+
maxY: ay + ink.maxY,
|
|
1534
|
+
},
|
|
1535
|
+
(sctx, dx, dy) =>
|
|
1536
|
+
sctx._drawGlyphsDevice(
|
|
1537
|
+
this.Render.PictOp.Over,
|
|
1538
|
+
sctx._backgroundPicture,
|
|
1539
|
+
positioned.map((p) => ({ ...p, x: p.x + dx, y: p.y + dy })),
|
|
1540
|
+
),
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1102
1544
|
/** the shadow of a `fill()`/`stroke()`-shaped call, from its arguments */
|
|
1103
1545
|
_shadowOfPath(args, stroke) {
|
|
1104
1546
|
if (stroke) {
|
|
@@ -1525,16 +1967,19 @@ class RenderingContext2d {
|
|
|
1525
1967
|
// A rectangular clip narrows where the coverage is *composited*; the
|
|
1526
1968
|
// mask content outside it is stale by the same argument as outside `b`,
|
|
1527
1969
|
// and a box the clip rejects outright needs no coverage at all. A stack
|
|
1528
|
-
// whose mask was dropped by restore() may still hold a poly
|
|
1529
|
-
//
|
|
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.
|
|
1530
1973
|
let out = b;
|
|
1531
|
-
if (
|
|
1532
|
-
|
|
1533
|
-
if (cr) {
|
|
1534
|
-
out = intersectBox(b, cr);
|
|
1535
|
-
if (!out) return false;
|
|
1536
|
-
} else {
|
|
1974
|
+
if (!this.clipMask) {
|
|
1975
|
+
if (this._hasPolyClip) {
|
|
1537
1976
|
this._requireClipMask();
|
|
1977
|
+
} else {
|
|
1978
|
+
const cr = this._clipRect();
|
|
1979
|
+
if (cr) {
|
|
1980
|
+
out = intersectBox(b, cr);
|
|
1981
|
+
if (!out) return false;
|
|
1982
|
+
}
|
|
1538
1983
|
}
|
|
1539
1984
|
}
|
|
1540
1985
|
if (!this._uploadCoverage({ ...job, dx: -b.x, dy: -b.y }, b, edges)) {
|
|
@@ -1578,7 +2023,7 @@ class RenderingContext2d {
|
|
|
1578
2023
|
op,
|
|
1579
2024
|
src.id,
|
|
1580
2025
|
this.fillMask.id,
|
|
1581
|
-
this.
|
|
2026
|
+
this._dst(),
|
|
1582
2027
|
out.x,
|
|
1583
2028
|
out.y,
|
|
1584
2029
|
out.x,
|
|
@@ -1976,7 +2421,10 @@ class RenderingContext2d {
|
|
|
1976
2421
|
const direct =
|
|
1977
2422
|
!hasRound &&
|
|
1978
2423
|
this.globalAlpha >= 1 &&
|
|
1979
|
-
|
|
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() &&
|
|
1980
2428
|
op === this.Render.PictOp.Over;
|
|
1981
2429
|
const chunk = 4000 * 6;
|
|
1982
2430
|
if (direct) {
|
|
@@ -1994,7 +2442,7 @@ class RenderingContext2d {
|
|
|
1994
2442
|
src.id,
|
|
1995
2443
|
Math.floor(batch[0]),
|
|
1996
2444
|
Math.floor(batch[1]),
|
|
1997
|
-
this.
|
|
2445
|
+
this._dst(),
|
|
1998
2446
|
this.Render.a8,
|
|
1999
2447
|
batch,
|
|
2000
2448
|
);
|
|
@@ -2152,6 +2600,12 @@ class RenderingContext2d {
|
|
|
2152
2600
|
* is not at the window's origin — landed at the untransformed coordinates
|
|
2153
2601
|
* and was then cut by the clip, while the neighbouring `fillRect` and
|
|
2154
2602
|
* `drawImage` moved (issue #280).
|
|
2603
|
+
*
|
|
2604
|
+
* The shadow state applies too, as it does to `fillText`: one blurred
|
|
2605
|
+
* coverage surface for the whole call, cached on the runs' identity and
|
|
2606
|
+
* relative positions, painted under the glyphs (issue #283). A paragraph
|
|
2607
|
+
* whose spans change colour draws as several calls, and — as several
|
|
2608
|
+
* `fillText`s would — casts a shadow per call.
|
|
2155
2609
|
*/
|
|
2156
2610
|
drawGlyphs(op, src, positioned) {
|
|
2157
2611
|
const m = this._m;
|
|
@@ -2161,6 +2615,7 @@ class RenderingContext2d {
|
|
|
2161
2615
|
return { ...p, x, y };
|
|
2162
2616
|
});
|
|
2163
2617
|
}
|
|
2618
|
+
if (this._shadowed()) this._shadowOfGlyphs(positioned);
|
|
2164
2619
|
this._drawGlyphsDevice(op, src, positioned);
|
|
2165
2620
|
}
|
|
2166
2621
|
|
|
@@ -2182,26 +2637,20 @@ class RenderingContext2d {
|
|
|
2182
2637
|
const app = this.window.app;
|
|
2183
2638
|
const R = this.Render;
|
|
2184
2639
|
if (!prepareStyle(src, this._m)) return;
|
|
2185
|
-
if (!this.
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
rect.y,
|
|
2200
|
-
rect.w,
|
|
2201
|
-
rect.h,
|
|
2202
|
-
]);
|
|
2203
|
-
drawGlyphRuns(app, op, src.id, this.picture.id, positioned);
|
|
2204
|
-
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();
|
|
2205
2654
|
this._markDirty();
|
|
2206
2655
|
return;
|
|
2207
2656
|
}
|
|
@@ -2240,7 +2689,7 @@ class RenderingContext2d {
|
|
|
2240
2689
|
op,
|
|
2241
2690
|
src.id,
|
|
2242
2691
|
this.fillMask.id,
|
|
2243
|
-
this.
|
|
2692
|
+
this._dst(),
|
|
2244
2693
|
0,
|
|
2245
2694
|
0,
|
|
2246
2695
|
0,
|
|
@@ -2263,22 +2712,14 @@ class RenderingContext2d {
|
|
|
2263
2712
|
if (!traps.length) return;
|
|
2264
2713
|
const app = this.window.app;
|
|
2265
2714
|
const R = this.Render;
|
|
2266
|
-
if (!this.
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
if (rect
|
|
2274
|
-
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
2275
|
-
rect.x,
|
|
2276
|
-
rect.y,
|
|
2277
|
-
rect.w,
|
|
2278
|
-
rect.h,
|
|
2279
|
-
]);
|
|
2280
|
-
compositeTraps(app, op, src.id, this.picture.id, traps);
|
|
2281
|
-
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();
|
|
2282
2723
|
this._markDirty();
|
|
2283
2724
|
return;
|
|
2284
2725
|
}
|
|
@@ -2316,7 +2757,7 @@ class RenderingContext2d {
|
|
|
2316
2757
|
op,
|
|
2317
2758
|
src.id,
|
|
2318
2759
|
this.fillMask.id,
|
|
2319
|
-
this.
|
|
2760
|
+
this._dst(),
|
|
2320
2761
|
0,
|
|
2321
2762
|
0,
|
|
2322
2763
|
0,
|
|
@@ -2329,19 +2770,111 @@ class RenderingContext2d {
|
|
|
2329
2770
|
this._markDirty();
|
|
2330
2771
|
}
|
|
2331
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
|
+
|
|
2332
2850
|
// combined clip ∩ globalAlpha mask for direct composites (rect/image
|
|
2333
2851
|
// fast paths); returns a picture id or 0. Reuses the fill scratch mask.
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
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;
|
|
2338
2869
|
if (this.globalAlpha >= 1) return clipMask ? clipMask.id : 0;
|
|
2339
2870
|
this._ensureFillMask();
|
|
2871
|
+
const b = this._maskBox(box);
|
|
2872
|
+
if (b.w === 0 || b.h === 0) return this.fillMask.id; // nothing is sampled
|
|
2340
2873
|
this.Render.FillRectangles(
|
|
2341
2874
|
this.Render.PictOp.Src,
|
|
2342
2875
|
this.fillMask.id,
|
|
2343
2876
|
[0, 0, 0, this.globalAlpha],
|
|
2344
|
-
[
|
|
2877
|
+
[b.x, b.y, b.w, b.h],
|
|
2345
2878
|
);
|
|
2346
2879
|
if (clipMask) {
|
|
2347
2880
|
this.Render.Composite(
|
|
@@ -2349,14 +2882,14 @@ class RenderingContext2d {
|
|
|
2349
2882
|
clipMask.id,
|
|
2350
2883
|
0,
|
|
2351
2884
|
this.fillMask.id,
|
|
2885
|
+
b.x,
|
|
2886
|
+
b.y,
|
|
2352
2887
|
0,
|
|
2353
2888
|
0,
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
this.width,
|
|
2359
|
-
this.height,
|
|
2889
|
+
b.x,
|
|
2890
|
+
b.y,
|
|
2891
|
+
b.w,
|
|
2892
|
+
b.h,
|
|
2360
2893
|
);
|
|
2361
2894
|
}
|
|
2362
2895
|
return this.fillMask.id;
|
|
@@ -2373,10 +2906,10 @@ class RenderingContext2d {
|
|
|
2373
2906
|
// and what every caller predating ARGB windows expects.
|
|
2374
2907
|
clearRect(x, y, w, h) {
|
|
2375
2908
|
const alpha = this._hasAlpha;
|
|
2376
|
-
if (matIsIdentity(this._m) && !this.
|
|
2909
|
+
if (matIsIdentity(this._m) && !this._hasPolyClip && !this._clipRect()) {
|
|
2377
2910
|
this.Render.FillRectangles(
|
|
2378
2911
|
this.Render.PictOp.Src,
|
|
2379
|
-
this.
|
|
2912
|
+
this._dst(),
|
|
2380
2913
|
alpha ? [0, 0, 0, 0] : [1, 1, 1, 1],
|
|
2381
2914
|
[x, y, w, h],
|
|
2382
2915
|
);
|
|
@@ -2411,20 +2944,30 @@ class RenderingContext2d {
|
|
|
2411
2944
|
_fillRect(x, y, w, h) {
|
|
2412
2945
|
if (matIsIdentity(this._m)) {
|
|
2413
2946
|
if (!prepareStyle(this._backgroundPicture, this._m)) return;
|
|
2414
|
-
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 };
|
|
2415
2958
|
this.Render.Composite(
|
|
2416
|
-
|
|
2959
|
+
op,
|
|
2417
2960
|
this._backgroundPicture.id,
|
|
2418
|
-
mask,
|
|
2419
|
-
this.
|
|
2420
|
-
x,
|
|
2421
|
-
y,
|
|
2422
|
-
x,
|
|
2423
|
-
y,
|
|
2424
|
-
x,
|
|
2425
|
-
y,
|
|
2426
|
-
w,
|
|
2427
|
-
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,
|
|
2428
2971
|
);
|
|
2429
2972
|
this._markDirty();
|
|
2430
2973
|
return;
|
|
@@ -2480,11 +3023,11 @@ class RenderingContext2d {
|
|
|
2480
3023
|
this._shadowOfPolys(flattenPath(rectPath._cmds, this._m), {});
|
|
2481
3024
|
}
|
|
2482
3025
|
|
|
2483
|
-
const clip = this.
|
|
3026
|
+
const clip = this._clipRect();
|
|
2484
3027
|
if (
|
|
2485
3028
|
!matIsIdentity(this._m) ||
|
|
2486
3029
|
!isPlainColor(this._fillStyle) ||
|
|
2487
|
-
|
|
3030
|
+
this._hasPolyClip
|
|
2488
3031
|
) {
|
|
2489
3032
|
for (let i = 0; i < flat.length; i += 4)
|
|
2490
3033
|
this._fillRect(flat[i], flat[i + 1], flat[i + 2], flat[i + 3]);
|
|
@@ -2492,14 +3035,7 @@ class RenderingContext2d {
|
|
|
2492
3035
|
}
|
|
2493
3036
|
if (clip && (clip.w === 0 || clip.h === 0)) return; // clipped away
|
|
2494
3037
|
const R = this.Render;
|
|
2495
|
-
if (clip)
|
|
2496
|
-
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
2497
|
-
clip.x,
|
|
2498
|
-
clip.y,
|
|
2499
|
-
clip.w,
|
|
2500
|
-
clip.h,
|
|
2501
|
-
]);
|
|
2502
|
-
}
|
|
3038
|
+
if (clip) this._setPictureClip(clip);
|
|
2503
3039
|
// globalAlpha folds into the premultiplied colour — scaling all four
|
|
2504
3040
|
// components is exactly what compositing at that opacity means, for
|
|
2505
3041
|
// any composite op (the mask slot would only scale the source the same
|
|
@@ -2510,12 +3046,12 @@ class RenderingContext2d {
|
|
|
2510
3046
|
for (let i = 0; i < flat.length; i += chunk) {
|
|
2511
3047
|
R.FillRectangles(
|
|
2512
3048
|
this._op(),
|
|
2513
|
-
this.
|
|
3049
|
+
this._dst(),
|
|
2514
3050
|
color,
|
|
2515
3051
|
i === 0 && flat.length <= chunk ? flat : flat.slice(i, i + chunk),
|
|
2516
3052
|
);
|
|
2517
3053
|
}
|
|
2518
|
-
if (clip) this.
|
|
3054
|
+
if (clip) this._invalidatePictureClip();
|
|
2519
3055
|
this._markDirty();
|
|
2520
3056
|
}
|
|
2521
3057
|
|
|
@@ -2628,8 +3164,8 @@ class RenderingContext2d {
|
|
|
2628
3164
|
// change what it covers — the rule argument needs no inspection)
|
|
2629
3165
|
if (!isPlainColor(this._fillStyle)) return this._shapeMiss("gradient");
|
|
2630
3166
|
if (this._gco !== "source-over") return this._shapeMiss("composite-op");
|
|
2631
|
-
|
|
2632
|
-
|
|
3167
|
+
if (this._hasPolyClip) return this._shapeMiss("clip-mask");
|
|
3168
|
+
const clip = this._clipRect();
|
|
2633
3169
|
|
|
2634
3170
|
const { x, y, w, h, radii } = box;
|
|
2635
3171
|
if (
|
|
@@ -2735,8 +3271,8 @@ class RenderingContext2d {
|
|
|
2735
3271
|
if (!isPlainColor(this._strokeStyle)) return this._shapeMiss("gradient");
|
|
2736
3272
|
if (this._gco !== "source-over") return this._shapeMiss("composite-op");
|
|
2737
3273
|
if (this._lineDash.length) return this._shapeMiss("dashes");
|
|
2738
|
-
|
|
2739
|
-
|
|
3274
|
+
if (this._hasPolyClip) return this._shapeMiss("clip-mask");
|
|
3275
|
+
const clip = this._clipRect();
|
|
2740
3276
|
|
|
2741
3277
|
const bw = this.lineWidth;
|
|
2742
3278
|
if (!Number.isFinite(bw) || bw <= 0) return this._shapeMiss("geometry");
|
|
@@ -2866,22 +3402,16 @@ class RenderingContext2d {
|
|
|
2866
3402
|
if (clip && (clip.w === 0 || clip.h === 0)) return; // clipped away
|
|
2867
3403
|
const R = this.Render;
|
|
2868
3404
|
const app = this.window.app;
|
|
2869
|
-
if (clip)
|
|
2870
|
-
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
2871
|
-
clip.x,
|
|
2872
|
-
clip.y,
|
|
2873
|
-
clip.w,
|
|
2874
|
-
clip.h,
|
|
2875
|
-
]);
|
|
2876
|
-
}
|
|
3405
|
+
if (clip) this._setPictureClip(clip);
|
|
2877
3406
|
if (corners.length) {
|
|
2878
3407
|
const page = getShapeGlyphPage(app);
|
|
2879
3408
|
page.ensure(corners);
|
|
2880
3409
|
const items = [];
|
|
2881
3410
|
for (const spec of corners) {
|
|
3411
|
+
const e = page.entry(spec.key);
|
|
2882
3412
|
items.push({
|
|
2883
|
-
gs:
|
|
2884
|
-
lid:
|
|
3413
|
+
gs: e.gs,
|
|
3414
|
+
lid: e.lid,
|
|
2885
3415
|
adv: 0,
|
|
2886
3416
|
x: spec.px,
|
|
2887
3417
|
y: spec.py,
|
|
@@ -2893,7 +3423,7 @@ class RenderingContext2d {
|
|
|
2893
3423
|
encoded.bits,
|
|
2894
3424
|
R.PictOp.Over,
|
|
2895
3425
|
src.id,
|
|
2896
|
-
this.
|
|
3426
|
+
this._dst(),
|
|
2897
3427
|
0,
|
|
2898
3428
|
encoded.gsid,
|
|
2899
3429
|
items[0].x,
|
|
@@ -2903,9 +3433,9 @@ class RenderingContext2d {
|
|
|
2903
3433
|
trimShapeGlyphs(app);
|
|
2904
3434
|
}
|
|
2905
3435
|
if (rects.length) {
|
|
2906
|
-
R.FillRectangles(R.PictOp.Over, this.
|
|
3436
|
+
R.FillRectangles(R.PictOp.Over, this._dst(), color, rects);
|
|
2907
3437
|
}
|
|
2908
|
-
if (clip) this.
|
|
3438
|
+
if (clip) this._invalidatePictureClip();
|
|
2909
3439
|
this._markDirty();
|
|
2910
3440
|
}
|
|
2911
3441
|
|
|
@@ -2940,8 +3470,7 @@ class RenderingContext2d {
|
|
|
2940
3470
|
// be honoured by rounding — which would differ from the mask path's
|
|
2941
3471
|
// antialiased edge by up to a pixel. Rare, so leave those to the mask
|
|
2942
3472
|
// rather than quietly changing what they look like.
|
|
2943
|
-
|
|
2944
|
-
if (![x, y, w, h].every(integral)) return null;
|
|
3473
|
+
if (![x, y, w, h].every(isIntegral)) return null;
|
|
2945
3474
|
return {
|
|
2946
3475
|
x: Math.round(x),
|
|
2947
3476
|
y: Math.round(y),
|
|
@@ -2950,14 +3479,21 @@ class RenderingContext2d {
|
|
|
2950
3479
|
};
|
|
2951
3480
|
}
|
|
2952
3481
|
|
|
2953
|
-
/**
|
|
2954
|
-
*
|
|
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
|
+
*/
|
|
2955
3491
|
_clipRect() {
|
|
2956
|
-
if (
|
|
3492
|
+
if (this._hasPolyClip) return null;
|
|
2957
3493
|
let out = null;
|
|
2958
3494
|
for (const entry of this._clips) {
|
|
2959
3495
|
const r = entry.rect;
|
|
2960
|
-
if (!r)
|
|
3496
|
+
if (!r) continue; // a region entry
|
|
2961
3497
|
if (!out) {
|
|
2962
3498
|
out = { ...r };
|
|
2963
3499
|
continue;
|
|
@@ -2968,6 +3504,7 @@ class RenderingContext2d {
|
|
|
2968
3504
|
const bottom = Math.min(out.y + out.h, r.y + r.h);
|
|
2969
3505
|
out = { x, y, w: Math.max(0, right - x), h: Math.max(0, bottom - y) };
|
|
2970
3506
|
}
|
|
3507
|
+
if (!out) return null;
|
|
2971
3508
|
// clamp into the surface: the server rejects out-of-range rectangles
|
|
2972
3509
|
const x = Math.max(0, Math.min(out.x, this.width));
|
|
2973
3510
|
const y = Math.max(0, Math.min(out.y, this.height));
|
|
@@ -2976,11 +3513,31 @@ class RenderingContext2d {
|
|
|
2976
3513
|
return { x, y, w, h };
|
|
2977
3514
|
}
|
|
2978
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
|
+
|
|
2979
3536
|
clip(...args) {
|
|
2980
3537
|
const { polys, rule } = this._polysFor(args);
|
|
2981
3538
|
const entry = { polys, rule, rect: RenderingContext2d._rectOfPolys(polys) };
|
|
2982
3539
|
// copy-on-write: earlier save() snapshots keep their own clip list
|
|
2983
|
-
this.
|
|
3540
|
+
this._setClips(this._clips.concat([entry]));
|
|
2984
3541
|
|
|
2985
3542
|
// A stack of rectangles stays virtual: consumers apply the intersected
|
|
2986
3543
|
// rectangle server-side (SetPictureClipRectangles, bounded composites),
|
|
@@ -2997,6 +3554,36 @@ class RenderingContext2d {
|
|
|
2997
3554
|
this._intersectClip(entry);
|
|
2998
3555
|
}
|
|
2999
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
|
+
|
|
3000
3587
|
/**
|
|
3001
3588
|
* Build the a8 mask from the whole clip stack, on first demand. Rect
|
|
3002
3589
|
* entries are plain fills; only genuinely non-rectangular entries
|
|
@@ -3012,7 +3599,18 @@ class RenderingContext2d {
|
|
|
3012
3599
|
[0, 0, 0, 0],
|
|
3013
3600
|
[0, 0, this.width, this.height],
|
|
3014
3601
|
);
|
|
3015
|
-
|
|
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];
|
|
3016
3614
|
if (first.rect) {
|
|
3017
3615
|
const r = first.rect;
|
|
3018
3616
|
const x = Math.max(0, Math.min(r.x, this.width));
|
|
@@ -3030,8 +3628,7 @@ class RenderingContext2d {
|
|
|
3030
3628
|
} else {
|
|
3031
3629
|
this._applyPolyClip(first, R.PictOp.Src);
|
|
3032
3630
|
}
|
|
3033
|
-
for (let i = 1; i <
|
|
3034
|
-
this._intersectClip(this._clips[i]);
|
|
3631
|
+
for (let i = 1; i < shapes.length; i++) this._intersectClip(shapes[i]);
|
|
3035
3632
|
}
|
|
3036
3633
|
|
|
3037
3634
|
/** The live mask, built if the stack has not needed one yet. Callers on
|
|
@@ -3739,28 +4336,27 @@ class RenderingContext2d {
|
|
|
3739
4336
|
* Uniform alpha needs no surface-sized mask either: a 1x1 repeating
|
|
3740
4337
|
* picture is the same thing to the server, with no pixel work at all.
|
|
3741
4338
|
*/
|
|
3742
|
-
_beginDirectComposite() {
|
|
3743
|
-
const rect = this.
|
|
3744
|
-
if (!rect)
|
|
3745
|
-
|
|
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
|
+
}
|
|
3746
4351
|
if (rect.w === 0 || rect.h === 0)
|
|
3747
4352
|
return { mask: 0, clipped: false, empty: true };
|
|
3748
|
-
this.
|
|
3749
|
-
|
|
3750
|
-
rect.y,
|
|
3751
|
-
rect.w,
|
|
3752
|
-
rect.h,
|
|
3753
|
-
]);
|
|
3754
|
-
const mask =
|
|
3755
|
-
this.globalAlpha >= 1
|
|
3756
|
-
? 0
|
|
3757
|
-
: this.createSolidPicture(0, 0, 0, this.globalAlpha).id;
|
|
3758
|
-
return { mask, clipped: true, empty: false };
|
|
4353
|
+
this._setPictureClip(rect);
|
|
4354
|
+
return { mask: this._alphaMask(), clipped: true, empty: false };
|
|
3759
4355
|
}
|
|
3760
4356
|
|
|
3761
4357
|
_endDirectComposite(state) {
|
|
3762
4358
|
if (!state.clipped) return;
|
|
3763
|
-
this.
|
|
4359
|
+
this._invalidatePictureClip();
|
|
3764
4360
|
}
|
|
3765
4361
|
|
|
3766
4362
|
/** The source colour for a coverage composite, with `globalAlpha` folded
|
|
@@ -3810,12 +4406,12 @@ class RenderingContext2d {
|
|
|
3810
4406
|
const mx = scaled ? 0 : sx;
|
|
3811
4407
|
const my = scaled ? 0 : sy;
|
|
3812
4408
|
|
|
3813
|
-
const rect = this.
|
|
4409
|
+
const rect = this._clipRect();
|
|
3814
4410
|
// the mask slot can hold exactly one picture, so anything that cannot be
|
|
3815
4411
|
// folded into the colour or handed to the server as a clip rectangle has
|
|
3816
4412
|
// to be intersected into the scratch mask first
|
|
3817
4413
|
const scratch =
|
|
3818
|
-
|
|
4414
|
+
this._hasPolyClip ||
|
|
3819
4415
|
(this.globalAlpha < 1 && !isPlainColor(this._fillStyle));
|
|
3820
4416
|
|
|
3821
4417
|
if (scratch) {
|
|
@@ -3856,7 +4452,7 @@ class RenderingContext2d {
|
|
|
3856
4452
|
this.height,
|
|
3857
4453
|
);
|
|
3858
4454
|
}
|
|
3859
|
-
if (this.
|
|
4455
|
+
if (this._hasPolyClip) {
|
|
3860
4456
|
const clipMask = this._requireClipMask();
|
|
3861
4457
|
R.Composite(
|
|
3862
4458
|
R.PictOp.In,
|
|
@@ -3875,12 +4471,7 @@ class RenderingContext2d {
|
|
|
3875
4471
|
} else if (rect) {
|
|
3876
4472
|
// scratch was forced by alpha-over-gradient, not by the clip: the
|
|
3877
4473
|
// rectangle still applies, server-side, around the final composite
|
|
3878
|
-
|
|
3879
|
-
rect.x,
|
|
3880
|
-
rect.y,
|
|
3881
|
-
rect.w,
|
|
3882
|
-
rect.h,
|
|
3883
|
-
]);
|
|
4474
|
+
this._setPictureClip(rect);
|
|
3884
4475
|
}
|
|
3885
4476
|
// The scratch mask is surface-sized and surface-aligned — the coverage
|
|
3886
4477
|
// went into it at (dx, dy) — and so is a gradient source, so both are
|
|
@@ -3892,7 +4483,7 @@ class RenderingContext2d {
|
|
|
3892
4483
|
op,
|
|
3893
4484
|
this._backgroundPicture.id,
|
|
3894
4485
|
this.fillMask.id,
|
|
3895
|
-
this.
|
|
4486
|
+
this._dst(),
|
|
3896
4487
|
dx,
|
|
3897
4488
|
dy,
|
|
3898
4489
|
dx,
|
|
@@ -3902,23 +4493,9 @@ class RenderingContext2d {
|
|
|
3902
4493
|
dw,
|
|
3903
4494
|
dh,
|
|
3904
4495
|
);
|
|
3905
|
-
if (rect)
|
|
3906
|
-
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
3907
|
-
0,
|
|
3908
|
-
0,
|
|
3909
|
-
this.width,
|
|
3910
|
-
this.height,
|
|
3911
|
-
]);
|
|
3912
|
-
}
|
|
4496
|
+
if (rect) this._invalidatePictureClip();
|
|
3913
4497
|
} else if (!rect || (rect.w > 0 && rect.h > 0)) {
|
|
3914
|
-
if (rect)
|
|
3915
|
-
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
3916
|
-
rect.x,
|
|
3917
|
-
rect.y,
|
|
3918
|
-
rect.w,
|
|
3919
|
-
rect.h,
|
|
3920
|
-
]);
|
|
3921
|
-
}
|
|
4498
|
+
if (rect) this._setPictureClip(rect);
|
|
3922
4499
|
// the source is a 1x1 repeating solid (offset irrelevant) or the fill
|
|
3923
4500
|
// picture, which for a gradient is surface-aligned: same offset as the
|
|
3924
4501
|
// destination, exactly as in _fillPolys
|
|
@@ -3926,7 +4503,7 @@ class RenderingContext2d {
|
|
|
3926
4503
|
op,
|
|
3927
4504
|
this._coverageSource().id,
|
|
3928
4505
|
picture.id,
|
|
3929
|
-
this.
|
|
4506
|
+
this._dst(),
|
|
3930
4507
|
dx,
|
|
3931
4508
|
dy,
|
|
3932
4509
|
mx,
|
|
@@ -3936,9 +4513,7 @@ class RenderingContext2d {
|
|
|
3936
4513
|
dw,
|
|
3937
4514
|
dh,
|
|
3938
4515
|
);
|
|
3939
|
-
if (rect)
|
|
3940
|
-
this._resetPictureClip();
|
|
3941
|
-
}
|
|
4516
|
+
if (rect) this._invalidatePictureClip();
|
|
3942
4517
|
}
|
|
3943
4518
|
|
|
3944
4519
|
if (scaled) {
|
|
@@ -3984,7 +4559,7 @@ class RenderingContext2d {
|
|
|
3984
4559
|
return;
|
|
3985
4560
|
}
|
|
3986
4561
|
|
|
3987
|
-
const state = this._beginDirectComposite();
|
|
4562
|
+
const state = this._beginDirectComposite({ x: dx, y: dy, w: dw, h: dh });
|
|
3988
4563
|
if (state.empty) return;
|
|
3989
4564
|
const mask = state.mask;
|
|
3990
4565
|
const scaled = dw !== sw || dh !== sh;
|
|
@@ -4007,7 +4582,7 @@ class RenderingContext2d {
|
|
|
4007
4582
|
op,
|
|
4008
4583
|
picture.id,
|
|
4009
4584
|
mask,
|
|
4010
|
-
this.
|
|
4585
|
+
this._dst(),
|
|
4011
4586
|
0,
|
|
4012
4587
|
0,
|
|
4013
4588
|
dx,
|
|
@@ -4028,7 +4603,7 @@ class RenderingContext2d {
|
|
|
4028
4603
|
op,
|
|
4029
4604
|
picture.id,
|
|
4030
4605
|
mask,
|
|
4031
|
-
this.
|
|
4606
|
+
this._dst(),
|
|
4032
4607
|
sx,
|
|
4033
4608
|
sy,
|
|
4034
4609
|
dx,
|
|
@@ -4054,19 +4629,25 @@ class RenderingContext2d {
|
|
|
4054
4629
|
if (image instanceof RenderingContext2d) {
|
|
4055
4630
|
// TODO: if need to scale, set transform
|
|
4056
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;
|
|
4057
4638
|
this.Render.Composite(
|
|
4058
4639
|
this.Render.PictOp.Over,
|
|
4059
4640
|
image.picture.id,
|
|
4060
|
-
this._compositeMask(),
|
|
4061
|
-
this.
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
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,
|
|
4070
4651
|
);
|
|
4071
4652
|
this._markDirty();
|
|
4072
4653
|
} else if (
|
|
@@ -4075,6 +4656,13 @@ class RenderingContext2d {
|
|
|
4075
4656
|
typeof image.context.getImageData === "function"
|
|
4076
4657
|
) {
|
|
4077
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;
|
|
4078
4666
|
const imageData = image.context.getImageData(sx, sy, sWidth, sHeight);
|
|
4079
4667
|
// node-canvas hands over straight RGBA, and the rgba32 picture below is
|
|
4080
4668
|
// premultiplied — this used to swap the channels and stop there, which
|
|
@@ -4114,16 +4702,16 @@ class RenderingContext2d {
|
|
|
4114
4702
|
this.Render.Composite(
|
|
4115
4703
|
this.Render.PictOp.Over,
|
|
4116
4704
|
picture.id,
|
|
4117
|
-
this._compositeMask(),
|
|
4118
|
-
this.
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
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,
|
|
4127
4715
|
);
|
|
4128
4716
|
this._markDirty();
|
|
4129
4717
|
} finally {
|
|
@@ -4162,6 +4750,14 @@ class RenderingContext2d {
|
|
|
4162
4750
|
);
|
|
4163
4751
|
if (x1 <= x0 || y1 <= y0) return;
|
|
4164
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
|
+
|
|
4165
4761
|
// device -> source sample (adding the source-rect origin)
|
|
4166
4762
|
const srcM = [inv[0], inv[1], inv[2], inv[3], inv[4] + sx, inv[5] + sy];
|
|
4167
4763
|
this.Render.SetPictureTransform(picture.id, [
|
|
@@ -4176,20 +4772,19 @@ class RenderingContext2d {
|
|
|
4176
4772
|
1,
|
|
4177
4773
|
]);
|
|
4178
4774
|
picture.setFilter("bilinear");
|
|
4179
|
-
const mask = this._compositeMask();
|
|
4180
4775
|
this.Render.Composite(
|
|
4181
4776
|
op,
|
|
4182
4777
|
picture.id,
|
|
4183
|
-
mask,
|
|
4184
|
-
this.
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
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,
|
|
4193
4788
|
);
|
|
4194
4789
|
this.Render.SetPictureTransform(picture.id, [1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
4195
4790
|
picture.setFilter("nearest");
|