ntk 8.23.0 → 8.23.2
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/image.js +11 -4
- package/lib/picture.js +92 -1
- package/lib/renderingcontext_2d.js +164 -83
- package/lib/shadow.js +2 -2
- package/lib/surface.js +8 -2
- package/package.json +1 -1
package/lib/image.js
CHANGED
|
@@ -3,7 +3,7 @@ import { PNG } from 'pngjs';
|
|
|
3
3
|
|
|
4
4
|
import { builtin } from './builtin.js';
|
|
5
5
|
import { fromStraightRgba, pixelLayout } from './imagedata.js';
|
|
6
|
-
import Picture from './picture.js';
|
|
6
|
+
import Picture, { OWN_PICTURE } from './picture.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* A decoded raster image: `width`, `height` and non-premultiplied RGBA
|
|
@@ -31,10 +31,17 @@ export class Image {
|
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* The server-side Picture for this image on `app`'s display (uploaded
|
|
34
|
-
* and cached on first use)
|
|
35
|
-
* when compositing manually via the
|
|
34
|
+
* and cached on first use), read as stored: no transform, the `nearest`
|
|
35
|
+
* filter, no repeat. Useful directly when compositing manually via the
|
|
36
|
+
* Render extension; set anything else on it through the Picture's own
|
|
37
|
+
* `setTransform`/`setFilter`/`setRepeat`.
|
|
36
38
|
*/
|
|
37
39
|
picture(app) {
|
|
40
|
+
return this[OWN_PICTURE](app)._settle();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The picture as ntk's draws left it (`drawImage` sets what it needs). */
|
|
44
|
+
[OWN_PICTURE](app) {
|
|
38
45
|
let upload = this._uploads.get(app);
|
|
39
46
|
if (upload) return upload.picture;
|
|
40
47
|
|
|
@@ -87,7 +94,7 @@ export class Image {
|
|
|
87
94
|
* this image everywhere else.
|
|
88
95
|
*/
|
|
89
96
|
pixmap(app) {
|
|
90
|
-
this
|
|
97
|
+
this[OWN_PICTURE](app);
|
|
91
98
|
return this._uploads.get(app).pixmap;
|
|
92
99
|
}
|
|
93
100
|
|
package/lib/picture.js
CHANGED
|
@@ -7,6 +7,18 @@ const registry = new FinalizationRegistry(({ Render, X, id }) => {
|
|
|
7
7
|
});
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
+
/** RENDER's identity transform, row-major as `SetPictureTransform` takes it. */
|
|
11
|
+
export const IDENTITY_TRANSFORM = Object.freeze([1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
12
|
+
const NO_PARAMS = Object.freeze([]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The picture an `Image` or a `Surface` keeps for ntk's own draws, without
|
|
16
|
+
* putting it back as stored (`picture(app)` does that): `drawImage` reads it
|
|
17
|
+
* through and leaves its state for the next draw to set
|
|
18
|
+
* (docs/context-2d.md#how-an-image-is-read).
|
|
19
|
+
*/
|
|
20
|
+
export const OWN_PICTURE = Symbol('ntk.ownPicture');
|
|
21
|
+
|
|
10
22
|
export default class Picture {
|
|
11
23
|
constructor(app, args = {}) {
|
|
12
24
|
const X = app.X;
|
|
@@ -23,6 +35,20 @@ export default class Picture {
|
|
|
23
35
|
}
|
|
24
36
|
this._owned = true;
|
|
25
37
|
registry.register(this, { Render: this.Render, X, id: this.id }, this);
|
|
38
|
+
// How the server reads the picture — its transform, filter and repeat —
|
|
39
|
+
// as last set through the methods below, so a draw that needs what is
|
|
40
|
+
// there already sends nothing. Flipping them is not free: a filter
|
|
41
|
+
// changed and changed back around every composite cost Xwayland's glamor
|
|
42
|
+
// 36 ms over 256 composites where the composites took 0.6, and a
|
|
43
|
+
// transform put back to identity after each cost pixman 7.
|
|
44
|
+
this._transform = IDENTITY_TRANSFORM;
|
|
45
|
+
this._filter = 'nearest';
|
|
46
|
+
this._filterParams = NO_PARAMS;
|
|
47
|
+
this._repeat = args.repeat ?? 0;
|
|
48
|
+
// Whether what the picture is read through now is what a draw left
|
|
49
|
+
// (`_readThrough`) rather than what was set on it: the one `_settle`
|
|
50
|
+
// puts back. A filter hung on a surface to be drawn through stays.
|
|
51
|
+
this._leftByDraw = false;
|
|
26
52
|
}
|
|
27
53
|
|
|
28
54
|
/**
|
|
@@ -32,9 +58,67 @@ export default class Picture {
|
|
|
32
58
|
*
|
|
33
59
|
* That is what makes it right for resampling (`'bilinear'` under a
|
|
34
60
|
* transform) and a trap for anything expensive: see `setBlurFilter`.
|
|
61
|
+
*
|
|
62
|
+
* Nothing is sent when the picture has this filter already.
|
|
35
63
|
*/
|
|
36
|
-
setFilter(name, params) {
|
|
64
|
+
setFilter(name, params = NO_PARAMS) {
|
|
65
|
+
this._leftByDraw = false;
|
|
66
|
+
if (name === this._filter && sameNumbers(params, this._filterParams)) return;
|
|
37
67
|
this.Render.SetPictureFilter(this.id, name, params);
|
|
68
|
+
this._filter = name;
|
|
69
|
+
this._filterParams = params.length ? params.slice() : NO_PARAMS;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Set the transform the picture is read through: nine numbers, row-major,
|
|
74
|
+
* taking a destination point to the source point read for it, as RENDER's
|
|
75
|
+
* `SetPictureTransform` has them. Nothing is sent when it is the one the
|
|
76
|
+
* picture has.
|
|
77
|
+
*/
|
|
78
|
+
setTransform(m) {
|
|
79
|
+
this._leftByDraw = false;
|
|
80
|
+
if (sameNumbers(m, this._transform)) return;
|
|
81
|
+
this.Render.SetPictureTransform(this.id, m);
|
|
82
|
+
this._transform = sameNumbers(m, IDENTITY_TRANSFORM) ? IDENTITY_TRANSFORM : m.slice();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Set how the picture is read past its edge — RENDER's `repeat`: 0 none,
|
|
86
|
+
* 1 normal, 2 pad, 3 reflect. Nothing is sent when it is so already. */
|
|
87
|
+
setRepeat(repeat) {
|
|
88
|
+
this._leftByDraw = false;
|
|
89
|
+
if (repeat === this._repeat) return;
|
|
90
|
+
this.Render.ChangePicture(this.id, { repeat });
|
|
91
|
+
this._repeat = repeat;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Read as stored: no transform, the `nearest` filter and no repeat — how a
|
|
96
|
+
* composite takes the pixels themselves. What a picture is left as by a
|
|
97
|
+
* draw that read it some other way is put back here, and only what differs
|
|
98
|
+
* is sent.
|
|
99
|
+
*/
|
|
100
|
+
plain() {
|
|
101
|
+
this.setTransform(IDENTITY_TRANSFORM);
|
|
102
|
+
this.setFilter('nearest');
|
|
103
|
+
this.setRepeat(0);
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A draw reading the picture through these, and leaving them for the
|
|
108
|
+
* next draw to set (docs/context-2d.md#how-an-image-is-read). */
|
|
109
|
+
_readThrough(transform, filter, repeat) {
|
|
110
|
+
this.setTransform(transform);
|
|
111
|
+
this.setFilter(filter);
|
|
112
|
+
this.setRepeat(repeat);
|
|
113
|
+
this._leftByDraw = true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Put back what a draw left, and nothing that was set on the picture:
|
|
117
|
+
* what `Image.picture(app)` and `Surface.picture(app)` hand out, and what a
|
|
118
|
+
* draw that reads the pixels as they are reads. */
|
|
119
|
+
_settle() {
|
|
120
|
+
if (this._leftByDraw) this.plain();
|
|
121
|
+
return this;
|
|
38
122
|
}
|
|
39
123
|
|
|
40
124
|
/**
|
|
@@ -93,6 +177,13 @@ export default class Picture {
|
|
|
93
177
|
}
|
|
94
178
|
}
|
|
95
179
|
|
|
180
|
+
function sameNumbers(a, b) {
|
|
181
|
+
if (a === b) return true;
|
|
182
|
+
if (a.length !== b.length) return false;
|
|
183
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
96
187
|
// normalized 2d gaussian, row-major size x size
|
|
97
188
|
function gaussianKernel(size, sigma) {
|
|
98
189
|
const kernel = new Array(size * size);
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
subpathHead,
|
|
26
26
|
subpathTail,
|
|
27
27
|
} from "./path.js";
|
|
28
|
-
import Picture from "./picture.js";
|
|
28
|
+
import Picture, { OWN_PICTURE } from "./picture.js";
|
|
29
29
|
import { formatForDepth } from "./pictformat.js";
|
|
30
30
|
import Pixmap from "./pixmap.js";
|
|
31
31
|
import { REGION_DOCS, regionId } from "./region.js";
|
|
@@ -564,6 +564,58 @@ function contextSource(ctx, app) {
|
|
|
564
564
|
};
|
|
565
565
|
}
|
|
566
566
|
|
|
567
|
+
/**
|
|
568
|
+
* The picture a draw reads `image` through, and whether ntk keeps it. An
|
|
569
|
+
* `Image`'s and a `Surface`'s are kept: nothing reads them but ntk's draws
|
|
570
|
+
* and their `picture(app)`, which puts one back as stored first, so a draw
|
|
571
|
+
* sets the transform, filter and repeat it reads through (`readAs`) and
|
|
572
|
+
* leaves them for the next. A surface drawn through a matrix a tile at a
|
|
573
|
+
* time had each tile's transform and filter set, and both put back after
|
|
574
|
+
* it: five requests a tile where two do, and the putting back was most of
|
|
575
|
+
* the server's time (docs/context-2d.md#how-an-image-is-read). Any other
|
|
576
|
+
* picture — a context's own, a caller's handed in through a `picture(app)`
|
|
577
|
+
* of its own — is put back as stored once read (`readDone`), as every draw
|
|
578
|
+
* used to put back every picture.
|
|
579
|
+
*/
|
|
580
|
+
function sourcePicture(image, app) {
|
|
581
|
+
const own = image[OWN_PICTURE];
|
|
582
|
+
return own
|
|
583
|
+
? { picture: own.call(image, app), kept: true }
|
|
584
|
+
: { picture: image.picture(app), kept: false };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Have `picture` read through `transform`, `filter` and `repeat`. An ntk
|
|
588
|
+
* Picture sends only what it does not have; a caller's handle, which may be
|
|
589
|
+
* no more than an id and a `setFilter`, is sent all of it. */
|
|
590
|
+
function readAs(R, picture, transform, filter, repeat) {
|
|
591
|
+
if (picture instanceof Picture) {
|
|
592
|
+
picture._readThrough(transform, filter, repeat);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
R.SetPictureTransform(picture.id, transform);
|
|
596
|
+
picture.setFilter(filter);
|
|
597
|
+
if (repeat) R.ChangePicture(picture.id, { repeat });
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** After `readAs`: a picture ntk does not keep is put back as stored. */
|
|
601
|
+
function readDone(R, picture, kept, repeat) {
|
|
602
|
+
if (kept) return;
|
|
603
|
+
if (picture instanceof Picture) {
|
|
604
|
+
picture.plain();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
R.SetPictureTransform(picture.id, IDENTITY_TRANSFORM);
|
|
608
|
+
picture.setFilter("nearest");
|
|
609
|
+
if (repeat) R.ChangePicture(picture.id, { repeat: 0 });
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** Before a read of `picture`'s pixels as they are. One ntk does not keep
|
|
613
|
+
* is so at rest; one it keeps is put back from whatever the last draw left,
|
|
614
|
+
* and a filter set on it is kept, as it always was. */
|
|
615
|
+
function readPlain(picture, kept) {
|
|
616
|
+
if (kept) picture._settle();
|
|
617
|
+
}
|
|
618
|
+
|
|
567
619
|
function destroyedSourceError() {
|
|
568
620
|
return new Error(
|
|
569
621
|
"drawImage: the 2d context passed as the image has been destroyed — by\n" +
|
|
@@ -753,6 +805,10 @@ class RenderingContext2d {
|
|
|
753
805
|
this.fillMaskDrawable = null;
|
|
754
806
|
this.clipMask = null;
|
|
755
807
|
this.clipMaskDrawable = null;
|
|
808
|
+
// a clip mask a restore let go of, for the next path clip (`_rebuildClipMask`)
|
|
809
|
+
this._keptClipMask = null;
|
|
810
|
+
// where the clip mask may hold coverage, [x, y, w, h]; null for nowhere
|
|
811
|
+
this._clipMaskInk = null;
|
|
756
812
|
this._textStyle = null;
|
|
757
813
|
this._lastFontString = null;
|
|
758
814
|
this.textAlign = "start";
|
|
@@ -1172,6 +1228,8 @@ class RenderingContext2d {
|
|
|
1172
1228
|
this.clipMaskDrawable.destroy();
|
|
1173
1229
|
this.clipMask = this.clipMaskDrawable = null;
|
|
1174
1230
|
}
|
|
1231
|
+
// one kept is the size the surface was
|
|
1232
|
+
this._dropKeptClipMask();
|
|
1175
1233
|
// clip paths are device-space and survive the resize; rebuild the mask
|
|
1176
1234
|
if (this._clips && this._clips.length) this._rebuildClipMask();
|
|
1177
1235
|
}
|
|
@@ -1586,8 +1644,9 @@ class RenderingContext2d {
|
|
|
1586
1644
|
const source = this._fillSource;
|
|
1587
1645
|
this.fillStyle = this._shadowColor;
|
|
1588
1646
|
try {
|
|
1647
|
+
const { picture, kept } = sourcePicture(surface, this.window.app);
|
|
1589
1648
|
this._drawCoverage(
|
|
1590
|
-
|
|
1649
|
+
picture,
|
|
1591
1650
|
0,
|
|
1592
1651
|
0,
|
|
1593
1652
|
surface.width,
|
|
@@ -1597,6 +1656,7 @@ class RenderingContext2d {
|
|
|
1597
1656
|
surface.width,
|
|
1598
1657
|
surface.height,
|
|
1599
1658
|
this._op(),
|
|
1659
|
+
kept,
|
|
1600
1660
|
);
|
|
1601
1661
|
} finally {
|
|
1602
1662
|
this._fillStyle = style;
|
|
@@ -1992,7 +2052,10 @@ class RenderingContext2d {
|
|
|
1992
2052
|
this._shadowTile(plan),
|
|
1993
2053
|
);
|
|
1994
2054
|
if (!tile) return false;
|
|
1995
|
-
|
|
2055
|
+
// A piece drawn at its own size reads the tile as stored and a stretched
|
|
2056
|
+
// one through its scale: `_drawCoverage` sets each, and sends only what
|
|
2057
|
+
// the piece before did not leave.
|
|
2058
|
+
const { picture, kept } = sourcePicture(tile, app);
|
|
1996
2059
|
// put back as it was, as _paintShadow does
|
|
1997
2060
|
const style = this._fillStyle;
|
|
1998
2061
|
const source = this._fillSource;
|
|
@@ -2003,16 +2066,7 @@ class RenderingContext2d {
|
|
|
2003
2066
|
if (clip && !boxesMeet({ x: dx, y: dy, width: dw, height: dh }, clip)) {
|
|
2004
2067
|
continue;
|
|
2005
2068
|
}
|
|
2006
|
-
|
|
2007
|
-
// transform the last stretched one left on the picture
|
|
2008
|
-
if (sw === dw && sh === dh && picture._tileScaled) {
|
|
2009
|
-
this.Render.SetPictureTransform(picture.id, IDENTITY_TRANSFORM);
|
|
2010
|
-
picture.setFilter("nearest");
|
|
2011
|
-
picture._tileScaled = false;
|
|
2012
|
-
} else if (sw !== dw || sh !== dh) {
|
|
2013
|
-
picture._tileScaled = true;
|
|
2014
|
-
}
|
|
2015
|
-
this._drawCoverage(picture, sx, sy, sw, sh, dx, dy, dw, dh, op);
|
|
2069
|
+
this._drawCoverage(picture, sx, sy, sw, sh, dx, dy, dw, dh, op, kept);
|
|
2016
2070
|
}
|
|
2017
2071
|
} finally {
|
|
2018
2072
|
this._fillStyle = style;
|
|
@@ -2313,6 +2367,16 @@ class RenderingContext2d {
|
|
|
2313
2367
|
|
|
2314
2368
|
_ensureClipMask() {
|
|
2315
2369
|
if (this.clipMask) return;
|
|
2370
|
+
const kept = this._keptClipMask;
|
|
2371
|
+
if (kept) {
|
|
2372
|
+
// the one a restore let go of: what it holds is cleared where it was
|
|
2373
|
+
// written, by `_materializeClipMask`
|
|
2374
|
+
this._keptClipMask = null;
|
|
2375
|
+
this.clipMask = kept.mask;
|
|
2376
|
+
this.clipMaskDrawable = kept.drawable;
|
|
2377
|
+
this._clipMaskInk = kept.ink;
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2316
2380
|
this.clipMaskDrawable = new Pixmap(this.window.app, {
|
|
2317
2381
|
depth: 8,
|
|
2318
2382
|
width: this.width,
|
|
@@ -2322,6 +2386,8 @@ class RenderingContext2d {
|
|
|
2322
2386
|
drawable: this.clipMaskDrawable,
|
|
2323
2387
|
format: this.Render.a8,
|
|
2324
2388
|
});
|
|
2389
|
+
// a fresh pixmap's contents are undefined, all of them
|
|
2390
|
+
this._clipMaskInk = [0, 0, this.width, this.height];
|
|
2325
2391
|
}
|
|
2326
2392
|
|
|
2327
2393
|
/**
|
|
@@ -4852,12 +4918,16 @@ class RenderingContext2d {
|
|
|
4852
4918
|
_materializeClipMask() {
|
|
4853
4919
|
const R = this.Render;
|
|
4854
4920
|
this._ensureClipMask();
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4921
|
+
// Cleared where it may hold coverage, not all over: the mask is the size
|
|
4922
|
+
// of the surface, and a clip a rounded row of a list puts on a window
|
|
4923
|
+
// cleared the whole window's worth of it for each row of each frame.
|
|
4924
|
+
// What it holds is never outside its first shape's box, since what is
|
|
4925
|
+
// intersected with it only takes coverage away (`_clipMaskInk`).
|
|
4926
|
+
const ink = this._clipMaskInk;
|
|
4927
|
+
if (ink && ink[2] > 0 && ink[3] > 0) {
|
|
4928
|
+
R.FillRectangles(R.PictOp.Src, this.clipMask.id, [0, 0, 0, 0], ink);
|
|
4929
|
+
}
|
|
4930
|
+
this._clipMaskInk = null;
|
|
4861
4931
|
// region entries are the picture's clip, not the mask's business
|
|
4862
4932
|
const shapes = this._clips.filter((entry) => !entry.region);
|
|
4863
4933
|
if (!shapes.length) {
|
|
@@ -4867,6 +4937,7 @@ class RenderingContext2d {
|
|
|
4867
4937
|
[0, 0, 0, 1],
|
|
4868
4938
|
[0, 0, this.width, this.height],
|
|
4869
4939
|
);
|
|
4940
|
+
this._clipMaskInk = [0, 0, this.width, this.height];
|
|
4870
4941
|
return;
|
|
4871
4942
|
}
|
|
4872
4943
|
const first = shapes[0];
|
|
@@ -4883,9 +4954,20 @@ class RenderingContext2d {
|
|
|
4883
4954
|
[0, 0, 0, 1],
|
|
4884
4955
|
[x, y, right - x, bottom - y],
|
|
4885
4956
|
);
|
|
4957
|
+
this._clipMaskInk = [x, y, right - x, bottom - y];
|
|
4886
4958
|
}
|
|
4887
4959
|
} else {
|
|
4888
4960
|
this._applyPolyClip(first, R.PictOp.Src);
|
|
4961
|
+
const bb = this._polysBBox(first.polys);
|
|
4962
|
+
if (bb && bb.w > 0 && bb.h > 0) {
|
|
4963
|
+
const x = Math.max(0, Math.floor(bb.x));
|
|
4964
|
+
const y = Math.max(0, Math.floor(bb.y));
|
|
4965
|
+
const right = Math.min(this.width, Math.ceil(bb.x + bb.w));
|
|
4966
|
+
const bottom = Math.min(this.height, Math.ceil(bb.y + bb.h));
|
|
4967
|
+
if (right > x && bottom > y) {
|
|
4968
|
+
this._clipMaskInk = [x, y, right - x, bottom - y];
|
|
4969
|
+
}
|
|
4970
|
+
}
|
|
4889
4971
|
}
|
|
4890
4972
|
for (let i = 1; i < shapes.length; i++) this._intersectClip(shapes[i]);
|
|
4891
4973
|
}
|
|
@@ -5036,9 +5118,18 @@ class RenderingContext2d {
|
|
|
5036
5118
|
if (!rasterizer) return false;
|
|
5037
5119
|
const { shapes, edges } = fillableShapes(entry.polys);
|
|
5038
5120
|
if (!shapes.length) return false;
|
|
5039
|
-
//
|
|
5040
|
-
//
|
|
5041
|
-
|
|
5121
|
+
// A clip's coverage is rasterized here up to the policy's byte bound,
|
|
5122
|
+
// whatever its edges, where a fill is routed by its cost (`_routeFor`).
|
|
5123
|
+
// The server route is AddTraps into the pixmap the coverage lands on,
|
|
5124
|
+
// and AddTraps adds into a picture that exists: glamor — Xwayland's,
|
|
5125
|
+
// and Xorg's on most GPUs — has no GPU path for it, and reads the
|
|
5126
|
+
// pixmap back, rasterizes in software and uploads it again, a GPU sync
|
|
5127
|
+
// for every clip. A rounded row clipped that way took 5.6 ms on a virgl
|
|
5128
|
+
// Xwayland, where its raster here and a PutImage took 0.14; pixman,
|
|
5129
|
+
// which adds the traps itself, takes 0.03 ms less for it.
|
|
5130
|
+
const extent = this._polysExtent(entry.polys) ?? bb;
|
|
5131
|
+
const area = Math.min(extent.w, this.width) * Math.min(extent.h, this.height);
|
|
5132
|
+
if (!(area > 0 && area <= this.window.app.rasterPolicy.maxBytes) || !edges)
|
|
5042
5133
|
return false;
|
|
5043
5134
|
|
|
5044
5135
|
const coverage = rasterizer.rasterize({
|
|
@@ -5100,17 +5191,33 @@ class RenderingContext2d {
|
|
|
5100
5191
|
}
|
|
5101
5192
|
|
|
5102
5193
|
_rebuildClipMask() {
|
|
5103
|
-
// The stack changed shape (restore, resize):
|
|
5194
|
+
// The stack changed shape (restore, resize): let the mask go rather than
|
|
5104
5195
|
// rebuild it eagerly. A stack that went back to rectangles never needs
|
|
5105
5196
|
// one again, and one that still holds a poly rebuilds on first demand
|
|
5106
5197
|
// through _requireClipMask — same work, paid only if something draws.
|
|
5198
|
+
// It is kept, not destroyed, for the next path clip to fill again: a
|
|
5199
|
+
// pixmap the size of the surface was made and freed for every rounded
|
|
5200
|
+
// clip, sixteen a frame for a list of eight rounded rows, where the fill
|
|
5201
|
+
// mask beside it is kept for the context's life.
|
|
5107
5202
|
if (this.clipMask) {
|
|
5108
|
-
this.
|
|
5109
|
-
this.
|
|
5203
|
+
this._dropKeptClipMask();
|
|
5204
|
+
this._keptClipMask = {
|
|
5205
|
+
mask: this.clipMask,
|
|
5206
|
+
drawable: this.clipMaskDrawable,
|
|
5207
|
+
ink: this._clipMaskInk,
|
|
5208
|
+
};
|
|
5110
5209
|
this.clipMask = this.clipMaskDrawable = null;
|
|
5111
5210
|
}
|
|
5112
5211
|
}
|
|
5113
5212
|
|
|
5213
|
+
_dropKeptClipMask() {
|
|
5214
|
+
const kept = this._keptClipMask;
|
|
5215
|
+
if (!kept) return;
|
|
5216
|
+
this._keptClipMask = null;
|
|
5217
|
+
kept.mask.destroy();
|
|
5218
|
+
kept.drawable.destroy();
|
|
5219
|
+
}
|
|
5220
|
+
|
|
5114
5221
|
isPointInPath(...args) {
|
|
5115
5222
|
let path = null;
|
|
5116
5223
|
let rest = args;
|
|
@@ -5694,7 +5801,7 @@ class RenderingContext2d {
|
|
|
5694
5801
|
* coverage is composited through here as it is. Under a transform,
|
|
5695
5802
|
* `drawImage` places coverage with `_drawCoverageTurned` instead.
|
|
5696
5803
|
*/
|
|
5697
|
-
_drawCoverage(picture, sx, sy, sw, sh, dx, dy, dw, dh, op) {
|
|
5804
|
+
_drawCoverage(picture, sx, sy, sw, sh, dx, dy, dw, dh, op, kept = false) {
|
|
5698
5805
|
// Coverage the surface has none of is drawn as nothing. X takes the
|
|
5699
5806
|
// destination's corner in 16 bits, and a shadow thrown past it — an
|
|
5700
5807
|
// offset of 65,535, or text a million pixels away with its shadow —
|
|
@@ -5705,31 +5812,15 @@ class RenderingContext2d {
|
|
|
5705
5812
|
// whose transform collapsed paints nothing
|
|
5706
5813
|
if (!prepareStyle(this._backgroundPicture, this._m, this.width, this.height)) return;
|
|
5707
5814
|
const scaled = dw !== sw || dh !== sh;
|
|
5815
|
+
// the edge clamped when scaled, as drawImage's scaled path explains
|
|
5708
5816
|
if (scaled) {
|
|
5709
|
-
R
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
sx,
|
|
5713
|
-
0,
|
|
5714
|
-
sh / dh,
|
|
5715
|
-
sy,
|
|
5716
|
-
0,
|
|
5717
|
-
0,
|
|
5718
|
-
1,
|
|
5719
|
-
]);
|
|
5720
|
-
picture.setFilter("bilinear");
|
|
5721
|
-
// the edge clamped, as drawImage's scaled path explains
|
|
5722
|
-
R.ChangePicture(picture.id, { repeat: 2 }); // Repeat.Pad
|
|
5817
|
+
readAs(R, picture, [sw / dw, 0, sx, 0, sh / dh, sy, 0, 0, 1], "bilinear", 2);
|
|
5818
|
+
} else {
|
|
5819
|
+
readPlain(picture, kept);
|
|
5723
5820
|
}
|
|
5724
5821
|
// with a transform in place the mask is already sampled from (sx, sy)
|
|
5725
5822
|
this._paintCoverage(picture, scaled ? 0 : sx, scaled ? 0 : sy, dx, dy, dw, dh, op);
|
|
5726
|
-
|
|
5727
|
-
if (scaled) {
|
|
5728
|
-
// restore defaults so the cached surface stays reusable as-is
|
|
5729
|
-
R.SetPictureTransform(picture.id, [1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
5730
|
-
picture.setFilter("nearest");
|
|
5731
|
-
R.ChangePicture(picture.id, { repeat: 0 }); // Repeat.None
|
|
5732
|
-
}
|
|
5823
|
+
if (scaled) readDone(R, picture, kept, 2);
|
|
5733
5824
|
}
|
|
5734
5825
|
|
|
5735
5826
|
/**
|
|
@@ -5883,12 +5974,12 @@ class RenderingContext2d {
|
|
|
5883
5974
|
if (rect && !intersectBox(box, rect)) return;
|
|
5884
5975
|
if (!prepareStyle(this._backgroundPicture, this._m, this.width, this.height)) return;
|
|
5885
5976
|
const R = this.Render;
|
|
5886
|
-
const picture = image
|
|
5887
|
-
|
|
5888
|
-
|
|
5977
|
+
const { picture, kept } = sourcePicture(image, this.window.app);
|
|
5978
|
+
// no repeat: under a turn the box is the bounding box, and a padded
|
|
5979
|
+
// edge would paint its corners
|
|
5980
|
+
readAs(R, picture, transform, "bilinear", 0);
|
|
5889
5981
|
this._paintCoverage(picture, 0, 0, box.x, box.y, box.w, box.h, op);
|
|
5890
|
-
R
|
|
5891
|
-
picture.setFilter("nearest");
|
|
5982
|
+
readDone(R, picture, kept, 0);
|
|
5892
5983
|
}
|
|
5893
5984
|
|
|
5894
5985
|
/**
|
|
@@ -5941,31 +6032,19 @@ class RenderingContext2d {
|
|
|
5941
6032
|
}
|
|
5942
6033
|
|
|
5943
6034
|
if (image.format === "a8") {
|
|
5944
|
-
const picture = image
|
|
5945
|
-
this._drawCoverage(picture, sx, sy, sw, sh, dx, dy, dw, dh, op);
|
|
6035
|
+
const { picture, kept } = sourcePicture(image, app);
|
|
6036
|
+
this._drawCoverage(picture, sx, sy, sw, sh, dx, dy, dw, dh, op, kept);
|
|
5946
6037
|
return;
|
|
5947
6038
|
}
|
|
5948
6039
|
|
|
5949
6040
|
const state = this._beginDirectComposite({ x: dx, y: dy, w: dw, h: dh }, op);
|
|
5950
6041
|
if (state.empty) return;
|
|
5951
6042
|
// only now that the clip has had its say: a node-canvas uploads here
|
|
5952
|
-
const picture = image
|
|
6043
|
+
const { picture, kept } = sourcePicture(image, app);
|
|
5953
6044
|
const scaled = dw !== sw || dh !== sh;
|
|
5954
6045
|
if (scaled) {
|
|
5955
|
-
//
|
|
5956
|
-
// samples: dest pixel (i, j) reads (sx + i*sw/dw, sy + j*sh/dh)
|
|
5957
|
-
this.Render.SetPictureTransform(picture.id, [
|
|
5958
|
-
sw / dw,
|
|
5959
|
-
0,
|
|
5960
|
-
sx,
|
|
5961
|
-
0,
|
|
5962
|
-
sh / dh,
|
|
5963
|
-
sy,
|
|
5964
|
-
0,
|
|
5965
|
-
0,
|
|
5966
|
-
1,
|
|
5967
|
-
]);
|
|
5968
|
-
picture.setFilter("bilinear");
|
|
6046
|
+
// The picture transform maps composite coordinates into source
|
|
6047
|
+
// samples: dest pixel (i, j) reads (sx + i*sw/dw, sy + j*sh/dh).
|
|
5969
6048
|
// Bilinear sampling at the rim reads half a source pixel past the
|
|
5970
6049
|
// image, and a picture's default repeat reads transparent there: an
|
|
5971
6050
|
// upscaled image faded into whatever it was drawn over across its
|
|
@@ -5975,7 +6054,13 @@ class RenderingContext2d {
|
|
|
5975
6054
|
// composite box is the destination rectangle exactly: under a
|
|
5976
6055
|
// rotation the box is the bounding box, and pad would paint its
|
|
5977
6056
|
// corners.
|
|
5978
|
-
|
|
6057
|
+
readAs(
|
|
6058
|
+
this.Render,
|
|
6059
|
+
picture,
|
|
6060
|
+
[sw / dw, 0, sx, 0, sh / dh, sy, 0, 0, 1],
|
|
6061
|
+
"bilinear",
|
|
6062
|
+
2, // Repeat.Pad
|
|
6063
|
+
);
|
|
5979
6064
|
this._compositeOnto(
|
|
5980
6065
|
state.after,
|
|
5981
6066
|
op,
|
|
@@ -5990,14 +6075,9 @@ class RenderingContext2d {
|
|
|
5990
6075
|
dw,
|
|
5991
6076
|
dh,
|
|
5992
6077
|
);
|
|
5993
|
-
|
|
5994
|
-
this.Render.SetPictureTransform(
|
|
5995
|
-
picture.id,
|
|
5996
|
-
[1, 0, 0, 0, 1, 0, 0, 0, 1],
|
|
5997
|
-
);
|
|
5998
|
-
picture.setFilter("nearest");
|
|
5999
|
-
this.Render.ChangePicture(picture.id, { repeat: 0 }); // Repeat.None
|
|
6078
|
+
readDone(this.Render, picture, kept, 2);
|
|
6000
6079
|
} else {
|
|
6080
|
+
readPlain(picture, kept);
|
|
6001
6081
|
this._compositeOnto(
|
|
6002
6082
|
state.after,
|
|
6003
6083
|
op,
|
|
@@ -6230,9 +6310,11 @@ class RenderingContext2d {
|
|
|
6230
6310
|
format: coverage ? R.a8 : R.rgba32,
|
|
6231
6311
|
});
|
|
6232
6312
|
try {
|
|
6313
|
+
const { picture: from, kept } = sourcePicture(image, app);
|
|
6314
|
+
readPlain(from, kept);
|
|
6233
6315
|
R.Composite(
|
|
6234
6316
|
R.PictOp.Src,
|
|
6235
|
-
|
|
6317
|
+
from.id,
|
|
6236
6318
|
0,
|
|
6237
6319
|
picture.id,
|
|
6238
6320
|
x0,
|
|
@@ -6314,9 +6396,9 @@ class RenderingContext2d {
|
|
|
6314
6396
|
const direct = this._boxedComposite(bbox);
|
|
6315
6397
|
if (direct && !direct.box) return;
|
|
6316
6398
|
|
|
6317
|
-
const picture = image
|
|
6318
|
-
|
|
6319
|
-
picture
|
|
6399
|
+
const { picture, kept } = sourcePicture(image, this.window.app);
|
|
6400
|
+
// no repeat: the box is the bounding box of the turned rectangle
|
|
6401
|
+
readAs(this.Render, picture, transform, "bilinear", 0);
|
|
6320
6402
|
if (direct) {
|
|
6321
6403
|
const box = direct.box;
|
|
6322
6404
|
this.Render.Composite(
|
|
@@ -6336,8 +6418,7 @@ class RenderingContext2d {
|
|
|
6336
6418
|
} else {
|
|
6337
6419
|
this._compositeMasked(op, picture.id, 0, 0, bbox);
|
|
6338
6420
|
}
|
|
6339
|
-
this.Render
|
|
6340
|
-
picture.setFilter("nearest");
|
|
6421
|
+
readDone(this.Render, picture, kept, 0);
|
|
6341
6422
|
this._markDirty();
|
|
6342
6423
|
}
|
|
6343
6424
|
}
|
package/lib/shadow.js
CHANGED
|
@@ -118,7 +118,7 @@ function halveCoverage(surface, R) {
|
|
|
118
118
|
const small = new Surface(surface.app, { width, height, format: 'a8' });
|
|
119
119
|
const picture = surface.picture();
|
|
120
120
|
picture.setFilter('bilinear');
|
|
121
|
-
|
|
121
|
+
picture.setTransform([2, 0, 0, 0, 2, 0, 0, 0, 1]);
|
|
122
122
|
R.Composite(R.PictOp.Src, picture.id, 0, small.picture().id, 0, 0, 0, 0, 0, 0, width, height);
|
|
123
123
|
surface.destroy();
|
|
124
124
|
return small;
|
|
@@ -143,7 +143,7 @@ function expandCoverage(small, width, height, scale, R) {
|
|
|
143
143
|
const out = new Surface(small.app, { width, height, format: 'a8' });
|
|
144
144
|
const picture = small.picture();
|
|
145
145
|
picture.setFilter('bilinear');
|
|
146
|
-
|
|
146
|
+
picture.setTransform([1 / scale, 0, 0, 0, 1 / scale, 0, 0, 0, 1]);
|
|
147
147
|
R.Composite(R.PictOp.Src, picture.id, 0, out.picture().id, 0, 0, 0, 0, 0, 0, width, height);
|
|
148
148
|
small.destroy();
|
|
149
149
|
return out;
|
package/lib/surface.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Picture from './picture.js';
|
|
1
|
+
import Picture, { OWN_PICTURE } from './picture.js';
|
|
2
2
|
import Pixmap from './pixmap.js';
|
|
3
3
|
import { safeRelease } from './cleanup.js';
|
|
4
4
|
|
|
@@ -62,10 +62,16 @@ export class Surface {
|
|
|
62
62
|
return this.width * this.height * (this.depth === 8 ? 1 : 4);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/** The server-side Picture, on the app this surface belongs to
|
|
65
|
+
/** The server-side Picture, on the app this surface belongs to, read as
|
|
66
|
+
* stored: no transform, the `nearest` filter, no repeat. Mirrors
|
|
66
67
|
* `Image.picture(app)` so `drawImage` can take either without asking which
|
|
67
68
|
* it has. */
|
|
68
69
|
picture(app) {
|
|
70
|
+
return this[OWN_PICTURE](app)._settle();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The picture as ntk's draws left it (`drawImage` sets what it needs). */
|
|
74
|
+
[OWN_PICTURE](app) {
|
|
69
75
|
if (app && app !== this.app) {
|
|
70
76
|
throw new Error('Surface: belongs to a different X connection');
|
|
71
77
|
}
|