ntk 8.23.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 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). Used by `ctx.drawImage`; useful directly
35
- * when compositing manually via the Render extension.
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.picture(app);
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
- surface.picture(this.window.app),
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
- const picture = tile.picture(app);
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
- // a piece drawn at its own size reads the tile as it is: undo the
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.SetPictureTransform(picture.id, [
5710
- sw / dw,
5711
- 0,
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.picture(this.window.app);
5887
- R.SetPictureTransform(picture.id, transform);
5888
- picture.setFilter("bilinear");
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.SetPictureTransform(picture.id, IDENTITY_TRANSFORM);
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.picture(app);
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.picture(app);
5984
+ const { picture, kept } = sourcePicture(image, app);
5953
5985
  const scaled = dw !== sw || dh !== sh;
5954
5986
  if (scaled) {
5955
- // the picture transform maps composite coordinates into source
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
- this.Render.ChangePicture(picture.id, { repeat: 2 }); // Repeat.Pad
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
- // restore defaults so the cached upload can be reused as-is
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
- image.picture(app).id,
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.picture(this.window.app);
6318
- this.Render.SetPictureTransform(picture.id, transform);
6319
- picture.setFilter("bilinear");
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.SetPictureTransform(picture.id, [1, 0, 0, 0, 1, 0, 0, 0, 1]);
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
- R.SetPictureTransform(picture.id, [2, 0, 0, 0, 2, 0, 0, 0, 1]);
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
- R.SetPictureTransform(picture.id, [1 / scale, 0, 0, 0, 1 / scale, 0, 0, 0, 1]);
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. Mirrors
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.23.0",
3
+ "version": "8.23.1",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",