ntk 8.1.0 → 8.2.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 +146 -3
- package/lib/text/glyphs.js +61 -0
- package/lib/text/layout.js +16 -3
- package/package.json +1 -1
|
@@ -52,6 +52,8 @@ import {
|
|
|
52
52
|
compositeTraps,
|
|
53
53
|
drawGlyphRuns,
|
|
54
54
|
encodeGlyphItems,
|
|
55
|
+
positionedRunsInk,
|
|
56
|
+
runId,
|
|
55
57
|
} from "./text/glyphs.js";
|
|
56
58
|
import { TextLayout } from "./text/layout.js";
|
|
57
59
|
import { reorderRuns } from "./text/shape.js";
|
|
@@ -1099,6 +1101,110 @@ class RenderingContext2d {
|
|
|
1099
1101
|
);
|
|
1100
1102
|
}
|
|
1101
1103
|
|
|
1104
|
+
/**
|
|
1105
|
+
* The shadow of positioned glyph runs, cached — what `drawGlyphs`, and
|
|
1106
|
+
* therefore every `TextLayout.draw`, casts (issue #283).
|
|
1107
|
+
*
|
|
1108
|
+
* A paragraph gets **one** coverage surface, not one per line: the runs
|
|
1109
|
+
* already carry their own baselines, so they all go into the same surface
|
|
1110
|
+
* exactly as they all go into the same glyph composite. Nothing is
|
|
1111
|
+
* re-shaped — the caller handed us the runs, which is why this path is
|
|
1112
|
+
* cheaper than `_shadowOfText`, not dearer.
|
|
1113
|
+
*
|
|
1114
|
+
* The cached copy is position-independent, as `fillText`'s is: geometry is
|
|
1115
|
+
* stored relative to the first run's origin and the composite carries it
|
|
1116
|
+
* to wherever the text is drawn. The key is that relative geometry plus
|
|
1117
|
+
* the identity of each run, so the same string laid out to two widths —
|
|
1118
|
+
* same runs, different line origins — is two shadows, and re-drawing one
|
|
1119
|
+
* layout is one lookup no matter how many glyphs are in it.
|
|
1120
|
+
*
|
|
1121
|
+
* @param {Array<{run, x, y, textRendering?}>} positioned device-space runs
|
|
1122
|
+
*/
|
|
1123
|
+
_shadowOfGlyphs(positioned) {
|
|
1124
|
+
if (!positioned.length) return;
|
|
1125
|
+
const app = this.window.app;
|
|
1126
|
+
const policy = shadowPolicyOf(app);
|
|
1127
|
+
const sigma = shadowSigma(this._shadowBlur, policy);
|
|
1128
|
+
const reach = shadowReach(sigma);
|
|
1129
|
+
|
|
1130
|
+
// Run origins relative to the first, rounded: whole-pixel offsets are
|
|
1131
|
+
// what the bitmap glyph path draws at anyway, and they keep the key
|
|
1132
|
+
// stable as the paragraph moves — `(x + a) - (x + b)` is not exactly
|
|
1133
|
+
// `a - b` in floating point, and an origin-dependent key would miss the
|
|
1134
|
+
// cache on every scroll.
|
|
1135
|
+
const ax = positioned[0].x;
|
|
1136
|
+
const ay = positioned[0].y;
|
|
1137
|
+
const local = positioned.map((p) => ({
|
|
1138
|
+
run: p.run,
|
|
1139
|
+
x: Math.round(p.x - ax),
|
|
1140
|
+
y: Math.round(p.y - ay),
|
|
1141
|
+
textRendering: p.textRendering,
|
|
1142
|
+
}));
|
|
1143
|
+
const key = `${local
|
|
1144
|
+
.map((p) => `${runId(p.run)},${p.x},${p.y},${p.textRendering ?? ""}`)
|
|
1145
|
+
.join("\u0000")}\u0000${sigma}`;
|
|
1146
|
+
|
|
1147
|
+
let ink = null;
|
|
1148
|
+
const surface = cachedShadow(app, key, () => {
|
|
1149
|
+
ink = positionedRunsInk(local);
|
|
1150
|
+
if (!ink) return null; // a line of spaces inks nothing
|
|
1151
|
+
const box = {
|
|
1152
|
+
x: Math.floor(ink.minX) - 1 - reach,
|
|
1153
|
+
y: Math.floor(ink.minY) - 1 - reach,
|
|
1154
|
+
};
|
|
1155
|
+
box.w = Math.ceil(ink.maxX) + 1 + reach - box.x;
|
|
1156
|
+
box.h = Math.ceil(ink.maxY) + 1 + reach - box.y;
|
|
1157
|
+
if (box.w * box.h > policy.maxPixels) return null;
|
|
1158
|
+
let coverage = new Surface(app, {
|
|
1159
|
+
width: box.w,
|
|
1160
|
+
height: box.h,
|
|
1161
|
+
format: "a8",
|
|
1162
|
+
});
|
|
1163
|
+
coverage.render((sctx) => {
|
|
1164
|
+
this._loadShadowState(sctx, 0, 0);
|
|
1165
|
+
// the origins are already device-space and placed by hand
|
|
1166
|
+
sctx._m = [1, 0, 0, 1, 0, 0];
|
|
1167
|
+
sctx._drawGlyphsDevice(
|
|
1168
|
+
this.Render.PictOp.Over,
|
|
1169
|
+
sctx._backgroundPicture,
|
|
1170
|
+
local.map((p) => ({ ...p, x: p.x - box.x, y: p.y - box.y })),
|
|
1171
|
+
);
|
|
1172
|
+
});
|
|
1173
|
+
if (sigma > 0) coverage = blurCoverage(coverage, sigma);
|
|
1174
|
+
// where the anchor sits inside the surface — whole pixels, so the
|
|
1175
|
+
// composite below can carry it anywhere
|
|
1176
|
+
coverage._shadowOrigin = { x: -box.x, y: -box.y };
|
|
1177
|
+
return coverage;
|
|
1178
|
+
});
|
|
1179
|
+
if (surface) {
|
|
1180
|
+
const origin = surface._shadowOrigin;
|
|
1181
|
+
this._paintShadow(
|
|
1182
|
+
surface,
|
|
1183
|
+
Math.round(ax + this._shadowOffsetX) - origin.x,
|
|
1184
|
+
Math.round(ay + this._shadowOffsetY) - origin.y,
|
|
1185
|
+
);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (!ink) return;
|
|
1189
|
+
// Padded ink larger than a shadow surface may be: fall back to the
|
|
1190
|
+
// clipped, uncached path, which sizes itself to the part of the shadow
|
|
1191
|
+
// that can actually be seen — the same escape `_shadowOfText` takes.
|
|
1192
|
+
this._shadowOfDrawing(
|
|
1193
|
+
{
|
|
1194
|
+
minX: ax + ink.minX,
|
|
1195
|
+
maxX: ax + ink.maxX,
|
|
1196
|
+
minY: ay + ink.minY,
|
|
1197
|
+
maxY: ay + ink.maxY,
|
|
1198
|
+
},
|
|
1199
|
+
(sctx, dx, dy) =>
|
|
1200
|
+
sctx._drawGlyphsDevice(
|
|
1201
|
+
this.Render.PictOp.Over,
|
|
1202
|
+
sctx._backgroundPicture,
|
|
1203
|
+
positioned.map((p) => ({ ...p, x: p.x + dx, y: p.y + dy })),
|
|
1204
|
+
),
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1102
1208
|
/** the shadow of a `fill()`/`stroke()`-shaped call, from its arguments */
|
|
1103
1209
|
_shadowOfPath(args, stroke) {
|
|
1104
1210
|
if (stroke) {
|
|
@@ -2131,7 +2237,9 @@ class RenderingContext2d {
|
|
|
2131
2237
|
* @param {Picture} src source picture the glyphs paint with — a solid
|
|
2132
2238
|
* (`ctx.createSolidPicture(r, g, b, a)`, premultiplied 0..1) or a gradient
|
|
2133
2239
|
* @param {Array<{run, x, y, textRendering?}>} positioned runs in visual
|
|
2134
|
-
* order; `x`/`y` is the run's baseline origin in
|
|
2240
|
+
* order; `x`/`y` is the run's baseline origin in **user space** — the
|
|
2241
|
+
* current transform applies to it, as it does to every other drawing
|
|
2242
|
+
* call. `run` is
|
|
2135
2243
|
* `{ font, size, glyphs }` — a `Font`, a pixel size, and glyphs
|
|
2136
2244
|
* `{ id, ax, dx, dy }` in drawing order: `id` a font glyph id
|
|
2137
2245
|
* (`Font.shape()`'s `glyphs[].id`, or `Font.glyphIdFor(cp)`), `ax` the
|
|
@@ -2142,6 +2250,39 @@ class RenderingContext2d {
|
|
|
2142
2250
|
* (`codePoints`, `width`, …) are ignored. `textRendering` optionally
|
|
2143
2251
|
* overrides the bitmap/vector routing per run (docs/text.md).
|
|
2144
2252
|
*
|
|
2253
|
+
* The transform moves each run's origin, exactly as `fillText` moves its
|
|
2254
|
+
* anchor; the glyphs themselves are not rotated or scaled by it (size the
|
|
2255
|
+
* font via `ctx.font`, or `run.size`, instead). Advances and `dx`/`dy` are
|
|
2256
|
+
* therefore device pixels on both calls. Without this, a `TextLayout`
|
|
2257
|
+
* drawn into a translated context — which is every react-x11 canvas that
|
|
2258
|
+
* is not at the window's origin — landed at the untransformed coordinates
|
|
2259
|
+
* and was then cut by the clip, while the neighbouring `fillRect` and
|
|
2260
|
+
* `drawImage` moved (issue #280).
|
|
2261
|
+
*
|
|
2262
|
+
* The shadow state applies too, as it does to `fillText`: one blurred
|
|
2263
|
+
* coverage surface for the whole call, cached on the runs' identity and
|
|
2264
|
+
* relative positions, painted under the glyphs (issue #283). A paragraph
|
|
2265
|
+
* whose spans change colour draws as several calls, and — as several
|
|
2266
|
+
* `fillText`s would — casts a shadow per call.
|
|
2267
|
+
*/
|
|
2268
|
+
drawGlyphs(op, src, positioned) {
|
|
2269
|
+
const m = this._m;
|
|
2270
|
+
if (!matIsIdentity(m)) {
|
|
2271
|
+
positioned = positioned.map((p) => {
|
|
2272
|
+
const [x, y] = matApply(m, p.x, p.y);
|
|
2273
|
+
return { ...p, x, y };
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
if (this._shadowed()) this._shadowOfGlyphs(positioned);
|
|
2277
|
+
this._drawGlyphsDevice(op, src, positioned);
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
/**
|
|
2281
|
+
* `drawGlyphs` with the origins already in device space — the primitive
|
|
2282
|
+
* under it, for callers that place glyphs themselves (`fillText`, which
|
|
2283
|
+
* has to add the alignment and baseline offsets *after* the transform,
|
|
2284
|
+
* because glyph advances are device pixels).
|
|
2285
|
+
*
|
|
2145
2286
|
* CompositeGlyphs writes straight to the destination picture, so it has
|
|
2146
2287
|
* no way to consult our clip mask — text drawn through it used to spill
|
|
2147
2288
|
* out of clipped boxes while every fill and stroke stayed inside. With a
|
|
@@ -2150,7 +2291,7 @@ class RenderingContext2d {
|
|
|
2150
2291
|
* through the result — the same shape as _fillPolys. A rectangular clip
|
|
2151
2292
|
* takes the server-side fast path below instead of the mask.
|
|
2152
2293
|
*/
|
|
2153
|
-
|
|
2294
|
+
_drawGlyphsDevice(op, src, positioned) {
|
|
2154
2295
|
const app = this.window.app;
|
|
2155
2296
|
const R = this.Render;
|
|
2156
2297
|
if (!prepareStyle(src, this._m)) return;
|
|
@@ -3314,7 +3455,9 @@ class RenderingContext2d {
|
|
|
3314
3455
|
});
|
|
3315
3456
|
cursor += run.width;
|
|
3316
3457
|
}
|
|
3317
|
-
|
|
3458
|
+
// already device space: the anchor went through the matrix above, and
|
|
3459
|
+
// the offsets and advances added to it are device pixels
|
|
3460
|
+
this._drawGlyphsDevice(
|
|
3318
3461
|
this.Render.PictOp.Over,
|
|
3319
3462
|
this._backgroundPicture,
|
|
3320
3463
|
positioned,
|
package/lib/text/glyphs.js
CHANGED
|
@@ -225,6 +225,67 @@ export function positionGlyphs(positioned) {
|
|
|
225
225
|
return out;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Ink extents of positioned runs, in whatever coordinates their origins are
|
|
230
|
+
* given in — the union of every glyph's bounding box, laid out exactly as
|
|
231
|
+
* `positionGlyphs` lays it out (pen at `x`, glyph at `pen + dx`, `y - dy`,
|
|
232
|
+
* pen advanced by `ax`).
|
|
233
|
+
*
|
|
234
|
+
* Returns `null` when nothing inks: a run of spaces has extents but no
|
|
235
|
+
* bounding box, and neither does an empty array. Blank glyphs report an
|
|
236
|
+
* empty `cbox` (`minX` infinite), which the comparisons below drop on their
|
|
237
|
+
* own.
|
|
238
|
+
*
|
|
239
|
+
* This is what sizes a glyph shadow's coverage surface — the run-shaped
|
|
240
|
+
* counterpart of the context's `_shapedInk`, which measures one shaped
|
|
241
|
+
* string from its own origin.
|
|
242
|
+
*
|
|
243
|
+
* @param {Array<{run, x, y}>} positioned
|
|
244
|
+
* @returns {{minX: number, minY: number, maxX: number, maxY: number}|null}
|
|
245
|
+
*/
|
|
246
|
+
export function positionedRunsInk(positioned) {
|
|
247
|
+
let minX = Infinity;
|
|
248
|
+
let minY = Infinity;
|
|
249
|
+
let maxX = -Infinity;
|
|
250
|
+
let maxY = -Infinity;
|
|
251
|
+
for (const { run, x, y } of positioned) {
|
|
252
|
+
let cursor = x;
|
|
253
|
+
for (const g of run.glyphs) {
|
|
254
|
+
const e = run.font.glyphExtents(g.id, run.size);
|
|
255
|
+
const gx = cursor + g.dx;
|
|
256
|
+
const gy = y - g.dy;
|
|
257
|
+
cursor += g.ax;
|
|
258
|
+
if (gx + e.minX < minX) minX = gx + e.minX;
|
|
259
|
+
if (gx + e.maxX > maxX) maxX = gx + e.maxX;
|
|
260
|
+
if (gy + e.minY < minY) minY = gy + e.minY;
|
|
261
|
+
if (gy + e.maxY > maxY) maxY = gy + e.maxY;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return minX <= maxX && minY <= maxY ? { minX, minY, maxX, maxY } : null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Identity, not content, for anything that wants to name a run cheaply.
|
|
268
|
+
// A shaped run is immutable and shared — the shaping memo hands the same
|
|
269
|
+
// object back, and a TextLayout holds on to the ones its lines are made of
|
|
270
|
+
// — so a small integer per object is a complete name for the glyphs in it,
|
|
271
|
+
// bought at O(1) instead of O(glyphs). Weak, so naming a run keeps nothing
|
|
272
|
+
// alive.
|
|
273
|
+
const runIds = new WeakMap();
|
|
274
|
+
let nextRunId = 0;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* A stable small integer for one run object, for cache keys that would
|
|
278
|
+
* otherwise have to serialize its glyphs. Runs built fresh on every draw
|
|
279
|
+
* (rather than kept, as a `TextLayout` keeps them) get a fresh id each time
|
|
280
|
+
* and so never hit such a cache — which is the honest answer, since nothing
|
|
281
|
+
* cheap can tell them apart.
|
|
282
|
+
*/
|
|
283
|
+
export function runId(run) {
|
|
284
|
+
let id = runIds.get(run);
|
|
285
|
+
if (id === undefined) runIds.set(run, (id = ++nextRunId));
|
|
286
|
+
return id;
|
|
287
|
+
}
|
|
288
|
+
|
|
228
289
|
/**
|
|
229
290
|
* Decide how a (face, size) renders: cached bitmap glyphs or per-draw
|
|
230
291
|
* trapezoids. See DEFAULT_TEXT_POLICY for the reasoning; the middle band
|
package/lib/text/layout.js
CHANGED
|
@@ -468,9 +468,21 @@ export class TextLayout {
|
|
|
468
468
|
}
|
|
469
469
|
|
|
470
470
|
/**
|
|
471
|
-
* Draw onto a 2d context at (x, y) = top-left of the layout box
|
|
471
|
+
* Draw onto a 2d context at (x, y) = top-left of the layout box, in the
|
|
472
|
+
* context's **user space**: the current transform applies to the origin,
|
|
473
|
+
* the same way it applies to `fillText`, `fillRect` and `drawImage`, so a
|
|
474
|
+
* paragraph drawn into a translated context lands where the rest of the
|
|
475
|
+
* drawing does (issue #280). The glyphs themselves are not rotated or
|
|
476
|
+
* scaled by it — set the span size instead.
|
|
477
|
+
*
|
|
472
478
|
* Span `color`s override the context fillStyle; consecutive same-color
|
|
473
479
|
* runs are batched into single CompositeGlyphs requests.
|
|
480
|
+
*
|
|
481
|
+
* `caretPosition`/`indexAt` and the line/run geometry speak the same
|
|
482
|
+
* layout-relative coordinates as the (x, y) here, so hit testing stays
|
|
483
|
+
* `layout.indexAt(px - x, py - y)` — with (px, py) in user space too,
|
|
484
|
+
* which for a pointer event under a transformed context means undoing
|
|
485
|
+
* `ctx.getTransform()` first.
|
|
474
486
|
*/
|
|
475
487
|
draw(ctx, x = 0, y = 0) {
|
|
476
488
|
const app = ctx.window.app;
|
|
@@ -484,8 +496,9 @@ export class TextLayout {
|
|
|
484
496
|
const flush = () => {
|
|
485
497
|
if (batch.length === 0) return;
|
|
486
498
|
const src = batchColor ? ctx._stylePicture(batchColor) : ctx._backgroundPicture;
|
|
487
|
-
// via the context so the clip
|
|
488
|
-
// straight onto the picture and
|
|
499
|
+
// via the context so the clip and the transform are applied
|
|
500
|
+
// (drawGlyphRuns composites straight onto the picture and can see
|
|
501
|
+
// neither)
|
|
489
502
|
if (typeof ctx.drawGlyphs === 'function') {
|
|
490
503
|
ctx.drawGlyphs(Render.PictOp.Over, src, batch);
|
|
491
504
|
} else {
|