ntk 8.8.0 → 8.8.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/gl.js +19 -13
- package/lib/renderingcontext_2d.js +22 -7
- package/lib/text/glyphs.js +73 -2
- package/package.json +2 -2
package/lib/gl.js
CHANGED
|
@@ -44,10 +44,13 @@ import { nodeRequire } from './builtin.js';
|
|
|
44
44
|
* Linux only, macOS needs no device node.
|
|
45
45
|
* - `GL_REMOTE_DISPLAY` — a TCP or forwarded display; both flavors are
|
|
46
46
|
* local-only by construction.
|
|
47
|
-
* - `GL_NO_FD_PASSING` — the display is local, but this
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
47
|
+
* - `GL_NO_FD_PASSING` — the display is local, but this connection cannot
|
|
48
|
+
* send a descriptor over the socket. x11 has a transport for each runtime
|
|
49
|
+
* it supports — `process.binding('pipe_wrap')` under Node, `bun:ffi`
|
|
50
|
+
* calling `sendmsg(2)` under Bun since x11 4.1.0 — so what is left here is
|
|
51
|
+
* an x11 below that under Bun, a runtime with neither path (Deno), or a
|
|
52
|
+
* transport that would not initialise. Linux only — Apple-DRI passes no
|
|
53
|
+
* descriptors.
|
|
51
54
|
* - `GL_NO_DRI3` — the server has no DRI3/Present (Xvfb, Xephyr, XQuartz —
|
|
52
55
|
* though XQuartz has its own path, see `GL_NO_APPLEDRI`).
|
|
53
56
|
* - `GL_NO_APPLEDRI` — macOS, and the server has no Apple-DRI extension:
|
|
@@ -418,17 +421,20 @@ export function glCapabilities(app) {
|
|
|
418
421
|
|
|
419
422
|
if (!canPassDescriptors(app.display)) {
|
|
420
423
|
// The socket is local; what is missing is the ability to send a
|
|
421
|
-
// descriptor along it. x11
|
|
422
|
-
// `process.binding('pipe_wrap')`,
|
|
423
|
-
//
|
|
424
|
-
//
|
|
424
|
+
// descriptor along it. x11 carries one through Node's internal
|
|
425
|
+
// `process.binding('pipe_wrap')`, and under Bun through `bun:ffi`
|
|
426
|
+
// calling `sendmsg(2)` — the latter since x11 4.1.0, so an x11 below
|
|
427
|
+
// that under Bun lands here, as does a runtime with neither path and a
|
|
428
|
+
// transport that would not initialise. Saying "not a local socket"
|
|
429
|
+
// would send the reader off to check their DISPLAY, which is fine.
|
|
425
430
|
return fail(
|
|
426
431
|
GLError.NO_FD_PASSING,
|
|
427
|
-
`this
|
|
428
|
-
'The connection is local; the
|
|
429
|
-
"x11
|
|
430
|
-
'
|
|
431
|
-
'
|
|
432
|
+
`this connection cannot pass file descriptors over the X socket under ${runtimeName()}, and DRI3 works by passing one`,
|
|
433
|
+
'The connection is local; the descriptor transport is what is missing.\n' +
|
|
434
|
+
"x11 carries one through Node's process.binding('pipe_wrap') under Node, and\n" +
|
|
435
|
+
'through bun:ffi sendmsg(2) under Bun from x11 4.1.0 on. Under Bun on an older\n' +
|
|
436
|
+
'x11, `npm install x11@^4.1.0` is the whole fix. Otherwise leave glPolicy at\n' +
|
|
437
|
+
'its default and use indirect GLX, which needs no descriptor passing at all.'
|
|
432
438
|
);
|
|
433
439
|
}
|
|
434
440
|
|
|
@@ -55,7 +55,9 @@ import {
|
|
|
55
55
|
drawGlyphRuns,
|
|
56
56
|
encodeGlyphItems,
|
|
57
57
|
positionedRunsInk,
|
|
58
|
+
roundFrom,
|
|
58
59
|
runId,
|
|
60
|
+
snapOrigin,
|
|
59
61
|
} from "./text/glyphs.js";
|
|
60
62
|
import { TextLayout } from "./text/layout.js";
|
|
61
63
|
import { reorderRuns } from "./text/shape.js";
|
|
@@ -1365,6 +1367,12 @@ class RenderingContext2d {
|
|
|
1365
1367
|
* whole pixel inside the surface, and the composite carries it to wherever
|
|
1366
1368
|
* the text is. Glyph origins are rounded to whole pixels on the way to the
|
|
1367
1369
|
* server anyway, so nothing is lost by it.
|
|
1370
|
+
*
|
|
1371
|
+
* Where it lands is rounded the way `positionGlyphs` rounds a glyph
|
|
1372
|
+
* (`roundFrom`), so the shadow moves with its text: drawn whole pixels
|
|
1373
|
+
* away, it lands exactly that many pixels away, and a scroll blit can copy
|
|
1374
|
+
* it (issue #350). Rounded as one floating-point sum, an anchor on a half
|
|
1375
|
+
* pixel went down at x 104 and up at x 152.
|
|
1368
1376
|
*/
|
|
1369
1377
|
_shadowOfText(text, x, y) {
|
|
1370
1378
|
const app = this.window.app;
|
|
@@ -1435,8 +1443,8 @@ class RenderingContext2d {
|
|
|
1435
1443
|
if (!surface) return;
|
|
1436
1444
|
this._paintShadow(
|
|
1437
1445
|
surface,
|
|
1438
|
-
|
|
1439
|
-
|
|
1446
|
+
roundFrom(runX, this._shadowOffsetX) - originX,
|
|
1447
|
+
roundFrom(runY, this._shadowOffsetY) - originY,
|
|
1440
1448
|
);
|
|
1441
1449
|
}
|
|
1442
1450
|
|
|
@@ -1470,13 +1478,19 @@ class RenderingContext2d {
|
|
|
1470
1478
|
// what the bitmap glyph path draws at anyway, and they keep the key
|
|
1471
1479
|
// stable as the paragraph moves — `(x + a) - (x + b)` is not exactly
|
|
1472
1480
|
// `a - b` in floating point, and an origin-dependent key would miss the
|
|
1473
|
-
// cache on every scroll.
|
|
1481
|
+
// cache on every scroll. Rounding alone does not absorb that where
|
|
1482
|
+
// `a - b` is on a half pixel: the difference came to a hair under it at
|
|
1483
|
+
// x 104 and exactly on it at x 152, and rounded two ways. So both
|
|
1484
|
+
// origins are snapped first, as `positionGlyphs` snaps them (issue
|
|
1485
|
+
// #350), and snapped origins subtract exactly.
|
|
1474
1486
|
const ax = positioned[0].x;
|
|
1475
1487
|
const ay = positioned[0].y;
|
|
1488
|
+
const sx = snapOrigin(ax);
|
|
1489
|
+
const sy = snapOrigin(ay);
|
|
1476
1490
|
const local = positioned.map((p) => ({
|
|
1477
1491
|
run: p.run,
|
|
1478
|
-
x: Math.round(p.x -
|
|
1479
|
-
y: Math.round(p.y -
|
|
1492
|
+
x: Math.round(snapOrigin(p.x) - sx),
|
|
1493
|
+
y: Math.round(snapOrigin(p.y) - sy),
|
|
1480
1494
|
textRendering: p.textRendering,
|
|
1481
1495
|
}));
|
|
1482
1496
|
const key = `${local
|
|
@@ -1516,11 +1530,12 @@ class RenderingContext2d {
|
|
|
1516
1530
|
return coverage;
|
|
1517
1531
|
});
|
|
1518
1532
|
if (surface) {
|
|
1533
|
+
// anchored as fillText's shadow is, so it moves with the text
|
|
1519
1534
|
const origin = surface._shadowOrigin;
|
|
1520
1535
|
this._paintShadow(
|
|
1521
1536
|
surface,
|
|
1522
|
-
|
|
1523
|
-
|
|
1537
|
+
roundFrom(ax, this._shadowOffsetX) - origin.x,
|
|
1538
|
+
roundFrom(ay, this._shadowOffsetY) - origin.y,
|
|
1524
1539
|
);
|
|
1525
1540
|
return;
|
|
1526
1541
|
}
|
package/lib/text/glyphs.js
CHANGED
|
@@ -319,6 +319,11 @@ export function encodeGlyphItems(items, bits) {
|
|
|
319
319
|
return { gsid, bits, elts };
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
// steps per pixel a text origin is snapped to before anything rounds against
|
|
323
|
+
// it (see positionGlyphs and snapOrigin); a power of two, so the snap and the
|
|
324
|
+
// split are exact
|
|
325
|
+
const ORIGIN_STEPS = 256;
|
|
326
|
+
|
|
322
327
|
/**
|
|
323
328
|
* Compute device positions for shaped runs — the single source of truth for
|
|
324
329
|
* where each glyph's origin lands, shared by the renderer and by tests that
|
|
@@ -328,20 +333,86 @@ export function encodeGlyphItems(items, bits) {
|
|
|
328
333
|
* Font.shape()/shapeText() (already in visual order) and x/y is the run's
|
|
329
334
|
* baseline origin in device space.
|
|
330
335
|
*
|
|
336
|
+
* Placement is translation-invariant (issue #350): the same run drawn whole
|
|
337
|
+
* pixels away puts every glyph exactly that many pixels away, so a renderer
|
|
338
|
+
* that copies pixels it already drew — a scroll blit — matches a repaint
|
|
339
|
+
* byte for byte. The origin arrives as a whole-pixel position plus the
|
|
340
|
+
* layout's fractional offsets, summed in floating point, and the last bits
|
|
341
|
+
* of that sum depend on the whole part's magnitude: rounded as one number, a
|
|
342
|
+
* pen exactly on a half pixel went down at x 104 and up at x 152. So the
|
|
343
|
+
* origin is snapped to 1/256 px first and split into whole pixels and a
|
|
344
|
+
* fraction in [0, 1), and the pen walks from the fraction alone. Two origins
|
|
345
|
+
* whole pixels apart snap to fractions that are bit-identical, so every
|
|
346
|
+
* rounding after the snap sees the same numbers wherever the run is drawn.
|
|
347
|
+
* (Taking the fraction after the snap is what keeps it below 1: an origin a
|
|
348
|
+
* hair under a whole pixel and one exactly on it walk the same pen from 0.)
|
|
349
|
+
*
|
|
350
|
+
* The snap moves a glyph only where its pen lies within 1/512 px of a
|
|
351
|
+
* rounding boundary. The one knife-edge left is the snap's own — an origin
|
|
352
|
+
* within an ulp of an odd multiple of 1/512 px — a far narrower target than
|
|
353
|
+
* any pen along the run landing on a half pixel.
|
|
354
|
+
*
|
|
331
355
|
* @returns {Array<{run, glyph, x, y}>} integer glyph-origin positions
|
|
332
356
|
*/
|
|
333
357
|
export function positionGlyphs(positioned) {
|
|
334
358
|
const out = [];
|
|
335
359
|
for (const { run, x, y } of positioned) {
|
|
336
|
-
|
|
360
|
+
const sx = snapOrigin(x);
|
|
361
|
+
const sy = snapOrigin(y);
|
|
362
|
+
const ox = Math.floor(sx);
|
|
363
|
+
const oy = Math.floor(sy);
|
|
364
|
+
const fy = sy - oy;
|
|
365
|
+
let cursor = sx - ox;
|
|
337
366
|
for (const g of run.glyphs) {
|
|
338
|
-
out.push({
|
|
367
|
+
out.push({
|
|
368
|
+
run,
|
|
369
|
+
glyph: g,
|
|
370
|
+
x: ox + Math.round(cursor + g.dx),
|
|
371
|
+
y: oy + Math.round(fy - g.dy)
|
|
372
|
+
});
|
|
339
373
|
cursor += g.ax;
|
|
340
374
|
}
|
|
341
375
|
}
|
|
342
376
|
return out;
|
|
343
377
|
}
|
|
344
378
|
|
|
379
|
+
/**
|
|
380
|
+
* A device-space origin snapped to 1/256 px, the way `positionGlyphs` takes
|
|
381
|
+
* a run's origin before anything rounds against it (issue #350).
|
|
382
|
+
*
|
|
383
|
+
* What comes back is an exact multiple of 1/256, so whole pixels split off
|
|
384
|
+
* it, and two snapped origins subtract, with no rounding at all. Origins
|
|
385
|
+
* whole pixels apart snap exactly that many pixels apart, bit for bit, even
|
|
386
|
+
* though the origins themselves — a whole-pixel position plus fractional
|
|
387
|
+
* layout offsets, summed in floating point — differ in their last bits.
|
|
388
|
+
* Anything else that rounds against a text origin needs the same: a text
|
|
389
|
+
* shadow's anchor, and the offsets between the runs its coverage holds.
|
|
390
|
+
*
|
|
391
|
+
* @param {number} v a device coordinate
|
|
392
|
+
* @returns {number} `v` to the nearest 1/256 px
|
|
393
|
+
*/
|
|
394
|
+
export function snapOrigin(v) {
|
|
395
|
+
return Math.round(v * ORIGIN_STEPS) / ORIGIN_STEPS;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* `Math.round(origin + offset)`, rounded the way `positionGlyphs` rounds a
|
|
400
|
+
* glyph `offset` px from its run's origin: the snapped origin's whole
|
|
401
|
+
* pixels, plus its fraction and the offset rounded together. The one sum
|
|
402
|
+
* that rounds never sees the whole part, so moving the origin by whole
|
|
403
|
+
* pixels moves the answer by exactly those pixels, whatever the offset is.
|
|
404
|
+
* It is where a text shadow composites, `offset` being the shadow's own.
|
|
405
|
+
*
|
|
406
|
+
* @param {number} origin a device coordinate, a run's origin
|
|
407
|
+
* @param {number} offset px from it, along the same axis
|
|
408
|
+
* @returns {number} the whole pixel `origin + offset` lands on
|
|
409
|
+
*/
|
|
410
|
+
export function roundFrom(origin, offset) {
|
|
411
|
+
const s = snapOrigin(origin);
|
|
412
|
+
const whole = Math.floor(s);
|
|
413
|
+
return whole + Math.round(s - whole + offset);
|
|
414
|
+
}
|
|
415
|
+
|
|
345
416
|
/**
|
|
346
417
|
* Ink extents of positioned runs, in whatever coordinates their origins are
|
|
347
418
|
* given in — the union of every glyph's bounding box, laid out exactly as
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ntk",
|
|
3
|
-
"version": "8.8.
|
|
3
|
+
"version": "8.8.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",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"linebreak": "^1.1.0",
|
|
45
45
|
"parse-color": "^1.0.0",
|
|
46
46
|
"pngjs": "^7.0.0",
|
|
47
|
-
"x11": "^4.0
|
|
47
|
+
"x11": "^4.1.0"
|
|
48
48
|
},
|
|
49
49
|
"optionalDependencies": {
|
|
50
50
|
"x11-dri": ">=0.5.0 <1"
|