ntk 7.5.0 → 7.6.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/lib/renderingcontext_2d.js +97 -2
- package/lib/surface.js +62 -0
- package/lib/text/font.js +21 -0
- package/package.json +1 -1
|
@@ -1396,14 +1396,35 @@ class RenderingContext2d {
|
|
|
1396
1396
|
}
|
|
1397
1397
|
|
|
1398
1398
|
/**
|
|
1399
|
-
* Composite
|
|
1399
|
+
* Composite glyph runs onto this context, honouring the clip. This is
|
|
1400
|
+
* the primitive `fillText` and `TextLayout.draw` are built on, and it is
|
|
1401
|
+
* public: the run shape below is a documented contract
|
|
1402
|
+
* (docs/text.md#glyph-runs), so renderers that position glyphs
|
|
1403
|
+
* themselves — a terminal grid, a tabular column — can hand-build runs
|
|
1404
|
+
* instead of shaping.
|
|
1405
|
+
*
|
|
1406
|
+
* @param {number} op Render.PictOp (`ctx.Render.PictOp.Over` for normal text)
|
|
1407
|
+
* @param {Picture} src source picture the glyphs paint with — a solid
|
|
1408
|
+
* (`ctx.createSolidPicture(r, g, b, a)`, premultiplied 0..1) or a gradient
|
|
1409
|
+
* @param {Array<{run, x, y, textRendering?}>} positioned runs in visual
|
|
1410
|
+
* order; `x`/`y` is the run's baseline origin in device space. `run` is
|
|
1411
|
+
* `{ font, size, glyphs }` — a `Font`, a pixel size, and glyphs
|
|
1412
|
+
* `{ id, ax, dx, dy }` in drawing order: `id` a font glyph id
|
|
1413
|
+
* (`Font.shape()`'s `glyphs[].id`, or `Font.glyphIdFor(cp)`), `ax` the
|
|
1414
|
+
* pen advance in px, `dx`/`dy` the drawing offset from the pen position
|
|
1415
|
+
* (y-up: positive `dy` raises the glyph). The pen starts at `x`; each
|
|
1416
|
+
* glyph inks at `(pen + dx, y - dy)` and then advances it by `ax`.
|
|
1417
|
+
* `Font.shape()` returns runs of exactly this shape; extra fields
|
|
1418
|
+
* (`codePoints`, `width`, …) are ignored. `textRendering` optionally
|
|
1419
|
+
* overrides the bitmap/vector routing per run (docs/text.md).
|
|
1400
1420
|
*
|
|
1401
1421
|
* CompositeGlyphs writes straight to the destination picture, so it has
|
|
1402
1422
|
* no way to consult our clip mask — text drawn through it used to spill
|
|
1403
1423
|
* out of clipped boxes while every fill and stroke stayed inside. With a
|
|
1404
1424
|
* clip active, render the glyph coverage into the scratch a8 mask
|
|
1405
1425
|
* instead, intersect that with the clip, and paint the real source
|
|
1406
|
-
* through the result — the same shape as _fillPolys.
|
|
1426
|
+
* through the result — the same shape as _fillPolys. A rectangular clip
|
|
1427
|
+
* takes the server-side fast path below instead of the mask.
|
|
1407
1428
|
*/
|
|
1408
1429
|
drawGlyphs(op, src, positioned) {
|
|
1409
1430
|
const app = this.window.app;
|
|
@@ -1652,6 +1673,80 @@ class RenderingContext2d {
|
|
|
1652
1673
|
this._fillPolys(flattenPath(tmp._cmds, this._m), "nonzero");
|
|
1653
1674
|
}
|
|
1654
1675
|
|
|
1676
|
+
/**
|
|
1677
|
+
* Fill a batch of axis-aligned rectangles: `fillRect` once per rectangle
|
|
1678
|
+
* semantically — fillStyle, globalAlpha, the composite op, the clip and
|
|
1679
|
+
* damage reporting all apply — but priced for the "many small rectangles
|
|
1680
|
+
* per frame" caller (issue #253): terminal cell backgrounds, sparkline
|
|
1681
|
+
* bars, heat maps, row striping.
|
|
1682
|
+
*
|
|
1683
|
+
* `rects` is an array of `[x, y, w, h]` quadruples or one flat
|
|
1684
|
+
* `[x0, y0, w0, h0, x1, ...]` array; rectangles with non-positive width
|
|
1685
|
+
* or height are skipped.
|
|
1686
|
+
*
|
|
1687
|
+
* A solid-colour fillStyle under an identity transform and a rectangular
|
|
1688
|
+
* (or absent) clip compiles the whole list into a single
|
|
1689
|
+
* `Render.FillRectangles`, where N `fillRect` calls cost N composites.
|
|
1690
|
+
* Gradient/Picture styles, transforms and non-rectangular clips fall
|
|
1691
|
+
* back to that `fillRect` loop, so the answer is always right and only
|
|
1692
|
+
* the request count varies.
|
|
1693
|
+
*/
|
|
1694
|
+
fillRects(rects) {
|
|
1695
|
+
if (!rects || !rects.length || this.globalAlpha <= 0) return;
|
|
1696
|
+
// normalize to one flat list, dropping empty rectangles up front — the
|
|
1697
|
+
// wire encodes width and height unsigned, so a negative would wrap
|
|
1698
|
+
const flat = [];
|
|
1699
|
+
if (Array.isArray(rects[0])) {
|
|
1700
|
+
for (const r of rects) {
|
|
1701
|
+
if (r[2] > 0 && r[3] > 0) flat.push(r[0], r[1], r[2], r[3]);
|
|
1702
|
+
}
|
|
1703
|
+
} else {
|
|
1704
|
+
for (let i = 0; i + 3 < rects.length; i += 4) {
|
|
1705
|
+
if (rects[i + 2] > 0 && rects[i + 3] > 0)
|
|
1706
|
+
flat.push(rects[i], rects[i + 1], rects[i + 2], rects[i + 3]);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
if (!flat.length) return;
|
|
1710
|
+
|
|
1711
|
+
const clip = this._clips.length ? this._clipRect() : null;
|
|
1712
|
+
if (
|
|
1713
|
+
!matIsIdentity(this._m) ||
|
|
1714
|
+
!isPlainColor(this._fillStyle) ||
|
|
1715
|
+
(this._clips.length && !clip)
|
|
1716
|
+
) {
|
|
1717
|
+
for (let i = 0; i < flat.length; i += 4)
|
|
1718
|
+
this.fillRect(flat[i], flat[i + 1], flat[i + 2], flat[i + 3]);
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
if (clip && (clip.w === 0 || clip.h === 0)) return; // clipped away
|
|
1722
|
+
const R = this.Render;
|
|
1723
|
+
if (clip) {
|
|
1724
|
+
R.SetPictureClipRectangles(this.picture.id, 0, 0, [
|
|
1725
|
+
clip.x,
|
|
1726
|
+
clip.y,
|
|
1727
|
+
clip.w,
|
|
1728
|
+
clip.h,
|
|
1729
|
+
]);
|
|
1730
|
+
}
|
|
1731
|
+
// globalAlpha folds into the premultiplied colour — scaling all four
|
|
1732
|
+
// components is exactly what compositing at that opacity means, for
|
|
1733
|
+
// any composite op (the mask slot would only scale the source the same
|
|
1734
|
+
// way)
|
|
1735
|
+
const color = this._foldedColor(this._fillStyle);
|
|
1736
|
+
// stay under the server's maximum request size
|
|
1737
|
+
const chunk = 10000 * 4;
|
|
1738
|
+
for (let i = 0; i < flat.length; i += chunk) {
|
|
1739
|
+
R.FillRectangles(
|
|
1740
|
+
this._op(),
|
|
1741
|
+
this.picture.id,
|
|
1742
|
+
color,
|
|
1743
|
+
i === 0 && flat.length <= chunk ? flat : flat.slice(i, i + chunk),
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
if (clip) this._resetPictureClip();
|
|
1747
|
+
this._markDirty();
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1655
1750
|
strokeRect(x, y, w, h) {
|
|
1656
1751
|
const m = this._m;
|
|
1657
1752
|
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.
|