ntk 8.1.1 → 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 +113 -0
- package/lib/text/glyphs.js +61 -0
- 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) {
|
|
@@ -2152,6 +2258,12 @@ class RenderingContext2d {
|
|
|
2152
2258
|
* is not at the window's origin — landed at the untransformed coordinates
|
|
2153
2259
|
* and was then cut by the clip, while the neighbouring `fillRect` and
|
|
2154
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.
|
|
2155
2267
|
*/
|
|
2156
2268
|
drawGlyphs(op, src, positioned) {
|
|
2157
2269
|
const m = this._m;
|
|
@@ -2161,6 +2273,7 @@ class RenderingContext2d {
|
|
|
2161
2273
|
return { ...p, x, y };
|
|
2162
2274
|
});
|
|
2163
2275
|
}
|
|
2276
|
+
if (this._shadowed()) this._shadowOfGlyphs(positioned);
|
|
2164
2277
|
this._drawGlyphsDevice(op, src, positioned);
|
|
2165
2278
|
}
|
|
2166
2279
|
|
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
|