@hncode/hncode 0.5.5 → 0.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.
Files changed (56) hide show
  1. package/package.json +37 -38
  2. package/src/agent.js +67 -2
  3. package/src/image-row.js +111 -0
  4. package/src/latex.js +1557 -0
  5. package/src/llm.js +19 -4
  6. package/src/mermaid/ansi.js +55 -0
  7. package/src/mermaid/canvas.js +449 -0
  8. package/src/mermaid/class-style.js +57 -0
  9. package/src/mermaid/css-colors.js +153 -0
  10. package/src/mermaid/diagrams/class.js +289 -0
  11. package/src/mermaid/diagrams/er.js +201 -0
  12. package/src/mermaid/diagrams/flowchart.js +488 -0
  13. package/src/mermaid/diagrams/gitgraph.js +205 -0
  14. package/src/mermaid/diagrams/mindmap.js +101 -0
  15. package/src/mermaid/diagrams/pie.js +104 -0
  16. package/src/mermaid/diagrams/sequence.js +282 -0
  17. package/src/mermaid/diagrams/state.js +292 -0
  18. package/src/mermaid/diagrams/timeline.js +99 -0
  19. package/src/mermaid/graph.js +138 -0
  20. package/src/mermaid/index.js +59 -0
  21. package/src/mermaid/labels.js +339 -0
  22. package/src/mermaid/layout-seq.js +225 -0
  23. package/src/mermaid/layout.js +1454 -0
  24. package/src/mermaid/registry.js +54 -0
  25. package/src/mermaid/source-box.js +102 -0
  26. package/src/mermaid/statements.js +235 -0
  27. package/src/mermaid/types.js +1 -0
  28. package/src/mermaid/width.js +29 -0
  29. package/src/screen-diff.js +512 -0
  30. package/src/screen-paint.js +128 -0
  31. package/src/screen.js +277 -0
  32. package/src/subagent-types.js +136 -147
  33. package/src/term-caps.js +10 -4
  34. package/src/tex/font.js +292 -0
  35. package/src/tex/fonts/LICENSE.txt +11 -0
  36. package/src/tex/fonts/STIX2Math.ttf +0 -0
  37. package/src/tex/index.js +293 -0
  38. package/src/tex/kitty.js +186 -0
  39. package/src/tex/layout.js +446 -0
  40. package/src/tex/parse.js +331 -0
  41. package/src/tex/raster.js +216 -0
  42. package/src/tex/render.js +167 -0
  43. package/src/tex/sixel.js +326 -0
  44. package/src/tex/svg.js +237 -0
  45. package/src/tool-result.js +2 -2
  46. package/src/tools/agent-swarm.js +3 -15
  47. package/src/tools/agent.js +454 -464
  48. package/src/tools/ask-user-question.js +195 -206
  49. package/src/tools/bash.js +39 -3
  50. package/src/tools/edit.js +10 -112
  51. package/src/tools/fetch-url.js +130 -120
  52. package/src/tools/memory.js +80 -88
  53. package/src/tools/read.js +27 -23
  54. package/src/tools/todo.js +8 -13
  55. package/src/tools/validate-args.js +78 -0
  56. package/src/tui.js +1346 -496
