ntk 8.22.0 → 8.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/image.js +11 -4
- package/lib/picture.js +92 -1
- package/lib/renderingcontext_2d.js +93 -71
- package/lib/shadow.js +2 -2
- package/lib/surface.js +8 -2
- package/lib/text/fontmanager.js +10 -1
- package/lib/text/layout.js +91 -2
- package/lib/text/shape.js +16 -3
- 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" +
|
|
@@ -1586,8 +1638,9 @@ class RenderingContext2d {
|
|
|
1586
1638
|
const source = this._fillSource;
|
|
1587
1639
|
this.fillStyle = this._shadowColor;
|
|
1588
1640
|
try {
|
|
1641
|
+
const { picture, kept } = sourcePicture(surface, this.window.app);
|
|
1589
1642
|
this._drawCoverage(
|
|
1590
|
-
|
|
1643
|
+
picture,
|
|
1591
1644
|
0,
|
|
1592
1645
|
0,
|
|
1593
1646
|
surface.width,
|
|
@@ -1597,6 +1650,7 @@ class RenderingContext2d {
|
|
|
1597
1650
|
surface.width,
|
|
1598
1651
|
surface.height,
|
|
1599
1652
|
this._op(),
|
|
1653
|
+
kept,
|
|
1600
1654
|
);
|
|
1601
1655
|
} finally {
|
|
1602
1656
|
this._fillStyle = style;
|
|
@@ -1992,7 +2046,10 @@ class RenderingContext2d {
|
|
|
1992
2046
|
this._shadowTile(plan),
|
|
1993
2047
|
);
|
|
1994
2048
|
if (!tile) return false;
|
|
1995
|
-
|
|
2049
|
+
// A piece drawn at its own size reads the tile as stored and a stretched
|
|
2050
|
+
// one through its scale: `_drawCoverage` sets each, and sends only what
|
|
2051
|
+
// the piece before did not leave.
|
|
2052
|
+
const { picture, kept } = sourcePicture(tile, app);
|
|
1996
2053
|
// put back as it was, as _paintShadow does
|
|
1997
2054
|
const style = this._fillStyle;
|
|
1998
2055
|
const source = this._fillSource;
|
|
@@ -2003,16 +2060,7 @@ class RenderingContext2d {
|
|
|
2003
2060
|
if (clip && !boxesMeet({ x: dx, y: dy, width: dw, height: dh }, clip)) {
|
|
2004
2061
|
continue;
|
|
2005
2062
|
}
|
|
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);
|
|
2063
|
+
this._drawCoverage(picture, sx, sy, sw, sh, dx, dy, dw, dh, op, kept);
|
|
2016
2064
|
}
|
|
2017
2065
|
} finally {
|
|
2018
2066
|
this._fillStyle = style;
|
|
@@ -5694,7 +5742,7 @@ class RenderingContext2d {
|
|
|
5694
5742
|
* coverage is composited through here as it is. Under a transform,
|
|
5695
5743
|
* `drawImage` places coverage with `_drawCoverageTurned` instead.
|
|
5696
5744
|
*/
|
|
5697
|
-
_drawCoverage(picture, sx, sy, sw, sh, dx, dy, dw, dh, op) {
|
|
5745
|
+
_drawCoverage(picture, sx, sy, sw, sh, dx, dy, dw, dh, op, kept = false) {
|
|
5698
5746
|
// Coverage the surface has none of is drawn as nothing. X takes the
|
|
5699
5747
|
// destination's corner in 16 bits, and a shadow thrown past it — an
|
|
5700
5748
|
// offset of 65,535, or text a million pixels away with its shadow —
|
|
@@ -5705,31 +5753,15 @@ class RenderingContext2d {
|
|
|
5705
5753
|
// whose transform collapsed paints nothing
|
|
5706
5754
|
if (!prepareStyle(this._backgroundPicture, this._m, this.width, this.height)) return;
|
|
5707
5755
|
const scaled = dw !== sw || dh !== sh;
|
|
5756
|
+
// the edge clamped when scaled, as drawImage's scaled path explains
|
|
5708
5757
|
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
|
|
5758
|
+
readAs(R, picture, [sw / dw, 0, sx, 0, sh / dh, sy, 0, 0, 1], "bilinear", 2);
|
|
5759
|
+
} else {
|
|
5760
|
+
readPlain(picture, kept);
|
|
5723
5761
|
}
|
|
5724
5762
|
// with a transform in place the mask is already sampled from (sx, sy)
|
|
5725
5763
|
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
|
-
}
|
|
5764
|
+
if (scaled) readDone(R, picture, kept, 2);
|
|
5733
5765
|
}
|
|
5734
5766
|
|
|
5735
5767
|
/**
|
|
@@ -5883,12 +5915,12 @@ class RenderingContext2d {
|
|
|
5883
5915
|
if (rect && !intersectBox(box, rect)) return;
|
|
5884
5916
|
if (!prepareStyle(this._backgroundPicture, this._m, this.width, this.height)) return;
|
|
5885
5917
|
const R = this.Render;
|
|
5886
|
-
const picture = image
|
|
5887
|
-
|
|
5888
|
-
|
|
5918
|
+
const { picture, kept } = sourcePicture(image, this.window.app);
|
|
5919
|
+
// no repeat: under a turn the box is the bounding box, and a padded
|
|
5920
|
+
// edge would paint its corners
|
|
5921
|
+
readAs(R, picture, transform, "bilinear", 0);
|
|
5889
5922
|
this._paintCoverage(picture, 0, 0, box.x, box.y, box.w, box.h, op);
|
|
5890
|
-
R
|
|
5891
|
-
picture.setFilter("nearest");
|
|
5923
|
+
readDone(R, picture, kept, 0);
|
|
5892
5924
|
}
|
|
5893
5925
|
|
|
5894
5926
|
/**
|
|
@@ -5941,31 +5973,19 @@ class RenderingContext2d {
|
|
|
5941
5973
|
}
|
|
5942
5974
|
|
|
5943
5975
|
if (image.format === "a8") {
|
|
5944
|
-
const picture = image
|
|
5945
|
-
this._drawCoverage(picture, sx, sy, sw, sh, dx, dy, dw, dh, op);
|
|
5976
|
+
const { picture, kept } = sourcePicture(image, app);
|
|
5977
|
+
this._drawCoverage(picture, sx, sy, sw, sh, dx, dy, dw, dh, op, kept);
|
|
5946
5978
|
return;
|
|
5947
5979
|
}
|
|
5948
5980
|
|
|
5949
5981
|
const state = this._beginDirectComposite({ x: dx, y: dy, w: dw, h: dh }, op);
|
|
5950
5982
|
if (state.empty) return;
|
|
5951
5983
|
// only now that the clip has had its say: a node-canvas uploads here
|
|
5952
|
-
const picture = image
|
|
5984
|
+
const { picture, kept } = sourcePicture(image, app);
|
|
5953
5985
|
const scaled = dw !== sw || dh !== sh;
|
|
5954
5986
|
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");
|
|
5987
|
+
// The picture transform maps composite coordinates into source
|
|
5988
|
+
// samples: dest pixel (i, j) reads (sx + i*sw/dw, sy + j*sh/dh).
|
|
5969
5989
|
// Bilinear sampling at the rim reads half a source pixel past the
|
|
5970
5990
|
// image, and a picture's default repeat reads transparent there: an
|
|
5971
5991
|
// upscaled image faded into whatever it was drawn over across its
|
|
@@ -5975,7 +5995,13 @@ class RenderingContext2d {
|
|
|
5975
5995
|
// composite box is the destination rectangle exactly: under a
|
|
5976
5996
|
// rotation the box is the bounding box, and pad would paint its
|
|
5977
5997
|
// corners.
|
|
5978
|
-
|
|
5998
|
+
readAs(
|
|
5999
|
+
this.Render,
|
|
6000
|
+
picture,
|
|
6001
|
+
[sw / dw, 0, sx, 0, sh / dh, sy, 0, 0, 1],
|
|
6002
|
+
"bilinear",
|
|
6003
|
+
2, // Repeat.Pad
|
|
6004
|
+
);
|
|
5979
6005
|
this._compositeOnto(
|
|
5980
6006
|
state.after,
|
|
5981
6007
|
op,
|
|
@@ -5990,14 +6016,9 @@ class RenderingContext2d {
|
|
|
5990
6016
|
dw,
|
|
5991
6017
|
dh,
|
|
5992
6018
|
);
|
|
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
|
|
6019
|
+
readDone(this.Render, picture, kept, 2);
|
|
6000
6020
|
} else {
|
|
6021
|
+
readPlain(picture, kept);
|
|
6001
6022
|
this._compositeOnto(
|
|
6002
6023
|
state.after,
|
|
6003
6024
|
op,
|
|
@@ -6230,9 +6251,11 @@ class RenderingContext2d {
|
|
|
6230
6251
|
format: coverage ? R.a8 : R.rgba32,
|
|
6231
6252
|
});
|
|
6232
6253
|
try {
|
|
6254
|
+
const { picture: from, kept } = sourcePicture(image, app);
|
|
6255
|
+
readPlain(from, kept);
|
|
6233
6256
|
R.Composite(
|
|
6234
6257
|
R.PictOp.Src,
|
|
6235
|
-
|
|
6258
|
+
from.id,
|
|
6236
6259
|
0,
|
|
6237
6260
|
picture.id,
|
|
6238
6261
|
x0,
|
|
@@ -6314,9 +6337,9 @@ class RenderingContext2d {
|
|
|
6314
6337
|
const direct = this._boxedComposite(bbox);
|
|
6315
6338
|
if (direct && !direct.box) return;
|
|
6316
6339
|
|
|
6317
|
-
const picture = image
|
|
6318
|
-
|
|
6319
|
-
picture
|
|
6340
|
+
const { picture, kept } = sourcePicture(image, this.window.app);
|
|
6341
|
+
// no repeat: the box is the bounding box of the turned rectangle
|
|
6342
|
+
readAs(this.Render, picture, transform, "bilinear", 0);
|
|
6320
6343
|
if (direct) {
|
|
6321
6344
|
const box = direct.box;
|
|
6322
6345
|
this.Render.Composite(
|
|
@@ -6336,8 +6359,7 @@ class RenderingContext2d {
|
|
|
6336
6359
|
} else {
|
|
6337
6360
|
this._compositeMasked(op, picture.id, 0, 0, bbox);
|
|
6338
6361
|
}
|
|
6339
|
-
this.Render
|
|
6340
|
-
picture.setFilter("nearest");
|
|
6362
|
+
readDone(this.Render, picture, kept, 0);
|
|
6341
6363
|
this._markDirty();
|
|
6342
6364
|
}
|
|
6343
6365
|
}
|
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
|
}
|
package/lib/text/fontmanager.js
CHANGED
|
@@ -565,6 +565,15 @@ export default class FontManager {
|
|
|
565
565
|
return new TextLayout(this, content, style, options);
|
|
566
566
|
}
|
|
567
567
|
|
|
568
|
+
/**
|
|
569
|
+
* Whether `layout` takes `justify`, and fills a line with its word
|
|
570
|
+
* separators itself. True; a caller that asks before it passes the option
|
|
571
|
+
* keeps its own way of justifying for an engine without it.
|
|
572
|
+
*/
|
|
573
|
+
get justifies() {
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
|
|
568
577
|
/**
|
|
569
578
|
* Memoized shaping used by TextLayout and the canvas text path, bounded at
|
|
570
579
|
* two generations of `SHAPE_GENERATION` words. `levelsKey` is a compact
|
|
@@ -671,7 +680,7 @@ export default class FontManager {
|
|
|
671
680
|
*/
|
|
672
681
|
_shapeCachedWhole(text, style) {
|
|
673
682
|
const emb = embeddingLevels(text, style.direction);
|
|
674
|
-
const baseLevel = emb.paragraphs
|
|
683
|
+
const baseLevel = emb.paragraphs[0].level;
|
|
675
684
|
const shaped = this._shapeCached(text, style, normalizedLevels(emb.levels, 0, text.length));
|
|
676
685
|
return shaped.baseLevel === baseLevel ? shaped : { ...shaped, baseLevel };
|
|
677
686
|
}
|
package/lib/text/layout.js
CHANGED
|
@@ -12,6 +12,29 @@ const WS = new Set([0x20, 0x09]);
|
|
|
12
12
|
const HARD_BREAKS = /[\n\r\u2028\u2029]+$/;
|
|
13
13
|
const TRAILING_WS = /[ \t]+$/;
|
|
14
14
|
|
|
15
|
+
// The word separators a justified line's room is shared out at (CSS Text 3,
|
|
16
|
+
// 7.4, `text-justify: auto` as a browser has it for the scripts that space
|
|
17
|
+
// their words): a space and a no-break space.
|
|
18
|
+
const SEPARATORS = new Set([0x20, 0xa0]);
|
|
19
|
+
const separates = (g) => g.codePoints.length === 1 && SEPARATORS.has(g.codePoints[0]);
|
|
20
|
+
|
|
21
|
+
// What ends a line `justify` sets as it does a paragraph's last: a line feed,
|
|
22
|
+
// a carriage return or a paragraph separator, as a browser's forced break
|
|
23
|
+
// does. A line separator, U+2028, is not one, so a caller can ask a line to
|
|
24
|
+
// break there and be justified.
|
|
25
|
+
const JUSTIFY_STOPS = /[\n\r\u2029]$/;
|
|
26
|
+
|
|
27
|
+
// `justify`'s lines as bits: those that go on to another (REST), and the
|
|
28
|
+
// paragraph's last with those a forced break ends (LAST).
|
|
29
|
+
const REST = 1;
|
|
30
|
+
const LAST = 2;
|
|
31
|
+
function justifyBits(justify) {
|
|
32
|
+
if (justify === true || justify === 'rest') return REST;
|
|
33
|
+
if (justify === 'last') return LAST;
|
|
34
|
+
if (justify === 'all') return REST | LAST;
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
15
38
|
/** Scripts written without spaces between words, whose line breaking
|
|
16
39
|
* needs a dictionary (UAX #14's SA class, and Javanese, Balinese and
|
|
17
40
|
* Buginese, which are AL there): a token of theirs is a run of words. */
|
|
@@ -162,6 +185,44 @@ function isWsGlyph(g) {
|
|
|
162
185
|
return g.codePoints.length > 0 && g.codePoints.every((cp) => WS.has(cp));
|
|
163
186
|
}
|
|
164
187
|
|
|
188
|
+
/**
|
|
189
|
+
* A line set to fill `width`: what it leaves of it shared equally among its
|
|
190
|
+
* word separators (`SEPARATORS`), each glyph's advance widened by its share
|
|
191
|
+
* — the white space it ends on hangs, and takes none. Its runs are made
|
|
192
|
+
* anew, those with a separator in them over glyphs of their own, since a
|
|
193
|
+
* shaped run is the paragraph's and shared by every layout of it; caret
|
|
194
|
+
* positions, hit testing, drawing and coverage all read the advances, and
|
|
195
|
+
* agree. A line with no separator, or with no room left, keeps its
|
|
196
|
+
* alignment. Answers the line's width.
|
|
197
|
+
*/
|
|
198
|
+
function justifyLine(line, width) {
|
|
199
|
+
const free = width - line.width;
|
|
200
|
+
if (!(free > 0)) return line.width;
|
|
201
|
+
let count = 0;
|
|
202
|
+
for (const r of line.runs) {
|
|
203
|
+
for (const g of r.run.glyphs) if (separates(g)) count++;
|
|
204
|
+
}
|
|
205
|
+
if (count === 0) return line.width;
|
|
206
|
+
const extra = free / count;
|
|
207
|
+
let shift = 0;
|
|
208
|
+
line.runs = line.runs.map((r) => {
|
|
209
|
+
const moved = shift ? { ...r, x: r.x + shift } : r;
|
|
210
|
+
let k = 0;
|
|
211
|
+
for (const g of r.run.glyphs) if (separates(g)) k++;
|
|
212
|
+
if (k === 0) return moved;
|
|
213
|
+
shift += k * extra;
|
|
214
|
+
const glyphs = r.run.glyphs.map((g) => (separates(g) ? { ...g, ax: g.ax + extra } : g));
|
|
215
|
+
return {
|
|
216
|
+
...moved,
|
|
217
|
+
width: r.width + k * extra,
|
|
218
|
+
run: { ...r.run, glyphs, width: r.run.width + k * extra }
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
line.width += count * extra;
|
|
222
|
+
line.advance += count * extra;
|
|
223
|
+
return line.width;
|
|
224
|
+
}
|
|
225
|
+
|
|
165
226
|
/**
|
|
166
227
|
* Multi-line text layout: breaks (possibly styled) text into lines for a
|
|
167
228
|
* target container width, with full shaping — kerning, ligatures, complex
|
|
@@ -205,6 +266,13 @@ function isWsGlyph(g) {
|
|
|
205
266
|
* past the line's end, as CSS's `overflow-wrap: normal` does — but for
|
|
206
267
|
* text in a script written without spaces (Thai, Khmer, Javanese…), whose
|
|
207
268
|
* words the breaker cannot find, which is cut as a fallback
|
|
269
|
+
* - `justify` — the lines set to fill `maxWidth`, what each leaves of it
|
|
270
|
+
* shared equally among its word separators, a space and a no-break space:
|
|
271
|
+
* `true` (or `'rest'`) for every line but the paragraph's last and each a
|
|
272
|
+
* forced break ends, as CSS's `text-align: justify`; `'last'` for only
|
|
273
|
+
* those; `'all'` for every line. A line with no separator, or none of the
|
|
274
|
+
* width to spare, keeps `align`'s place, as do the lines not justified.
|
|
275
|
+
* The breaks are the layout's at its width; justifying moves none
|
|
208
276
|
* - `fit` — 'items' fits and measures a line as a browser does: each span's
|
|
209
277
|
* part of it rounded up to a 64th of a pixel before it is added, as Blink
|
|
210
278
|
* rounds an element's text on a line up to a LayoutUnit; a line's `width`
|
|
@@ -436,6 +504,11 @@ export class TextLayout {
|
|
|
436
504
|
natural = m.lineHeight;
|
|
437
505
|
}
|
|
438
506
|
if (runs.length) fitted += round(item);
|
|
507
|
+
// a line's place in the paragraph, for `justify`: its last, which a
|
|
508
|
+
// `maxLines` cut leaves no line, or one a forced break ends
|
|
509
|
+
const ends =
|
|
510
|
+
(li === lineTokens.length - 1 && !this.truncated) ||
|
|
511
|
+
JUSTIFY_STOPS.test(text.slice(Math.max(lineStart, lineEnd - 1), lineEnd));
|
|
439
512
|
if (fitted > layoutWidth) layoutWidth = fitted;
|
|
440
513
|
// Half-leading (CSS Inline Layout 3): the slack between the glyphs and
|
|
441
514
|
// the line box is split evenly above and below, rather than all of it
|
|
@@ -462,11 +535,27 @@ export class TextLayout {
|
|
|
462
535
|
start: lineStart,
|
|
463
536
|
end: lineEnd,
|
|
464
537
|
_contentEnd: contentEnd,
|
|
465
|
-
_trailing: trailing
|
|
538
|
+
_trailing: trailing,
|
|
539
|
+
_ends: ends,
|
|
540
|
+
_elided: !!ellipsis
|
|
466
541
|
});
|
|
467
542
|
y += box;
|
|
468
543
|
}
|
|
469
544
|
|
|
545
|
+
// ---- justification ----
|
|
546
|
+
// After the lines are broken, so a width the lines are set at is no
|
|
547
|
+
// shaping: a paragraph laid out again at another width breaks the glyphs
|
|
548
|
+
// it has into other lines and spaces them, where one that spaced its
|
|
549
|
+
// words through letter spacing was shaped again at every width.
|
|
550
|
+
const justify = justifyBits(options.justify);
|
|
551
|
+
if (justify && Number.isFinite(maxWidth)) {
|
|
552
|
+
for (const line of this.lines) {
|
|
553
|
+
if (line._elided || !(justify & (line._ends ? LAST : REST))) continue;
|
|
554
|
+
const width = justifyLine(line, maxWidth);
|
|
555
|
+
if (width > layoutWidth) layoutWidth = width;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
470
559
|
this.width = layoutWidth;
|
|
471
560
|
this.height = y;
|
|
472
561
|
|
|
@@ -537,7 +626,7 @@ export class TextLayout {
|
|
|
537
626
|
const text = spans.map((s) => s.text).join('');
|
|
538
627
|
const emb = embeddingLevels(text, direction);
|
|
539
628
|
const levels = emb.levels;
|
|
540
|
-
const baseLevel = emb.paragraphs
|
|
629
|
+
const baseLevel = emb.paragraphs[0].level & 1;
|
|
541
630
|
|
|
542
631
|
// span boundaries, for binary search by char position
|
|
543
632
|
const spanStarts = [];
|
package/lib/text/shape.js
CHANGED
|
@@ -21,17 +21,30 @@ function neverSwitches(cp) {
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* UAX#9 embedding levels for a string. `direction` is 'ltr', 'rtl' or 'auto'
|
|
24
|
-
* (first-strong detection). Returns bidi-js's result object
|
|
24
|
+
* (first-strong detection). Returns bidi-js's result object, which always
|
|
25
|
+
* has a first paragraph: its level is the text's base level.
|
|
25
26
|
*/
|
|
26
27
|
export function embeddingLevels(text, direction = 'auto') {
|
|
27
28
|
const explicit = direction === 'ltr' || direction === 'rtl' ? direction : null;
|
|
29
|
+
// An empty text is a paragraph too, at the level its direction names.
|
|
30
|
+
// bidi-js finds no paragraph in it, and a base level read as 0 for want
|
|
31
|
+
// of one set an empty right-to-left field's line, and the caret on it,
|
|
32
|
+
// at the left — where the same field holding only a line break had it
|
|
33
|
+
// at the right. Under 'auto' there is no strong character to find, and
|
|
34
|
+
// the paragraph is left-to-right (UAX#9 P3).
|
|
35
|
+
if (!text.length) {
|
|
36
|
+
return {
|
|
37
|
+
levels: new Uint8Array(0),
|
|
38
|
+
paragraphs: [{ start: 0, end: -1, level: explicit === 'rtl' ? 1 : 0 }]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
28
41
|
// Text that nothing can make right-to-left is level 0 throughout, which
|
|
29
42
|
// is what the algorithm would work out a character at a time: every
|
|
30
43
|
// layout asks, and most text is Latin, CJK or digits with no bidi at all.
|
|
31
44
|
if (explicit !== 'rtl' && !MAY_REVERSE.test(text)) {
|
|
32
45
|
return {
|
|
33
46
|
levels: new Uint8Array(text.length),
|
|
34
|
-
paragraphs:
|
|
47
|
+
paragraphs: [{ start: 0, end: text.length - 1, level: 0 }]
|
|
35
48
|
};
|
|
36
49
|
}
|
|
37
50
|
return bidi.getEmbeddingLevels(text, explicit);
|
|
@@ -161,7 +174,7 @@ export function shapeText(fonts, text, style, levels) {
|
|
|
161
174
|
if (!levels) {
|
|
162
175
|
const emb = embeddingLevels(text, style.direction);
|
|
163
176
|
levels = emb.levels;
|
|
164
|
-
baseLevel = emb.paragraphs
|
|
177
|
+
baseLevel = emb.paragraphs[0].level;
|
|
165
178
|
} else {
|
|
166
179
|
baseLevel = levels.length ? levels[0] & ~1 : 0;
|
|
167
180
|
}
|