@mettascript/grapher 2.0.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/dist/node.js ADDED
@@ -0,0 +1,1779 @@
1
+ // src/node.ts
2
+ import { ExpressionAtom as ExpressionAtom4, MeTTa } from "@mettascript/hyperon";
3
+
4
+ // src/parse.ts
5
+ import { SExprParser, standardTokenizer } from "@mettascript/hyperon";
6
+ var sharedTokenizer;
7
+ var tokenizer = () => sharedTokenizer ??= standardTokenizer();
8
+ function parseProgram(src) {
9
+ return new SExprParser(src).parseAll(tokenizer());
10
+ }
11
+
12
+ // src/reduce.ts
13
+ import { E, S, ExpressionAtom } from "@mettascript/hyperon";
14
+ var MAX_WIDTH = 24;
15
+ function lazyPositions(head, metta, cache) {
16
+ const key = head.toString();
17
+ const hit = cache.get(key);
18
+ if (hit !== void 0) return hit;
19
+ const out = /* @__PURE__ */ new Set();
20
+ const types = metta.evaluateAtom(E(S("get-type"), head));
21
+ const type = types[0];
22
+ if (type instanceof ExpressionAtom) {
23
+ const items = type.children();
24
+ if (items[0]?.toString() === "->")
25
+ for (let i = 1; i < items.length - 1; i++) {
26
+ const t = items[i].toString();
27
+ if (t === "Atom" || t === "Expression") out.add(i);
28
+ }
29
+ }
30
+ cache.set(key, out);
31
+ return out;
32
+ }
33
+ function stepAll(atom, metta) {
34
+ const results = metta.evaluateAtom(E(S("eval"), atom));
35
+ const src = atom.toString();
36
+ const out = [];
37
+ const seen = /* @__PURE__ */ new Set();
38
+ for (const r of results) {
39
+ if (r instanceof ExpressionAtom) {
40
+ const items = r.children();
41
+ if (items.length === 2 && items[0].toString() === "eval" && items[1].toString() === src)
42
+ continue;
43
+ }
44
+ const s = r.toString();
45
+ if (s === src || seen.has(s)) continue;
46
+ seen.add(s);
47
+ out.push(r);
48
+ }
49
+ return out.length === 0 ? null : out;
50
+ }
51
+ function reduceStep(atom, metta, cache = /* @__PURE__ */ new Map()) {
52
+ if (atom instanceof ExpressionAtom) {
53
+ const items = atom.children();
54
+ const head = items[0];
55
+ const lazy = head !== void 0 ? lazyPositions(head, metta, cache) : /* @__PURE__ */ new Set();
56
+ for (let i = 1; i < items.length; i++) {
57
+ if (lazy.has(i)) continue;
58
+ const reduced = reduceStep(items[i], metta, cache);
59
+ if (reduced !== null)
60
+ return reduced.map((r) => {
61
+ const next = [...items];
62
+ next[i] = r;
63
+ return E(...next);
64
+ });
65
+ }
66
+ }
67
+ return stepAll(atom, metta);
68
+ }
69
+ function sameSet(a, b) {
70
+ if (a.length !== b.length) return false;
71
+ const as = a.map(String).sort();
72
+ const bs = b.map(String).sort();
73
+ return as.every((s, i) => s === bs[i]);
74
+ }
75
+ function reduceTrace(atom, metta, maxSteps = 300) {
76
+ const cache = /* @__PURE__ */ new Map();
77
+ const frontiers = [[atom]];
78
+ let frontier = [atom];
79
+ let settled = false;
80
+ for (let i = 0; i < maxSteps; i++) {
81
+ const next = [];
82
+ const seen = /* @__PURE__ */ new Set();
83
+ let changed = false;
84
+ for (const term of frontier) {
85
+ const step = reduceStep(term, metta, cache);
86
+ if (step !== null) changed = true;
87
+ for (const s of step ?? [term]) {
88
+ const key = s.toString();
89
+ if (!seen.has(key)) {
90
+ seen.add(key);
91
+ next.push(s);
92
+ }
93
+ }
94
+ }
95
+ if (!changed) {
96
+ settled = true;
97
+ break;
98
+ }
99
+ frontier = next.length > MAX_WIDTH ? next.slice(0, MAX_WIDTH) : next;
100
+ frontiers.push(frontier);
101
+ }
102
+ if (settled) {
103
+ const result = metta.evaluateAtom(atom);
104
+ if (result.length > 0 && !sameSet(frontier, result))
105
+ frontiers.push(result.length > MAX_WIDTH ? result.slice(0, MAX_WIDTH) : result);
106
+ }
107
+ return frontiers;
108
+ }
109
+
110
+ // src/block/geometry.ts
111
+ var T = 0.39;
112
+ function rot(x, y, deg) {
113
+ const a = deg * Math.PI / 180;
114
+ const c = Math.cos(a);
115
+ const s = Math.sin(a);
116
+ return { x: x * c - y * s, y: x * s + y * c };
117
+ }
118
+ function f(n) {
119
+ return (Math.round(n * 100) / 100).toString();
120
+ }
121
+ function pulledPointsToPath(points) {
122
+ const n = points.length;
123
+ if (n === 0) return "";
124
+ const first = points[0];
125
+ let d = `M ${f(first.x)} ${f(first.y)}`;
126
+ for (let i = 0; i < n; i++) {
127
+ const prev = points[i];
128
+ const cur = points[(i + 1) % n];
129
+ const vx = cur.x - prev.x;
130
+ const vy = cur.y - prev.y;
131
+ const r1 = rot(vx, vy, prev.rangle);
132
+ const c1x = prev.x + prev.rpull * r1.x;
133
+ const c1y = prev.y + prev.rpull * r1.y;
134
+ const r2 = rot(-vx, -vy, cur.langle);
135
+ const c2x = cur.x + cur.lpull * r2.x;
136
+ const c2y = cur.y + cur.lpull * r2.y;
137
+ d += ` C ${f(c1x)} ${f(c1y)} ${f(c2x)} ${f(c2y)} ${f(cur.x)} ${f(cur.y)}`;
138
+ }
139
+ return d + " Z";
140
+ }
141
+ function roundedRectPoints(width, height, initR) {
142
+ const r = width < 2 * initR ? Math.floor(width / 2) : Math.round(initR);
143
+ const sl = Math.max(0, width - 2 * r);
144
+ const sh = Math.max(0, height - 2 * r);
145
+ return [
146
+ { lpull: 0, langle: 0, x: 0, y: r, rpull: T, rangle: -45 },
147
+ { lpull: T, langle: 45, x: r, y: 0, rpull: 0, rangle: 0 },
148
+ { lpull: 0, langle: 0, x: r + sl, y: 0, rpull: T, rangle: -45 },
149
+ { lpull: T, langle: 45, x: 2 * r + sl, y: r, rpull: 0, rangle: 0 },
150
+ { lpull: 0, langle: 0, x: 2 * r + sl, y: r + sh, rpull: T, rangle: -45 },
151
+ { lpull: T, langle: 45, x: r + sl, y: 2 * r + sh, rpull: 0, rangle: 0 },
152
+ { lpull: 0, langle: 0, x: r, y: 2 * r + sh, rpull: T, rangle: -45 },
153
+ { lpull: T, langle: 45, x: 0, y: r + sh, rpull: T, rangle: 0 }
154
+ ];
155
+ }
156
+ function roundedRectPath(width, height, r) {
157
+ return pulledPointsToPath(roundedRectPoints(width, height, r));
158
+ }
159
+ function sign3(a, b, polarity) {
160
+ if (a > b) return polarity;
161
+ if (a === b) return 0;
162
+ return -polarity;
163
+ }
164
+ function augment(polarity, init, rows) {
165
+ const withLtp = [];
166
+ let prev = init;
167
+ for (const row of rows) {
168
+ withLtp.push({ w: row.x, h: row.h, ltp: sign3(row.x, prev, polarity), ltn: 0 });
169
+ prev = row.x;
170
+ }
171
+ let next = init;
172
+ for (let i = withLtp.length - 1; i >= 0; i--) {
173
+ const row = withLtp[i];
174
+ row.ltn = sign3(row.w, next, polarity);
175
+ next = row.w;
176
+ }
177
+ return withLtp;
178
+ }
179
+ function fourPointRow(p, n, offset, ltp, ltn, initH, finalH, r) {
180
+ const y1 = 2 * n * r + initH;
181
+ const y2 = 2 * n * r + p * r + initH;
182
+ const y3 = 2 * n * r + p * r + finalH;
183
+ const y4 = 2 * n * r + p * 2 * r + finalH;
184
+ const x1 = offset + -p * ltp * r;
185
+ const x2 = offset;
186
+ const x3 = offset;
187
+ const x4 = offset + -p * ltn * r;
188
+ return [
189
+ { lpull: T, langle: 0, x: x1, y: y1, rpull: T, rangle: ltp * -45 },
190
+ { lpull: T, langle: ltp * 45, x: x2, y: y2, rpull: T, rangle: 0 },
191
+ { lpull: T, langle: 0, x: x3, y: y3, rpull: T, rangle: ltn * -45 },
192
+ { lpull: T, langle: ltn * 45, x: x4, y: y4, rpull: T, rangle: 0 }
193
+ ];
194
+ }
195
+ function calcRows(p, init, numRows, totalH, rows, headerException, r) {
196
+ const aug = augment(p, init, rows);
197
+ if (headerException && aug.length > 0) aug[0].ltn = Math.abs(aug[0].ltn);
198
+ const points = [];
199
+ let n = numRows;
200
+ let h = totalH;
201
+ for (const row of aug) {
202
+ points.push(...fourPointRow(p, n, row.w, row.ltp, row.ltn, h, h + p * row.h, r));
203
+ n = n + p;
204
+ h = h + p * row.h;
205
+ }
206
+ return { points, numRows: n, totalH: h };
207
+ }
208
+ function roundedBackingPoints(rightProfile, leftProfile, r, headerException) {
209
+ const srcRight = rightProfile.map((row) => ({ x: row.x, h: row.h - 2 * r }));
210
+ const srcLeft = leftProfile.map((row) => ({ x: row.x, h: row.h - 2 * r }));
211
+ const right = calcRows(1, 0, 0, 0, srcRight, headerException, r);
212
+ const left = calcRows(
213
+ -1,
214
+ Infinity,
215
+ right.numRows,
216
+ right.totalH,
217
+ [...srcLeft].reverse(),
218
+ false,
219
+ r
220
+ );
221
+ return [...right.points, ...left.points];
222
+ }
223
+ function roundedBackingPath(rightProfile, leftProfile, r, headerException) {
224
+ return pulledPointsToPath(roundedBackingPoints(rightProfile, leftProfile, r, headerException));
225
+ }
226
+
227
+ // src/anim.ts
228
+ var lerp = (a, b, t) => a + (b - a) * t;
229
+ function ease(t) {
230
+ return t * t * t * (t * (t * 6 - 15) + 10);
231
+ }
232
+ var DEFAULT_TRACE_MS = 550;
233
+ function arcPoint(ax, ay, bx, by, t) {
234
+ const mx = lerp(ax, bx, t);
235
+ const my = lerp(ay, by, t);
236
+ const dx = bx - ax;
237
+ const dy = by - ay;
238
+ const dist = Math.hypot(dx, dy);
239
+ if (dist < 1) return { x: mx, y: my };
240
+ const off = Math.sin(Math.PI * t) * Math.min(dist * 0.16, 36);
241
+ return { x: mx + -dy / dist * off, y: my + dx / dist * off };
242
+ }
243
+
244
+ // src/block/animate.ts
245
+ var backingD = (box, s) => box.orient === "h" ? roundedRectPath(box.w, box.h, s.radius) : roundedBackingPath(box.rightProfile, box.leftProfile, s.radius, box.headerException);
246
+ function boxesToPrims(boxes, s, selectedPath) {
247
+ const out = [];
248
+ const draw = (box) => {
249
+ const pk = JSON.stringify(box.path);
250
+ if (box.kind === "expr") {
251
+ out.push({
252
+ t: "path",
253
+ key: `b:${pk}`,
254
+ d: backingD(box, s),
255
+ tx: box.x,
256
+ ty: box.y,
257
+ fill: box.fill,
258
+ stroke: box.outline,
259
+ op: 1,
260
+ dataPath: pk
261
+ });
262
+ if (box.children.length === 0)
263
+ out.push({
264
+ t: "text",
265
+ key: `t:${pk}`,
266
+ x: box.x + box.w / 2,
267
+ y: box.y + box.h / 2,
268
+ text: "()",
269
+ fill: s.identifierColor,
270
+ size: s.fontSize,
271
+ weight: false,
272
+ op: 1
273
+ });
274
+ for (const c of box.children) draw(c);
275
+ return;
276
+ }
277
+ if (box.kind === "hole") {
278
+ const ph = s.fontSize + 6;
279
+ const py = box.y + (box.h - ph) / 2;
280
+ out.push({
281
+ t: "path",
282
+ key: `b:${pk}`,
283
+ d: roundedRectPath(box.w, ph, ph / 2),
284
+ tx: box.x,
285
+ ty: py,
286
+ fill: s.holeFill,
287
+ stroke: "none",
288
+ op: 1,
289
+ dataPath: pk
290
+ });
291
+ out.push({
292
+ t: "text",
293
+ key: `t:${pk}`,
294
+ x: box.x + box.w / 2,
295
+ y: box.y + box.h / 2,
296
+ text: box.text,
297
+ fill: s.holeText,
298
+ size: s.fontSize,
299
+ weight: true,
300
+ op: 1
301
+ });
302
+ return;
303
+ }
304
+ out.push({ t: "hit", key: `r:${pk}`, x: box.x, y: box.y, w: box.w, h: box.h, dataPath: pk });
305
+ out.push({
306
+ t: "text",
307
+ key: `t:${pk}`,
308
+ x: box.x + box.w / 2,
309
+ y: box.y + box.h / 2,
310
+ text: box.text,
311
+ fill: box.color,
312
+ size: s.fontSize,
313
+ weight: false,
314
+ op: 1
315
+ });
316
+ };
317
+ for (const b of boxes) draw(b);
318
+ if (selectedPath !== null) {
319
+ const sel = findBox(boxes, selectedPath);
320
+ if (sel !== null) {
321
+ const selD = sel.kind === "expr" ? backingD(sel, s) : roundedRectPath(sel.w, sel.h, sel.kind === "hole" ? s.radiusAdj : s.radius);
322
+ out.push({
323
+ t: "path",
324
+ key: "sel",
325
+ d: selD,
326
+ tx: sel.x,
327
+ ty: sel.y,
328
+ fill: "none",
329
+ stroke: s.selectedColor,
330
+ op: 1
331
+ });
332
+ const hh = s.unitHeight;
333
+ const hw = s.unitWidth * 1.8;
334
+ const hx = sel.x - hw - s.unitWidth * 0.4;
335
+ const hy = sel.y + sel.h / 2 - hh / 2;
336
+ out.push({
337
+ t: "path",
338
+ key: "hbg",
339
+ d: roundedRectPath(hw, hh, s.radiusAdj),
340
+ tx: hx,
341
+ ty: hy,
342
+ fill: s.selectedColor,
343
+ stroke: "none",
344
+ op: 1
345
+ });
346
+ out.push({
347
+ t: "text",
348
+ key: "harrow",
349
+ x: hx + hw / 2,
350
+ y: hy + hh / 2,
351
+ text: "\u2192",
352
+ fill: s.selectedAtomColor,
353
+ size: s.fontSize,
354
+ weight: false,
355
+ op: 1
356
+ });
357
+ }
358
+ }
359
+ return out;
360
+ }
361
+ function findBox(boxes, path) {
362
+ const same = (a) => a.length === path.length && a.every((v, i) => v === path[i]);
363
+ const walk = (b) => {
364
+ if (same(b.path)) return b;
365
+ if (b.kind === "expr")
366
+ for (const c of b.children) {
367
+ const hit = walk(c);
368
+ if (hit !== null) return hit;
369
+ }
370
+ return null;
371
+ };
372
+ for (const b of boxes) {
373
+ const hit = walk(b);
374
+ if (hit !== null) return hit;
375
+ }
376
+ return null;
377
+ }
378
+ var NUM = /-?\d+(\.\d+)?/g;
379
+ function lerpD(a, b, t) {
380
+ const na = a.match(NUM);
381
+ const nb = b.match(NUM);
382
+ if (na === null || nb === null || na.length !== nb.length) return b;
383
+ let i = 0;
384
+ return b.replace(NUM, () => {
385
+ const v = lerp(Number(na[i]), Number(nb[i]), t);
386
+ i++;
387
+ return (Math.round(v * 100) / 100).toString();
388
+ });
389
+ }
390
+ function compatible(p, n) {
391
+ if (p.t !== n.t) return false;
392
+ if (p.t === "path" && n.t === "path")
393
+ return (p.d.match(NUM)?.length ?? 0) === (n.d.match(NUM)?.length ?? 0);
394
+ if (p.t === "text" && n.t === "text") return p.text === n.text;
395
+ return true;
396
+ }
397
+ function faded(p, op) {
398
+ return p.t === "hit" ? p : { ...p, op };
399
+ }
400
+ function morph(p, n, t) {
401
+ if (p.t === "path" && n.t === "path")
402
+ return {
403
+ ...n,
404
+ d: lerpD(p.d, n.d, t),
405
+ tx: lerp(p.tx, n.tx, t),
406
+ ty: lerp(p.ty, n.ty, t),
407
+ op: lerp(p.op, n.op, t)
408
+ };
409
+ if (p.t === "text" && n.t === "text")
410
+ return {
411
+ ...n,
412
+ x: lerp(p.x, n.x, t),
413
+ y: lerp(p.y, n.y, t),
414
+ size: lerp(p.size, n.size, t),
415
+ op: lerp(p.op, n.op, t)
416
+ };
417
+ return n;
418
+ }
419
+ function push(m, k, v) {
420
+ const arr = m.get(k);
421
+ if (arr === void 0) m.set(k, [v]);
422
+ else arr.push(v);
423
+ }
424
+ function interpolate(prev, next, t) {
425
+ const prevMap = new Map(prev.map((p) => [p.key, p]));
426
+ const nextKeys = new Set(next.map((n) => n.key));
427
+ const exitingByText = /* @__PURE__ */ new Map();
428
+ const enteringByText = /* @__PURE__ */ new Map();
429
+ for (const p of prev)
430
+ if (p.t === "text" && !p.weight && !nextKeys.has(p.key)) push(exitingByText, p.text, p);
431
+ for (const n of next)
432
+ if (n.t === "text" && !n.weight && !prevMap.has(n.key)) push(enteringByText, n.text, n);
433
+ const glideFrom = /* @__PURE__ */ new Map();
434
+ const glided = /* @__PURE__ */ new Set();
435
+ for (const [txt, ns] of enteringByText) {
436
+ const ps = exitingByText.get(txt);
437
+ if (ns.length === 1 && ps !== void 0 && ps.length === 1) {
438
+ glideFrom.set(ns[0].key, ps[0]);
439
+ glided.add(ps[0].key);
440
+ }
441
+ }
442
+ const out = [];
443
+ for (const n of next) {
444
+ if (n.t === "hit") continue;
445
+ const p = prevMap.get(n.key) ?? glideFrom.get(n.key);
446
+ if (p === void 0) {
447
+ out.push(faded(n, t));
448
+ } else if (compatible(p, n)) {
449
+ out.push(morph(p, n, t));
450
+ } else {
451
+ out.push(faded(n, t));
452
+ out.push(faded({ ...p, key: `${p.key}:x` }, 1 - t));
453
+ }
454
+ }
455
+ for (const p of prev) {
456
+ if (p.t === "hit") continue;
457
+ if (!nextKeys.has(p.key) && !glided.has(p.key)) out.push(faded(p, 1 - t));
458
+ }
459
+ return out;
460
+ }
461
+
462
+ // src/block/layout.ts
463
+ import {
464
+ ExpressionAtom as ExpressionAtom2,
465
+ VariableAtom,
466
+ SymbolAtom,
467
+ GroundedAtom
468
+ } from "@mettascript/hyperon";
469
+
470
+ // src/color.ts
471
+ var GLYPHS = {
472
+ "*": "\xD7",
473
+ // ×
474
+ "-": "\u2212",
475
+ // −
476
+ "/": "\xF7",
477
+ // ÷
478
+ "%": "mod",
479
+ ">=": "\u2265",
480
+ // ≥
481
+ "<=": "\u2264",
482
+ // ≤
483
+ "!=": "\u2260",
484
+ // ≠
485
+ "->": "\u2192",
486
+ // →
487
+ "=": "\u2261",
488
+ // ≡
489
+ and: "\u2227",
490
+ // ∧
491
+ or: "\u2228",
492
+ // ∨
493
+ not: "\xAC",
494
+ // ¬
495
+ xor: "\u2295",
496
+ // ⊕
497
+ superpose: "\u222A"
498
+ // ∪
499
+ };
500
+ function displayGlyph(name) {
501
+ return GLYPHS[name] ?? name;
502
+ }
503
+ var NUMERIC = /^[+-]?(\d+\.?\d*|\.\d+)$/;
504
+ var OPERATORS = /* @__PURE__ */ new Set([
505
+ "=",
506
+ ":",
507
+ "->",
508
+ "==",
509
+ "!=",
510
+ "!",
511
+ "+",
512
+ "-",
513
+ "*",
514
+ "/",
515
+ "%",
516
+ "<",
517
+ ">",
518
+ ">=",
519
+ "<=",
520
+ "and",
521
+ "or",
522
+ "not",
523
+ "xor"
524
+ ]);
525
+ var CONTROL = /* @__PURE__ */ new Set(["if", "case", "cond", "match", "switch", "unify"]);
526
+ var BOOLEANS = /* @__PURE__ */ new Set(["True", "False"]);
527
+ function roleOf(name) {
528
+ if (name.startsWith("$")) return "variable";
529
+ if (name.startsWith("&")) return "spaceref";
530
+ if (name.startsWith("@")) return "at";
531
+ if (name.startsWith('"')) return "string";
532
+ if (NUMERIC.test(name)) return "number";
533
+ if (BOOLEANS.has(name)) return "boolean";
534
+ if (OPERATORS.has(name)) return "operator";
535
+ if (CONTROL.has(name)) return "control";
536
+ return "symbol";
537
+ }
538
+ var DARK = "#0d1117";
539
+ var PALETTE = {
540
+ // a variable is drawn hollow (a transparent circle), so its fill is used as the outline and its text is
541
+ // the same color, visible on the canvas rather than dark-on-fill.
542
+ variable: { fill: "#ffa657", text: "#ffa657" },
543
+ spaceref: { fill: "#ffa657", text: DARK },
544
+ at: { fill: "#d2a8ff", text: DARK },
545
+ number: { fill: "#79c0ff", text: DARK },
546
+ string: { fill: "#a5d6ff", text: DARK },
547
+ boolean: { fill: "#39c5cf", text: DARK },
548
+ operator: { fill: "#ff7b72", text: DARK },
549
+ control: { fill: "#f2cc60", text: DARK },
550
+ paren: { fill: "#7ee787", text: DARK },
551
+ symbol: { fill: "#454c5a", text: "#e6edf3" }
552
+ };
553
+ function colorFor(node) {
554
+ if (node.kind === "list") return PALETTE.paren;
555
+ if (node.kind === "dot") return { fill: "#6e7681", text: "#e6edf3" };
556
+ return PALETTE[roleOf(node.name)];
557
+ }
558
+ var srgbToLinear = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
559
+ var linearToSrgb = (c) => c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
560
+ function hexToOklab(hex) {
561
+ const m = /^#([0-9a-fA-F]{6})$/.exec(hex);
562
+ if (m === null) return null;
563
+ const v = parseInt(m[1], 16);
564
+ const r = srgbToLinear((v >> 16 & 255) / 255);
565
+ const g = srgbToLinear((v >> 8 & 255) / 255);
566
+ const b = srgbToLinear((v & 255) / 255);
567
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
568
+ const m2 = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
569
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
570
+ return [
571
+ 0.2104542553 * l + 0.793617785 * m2 - 0.0040720468 * s,
572
+ 1.9779984951 * l - 2.428592205 * m2 + 0.4505937099 * s,
573
+ 0.0259040371 * l + 0.7827717662 * m2 - 0.808675766 * s
574
+ ];
575
+ }
576
+ function oklabToHex([L, A, B]) {
577
+ const l_ = L + 0.3963377774 * A + 0.2158037573 * B;
578
+ const m_ = L - 0.1055613458 * A - 0.0638541728 * B;
579
+ const s_ = L - 0.0894841775 * A - 1.291485548 * B;
580
+ const l = l_ * l_ * l_;
581
+ const m = m_ * m_ * m_;
582
+ const s = s_ * s_ * s_;
583
+ const r = linearToSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s);
584
+ const g = linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s);
585
+ const b = linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s);
586
+ const hx = (c) => Math.max(0, Math.min(255, Math.round(c * 255))).toString(16).padStart(2, "0");
587
+ return `#${hx(r)}${hx(g)}${hx(b)}`;
588
+ }
589
+ function lerpColor(a, b, t) {
590
+ const ca = hexToOklab(a);
591
+ const cb = hexToOklab(b);
592
+ if (ca === null || cb === null) return t < 0.5 ? a : b;
593
+ return oklabToHex([
594
+ ca[0] + (cb[0] - ca[0]) * t,
595
+ ca[1] + (cb[1] - ca[1]) * t,
596
+ ca[2] + (cb[2] - ca[2]) * t
597
+ ]);
598
+ }
599
+
600
+ // src/block/color.ts
601
+ function glyphColor(name, isHead, s) {
602
+ switch (roleOf(name)) {
603
+ case "number":
604
+ return s.literalColor;
605
+ case "string":
606
+ return s.stringColor;
607
+ case "operator":
608
+ case "control":
609
+ return s.operatorColor;
610
+ case "spaceref":
611
+ return s.spacerefColor;
612
+ case "at":
613
+ return s.atColor;
614
+ default:
615
+ return isHead ? s.formColor : s.identifierColor;
616
+ }
617
+ }
618
+ function blockColors(depth, s) {
619
+ const fill = depth % 2 === 0 ? s.backgroundBlockColor : s.bkgColor;
620
+ return { fill, outline: s.outlineBlockColor };
621
+ }
622
+
623
+ // src/block/layout.ts
624
+ var IF_LIKE = /* @__PURE__ */ new Set(["if", "and", "or"]);
625
+ var LAMBDA_LIKE = /* @__PURE__ */ new Set([
626
+ "=",
627
+ "let",
628
+ "let*",
629
+ "match",
630
+ "function",
631
+ "lambda",
632
+ "\\",
633
+ "sealed",
634
+ "chain"
635
+ ]);
636
+ var COND_LIKE = /* @__PURE__ */ new Set(["case", "cond", "superpose", "collapse", "unify"]);
637
+ function classify(head, u) {
638
+ if (IF_LIKE.has(head))
639
+ return { headerItems: 2, indent: u, straightLeft: true, headerException: false };
640
+ if (LAMBDA_LIKE.has(head))
641
+ return { headerItems: 2, indent: u, straightLeft: true, headerException: true };
642
+ if (COND_LIKE.has(head))
643
+ return { headerItems: 1, indent: u, straightLeft: true, headerException: false };
644
+ return null;
645
+ }
646
+ function isHeaded(atom) {
647
+ const items = atom.children();
648
+ const head = items[0];
649
+ return items.length >= 2 && head !== void 0 && isLeaf(head);
650
+ }
651
+ function isLeaf(atom) {
652
+ return atom instanceof SymbolAtom || atom instanceof VariableAtom || atom instanceof GroundedAtom;
653
+ }
654
+ function needsTail(box) {
655
+ return box.kind !== "expr";
656
+ }
657
+ function layoutRow(boxes, startX, u) {
658
+ let x = startX;
659
+ let h = 0;
660
+ let right = startX;
661
+ for (const b of boxes) {
662
+ b.x = x;
663
+ b.y = 0;
664
+ right = x + b.w;
665
+ h = Math.max(h, b.h);
666
+ x = right + u;
667
+ }
668
+ return { right, h };
669
+ }
670
+ function build(atom, depth, path, isHead, s) {
671
+ const u = s.unitWidth;
672
+ if (atom instanceof VariableAtom) {
673
+ const text = atom.toString();
674
+ return {
675
+ kind: "hole",
676
+ path,
677
+ x: 0,
678
+ y: 0,
679
+ w: text.length * u + u,
680
+ // a half unit of breathing room on each side of the name
681
+ h: s.unitHeight,
682
+ depth,
683
+ text
684
+ };
685
+ }
686
+ if (!(atom instanceof ExpressionAtom2)) {
687
+ const name = atom.toString();
688
+ const text = displayGlyph(name);
689
+ return {
690
+ kind: "atom",
691
+ path,
692
+ x: 0,
693
+ y: 0,
694
+ w: text.length * u,
695
+ h: s.unitHeight,
696
+ depth,
697
+ text,
698
+ color: glyphColor(name, isHead, s)
699
+ };
700
+ }
701
+ const items = atom.children();
702
+ if (items.length === 0) {
703
+ const { fill: fill2, outline: outline2 } = blockColors(depth, s);
704
+ const w2 = 3 * u;
705
+ const h = s.unitHeight;
706
+ return {
707
+ kind: "expr",
708
+ path,
709
+ x: 0,
710
+ y: 0,
711
+ w: w2,
712
+ h,
713
+ depth,
714
+ orient: "h",
715
+ fill: fill2,
716
+ outline: outline2,
717
+ headerException: false,
718
+ children: [],
719
+ rightProfile: [{ x: w2, h }],
720
+ leftProfile: [{ x: 0, h }]
721
+ };
722
+ }
723
+ const headed = isHeaded(atom);
724
+ const children = items.map((child, k) => {
725
+ const childIsHead = headed && k === 0;
726
+ return build(child, childIsHead ? depth : depth + 1, [...path, k], childIsHead, s);
727
+ });
728
+ const { fill, outline } = blockColors(depth, s);
729
+ const totalChars = children.reduce((sum, b) => sum + b.w, 0) / u;
730
+ const headName = headed ? items[0].toString() : "";
731
+ const cls = headed && children.length >= 2 ? classify(headName, u) : null;
732
+ const stacks = cls !== null && totalChars >= s.cutoff && children.length > cls.headerItems;
733
+ if (!stacks) {
734
+ const { right, h } = layoutRow(children, u, u);
735
+ const last = children[children.length - 1];
736
+ const w2 = right + (last !== void 0 && needsTail(last) ? u : 0);
737
+ return {
738
+ kind: "expr",
739
+ path,
740
+ x: 0,
741
+ y: 0,
742
+ w: w2,
743
+ h,
744
+ depth,
745
+ orient: "h",
746
+ fill,
747
+ outline,
748
+ headerException: false,
749
+ children,
750
+ rightProfile: [{ x: w2, h }],
751
+ leftProfile: [{ x: 0, h }]
752
+ };
753
+ }
754
+ const c = cls;
755
+ const header = children.slice(0, c.headerItems);
756
+ const body = children.slice(c.headerItems);
757
+ const head = layoutRow(header, u, u);
758
+ const lastHeader = header[header.length - 1];
759
+ const rightProfile = [
760
+ { x: head.right + (lastHeader !== void 0 && needsTail(lastHeader) ? u : 0), h: head.h }
761
+ ];
762
+ const leftProfile = [{ x: 0, h: head.h }];
763
+ let yCursor = head.h;
764
+ for (const b of body) {
765
+ b.x = c.indent;
766
+ b.y = yCursor;
767
+ yCursor += b.h;
768
+ rightProfile.push({ x: c.indent + b.w + (needsTail(b) ? u : 0), h: b.h });
769
+ leftProfile.push({ x: c.straightLeft ? 0 : c.indent, h: b.h });
770
+ }
771
+ const w = Math.max(...rightProfile.map((r) => r.x));
772
+ return {
773
+ kind: "expr",
774
+ path,
775
+ x: 0,
776
+ y: 0,
777
+ w,
778
+ h: yCursor,
779
+ depth,
780
+ orient: "v",
781
+ fill,
782
+ outline,
783
+ headerException: c.headerException,
784
+ children,
785
+ rightProfile,
786
+ leftProfile
787
+ };
788
+ }
789
+ function place(box, ox, oy) {
790
+ box.x += ox;
791
+ box.y += oy;
792
+ if (box.kind === "expr") for (const child of box.children) place(child, box.x, box.y);
793
+ }
794
+ function placeProgram(atoms, s) {
795
+ const gap = s.unitHeight;
796
+ const boxes = [];
797
+ let y = 0;
798
+ for (let i = 0; i < atoms.length; i++) {
799
+ const box = build(atoms[i], 0, [i], false, s);
800
+ place(box, 0, y);
801
+ boxes.push(box);
802
+ y += box.h + gap;
803
+ }
804
+ return boxes;
805
+ }
806
+
807
+ // src/block/settings.ts
808
+ var SITE_PALETTE = {
809
+ canvas: "#1b1d23",
810
+ bkgColor: "#21262d",
811
+ backgroundBlockColor: "#2b313b",
812
+ outlineBlockColor: "#3d444d",
813
+ formColor: "#e6edf3",
814
+ identifierColor: "#e6edf3",
815
+ literalColor: "#79c0ff",
816
+ stringColor: "#a5d6ff",
817
+ operatorColor: "#ff7b72",
818
+ spacerefColor: "#ffa657",
819
+ atColor: "#d2a8ff",
820
+ holeFill: "#ffa657",
821
+ holeSide: "#ffa657",
822
+ holeText: "#1b1d23",
823
+ selectedColor: "#f2cc60",
824
+ selectedAtomColor: "#1b1d23"
825
+ };
826
+ var CUTOFF = 14;
827
+ function makeSettings(fontSize, unitWidth, palette = SITE_PALETTE) {
828
+ const unitHeight = Math.round(fontSize * 1.55);
829
+ const radius = Math.max(2, Math.round(unitHeight / 2) - 1);
830
+ const radiusAdj = Math.max(2, Math.round(radius * 5 / 7));
831
+ return { fontSize, unitWidth, unitHeight, radius, radiusAdj, cutoff: CUTOFF, ...palette };
832
+ }
833
+
834
+ // src/svg-gif.ts
835
+ async function encodeSvgAnimation(animation, rasterize, lib, maxColors = 128) {
836
+ const { frames, width, height, background } = animation;
837
+ if (frames.length === 0) throw new Error("no SVG frames to encode");
838
+ if (!Number.isInteger(width) || width < 1 || !Number.isInteger(height) || height < 1)
839
+ throw new Error(`invalid GIF dimensions: ${width}x${height}`);
840
+ if (!Number.isInteger(maxColors) || maxColors < 2 || maxColors > 256)
841
+ throw new Error(`maxColors must be an integer from 2 to 256, got ${maxColors}`);
842
+ const expectedBytes = width * height * 4;
843
+ const gif = lib.GIFEncoder();
844
+ for (const frame of frames) {
845
+ const rgba = await rasterize(frame.svg, width, height, background);
846
+ if (rgba.byteLength !== expectedBytes)
847
+ throw new Error(
848
+ `SVG rasterizer returned ${rgba.byteLength} bytes for ${width}x${height}; expected ${expectedBytes}`
849
+ );
850
+ const palette = lib.quantize(rgba, maxColors);
851
+ gif.writeFrame(lib.applyPalette(rgba, palette), width, height, {
852
+ palette,
853
+ delay: frame.delay
854
+ });
855
+ }
856
+ gif.finish();
857
+ return gif.bytes();
858
+ }
859
+
860
+ // src/block/gif.ts
861
+ function framesPerStepFor(opts, transitions) {
862
+ const wanted = opts.framesPerStep ?? Math.max(1, Math.round((opts.morphMs ?? DEFAULT_TRACE_MS) / (opts.stepMs ?? 40)));
863
+ return Math.max(
864
+ 1,
865
+ Math.min(wanted, Math.floor((opts.maxFrames ?? 180) / Math.max(1, transitions)))
866
+ );
867
+ }
868
+ function boundsOf(boxes, s) {
869
+ let minX = Infinity;
870
+ let minY = Infinity;
871
+ let maxX = -Infinity;
872
+ let maxY = -Infinity;
873
+ const walk = (b) => {
874
+ minX = Math.min(minX, b.x);
875
+ minY = Math.min(minY, b.y);
876
+ maxX = Math.max(maxX, b.x + b.w);
877
+ maxY = Math.max(maxY, b.y + b.h);
878
+ if (b.kind === "expr") for (const c of b.children) walk(c);
879
+ };
880
+ for (const b of boxes) walk(b);
881
+ if (!Number.isFinite(minX)) return { x: 0, y: 0, w: 1, h: 1 };
882
+ const pad = s.unitHeight;
883
+ return { x: minX - pad, y: minY - pad, w: maxX - minX + 2 * pad, h: maxY - minY + 2 * pad };
884
+ }
885
+ function union(a, b) {
886
+ const x = Math.min(a.x, b.x);
887
+ const y = Math.min(a.y, b.y);
888
+ return { x, y, w: Math.max(a.x + a.w, b.x + b.w) - x, h: Math.max(a.y + a.h, b.y + b.h) - y };
889
+ }
890
+ function esc(t) {
891
+ return t.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
892
+ }
893
+ var round = (n) => Math.round(n * 1e3) / 1e3;
894
+ var FONT = "Iosevka, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
895
+ function frameSvg(prims, vb, w, h, bg) {
896
+ const parts = [
897
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="${round(vb.x)} ${round(vb.y)} ${round(vb.w)} ${round(vb.h)}">`,
898
+ `<rect x="${round(vb.x)}" y="${round(vb.y)}" width="${round(vb.w)}" height="${round(vb.h)}" fill="${bg}"/>`
899
+ ];
900
+ for (const p of prims) {
901
+ if (p.t === "path") {
902
+ parts.push(
903
+ `<path d="${p.d}" transform="translate(${round(p.tx)} ${round(p.ty)})" fill="${p.fill}" stroke="${p.stroke}" stroke-width="1" opacity="${round(p.op)}"/>`
904
+ );
905
+ } else if (p.t === "text") {
906
+ const weight = p.weight ? ' font-weight="600"' : "";
907
+ parts.push(
908
+ `<text x="${round(p.x)}" y="${round(p.y)}" font-size="${p.size}" fill="${p.fill}" opacity="${round(p.op)}" text-anchor="middle" dominant-baseline="central" font-family="${FONT}"${weight}>${esc(p.text)}</text>`
909
+ );
910
+ }
911
+ }
912
+ parts.push("</svg>");
913
+ return parts.join("");
914
+ }
915
+ function blockReductionSvgsWithSettings(states, s, opts = {}) {
916
+ if (states.length === 0) throw new Error("no reduction states to export");
917
+ const width = opts.width ?? 720;
918
+ const holdMs = opts.holdMs ?? 260;
919
+ const stepMs = opts.stepMs ?? 40;
920
+ const framePrims = states.map((frontier) => boxesToPrims(placeProgram(frontier, s), s, null));
921
+ let vb = boundsOf(placeProgram(states[0], s), s);
922
+ for (const frontier of states) vb = union(vb, boundsOf(placeProgram(frontier, s), s));
923
+ const height = Math.max(1, Math.round(width * vb.h / vb.w));
924
+ const background = opts.background ?? s.canvas;
925
+ const frames = [];
926
+ const n = states.length;
927
+ const perStep = framesPerStepFor(opts, n - 1);
928
+ frames.push({ svg: frameSvg(framePrims[0], vb, width, height, background), delay: holdMs });
929
+ for (let i = 1; i < n; i++) {
930
+ for (let k = 1; k <= perStep; k++) {
931
+ const prims = interpolate(framePrims[i - 1], framePrims[i], ease(k / perStep));
932
+ frames.push({
933
+ svg: frameSvg(prims, vb, width, height, background),
934
+ delay: k === perStep ? holdMs : stepMs
935
+ });
936
+ }
937
+ }
938
+ return { frames, width, height, background };
939
+ }
940
+
941
+ // src/atom.ts
942
+ import {
943
+ SymbolAtom as SymbolAtom2,
944
+ VariableAtom as VariableAtom2,
945
+ ExpressionAtom as ExpressionAtom3,
946
+ GroundedAtom as GroundedAtom2,
947
+ S as S2,
948
+ E as E2
949
+ } from "@mettascript/hyperon";
950
+
951
+ // src/ids.ts
952
+ var counter = 0;
953
+ function nextId() {
954
+ return `n${++counter}`;
955
+ }
956
+
957
+ // src/model.ts
958
+ var X_EPSILON = 0.01;
959
+ var Graph = class _Graph {
960
+ nodes = /* @__PURE__ */ new Map();
961
+ childIds = /* @__PURE__ */ new Map();
962
+ // parent -> ordered child ids
963
+ parentIds = /* @__PURE__ */ new Map();
964
+ // child -> parent ids
965
+ /** Add a node. An explicit `id` is preserved (for loading); otherwise a fresh one is minted. Defaults:
966
+ * kind `symbol`, position `(0, 0)`. */
967
+ add(spec) {
968
+ const node = {
969
+ id: spec.id ?? nextId(),
970
+ name: spec.name,
971
+ kind: spec.kind ?? "symbol",
972
+ x: spec.x ?? 0,
973
+ y: spec.y ?? 0
974
+ };
975
+ this.nodes.set(node.id, node);
976
+ return node;
977
+ }
978
+ /** Remove a node and every edge touching it. */
979
+ remove(id) {
980
+ for (const p of this.parentsOf(id)) this.disconnect(p, id);
981
+ for (const c of this.childrenOf(id)) this.disconnect(id, c);
982
+ this.nodes.delete(id);
983
+ this.childIds.delete(id);
984
+ this.parentIds.delete(id);
985
+ }
986
+ /** Move a node to a new position. */
987
+ move(id, x, y) {
988
+ const node = this.nodes.get(id);
989
+ if (node) {
990
+ node.x = x;
991
+ node.y = y;
992
+ }
993
+ }
994
+ /** This node's child ids, in insertion order. */
995
+ childrenOf(id) {
996
+ return [...this.childIds.get(id) ?? []];
997
+ }
998
+ /** This node's parent ids, in insertion order. */
999
+ parentsOf(id) {
1000
+ return [...this.parentIds.get(id) ?? []];
1001
+ }
1002
+ /** Whether `parent -> child` would be a legal edge: not a self-loop, a duplicate, an unknown node, or a
1003
+ * cycle (the parent already sitting below the child). Lets the editor reject a drag before committing it. */
1004
+ canConnect(parentId, childId) {
1005
+ if (parentId === childId) return false;
1006
+ if (!this.nodes.has(parentId) || !this.nodes.has(childId)) return false;
1007
+ if ((this.childIds.get(parentId) ?? []).includes(childId)) return false;
1008
+ if (this.reaches(childId, parentId)) return false;
1009
+ return true;
1010
+ }
1011
+ /** Connect `parent` to `child`. Returns false and does nothing when the edge would be illegal (see
1012
+ * {@link canConnect}). */
1013
+ connect(parentId, childId) {
1014
+ if (!this.canConnect(parentId, childId)) return false;
1015
+ this.childIds.set(parentId, [...this.childIds.get(parentId) ?? [], childId]);
1016
+ this.parentIds.set(childId, [...this.parentIds.get(childId) ?? [], parentId]);
1017
+ return true;
1018
+ }
1019
+ /** Remove the `parent -> child` edge if present. */
1020
+ disconnect(parentId, childId) {
1021
+ const kids = this.childIds.get(parentId);
1022
+ if (kids)
1023
+ this.childIds.set(
1024
+ parentId,
1025
+ kids.filter((k) => k !== childId)
1026
+ );
1027
+ const parents = this.parentIds.get(childId);
1028
+ if (parents)
1029
+ this.parentIds.set(
1030
+ childId,
1031
+ parents.filter((p) => p !== parentId)
1032
+ );
1033
+ }
1034
+ /** Children of `id` in screen order: by x, ties (within {@link X_EPSILON}) broken by y. This is how
1035
+ * argument order is read off the canvas. */
1036
+ sortedChildren(id) {
1037
+ const kids = this.childrenOf(id).map((k) => this.nodes.get(k)).filter((n) => n !== void 0);
1038
+ return kids.sort((a, b) => Math.abs(a.x - b.x) < X_EPSILON ? a.y - b.y : a.x - b.x);
1039
+ }
1040
+ /** The roots (parentless nodes) reachable by walking up from `id`. A parentless node is its own head. */
1041
+ findHeads(id) {
1042
+ const heads = /* @__PURE__ */ new Map();
1043
+ const seen = /* @__PURE__ */ new Set();
1044
+ const visit = (cur) => {
1045
+ if (seen.has(cur)) return;
1046
+ seen.add(cur);
1047
+ const parents = this.parentIds.get(cur) ?? [];
1048
+ if (parents.length === 0) {
1049
+ const node = this.nodes.get(cur);
1050
+ if (node) heads.set(cur, node);
1051
+ } else {
1052
+ for (const p of parents) visit(p);
1053
+ }
1054
+ };
1055
+ visit(id);
1056
+ return [...heads.values()];
1057
+ }
1058
+ /** Every root in the graph (nodes with no parents). */
1059
+ heads() {
1060
+ return [...this.nodes.values()].filter((n) => (this.parentIds.get(n.id) ?? []).length === 0);
1061
+ }
1062
+ /** A deep copy: same node ids, positions, and edges. */
1063
+ clone() {
1064
+ const g = new _Graph();
1065
+ for (const n of this.nodes.values()) g.add({ ...n });
1066
+ for (const [p, kids] of this.childIds) for (const c of kids) g.connect(p, c);
1067
+ return g;
1068
+ }
1069
+ /** Can `from` reach `to` by following child edges downward? */
1070
+ reaches(from, to) {
1071
+ const seen = /* @__PURE__ */ new Set();
1072
+ const stack = [from];
1073
+ while (stack.length > 0) {
1074
+ const cur = stack.pop();
1075
+ if (cur === void 0) break;
1076
+ if (cur === to) return true;
1077
+ if (seen.has(cur)) continue;
1078
+ seen.add(cur);
1079
+ for (const c of this.childIds.get(cur) ?? []) stack.push(c);
1080
+ }
1081
+ return false;
1082
+ }
1083
+ };
1084
+
1085
+ // src/measure.ts
1086
+ var NODE_H = 22;
1087
+ var CHAR_W = 7;
1088
+ var PAD_X = 12;
1089
+ var MIN_W = 24;
1090
+ function displayText(node) {
1091
+ if (node.kind === "list") return "( )";
1092
+ if (node.kind === "dot") return "\u2022";
1093
+ return node.name.length > 0 ? displayGlyph(node.name) : "?";
1094
+ }
1095
+ function nodeWidth(node) {
1096
+ return Math.max(MIN_W, displayText(node).length * CHAR_W + PAD_X);
1097
+ }
1098
+
1099
+ // src/layout.ts
1100
+ var ROW = NODE_H + 26;
1101
+ var GAP = 14;
1102
+ var HEAD_GAP = 3 * NODE_H;
1103
+ function layout(graph, opts) {
1104
+ const originX = opts?.originX ?? 0;
1105
+ const originY = opts?.originY ?? 0;
1106
+ const scaleOf = opts?.scaleOf ?? (() => 1);
1107
+ const visited = /* @__PURE__ */ new Set();
1108
+ let cursorX = originX;
1109
+ const place2 = (id, depth) => {
1110
+ const node = graph.nodes.get(id);
1111
+ if (node === void 0) return cursorX;
1112
+ if (visited.has(id)) return node.x;
1113
+ visited.add(id);
1114
+ node.y = originY + depth * ROW;
1115
+ const kids = graph.childrenOf(id);
1116
+ if (kids.length === 0) {
1117
+ const w = nodeWidth(node) * scaleOf(node);
1118
+ node.x = cursorX + w / 2;
1119
+ cursorX += w + GAP;
1120
+ return node.x;
1121
+ }
1122
+ const xs = kids.map((k) => place2(k, depth + 1));
1123
+ node.x = (Math.min(...xs) + Math.max(...xs)) / 2;
1124
+ return node.x;
1125
+ };
1126
+ for (const root of graph.heads()) {
1127
+ place2(root.id, 0);
1128
+ cursorX += HEAD_GAP;
1129
+ }
1130
+ }
1131
+
1132
+ // src/atom.ts
1133
+ function atomToGraph(atoms, graph = new Graph()) {
1134
+ for (const atom of atoms) buildNode(graph, atom);
1135
+ layout(graph);
1136
+ return graph;
1137
+ }
1138
+ function isAtomic(atom) {
1139
+ return atom instanceof SymbolAtom2 || atom instanceof VariableAtom2 || atom instanceof GroundedAtom2;
1140
+ }
1141
+ function leafName(atom) {
1142
+ return atom.toString();
1143
+ }
1144
+ function buildNode(graph, atom) {
1145
+ if (atom instanceof ExpressionAtom3) {
1146
+ const items = atom.children();
1147
+ const head = items[0];
1148
+ if (items.length >= 2 && head !== void 0 && isAtomic(head)) {
1149
+ const node2 = graph.add({ name: leafName(head), kind: "symbol" });
1150
+ for (const child of items.slice(1)) graph.connect(node2.id, buildNode(graph, child));
1151
+ return node2.id;
1152
+ }
1153
+ const node = graph.add({ name: "", kind: "list" });
1154
+ for (const child of items) graph.connect(node.id, buildNode(graph, child));
1155
+ return node.id;
1156
+ }
1157
+ return graph.add({ name: leafName(atom), kind: "symbol" }).id;
1158
+ }
1159
+
1160
+ // src/variables.ts
1161
+ function variableLinks(graph) {
1162
+ const groups = /* @__PURE__ */ new Map();
1163
+ for (const node of graph.nodes.values()) {
1164
+ if (node.kind !== "symbol" || !node.name.startsWith("$")) continue;
1165
+ const head = graph.findHeads(node.id)[0];
1166
+ if (head === void 0) continue;
1167
+ const key = `${head.id}\0${node.name}`;
1168
+ const arr = groups.get(key);
1169
+ if (arr) arr.push(node.id);
1170
+ else groups.set(key, [node.id]);
1171
+ }
1172
+ const links = [];
1173
+ for (const ids of groups.values()) {
1174
+ if (ids.length < 2) continue;
1175
+ const sorted = ids.map((id) => graph.nodes.get(id)).filter((n) => n !== void 0).sort((a, b) => a.x - b.x || a.y - b.y);
1176
+ for (let i = 0; i + 1 < sorted.length; i++) links.push([sorted[i].id, sorted[i + 1].id]);
1177
+ }
1178
+ return links;
1179
+ }
1180
+
1181
+ // src/shapes.ts
1182
+ var CORNER_SEG = 6;
1183
+ function shapePolygon(role, w) {
1184
+ const hw = w / 2;
1185
+ const hh = NODE_H / 2;
1186
+ switch (role) {
1187
+ case "operator":
1188
+ case "control":
1189
+ return [
1190
+ [0, -hh],
1191
+ [hw, 0],
1192
+ [0, hh],
1193
+ [-hw, 0]
1194
+ ];
1195
+ // diamond
1196
+ case "variable":
1197
+ return ellipse(hw, hh);
1198
+ // hollow circle
1199
+ case "number":
1200
+ case "string":
1201
+ return roundedRect(hw, hh, hh);
1202
+ // pill
1203
+ case "boolean":
1204
+ return roundedRect(hw, hh, 3);
1205
+ // square
1206
+ case "list":
1207
+ return [
1208
+ [-hw + 7, -hh],
1209
+ [hw - 7, -hh],
1210
+ [hw, 0],
1211
+ [hw - 7, hh],
1212
+ [-hw + 7, hh],
1213
+ [-hw, 0]
1214
+ ];
1215
+ // hexagon
1216
+ default:
1217
+ return roundedRect(hw, hh, 5);
1218
+ }
1219
+ }
1220
+ function ellipse(hw, hh) {
1221
+ const n = 24;
1222
+ const out = [];
1223
+ for (let i = 0; i < n; i++) {
1224
+ const a = 2 * Math.PI * i / n;
1225
+ out.push([hw * Math.cos(a), hh * Math.sin(a)]);
1226
+ }
1227
+ return out;
1228
+ }
1229
+ function roundedRect(hw, hh, r) {
1230
+ const rr = Math.min(r, hw, hh);
1231
+ const out = [];
1232
+ const corners = [
1233
+ [hw - rr, hh - rr, 0],
1234
+ [-(hw - rr), hh - rr, Math.PI / 2],
1235
+ [-(hw - rr), -(hh - rr), Math.PI],
1236
+ [hw - rr, -(hh - rr), 3 * Math.PI / 2]
1237
+ ];
1238
+ for (const [cx, cy, a0] of corners)
1239
+ for (let i = 0; i <= CORNER_SEG; i++) {
1240
+ const a = a0 + i / CORNER_SEG * (Math.PI / 2);
1241
+ out.push([cx + rr * Math.cos(a), cy + rr * Math.sin(a)]);
1242
+ }
1243
+ return out;
1244
+ }
1245
+ function raycast(poly, ang) {
1246
+ const dx = Math.cos(ang);
1247
+ const dy = Math.sin(ang);
1248
+ let best = Infinity;
1249
+ let hit = [0, 0];
1250
+ for (let i = 0; i < poly.length; i++) {
1251
+ const [x1, y1] = poly[i];
1252
+ const [x2, y2] = poly[(i + 1) % poly.length];
1253
+ const ex = x2 - x1;
1254
+ const ey = y2 - y1;
1255
+ const det = ex * dy - dx * ey;
1256
+ if (Math.abs(det) < 1e-9) continue;
1257
+ const t = (ex * y1 - ey * x1) / det;
1258
+ const s = (dx * y1 - dy * x1) / det;
1259
+ if (t > 0 && s >= -1e-6 && s <= 1 + 1e-6 && t < best) {
1260
+ best = t;
1261
+ hit = [t * dx, t * dy];
1262
+ }
1263
+ }
1264
+ return hit;
1265
+ }
1266
+ function shapePoints(node, n) {
1267
+ const role = node.kind === "symbol" ? roleOf(node.name) : node.kind;
1268
+ const poly = shapePolygon(role, nodeWidth(node));
1269
+ const out = [];
1270
+ for (let i = 0; i < n; i++) out.push(raycast(poly, 2 * Math.PI * i / n));
1271
+ return out;
1272
+ }
1273
+
1274
+ // src/render.ts
1275
+ var SHAPE_N = 40;
1276
+ var CANVAS_BG = "#1b1d23";
1277
+ var CSS = `
1278
+ .mg-svg { width: 100%; height: 100%; display: block; overflow: clip; background: ${CANVAS_BG}; user-select: none; font-family: ui-monospace, monospace; }
1279
+ .mg-edge { stroke-width: 1.6; }
1280
+ .mg-var-link { stroke: #ffa657; stroke-width: 1; stroke-dasharray: 3 4; opacity: 0.45; }
1281
+ .mg-node text { font-size: 12px; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
1282
+ .mg-node .box { stroke: #00000055; stroke-width: 1; cursor: grab; }
1283
+ .mg-node .var { fill: none; stroke-width: 1.8; cursor: grab; }
1284
+ .mg-sel { fill: none; stroke: #38bdf8; stroke-width: 1.5; }
1285
+ .mg-sel.primary { stroke: #f59e0b; }
1286
+ .mg-port { fill: #cbd5e1; stroke: #1b1d23; stroke-width: 1; cursor: crosshair; }
1287
+ .mg-result { font-size: 11px; fill: #9ca3af; }
1288
+ .mg-result.error { fill: #f87171; }
1289
+ .mg-viz-hi { fill: none; stroke: #f2cc60; stroke-width: 2.5; }
1290
+ .mg-viz-label { font-size: 11px; fill: #f2cc60; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
1291
+ .mg-overlay { pointer-events: none; }
1292
+ `;
1293
+ function pointsRadius(points) {
1294
+ let r = 0;
1295
+ for (const [x, y] of points) r = Math.max(r, Math.hypot(x, y));
1296
+ return r;
1297
+ }
1298
+ function traceKeyMap(graph) {
1299
+ const keys = /* @__PURE__ */ new Map();
1300
+ const walk = (id, path) => {
1301
+ keys.set(id, path);
1302
+ graph.sortedChildren(id).forEach((c, i) => walk(c.id, `${path}.${i}`));
1303
+ };
1304
+ graph.heads().forEach((h, i) => walk(h.id, `h${i}`));
1305
+ return keys;
1306
+ }
1307
+ function traceFrame(graph, viewport) {
1308
+ const keys = traceKeyMap(graph);
1309
+ const slots = [];
1310
+ for (const n of graph.nodes.values()) {
1311
+ const key = keys.get(n.id);
1312
+ if (key === void 0) continue;
1313
+ const color = colorFor(n);
1314
+ const points = shapePoints(n, SHAPE_N);
1315
+ slots.push({
1316
+ key,
1317
+ leaf: graph.childrenOf(n.id).length === 0,
1318
+ x: n.x,
1319
+ y: n.y,
1320
+ points,
1321
+ radius: pointsRadius(points),
1322
+ fill: color.fill,
1323
+ text: displayText(n),
1324
+ textColor: color.text,
1325
+ hollow: (n.kind === "symbol" ? roleOf(n.name) : n.kind) === "variable"
1326
+ });
1327
+ }
1328
+ const edges = [];
1329
+ for (const parent of graph.nodes.values()) {
1330
+ const pk = keys.get(parent.id);
1331
+ if (pk === void 0) continue;
1332
+ const pColor = colorFor(parent).fill;
1333
+ for (const cid of graph.childrenOf(parent.id)) {
1334
+ const child = graph.nodes.get(cid);
1335
+ const ck = keys.get(cid);
1336
+ if (child !== void 0 && ck !== void 0)
1337
+ edges.push({
1338
+ key: `${pk}>${ck}`,
1339
+ a: pk,
1340
+ b: ck,
1341
+ colorA: pColor,
1342
+ colorB: colorFor(child).fill
1343
+ });
1344
+ }
1345
+ }
1346
+ for (const [aId, bId] of variableLinks(graph)) {
1347
+ const ak = keys.get(aId);
1348
+ const bk = keys.get(bId);
1349
+ if (ak !== void 0 && bk !== void 0)
1350
+ edges.push({ key: `v:${ak}:${bk}`, a: ak, b: bk, colorA: "", colorB: "" });
1351
+ }
1352
+ return { slots, edges, viewport };
1353
+ }
1354
+ function interpolateTrace(from, to, t) {
1355
+ const fromByKey = new Map(from.slots.map((s) => [s.key, s]));
1356
+ const toNodes = new Map(to.slots.map((s) => [s.key, s]));
1357
+ const used = /* @__PURE__ */ new Set();
1358
+ const matchOf = /* @__PURE__ */ new Map();
1359
+ for (const s of to.slots) {
1360
+ const f2 = fromByKey.get(s.key);
1361
+ if (f2 !== void 0 && !(f2.leaf && !s.leaf)) {
1362
+ matchOf.set(s.key, f2);
1363
+ used.add(f2);
1364
+ }
1365
+ }
1366
+ const byTo = /* @__PURE__ */ new Map();
1367
+ const byFrom = /* @__PURE__ */ new Map();
1368
+ const rendered = [];
1369
+ for (const s of to.slots) {
1370
+ const f2 = matchOf.get(s.key);
1371
+ const p = f2 !== void 0 ? morphPlacement(f2, s, t) : singlePlacement(s, t);
1372
+ byTo.set(s.key, p);
1373
+ if (f2 !== void 0) byFrom.set(f2.key, p);
1374
+ rendered.push(p);
1375
+ }
1376
+ for (const s of from.slots) {
1377
+ if (used.has(s)) continue;
1378
+ const target = toNodes.get(s.key) ?? ancestorTarget(s.key, toNodes);
1379
+ const p = target !== void 0 ? collapsePlacement(s, target, t) : singlePlacement(s, 1 - t);
1380
+ byFrom.set(s.key, p);
1381
+ rendered.push(p);
1382
+ }
1383
+ const edges = [];
1384
+ const pushEdge = (e, a, b, pres = 1) => {
1385
+ if (a === void 0 || b === void 0) return;
1386
+ const op = Math.min(a.op, b.op, pres);
1387
+ if (op > 0.02)
1388
+ edges.push({ colorA: e.colorA, colorB: e.colorB, x1: a.x, y1: a.y, x2: b.x, y2: b.y, op });
1389
+ };
1390
+ const toEdgeKeys = new Set(to.edges.map((e) => e.key));
1391
+ const fromEdgeKeys = new Set(from.edges.map((e) => e.key));
1392
+ for (const e of to.edges)
1393
+ pushEdge(e, byTo.get(e.a), byTo.get(e.b), fromEdgeKeys.has(e.key) ? 1 : t);
1394
+ for (const e of from.edges)
1395
+ if (!toEdgeKeys.has(e.key)) pushEdge(e, byFrom.get(e.a), byFrom.get(e.b), 1 - t);
1396
+ const viewport = {
1397
+ panX: lerp(from.viewport.panX, to.viewport.panX, t),
1398
+ panY: lerp(from.viewport.panY, to.viewport.panY, t),
1399
+ scale: lerp(from.viewport.scale, to.viewport.scale, t)
1400
+ };
1401
+ return {
1402
+ nodes: rendered.filter((p) => p.op > 0.02),
1403
+ edges,
1404
+ viewport,
1405
+ redex: findRedex(from, used, toNodes, t)
1406
+ };
1407
+ }
1408
+ function findRedex(from, used, toNodes, t) {
1409
+ const op = Math.max(0, 1 - t * 1.7);
1410
+ if (op <= 0.01) return void 0;
1411
+ let root;
1412
+ let depth = Infinity;
1413
+ for (const f2 of from.slots) {
1414
+ const toAt = toNodes.get(f2.key);
1415
+ const changed = !used.has(f2) || toAt !== void 0 && toAt.text !== f2.text;
1416
+ const d = f2.key.split(".").length;
1417
+ if (changed && d < depth) {
1418
+ depth = d;
1419
+ root = f2;
1420
+ }
1421
+ }
1422
+ if (root === void 0) return void 0;
1423
+ const prefix = root.key + ".";
1424
+ let minX = Infinity;
1425
+ let minY = Infinity;
1426
+ let maxX = -Infinity;
1427
+ let maxY = -Infinity;
1428
+ for (const f2 of from.slots)
1429
+ if (f2.key === root.key || f2.key.startsWith(prefix)) {
1430
+ const ext = f2.radius;
1431
+ minX = Math.min(minX, f2.x - ext);
1432
+ maxX = Math.max(maxX, f2.x + ext);
1433
+ minY = Math.min(minY, f2.y - ext);
1434
+ maxY = Math.max(maxY, f2.y + ext);
1435
+ }
1436
+ return {
1437
+ x: (minX + maxX) / 2,
1438
+ y: (minY + maxY) / 2,
1439
+ rx: (maxX - minX) / 2 + 8,
1440
+ ry: (maxY - minY) / 2 + 8,
1441
+ op,
1442
+ color: root.fill
1443
+ // the raw role color; the glow tints it against the live background at paint time
1444
+ };
1445
+ }
1446
+ function morphPlacement(f2, s, t) {
1447
+ const p = arcPoint(f2.x, f2.y, s.x, s.y, t);
1448
+ const points = s.points.map((pt, i) => {
1449
+ const q = f2.points[i] ?? pt;
1450
+ return [lerp(q[0], pt[0], t), lerp(q[1], pt[1], t)];
1451
+ });
1452
+ const texts = f2.text === s.text ? [{ text: s.text, color: lerpColor(f2.textColor, s.textColor, t), op: 1 }] : [
1453
+ { text: f2.text, color: f2.textColor, op: 1 - t },
1454
+ { text: s.text, color: s.textColor, op: t }
1455
+ ];
1456
+ const fillOp = lerp(f2.hollow ? 0 : 1, s.hollow ? 0 : 1, t);
1457
+ return { x: p.x, y: p.y, op: 1, points, fill: lerpColor(f2.fill, s.fill, t), fillOp, texts };
1458
+ }
1459
+ function singlePlacement(s, op) {
1460
+ return {
1461
+ x: s.x,
1462
+ y: s.y,
1463
+ op,
1464
+ points: s.points,
1465
+ fill: s.fill,
1466
+ fillOp: s.hollow ? 0 : 1,
1467
+ texts: [{ text: s.text, color: s.textColor, op: 1 }]
1468
+ };
1469
+ }
1470
+ function ancestorTarget(key, toNodes) {
1471
+ let k = key;
1472
+ for (; ; ) {
1473
+ const dot = k.lastIndexOf(".");
1474
+ if (dot < 0) return void 0;
1475
+ k = k.slice(0, dot);
1476
+ const s = toNodes.get(k);
1477
+ if (s !== void 0) return s;
1478
+ }
1479
+ }
1480
+ function collapsePlacement(s, target, t) {
1481
+ const startDist = Math.hypot(s.x - target.x, s.y - target.y);
1482
+ const sR = s.radius;
1483
+ const tR = target.radius;
1484
+ const text = [{ text: s.text, color: s.textColor, op: 1 }];
1485
+ if (startDist < sR + tR)
1486
+ return {
1487
+ x: s.x,
1488
+ y: s.y,
1489
+ op: 1 - t,
1490
+ points: s.points,
1491
+ fill: s.fill,
1492
+ fillOp: s.hollow ? 0 : 1,
1493
+ texts: text
1494
+ };
1495
+ const x = lerp(s.x, target.x, t);
1496
+ const y = lerp(s.y, target.y, t);
1497
+ const m = Math.max(0, Math.min(1, 1 - (1 - t) * startDist / (sR + tR)));
1498
+ const points = m <= 0 ? s.points : target.points.map((pt, i) => {
1499
+ const q = s.points[i] ?? pt;
1500
+ return [lerp(q[0], pt[0], m), lerp(q[1], pt[1], m)];
1501
+ });
1502
+ const op = m < 0.7 ? 1 : (1 - m) / 0.3;
1503
+ return {
1504
+ x,
1505
+ y,
1506
+ op,
1507
+ points,
1508
+ fill: lerpColor(s.fill, target.fill, m),
1509
+ fillOp: s.hollow ? 0 : 1,
1510
+ texts: text,
1511
+ merging: m > 1e-3
1512
+ // in the gooey layer only once it has reached the result and begun to meld
1513
+ };
1514
+ }
1515
+
1516
+ // src/sidebyside-gif.ts
1517
+ var VP = { scale: 1, panX: 0, panY: 0 };
1518
+ var round2 = (n) => Math.round(n * 100) / 100;
1519
+ function traceBounds(frame) {
1520
+ let minX = Infinity;
1521
+ let minY = Infinity;
1522
+ let maxX = -Infinity;
1523
+ let maxY = -Infinity;
1524
+ for (const s of frame.slots) {
1525
+ minX = Math.min(minX, s.x - s.radius);
1526
+ minY = Math.min(minY, s.y - s.radius);
1527
+ maxX = Math.max(maxX, s.x + s.radius);
1528
+ maxY = Math.max(maxY, s.y + s.radius);
1529
+ }
1530
+ if (!Number.isFinite(minX)) return { x: 0, y: 0, w: 1, h: 1 };
1531
+ return {
1532
+ x: minX - NODE_H,
1533
+ y: minY - NODE_H,
1534
+ w: maxX - minX + 2 * NODE_H,
1535
+ h: maxY - minY + 2 * NODE_H
1536
+ };
1537
+ }
1538
+ function graphTraceSvg(trace, vb, w, h, bg) {
1539
+ const parts = [
1540
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="${round2(vb.x)} ${round2(vb.y)} ${round2(vb.w)} ${round2(vb.h)}">`,
1541
+ `<rect x="${round2(vb.x)}" y="${round2(vb.y)}" width="${round2(vb.w)}" height="${round2(vb.h)}" fill="${bg}"/>`
1542
+ ];
1543
+ for (const e of trace.edges) {
1544
+ parts.push(
1545
+ `<line x1="${round2(e.x1)}" y1="${round2(e.y1)}" x2="${round2(e.x2)}" y2="${round2(e.y2)}" stroke="${e.colorB || "#6e7681"}" stroke-width="2" stroke-opacity="${round2(e.op)}"/>`
1546
+ );
1547
+ }
1548
+ for (const p of trace.nodes) {
1549
+ const pts = p.points.map(([x, y]) => `${round2(p.x + x)},${round2(p.y + y)}`).join(" ");
1550
+ parts.push(
1551
+ `<polygon points="${pts}" fill="${p.fill}" fill-opacity="${round2(p.fillOp * p.op)}" stroke="${p.fill}" stroke-opacity="${round2((1 - p.fillOp * 0.7) * p.op)}" stroke-width="2"/>`
1552
+ );
1553
+ for (const tx of p.texts) {
1554
+ parts.push(
1555
+ `<text x="${round2(p.x)}" y="${round2(p.y)}" font-size="13" fill="${tx.color}" opacity="${round2(tx.op * p.op)}" text-anchor="middle" dominant-baseline="central" font-family="${FONT}">${esc(tx.text)}</text>`
1556
+ );
1557
+ }
1558
+ }
1559
+ parts.push("</svg>");
1560
+ return parts.join("");
1561
+ }
1562
+ function sideBySideReductionSvgs(states, opts = {}, s = makeSettings(17, 10)) {
1563
+ if (states.length < 2)
1564
+ throw new Error("need at least two reduction states for a side-by-side GIF");
1565
+ const holdMs = opts.holdMs ?? 260;
1566
+ const stepMs = opts.stepMs ?? 40;
1567
+ const blockPrims = states.map((f2) => boxesToPrims(placeProgram(f2, s), s, null));
1568
+ let blockVb = boundsOf(placeProgram(states[0], s), s);
1569
+ for (const f2 of states) blockVb = union(blockVb, boundsOf(placeProgram(f2, s), s));
1570
+ const graphFrames = states.map((f2) => traceFrame(atomToGraph(f2), VP));
1571
+ let graphVb = traceBounds(graphFrames[0]);
1572
+ for (const gf of graphFrames) graphVb = union(graphVb, traceBounds(gf));
1573
+ const cellAspect = Math.sqrt(graphVb.w / graphVb.h * (blockVb.w / blockVb.h));
1574
+ const naturalPanelH = 340;
1575
+ const naturalGap = 28;
1576
+ const naturalLabelH = 30;
1577
+ const naturalCellW = Math.max(1, Math.round(naturalPanelH * cellAspect));
1578
+ const naturalWidth = naturalCellW * 2 + naturalGap;
1579
+ const scale = (opts.width ?? naturalWidth) / naturalWidth;
1580
+ const panelH = Math.max(1, Math.round(naturalPanelH * scale));
1581
+ const gap = Math.max(1, Math.round(naturalGap * scale));
1582
+ const labelH = Math.max(1, Math.round(naturalLabelH * scale));
1583
+ const cellW = Math.max(1, Math.round(naturalCellW * scale));
1584
+ const totalW = cellW + gap + cellW;
1585
+ const totalH = panelH + labelH;
1586
+ const background = opts.background ?? s.canvas;
1587
+ const fontSize = Math.max(8, Math.round(13 * scale));
1588
+ const frames = [];
1589
+ const addFrame = (bp, gt, delay) => {
1590
+ const graph = graphTraceSvg(gt, graphVb, cellW, panelH, background);
1591
+ const blocks = frameSvg(bp, blockVb, cellW, panelH, background);
1592
+ const graphUri = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(graph);
1593
+ const blocksUri = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(blocks);
1594
+ frames.push({
1595
+ delay,
1596
+ svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="${totalH}" viewBox="0 0 ${totalW} ${totalH}"><rect width="${totalW}" height="${totalH}" fill="${background}"/><text x="${cellW / 2}" y="${labelH / 2}" font-size="${fontSize}" font-weight="600" fill="#8b949e" text-anchor="middle" dominant-baseline="central" font-family="${FONT}">graph</text><text x="${cellW + gap + cellW / 2}" y="${labelH / 2}" font-size="${fontSize}" font-weight="600" fill="#8b949e" text-anchor="middle" dominant-baseline="central" font-family="${FONT}">blocks</text><image href="${graphUri}" x="0" y="${labelH}" width="${cellW}" height="${panelH}"/><image href="${blocksUri}" x="${cellW + gap}" y="${labelH}" width="${cellW}" height="${panelH}"/></svg>`
1597
+ });
1598
+ };
1599
+ const n = states.length;
1600
+ const perStep = framesPerStepFor(opts, n - 1);
1601
+ addFrame(blockPrims[0], interpolateTrace(graphFrames[0], graphFrames[0], 0), holdMs);
1602
+ for (let i = 1; i < n; i++) {
1603
+ for (let k = 1; k <= perStep; k++) {
1604
+ const t = ease(k / perStep);
1605
+ const bp = interpolate(blockPrims[i - 1], blockPrims[i], t);
1606
+ const gt = interpolateTrace(graphFrames[i - 1], graphFrames[i], t);
1607
+ addFrame(bp, gt, k === perStep ? holdMs : stepMs);
1608
+ }
1609
+ }
1610
+ return { frames, width: totalW, height: totalH, background };
1611
+ }
1612
+ function graphReductionSvgs(states, opts = {}) {
1613
+ if (states.length < 2) throw new Error("need at least two reduction states for a graph GIF");
1614
+ const bg = opts.background ?? CANVAS_BG;
1615
+ const holdMs = opts.holdMs ?? 260;
1616
+ const stepMs = opts.stepMs ?? 40;
1617
+ const width = opts.width ?? 880;
1618
+ const height = Math.round(width * 0.5);
1619
+ const frames = states.map((f2) => {
1620
+ const fr = traceFrame(atomToGraph(f2), VP);
1621
+ const b = traceBounds(fr);
1622
+ const scale = Math.max(0.68, Math.min(Math.min(width / b.w, height / b.h), 1));
1623
+ const viewport = {
1624
+ scale,
1625
+ panX: width / 2 - (b.x + b.w / 2) * scale,
1626
+ panY: height / 2 - (b.y + b.h / 2) * scale
1627
+ };
1628
+ return { ...fr, viewport };
1629
+ });
1630
+ const out = [];
1631
+ const push2 = (gt, delay) => {
1632
+ const v = gt.viewport;
1633
+ const vb = {
1634
+ x: -v.panX / v.scale,
1635
+ y: -v.panY / v.scale,
1636
+ w: width / v.scale,
1637
+ h: height / v.scale
1638
+ };
1639
+ out.push({ svg: graphTraceSvg(gt, vb, width, height, bg), delay });
1640
+ };
1641
+ const n = states.length;
1642
+ const perStep = framesPerStepFor(opts, n - 1);
1643
+ push2(interpolateTrace(frames[0], frames[0], 0), holdMs);
1644
+ for (let i = 1; i < n; i++) {
1645
+ for (let k = 1; k <= perStep; k++) {
1646
+ const t = ease(k / perStep);
1647
+ push2(interpolateTrace(frames[i - 1], frames[i], t), k === perStep ? holdMs : stepMs);
1648
+ }
1649
+ }
1650
+ return { frames: out, width, height, background: bg };
1651
+ }
1652
+ function blockReductionSvgs(states, opts = {}) {
1653
+ if (states.length < 2) throw new Error("need at least two reduction states for a block GIF");
1654
+ return blockReductionSvgsWithSettings(states, makeSettings(17, 10), opts);
1655
+ }
1656
+
1657
+ // src/node.ts
1658
+ var MAX_DIMENSION = 4096;
1659
+ var MAX_FRAMES = 360;
1660
+ var MAX_TOTAL_PIXELS = 1e8;
1661
+ var MAX_OUTPUT_BYTES = 128 * 1024 * 1024;
1662
+ function positiveInteger(name, value, max) {
1663
+ if (!Number.isInteger(value) || value < 1 || value > max)
1664
+ throw new Error(`${name} must be an integer from 1 to ${max}, got ${value}`);
1665
+ }
1666
+ function validateOptions(opts) {
1667
+ if (opts.view !== void 0 && opts.view !== "blocks" && opts.view !== "graph" && opts.view !== "side-by-side")
1668
+ throw new Error(`view must be "blocks", "graph", or "side-by-side", got ${String(opts.view)}`);
1669
+ if (opts.width !== void 0) positiveInteger("width", opts.width, MAX_DIMENSION);
1670
+ if (opts.framesPerStep !== void 0)
1671
+ positiveInteger("framesPerStep", opts.framesPerStep, MAX_FRAMES);
1672
+ if (opts.maxFrames !== void 0) positiveInteger("maxFrames", opts.maxFrames, MAX_FRAMES);
1673
+ if (opts.maxSteps !== void 0) positiveInteger("maxSteps", opts.maxSteps, 1e4);
1674
+ for (const [name, value] of [
1675
+ ["holdMs", opts.holdMs],
1676
+ ["stepMs", opts.stepMs],
1677
+ ["morphMs", opts.morphMs]
1678
+ ])
1679
+ if (value !== void 0 && (!Number.isFinite(value) || value < 0 || value > 6e4))
1680
+ throw new Error(`${name} must be between 0 and 60000, got ${value}`);
1681
+ }
1682
+ function inputAtoms(input) {
1683
+ if (typeof input === "string") return parseProgram(input);
1684
+ return Array.isArray(input) ? [...input] : [input];
1685
+ }
1686
+ function isDefinition(atom) {
1687
+ if (!(atom instanceof ExpressionAtom4)) return false;
1688
+ const head = atom.children()[0]?.toString();
1689
+ return head === "=" || head === ":";
1690
+ }
1691
+ function traceInput(input, opts) {
1692
+ const atoms = inputAtoms(input);
1693
+ const query = [...atoms].reverse().find((atom) => !isDefinition(atom));
1694
+ if (query === void 0)
1695
+ throw new Error("renderReductionGif: input has no query; add a non-definition atom");
1696
+ const metta = opts.metta ?? new MeTTa();
1697
+ for (const atom of atoms) metta.space().addAtom(atom);
1698
+ const states = reduceTrace(query, metta, opts.maxSteps ?? 300);
1699
+ if (states.length < 2)
1700
+ throw new Error(`renderReductionGif: query does not reduce: ${query.toString()}`);
1701
+ return states;
1702
+ }
1703
+ function framesFor(states, opts) {
1704
+ switch (opts.view ?? "blocks") {
1705
+ case "blocks":
1706
+ return blockReductionSvgs(states, opts);
1707
+ case "graph":
1708
+ return graphReductionSvgs(states, opts);
1709
+ case "side-by-side":
1710
+ return sideBySideReductionSvgs(states, opts);
1711
+ }
1712
+ }
1713
+ function validateAnimation(animation, opts) {
1714
+ positiveInteger("rendered width", animation.width, MAX_DIMENSION);
1715
+ positiveInteger("rendered height", animation.height, MAX_DIMENSION);
1716
+ const frameLimit = opts.maxFrames ?? 180;
1717
+ if (animation.frames.length > frameLimit)
1718
+ throw new Error(
1719
+ `renderReductionGif: ${animation.frames.length} frames exceed maxFrames ${frameLimit}`
1720
+ );
1721
+ const pixels = animation.width * animation.height * animation.frames.length;
1722
+ if (!Number.isSafeInteger(pixels) || pixels > MAX_TOTAL_PIXELS)
1723
+ throw new Error(
1724
+ `renderReductionGif: ${pixels} raster pixels exceed the ${MAX_TOTAL_PIXELS} safety limit`
1725
+ );
1726
+ }
1727
+ async function loadGifEncoder() {
1728
+ try {
1729
+ const loaded = await import("gifenc");
1730
+ const lib = typeof loaded.GIFEncoder === "function" ? loaded : loaded.default;
1731
+ if (lib === void 0 || typeof lib.GIFEncoder !== "function" || typeof lib.quantize !== "function" || typeof lib.applyPalette !== "function")
1732
+ throw new Error("gifenc did not expose GIFEncoder, quantize, and applyPalette");
1733
+ return lib;
1734
+ } catch (error) {
1735
+ throw new Error(
1736
+ "@mettascript/grapher/node requires gifenc; install it with `npm install gifenc sharp`",
1737
+ { cause: error }
1738
+ );
1739
+ }
1740
+ }
1741
+ async function sharpRasterizer() {
1742
+ let sharp;
1743
+ try {
1744
+ sharp = (await import("sharp")).default;
1745
+ } catch (error) {
1746
+ throw new Error(
1747
+ "@mettascript/grapher/node requires sharp; install it with `npm install sharp gifenc`",
1748
+ { cause: error }
1749
+ );
1750
+ }
1751
+ return async (svg, width, height, background) => {
1752
+ const { data, info } = await sharp(Buffer.from(svg)).resize(width, height, { fit: "fill" }).flatten({ background }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
1753
+ if (info.width !== width || info.height !== height || info.channels !== 4)
1754
+ throw new Error(
1755
+ `sharp returned ${info.width}x${info.height}x${info.channels}; expected ${width}x${height}x4`
1756
+ );
1757
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1758
+ };
1759
+ }
1760
+ async function renderReductionGif(input, opts = {}) {
1761
+ validateOptions(opts);
1762
+ const animation = framesFor(traceInput(input, opts), opts);
1763
+ validateAnimation(animation, opts);
1764
+ const [rasterize, encoder] = await Promise.all([sharpRasterizer(), loadGifEncoder()]);
1765
+ const bytes = await encodeSvgAnimation(
1766
+ animation,
1767
+ rasterize,
1768
+ encoder,
1769
+ (opts.view ?? "blocks") === "blocks" ? 256 : 128
1770
+ );
1771
+ if (bytes.byteLength > MAX_OUTPUT_BYTES)
1772
+ throw new Error(
1773
+ `renderReductionGif: ${bytes.byteLength} output bytes exceed the ${MAX_OUTPUT_BYTES} safety limit`
1774
+ );
1775
+ return bytes;
1776
+ }
1777
+ export {
1778
+ renderReductionGif
1779
+ };