@@ -0,0 +1,331 @@
1
+ /**
2
+ * TeX → a laid-out box tree, measured in font units.
3
+ *
4
+ * The output of this module is geometric, not pixels: each node knows where it goes relative to its parent
5
+ * and how big it is, so the rasteriser only has to walk the tree once. Sizes follow TeX's rules — a
6
+ * superscript is 0.7 of its base, a sub-subscript 0.5 — because matching them is what makes a formula look
7
+ * typeset rather than assembled.
8
+ *
9
+ * Scope: the constructs that appear in discussion rather than in a paper. Fractions, roots, sub- and
10
+ * superscripts, big operators with limits, matrices and `cases`, accents, and text runs. Anything else
11
+ * degrades to its own source rather than to a half-drawn box.
12
+ */
13
+
14
+ import { SYMBOLS } from '../latex.js';
15
+
16
+ const SCRIPT_RATIO = 0.7;
17
+ const SCRIPT_SCRIPT_RATIO = 0.55;
18
+
19
+ /** Vertical space around a fraction bar, in em fractions. */
20
+ const FRAC_GAP = 0.12;
21
+ /** Gap between a big operator and its limits. */
22
+ const LIMIT_GAP = 0.08;
23
+ /** Space either side of a binary operator, in em. */
24
+ const BIN_SPACE = 0.22;
25
+ /** Space after a comma in a list. */
26
+ const PUNCT_SPACE = 0.16;
27
+
28
+ /** Glyph classes that need spacing around them. */
29
+ const BINARY = new Set(['+', '−', '-', '±', '∓', '×', '÷', '·', '∗', '∪', '∩', '∧', '∨', '⊕', '⊗']);
30
+ const RELATION = new Set(['=', '≠', '≈', '≡', '≤', '≥', '<', '>', '∈', '∉', '⊂', '⊃', '⊆', '⊇', '→', '←', '↔', '⇒', '⇐', '⇔', '∝']);
31
+ const RELATION_NAMES = new Set(['leq', 'le', 'geq', 'ge', 'neq', 'ne', 'approx', 'equiv', 'sim', 'simeq',
32
+ 'propto', 'in', 'notin', 'subset', 'supset', 'subseteq', 'supseteq', 'to', 'rightarrow', 'leftarrow',
33
+ 'leftrightarrow', 'Rightarrow', 'Leftarrow', 'Leftrightarrow', 'implies', 'iff', 'mapsto']);
34
+ const BINARY_NAMES = new Set(['pm', 'mp', 'times', 'div', 'cdot', 'ast', 'cup', 'cap', 'wedge', 'vee',
35
+ 'oplus', 'otimes', 'land', 'lor', 'setminus']);
36
+
37
+ /** Operators that take their limits above and below rather than to the right. */
38
+ const BIG_OPERATORS = new Set(['sum', 'prod', 'int', 'oint', 'bigcup', 'bigcap', 'coprod']);
39
+ const BIG_GLYPHS = { sum: '∑', prod: '∏', int: '∫', oint: '∮', bigcup: '⋃', bigcap: '⋂', coprod: '∐' };
40
+
41
+ /** Accent commands and the combining variant a terminal font carries. */
42
+ const ACCENTS = {
43
+ hat: '\u0302', widehat: '\u0302',
44
+ bar: '\u0304', overline: '\u0304',
45
+ vec: '\u20d7', overrightarrow: '\u20d7',
46
+ tilde: '\u0303', widetilde: '\u0303',
47
+ dot: '\u0307', ddot: '\u0308',
48
+ };
49
+
50
+ /**
51
+ * A box: width/height/depth in font units, plus the children that paint into it.
52
+ * `x`/`y` on a child are relative to this box's origin at its baseline on the left edge.
53
+ */
54
+ function box(width, height, depth) {
55
+ return { width, height, depth, children: [] };
56
+ }
57
+
58
+ const em = (n) => Math.round(n);
59
+
60
+ /**
61
+ * Tokenise TeX into commands, groups and single characters.
62
+ * Braces become explicit tokens so the parser can follow them.
63
+ */
64
+ function tokenize(src) {
65
+ const out = [];
66
+ let i = 0;
67
+ while (i < src.length) {
68
+ const ch = src[i];
69
+
70
+ if (ch === '\\') {
71
+ const m = /^\\([A-Za-z]+|.)/.exec(src.slice(i));
72
+ if (!m) { i++; continue; }
73
+ out.push({ type: 'cmd', name: m[1] });
74
+ i += m[0].length;
75
+ // A control word swallows the space that follows it.
76
+ if (/^[A-Za-z]+$/.test(m[1])) while (src[i] === ' ') i++;
77
+ continue;
78
+ }
79
+
80
+ if (ch === '{') { out.push({ type: 'open' }); i++; continue; }
81
+ if (ch === '}') { out.push({ type: 'close' }); i++; continue; }
82
+ if (ch === '^') { out.push({ type: 'sup' }); i++; continue; }
83
+ if (ch === '_') { out.push({ type: 'sub' }); i++; continue; }
84
+ if (ch === '&') { out.push({ type: 'align' }); i++; continue; }
85
+ if (ch === ' ' || ch === '\t' || ch === '\n') { i++; continue; }
86
+
87
+ // Group surrogate pairs so an emoji or a rare symbol stays one token.
88
+ const cp = src.codePointAt(i);
89
+ const str = String.fromCodePoint(cp);
90
+ out.push({ type: 'char', value: str });
91
+ i += str.length;
92
+ }
93
+ return out;
94
+ }
95
+
96
+ /**
97
+ * Parse a token stream into an unmeasured node list.
98
+ * Nodes are the semantic pieces: atom, fraction, root, scripts, bigop, row, matrix.
99
+ */
100
+ function parse(tokens, opts = {}) {
101
+ const rows = [];
102
+ let i = 0;
103
+
104
+ const parseGroup = () => {
105
+ // Called with `i` just past an `open`; consumes through its matching `close`.
106
+ const inner = [];
107
+ let depth = 1;
108
+ while (i < tokens.length && depth > 0) {
109
+ if (tokens[i].type === 'open') depth++;
110
+ else if (tokens[i].type === 'close') {
111
+ depth--;
112
+ if (depth === 0) { i++; break; }
113
+ }
114
+ inner.push(tokens[i]);
115
+ i++;
116
+ }
117
+ return parse(inner, opts);
118
+ };
119
+
120
+ const parseArg = () => {
121
+ // One argument: a braced group, a single token, or a command.
122
+ if (i >= tokens.length) return [];
123
+ if (tokens[i].type === 'open') { i++; return parseGroup(); }
124
+ const t = tokens[i++];
125
+ return parse([t], opts);
126
+ };
127
+
128
+ while (i < tokens.length) {
129
+ const t = tokens[i];
130
+
131
+ if (t.type === 'char') {
132
+ i++;
133
+ // Attach scripts to the character just read.
134
+ const node = { kind: 'atom', value: t.value };
135
+ node.scripts = parseScripts();
136
+ rows.push(node);
137
+ continue;
138
+ }
139
+
140
+ if (t.type === 'cmd') {
141
+ const name = t.name;
142
+ i++;
143
+
144
+ if (name === 'frac' || name === 'dfrac' || name === 'tfrac') {
145
+ const num = parseArg();
146
+ const den = parseArg();
147
+ const node = { kind: 'frac', num, den };
148
+ node.scripts = parseScripts();
149
+ rows.push(node);
150
+ continue;
151
+ }
152
+
153
+ if (name === 'sqrt') {
154
+ // An optional index `\sqrt[3]{x}`.
155
+ let index = null;
156
+ if (tokens[i] && tokens[i].type === 'char' && tokens[i].value === '[') {
157
+ i++;
158
+ const idxTokens = [];
159
+ while (i < tokens.length && !(tokens[i].type === 'char' && tokens[i].value === ']')) idxTokens.push(tokens[i++]);
160
+ i++;
161
+ index = parse(idxTokens, opts);
162
+ }
163
+ const body = parseArg();
164
+ const node = { kind: 'root', body, index };
165
+ node.scripts = parseScripts();
166
+ rows.push(node);
167
+ continue;
168
+ }
169
+
170
+ if (name === 'text' || name === 'mathrm' || name === 'mathbf' || name === 'mathit' || name === 'operatorname') {
171
+ const body = parseArg();
172
+ rows.push({ kind: 'row', body, upright: true });
173
+ continue;
174
+ }
175
+
176
+ if (name === 'begin') {
177
+ const env = readEnvName();
178
+ const matrix = parseEnvironment(env);
179
+ rows.push(matrix);
180
+ continue;
181
+ }
182
+
183
+ if (ACCENTS[name]) {
184
+ const body = parseArg();
185
+ const node = { kind: 'accent', accent: ACCENTS[name], body };
186
+ node.scripts = parseScripts();
187
+ rows.push(node);
188
+ continue;
189
+ }
190
+
191
+ if (BIG_OPERATORS.has(name)) {
192
+ const node = { kind: 'bigop', value: BIG_GLYPHS[name], name };
193
+ node.scripts = parseScripts();
194
+ rows.push(node);
195
+ continue;
196
+ }
197
+
198
+ if (name === 'left' || name === 'right') {
199
+ // Delimiter sizing is ignored; the delimiter itself is the next token.
200
+ if (tokens[i] && tokens[i].type === 'char') {
201
+ const delim = tokens[i].value;
202
+ i++;
203
+ if (name === 'left') rows.push({ kind: 'delim', value: delim, side: 'left' });
204
+ else rows.push({ kind: 'delim', value: delim, side: 'right' });
205
+ }
206
+ continue;
207
+ }
208
+
209
+ if (name === 'quad') { rows.push({ kind: 'space', width: 1 }); continue; }
210
+ if (name === 'qquad') { rows.push({ kind: 'space', width: 2 }); continue; }
211
+ if (name === ',' || name === ';' || name === ':' || name === '!') { rows.push({ kind: 'space', width: 0.17 }); continue; }
212
+
213
+ const sym = SYMBOLS[name];
214
+ if (sym !== undefined) {
215
+ const node = { kind: 'atom', value: sym };
216
+ node.scripts = parseScripts();
217
+ if (RELATION_NAMES.has(name)) node.cls = 'rel';
218
+ else if (BINARY_NAMES.has(name)) node.cls = 'bin';
219
+ rows.push(node);
220
+ continue;
221
+ }
222
+
223
+ // Unknown command: show its own source rather than dropping it silently.
224
+ rows.push({ kind: 'atom', value: '\\' + name, cls: 'raw' });
225
+ continue;
226
+ }
227
+
228
+ if (t.type === 'sup' || t.type === 'sub') {
229
+ // A script with nothing before it: keep it attached to an empty atom.
230
+ const node = { kind: 'atom', value: '' };
231
+ node.scripts = parseScripts();
232
+ rows.push(node);
233
+ continue;
234
+ }
235
+
236
+ if (t.type === 'align') {
237
+ i++;
238
+ rows.push({ kind: 'align' });
239
+ continue;
240
+ }
241
+
242
+ // open/close outside a group: treat as grouping noise.
243
+ i++;
244
+ }
245
+
246
+ return rows;
247
+
248
+ function parseScripts() {
249
+ const s = { sup: null, sub: null };
250
+ for (;;) {
251
+ if (i < tokens.length && (tokens[i].type === 'sup' || tokens[i].type === 'sub')) {
252
+ const which = tokens[i].type;
253
+ i++;
254
+ const arg = parseArg();
255
+ if (which === 'sup') s.sup = arg; else s.sub = arg;
256
+ } else break;
257
+ }
258
+ return (s.sup || s.sub) ? s : null;
259
+ }
260
+
261
+ function readEnvName() {
262
+ // `\begin{name}`: the name arrives as a group of characters.
263
+ if (!(tokens[i] && tokens[i].type === 'open')) return '';
264
+ i++;
265
+ let name = '';
266
+ while (i < tokens.length && tokens[i].type !== 'close') {
267
+ if (tokens[i].type === 'char') name += tokens[i].value;
268
+ else if (tokens[i].type === 'cmd') name += tokens[i].name;
269
+ i++;
270
+ }
271
+ i++;
272
+ return name;
273
+ }
274
+
275
+ function parseEnvironment(env) {
276
+ // Collect the body up to the matching `\end{env}`, splitting on `\\` and `&`.
277
+ const cells = [[[]]];
278
+ let depth = 1;
279
+ while (i < tokens.length) {
280
+ const tok = tokens[i];
281
+ if (tok.type === 'cmd' && tok.name === 'begin') {
282
+ depth++;
283
+ cells[cells.length - 1][cells[cells.length - 1].length - 1].push(tok);
284
+ i++;
285
+ continue;
286
+ }
287
+ if (tok.type === 'cmd' && tok.name === 'end') {
288
+ depth--;
289
+ i++;
290
+ // Consume `{env}`.
291
+ if (tokens[i] && tokens[i].type === 'open') {
292
+ i++;
293
+ while (i < tokens.length && tokens[i].type !== 'close') i++;
294
+ i++;
295
+ }
296
+ if (depth === 0) break;
297
+ continue;
298
+ }
299
+ if (tok.type === 'cmd' && tok.name === '\\') {
300
+ cells.push([[]]);
301
+ i++;
302
+ continue;
303
+ }
304
+ if (tok.type === 'align') {
305
+ cells[cells.length - 1].push([]);
306
+ i++;
307
+ continue;
308
+ }
309
+ cells[cells.length - 1][cells[cells.length - 1].length - 1].push(tok);
310
+ i++;
311
+ }
312
+
313
+ const kind = /matrix|array/.test(env) ? 'matrix'
314
+ : /cases/.test(env) ? 'cases'
315
+ : /align|gather|split/.test(env) ? 'align'
316
+ : 'matrix';
317
+
318
+ return {
319
+ kind: 'env',
320
+ env: kind,
321
+ rows: cells.map((row) => row.map((cell) => parse(cell, opts))),
322
+ };
323
+ }
324
+ }
325
+
326
+ /** Parse TeX source into an unmeasured node tree. */
327
+ export function parseTex(src) {
328
+ return parse(tokenize(src));
329
+ }
330
+
331
+ export { FRAC_GAP, LIMIT_GAP, BIN_SPACE, PUNCT_SPACE, SCRIPT_RATIO, SCRIPT_SCRIPT_RATIO, BINARY, RELATION };
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Glyph outlines → antialiased coverage.
3
+ *
4
+ * The rasteriser is a scanline fill with 4×4 supersampling: for each pixel row, every contour crossing adds
5
+ * a sub-sample hit, and a pixel's coverage is the fraction of its 16 sub-samples inside the shape. That is
6
+ * enough for text at the sizes a formula uses, and it avoids the winding-number machinery a vector
7
+ * renderer would carry.
8
+ *
9
+ * Coverage, not colour: the caller decides what colour to composite, which keeps one raster useful for any
10
+ * theme.
11
+ */
12
+
13
+ /** Sub-samples per pixel axis. 4 gives 16 levels of antialiasing. */
14
+ const SS = 4;
15
+
16
+ /**
17
+ * An axis-aligned path flattened from a glyph outline.
18
+ *
19
+ * TrueType contours mix on-curve and off-curve points; consecutive off-curve points imply the control point
20
+ * halfway between them. Flattening to line segments first keeps the fill loop simple.
21
+ */
22
+ function flattenContours(contours, scale, offsetX, offsetY, scaleY) {
23
+ const polys = [];
24
+ for (const contour of contours) {
25
+ if (contour.length < 2) continue;
26
+ const pts = [];
27
+ const n = contour.length;
28
+
29
+ // A contour need not start on a curve point; when it does not, the start is the midpoint between
30
+ // the first two off-curve points.
31
+ let startIdx = contour.findIndex((p) => p.on);
32
+ let startX, startY;
33
+ if (startIdx < 0) {
34
+ startX = (contour[0].x + contour[1].x) / 2;
35
+ startY = (contour[0].y + contour[1].y) / 2;
36
+ startIdx = 0;
37
+ } else {
38
+ startX = contour[startIdx].x;
39
+ startY = contour[startIdx].y;
40
+ }
41
+ const toX = (x) => x * scale + offsetX;
42
+ const toY = (y) => y * scaleY + offsetY;
43
+ pts.push([toX(startX), toY(startY)]);
44
+
45
+ let prevX = startX;
46
+ let prevY = startY;
47
+ let ctrl = null;
48
+
49
+ for (let k = 1; k <= n; k++) {
50
+ const p = contour[(startIdx + k) % n];
51
+ if (p.on) {
52
+ if (ctrl) {
53
+ emitQuad(pts, prevX, prevY, ctrl.x, ctrl.y, p.x, p.y, toX, toY);
54
+ ctrl = null;
55
+ } else {
56
+ pts.push([toX(p.x), toY(p.y)]);
57
+ }
58
+ prevX = p.x; prevY = p.y;
59
+ } else {
60
+ if (ctrl) {
61
+ // Two off-curve points in a row imply an on-curve point halfway between them.
62
+ const mx = (ctrl.x + p.x) / 2;
63
+ const my = (ctrl.y + p.y) / 2;
64
+ emitQuad(pts, prevX, prevY, ctrl.x, ctrl.y, mx, my, toX, toY);
65
+ prevX = mx; prevY = my;
66
+ }
67
+ ctrl = p;
68
+ }
69
+ }
70
+
71
+ // Close the contour back to where it started.
72
+ if (ctrl) emitQuad(pts, prevX, prevY, ctrl.x, ctrl.y, startX, startY, toX, toY);
73
+ polys.push(pts);
74
+ }
75
+ return polys;
76
+ }
77
+
78
+ /** Flatten one quadratic bezier into line segments, appending to `pts`. */
79
+ function emitQuad(pts, x0, y0, cx, cy, x1, y1, toX, toY) {
80
+ const steps = Math.max(2, Math.min(16, Math.ceil(Math.hypot(toX(x1) - toX(x0), toY(y1) - toY(y0)) / 2) || 2));
81
+ for (let i = 1; i <= steps; i++) {
82
+ const t = i / steps;
83
+ const mt = 1 - t;
84
+ const x = mt * mt * x0 + 2 * mt * t * cx + t * t * x1;
85
+ const y = mt * mt * y0 + 2 * mt * t * cy + t * t * y1;
86
+ pts.push([toX(x), toY(y)]);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Rasterise contours into a coverage bitmap.
92
+ *
93
+ * @param {Array<Array<{x:number,y:number,on:boolean}>>} contours Glyph outline in font units.
94
+ * @param {number} scale Font units → pixels.
95
+ * @param {number} offsetX Where the glyph origin lands, in pixels.
96
+ * @param {number} offsetY Where the baseline lands, in pixels (y grows DOWN in the output).
97
+ * @param {number} width Bitmap width in pixels.
98
+ * @param {number} height Bitmap height in pixels.
99
+ * @returns {Float32Array} Coverage 0..1 per pixel, row-major.
100
+ */
101
+ export function rasterize(contours, scale, offsetX, offsetY, width, height) {
102
+ const cover = new Float32Array(width * height);
103
+ if (!contours.length || width <= 0 || height <= 0) return cover;
104
+
105
+ // The outline's y grows up; the bitmap's grows down, so the offset flips it.
106
+ // Negating the y scale flips the outline into bitmap space, so `offsetY` is a real baseline row.
107
+ const polys = flattenContours(contours, scale, offsetX, offsetY, -scale);
108
+ if (!polys.length) return cover;
109
+
110
+ let minY = Infinity;
111
+ let maxY = -Infinity;
112
+ for (const poly of polys) for (const [, y] of poly) { if (y < minY) minY = y; if (y > maxY) maxY = y; }
113
+
114
+ const y0 = Math.max(0, Math.floor(minY));
115
+ const y1 = Math.min(height - 1, Math.ceil(maxY));
116
+
117
+ // Horizontal edges never contribute a crossing; skip them.
118
+ const edges = [];
119
+ for (const poly of polys) {
120
+ for (let i = 0; i < poly.length; i++) {
121
+ const [ax, ay] = poly[i];
122
+ const [bx, by] = poly[(i + 1) % poly.length];
123
+ if (ay === by) continue;
124
+ edges.push(ax < bx ? [ax, ay, bx, by] : [bx, by, ax, ay]);
125
+ }
126
+ }
127
+ if (!edges.length) return cover;
128
+
129
+ const rowHits = new Map(); // pixel index -> sub-sample hits
130
+
131
+ for (let py = y0; py <= y1; py++) {
132
+ for (let sy = 0; sy < SS; sy++) {
133
+ const y = py + (sy + 0.5) / SS;
134
+ const xs = [];
135
+ for (const [ax, ay, bx, by] of edges) {
136
+ if ((y >= ay && y < by) || (y >= by && y < ay)) {
137
+ const t = (y - ay) / (by - ay);
138
+ xs.push(ax + (bx - ax) * t);
139
+ }
140
+ }
141
+ if (xs.length < 2) continue;
142
+ xs.sort((a, b) => a - b);
143
+ // Even-odd fill: spans between crossing pairs are inside.
144
+ for (let i = 0; i + 1 < xs.length; i += 2) {
145
+ const spanFrom = xs[i];
146
+ const spanTo = xs[i + 1];
147
+ if (spanTo <= spanFrom) continue;
148
+ const pxFrom = Math.max(0, Math.floor(spanFrom));
149
+ const pxTo = Math.min(width - 1, Math.ceil(spanTo) - 1);
150
+ for (let px = pxFrom; px <= pxTo; px++) {
151
+ // Sub-sample coverage of this pixel by this span.
152
+ const left = Math.max(spanFrom, px);
153
+ const right = Math.min(spanTo, px + 1);
154
+ if (right <= left) continue;
155
+ for (let sx = 0; sx < SS; sx++) {
156
+ const x = px + (sx + 0.5) / SS;
157
+ if (x >= left && x < right) {
158
+ const key = py * width + px;
159
+ rowHits.set(key, (rowHits.get(key) || 0) + 1);
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ const total = SS * SS;
168
+ for (const [key, hits] of rowHits) cover[key] = Math.min(1, hits / total);
169
+ return cover;
170
+ }
171
+
172
+ /**
173
+ * Composite a coverage bitmap onto an RGBA buffer.
174
+ *
175
+ * @param {Uint8ClampedArray} rgba Destination, length width*height*4.
176
+ * @param {Float32Array} cover Coverage 0..1, row-major.
177
+ * @param {number} width Bitmap width.
178
+ * @param {number} height Bitmap height.
179
+ * @param {{r:number,g:number,b:number}} colour Ink colour.
180
+ * @param {{x:number,y:number}} at Where the coverage bitmap's top-left goes.
181
+ */
182
+ /**
183
+ * Composite a coverage bitmap onto an RGBA buffer.
184
+ *
185
+ * `stride` is the destination width, which is not the coverage width: the glyph is a small patch placed
186
+ * inside a larger image, and using one width for both wrote every row at the wrong offset.
187
+ *
188
+ * @param {Uint8ClampedArray} rgba Destination, length stride*height*4.
189
+ * @param {Float32Array} cover Coverage 0..1, row-major, width*height.
190
+ * @param {number} width Coverage width.
191
+ * @param {number} height Coverage height.
192
+ * @param {number} stride Destination width in pixels.
193
+ * @param {{r:number,g:number,b:number}} colour Ink colour.
194
+ * @param {{x:number,y:number}} at Where the coverage patch goes, in destination pixels.
195
+ */
196
+ export function composite(rgba, cover, width, height, stride, colour, at) {
197
+ for (let y = 0; y < height; y++) {
198
+ const dy = at.y + y;
199
+ if (dy < 0) continue;
200
+ for (let x = 0; x < width; x++) {
201
+ const a = cover[y * width + x];
202
+ if (a <= 0) continue;
203
+ const dx = at.x + x;
204
+ if (dx < 0 || dx >= stride) continue;
205
+ const i = (dy * stride + dx) * 4;
206
+ if (i + 3 >= rgba.length) continue;
207
+ const dstA = rgba[i + 3] / 255;
208
+ const outA = a + dstA * (1 - a);
209
+ if (outA <= 0) continue;
210
+ rgba[i] = (colour.r * a + rgba[i] * dstA * (1 - a)) / outA;
211
+ rgba[i + 1] = (colour.g * a + rgba[i + 1] * dstA * (1 - a)) / outA;
212
+ rgba[i + 2] = (colour.b * a + rgba[i + 2] * dstA * (1 - a)) / outA;
213
+ rgba[i + 3] = outA * 255;
214
+ }
215
+ }
216
+ }