ntk 7.5.0 → 7.6.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/renderingcontext_2d.js +135 -6
- package/lib/surface.js +62 -0
- package/lib/text/font.js +21 -0
- package/package.json +1 -1
|
@@ -1106,10 +1106,23 @@ class RenderingContext2d {
|
|
|
1106
1106
|
// a segment that is already at least r long
|
|
1107
1107
|
if (dot >= 0 && r * r <= shortest) continue;
|
|
1108
1108
|
const cross = ax * by - ay * bx;
|
|
1109
|
-
// r·tan(φ/2) > min(|a|, |b|), squared. A non-positive denominator
|
|
1110
|
-
// the reversal the tangent is infinite at
|
|
1111
|
-
|
|
1112
|
-
|
|
1109
|
+
// r·tan(φ/2) > min(|a|, |b|), squared. A non-positive denominator
|
|
1110
|
+
// is the reversal the tangent is infinite at — but near one the
|
|
1111
|
+
// denominator is pure cancellation (√(la·lb) and -dot agree to ~15
|
|
1112
|
+
// digits), so an exact double-back lands a few ulps to either side
|
|
1113
|
+
// of 0. The same-x bursts of issue #259 have cross exactly 0 too,
|
|
1114
|
+
// so when denom rounded positive neither test here fired and the
|
|
1115
|
+
// extruder met the reversal itself, normalizing a zero-length
|
|
1116
|
+
// tangent into NaN join vertices. Below its own noise floor denom
|
|
1117
|
+
// only means "within microradians of a reversal", where the true
|
|
1118
|
+
// tangent exceeds a million and no segment can hold the join: cut
|
|
1119
|
+
// unconditionally.
|
|
1120
|
+
const ab = Math.sqrt(la * lb);
|
|
1121
|
+
const denom = ab + dot;
|
|
1122
|
+
if (
|
|
1123
|
+
denom <= ab * 1e-12 ||
|
|
1124
|
+
(r * cross) ** 2 > shortest * denom * denom
|
|
1125
|
+
) {
|
|
1113
1126
|
(cuts ??= []).push(i);
|
|
1114
1127
|
}
|
|
1115
1128
|
}
|
|
@@ -1277,6 +1290,27 @@ class RenderingContext2d {
|
|
|
1277
1290
|
for (const run of dashed.runs) extrudeRun(cleanRun(run), false);
|
|
1278
1291
|
}
|
|
1279
1292
|
}
|
|
1293
|
+
// Nothing non-finite may reach the server: FIXED encoding turns NaN
|
|
1294
|
+
// into 0, so one poisoned vertex renders as a wedge to the origin on a
|
|
1295
|
+
// real display — while the in-process server quietly drops it, which
|
|
1296
|
+
// is why no headless pixel test can see this for us (issue #259). The
|
|
1297
|
+
// cuts above are meant to keep the extruder off that path; this keeps
|
|
1298
|
+
// a future miss from being catastrophic. A sum is finite iff every
|
|
1299
|
+
// term is.
|
|
1300
|
+
let w = 0;
|
|
1301
|
+
for (let i = 0; i < tris.length; i += 6) {
|
|
1302
|
+
const sum =
|
|
1303
|
+
tris[i] +
|
|
1304
|
+
tris[i + 1] +
|
|
1305
|
+
tris[i + 2] +
|
|
1306
|
+
tris[i + 3] +
|
|
1307
|
+
tris[i + 4] +
|
|
1308
|
+
tris[i + 5];
|
|
1309
|
+
if (!Number.isFinite(sum)) continue;
|
|
1310
|
+
if (w < i) for (let k = 0; k < 6; k++) tris[w + k] = tris[i + k];
|
|
1311
|
+
w += 6;
|
|
1312
|
+
}
|
|
1313
|
+
if (w < tris.length) tris.length = w;
|
|
1280
1314
|
if (!tris.length) return;
|
|
1281
1315
|
|
|
1282
1316
|
const op = this._op();
|
|
@@ -1396,14 +1430,35 @@ class RenderingContext2d {
|
|
|
1396
1430
|
}
|
|
1397
1431
|
|
|
1398
1432
|
/**
|
|
1399
|
-
* Composite
|
|
1433
|
+
* Composite glyph runs onto this context, honouring the clip. This is
|
|
1434
|
+
* the primitive `fillText` and `TextLayout.draw` are built on, and it is
|
|
1435
|
+
* public: the run shape below is a documented contract
|
|
1436
|
+
* (docs/text.md#glyph-runs), so renderers that position glyphs
|
|
1437
|
+
* themselves — a terminal grid, a tabular column — can hand-build runs
|
|
1438
|
+
* instead of shaping.
|
|
1439
|
+
*
|
|
1440
|
+
* @param {number} op Render.PictOp (`ctx.Render.PictOp.Over` for normal text)
|
|
1441
|
+
* @param {Picture} src source picture the glyphs paint with — a solid
|
|
1442
|
+
* (`ctx.createSolidPicture(r, g, b, a)`, premultiplied 0..1) or a gradient
|
|
1443
|
+
* @param {Array<{run, x, y, textRendering?}>} positioned runs in visual
|
|
1444
|
+
* order; `x`/`y` is the run's baseline origin in device space. `run` is
|
|
1445
|
+
* `{ font, size, glyphs }` — a `Font`, a pixel size, and glyphs
|
|
1446
|
+
* `{ id, ax, dx, dy }` in drawing order: `id` a font glyph id
|
|
1447
|
+
* (`Font.shape()`'s `glyphs[].id`, or `Font.glyphIdFor(cp)`), `ax` the
|
|
1448
|
+
* pen advance in px, `dx`/`dy` the drawing offset from the pen position
|
|
1449
|
+
* (y-up: positive `dy` raises the glyph). The pen starts at `x`; each
|
|
1450
|
+
* glyph inks at `(pen + dx, y - dy)` and then advances it by `ax`.
|
|
1451
|
+
* `Font.shape()` returns runs of exactly this shape; extra fields
|
|
1452
|
+
* (`codePoints`, `width`, …) are ignored. `textRendering` optionally
|
|
1453
|
+
* overrides the bitmap/vector routing per run (docs/text.md).
|
|
1400
1454
|
*
|
|
1401
1455
|
* CompositeGlyphs writes straight to the destination picture, so it has
|
|
1402
1456
|
* no way to consult our clip mask — text drawn through it used to spill
|
|
1403
1457
|
* out of clipped boxes while every fill and stroke stayed inside. With a
|
|
1404
1458
|
* clip active, render the glyph coverage into the scratch a8 mask
|
|
1405
1459
|
* instead, intersect that with the clip, and paint the real source
|
|
1406
|
-
* through the result — the same shape as _fillPolys.
|
|
1460
|
+
* through the result — the same shape as _fillPolys. A rectangular clip
|
|
1461
|
+
* takes the server-side fast path below instead of the mask.
|
|
1407
1462
|
*/
|
|
1408
1463
|
drawGlyphs(op, src, positioned) {
|
|
1409
1464
|
const app = this.window.app;
|
|
@@ -1652,6 +1707,80 @@ class RenderingContext2d {
|
|
|
1652
1707
|
this._fillPolys(flattenPath(tmp._cmds, this._m), "nonzero");
|
|
1653
1708
|
}
|
|
1654
1709
|
|
|
1710
|
+
/**
|
|
1711
|
+
* Fill a batch of axis-aligned rectangles: `fillRect` once per rectangle
|
|
1712
|
+
* semantically — fillStyle, globalAlpha, the composite op, the clip and
|
|
1713
|
+
* damage reporting all apply — but priced for the "many small rectangles
|
|
1714
|
+
* per frame" caller (issue #253): terminal cell backgrounds, sparkline
|
|
1715
|
+
* bars, heat maps, row striping.
|
|
1716
|
+
*
|
|
1717
|
+
* `rects` is an array of `[x, y, w, h]` quadruples or one flat
|
|
1718
|
+
* `[x0, y0, w0, h0, x1, ...]` array; rectangles with non-positive width
|
|
1719
|
+
* or height are skipped.
|
|
1720
|
+
*
|
|
1721
|
+
* A solid-colour fillStyle under an identity transform and a rectangular
|
|
1722
|
+
* (or absent) clip compiles the whole list into a single
|
|
1723
|
+
* `Render.FillRectangles`, where N `fillRect` calls cost N composites.
|
|
1724
|
+
* Gradient/Picture styles, transforms and non-rectangular clips fall
|
|
1725
|
+
* back to that `fillRect` loop, so the answer is always right and only
|
|
1726
|
+
* the request count varies.
|
|
1727
|
+
*/
|
|
1728
|
+
fillRects(rects) {
|
|
1729
|
+
if (!rects || !rects.length || this.globalAlpha <= 0) return;
|
|
1730
|
+
// normalize to one flat list, dropping empty rectangles up front — the
|
|
1731
|
+
// wire encodes width and height unsigned, so a negative would wrap
|
|
1732
|
+
const flat = [];
|
|
1733
|
+
if (Array.isArray(rects[0])) {
|
|
1734
|
+
for (const r of rects) {
|
|
1735
|
+
if (r[2] > 0 && r[3] > 0) flat.push(r[0], r[1], r[2], r[3]);
|
|
1736
|
+
}
|
|
1737
|
+
} else {
|
|
1738
|
+
for (let i = 0; i + 3 < rects.length; i += 4) {
|
|
1739
|
+
if (rects[i + 2] > 0 && rects[i + 3] > 0)
|
|
1740
|
+
flat.push(rects[i], rects[i + 1], rects[i + 2], rects[i + 3]);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
if (!flat.length) return;
|
|
1744
|
+
|
|
1745
|
+
const clip = this._clips.length ? this._clipRect() : null;
|
|
1746
|
+
if (
|
|
1747
|
+
!matIsIdentity(this._m) ||
|
|
1748
|
+
!isPlainColor(this._fillStyle) ||
|
|
1749
|
+
(this._clips.length && !clip)
|
|
1750
|
+
) {
|
|
1751
|
+
for (let i = 0; i < flat.length; i += 4)
|
|
1752
|
+
this.fillRect(flat[i], flat[i + 1], flat[i + 2], flat[i + 3]);
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (clip && (clip.w === 0 || clip.h === 0)) return; // clipped away
|
|
1756
|
+
const R = this.Render;
|
|
1757
|
+
if (clip) {
|
|
1758
|
+
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
1759
|
+
clip.x,
|
|
1760
|
+
clip.y,
|
|
1761
|
+
clip.w,
|
|
1762
|
+
clip.h,
|
|
1763
|
+
]);
|
|
1764
|
+
}
|
|
1765
|
+
// globalAlpha folds into the premultiplied colour — scaling all four
|
|
1766
|
+
// components is exactly what compositing at that opacity means, for
|
|
1767
|
+
// any composite op (the mask slot would only scale the source the same
|
|
1768
|
+
// way)
|
|
1769
|
+
const color = this._foldedColor(this._fillStyle);
|
|
1770
|
+
// stay under the server's maximum request size
|
|
1771
|
+
const chunk = 10000 * 4;
|
|
1772
|
+
for (let i = 0; i < flat.length; i += chunk) {
|
|
1773
|
+
R.FillRectangles(
|
|
1774
|
+
this._op(),
|
|
1775
|
+
this.picture.id,
|
|
1776
|
+
color,
|
|
1777
|
+
i === 0 && flat.length <= chunk ? flat : flat.slice(i, i + chunk),
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
if (clip) this._resetPictureClip();
|
|
1781
|
+
this._markDirty();
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1655
1784
|
strokeRect(x, y, w, h) {
|
|
1656
1785
|
const m = this._m;
|
|
1657
1786
|
if (m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1) {
|
package/lib/surface.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Picture from './picture.js';
|
|
2
2
|
import Pixmap from './pixmap.js';
|
|
3
|
+
import { safeRelease } from './cleanup.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* An offscreen drawing surface: a pixmap, its Picture, and enough of the
|
|
@@ -103,6 +104,67 @@ export class Surface {
|
|
|
103
104
|
return this.pixmap.getContext(name, ...args);
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Scroll the pixels of `src` (surface coordinates, `{x, y, width, height}`)
|
|
109
|
+
* by (dx, dy) in place, server-side: one CopyArea of the band that
|
|
110
|
+
* survives the shift, in place of the caller re-drawing everything that
|
|
111
|
+
* merely moved. Returns true when the copy was issued; false means
|
|
112
|
+
* "nothing survives the shift here", and the caller repaints `src` exactly
|
|
113
|
+
* as it would have without this method — every refusal is the status quo.
|
|
114
|
+
*
|
|
115
|
+
* Refused when the delta is fractional (a sub-pixel shift changes every
|
|
116
|
+
* pixel, so there is nothing to copy), when it is zero, when nothing of
|
|
117
|
+
* `src` survives the shift after clamping to the surface, or on a
|
|
118
|
+
* destroyed surface.
|
|
119
|
+
*
|
|
120
|
+
* This is `Window#scrollRegion` for a retained offscreen surface, minus
|
|
121
|
+
* the damage bookkeeping a pixmap does not have: the caller draws the
|
|
122
|
+
* exposed strip and composites as usual. The overlapping self-copy is
|
|
123
|
+
* fully defined — pixmap contents cannot be occluded, and the server
|
|
124
|
+
* fetches the source region before storing. The copy goes out with a
|
|
125
|
+
* shared graphicsExposures: 0 GC — one per app and depth, created on
|
|
126
|
+
* first use and released with the connection, where a default GC would
|
|
127
|
+
* emit a NoExposure event per copy — and in-order with the caller's
|
|
128
|
+
* follow-up drawing of the exposed strip on the same connection.
|
|
129
|
+
*/
|
|
130
|
+
copyWithin(src, dx, dy) {
|
|
131
|
+
if (this._destroyed) return false;
|
|
132
|
+
if (!Number.isInteger(dx) || !Number.isInteger(dy) || (dx === 0 && dy === 0)) return false;
|
|
133
|
+
const x0 = Math.max(0, Math.floor(src.x));
|
|
134
|
+
const y0 = Math.max(0, Math.floor(src.y));
|
|
135
|
+
const x1 = Math.min(this.width, Math.ceil(src.x + src.width));
|
|
136
|
+
const y1 = Math.min(this.height, Math.ceil(src.y + src.height));
|
|
137
|
+
// the band that survives: dest = clamped src ∩ (clamped src + delta)
|
|
138
|
+
const dstX0 = Math.max(x0, x0 + dx);
|
|
139
|
+
const dstY0 = Math.max(y0, y0 + dy);
|
|
140
|
+
const dstX1 = Math.min(x1, x1 + dx);
|
|
141
|
+
const dstY1 = Math.min(y1, y1 + dy);
|
|
142
|
+
if (dstX1 <= dstX0 || dstY1 <= dstY0) return false;
|
|
143
|
+
const X = this.app.X;
|
|
144
|
+
// a GC is valid for any drawable of its depth on the screen, so one per
|
|
145
|
+
// app and depth serves every surface — the upload-GC pattern in image.js
|
|
146
|
+
const gcs = (this.app._surfaceCopyGC ??= {});
|
|
147
|
+
safeRelease(X, () => {
|
|
148
|
+
let gc = gcs[this.depth];
|
|
149
|
+
if (!gc) {
|
|
150
|
+
gc = gcs[this.depth] = X.AllocID();
|
|
151
|
+
X.CreateGC(gc, this.pixmap.id, { graphicsExposures: 0 });
|
|
152
|
+
}
|
|
153
|
+
X.CopyArea(
|
|
154
|
+
this.pixmap.id,
|
|
155
|
+
this.pixmap.id,
|
|
156
|
+
gc,
|
|
157
|
+
dstX0 - dx,
|
|
158
|
+
dstY0 - dy,
|
|
159
|
+
dstX0,
|
|
160
|
+
dstY0,
|
|
161
|
+
dstX1 - dstX0,
|
|
162
|
+
dstY1 - dstY0
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
106
168
|
destroy() {
|
|
107
169
|
if (this._destroyed) return;
|
|
108
170
|
this._destroyed = true;
|
package/lib/text/font.js
CHANGED
|
@@ -240,6 +240,27 @@ export default class Font {
|
|
|
240
240
|
return this.fk.hasGlyphForCodePoint(codepoint);
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Glyph id for a codepoint, or `null` when this face does not cover it —
|
|
245
|
+
* the lookup twin of `hasGlyph`, for callers that position glyphs
|
|
246
|
+
* themselves (a terminal grid, a tabular column) and hand runs straight
|
|
247
|
+
* to `ctx.drawGlyphs` without shaping. See docs/text.md#glyph-runs.
|
|
248
|
+
*
|
|
249
|
+
* `null` rather than 0: fontkit resolves an uncovered codepoint to the
|
|
250
|
+
* `.notdef` glyph, and a caller building runs wants "not covered" as a
|
|
251
|
+
* branch (pick a fallback face) rather than as a tofu box discovered on
|
|
252
|
+
* screen. This is a cmap lookup only — no shaping, so no ligatures,
|
|
253
|
+
* kerning or contextual forms; text that needs those goes through
|
|
254
|
+
* `shape()`.
|
|
255
|
+
*
|
|
256
|
+
* @param {number} codepoint Unicode codepoint (`str.codePointAt(i)`)
|
|
257
|
+
* @returns {number|null} font glyph id, as `shape()` would report in `glyphs[].id`
|
|
258
|
+
*/
|
|
259
|
+
glyphIdFor(codepoint) {
|
|
260
|
+
if (!this.fk.hasGlyphForCodePoint(codepoint)) return null;
|
|
261
|
+
return this.fk.glyphForCodePoint(codepoint).id;
|
|
262
|
+
}
|
|
263
|
+
|
|
243
264
|
/**
|
|
244
265
|
* Shape a run of text: returns glyphs with pixel-space positioning.
|
|
245
266
|
* RTL runs come back in visual (left-to-right drawing) order.
|