ntk 7.6.1 → 8.0.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/README.md +9 -10
- package/lib/image.js +12 -0
- package/lib/index.js +9 -30
- package/lib/maskcluster.js +191 -0
- package/lib/renderingcontext_2d.js +590 -175
- package/lib/text/layout.js +4 -4
- package/lib/widgets/svgview.js +5 -4
- package/package.json +4 -9
- package/lib/widgets/css.js +0 -673
- package/lib/widgets/highlight.js +0 -97
- package/lib/widgets/htmlview.js +0 -893
- package/lib/widgets/markdown.js +0 -148
- package/lib/widgets/markdownview.js +0 -492
- package/lib/widgets/tex.js +0 -679
- package/lib/yoga.js +0 -119
package/README.md
CHANGED
|
@@ -53,18 +53,17 @@ rasterizer and cached server-side as XRender glyphs, so drawing a line of
|
|
|
53
53
|
text costs about a byte per glyph on the wire. Font names resolve through
|
|
54
54
|
fontconfig (`fc-match`). Very large and continuously animated sizes render
|
|
55
55
|
as server-side trapezoids instead of cached bitmaps. A `TextLayout` engine
|
|
56
|
-
wraps styled text to a target width
|
|
57
|
-
markdown (with syntax-highlighted code fences and KaTeX math via
|
|
58
|
-
`TexView`) on top of it — see [docs/text.md](docs/text.md) and
|
|
59
|
-
[docs/tex.md](docs/tex.md).
|
|
56
|
+
wraps styled text to a target width — see [docs/text.md](docs/text.md).
|
|
60
57
|
|
|
61
58
|
PNG/JPEG images decode client-side (`loadImage`) and composite server-side
|
|
62
|
-
via `ctx.drawImage` ([docs/images.md](docs/images.md)). An `
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
via `ctx.drawImage` ([docs/images.md](docs/images.md)). An `SvgView` widget
|
|
60
|
+
renders static SVG (shapes, gradients, transforms, `use`) through the same
|
|
61
|
+
2d pipeline ([docs/svg.md](docs/svg.md)).
|
|
62
|
+
|
|
63
|
+
Rendering documents — markdown, formulas, rich text — is not ntk's job:
|
|
64
|
+
it draws, and a document is a tree of layout decisions on top of that.
|
|
65
|
+
[`@react-x11/components`](https://github.com/sidorares/react-x11-components)
|
|
66
|
+
is where those live, over the react-x11 renderer.
|
|
68
67
|
|
|
69
68
|
```js
|
|
70
69
|
import { createClient } from 'ntk';
|
package/lib/image.js
CHANGED
|
@@ -79,6 +79,18 @@ export class Image {
|
|
|
79
79
|
return picture;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* The Pixmap those pixels live in on `app` (uploading on first use, like
|
|
84
|
+
* `picture`). What it is for is building a *second* Picture over the same
|
|
85
|
+
* upload — `ctx.createPattern` needs a repeating one, and changing the
|
|
86
|
+
* cached picture's attributes instead would change how `drawImage` samples
|
|
87
|
+
* this image everywhere else.
|
|
88
|
+
*/
|
|
89
|
+
pixmap(app) {
|
|
90
|
+
this.picture(app);
|
|
91
|
+
return this._uploads.get(app).pixmap;
|
|
92
|
+
}
|
|
93
|
+
|
|
82
94
|
/** free server-side copies of this image (safe to draw again afterwards) */
|
|
83
95
|
destroy() {
|
|
84
96
|
for (const { pixmap, picture } of this._uploads.values()) {
|
package/lib/index.js
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
encodeXEmbedInfo,
|
|
17
17
|
readXEmbedInfo
|
|
18
18
|
} from './xembed.js';
|
|
19
|
-
import { loadLayout } from './yoga.js';
|
|
20
19
|
import { decodeKey, groupForState } from './keyboard.js';
|
|
21
20
|
import Pixmap from './pixmap.js';
|
|
22
21
|
import Picture from './picture.js';
|
|
@@ -47,21 +46,17 @@ import {
|
|
|
47
46
|
defaultRasterizer,
|
|
48
47
|
setDefaultRasterizer
|
|
49
48
|
} from './rasterize.js';
|
|
49
|
+
import { DEFAULT_MASK_POLICY } from './maskcluster.js';
|
|
50
50
|
import { DEFAULT_SHAPE_POLICY } from './shapeglyphs.js';
|
|
51
51
|
import { TextLayout } from './text/layout.js';
|
|
52
|
-
import HtmlView from './widgets/htmlview.js';
|
|
53
52
|
import SvgView from './widgets/svgview.js';
|
|
54
|
-
import
|
|
55
|
-
import TexView, { configureTex, layoutTex, TexBox } from './widgets/tex.js';
|
|
56
|
-
import { tokenize as highlightCode } from './widgets/highlight.js';
|
|
57
|
-
import { cssColorStraight, premultiply } from './color.js';
|
|
58
|
-
import { cssColor, cssLength } from './widgets/css.js';
|
|
53
|
+
import { cssColor, cssColorStraight, premultiply } from './color.js';
|
|
59
54
|
|
|
60
55
|
// rendering context modules register themselves on Drawable. The direct one
|
|
61
56
|
// comes last on purpose: it wraps the 'opengl' factory the indirect one just
|
|
62
57
|
// registered, so that the backend-neutral name can dispatch on glPolicy.
|
|
63
58
|
import './renderingcontext_x11.js';
|
|
64
|
-
import './renderingcontext_2d.js';
|
|
59
|
+
import { CanvasGradient, CanvasPattern } from './renderingcontext_2d.js';
|
|
65
60
|
import './renderingcontext_opengl.js';
|
|
66
61
|
import './renderingcontext_gles.js';
|
|
67
62
|
|
|
@@ -115,11 +110,7 @@ export function createClient(options, callback) {
|
|
|
115
110
|
const x11Options = { ...(options || {}) };
|
|
116
111
|
if (x11Options.bufferRequests === undefined) x11Options.bufferRequests = DEFAULT_BUFFER_REQUESTS;
|
|
117
112
|
|
|
118
|
-
|
|
119
|
-
// usable synchronously by the time the App exists (see lib/yoga.js)
|
|
120
|
-
const layout = loadLayout();
|
|
121
|
-
|
|
122
|
-
const connecting = new Promise((resolve, reject) => {
|
|
113
|
+
const promise = new Promise((resolve, reject) => {
|
|
123
114
|
// Resolve the font spec here rather than lazily in `app.fonts`, so a
|
|
124
115
|
// missing directory is a rejected connect instead of a surprise inside
|
|
125
116
|
// the first paint. Inside the executor so it rejects rather than throws
|
|
@@ -205,8 +196,6 @@ export function createClient(options, callback) {
|
|
|
205
196
|
});
|
|
206
197
|
});
|
|
207
198
|
|
|
208
|
-
const promise = Promise.all([connecting, layout]).then(([app]) => app);
|
|
209
|
-
|
|
210
199
|
if (callback) {
|
|
211
200
|
promise.then(
|
|
212
201
|
(app) => callback(null, app),
|
|
@@ -237,6 +226,10 @@ export {
|
|
|
237
226
|
Pixmap,
|
|
238
227
|
Picture,
|
|
239
228
|
Surface,
|
|
229
|
+
// fill/stroke styles the 2d context hands back (docs/context-2d.md) —
|
|
230
|
+
// exported for `instanceof`, not to be constructed directly
|
|
231
|
+
CanvasGradient,
|
|
232
|
+
CanvasPattern,
|
|
240
233
|
Image,
|
|
241
234
|
ImageData,
|
|
242
235
|
pixelLayout,
|
|
@@ -260,20 +253,13 @@ export {
|
|
|
260
253
|
defaultRasterizer,
|
|
261
254
|
setDefaultRasterizer,
|
|
262
255
|
DEFAULT_RASTER_POLICY,
|
|
256
|
+
DEFAULT_MASK_POLICY,
|
|
263
257
|
DEFAULT_SHAPE_POLICY,
|
|
264
258
|
TextLayout,
|
|
265
|
-
HtmlView,
|
|
266
259
|
SvgView,
|
|
267
|
-
MarkdownView,
|
|
268
|
-
TexView,
|
|
269
|
-
TexBox,
|
|
270
|
-
layoutTex,
|
|
271
|
-
configureTex,
|
|
272
|
-
highlightCode,
|
|
273
260
|
cssColor,
|
|
274
261
|
cssColorStraight,
|
|
275
262
|
premultiply,
|
|
276
|
-
cssLength,
|
|
277
263
|
decodeKey,
|
|
278
264
|
groupForState,
|
|
279
265
|
// the `code` on a failed GL setup: branch on it rather than on the message
|
|
@@ -283,11 +269,4 @@ export {
|
|
|
283
269
|
GL_MODES,
|
|
284
270
|
DEFAULT_GL_POLICY
|
|
285
271
|
};
|
|
286
|
-
// The layout engine ntk lays HtmlView out with — downstream layout consumers
|
|
287
|
-
// (e.g. the react-x11 renderer) must import it from here rather than from
|
|
288
|
-
// `yoga-layout`, or they get a second WASM instance whose Nodes cannot be
|
|
289
|
-
// mixed with ntk's. Its enum constants are readable as soon as ntk is
|
|
290
|
-
// imported; `Node`/`Config` need the WASM, which `createClient()` loads —
|
|
291
|
-
// `loadLayout()` is there for widgets used without an App.
|
|
292
|
-
export { default as Yoga, loadLayout, layoutLoaded } from './yoga.js';
|
|
293
272
|
export default { createClient };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Splitting one drawing's coverage mask into a few, when its pieces are
|
|
2
|
+
// scattered (issue #264).
|
|
3
|
+
//
|
|
4
|
+
// A fill or stroke rasterizes coverage into one a8 mask sized to the
|
|
5
|
+
// drawing's ink bounding box, and that mask costs width x height whatever
|
|
6
|
+
// the coverage inside it is. For one shape the bound is right. For a path
|
|
7
|
+
// holding N disjoint subpaths the bound is their *union*, so batching N
|
|
8
|
+
// draws into one path trades N small masks for one big one — which wins
|
|
9
|
+
// when the pieces span the box anyway (long edges of a graph) and loses
|
|
10
|
+
// badly when they do not (its handle dots): at 1100x700, 735 batched edge
|
|
11
|
+
// strokes cost 3.9 MB -> 1.3 MB of mask and 53% less frame time, while 19
|
|
12
|
+
// edges plus 40 small discs batched the same way cost ~0.75 MB *more* and
|
|
13
|
+
// 40% more frame time than drawing them singly.
|
|
14
|
+
//
|
|
15
|
+
// Rather than leave that cliff to every caller, the boxes go through here
|
|
16
|
+
// first and the mask is emitted once per cluster. The partition is by gaps
|
|
17
|
+
// only: a cut is legal where nothing straddles it, which is what keeps
|
|
18
|
+
// every cluster box disjoint from every other. Disjoint boxes are what make
|
|
19
|
+
// the split invisible — no pixel is composited twice (a translucent colour
|
|
20
|
+
// would blend twice at any overlap), and the winding number a fill asks for
|
|
21
|
+
// is unchanged, because a closed subpath contributes nothing to the winding
|
|
22
|
+
// of a point outside its own box.
|
|
23
|
+
//
|
|
24
|
+
// Cutting is not free: each extra mask is a few more requests. So a cut has
|
|
25
|
+
// to pay for itself in mask area, and the cheapest useful unit of area is
|
|
26
|
+
// the policy's `minSaving`. That single rule bounds the outcome from both
|
|
27
|
+
// sides — every cut removes at least `minSaving` pixels of mask, so the
|
|
28
|
+
// number of clusters can never exceed the union area divided by it.
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* When one drawing's mask is worth splitting into several.
|
|
32
|
+
*
|
|
33
|
+
* - `minSaving` — mask pixels a cut has to remove to be worth the extra
|
|
34
|
+
* mask pass it costs. 64x64 is the same "small enough not to think about"
|
|
35
|
+
* box `DEFAULT_RASTER_POLICY.maxArea` uses: below it, an extra pass is
|
|
36
|
+
* dearer than the area it would save.
|
|
37
|
+
* - `maxMasks` — hard cap on clusters per drawing, so a pathological path
|
|
38
|
+
* (thousands of scattered dots) cannot turn one drawing into thousands of
|
|
39
|
+
* request groups. Cuts are taken most-valuable-first, so the cap keeps the
|
|
40
|
+
* ones that matter.
|
|
41
|
+
*
|
|
42
|
+
* `{ maxMasks: 1 }` — or `minSaving: Infinity` — disables the split, which
|
|
43
|
+
* is what a composite op that writes outside its coverage gets.
|
|
44
|
+
*/
|
|
45
|
+
export const DEFAULT_MASK_POLICY = {
|
|
46
|
+
minSaving: 64 * 64,
|
|
47
|
+
maxMasks: 32
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** the policy for one app, merged over the defaults */
|
|
51
|
+
export function maskPolicyOf(app) {
|
|
52
|
+
return app.maskPolicy
|
|
53
|
+
? { ...DEFAULT_MASK_POLICY, ...app.maskPolicy }
|
|
54
|
+
: DEFAULT_MASK_POLICY;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const area = (b) => b.w * b.h;
|
|
58
|
+
|
|
59
|
+
/** The smallest {x, y, w, h} box holding both of two boxes. */
|
|
60
|
+
export function unionBox(a, b) {
|
|
61
|
+
const x = Math.min(a.x, b.x);
|
|
62
|
+
const y = Math.min(a.y, b.y);
|
|
63
|
+
return {
|
|
64
|
+
x,
|
|
65
|
+
y,
|
|
66
|
+
w: Math.max(a.x + a.w, b.x + b.w) - x,
|
|
67
|
+
h: Math.max(a.y + a.h, b.y + b.h) - y
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function unionOf(boxes, order) {
|
|
72
|
+
let out = { ...boxes[order[0]] };
|
|
73
|
+
for (let i = 1; i < order.length; ++i) out = unionBox(out, boxes[order[i]]);
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The most valuable gap cut of one group along one axis, or null when the
|
|
79
|
+
* group has no gap on it.
|
|
80
|
+
*
|
|
81
|
+
* `order` is the group's members sorted by that axis' start. A cut between
|
|
82
|
+
* members i and i+1 is legal only where nothing straddles it — the next box
|
|
83
|
+
* has to start at or past the far edge of every box before it, which for a
|
|
84
|
+
* union box is just its own far edge. What the cut is worth is the mask area
|
|
85
|
+
* it removes: the parent box less the two halves.
|
|
86
|
+
*/
|
|
87
|
+
function bestGap(boxes, order, box, axis) {
|
|
88
|
+
const n = order.length;
|
|
89
|
+
const start = axis === 'x' ? 'x' : 'y';
|
|
90
|
+
const size = axis === 'x' ? 'w' : 'h';
|
|
91
|
+
|
|
92
|
+
// suffix[i] is the union box of order[i..n-1]; the prefix is carried
|
|
93
|
+
// along the scan below
|
|
94
|
+
const suffix = new Array(n);
|
|
95
|
+
suffix[n - 1] = { ...boxes[order[n - 1]] };
|
|
96
|
+
for (let i = n - 2; i >= 0; --i) {
|
|
97
|
+
suffix[i] = unionBox(boxes[order[i]], suffix[i + 1]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let best = null;
|
|
101
|
+
let prefix = { ...boxes[order[0]] };
|
|
102
|
+
for (let i = 0; i + 1 < n; ++i) {
|
|
103
|
+
if (i > 0) prefix = unionBox(prefix, boxes[order[i]]);
|
|
104
|
+
const next = boxes[order[i + 1]];
|
|
105
|
+
if (next[start] < prefix[start] + prefix[size]) continue; // straddled
|
|
106
|
+
const saving = area(box) - area(prefix) - area(suffix[i + 1]);
|
|
107
|
+
if (!best || saving > best.saving) {
|
|
108
|
+
best = { axis, at: i, saving, left: prefix, right: suffix[i + 1] };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return best;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** a group of pieces, its box, and the best cut available to it */
|
|
115
|
+
function makeNode(boxes, byX, byY, box) {
|
|
116
|
+
const node = { box: box ?? unionOf(boxes, byX), byX, byY, split: null };
|
|
117
|
+
if (byX.length > 1) {
|
|
118
|
+
const x = bestGap(boxes, byX, node.box, 'x');
|
|
119
|
+
const y = bestGap(boxes, byY, node.box, 'y');
|
|
120
|
+
node.split = !x ? y : !y || x.saving >= y.saving ? x : y;
|
|
121
|
+
}
|
|
122
|
+
return node;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** the two halves of `node`, each with its own next-best cut */
|
|
126
|
+
function splitNode(boxes, node) {
|
|
127
|
+
const { axis, at, left, right } = node.split;
|
|
128
|
+
const cut = axis === 'x' ? node.byX : node.byY;
|
|
129
|
+
const other = axis === 'x' ? node.byY : node.byX;
|
|
130
|
+
const inLeft = new Set(cut.slice(0, at + 1));
|
|
131
|
+
// the other axis' order survives the filter, so neither half is re-sorted
|
|
132
|
+
const leftOther = other.filter((i) => inLeft.has(i));
|
|
133
|
+
const rightOther = other.filter((i) => !inLeft.has(i));
|
|
134
|
+
const leftCut = cut.slice(0, at + 1);
|
|
135
|
+
const rightCut = cut.slice(at + 1);
|
|
136
|
+
return axis === 'x'
|
|
137
|
+
? [
|
|
138
|
+
makeNode(boxes, leftCut, leftOther, left),
|
|
139
|
+
makeNode(boxes, rightCut, rightOther, right)
|
|
140
|
+
]
|
|
141
|
+
: [
|
|
142
|
+
makeNode(boxes, leftOther, leftCut, left),
|
|
143
|
+
makeNode(boxes, rightOther, rightCut, right)
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Partition one drawing's pieces into mask clusters.
|
|
149
|
+
*
|
|
150
|
+
* @param {Array<{x, y, w, h}>} boxes one integer box per piece — a fill's
|
|
151
|
+
* subpaths, a stroke's islands of triangles — already clamped to the
|
|
152
|
+
* surface. Boxes may overlap; overlapping ones always end up in the same
|
|
153
|
+
* cluster.
|
|
154
|
+
* @param {{minSaving: number, maxMasks: number}} [policy]
|
|
155
|
+
* @returns {Array<{x, y, w, h, items: number[]}>} clusters, each with the
|
|
156
|
+
* indices of the pieces it holds. Cluster boxes are pairwise disjoint,
|
|
157
|
+
* and their union is the union of `boxes`.
|
|
158
|
+
*/
|
|
159
|
+
export function clusterBoxes(boxes, policy = DEFAULT_MASK_POLICY) {
|
|
160
|
+
const n = boxes.length;
|
|
161
|
+
if (!n) return [];
|
|
162
|
+
// merged here as well as in maskPolicyOf, so that a partial policy from a
|
|
163
|
+
// direct caller ({ maxMasks: 1 }) cannot silently zero the other field
|
|
164
|
+
const { minSaving, maxMasks: cap } = { ...DEFAULT_MASK_POLICY, ...policy };
|
|
165
|
+
const maxMasks = Math.max(1, cap | 0);
|
|
166
|
+
const all = boxes.map((_, i) => i);
|
|
167
|
+
if (n === 1 || maxMasks === 1 || !(minSaving < Infinity)) {
|
|
168
|
+
return [{ ...unionOf(boxes, all), items: all }];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const byX = all.slice().sort((a, b) => boxes[a].x - boxes[b].x || a - b);
|
|
172
|
+
const byY = all.slice().sort((a, b) => boxes[a].y - boxes[b].y || a - b);
|
|
173
|
+
const nodes = [makeNode(boxes, byX, byY)];
|
|
174
|
+
// most-valuable cut first, so a maxMasks cap keeps the cuts that matter
|
|
175
|
+
while (nodes.length < maxMasks) {
|
|
176
|
+
let pick = -1;
|
|
177
|
+
let best = minSaving;
|
|
178
|
+
for (let i = 0; i < nodes.length; ++i) {
|
|
179
|
+
const split = nodes[i].split;
|
|
180
|
+
if (split && split.saving >= best) {
|
|
181
|
+
best = split.saving;
|
|
182
|
+
pick = i;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (pick < 0) break;
|
|
186
|
+
const [a, b] = splitNode(boxes, nodes[pick]);
|
|
187
|
+
nodes[pick] = a;
|
|
188
|
+
nodes.push(b);
|
|
189
|
+
}
|
|
190
|
+
return nodes.map((node) => ({ ...node.box, items: node.byX }));
|
|
191
|
+
}
|