fui-studio-mcp 0.1.0 → 0.1.2

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.
@@ -276,17 +276,17 @@ function emptyProject(name = "untitled") {
276
276
  return Project.parse({ name, canvas: { origin: "center" } });
277
277
  }
278
278
  var ROOT_TYPES = /* @__PURE__ */ new Set(["canvas", "layer", "ograf"]);
279
- function layerAssignment(graph) {
280
- const typeOf = new Map(graph.nodes.map((n) => [n.id, n.type]));
279
+ function layerAssignment(graph2) {
280
+ const typeOf = new Map(graph2.nodes.map((n) => [n.id, n.type]));
281
281
  const sources = /* @__PURE__ */ new Map();
282
- for (const e of graph.edges) {
282
+ for (const e of graph2.edges) {
283
283
  if (!typeOf.has(e.from) || !typeOf.has(e.to)) continue;
284
284
  const list = sources.get(e.to);
285
285
  if (list) list.push(e.from);
286
286
  else sources.set(e.to, [e.from]);
287
287
  }
288
288
  const reached = /* @__PURE__ */ new Map();
289
- for (const layer of graph.nodes) {
289
+ for (const layer of graph2.nodes) {
290
290
  if (layer.type !== "layer") continue;
291
291
  const stack = [...sources.get(layer.id) ?? []];
292
292
  while (stack.length > 0) {
@@ -303,25 +303,25 @@ function layerAssignment(graph) {
303
303
  }
304
304
  }
305
305
  const out = /* @__PURE__ */ new Map();
306
- for (const n of graph.nodes) {
306
+ for (const n of graph2.nodes) {
307
307
  const r = ROOT_TYPES.has(n.type) ? void 0 : reached.get(n.id);
308
308
  out.set(n.id, r && r.size === 1 ? [...r][0] : void 0);
309
309
  }
310
310
  return out;
311
311
  }
312
- function assignLayers(graph) {
313
- const assignment = layerAssignment(graph);
314
- for (const n of graph.nodes) {
312
+ function assignLayers(graph2) {
313
+ const assignment = layerAssignment(graph2);
314
+ for (const n of graph2.nodes) {
315
315
  const layer = assignment.get(n.id);
316
316
  if (layer) n.layer = layer;
317
317
  else delete n.layer;
318
318
  }
319
- return graph;
319
+ return graph2;
320
320
  }
321
- function adoptions(graph, nodeId) {
322
- const typeOf = new Map(graph.nodes.map((n) => [n.id, n]));
321
+ function adoptions(graph2, nodeId) {
322
+ const typeOf = new Map(graph2.nodes.map((n) => [n.id, n]));
323
323
  const sources = /* @__PURE__ */ new Map();
324
- for (const e of graph.edges) {
324
+ for (const e of graph2.edges) {
325
325
  const list = sources.get(e.to);
326
326
  if (list) list.push(e.from);
327
327
  else sources.set(e.to, [e.from]);
@@ -335,7 +335,7 @@ function adoptions(graph, nodeId) {
335
335
  cone.add(id);
336
336
  stack.push(...sources.get(id) ?? []);
337
337
  }
338
- const assignment = layerAssignment(graph);
338
+ const assignment = layerAssignment(graph2);
339
339
  const out = /* @__PURE__ */ new Map();
340
340
  for (const id of cone) {
341
341
  const layer = assignment.get(id);
@@ -354,11 +354,11 @@ var MIGRATIONS = {
354
354
  * the outline starts at width 0, so rendered output is byte-identical.
355
355
  */
356
356
  1: (doc) => {
357
- const graph = doc.graph;
357
+ const graph2 = doc.graph;
358
358
  const textIds = new Set(
359
- (graph?.nodes ?? []).filter((n) => V1_TEXT_TYPES.has(n.type)).map((n) => n.id)
359
+ (graph2?.nodes ?? []).filter((n) => V1_TEXT_TYPES.has(n.type)).map((n) => n.id)
360
360
  );
361
- for (const n of graph?.nodes ?? []) {
361
+ for (const n of graph2?.nodes ?? []) {
362
362
  if (!textIds.has(n.id)) continue;
363
363
  const params = { ...n.params ?? {} };
364
364
  const stroke = params.stroke;
@@ -373,7 +373,7 @@ var MIGRATIONS = {
373
373
  params.strokeWidth = 0;
374
374
  n.params = params;
375
375
  }
376
- for (const e of graph?.edges ?? []) {
376
+ for (const e of graph2?.edges ?? []) {
377
377
  if (textIds.has(e.to) && e.toPort === "stroke") e.toPort = "fill";
378
378
  }
379
379
  return { ...doc, version: 2 };
@@ -384,8 +384,8 @@ var MIGRATIONS = {
384
384
  * them stamped in explicitly, so nothing rerenders.
385
385
  */
386
386
  2: (doc) => {
387
- const graph = doc.graph;
388
- for (const n of graph?.nodes ?? []) {
387
+ const graph2 = doc.graph;
388
+ for (const n of graph2?.nodes ?? []) {
389
389
  if (n.type !== "text") continue;
390
390
  const params = { ...n.params ?? {} };
391
391
  params.fontFamily ??= "IBM Plex Mono";
@@ -411,8 +411,8 @@ var MIGRATIONS = {
411
411
  "center-y": { angle: 0, alignX: false },
412
412
  bottom: { angle: 90, alignX: false }
413
413
  };
414
- const graph = doc.graph;
415
- for (const n of graph?.nodes ?? []) {
414
+ const graph2 = doc.graph;
415
+ for (const n of graph2?.nodes ?? []) {
416
416
  if (n.type !== "align") continue;
417
417
  const params = { ...n.params ?? {} };
418
418
  const mode = typeof params.mode === "string" ? params.mode : "center-x";
@@ -432,8 +432,8 @@ var MIGRATIONS = {
432
432
  /** v4 → v5: Rect's default height became 120 (square). Old docs that leaned
433
433
  * on the 80px default get it stamped in, so nothing rerenders. */
434
434
  4: (doc) => {
435
- const graph = doc.graph;
436
- for (const n of graph?.nodes ?? []) {
435
+ const graph2 = doc.graph;
436
+ for (const n of graph2?.nodes ?? []) {
437
437
  if (n.type !== "rect") continue;
438
438
  const params = { ...n.params ?? {} };
439
439
  params.h ??= 80;
@@ -448,8 +448,8 @@ var MIGRATIONS = {
448
448
  * walked the narrow version and re-saved are past helping, but a v5 file
449
449
  * opened today keeps its labels at width 0. */
450
450
  5: (doc) => {
451
- const graph = doc.graph;
452
- for (const n of graph?.nodes ?? []) {
451
+ const graph2 = doc.graph;
452
+ for (const n of graph2?.nodes ?? []) {
453
453
  if (!TEXT_TYPES.has(n.type)) continue;
454
454
  const params = { ...n.params ?? {} };
455
455
  params.strokeWidth ??= 0;
@@ -460,8 +460,8 @@ var MIGRATIONS = {
460
460
  /** v6 → v7: Boolean wears its input's style by default now. Old booleans
461
461
  * were dressed by their own Style params; they keep that. */
462
462
  6: (doc) => {
463
- const graph = doc.graph;
464
- for (const n of graph?.nodes ?? []) {
463
+ const graph2 = doc.graph;
464
+ for (const n of graph2?.nodes ?? []) {
465
465
  if (n.type !== "boolean") continue;
466
466
  const params = { ...n.params ?? {} };
467
467
  params.keepStyle ??= false;
@@ -473,8 +473,8 @@ var MIGRATIONS = {
473
473
  * to the new default; typed ones map to a stable small number (the scatter
474
474
  * rerolls once — the knob was never semantic). */
475
475
  7: (doc) => {
476
- const graph = doc.graph;
477
- for (const n of graph?.nodes ?? []) {
476
+ const graph2 = doc.graph;
477
+ for (const n of graph2?.nodes ?? []) {
478
478
  if (n.type !== "jitter") continue;
479
479
  const params = { ...n.params ?? {} };
480
480
  if (typeof params.salt === "string") {
@@ -489,8 +489,8 @@ var MIGRATIONS = {
489
489
  * documents that leaned on the implicit custom-(0,0) default get it
490
490
  * stamped in, so nothing starts spinning in place that used to orbit. */
491
491
  8: (doc) => {
492
- const graph = doc.graph;
493
- for (const n of graph?.nodes ?? []) {
492
+ const graph2 = doc.graph;
493
+ for (const n of graph2?.nodes ?? []) {
494
494
  if (n.type !== "transform") continue;
495
495
  const params = { ...n.params ?? {} };
496
496
  params.pivot ??= "custom";
@@ -505,8 +505,8 @@ var MIGRATIONS = {
505
505
  9: (doc) => {
506
506
  const canvas = doc.canvas;
507
507
  const centered = canvas?.origin === "center";
508
- const graph = doc.graph;
509
- for (const n of graph?.nodes ?? []) {
508
+ const graph2 = doc.graph;
509
+ for (const n of graph2?.nodes ?? []) {
510
510
  if (n.type !== "transform" && n.type !== "array-linear") continue;
511
511
  const params = { ...n.params ?? {} };
512
512
  const raw = params.pivot;
@@ -532,8 +532,8 @@ var MIGRATIONS = {
532
532
  * Slice's X/Y became shape-centre-anchored; old documents measured from
533
533
  * the canvas origin, stamped in so every cut stays where it was made. */
534
534
  10: (doc) => {
535
- const graph = doc.graph;
536
- for (const n of graph?.nodes ?? []) {
535
+ const graph2 = doc.graph;
536
+ for (const n of graph2?.nodes ?? []) {
537
537
  const params = { ...n.params ?? {} };
538
538
  if (n.type === "jitter") params.uniformScale ??= false;
539
539
  else if (n.type === "slice") params.anchor ??= "canvas";
@@ -548,8 +548,8 @@ var MIGRATIONS = {
548
548
  * (base only: the toggle was never exposable, so no wires ride it), and
549
549
  * docs that leaned on the width-1 default get it stamped in. */
550
550
  11: (doc) => {
551
- const graph = doc.graph;
552
- for (const n of graph?.nodes ?? []) {
551
+ const graph2 = doc.graph;
552
+ for (const n of graph2?.nodes ?? []) {
553
553
  if (!TEXT_TYPES.has(n.type)) continue;
554
554
  const params = { ...n.params ?? {} };
555
555
  params.strokeWidth ??= 1;
@@ -575,8 +575,8 @@ var MIGRATIONS = {
575
575
  * on the old corner default get it stamped in, so no lattice slides by
576
576
  * half its span. */
577
577
  13: (doc) => {
578
- const graph = doc.graph;
579
- for (const n of graph?.nodes ?? []) {
578
+ const graph2 = doc.graph;
579
+ for (const n of graph2?.nodes ?? []) {
580
580
  if (n.type !== "array-grid") continue;
581
581
  const params = { ...n.params ?? {} };
582
582
  params.origin ??= "corner";
@@ -588,8 +588,8 @@ var MIGRATIONS = {
588
588
  * Documents that leaned on the old grow-from-the-input default get 'start'
589
589
  * stamped in, so no row slides by half its span. */
590
590
  14: (doc) => {
591
- const graph = doc.graph;
592
- for (const n of graph?.nodes ?? []) {
591
+ const graph2 = doc.graph;
592
+ for (const n of graph2?.nodes ?? []) {
593
593
  if (n.type !== "array-linear") continue;
594
594
  const params = { ...n.params ?? {} };
595
595
  params.origin ??= "start";
@@ -603,8 +603,8 @@ var MIGRATIONS = {
603
603
  * #ff0000, so a fresh override is visibly an override. Old documents
604
604
  * leaned on both defaults and get them stamped. */
605
605
  15: (doc) => {
606
- const graph = doc.graph;
607
- for (const n of graph?.nodes ?? []) {
606
+ const graph2 = doc.graph;
607
+ for (const n of graph2?.nodes ?? []) {
608
608
  if (n.type !== "style") continue;
609
609
  const params = { ...n.params ?? {} };
610
610
  params.setStroke ??= true;
@@ -617,9 +617,9 @@ var MIGRATIONS = {
617
617
  * param tap used to sit. A wire drawn from that tap (`param:ink`) now has no
618
618
  * handle; it becomes the named output (`ink`), which carries the same value. */
619
619
  16: (doc) => {
620
- const graph = doc.graph;
621
- const tokens = new Set((graph?.nodes ?? []).filter((n) => n.type === "style-tokens").map((n) => n.id));
622
- for (const e of graph?.edges ?? []) {
620
+ const graph2 = doc.graph;
621
+ const tokens = new Set((graph2?.nodes ?? []).filter((n) => n.type === "style-tokens").map((n) => n.id));
622
+ for (const e of graph2?.edges ?? []) {
623
623
  const port = e.fromPort;
624
624
  if (tokens.has(e.from) && typeof port === "string" && port.startsWith("param:")) e.fromPort = port.slice(6);
625
625
  }
@@ -631,11 +631,11 @@ var MIGRATIONS = {
631
631
  * in the same order as its defaults), the first Count of them ticked to
632
632
  * deal; wires into the swatch sockets follow their colours. */
633
633
  17: (doc) => {
634
- const graph = doc.graph;
634
+ const graph2 = doc.graph;
635
635
  const ROLE = ["accent", "dim", "faint", "ink", "warn", "bg"];
636
636
  const DEAL = ["dealAccent", "dealDim", "dealFaint", "dealInk", "dealWarn", "dealBg"];
637
637
  const palettes = /* @__PURE__ */ new Set();
638
- for (const n of graph?.nodes ?? []) {
638
+ for (const n of graph2?.nodes ?? []) {
639
639
  if (n.type !== "palette") continue;
640
640
  palettes.add(n.id);
641
641
  const old = n.params ?? {};
@@ -652,7 +652,7 @@ var MIGRATIONS = {
652
652
  n.type = "style-tokens";
653
653
  n.params = params;
654
654
  }
655
- for (const e of graph?.edges ?? []) {
655
+ for (const e of graph2?.edges ?? []) {
656
656
  if (!palettes.has(e.to) || typeof e.toPort !== "string") continue;
657
657
  const m = /^color([1-6])$/.exec(e.toPort);
658
658
  if (m) e.toPort = ROLE[Number(m[1]) - 1];
@@ -664,8 +664,8 @@ var MIGRATIONS = {
664
664
  * fall to the new default and key identically, so the drift is unchanged;
665
665
  * typed ones map to a stable small number and reroll once. */
666
666
  18: (doc) => {
667
- const graph = doc.graph;
668
- for (const n of graph?.nodes ?? []) {
667
+ const graph2 = doc.graph;
668
+ for (const n of graph2?.nodes ?? []) {
669
669
  if (n.type !== "noise") continue;
670
670
  const params = { ...n.params ?? {} };
671
671
  if (typeof params.salt === "string") {
@@ -679,9 +679,9 @@ var MIGRATIONS = {
679
679
  /** v19 → v20: the last text Variations became Seeds — LFO, Random Value,
680
680
  * Random Step and the three Switches — the same way Noise's did at v19. */
681
681
  19: (doc) => {
682
- const graph = doc.graph;
682
+ const graph2 = doc.graph;
683
683
  const TYPES = /* @__PURE__ */ new Set(["lfo", "random-value", "random-step", "switch", "switch-value", "switch-color"]);
684
- for (const n of graph?.nodes ?? []) {
684
+ for (const n of graph2?.nodes ?? []) {
685
685
  if (!TYPES.has(n.type)) continue;
686
686
  const params = { ...n.params ?? {} };
687
687
  if (typeof params.salt === "string") {
@@ -696,8 +696,8 @@ var MIGRATIONS = {
696
696
  * saved without the flag wore the theme under the old default, so it is
697
697
  * stamped `wear: true` and keeps dressing the studio as it did. */
698
698
  20: (doc) => {
699
- const graph = doc.graph;
700
- for (const n of graph?.nodes ?? []) {
699
+ const graph2 = doc.graph;
700
+ for (const n of graph2?.nodes ?? []) {
701
701
  if (n.type !== "style-tokens") continue;
702
702
  const params = { ...n.params ?? {} };
703
703
  if (params.wear === void 0) params.wear = true;
@@ -715,10 +715,10 @@ var MIGRATIONS = {
715
715
  * input shifts by one.
716
716
  */
717
717
  21: (doc) => {
718
- const graph = doc.graph;
718
+ const graph2 = doc.graph;
719
719
  const TYPES = /* @__PURE__ */ new Set(["switch", "switch-value", "switch-color"]);
720
720
  const baseOf = (p) => typeof p === "object" && p !== null ? p.base : p;
721
- for (const n of graph?.nodes ?? []) {
721
+ for (const n of graph2?.nodes ?? []) {
722
722
  if (!TYPES.has(n.type)) continue;
723
723
  const params = { ...n.params ?? {} };
724
724
  if (baseOf(params.range) === "normalized") continue;
@@ -745,8 +745,8 @@ var MIGRATIONS = {
745
745
  * frame — centre-out in a centre-origin document. Old documents scattered
746
746
  * from the top-left corner of X/Y; stamped in so nothing saved moves. */
747
747
  22: (doc) => {
748
- const graph = doc.graph;
749
- for (const n of graph?.nodes ?? []) {
748
+ const graph2 = doc.graph;
749
+ for (const n of graph2?.nodes ?? []) {
750
750
  if (n.type !== "scatter") continue;
751
751
  const params = { ...n.params ?? {} };
752
752
  params.anchor ??= "corner";
@@ -758,8 +758,8 @@ var MIGRATIONS = {
758
758
  * Dot Matrix — centred on X/Y in a centre-origin document. Old documents
759
759
  * hung the stack down and right from X/Y; stamped in so no line moves. */
760
760
  23: (doc) => {
761
- const graph = doc.graph;
762
- for (const n of graph?.nodes ?? []) {
761
+ const graph2 = doc.graph;
762
+ for (const n of graph2?.nodes ?? []) {
763
763
  if (n.type !== "random-code") continue;
764
764
  const params = { ...n.params ?? {} };
765
765
  params.stackAnchor ??= "corner";
@@ -773,9 +773,9 @@ var MIGRATIONS = {
773
773
  * row at the bottom; those with Square Cells on get Top stamped so no
774
774
  * line or copy moves. The rest take the new default when they tick it. */
775
775
  24: (doc) => {
776
- const graph = doc.graph;
776
+ const graph2 = doc.graph;
777
777
  const baseOf = (p) => typeof p === "object" && p !== null ? p.base : p;
778
- for (const n of graph?.nodes ?? []) {
778
+ for (const n of graph2?.nodes ?? []) {
779
779
  if (n.type !== "grid" && n.type !== "array-grid") continue;
780
780
  const params = { ...n.params ?? {} };
781
781
  if (baseOf(params.square) !== true) continue;
@@ -801,7 +801,7 @@ var MIGRATIONS = {
801
801
  * numbers are its own, and the drawing turns a quarter.
802
802
  */
803
803
  26: (doc) => {
804
- const graph = doc.graph;
804
+ const graph2 = doc.graph;
805
805
  const DEFAULT = {
806
806
  circle: 0,
807
807
  "tick-ring": 0,
@@ -811,7 +811,7 @@ var MIGRATIONS = {
811
811
  sort: 0,
812
812
  bend: void 0
813
813
  };
814
- for (const n of graph?.nodes ?? []) {
814
+ for (const n of graph2?.nodes ?? []) {
815
815
  const type = n.type;
816
816
  if (!(type in DEFAULT)) continue;
817
817
  const params = { ...n.params ?? {} };
@@ -851,10 +851,10 @@ var MIGRATIONS = {
851
851
  * plays always, as before.
852
852
  */
853
853
  27: (doc) => {
854
- const graph = doc.graph;
855
- if (graph) {
856
- const nodes = graph.nodes ??= [];
857
- const edges = graph.edges ??= [];
854
+ const graph2 = doc.graph;
855
+ if (graph2) {
856
+ const nodes = graph2.nodes ??= [];
857
+ const edges = graph2.edges ??= [];
858
858
  const byId = new Map(nodes.map((n) => [n.id, n]));
859
859
  for (const canvas of nodes.filter((n) => n.type === "canvas")) {
860
860
  let k = 0;
@@ -887,7 +887,7 @@ var MIGRATIONS = {
887
887
  k++;
888
888
  }
889
889
  }
890
- assignLayers(graph);
890
+ assignLayers(graph2);
891
891
  }
892
892
  return { ...doc, clips: Array.isArray(doc.clips) ? doc.clips : [], version: 28 };
893
893
  },
@@ -898,8 +898,8 @@ var MIGRATIONS = {
898
898
  * the SVG's `data-layer` reads the same string, so nothing moves.
899
899
  */
900
900
  28: (doc) => {
901
- const graph = doc.graph;
902
- for (const n of graph?.nodes ?? []) {
901
+ const graph2 = doc.graph;
902
+ for (const n of graph2?.nodes ?? []) {
903
903
  if (n.type !== "layer") continue;
904
904
  const params = { ...n.params ?? {} };
905
905
  const raw = params.name;
@@ -916,8 +916,8 @@ var MIGRATIONS = {
916
916
  * far) or below moves up as one, so nothing changes places.
917
917
  */
918
918
  29: (doc) => {
919
- const graph = doc.graph;
920
- const layers = (graph?.nodes ?? []).filter((n) => n.type === "layer");
919
+ const graph2 = doc.graph;
920
+ const layers = (graph2?.nodes ?? []).filter((n) => n.type === "layer");
921
921
  const orderOf = (n) => {
922
922
  const raw = n.params?.order;
923
923
  const base2 = typeof raw === "object" && raw !== null ? raw.base : raw;
@@ -941,9 +941,9 @@ var MIGRATIONS = {
941
941
  * Ref point from now on; its output is the same.
942
942
  */
943
943
  30: (doc) => {
944
- const graph = doc.graph;
945
- const refWired = new Set((graph?.edges ?? []).filter((e) => e.toPort === "ref").map((e) => e.to));
946
- for (const n of graph?.nodes ?? []) {
944
+ const graph2 = doc.graph;
945
+ const refWired = new Set((graph2?.edges ?? []).filter((e) => e.toPort === "ref").map((e) => e.to));
946
+ for (const n of graph2?.nodes ?? []) {
947
947
  if (n.type !== "transform" || !refWired.has(n.id)) continue;
948
948
  const params = { ...n.params ?? {} };
949
949
  const raw = params.pivot;
@@ -960,9 +960,9 @@ var MIGRATIONS = {
960
960
  * list is its place in the layer's stack, and that is kept.
961
961
  */
962
962
  31: (doc) => {
963
- const graph = doc.graph;
964
- const layers = new Set((graph?.nodes ?? []).filter((n) => n.type === "layer").map((n) => n.id));
965
- for (const e of graph?.edges ?? []) {
963
+ const graph2 = doc.graph;
964
+ const layers = new Set((graph2?.nodes ?? []).filter((n) => n.type === "layer").map((n) => n.id));
965
+ for (const e of graph2?.edges ?? []) {
966
966
  if (e.toPort === "texture" && layers.has(e.to)) e.toPort = "in";
967
967
  }
968
968
  return { ...doc, version: 32 };
@@ -974,8 +974,8 @@ var MIGRATIONS = {
974
974
  * does not change under it.
975
975
  */
976
976
  32: (doc) => {
977
- const graph = doc.graph;
978
- for (const node of graph?.nodes ?? []) {
977
+ const graph2 = doc.graph;
978
+ for (const node of graph2?.nodes ?? []) {
979
979
  if (node.type !== "grid" && node.type !== "polar-grid") continue;
980
980
  const params = node.params ??= {};
981
981
  params.subJoin ??= false;
@@ -1390,6 +1390,16 @@ function pathPolylines(d) {
1390
1390
  flush(false);
1391
1391
  return out;
1392
1392
  }
1393
+ function polylinesToPathD(lines) {
1394
+ const r35 = (n) => String(Math.round(n * 1e3) / 1e3);
1395
+ const parts = [];
1396
+ for (const line of lines) {
1397
+ if (line.points.length < 2) continue;
1398
+ const [first, ...rest] = line.points;
1399
+ parts.push(`M${r35(first.x)} ${r35(first.y)}` + rest.map((p) => `L${r35(p.x)} ${r35(p.y)}`).join("") + (line.closed ? "Z" : ""));
1400
+ }
1401
+ return parts.join("");
1402
+ }
1393
1403
  var CIRCLE_SEGMENTS = 96;
1394
1404
  function arcPoints(cx, cy, r, a0, a1) {
1395
1405
  const sweep = Math.abs(a1 - a0);
@@ -1582,8 +1592,8 @@ function centerOf(b) {
1582
1592
  return { x: b.x + b.w / 2, y: b.y + b.h / 2 };
1583
1593
  }
1584
1594
  function polar(cx, cy, r, deg) {
1585
- const rad = deg * Math.PI / 180;
1586
- return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
1595
+ const rad2 = deg * Math.PI / 180;
1596
+ return { x: cx + r * Math.cos(rad2), y: cy + r * Math.sin(rad2) };
1587
1597
  }
1588
1598
  function pointInPolygon(point, polygon) {
1589
1599
  let inside = false;
@@ -1595,6 +1605,217 @@ function pointInPolygon(point, polygon) {
1595
1605
  }
1596
1606
  return inside;
1597
1607
  }
1608
+ var NEAR_W = 0.01;
1609
+ function mat4Multiply(a, b) {
1610
+ const out = new Array(16);
1611
+ const A = a;
1612
+ const B = b;
1613
+ for (let r = 0; r < 4; r++) {
1614
+ for (let c = 0; c < 4; c++) {
1615
+ out[r * 4 + c] = A[r * 4] * B[c] + A[r * 4 + 1] * B[4 + c] + A[r * 4 + 2] * B[8 + c] + A[r * 4 + 3] * B[12 + c];
1616
+ }
1617
+ }
1618
+ return out;
1619
+ }
1620
+ function mat4Translate(x, y, z3) {
1621
+ return [1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z3, 0, 0, 0, 1];
1622
+ }
1623
+ function mat4Scale(sx, sy, sz) {
1624
+ return [sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, sz, 0, 0, 0, 0, 1];
1625
+ }
1626
+ var rad = (deg) => deg * Math.PI / 180;
1627
+ function mat4RotateX(deg) {
1628
+ const c = Math.cos(rad(deg));
1629
+ const s = Math.sin(rad(deg));
1630
+ return [1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1];
1631
+ }
1632
+ function mat4RotateY(deg) {
1633
+ const c = Math.cos(rad(deg));
1634
+ const s = Math.sin(rad(deg));
1635
+ return [c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1];
1636
+ }
1637
+ function mat4RotateZ(deg) {
1638
+ const c = Math.cos(rad(deg));
1639
+ const s = Math.sin(rad(deg));
1640
+ return [c, -s, 0, 0, s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
1641
+ }
1642
+ function isIdentityPose(pose) {
1643
+ return pose.x === 0 && pose.y === 0 && pose.z === 0 && pose.rx === 0 && pose.ry === 0 && pose.rz === 0 && pose.sx === 1 && pose.sy === 1;
1644
+ }
1645
+ function frameCentre(camera) {
1646
+ return camera.origin === "center" ? { x: 0, y: 0 } : { x: camera.width / 2, y: camera.height / 2 };
1647
+ }
1648
+ function layerModelMatrix(pose, centre) {
1649
+ const px = pose.pivot.x - centre.x;
1650
+ const py = pose.pivot.y - centre.y;
1651
+ let m = mat4Translate(pose.x, pose.y, pose.z);
1652
+ m = mat4Multiply(m, mat4Translate(px, py, 0));
1653
+ if (pose.rz !== 0) m = mat4Multiply(m, mat4RotateZ(pose.rz));
1654
+ if (pose.ry !== 0) m = mat4Multiply(m, mat4RotateY(pose.ry));
1655
+ if (pose.rx !== 0) m = mat4Multiply(m, mat4RotateX(pose.rx));
1656
+ if (pose.sx !== 1 || pose.sy !== 1) m = mat4Multiply(m, mat4Scale(pose.sx, pose.sy, 1));
1657
+ m = mat4Multiply(m, mat4Translate(-px, -py, 0));
1658
+ return m;
1659
+ }
1660
+ function planeHomography(model, perspective) {
1661
+ const row2 = perspective > 0 ? [model[8] / perspective, model[9] / perspective, 1 + model[11] / perspective] : [0, 0, 1];
1662
+ return [model[0], model[1], model[3], model[4], model[5], model[7], ...row2];
1663
+ }
1664
+ function mat3Multiply(a, b) {
1665
+ const out = new Array(9);
1666
+ const A = a;
1667
+ const B = b;
1668
+ for (let r = 0; r < 3; r++) {
1669
+ for (let c = 0; c < 3; c++) {
1670
+ out[r * 3 + c] = A[r * 3] * B[c] + A[r * 3 + 1] * B[3 + c] + A[r * 3 + 2] * B[6 + c];
1671
+ }
1672
+ }
1673
+ return out;
1674
+ }
1675
+ function mat3Translate(x, y) {
1676
+ return [1, 0, x, 0, 1, y, 0, 0, 1];
1677
+ }
1678
+ function layerHomography(pose, camera) {
1679
+ const c = frameCentre(camera);
1680
+ const hc = planeHomography(layerModelMatrix(pose, c), camera.perspective);
1681
+ return mat3Multiply(mat3Translate(c.x, c.y), mat3Multiply(hc, mat3Translate(-c.x, -c.y)));
1682
+ }
1683
+ function invertMat3(m) {
1684
+ const [a, b, c, d, e, f, g, h, i] = m;
1685
+ const A = e * i - f * h;
1686
+ const B = -(d * i - f * g);
1687
+ const C = d * h - e * g;
1688
+ const det = a * A + b * B + c * C;
1689
+ if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return void 0;
1690
+ const inv = 1 / det;
1691
+ return [
1692
+ A * inv,
1693
+ -(b * i - c * h) * inv,
1694
+ (b * f - c * e) * inv,
1695
+ B * inv,
1696
+ (a * i - c * g) * inv,
1697
+ -(a * f - c * d) * inv,
1698
+ C * inv,
1699
+ -(a * h - b * g) * inv,
1700
+ (a * e - b * d) * inv
1701
+ ];
1702
+ }
1703
+ function homTransform(h, p) {
1704
+ return {
1705
+ u: h[0] * p.x + h[1] * p.y + h[2],
1706
+ v: h[3] * p.x + h[4] * p.y + h[5],
1707
+ w: h[6] * p.x + h[7] * p.y + h[8]
1708
+ };
1709
+ }
1710
+ function rigidAffineOf(h) {
1711
+ const k = h[8];
1712
+ if (!Number.isFinite(k) || Math.abs(k) < 1e-12) return void 0;
1713
+ const eps = 1e-9;
1714
+ if (Math.abs(h[6] / k) > eps || Math.abs(h[7] / k) > eps) return void 0;
1715
+ const a = h[0] / k;
1716
+ const c = h[1] / k;
1717
+ const e = h[2] / k;
1718
+ const b = h[3] / k;
1719
+ const d = h[4] / k;
1720
+ const f = h[5] / k;
1721
+ if (Math.abs(a * a + b * b - 1) > eps || Math.abs(c * c + d * d - 1) > eps || Math.abs(a * c + b * d) > eps) return void 0;
1722
+ return [a, b, c, d, e, f];
1723
+ }
1724
+ function affineAt(h, p0) {
1725
+ const { u, v, w } = homTransform(h, p0);
1726
+ if (w < NEAR_W) return void 0;
1727
+ const w2 = w * w;
1728
+ const a = (h[0] * w - u * h[6]) / w2;
1729
+ const b = (h[3] * w - v * h[6]) / w2;
1730
+ const c = (h[1] * w - u * h[7]) / w2;
1731
+ const d = (h[4] * w - v * h[7]) / w2;
1732
+ const x = u / w;
1733
+ const y = v / w;
1734
+ return [a, b, c, d, x - (a * p0.x + c * p0.y), y - (b * p0.x + d * p0.y)];
1735
+ }
1736
+ function magnificationAt(h, p) {
1737
+ const m = affineAt(h, p);
1738
+ if (!m) return void 0;
1739
+ const [a, b, c, d] = m;
1740
+ const s = a * a + b * b + c * c + d * d;
1741
+ const det = a * d - b * c;
1742
+ const disc = Math.max(0, s * s - 4 * det * det);
1743
+ return Math.sqrt((s + Math.sqrt(disc)) / 2);
1744
+ }
1745
+ var lerpHom = (a, b, t) => ({
1746
+ u: a.u + (b.u - a.u) * t,
1747
+ v: a.v + (b.v - a.v) * t,
1748
+ w: a.w + (b.w - a.w) * t
1749
+ });
1750
+ function clipPolygonNear(pts) {
1751
+ if (pts.length === 0) return [];
1752
+ const out = [];
1753
+ let prev = pts[pts.length - 1];
1754
+ let prevIn = prev.w >= NEAR_W;
1755
+ for (const cur of pts) {
1756
+ const curIn = cur.w >= NEAR_W;
1757
+ if (curIn !== prevIn) out.push(lerpHom(prev, cur, (NEAR_W - prev.w) / (cur.w - prev.w)));
1758
+ if (curIn) out.push(cur);
1759
+ prev = cur;
1760
+ prevIn = curIn;
1761
+ }
1762
+ return out;
1763
+ }
1764
+ function clipPolylineNear(pts) {
1765
+ const runs = [];
1766
+ let run = [];
1767
+ for (let i = 0; i < pts.length; i++) {
1768
+ const cur = pts[i];
1769
+ const curIn = cur.w >= NEAR_W;
1770
+ if (i > 0) {
1771
+ const prev = pts[i - 1];
1772
+ const prevIn = prev.w >= NEAR_W;
1773
+ if (curIn !== prevIn) {
1774
+ const cut3 = lerpHom(prev, cur, (NEAR_W - prev.w) / (cur.w - prev.w));
1775
+ if (prevIn) {
1776
+ run.push(cut3);
1777
+ runs.push(run);
1778
+ run = [];
1779
+ } else run.push(cut3);
1780
+ }
1781
+ }
1782
+ if (curIn) run.push(cur);
1783
+ }
1784
+ if (run.length > 0) runs.push(run);
1785
+ return runs.filter((r) => r.length >= 2);
1786
+ }
1787
+ function projectPolyline(h, points, closed) {
1788
+ const hom = points.map((p) => homTransform(h, p));
1789
+ const divide = (pts) => pts.map((p) => ({ x: p.u / p.w, y: p.v / p.w }));
1790
+ if (closed) {
1791
+ const ring = clipPolygonNear(hom);
1792
+ return ring.length >= 3 ? [divide(ring)] : [];
1793
+ }
1794
+ return clipPolylineNear(hom).map(divide);
1795
+ }
1796
+ function projectedQuadBounds(h, rect) {
1797
+ const corners = [
1798
+ { x: rect.x, y: rect.y },
1799
+ { x: rect.x + rect.w, y: rect.y },
1800
+ { x: rect.x + rect.w, y: rect.y + rect.h },
1801
+ { x: rect.x, y: rect.y + rect.h }
1802
+ ];
1803
+ const ring = projectPolyline(h, corners, true)[0];
1804
+ return ring ? boundsOfPoints(ring) : void 0;
1805
+ }
1806
+ function planeDepth(model, p) {
1807
+ return model[8] * p.x + model[9] * p.y + model[11];
1808
+ }
1809
+ function planeFaces(model, p, perspective) {
1810
+ const nx = model[2];
1811
+ const ny = model[6];
1812
+ const nz = model[10];
1813
+ if (perspective <= 0) return nz > 0;
1814
+ const x = model[0] * p.x + model[1] * p.y + model[3];
1815
+ const y = model[4] * p.x + model[5] * p.y + model[7];
1816
+ const z3 = planeDepth(model, p);
1817
+ return nx * x + ny * y + nz * (z3 + perspective) > 0;
1818
+ }
1598
1819
  var GRADIENT_KINDS = /* @__PURE__ */ new Set(["linear", "radial", "conic"]);
1599
1820
  function isGradient(p) {
1600
1821
  return typeof p === "object" && p !== null && GRADIENT_KINDS.has(p.kind) && Array.isArray(p.stops);
@@ -1650,9 +1871,9 @@ function paintBox(g, shape) {
1650
1871
  function gradientGeometry(g, box) {
1651
1872
  if (!box || g.kind === "conic") return void 0;
1652
1873
  if (g.kind === "linear") {
1653
- const rad = g.angle * Math.PI / 180;
1654
- const dx = Math.cos(rad);
1655
- const dy = Math.sin(rad);
1874
+ const rad2 = g.angle * Math.PI / 180;
1875
+ const dx = Math.cos(rad2);
1876
+ const dy = Math.sin(rad2);
1656
1877
  const half = (Math.abs(box.w * dx) + Math.abs(box.h * dy)) / 2;
1657
1878
  if (!(half > 1e-9) || g.end === g.start) return void 0;
1658
1879
  const cx2 = box.x + box.w / 2;
@@ -2288,6 +2509,7 @@ function makeCookContext(input) {
2288
2509
  rasterize: input.rasterize,
2289
2510
  video: input.video,
2290
2511
  outline: input.outline,
2512
+ provided: input.provided,
2291
2513
  assets: input.assets ?? {},
2292
2514
  asset: input.asset,
2293
2515
  paramDefs: defs,
@@ -2474,12 +2696,12 @@ var CycleError = class extends Error {
2474
2696
  }
2475
2697
  cycle;
2476
2698
  };
2477
- function nodeMap(graph) {
2478
- return new Map(graph.nodes.map((n) => [n.id, n]));
2699
+ function nodeMap(graph2) {
2700
+ return new Map(graph2.nodes.map((n) => [n.id, n]));
2479
2701
  }
2480
- function incomingEdges(graph) {
2481
- const map = new Map(graph.nodes.map((n) => [n.id, []]));
2482
- for (const e of graph.edges) map.get(e.to)?.push(e);
2702
+ function incomingEdges(graph2) {
2703
+ const map = new Map(graph2.nodes.map((n) => [n.id, []]));
2704
+ for (const e of graph2.edges) map.get(e.to)?.push(e);
2483
2705
  return map;
2484
2706
  }
2485
2707
  function wireParam(toPort) {
@@ -2487,10 +2709,10 @@ function wireParam(toPort) {
2487
2709
  }
2488
2710
  var nodeVertex = (id) => `n|${id}`;
2489
2711
  var tapVertex = (id, key) => `t|${id}|${key}`;
2490
- function cookSteps(graph) {
2491
- const byId = nodeMap(graph);
2712
+ function cookSteps(graph2) {
2713
+ const byId = nodeMap(graph2);
2492
2714
  const tapped = /* @__PURE__ */ new Map();
2493
- for (const e of graph.edges) {
2715
+ for (const e of graph2.edges) {
2494
2716
  if (!byId.has(e.from) || !e.fromPort.startsWith("param:")) continue;
2495
2717
  const key = e.fromPort.slice(6);
2496
2718
  const list = tapped.get(e.from);
@@ -2499,7 +2721,7 @@ function cookSteps(graph) {
2499
2721
  }
2500
2722
  const vertices = [];
2501
2723
  const steps = /* @__PURE__ */ new Map();
2502
- for (const node of graph.nodes) {
2724
+ for (const node of graph2.nodes) {
2503
2725
  for (const key of tapped.get(node.id) ?? []) {
2504
2726
  const v2 = tapVertex(node.id, key);
2505
2727
  vertices.push(v2);
@@ -2515,10 +2737,10 @@ function cookSteps(graph) {
2515
2737
  outs.get(from).push(to);
2516
2738
  indeg.set(to, indeg.get(to) + 1);
2517
2739
  };
2518
- for (const node of graph.nodes) {
2740
+ for (const node of graph2.nodes) {
2519
2741
  for (const key of tapped.get(node.id) ?? []) link(tapVertex(node.id, key), nodeVertex(node.id));
2520
2742
  }
2521
- for (const e of graph.edges) {
2743
+ for (const e of graph2.edges) {
2522
2744
  if (!byId.has(e.from) || !byId.has(e.to)) continue;
2523
2745
  const src = e.fromPort.startsWith("param:") ? tapVertex(e.from, e.fromPort.slice(6)) : nodeVertex(e.from);
2524
2746
  link(src, nodeVertex(e.to));
@@ -2549,9 +2771,9 @@ function cookSteps(graph) {
2549
2771
  }
2550
2772
  return order;
2551
2773
  }
2552
- function topoSort(graph) {
2774
+ function topoSort(graph2) {
2553
2775
  const order = [];
2554
- for (const step of cookSteps(graph)) if (step.kind === "cook") order.push(step.node);
2776
+ for (const step of cookSteps(graph2)) if (step.kind === "cook") order.push(step.node);
2555
2777
  return order;
2556
2778
  }
2557
2779
  function selfWireOrder(edges) {
@@ -2585,25 +2807,25 @@ function selfWireOrder(edges) {
2585
2807
  if (rank.size !== indeg.size) return null;
2586
2808
  return [...edges].sort((a, b) => rank.get(a.fromPort.slice(6)) - rank.get(b.fromPort.slice(6)));
2587
2809
  }
2588
- function wouldCycle(graph, from, to, fromPort, toPort) {
2810
+ function wouldCycle(graph2, from, to, fromPort, toPort) {
2589
2811
  const probe = { id: "", from, fromPort: fromPort ?? "", to, toPort: toPort ?? "" };
2590
2812
  try {
2591
- cookSteps({ ...graph, edges: [...graph.edges, probe] });
2813
+ cookSteps({ ...graph2, edges: [...graph2.edges, probe] });
2592
2814
  return false;
2593
2815
  } catch (err) {
2594
2816
  if (err instanceof CycleError) return true;
2595
2817
  throw err;
2596
2818
  }
2597
2819
  }
2598
- function timeDependentNodes(graph, registry) {
2820
+ function timeDependentNodes(graph2, registry) {
2599
2821
  const dependent = /* @__PURE__ */ new Set();
2600
- const byId = nodeMap(graph);
2822
+ const byId = nodeMap(graph2);
2601
2823
  const enumWired = /* @__PURE__ */ new Set();
2602
- for (const e of graph.edges) {
2824
+ for (const e of graph2.edges) {
2603
2825
  const def = registry.forNode(byId.get(e.to));
2604
2826
  if (def?.params.some((p) => p.key === e.toPort && p.type === "enum")) enumWired.add(e.to);
2605
2827
  }
2606
- for (const node of graph.nodes) {
2828
+ for (const node of graph2.nodes) {
2607
2829
  const def = registry.forNode(node);
2608
2830
  const rooted = typeof def?.timeRoot === "function" ? enumWired.has(node.id) || def.timeRoot(node.params) : def?.timeRoot;
2609
2831
  if (rooted) {
@@ -2618,7 +2840,7 @@ function timeDependentNodes(graph, registry) {
2618
2840
  }
2619
2841
  }
2620
2842
  const outs = /* @__PURE__ */ new Map();
2621
- for (const e of graph.edges) {
2843
+ for (const e of graph2.edges) {
2622
2844
  const list = outs.get(e.from);
2623
2845
  if (list) list.push(e.to);
2624
2846
  else outs.set(e.from, [e.to]);
@@ -2635,16 +2857,16 @@ function timeDependentNodes(graph, registry) {
2635
2857
  }
2636
2858
  return dependent;
2637
2859
  }
2638
- function validateGraph(graph, registry, clips = []) {
2860
+ function validateGraph(graph2, registry, clips = []) {
2639
2861
  const issues = [];
2640
- const byId = nodeMap(graph);
2862
+ const byId = nodeMap(graph2);
2641
2863
  const clipped = new Set(clips.map((c) => c.layer));
2642
- for (const node of graph.nodes) {
2864
+ for (const node of graph2.nodes) {
2643
2865
  if (!registry.has(node.type)) {
2644
2866
  issues.push({ level: "error", nodeId: node.id, message: `Unknown node type "${node.type}"` });
2645
2867
  }
2646
2868
  }
2647
- for (const e of graph.edges) {
2869
+ for (const e of graph2.edges) {
2648
2870
  const from = byId.get(e.from);
2649
2871
  const to = byId.get(e.to);
2650
2872
  if (!from || !to) {
@@ -2669,7 +2891,7 @@ function validateGraph(graph, registry, clips = []) {
2669
2891
  }
2670
2892
  }
2671
2893
  }
2672
- for (const e of graph.edges) {
2894
+ for (const e of graph2.edges) {
2673
2895
  const from = byId.get(e.from);
2674
2896
  const to = byId.get(e.to);
2675
2897
  if (!from || !to) continue;
@@ -2696,7 +2918,7 @@ function validateGraph(graph, registry, clips = []) {
2696
2918
  }
2697
2919
  }
2698
2920
  const seen = /* @__PURE__ */ new Map();
2699
- for (const e of graph.edges) {
2921
+ for (const e of graph2.edges) {
2700
2922
  const toDef = registry.forNode(byId.get(e.to));
2701
2923
  const port = toDef?.inputs.find((i) => i.name === e.toPort);
2702
2924
  if (!port || port.multi) continue;
@@ -2711,8 +2933,8 @@ function validateGraph(graph, registry, clips = []) {
2711
2933
  }
2712
2934
  }
2713
2935
  let selfLoop = false;
2714
- for (const node of graph.nodes) {
2715
- const self = graph.edges.filter((e) => e.from === node.id && e.to === node.id);
2936
+ for (const node of graph2.nodes) {
2937
+ const self = graph2.edges.filter((e) => e.from === node.id && e.to === node.id);
2716
2938
  if (self.length > 0 && selfWireOrder(self) === null) {
2717
2939
  selfLoop = true;
2718
2940
  issues.push({
@@ -2724,7 +2946,7 @@ function validateGraph(graph, registry, clips = []) {
2724
2946
  }
2725
2947
  if (!selfLoop) {
2726
2948
  try {
2727
- topoSort(graph);
2949
+ topoSort(graph2);
2728
2950
  } catch (err) {
2729
2951
  if (err instanceof CycleError) {
2730
2952
  issues.push({ level: "error", message: err.message });
@@ -2744,7 +2966,7 @@ function stable(value) {
2744
2966
  }
2745
2967
  var identityKeys = /* @__PURE__ */ new WeakMap();
2746
2968
  var nextIdentityKey = 1;
2747
- function identityKey(obj) {
2969
+ function identityOf(obj) {
2748
2970
  let key = identityKeys.get(obj);
2749
2971
  if (key === void 0) {
2750
2972
  key = nextIdentityKey++;
@@ -2752,6 +2974,7 @@ function identityKey(obj) {
2752
2974
  }
2753
2975
  return key;
2754
2976
  }
2977
+ var identityKey = identityOf;
2755
2978
  var textureKey = (tex) => identityKey(tex);
2756
2979
  function hashOf(...parts) {
2757
2980
  const [a, b, c, d] = cyrb128(parts.join(" "));
@@ -2779,7 +3002,10 @@ function evaluate(project, registry, opts = {}) {
2779
3002
  const timeDeps = timeDependentNodes(project.graph, registry);
2780
3003
  const incoming = incomingEdges(project.graph);
2781
3004
  const hashes = /* @__PURE__ */ new Map();
2782
- const frameKey = `${project.canvas.w}x${project.canvas.h}@${project.canvas.origin}`;
3005
+ const canvasNode2 = project.graph.nodes.find((n) => n.type === "canvas");
3006
+ const lens = canvasNode2 ? normalizeParam(canvasNode2.params.perspective).base : void 0;
3007
+ const perspective = typeof lens === "number" ? Math.max(0, lens) : canvasNode2 ? 1500 : void 0;
3008
+ const frameKey = `${project.canvas.w}x${project.canvas.h}@${project.canvas.origin}${perspective === void 0 ? "" : `/${perspective}`}`;
2783
3009
  const clocks = envClocks(project, t, opts.audition);
2784
3010
  const taps = /* @__PURE__ */ new Map();
2785
3011
  const wiresOf = /* @__PURE__ */ new Map();
@@ -2918,6 +3144,10 @@ function evaluate(project, registry, opts = {}) {
2918
3144
  );
2919
3145
  continue;
2920
3146
  }
3147
+ const providedKey = opts.textures && def.standIns ? def.standIns(inputs, multiInputs).map((key) => {
3148
+ const tex = opts.textures(key);
3149
+ return `${key}=${tex ? String(textureKey(tex)) : "none"}`;
3150
+ }).join(",") : "";
2921
3151
  const nodeHash = hashOf(
2922
3152
  node.type,
2923
3153
  stable(node.params),
@@ -2938,7 +3168,10 @@ function evaluate(project, registry, opts = {}) {
2938
3168
  // it — one global salt, one full re-cook, then business as usual.
2939
3169
  opts.outliner ? "o1" : "o0",
2940
3170
  // A claiming output wired or not reshapes the others (Contours' major).
2941
- [...wiredOutputs].sort().join(",")
3171
+ [...wiredOutputs].sort().join(","),
3172
+ // Pixels this cook borrows from the worker, named by identity like a
3173
+ // stood-in texture node's: a delivery must miss, a repeat must hit.
3174
+ providedKey
2942
3175
  );
2943
3176
  hashes.set(node.id, nodeHash);
2944
3177
  const hit = upstreamTainted ? void 0 : cache22?.get(node.id, nodeHash);
@@ -2952,9 +3185,10 @@ function evaluate(project, registry, opts = {}) {
2952
3185
  name: node.name,
2953
3186
  seed,
2954
3187
  t: tNode,
2955
- canvas: { w: project.canvas.w, h: project.canvas.h, origin: project.canvas.origin },
3188
+ canvas: { w: project.canvas.w, h: project.canvas.h, origin: project.canvas.origin, ...perspective === void 0 ? {} : { perspective } },
2956
3189
  outline: opts.outliner,
2957
3190
  rasterize: opts.rasterize,
3191
+ provided: opts.textures,
2958
3192
  video: opts.video,
2959
3193
  assets: project.assets,
2960
3194
  asset: opts.asset,
@@ -3883,9 +4117,9 @@ function colorCorrectIsIdentity(o) {
3883
4117
  return o.brightness === 0 && o.contrast === 0 && o.saturation === 0 && o.hue % 360 === 0;
3884
4118
  }
3885
4119
  function hueMatrix(degrees) {
3886
- const rad = degrees * Math.PI / 180;
3887
- const cos = Math.cos(rad);
3888
- const sin = Math.sin(rad);
4120
+ const rad2 = degrees * Math.PI / 180;
4121
+ const cos = Math.cos(rad2);
4122
+ const sin = Math.sin(rad2);
3889
4123
  return new Float64Array([
3890
4124
  0.213 + cos * 0.787 - sin * 0.213,
3891
4125
  0.715 - cos * 0.715 - sin * 0.715,
@@ -4680,6 +4914,47 @@ void main() {
4680
4914
  vec4 top = B(ivec2(x0, y0)) * (1.0 - fx) + B(ivec2(x1, y0)) * fx;
4681
4915
  vec4 bottom = B(ivec2(x0, y1)) * (1.0 - fx) + B(ivec2(x1, y1)) * fx;
4682
4916
  o = vec4(rnd3((top * (1.0 - fy) + bottom * fy).rgb), rnd((top * (1.0 - fy) + bottom * fy).a)) / 255.0;
4917
+ }`,
4918
+ // warp.ts: each output centre back through the inverse homography onto
4919
+ // the bitmap, four taps mixed; taps off the bitmap are transparent black.
4920
+ warp: `${HEAD}
4921
+ uniform float u_m[9]; uniform vec3 u_place; uniform ivec2 u_out; uniform int u_w; uniform int u_h;
4922
+ vec4 T(int x, int y) { return (x < 0 || y < 0 || x >= u_w || y >= u_h) ? vec4(0.0) : B(ivec2(x, y)); }
4923
+ void main() {
4924
+ ivec2 p = P();
4925
+ float px = float(u_out.x + p.x) + 0.5;
4926
+ float py = float(u_out.y + p.y) + 0.5;
4927
+ float u = u_m[0] * px + u_m[1] * py + u_m[2];
4928
+ float v = u_m[3] * px + u_m[4] * py + u_m[5];
4929
+ float w = u_m[6] * px + u_m[7] * py + u_m[8];
4930
+ if (w <= 1e-9) { o = vec4(0.0); return; }
4931
+ float sx = (u / w - u_place.x) * u_place.z - 0.5;
4932
+ float sy = (v / w - u_place.y) * u_place.z - 0.5;
4933
+ if (sx < -1.0 || sy < -1.0 || sx > float(u_w) || sy > float(u_h)) { o = vec4(0.0); return; }
4934
+ int x0 = int(floor(sx)); int y0 = int(floor(sy));
4935
+ float fx = sx - float(x0); float fy = sy - float(y0);
4936
+ vec4 top = T(x0, y0) * (1.0 - fx) + T(x0 + 1, y0) * fx;
4937
+ vec4 bottom = T(x0, y0 + 1) * (1.0 - fx) + T(x0 + 1, y0 + 1) * fx;
4938
+ vec4 r = top * (1.0 - fy) + bottom * fy;
4939
+ o = vec4(rnd3(r.rgb), rnd(r.a)) / 255.0;
4940
+ }`,
4941
+ // dof.ts's focusSelect: the map's bytes unpremultiplied in whole numbers, the pixel kept or dropped.
4942
+ focusSelect: `${HEAD}
4943
+ uniform sampler2D u_map; uniform ivec2 u_off; uniform ivec2 u_msize; uniform int u_side; uniform ivec2 u_range;
4944
+ void main() {
4945
+ ivec2 p = P();
4946
+ ivec2 mp = p - u_off;
4947
+ bool mIn = mp.x >= 0 && mp.y >= 0 && mp.x < u_msize.x && mp.y < u_msize.y;
4948
+ ivec4 m = mIn ? ivec4(floor(texelFetch(u_map, mp, 0) * 255.0 + 0.5)) : ivec4(0);
4949
+ int level = 0;
4950
+ int side = 0;
4951
+ if (m.a > 0) {
4952
+ int h = m.a / 2;
4953
+ level = (m.r * 255 + h) / m.a;
4954
+ side = (m.g * 255 + h) / m.a >= 128 ? 1 : 0;
4955
+ }
4956
+ bool keep = side == u_side && level >= u_range.x && level <= u_range.y;
4957
+ o = keep ? S(p) : vec4(0.0);
4683
4958
  }`,
4684
4959
  matte: `${HEAD}
4685
4960
  uniform sampler2D u_matte; uniform ivec2 u_off; uniform ivec2 u_msize; uniform int u_luma; uniform int u_invert;
@@ -4869,6 +5144,125 @@ function resample(tex, width, height) {
4869
5144
  if (tex.at) result.at = tex.at;
4870
5145
  return result;
4871
5146
  }
5147
+ function warpTexture(src, place, hInv, out) {
5148
+ const W = Math.max(1, Math.round(out.w));
5149
+ const H = Math.max(1, Math.round(out.h));
5150
+ const g = gpuKernels();
5151
+ if (g) return g.warp(src, place, hInv, { x: out.x, y: out.y, w: W, h: H });
5152
+ const { width: w, height: h, data } = src;
5153
+ const pixels = new Uint8ClampedArray(W * H * 4);
5154
+ warpLoop(data, w, h, pixels, W, H, place, hInv, out.x, out.y);
5155
+ return { width: W, height: H, data: pixels, at: { x: out.x, y: out.y } };
5156
+ }
5157
+ function warpLoop(data, w, h, pixels, W, H, place, m, ox, oy) {
5158
+ const [m0, m1, m2, m3, m4, m5, m6, m7, m8] = m;
5159
+ const { x: px0, y: py0, scale: scale2 } = place;
5160
+ const tap = (x, y, k) => x < 0 || y < 0 || x >= w || y >= h ? 0 : data[(y * w + x) * 4 + k];
5161
+ for (let j = 0; j < H; j++) {
5162
+ const py = oy + j + 0.5;
5163
+ for (let i = 0; i < W; i++) {
5164
+ const px = ox + i + 0.5;
5165
+ const u = m0 * px + m1 * py + m2;
5166
+ const v = m3 * px + m4 * py + m5;
5167
+ const wq = m6 * px + m7 * py + m8;
5168
+ if (wq <= 1e-9) continue;
5169
+ const sx = (u / wq - px0) * scale2 - 0.5;
5170
+ const sy = (v / wq - py0) * scale2 - 0.5;
5171
+ if (sx < -1 || sy < -1 || sx > w || sy > h) continue;
5172
+ const x0 = Math.floor(sx);
5173
+ const y0 = Math.floor(sy);
5174
+ const fx = sx - x0;
5175
+ const fy = sy - y0;
5176
+ const o = (j * W + i) * 4;
5177
+ for (let k = 0; k < 4; k++) {
5178
+ const top = tap(x0, y0, k) * (1 - fx) + tap(x0 + 1, y0, k) * fx;
5179
+ const bottom = tap(x0, y0 + 1, k) * (1 - fx) + tap(x0 + 1, y0 + 1, k) * fx;
5180
+ pixels[o + k] = Math.round(top * (1 - fy) + bottom * fy);
5181
+ }
5182
+ }
5183
+ }
5184
+ }
5185
+ var FOCUS_LEVEL_PX = 0.25;
5186
+ function focusLevel(radius) {
5187
+ return Math.max(0, Math.min(255, Math.round(radius / FOCUS_LEVEL_PX)));
5188
+ }
5189
+ function ordered(near, far) {
5190
+ const ranges = (levels, side) => {
5191
+ const sorted = [...new Set(levels)].sort((a, b) => a - b);
5192
+ return sorted.map((level, i) => ({
5193
+ side,
5194
+ lo: i === 0 ? 0 : Math.floor((sorted[i - 1] + level) / 2) + 1,
5195
+ hi: i === sorted.length - 1 ? 255 : Math.floor((level + sorted[i + 1]) / 2),
5196
+ radius: level * FOCUS_LEVEL_PX
5197
+ }));
5198
+ };
5199
+ return [...ranges(far, 1).reverse(), ...ranges(near, 0)];
5200
+ }
5201
+ function focusSlices(levels, max = 24) {
5202
+ let q = 1;
5203
+ for (; ; ) {
5204
+ const snap2 = (l) => Math.min(255, Math.round(l / q) * q);
5205
+ const near = [0];
5206
+ const far = [];
5207
+ for (const { level, side } of levels) {
5208
+ const l = snap2(Math.max(0, Math.min(255, Math.round(level))));
5209
+ if (side === 1 && l > 0) far.push(l);
5210
+ else near.push(l);
5211
+ }
5212
+ const slices = ordered(near, far);
5213
+ if (slices.length <= max || q >= 256) return slices;
5214
+ q *= 2;
5215
+ }
5216
+ }
5217
+ function focusSelect(tex, map, ox, oy, side, lo, hi) {
5218
+ const g = gpuKernels();
5219
+ if (g) return g.focusSelect(tex, map, ox, oy, side, lo, hi);
5220
+ const { width, height, data } = tex;
5221
+ const m = map.data;
5222
+ const out = like2(tex);
5223
+ const o = out.data;
5224
+ for (let y = 0; y < height; y++) {
5225
+ const my = y - oy;
5226
+ const rowIn = my >= 0 && my < map.height;
5227
+ for (let x = 0; x < width; x++) {
5228
+ const mx = x - ox;
5229
+ let level = 0;
5230
+ let on = 0;
5231
+ if (rowIn && mx >= 0 && mx < map.width) {
5232
+ const j = (my * map.width + mx) * 4;
5233
+ const a = m[j + 3];
5234
+ if (a > 0) {
5235
+ const half = a >> 1;
5236
+ level = Math.floor((m[j] * 255 + half) / a);
5237
+ on = Math.floor((m[j + 1] * 255 + half) / a) >= 128 ? 1 : 0;
5238
+ }
5239
+ }
5240
+ if (on !== side || level < lo || level > hi) continue;
5241
+ const i = (y * width + x) * 4;
5242
+ o[i] = data[i];
5243
+ o[i + 1] = data[i + 1];
5244
+ o[i + 2] = data[i + 2];
5245
+ o[i + 3] = data[i + 3];
5246
+ }
5247
+ }
5248
+ return out;
5249
+ }
5250
+ function depthOfField(tex, map, slices, origin = "top-left") {
5251
+ if (slices.length === 0) return copyTexture(tex);
5252
+ const pt2 = placementOf(tex, origin);
5253
+ const pm = placementOf(map, origin);
5254
+ const ox = pm.x - pt2.x;
5255
+ const oy = pm.y - pt2.y;
5256
+ const g = gpuKernels();
5257
+ if (g) return g.depthOfField(tex, map, ox, oy, slices.map((s) => s.radius > 0 ? { ...s, radii: boxRadii(s.radius) } : s));
5258
+ let acc;
5259
+ for (const s of slices) {
5260
+ const cut3 = focusSelect(tex, map, ox, oy, s.side, s.lo, s.hi);
5261
+ const soft = s.radius > 0 ? blur(cut3, s.radius) : cut3;
5262
+ acc = acc ? blendTextures(acc, soft, "normal", 1, origin) : soft;
5263
+ }
5264
+ return acc;
5265
+ }
4872
5266
  var divRound = (num3, den) => Math.floor((2 * num3 + den) / (2 * den));
4873
5267
  function sampleCells(tex, grid) {
4874
5268
  const asked = grid.sub ?? 1;
@@ -5767,6 +6161,7 @@ var lineNode = {
5767
6161
  type: "line",
5768
6162
  label: "Line",
5769
6163
  category: "generator",
6164
+ description: "A straight stroke from (X1, Y1) to (X2, Y2).",
5770
6165
  // Both endpoints, so dragging moves the whole line.
5771
6166
  translate: [{ x: "x1", y: "y1" }, { x: "x2", y: "y2" }],
5772
6167
  inputs: [],
@@ -6235,8 +6630,8 @@ var videoNode = {
6235
6630
  `the bytes of \u201C${record.name}\u201D are not on this machine \u2014 drop the file on the graph to restore them (the CLI reads them from the project\u2019s .assets folder)`
6236
6631
  );
6237
6632
  }
6238
- const placed = frame.at && frame.at.x === at2.x && frame.at.y === at2.y ? frame : { ...frame, at: at2 };
6239
- return { out: textureVal(placed) };
6633
+ const placed2 = frame.at && frame.at.x === at2.x && frame.at.y === at2.y ? frame : { ...frame, at: at2 };
6634
+ return { out: textureVal(placed2) };
6240
6635
  }
6241
6636
  };
6242
6637
  var OPS2 = ["union", "subtract", "intersect", "exclude"];
@@ -8052,9 +8447,9 @@ function textShape(ctx, i, count, opts) {
8052
8447
  const letterSpacing = grid ? Math.round(opts.letterSpacing) : opts.letterSpacing;
8053
8448
  const onGrid = grid !== void 0 && rotation === 0;
8054
8449
  const style = glyphStyle(ctx, i, count);
8055
- const rad = rotation * Math.PI / 180;
8056
- const cos = Math.cos(rad);
8057
- const sin = Math.sin(rad);
8450
+ const rad2 = rotation * Math.PI / 180;
8451
+ const cos = Math.cos(rad2);
8452
+ const sin = Math.sin(rad2);
8058
8453
  const measured = ctx.outline?.measure?.({ text, family, weight, stretch, size: fontSize, letterSpacing });
8059
8454
  const capHeight = measured?.capHeight ?? fontSize * FALLBACK_CAP_HEIGHT;
8060
8455
  const vAnchor = ctx.enumOf("vAnchor", ["baseline", "middle", "top"]);
@@ -8526,13 +8921,13 @@ var textOnPathNode = {
8526
8921
  const gaps = closed ? runs.length : runs.length - 1;
8527
8922
  between = gaps > 0 ? (length - words) / gaps : 0;
8528
8923
  }
8529
- const placed = [];
8924
+ const placed2 = [];
8530
8925
  let pen = 0;
8531
8926
  for (const [r, { shape, run, outlined }] of runs.entries()) {
8532
8927
  if (r > 0) pen += between;
8533
8928
  const chars = [...shape.text];
8534
8929
  for (const [g, glyph] of run.glyphs.entries()) {
8535
- placed.push({ shape, d: glyph.d, ch: chars[g] ?? "", x: pen + glyph.x + g * spacing, advance: glyph.advance, outlined });
8930
+ placed2.push({ shape, d: glyph.d, ch: chars[g] ?? "", x: pen + glyph.x + g * spacing, advance: glyph.advance, outlined });
8536
8931
  }
8537
8932
  pen += runWidth(run);
8538
8933
  }
@@ -8542,12 +8937,12 @@ var textOnPathNode = {
8542
8937
  if (align === "middle") start = position - total / 2;
8543
8938
  else if (align === "end") start = position - total;
8544
8939
  else if (align === "justify") {
8545
- const gaps = closed ? placed.length : placed.length - 1;
8940
+ const gaps = closed ? placed2.length : placed2.length - 1;
8546
8941
  if (gaps > 0) gap = (length - total) / gaps;
8547
8942
  }
8548
8943
  const out = [];
8549
8944
  let index = 0;
8550
- for (const [k, g] of placed.entries()) {
8945
+ for (const [k, g] of placed2.entries()) {
8551
8946
  const mid = g.x + k * gap + g.advance / 2;
8552
8947
  const { p, t } = along(points, closed, length, start + mid);
8553
8948
  const angle = Math.atan2(t.y, t.x) * 180 / Math.PI;
@@ -9025,6 +9420,16 @@ var transformNode = {
9025
9420
  description: "With a Ref wired: the point on its bounds the pivot lands on. Move X/Y are measured from it; Rotate and Scale turn about it.",
9026
9421
  dimWhen: refUnwired
9027
9422
  },
9423
+ {
9424
+ key: "refMix",
9425
+ label: "Mix",
9426
+ type: "number",
9427
+ default: 1,
9428
+ step: 0.01,
9429
+ group: "Ref",
9430
+ description: "How far along the way to the Ref point: 0 leaves the shape where it was authored, 1 lands it on the point. Wire a Seq or Mapping in to glide it there; past 0\u20131 it overshoots.",
9431
+ dimWhen: refUnwired
9432
+ },
9028
9433
  {
9029
9434
  key: "refEach",
9030
9435
  label: "Per Shape",
@@ -9057,10 +9462,11 @@ var transformNode = {
9057
9462
  const b = each ? shapeBounds(ref[i % ref.length]) : whole;
9058
9463
  if (!b) return shape;
9059
9464
  const { fx, fy } = REF_POINTS[point];
9060
- const R = { x: b.x + fx * b.w, y: b.y + fy * b.h };
9465
+ const P = pivotFor(ctx, pivotMode, shape, i, n);
9466
+ const mix = ctx.num("refMix", i, n);
9467
+ const R = { x: P.x + mix * (b.x + fx * b.w - P.x), y: P.y + mix * (b.y + fy * b.h - P.y) };
9061
9468
  const tx = ctx.num("tx", i, n);
9062
9469
  const ty = ctx.num("ty", i, n);
9063
- const P = pivotFor(ctx, pivotMode, shape, i, n);
9064
9470
  const rot = ctx.num("rotation", i, n);
9065
9471
  const sx = ctx.num("sx", i, n);
9066
9472
  const sy = ctx.num("sy", i, n);
@@ -9138,13 +9544,13 @@ var arrayLinearNode = {
9138
9544
  const tx = ox + ctx.num("stepX", k, count, vars) * k;
9139
9545
  const ty = oy + ctx.num("stepY", k, count, vars) * k;
9140
9546
  const rotation = ctx.num("rotationStep", k, count, vars) * k;
9141
- const scale4 = ctx.num("scaleStep", k, count, vars) ** k;
9547
+ const scale5 = ctx.num("scaleStep", k, count, vars) ** k;
9142
9548
  const shapes = carry(k);
9143
9549
  for (const [j, shape] of shapes.entries()) {
9144
9550
  const index = out.length + j;
9145
9551
  const base2 = deal ? pivotFor(ctx, pivotMode, shape, j, shapes.length) : pivots[j];
9146
9552
  const pivot = pivotInMoveSpace(pivotMode, base2, tx, ty);
9147
- const grown = scale4 !== 1 ? scaleAbout(shape, scale4, scale4, pivot) : shape;
9553
+ const grown = scale5 !== 1 ? scaleAbout(shape, scale5, scale5, pivot) : shape;
9148
9554
  const m = compose({ tx, ty, rotation, pivot });
9149
9555
  out.push({
9150
9556
  ...grown,
@@ -9158,6 +9564,442 @@ var arrayLinearNode = {
9158
9564
  }
9159
9565
  };
9160
9566
  var MODIFIERS = [transformNode, arrayLinearNode];
9567
+ var isBitmap = (s) => isBitmapFamily(s.fontFamily);
9568
+ var isVariable = (s) => resolveFace(s.fontFamily, s.fontWeight ?? 400, s.fontStretch).variation !== void 0;
9569
+ var needsOutlining = (s) => isBitmap(s) || isVariable(s);
9570
+ var every = () => true;
9571
+ function anyText(scene, pick) {
9572
+ const any = (shapes) => shapes.some((s) => s.kind === "text" ? pick(s) : s.kind === "group" && any(s.children));
9573
+ return scene.layers.some((l) => any(l.shapes));
9574
+ }
9575
+ function outlineWhere(shape, outliner, pick) {
9576
+ if (shape.kind === "group") {
9577
+ const children = shape.children.map((c2) => outlineWhere(c2, outliner, pick));
9578
+ return children.some((c2, i) => c2 !== shape.children[i]) ? { ...shape, children } : shape;
9579
+ }
9580
+ if (shape.kind !== "text" || !pick(shape)) return shape;
9581
+ const weight = shape.fontWeight ?? 400;
9582
+ const g = outliner({ text: shape.text, family: shape.fontFamily, weight, stretch: shape.fontStretch, size: shape.fontSize, letterSpacing: shape.letterSpacing });
9583
+ if (!g) return shape;
9584
+ const shift = shape.anchor === "middle" ? -g.width / 2 : shape.anchor === "end" ? -g.width : 0;
9585
+ const [a, b, c, d, e, f] = shape.transform;
9586
+ const tx = shape.x + shift;
9587
+ const ty = shape.y;
9588
+ const { x: _x, y: _y, text: _t, fontSize: _s, fontFamily: _f, fontWeight: _w, fontStretch: _st, anchor: _a, letterSpacing: _l, metrics: _m, ...rest } = shape;
9589
+ return {
9590
+ ...rest,
9591
+ kind: "path",
9592
+ d: g.d,
9593
+ transform: [a, b, c, d, a * tx + c * ty + e, b * tx + d * ty + f],
9594
+ meta: { ...shape.meta, tags: [...shape.meta.tags ?? [], "glyphs"] }
9595
+ };
9596
+ }
9597
+ function outlineTextShape(shape, outliner, all = false) {
9598
+ return outlineWhere(shape, outliner, all ? every : needsOutlining);
9599
+ }
9600
+ function outlineScene(scene, outliner, pick) {
9601
+ if (!outliner || !anyText(scene, pick)) return scene;
9602
+ return {
9603
+ ...scene,
9604
+ layers: scene.layers.map((l) => {
9605
+ const shapes = l.shapes.map((s) => outlineWhere(s, outliner, pick));
9606
+ return shapes.some((s, i) => s !== l.shapes[i]) ? { ...l, shapes } : l;
9607
+ })
9608
+ };
9609
+ }
9610
+ function outlineBitmapText(scene, outliner) {
9611
+ return outlineScene(scene, outliner, isBitmap);
9612
+ }
9613
+ var LAYER_3D_DEFAULTS = {
9614
+ posX: 0,
9615
+ posY: 0,
9616
+ posZ: 0,
9617
+ rotX: 0,
9618
+ rotY: 0,
9619
+ rotZ: 0,
9620
+ scaleX: 100,
9621
+ scaleY: 100
9622
+ };
9623
+ var PICTURE_NODE_TYPES = /* @__PURE__ */ new Set(["image", "picture", "gradient", "noise-paint"]);
9624
+ function layerIsPosed(node, edges) {
9625
+ if (node.type !== "layer") return false;
9626
+ for (const [key, rest] of Object.entries(LAYER_3D_DEFAULTS)) {
9627
+ const p = normalizeParam(node.params[key]);
9628
+ if (p.track || p.expr || p.wire) return true;
9629
+ if (p.base !== void 0 && p.base !== rest) return true;
9630
+ if (edges.some((e) => e.to === node.id && e.toPort === key)) return true;
9631
+ }
9632
+ return false;
9633
+ }
9634
+ function posedLayersWithPictures(project) {
9635
+ const { nodes, edges } = project.graph;
9636
+ return nodes.filter((n) => layerIsPosed(n, edges) && nodes.some((m) => m.layer === n.id && PICTURE_NODE_TYPES.has(m.type)));
9637
+ }
9638
+ function texturePlace(texture, origin) {
9639
+ return texture.at ?? (origin === "center" ? { x: -texture.width / 2, y: -texture.height / 2 } : { x: 0, y: 0 });
9640
+ }
9641
+ function frameRect(camera) {
9642
+ return camera.origin === "center" ? { x: -camera.width / 2, y: -camera.height / 2, w: camera.width, h: camera.height } : { x: 0, y: 0, w: camera.width, h: camera.height };
9643
+ }
9644
+ function layerContentBounds(layer, origin) {
9645
+ const shapes = boundsOfShapes(layer.shapes);
9646
+ if (!layer.texture) return shapes;
9647
+ const p = texturePlace(layer.texture, origin);
9648
+ const pic = { x: p.x, y: p.y, w: layer.texture.width, h: layer.texture.height };
9649
+ return shapes ? union(shapes, pic) : pic;
9650
+ }
9651
+ function layerPivot(mode, layer, camera, custom) {
9652
+ if (mode === "canvas") return { x: 0, y: 0 };
9653
+ if (mode === "custom") return custom;
9654
+ if (mode === "shape") {
9655
+ const b = layerContentBounds(layer, camera.origin);
9656
+ return b ? centerOf(b) : frameCentre(camera);
9657
+ }
9658
+ return frameCentre(camera);
9659
+ }
9660
+ function isPictureShape(shape) {
9661
+ if (shape.kind === "image") return true;
9662
+ if (shape.kind === "group") return shape.children.some(isPictureShape);
9663
+ return typeof shape.style.fill === "object" || typeof shape.style.stroke === "object";
9664
+ }
9665
+ function layerPieces(layer) {
9666
+ const pieces = [];
9667
+ const at2 = layer.texture ? layer.textureAt ?? 0 : layer.shapes.length;
9668
+ const idOf = () => pieces.length === 0 ? layer.id : `${layer.id}/${pieces.length}`;
9669
+ const last = () => pieces[pieces.length - 1];
9670
+ const pushShape = (shape) => {
9671
+ const kind = isPictureShape(shape) ? "picture" : "vector";
9672
+ const tail = last();
9673
+ if (tail && tail.kind === kind) tail.shapes.push(shape);
9674
+ else pieces.push({ kind, id: idOf(), shapes: [shape] });
9675
+ };
9676
+ for (let i = 0; i < layer.shapes.length; i++) {
9677
+ if (i === at2 && layer.texture) {
9678
+ const tail = last();
9679
+ if (tail && tail.kind === "picture") {
9680
+ tail.texture = layer.texture;
9681
+ tail.textureAt = tail.shapes.length;
9682
+ } else pieces.push({ kind: "picture", id: idOf(), shapes: [], texture: layer.texture });
9683
+ }
9684
+ pushShape(layer.shapes[i]);
9685
+ }
9686
+ if (layer.texture && at2 >= layer.shapes.length) {
9687
+ const tail = last();
9688
+ if (tail && tail.kind === "picture") {
9689
+ tail.texture = layer.texture;
9690
+ tail.textureAt = tail.shapes.length;
9691
+ } else pieces.push({ kind: "picture", id: idOf(), shapes: [], texture: layer.texture });
9692
+ }
9693
+ return pieces;
9694
+ }
9695
+ function placeOnTangent(shape, h) {
9696
+ const b = shapeBounds(shape);
9697
+ const a = affineAt(h, b ? centerOf(b) : { x: 0, y: 0 });
9698
+ return a ? { ...shape, transform: multiply(a, shape.transform) } : void 0;
9699
+ }
9700
+ function projectShape(shape, parent, h, outliner) {
9701
+ const world = multiply(parent, shape.transform);
9702
+ switch (shape.kind) {
9703
+ case "group": {
9704
+ const children = shape.children.map((c) => projectShape(c, world, h, outliner)).filter((c) => c !== void 0);
9705
+ return { ...shape, transform: IDENTITY, children };
9706
+ }
9707
+ case "text": {
9708
+ if (outliner) {
9709
+ const outlined = outlineTextShape(shape, outliner, true);
9710
+ if (outlined.kind === "path") return projectShape(outlined, parent, h, outliner);
9711
+ }
9712
+ const a = affineAt(h, apply(world, { x: shape.x, y: shape.y }));
9713
+ return a ? { ...shape, transform: multiply(a, world) } : void 0;
9714
+ }
9715
+ case "image": {
9716
+ const a = affineAt(h, apply(world, { x: shape.x + shape.w / 2, y: shape.y + shape.h / 2 }));
9717
+ return a ? { ...shape, transform: multiply(a, world) } : void 0;
9718
+ }
9719
+ default: {
9720
+ const runs = flatten({ ...shape, transform: world }).flatMap(
9721
+ (line) => projectPolyline(h, line.points, line.closed).map((points) => ({ points, closed: line.closed }))
9722
+ );
9723
+ if (runs.length === 0) return void 0;
9724
+ return { id: shape.id, kind: "path", d: polylinesToPathD(runs), transform: IDENTITY, style: shape.style, meta: shape.meta };
9725
+ }
9726
+ }
9727
+ }
9728
+ function projectShapes(shapes, h, outliner) {
9729
+ const rigid = rigidAffineOf(h);
9730
+ if (rigid) return shapes.map((s) => ({ ...s, transform: multiply(rigid, s.transform) }));
9731
+ return shapes.map((s) => projectShape(s, IDENTITY, h, outliner)).filter((s) => s !== void 0);
9732
+ }
9733
+ var PictureCache = class {
9734
+ constructor(limit) {
9735
+ this.limit = limit;
9736
+ }
9737
+ limit;
9738
+ map = /* @__PURE__ */ new Map();
9739
+ get(key) {
9740
+ const v = this.map.get(key);
9741
+ if (v) {
9742
+ this.map.delete(key);
9743
+ this.map.set(key, v);
9744
+ }
9745
+ return v;
9746
+ }
9747
+ set(key, v) {
9748
+ gpuKernels()?.retain(v.tex);
9749
+ this.map.set(key, v);
9750
+ while (this.map.size > this.limit) {
9751
+ const first = this.map.entries().next().value;
9752
+ if (!first) break;
9753
+ this.map.delete(first[0]);
9754
+ gpuKernels()?.release(first[1].tex);
9755
+ }
9756
+ return v;
9757
+ }
9758
+ };
9759
+ var flats = new PictureCache(64);
9760
+ var warps = new PictureCache(64);
9761
+ var PAD2 = 2;
9762
+ var SCALE_STEP = 4;
9763
+ var MAX_SCALE = 2;
9764
+ function flatPicture(piece, box, s, camera, ctx) {
9765
+ const key = `${piece.shapes.map((sh) => identityOf(sh)).join(",")}|${piece.texture ? identityOf(piece.texture) : "-"}|${piece.textureAt ?? "-"}|${s}|${box.x},${box.y},${box.w},${box.h}`;
9766
+ const hit = flats.get(key);
9767
+ if (hit) return hit;
9768
+ const into = s === 1 ? translate(-box.x, -box.y) : multiply(scale(s), translate(-box.x, -box.y));
9769
+ const shapes = piece.shapes.map((sh) => ({ ...sh, transform: multiply(into, sh.transform) }));
9770
+ const width = Math.max(1, Math.ceil(box.w * s));
9771
+ const height = Math.max(1, Math.ceil(box.h * s));
9772
+ let texture;
9773
+ if (piece.texture) {
9774
+ const p = texturePlace(piece.texture, camera.origin);
9775
+ const at2 = { x: (p.x - box.x) * s, y: (p.y - box.y) * s };
9776
+ const sized = s === 1 ? piece.texture : resample(piece.texture, piece.texture.width * s, piece.texture.height * s);
9777
+ texture = placed(sized, at2);
9778
+ }
9779
+ const flat = { id: piece.id, name: "posed", opacity: 1, visible: true, order: 0, shapes };
9780
+ if (texture) {
9781
+ flat.texture = texture;
9782
+ if (piece.textureAt !== void 0) flat.textureAt = piece.textureAt;
9783
+ }
9784
+ const tex = ctx.rasterize({ width, height, background: "none", origin: "top-left", layers: [flat] }, { x: box.x, y: box.y });
9785
+ return flats.set(key, { tex, x: box.x, y: box.y, scale: s });
9786
+ }
9787
+ function placed(tex, at2) {
9788
+ const g = gpuKernels();
9789
+ if (g?.resident(tex)) return g.copy(tex, at2);
9790
+ return { ...tex, at: at2 };
9791
+ }
9792
+ function snap(b) {
9793
+ const x = Math.floor(b.x);
9794
+ const y = Math.floor(b.y);
9795
+ return { x, y, w: Math.ceil(b.x + b.w) - x, h: Math.ceil(b.y + b.h) - y };
9796
+ }
9797
+ function intersect(a, b) {
9798
+ const x = Math.max(a.x, b.x);
9799
+ const y = Math.max(a.y, b.y);
9800
+ const r = Math.min(a.x + a.w, b.x + b.w);
9801
+ const d = Math.min(a.y + a.h, b.y + b.h);
9802
+ return r > x && d > y ? { x, y, w: r - x, h: d - y } : void 0;
9803
+ }
9804
+ function picturePiece(piece, base2, h, pose, camera, ctx, crisp) {
9805
+ const plane = { h };
9806
+ const served = ctx.provided?.(piece.id);
9807
+ if (served) return { ...base2, id: piece.id, shapes: [], texture: served, plane };
9808
+ if (!ctx.rasterize) {
9809
+ const shapes = piece.shapes.map((s2) => placeOnTangent(s2, h)).filter((s2) => s2 !== void 0);
9810
+ const out2 = { ...base2, id: piece.id, shapes, plane };
9811
+ if (piece.texture) {
9812
+ out2.texture = piece.texture;
9813
+ if (piece.textureAt !== void 0) out2.textureAt = piece.textureAt;
9814
+ }
9815
+ return out2;
9816
+ }
9817
+ const empty = { ...base2, id: piece.id, shapes: [], plane };
9818
+ const content = layerContentBounds(piece, camera.origin);
9819
+ if (!content) return empty;
9820
+ const frame = frameRect(camera);
9821
+ const reach = { x: frame.x - frame.w, y: frame.y - frame.h, w: frame.w * 3, h: frame.h * 3 };
9822
+ const padded = { x: content.x - PAD2, y: content.y - PAD2, w: content.w + 2 * PAD2, h: content.h + 2 * PAD2 };
9823
+ const within = intersect(padded, reach);
9824
+ if (!within) return empty;
9825
+ const box = snap(within);
9826
+ const inv = invertMat3(h);
9827
+ const out = inv && projectedQuadBounds(h, box);
9828
+ const target = out && intersect(out, frame);
9829
+ if (!inv || !target) return empty;
9830
+ const rect = snap(target);
9831
+ const s = crisp ? 1 : Math.min(MAX_SCALE, Math.max(1, Math.ceil((magnificationAt(h, pose.pivot) ?? 1) * SCALE_STEP) / SCALE_STEP));
9832
+ const flat = flatPicture(piece, box, s, camera, ctx);
9833
+ const key = `${identityOf(flat.tex)}|${h.join(",")}|${rect.x},${rect.y},${rect.w},${rect.h}`;
9834
+ const hit = warps.get(key);
9835
+ const texture = hit ? hit.tex : warps.set(key, { tex: warpTexture(flat.tex, { x: flat.x, y: flat.y, scale: flat.scale }, inv, rect) }).tex;
9836
+ return { ...base2, id: piece.id, shapes: [], texture, plane };
9837
+ }
9838
+ function projectLayer(layer, camera, ctx, crisp = false) {
9839
+ const pose = layer.pose;
9840
+ if (!pose || !layer.visible) return [layer];
9841
+ const h = layerHomography(pose, camera);
9842
+ const { pose: _pose, plane: _plane, shapes: _shapes, texture: _texture, textureAt: _at, id: _id, ...base2 } = layer;
9843
+ return layerPieces(layer).map(
9844
+ (piece) => piece.kind === "vector" ? { ...base2, id: piece.id, shapes: projectShapes(piece.shapes, h, ctx.outline), plane: { h } } : picturePiece(piece, base2, h, pose, camera, ctx, crisp)
9845
+ );
9846
+ }
9847
+ var ownCamera = (p) => normalizeParam(p.camera).base === "own";
9848
+ var onePlane = (p) => (normalizeParam(p.plane).base ?? "stream") !== "shape";
9849
+ var pivotIsPreset2 = (p) => (normalizeParam(p.pivot).base ?? "shape") !== "custom";
9850
+ var PIVOTS = ["shape", "center", "canvas", "custom"];
9851
+ var transform3dNode = {
9852
+ type: "transform3d",
9853
+ label: "Transform 3D",
9854
+ category: "modifier",
9855
+ description: "Poses the stream as a plane under the camera: move in X, Y and Z, turn about X, Y and Z, scale \u2014 one plane for the stream, or one per shape.",
9856
+ inputs: [{ name: "in", type: "shapes" }],
9857
+ outputs: [{ name: "out", type: "shapes" }],
9858
+ params: [
9859
+ {
9860
+ key: "plane",
9861
+ label: "Plane",
9862
+ type: "enum",
9863
+ default: "stream",
9864
+ options: [
9865
+ { value: "stream", label: "One for the stream" },
9866
+ { value: "shape", label: "One per shape" }
9867
+ ],
9868
+ description: "One plane carrying every shape, or each shape on a plane of its own \u2014 then every value below resolves per shape, so a jitter or a stagger turns them one by one."
9869
+ },
9870
+ {
9871
+ key: "order",
9872
+ label: "Order",
9873
+ type: "enum",
9874
+ default: "stack",
9875
+ options: [
9876
+ { value: "stack", label: "As they came" },
9877
+ { value: "depth", label: "By depth" }
9878
+ ],
9879
+ description: "One per shape only: keep the stream\u2019s order, or draw the nearer plane over the farther \u2014 a carousel\u2019s front cards over its back ones. Planes still cannot cut through each other.",
9880
+ dimWhen: onePlane
9881
+ },
9882
+ {
9883
+ key: "hideBack",
9884
+ label: "Hide Back",
9885
+ type: "bool",
9886
+ default: false,
9887
+ inline: true,
9888
+ description: "Drop a plane turned past edge-on, showing its other side \u2014 a card flipped over, a carousel\u2019s far cards."
9889
+ },
9890
+ { key: "posX", label: "Pos. X", type: "number", default: 0, step: 1, unit: "px", description: "Moves the plane across the frame." },
9891
+ { key: "posY", label: "Pos. Y", type: "number", default: 0, step: 1, unit: "px", description: "Moves the plane down the frame." },
9892
+ {
9893
+ key: "posZ",
9894
+ label: "Pos. Z",
9895
+ type: "number",
9896
+ default: 0,
9897
+ step: 1,
9898
+ unit: "px",
9899
+ description: "Depth: positive moves the plane away from the camera and it draws smaller; negative brings it nearer."
9900
+ },
9901
+ { key: "rotX", label: "Rot. X", type: "number", default: 0, step: 1, unit: "deg", description: "Tilts the plane about a horizontal line through the pivot; positive leans the top away." },
9902
+ { key: "rotY", label: "Rot. Y", type: "number", default: 0, step: 1, unit: "deg", description: "Turns the plane about a vertical line through the pivot; positive turns the right edge away." },
9903
+ { key: "rotZ", label: "Rot. Z", type: "number", default: 0, step: 1, unit: "deg", description: "Spins the plane in its own surface, clockwise on screen." },
9904
+ { key: "scaleX", label: "Scale X", type: "number", default: 100, step: 1, unit: "%" },
9905
+ { key: "scaleY", label: "Scale Y", type: "number", default: 100, step: 1, unit: "%" },
9906
+ {
9907
+ key: "pivot",
9908
+ label: "Pivot",
9909
+ type: "enum",
9910
+ default: "shape",
9911
+ options: [
9912
+ { value: "shape", label: "Shape centre" },
9913
+ { value: "center", label: "Frame centre" },
9914
+ { value: "canvas", label: "Canvas origin" },
9915
+ { value: "custom", label: "Custom point" }
9916
+ ],
9917
+ description: "What the rotations and scale turn about. Shape centre is the stream\u2019s centre for one plane, each shape\u2019s own for one per shape; Frame centre is where the camera looks."
9918
+ },
9919
+ { key: "pivotX", label: "Pivot X", type: "number", default: 0, step: 0.5, unit: "px", dimWhen: pivotIsPreset2 },
9920
+ { key: "pivotY", label: "Pivot Y", type: "number", default: 0, step: 0.5, unit: "px", dimWhen: pivotIsPreset2 },
9921
+ {
9922
+ key: "camera",
9923
+ label: "Camera",
9924
+ type: "enum",
9925
+ default: "canvas",
9926
+ options: [
9927
+ { value: "canvas", label: "The Canvas\u2019s" },
9928
+ { value: "own", label: "Own perspective" }
9929
+ ],
9930
+ description: "Follow the Canvas\u2019s Perspective \u2014 one camera for the document \u2014 or give this plane a lens of its own."
9931
+ },
9932
+ {
9933
+ key: "perspective",
9934
+ label: "Perspective",
9935
+ type: "number",
9936
+ default: 1500,
9937
+ min: 0,
9938
+ step: 10,
9939
+ unit: "px",
9940
+ dimWhen: (p) => !ownCamera(p),
9941
+ description: "Own perspective only: the eye\u2019s distance from the frame; smaller is a wider lens, 0 is orthographic."
9942
+ }
9943
+ ],
9944
+ cook(ctx) {
9945
+ const shapes = ctx.shapes("in");
9946
+ if (shapes.length === 0) return { out: shapesVal(shapes) };
9947
+ const perspective = ctx.enumOf("camera", ["canvas", "own"]) === "own" ? Math.max(0, ctx.num("perspective")) : ctx.canvas.perspective ?? 1500;
9948
+ const camera = { perspective, width: ctx.canvas.w, height: ctx.canvas.h, origin: ctx.canvas.origin };
9949
+ const mode = ctx.enumOf("pivot", PIVOTS);
9950
+ const n = shapes.length;
9951
+ const pivotOf2 = (i, content) => {
9952
+ if (mode === "canvas") return { x: 0, y: 0 };
9953
+ if (mode === "custom") return { x: ctx.num("pivotX", i, n), y: ctx.num("pivotY", i, n) };
9954
+ if (mode === "shape" && content) return centerOf(content);
9955
+ return frameCentre(camera);
9956
+ };
9957
+ const poseAt = (i, count, content) => ({
9958
+ x: ctx.num("posX", i, count),
9959
+ y: ctx.num("posY", i, count),
9960
+ z: ctx.num("posZ", i, count),
9961
+ rx: ctx.num("rotX", i, count),
9962
+ ry: ctx.num("rotY", i, count),
9963
+ rz: ctx.num("rotZ", i, count),
9964
+ sx: ctx.num("scaleX", i, count) / 100,
9965
+ sy: ctx.num("scaleY", i, count) / 100,
9966
+ pivot: pivotOf2(i, content)
9967
+ });
9968
+ const hideBack = ctx.bool("hideBack");
9969
+ const centre = frameCentre(camera);
9970
+ const judged = (pose, content) => {
9971
+ const c = content ? centerOf(content) : pose.pivot;
9972
+ const p = { x: c.x - centre.x, y: c.y - centre.y };
9973
+ const model = layerModelMatrix(pose, centre);
9974
+ return { model, depth: planeDepth(model, p), faces: planeFaces(model, p, camera.perspective) };
9975
+ };
9976
+ const stamped = (projected, sources, model) => projected.map((shape, i) => {
9977
+ const b = shapeBounds(sources[i] ?? shape);
9978
+ const c = b ? centerOf(b) : centre;
9979
+ const depth = planeDepth(model, { x: c.x - centre.x, y: c.y - centre.y });
9980
+ return depth === 0 && shape.meta.depth === void 0 ? shape : { ...shape, meta: { ...shape.meta, depth } };
9981
+ });
9982
+ if (ctx.enumOf("plane", ["stream", "shape"]) === "stream") {
9983
+ const content = boundsOfShapes(shapes);
9984
+ const pose = poseAt(0, 1, content);
9985
+ if (isIdentityPose(pose)) return { out: shapesVal(shapes) };
9986
+ const { model, faces } = judged(pose, content);
9987
+ if (hideBack && !faces) return { out: shapesVal([]) };
9988
+ return { out: shapesVal(stamped(projectShapes(shapes, layerHomography(pose, camera), ctx.outline), shapes, model)) };
9989
+ }
9990
+ const byDepth = ctx.enumOf("order", ["stack", "depth"]) === "depth";
9991
+ const posed = [];
9992
+ shapes.forEach((shape, i) => {
9993
+ const content = shapeBounds(shape);
9994
+ const pose = poseAt(i, n, content);
9995
+ const { model, depth, faces } = judged(pose, content);
9996
+ if (hideBack && !faces) return;
9997
+ posed.push({ depth, shapes: isIdentityPose(pose) ? [shape] : stamped(projectShapes([shape], layerHomography(pose, camera), ctx.outline), [shape], model) });
9998
+ });
9999
+ if (byDepth) posed.sort((a, b) => b.depth - a.depth);
10000
+ return { out: shapesVal(posed.flatMap((p) => p.shapes)) };
10001
+ }
10002
+ };
9161
10003
  var CIRCLE_SEGMENTS2 = 96;
9162
10004
  var CORNER_STYLES = [
9163
10005
  { value: "round", label: "Round" },
@@ -10051,13 +10893,13 @@ var arrayRadialNode = {
10051
10893
  const angle = start + sweep * k / divisor;
10052
10894
  const vars = { k, angle };
10053
10895
  const radius = ctx.num("radius", k, count, vars);
10054
- const rad = angle * Math.PI / 180;
10896
+ const rad2 = angle * Math.PI / 180;
10055
10897
  let m;
10056
10898
  if (mode === "rotate") {
10057
10899
  m = multiply(multiply(translate(cx, cy), rotate(angle)), translate(-cx, -cy));
10058
10900
  } else {
10059
- const px = cx + radius * Math.cos(rad);
10060
- const py = cy + radius * Math.sin(rad);
10901
+ const px = cx + radius * Math.cos(rad2);
10902
+ const py = cy + radius * Math.sin(rad2);
10061
10903
  m = mode === "orbit" ? multiply(translate(px, py), rotate(angle)) : translate(px, py);
10062
10904
  }
10063
10905
  for (const [j, shape] of carry(k).entries()) {
@@ -10580,7 +11422,7 @@ var jitterNode = {
10580
11422
  const posYParam = ctx.num("positionY");
10581
11423
  const posY = posYParam < 0 ? posX : posYParam;
10582
11424
  const rot = ctx.num("rotation");
10583
- const scale4 = ctx.num("scale");
11425
+ const scale5 = ctx.num("scale");
10584
11426
  const axis2 = ctx.enumOf("scaleAxis", ["both", "x", "y"]);
10585
11427
  const uniform = ctx.bool("uniformScale");
10586
11428
  const opacity = ctx.num("opacity");
@@ -10598,8 +11440,8 @@ var jitterNode = {
10598
11440
  const tx = posX === 0 ? 0 : signed() * posX;
10599
11441
  const ty = posY === 0 ? 0 : signed() * posY;
10600
11442
  const rotation = rot === 0 ? 0 : signed() * rot;
10601
- const first = scale4 === 0 ? 1 : 1 + signed() * scale4;
10602
- const second = scale4 === 0 ? 1 : uniform || axis2 !== "both" ? first : 1 + signed() * scale4;
11443
+ const first = scale5 === 0 ? 1 : 1 + signed() * scale5;
11444
+ const second = scale5 === 0 ? 1 : uniform || axis2 !== "both" ? first : 1 + signed() * scale5;
10603
11445
  const sx = axis2 === "y" ? 1 : first;
10604
11446
  const sy = axis2 === "x" ? 1 : axis2 === "both" ? second : first;
10605
11447
  const grown = sx !== 1 || sy !== 1 ? scaleAbout(shape, sx, sy, pivotOf(shape)) : shape;
@@ -10714,8 +11556,8 @@ function drawnFill(fill, local2) {
10714
11556
  const rgba = parseHex(fill);
10715
11557
  if (!rgba) return "none";
10716
11558
  const a = Math.round(rgba[3] * local2 * local2 * 255);
10717
- const hex232 = (n) => n.toString(16).padStart(2, "0");
10718
- return `#${hex232(rgba[0])}${hex232(rgba[1])}${hex232(rgba[2])}${hex232(a)}`;
11559
+ const hex242 = (n) => n.toString(16).padStart(2, "0");
11560
+ return `#${hex242(rgba[0])}${hex242(rgba[1])}${hex242(rgba[2])}${hex242(a)}`;
10719
11561
  }
10720
11562
  function drawShape(ctx, shape, local2, emitted) {
10721
11563
  const lines = flatten(shape);
@@ -10738,8 +11580,8 @@ function drawShape(ctx, shape, local2, emitted) {
10738
11580
  return out.length > 0 ? out : [fadeShape(shape, local2)];
10739
11581
  }
10740
11582
  function wipeDirection(angleDeg) {
10741
- const rad = angleDeg * Math.PI / 180;
10742
- return { x: Math.cos(rad), y: Math.sin(rad) };
11583
+ const rad2 = angleDeg * Math.PI / 180;
11584
+ return { x: Math.cos(rad2), y: Math.sin(rad2) };
10743
11585
  }
10744
11586
  function wipeExtent(shapes, dir) {
10745
11587
  const dot = (x, y) => x * dir.x + y * dir.y;
@@ -11085,52 +11927,6 @@ function parseIndexList(text) {
11085
11927
  }
11086
11928
  return [...out].sort((m, n) => m - n);
11087
11929
  }
11088
- var isBitmap = (s) => isBitmapFamily(s.fontFamily);
11089
- var isVariable = (s) => resolveFace(s.fontFamily, s.fontWeight ?? 400, s.fontStretch).variation !== void 0;
11090
- var needsOutlining = (s) => isBitmap(s) || isVariable(s);
11091
- var every = () => true;
11092
- function anyText(scene, pick) {
11093
- const any = (shapes) => shapes.some((s) => s.kind === "text" ? pick(s) : s.kind === "group" && any(s.children));
11094
- return scene.layers.some((l) => any(l.shapes));
11095
- }
11096
- function outlineWhere(shape, outliner, pick) {
11097
- if (shape.kind === "group") {
11098
- const children = shape.children.map((c2) => outlineWhere(c2, outliner, pick));
11099
- return children.some((c2, i) => c2 !== shape.children[i]) ? { ...shape, children } : shape;
11100
- }
11101
- if (shape.kind !== "text" || !pick(shape)) return shape;
11102
- const weight = shape.fontWeight ?? 400;
11103
- const g = outliner({ text: shape.text, family: shape.fontFamily, weight, stretch: shape.fontStretch, size: shape.fontSize, letterSpacing: shape.letterSpacing });
11104
- if (!g) return shape;
11105
- const shift = shape.anchor === "middle" ? -g.width / 2 : shape.anchor === "end" ? -g.width : 0;
11106
- const [a, b, c, d, e, f] = shape.transform;
11107
- const tx = shape.x + shift;
11108
- const ty = shape.y;
11109
- const { x: _x, y: _y, text: _t, fontSize: _s, fontFamily: _f, fontWeight: _w, fontStretch: _st, anchor: _a, letterSpacing: _l, metrics: _m, ...rest } = shape;
11110
- return {
11111
- ...rest,
11112
- kind: "path",
11113
- d: g.d,
11114
- transform: [a, b, c, d, a * tx + c * ty + e, b * tx + d * ty + f],
11115
- meta: { ...shape.meta, tags: [...shape.meta.tags ?? [], "glyphs"] }
11116
- };
11117
- }
11118
- function outlineTextShape(shape, outliner, all = false) {
11119
- return outlineWhere(shape, outliner, all ? every : needsOutlining);
11120
- }
11121
- function outlineScene(scene, outliner, pick) {
11122
- if (!outliner || !anyText(scene, pick)) return scene;
11123
- return {
11124
- ...scene,
11125
- layers: scene.layers.map((l) => {
11126
- const shapes = l.shapes.map((s) => outlineWhere(s, outliner, pick));
11127
- return shapes.some((s, i) => s !== l.shapes[i]) ? { ...l, shapes } : l;
11128
- })
11129
- };
11130
- }
11131
- function outlineBitmapText(scene, outliner) {
11132
- return outlineScene(scene, outliner, isBitmap);
11133
- }
11134
11930
  var selectModeIsNot = (mode) => (params) => (normalizeParam(params.mode).base ?? "every-nth") !== mode;
11135
11931
  var selectNode = {
11136
11932
  type: "select",
@@ -11349,8 +12145,8 @@ var alignNode = {
11349
12145
  const alignX = ctx.bool("alignX");
11350
12146
  const alignY = ctx.bool("alignY");
11351
12147
  if (!alignX && !alignY) return { out: shapesVal(withRef(input)) };
11352
- const snap = ctx.int("snap");
11353
- const step = snap > 0 ? 90 / 2 ** (snap - 1) : 0;
12148
+ const snap2 = ctx.int("snap");
12149
+ const step = snap2 > 0 ? 90 / 2 ** (snap2 - 1) : 0;
11354
12150
  const side = ctx.enumOf("side", ["inside", "on-edge", "outside"]);
11355
12151
  const over = side === "inside" ? 0 : side === "on-edge" ? 0.5 : 1;
11356
12152
  const asOne = ctx.bool("asOne");
@@ -11367,9 +12163,9 @@ var alignNode = {
11367
12163
  if (!b) return shape;
11368
12164
  const raw = ctx.num("angle", i, n);
11369
12165
  const deg = step > 0 ? Math.round(raw / step) * step : raw;
11370
- const rad = deg * Math.PI / 180;
11371
- const cx = Math.cos(rad);
11372
- const cy = Math.sin(rad);
12166
+ const rad2 = deg * Math.PI / 180;
12167
+ const cx = Math.cos(rad2);
12168
+ const cy = Math.sin(rad2);
11373
12169
  const m = Math.max(Math.abs(cx), Math.abs(cy)) || 1;
11374
12170
  const reach = Math.max(0, Math.min(1, ctx.num("reach", i, n)));
11375
12171
  const ax = (1 + cx / m * reach) / 2;
@@ -11579,7 +12375,7 @@ var sortNode = {
11579
12375
  cook(ctx) {
11580
12376
  const input = ctx.shapes("in");
11581
12377
  const by = ctx.enumOf("by", SORT_MODES);
11582
- let ordered;
12378
+ let ordered2;
11583
12379
  if (by === "shuffle") {
11584
12380
  const clock = ctx.inputs["reseed"];
11585
12381
  let reseed = 0;
@@ -11589,39 +12385,39 @@ var sortNode = {
11589
12385
  }
11590
12386
  const salt = ctx.int("salt");
11591
12387
  const slot = new Map(input.map((s) => [s.id, ctx.rand("shuffle", salt, reseed, s.id).next()]));
11592
- ordered = [...input].sort((a, b) => slot.get(a.id) - slot.get(b.id) || a.id.localeCompare(b.id));
12388
+ ordered2 = [...input].sort((a, b) => slot.get(a.id) - slot.get(b.id) || a.id.localeCompare(b.id));
11593
12389
  } else if (by === "source") {
11594
12390
  const emitters = [];
11595
12391
  for (const s of input) if (!emitters.includes(s.meta.sourceNodeId)) emitters.push(s.meta.sourceNodeId);
11596
- ordered = [...input].sort((a, b) => {
12392
+ ordered2 = [...input].sort((a, b) => {
11597
12393
  const e = emitters.indexOf(a.meta.sourceNodeId) - emitters.indexOf(b.meta.sourceNodeId);
11598
12394
  return e !== 0 ? e : sourceIndex(a) - sourceIndex(b) || a.id.localeCompare(b.id);
11599
12395
  });
11600
12396
  } else {
11601
- const placed = [];
12397
+ const placed2 = [];
11602
12398
  const unplaced = [];
11603
12399
  for (const shape of input) {
11604
12400
  const b = shapeBounds(shape);
11605
12401
  if (b) {
11606
12402
  const c = centerOf(b);
11607
- placed.push({ shape, cx: c.x, cy: c.y, h: b.h });
12403
+ placed2.push({ shape, cx: c.x, cy: c.y, h: b.h });
11608
12404
  } else unplaced.push(shape);
11609
12405
  }
11610
12406
  if (by === "x") {
11611
- placed.sort((a, b) => a.cx - b.cx || a.cy - b.cy);
12407
+ placed2.sort((a, b) => a.cx - b.cx || a.cy - b.cy);
11612
12408
  } else if (by === "y") {
11613
- placed.sort((a, b) => a.cy - b.cy || a.cx - b.cx);
12409
+ placed2.sort((a, b) => a.cy - b.cy || a.cx - b.cx);
11614
12410
  } else if (by === "reading") {
11615
- const tol = ctx.num("rowTolerance") * median(placed.map((p) => p.h));
11616
- placed.sort((a, b) => a.cy - b.cy);
12411
+ const tol = ctx.num("rowTolerance") * median(placed2.map((p) => p.h));
12412
+ placed2.sort((a, b) => a.cy - b.cy);
11617
12413
  const rows = [];
11618
- for (const p of placed) {
12414
+ for (const p of placed2) {
11619
12415
  const row = rows[rows.length - 1];
11620
12416
  if (row && p.cy - row[0].cy <= tol) row.push(p);
11621
12417
  else rows.push([p]);
11622
12418
  }
11623
12419
  for (const row of rows) row.sort((a, b) => a.cx - b.cx);
11624
- placed.splice(0, placed.length, ...rows.flat());
12420
+ placed2.splice(0, placed2.length, ...rows.flat());
11625
12421
  } else {
11626
12422
  const bounds = boundsOfShapes(input);
11627
12423
  const c = bounds ? centerOf(bounds) : { x: 0, y: 0 };
@@ -11631,12 +12427,12 @@ var sortNode = {
11631
12427
  const k = ((deg - start) % 360 + 360) % 360;
11632
12428
  return k > 360 - 1e-6 ? 0 : k;
11633
12429
  };
11634
- placed.sort((a, b) => keyOf(a) - keyOf(b) || Math.hypot(a.cx - c.x, a.cy - c.y) - Math.hypot(b.cx - c.x, b.cy - c.y));
12430
+ placed2.sort((a, b) => keyOf(a) - keyOf(b) || Math.hypot(a.cx - c.x, a.cy - c.y) - Math.hypot(b.cx - c.x, b.cy - c.y));
11635
12431
  }
11636
- ordered = [...placed.map((p) => p.shape), ...unplaced];
12432
+ ordered2 = [...placed2.map((p) => p.shape), ...unplaced];
11637
12433
  }
11638
- if (ctx.bool("reverse")) ordered.reverse();
11639
- return { out: shapesVal(ordered.map((s, i) => renumbered(s, i))) };
12434
+ if (ctx.bool("reverse")) ordered2.reverse();
12435
+ return { out: shapesVal(ordered2.map((s, i) => renumbered(s, i))) };
11640
12436
  }
11641
12437
  };
11642
12438
  var groupNode = {
@@ -11890,9 +12686,9 @@ var randomValueNode = {
11890
12686
  cook(ctx) {
11891
12687
  const min = ctx.num("min");
11892
12688
  const max = ctx.num("max");
11893
- const snap = ctx.num("step");
12689
+ const snap2 = ctx.num("step");
11894
12690
  let value = ctx.rand("value", seedSalt(ctx.int("salt"))).range(Math.min(min, max), Math.max(min, max));
11895
- if (snap > 0) value = Math.round(value / snap) * snap;
12691
+ if (snap2 > 0) value = Math.round(value / snap2) * snap2;
11896
12692
  return { out: numVal(value) };
11897
12693
  }
11898
12694
  };
@@ -12020,9 +12816,9 @@ var randomStepNode = {
12020
12816
  const index = interval > 0 ? Math.floor(ctx.t / interval) : 0;
12021
12817
  const min = ctx.num("min");
12022
12818
  const max = ctx.num("max");
12023
- const snap = ctx.num("step");
12819
+ const snap2 = ctx.num("step");
12024
12820
  let value = ctx.rand("value", seedSalt(ctx.int("salt")), index).range(Math.min(min, max), Math.max(min, max));
12025
- if (snap > 0) value = Math.round(value / snap) * snap;
12821
+ if (snap2 > 0) value = Math.round(value / snap2) * snap2;
12026
12822
  return { out: numVal(value) };
12027
12823
  }
12028
12824
  };
@@ -12417,6 +13213,7 @@ var mathNode = {
12417
13213
  type: "math",
12418
13214
  label: "Math",
12419
13215
  category: "driver",
13216
+ description: "One operation on two numbers, A and B: wire a driver into A and set B to scale, offset, clamp or wrap it \u2014 Time \xD7 45 is a rotation in degrees per second.",
12420
13217
  inputs: [],
12421
13218
  outputs: [{ name: "out", type: "number" }],
12422
13219
  params: [
@@ -12981,6 +13778,7 @@ var blurNode = {
12981
13778
  type: "blur",
12982
13779
  label: "Blur",
12983
13780
  category: "post",
13781
+ description: "A Gaussian blur of the picture by Radius, the same bytes everywhere. For a blur that follows depth, see Depth of Field.",
12984
13782
  inputs: [{ name: "in", type: "texture" }],
12985
13783
  outputs: [{ name: "out", type: "texture" }],
12986
13784
  params: [{ key: "radius", label: "Radius", type: "number", default: 4, min: 0, max: 64, step: 0.5, unit: "px" }],
@@ -13130,9 +13928,9 @@ var chromaticNode = {
13130
13928
  const tex = ctx.texture();
13131
13929
  if (!tex) return QUIET3;
13132
13930
  const offset = ctx.num("offset");
13133
- const rad = ctx.num("angle") * Math.PI / 180;
13931
+ const rad2 = ctx.num("angle") * Math.PI / 180;
13134
13932
  return {
13135
- out: textureVal(chromaticAberration(tex, offset * Math.cos(rad), offset * Math.sin(rad)))
13933
+ out: textureVal(chromaticAberration(tex, offset * Math.cos(rad2), offset * Math.sin(rad2)))
13136
13934
  };
13137
13935
  }
13138
13936
  };
@@ -13140,6 +13938,7 @@ var scanlinesNode = {
13140
13938
  type: "scanlines",
13141
13939
  label: "Scanlines",
13142
13940
  category: "post",
13941
+ description: "The CRT look: every few rows (or columns) darkened by Strength, rolling with Roll.",
13143
13942
  inputs: [{ name: "in", type: "texture" }],
13144
13943
  outputs: [{ name: "out", type: "texture" }],
13145
13944
  // `speed` rolls the lines with t.
@@ -13534,9 +14333,130 @@ var matteNode = {
13534
14333
  return { out: textureVal(matte(tex, m, { mode: ctx.enumOf("mode", ["alpha", "luma"]), invert: ctx.bool("invert") }, ctx.canvas.origin ?? "top-left")) };
13535
14334
  }
13536
14335
  };
14336
+ function silhouette(shape, colour) {
14337
+ const s = shape.style;
14338
+ const style = {
14339
+ ...s,
14340
+ fill: s.fill === void 0 || s.fill === "none" ? "none" : colour,
14341
+ stroke: s.stroke === void 0 || s.stroke === "none" || (s.strokeWidth ?? 1) <= 0 ? "none" : colour,
14342
+ opacity: 1
14343
+ };
14344
+ switch (shape.kind) {
14345
+ case "group":
14346
+ return { ...shape, style, children: shape.children.map((c) => silhouette(c, colour)) };
14347
+ case "image":
14348
+ return { id: shape.id, kind: "rect", x: shape.x, y: shape.y, w: shape.w, h: shape.h, rx: 0, transform: shape.transform, style: { ...style, fill: colour, stroke: "none" }, meta: shape.meta };
14349
+ default:
14350
+ return { ...shape, style };
14351
+ }
14352
+ }
14353
+ var hex222 = (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0");
14354
+ function farthestFirst(items) {
14355
+ return [...items].sort((a, b) => b.depth - a.depth);
14356
+ }
14357
+ var depthOfFieldNode = {
14358
+ type: "depth-of-field",
14359
+ label: "Depth of Field",
14360
+ category: "post",
14361
+ description: "A lens with a focal plane: what sits at Focus stays sharp, and the picture blurs the farther a shape is from it, nearer or beyond. Wire the picture into In and the same shapes that made it into Depth \u2014 they remember how deep Transform 3D put them.",
14362
+ keywords: ["dof", "focus", "defocus", "bokeh", "lens", "rack focus", "depth", "3d", "blur"],
14363
+ quickKey: "focus",
14364
+ inputs: [
14365
+ { name: "in", type: "texture", label: "In" },
14366
+ { name: "depth", type: "shapes", label: "Depth" }
14367
+ ],
14368
+ outputs: [{ name: "out", type: "texture" }],
14369
+ params: [
14370
+ { key: "focus", label: "Focus", type: "number", default: 0, step: 10, unit: "px", description: "The depth that is sharp, in Pos. Z\u2019s terms: 0 is the frame itself, positive is away from the camera. Wire a Seq to pull focus." },
14371
+ { key: "range", label: "Range", type: "number", default: 200, min: 0, step: 10, unit: "px", description: "The depth kept sharp around Focus, half of it either side." },
14372
+ { key: "falloff", label: "Falloff", type: "number", default: 800, min: 0, step: 10, unit: "px", description: "How much farther out of the Range the blur takes to reach its full Blur; 0 snaps to it at once." },
14373
+ { key: "blur", label: "Blur", type: "number", default: 12, min: 0, max: 64, step: 0.5, unit: "px", description: "The blur at the far end of the Falloff, like the Blur node\u2019s Radius." }
14374
+ ],
14375
+ cook(ctx) {
14376
+ const tex = ctx.texture("in");
14377
+ if (!tex) return QUIET3;
14378
+ const shapes = ctx.shapes("depth");
14379
+ const most = ctx.num("blur");
14380
+ if (shapes.length === 0 || most <= 0 || ctx.canvas.w <= 0 || ctx.canvas.h <= 0) return { out: textureVal(tex) };
14381
+ if (!ctx.rasterize) {
14382
+ throw new Error("no rasteriser available yet \u2014 the preview loads one on demand; exports always have one");
14383
+ }
14384
+ const focus = ctx.num("focus");
14385
+ const range = Math.max(0, ctx.num("range"));
14386
+ const falloff = Math.max(0, ctx.num("falloff"));
14387
+ const judged = shapes.map((shape) => {
14388
+ const depth = shape.meta.depth ?? 0;
14389
+ const away = Math.abs(depth - focus) - range / 2;
14390
+ const k = away <= 0 ? 0 : falloff > 0 ? Math.min(1, away / falloff) : 1;
14391
+ const level = focusLevel(most * k);
14392
+ return { shape, depth, level, side: level > 0 && depth > focus ? 1 : 0 };
14393
+ });
14394
+ if (judged.every((j) => j.level === 0)) return { out: textureVal(tex) };
14395
+ const map = ctx.rasterize({
14396
+ width: ctx.canvas.w,
14397
+ height: ctx.canvas.h,
14398
+ background: "none",
14399
+ origin: ctx.canvas.origin,
14400
+ layers: [
14401
+ {
14402
+ id: ctx.nodeId,
14403
+ name: "focus",
14404
+ opacity: 1,
14405
+ visible: true,
14406
+ order: 0,
14407
+ shapes: farthestFirst(judged).map((j) => silhouette(j.shape, `#${hex222(j.level)}${j.side ? "ff" : "00"}00`))
14408
+ }
14409
+ ]
14410
+ });
14411
+ const slices = focusSlices(judged.map((j) => ({ level: j.level, side: j.side })));
14412
+ return { out: textureVal(depthOfField(tex, map, slices, ctx.canvas.origin ?? "top-left")) };
14413
+ }
14414
+ };
14415
+ var depthMapNode = {
14416
+ type: "depth-map",
14417
+ label: "Depth Map",
14418
+ category: "post",
14419
+ description: "The shapes drawn as their depth: a grey per shape from black at Near to white at Far, the nearer shape over the farther. What Depth of Field sees \u2014 and a depth fog, through a Blend.",
14420
+ keywords: ["depth", "z", "map", "zdepth", "fog", "3d", "grey"],
14421
+ inputs: [{ name: "in", type: "shapes" }],
14422
+ outputs: [{ name: "out", type: "texture" }],
14423
+ params: [
14424
+ { key: "near", label: "Near", type: "number", default: -1e3, step: 10, unit: "px", description: "The depth drawn black, in Pos. Z\u2019s terms; anything nearer is black too." },
14425
+ { key: "far", label: "Far", type: "number", default: 1e3, step: 10, unit: "px", description: "The depth drawn white; anything farther is white too. A shape no 3D node has posed sits at 0." }
14426
+ ],
14427
+ cook(ctx) {
14428
+ if (!ctx.rasterize) {
14429
+ throw new Error("no rasteriser available yet \u2014 the preview loads one on demand; exports always have one");
14430
+ }
14431
+ if (ctx.canvas.w <= 0 || ctx.canvas.h <= 0) throw new Error("has no size to draw at \u2014 give the project a canvas");
14432
+ const near = ctx.num("near");
14433
+ const span = ctx.num("far") - near;
14434
+ const judged = ctx.shapes("in").map((shape) => ({ shape, depth: shape.meta.depth ?? 0 }));
14435
+ const grey = (depth) => hex222(span === 0 ? 128 : Math.max(0, Math.min(1, (depth - near) / span)) * 255);
14436
+ const texture = ctx.rasterize({
14437
+ width: ctx.canvas.w,
14438
+ height: ctx.canvas.h,
14439
+ background: "none",
14440
+ origin: ctx.canvas.origin,
14441
+ layers: [
14442
+ {
14443
+ id: ctx.nodeId,
14444
+ name: "depth",
14445
+ opacity: 1,
14446
+ visible: true,
14447
+ order: 0,
14448
+ shapes: farthestFirst(judged).map((j) => silhouette(j.shape, `#${grey(j.depth).repeat(3)}`))
14449
+ }
14450
+ ]
14451
+ });
14452
+ return { out: textureVal(texture) };
14453
+ }
14454
+ };
13537
14455
  var POST_NODES = [
13538
14456
  rasterizeNode,
13539
14457
  blurNode,
14458
+ depthOfFieldNode,
14459
+ depthMapNode,
13540
14460
  glitchNode,
13541
14461
  chromaticNode,
13542
14462
  scanlinesNode,
@@ -13571,13 +14491,13 @@ var familyOf = (p) => resolveFamily(String(normalizeParam(p.fontFamily).base ??
13571
14491
  var FALLBACK_CAP_HEIGHT2 = 0.7;
13572
14492
  var FALLBACK_BLOCK = { top: 0.95, bottom: -0.35 };
13573
14493
  var styleParam2 = (key) => STYLE_PARAMS.find((p) => p.key === key);
13574
- var hex222 = (n) => n.toString(16).padStart(2, "0");
14494
+ var hex23 = (n) => n.toString(16).padStart(2, "0");
13575
14495
  function hexOf(rgba, k) {
13576
14496
  const a = rgba[k * 4 + 3];
13577
14497
  if (a === 0) return void 0;
13578
14498
  const straight = (v) => Math.min(255, Math.round(v * 255 / a));
13579
- const rgb = `#${hex222(straight(rgba[k * 4]))}${hex222(straight(rgba[k * 4 + 1]))}${hex222(straight(rgba[k * 4 + 2]))}`;
13580
- return a === 255 ? rgb : `${rgb}${hex222(a)}`;
14499
+ const rgb = `#${hex23(straight(rgba[k * 4]))}${hex23(straight(rgba[k * 4 + 1]))}${hex23(straight(rgba[k * 4 + 2]))}`;
14500
+ return a === 255 ? rgb : `${rgb}${hex23(a)}`;
13581
14501
  }
13582
14502
  function pathYBounds(d) {
13583
14503
  const nums = d.match(/-?\d*\.?\d+(?:e-?\d+)?/g);
@@ -13993,9 +14913,9 @@ var halftoneNode = {
13993
14913
  const ty = tex.at?.y ?? (ctx.canvas.origin === "center" ? -tex.height / 2 : 0);
13994
14914
  const cx = tex.width / 2;
13995
14915
  const cy = tex.height / 2;
13996
- const rad = angle * Math.PI / 180;
13997
- const ex = Math.cos(rad);
13998
- const ey = Math.sin(rad);
14916
+ const rad2 = angle * Math.PI / 180;
14917
+ const ex = Math.cos(rad2);
14918
+ const ey = Math.sin(rad2);
13999
14919
  const reach = Math.ceil(Math.hypot(cx, cy) / cell);
14000
14920
  const shapes = [];
14001
14921
  for (let j = -reach; j <= reach; j++) {
@@ -14196,10 +15116,15 @@ var contoursNode = {
14196
15116
  type: "contours",
14197
15117
  label: "Contours",
14198
15118
  category: "post",
14199
- description: "Pixels back into lines: the picture read as a height map and traced where it crosses evenly spaced levels \u2014 the lines of a topographic map, every Nth one a major line. Wire Noise Paint \u2192 Rect \u2192 Rasterize in for terrain; animate Offset to make the lines flow.",
15119
+ description: "Pixels back into lines: the picture read as a height map and traced where it crosses evenly spaced levels \u2014 the lines of a topographic map, every Nth one a major line (wire the Major output to give the majors a chain of their own). Wire Noise Paint \u2192 Rect \u2192 Rasterize in for terrain; animate Offset to make the lines flow.",
14200
15120
  keywords: ["contours", "contour", "topographic", "topo", "isolines", "iso", "height", "map", "terrain", "marching squares", "lines", "trace"],
14201
15121
  inputs: [{ name: "in", type: "texture", label: "In" }],
14202
- outputs: [{ name: "out", type: "shapes" }],
15122
+ outputs: [
15123
+ { name: "out", type: "shapes" },
15124
+ // Wired, the majors leave `out` for this socket, so each set can take
15125
+ // its own chain (Outline Stroke, a Glow) without a Select.
15126
+ { name: "major", type: "shapes", label: "Major", claims: true }
15127
+ ],
14203
15128
  params: [
14204
15129
  { key: "lines", label: "Lines", type: "int", default: 12, min: 1, max: 128, description: "How many levels between black and white: the lines\u2019 spacing in height." },
14205
15130
  { key: "offset", label: "Offset", type: "number", default: 0.5, step: 0.01, description: "Slides every level up the height map, in line spacings: 1 moves each line to where the next one was. Animate it for lines that flow; the majors travel with their lines." },
@@ -14221,6 +15146,7 @@ var contoursNode = {
14221
15146
  { key: "width", label: "Width", type: "number", default: 1, min: 0, step: 0.1, unit: "px", description: "A minor line\u2019s width.", group: "Lines" },
14222
15147
  { key: "majorEvery", label: "Major Every", type: "int", default: 5, min: 0, max: 32, description: "Every Nth level is a major line, drawn at Major Width and tagged major. 0: no majors.", group: "Lines" },
14223
15148
  { key: "majorWidth", label: "Major Width", type: "number", default: 3, min: 0, step: 0.1, unit: "px", group: "Lines" },
15149
+ { key: "majorStroke", label: "Major Stroke", type: "color", default: "none", description: "The major lines\u2019 colour. None: the same as Stroke.", group: "Lines" },
14224
15150
  { key: "opacity", label: "Opacity", type: "number", default: 1, min: 0, max: 1, step: 0.01, group: "Lines" }
14225
15151
  ],
14226
15152
  cook(ctx) {
@@ -14235,6 +15161,8 @@ var contoursNode = {
14235
15161
  const width = Math.max(0, ctx.num("width"));
14236
15162
  const every2 = Math.max(0, ctx.int("majorEvery"));
14237
15163
  const majorWidth = Math.max(0, ctx.num("majorWidth"));
15164
+ const majorColor = ctx.color("majorStroke");
15165
+ const majorStroke = majorColor && majorColor !== "none" ? majorColor : stroke;
14238
15166
  const opacity = ctx.num("opacity");
14239
15167
  const tx = tex.at?.x ?? (ctx.canvas.origin === "center" ? -tex.width / 2 : 0);
14240
15168
  const ty = tex.at?.y ?? (ctx.canvas.origin === "center" ? -tex.height / 2 : 0);
@@ -14244,6 +15172,8 @@ var contoursNode = {
14244
15172
  const base2 = Math.floor(offset);
14245
15173
  const frac = offset - base2;
14246
15174
  const shapes = [];
15175
+ const majors = [];
15176
+ const split = ctx.wiredOutputs.has("major");
14247
15177
  for (let k = 0; k < count; k++) {
14248
15178
  const level = (k + frac) / count;
14249
15179
  if (level <= 0 || level >= 1) continue;
@@ -14259,18 +15189,19 @@ var contoursNode = {
14259
15189
  d += `${p === 0 ? "M" : " L"} ${num22(sx(gx))} ${num22(sy(gy))}`;
14260
15190
  }
14261
15191
  if (line.closed) d += " Z";
14262
- const index = shapes.length;
14263
- shapes.push({
15192
+ const into = split && major ? majors : shapes;
15193
+ const index = into.length;
15194
+ into.push({
14264
15195
  kind: "path",
14265
15196
  d,
14266
- id: ctx.shapeId(index),
15197
+ id: ctx.shapeId(index, into === majors ? "major" : void 0),
14267
15198
  transform: IDENTITY,
14268
- style: { fill: "none", stroke, strokeWidth: major ? majorWidth : width, opacity },
15199
+ style: { fill: "none", stroke: major ? majorStroke : stroke, strokeWidth: major ? majorWidth : width, opacity },
14269
15200
  meta: ctx.meta(index, ["contour", major ? "major" : "minor", `level:${g}`])
14270
15201
  });
14271
15202
  }
14272
15203
  }
14273
- return { out: shapesVal(shapes) };
15204
+ return { out: shapesVal(shapes), major: shapesVal(split ? majors : shapes.filter((s2) => s2.meta.tags?.includes("major"))) };
14274
15205
  }
14275
15206
  };
14276
15207
  var BORDERS = {
@@ -14451,10 +15382,66 @@ var textBoxNode = {
14451
15382
  }
14452
15383
  };
14453
15384
  var notMono = (p) => normalizeParam(p.display).base !== "mono";
15385
+ var pivotIsPreset3 = (p) => (normalizeParam(p.pivot).base ?? "center") !== "custom";
15386
+ var LAYER_3D_PARAMS = [
15387
+ { key: "posX", label: "Pos. X", type: "number", default: 0, step: 1, unit: "px", group: "3D", description: "Moves the layer\u2019s plane across the frame." },
15388
+ { key: "posY", label: "Pos. Y", type: "number", default: 0, step: 1, unit: "px", group: "3D", description: "Moves the layer\u2019s plane down the frame." },
15389
+ {
15390
+ key: "posZ",
15391
+ label: "Pos. Z",
15392
+ type: "number",
15393
+ default: 0,
15394
+ step: 1,
15395
+ unit: "px",
15396
+ group: "3D",
15397
+ description: "Depth: positive moves the plane away from the camera and it draws smaller; negative brings it nearer."
15398
+ },
15399
+ {
15400
+ key: "rotX",
15401
+ label: "Rot. X",
15402
+ type: "number",
15403
+ default: 0,
15404
+ step: 1,
15405
+ unit: "deg",
15406
+ group: "3D",
15407
+ description: "Tilts the plane about a horizontal line through the pivot; positive leans the top away."
15408
+ },
15409
+ {
15410
+ key: "rotY",
15411
+ label: "Rot. Y",
15412
+ type: "number",
15413
+ default: 0,
15414
+ step: 1,
15415
+ unit: "deg",
15416
+ group: "3D",
15417
+ description: "Turns the plane about a vertical line through the pivot; positive turns the right edge away."
15418
+ },
15419
+ { key: "rotZ", label: "Rot. Z", type: "number", default: 0, step: 1, unit: "deg", group: "3D", description: "Spins the plane in its own surface, clockwise on screen." },
15420
+ { key: "scaleX", label: "Scale X", type: "number", default: 100, step: 1, unit: "%", group: "3D" },
15421
+ { key: "scaleY", label: "Scale Y", type: "number", default: 100, step: 1, unit: "%", group: "3D" },
15422
+ {
15423
+ key: "pivot",
15424
+ label: "Pivot",
15425
+ type: "enum",
15426
+ default: "center",
15427
+ group: "3D",
15428
+ options: [
15429
+ { value: "center", label: "Frame centre" },
15430
+ { value: "canvas", label: "Canvas origin" },
15431
+ { value: "shape", label: "Content centre" },
15432
+ { value: "custom", label: "Custom point" }
15433
+ ],
15434
+ description: "What the rotations and scale turn about: where the camera looks, the document\u2019s (0,0), the centre of what the layer holds, or a point of your own."
15435
+ },
15436
+ { key: "pivotX", label: "Pivot X", type: "number", default: 0, step: 0.5, unit: "px", group: "3D", dimWhen: pivotIsPreset3 },
15437
+ { key: "pivotY", label: "Pivot Y", type: "number", default: 0, step: 0.5, unit: "px", group: "3D", dimWhen: pivotIsPreset3 }
15438
+ ];
15439
+ var gathered = /* @__PURE__ */ new Map();
14454
15440
  var layerNode = {
14455
15441
  type: "layer",
14456
15442
  label: "Layer",
14457
15443
  category: "output",
15444
+ description: 'A composition of its own with its own page and clock: wire shapes (and one post chain) into "in", the Layer into the Canvas. Order stacks layers; the 3D group poses the whole layer as a plane under the camera.',
14458
15445
  inputs: [
14459
15446
  // Multi: a layer gathers streams directly — no obligatory Merge in front.
14460
15447
  // The one socket takes a texture too: the way pixels re-enter the scene.
@@ -14497,7 +15484,8 @@ var layerNode = {
14497
15484
  exposable: false,
14498
15485
  inspectorOnly: true,
14499
15486
  description: "Wrap the layer\u2019s clock at its Duration instead of switching off."
14500
- }
15487
+ },
15488
+ ...LAYER_3D_PARAMS
14501
15489
  ],
14502
15490
  cook(ctx) {
14503
15491
  const layer = {
@@ -14508,13 +15496,41 @@ var layerNode = {
14508
15496
  order: ctx.int("order"),
14509
15497
  shapes: []
14510
15498
  };
14511
- for (const v of ctx.inputList("in")) {
14512
- if (v.t === "shapes") layer.shapes.push(...v.v);
15499
+ const inputs = ctx.inputList("in");
15500
+ const shapes = [];
15501
+ for (const v of inputs) {
15502
+ if (v.t === "shapes") shapes.push(...v.v);
14513
15503
  else if (v.t === "texture" && !layer.texture) {
14514
15504
  layer.texture = v.v;
14515
- if (layer.shapes.length > 0) layer.textureAt = layer.shapes.length;
15505
+ if (shapes.length > 0) layer.textureAt = shapes.length;
14516
15506
  }
14517
15507
  }
15508
+ const key = inputs.map((v) => v.t === "shapes" ? identityOf(v.v) : "-").join(",");
15509
+ const kept = gathered.get(ctx.nodeId);
15510
+ if (kept && kept.key === key) layer.shapes = kept.shapes;
15511
+ else {
15512
+ layer.shapes = shapes;
15513
+ gathered.set(ctx.nodeId, { key, shapes });
15514
+ }
15515
+ const camera = { perspective: 0, width: ctx.canvas.w, height: ctx.canvas.h, origin: ctx.canvas.origin };
15516
+ const pose = {
15517
+ x: ctx.num("posX"),
15518
+ y: ctx.num("posY"),
15519
+ z: ctx.num("posZ"),
15520
+ rx: ctx.num("rotX"),
15521
+ ry: ctx.num("rotY"),
15522
+ rz: ctx.num("rotZ"),
15523
+ sx: ctx.num("scaleX") / 100,
15524
+ sy: ctx.num("scaleY") / 100,
15525
+ pivot: { x: 0, y: 0 }
15526
+ };
15527
+ if (!isIdentityPose(pose)) {
15528
+ pose.pivot = layerPivot(ctx.enumOf("pivot", ["center", "canvas", "shape", "custom"]), layer, camera, {
15529
+ x: ctx.num("pivotX"),
15530
+ y: ctx.num("pivotY")
15531
+ });
15532
+ layer.pose = pose;
15533
+ }
14518
15534
  return { out: layersVal([layer]) };
14519
15535
  }
14520
15536
  };
@@ -14546,6 +15562,18 @@ var canvasNode = {
14546
15562
  ],
14547
15563
  description: "Centre puts (0,0) mid-frame \u2014 HUD maths without the offsets."
14548
15564
  },
15565
+ // The fixed camera, for layers posed in 3D: it looks at the frame
15566
+ // centre from this far in front. CSS's `perspective`, AE's zoom.
15567
+ {
15568
+ key: "perspective",
15569
+ label: "Perspective",
15570
+ type: "number",
15571
+ default: 1500,
15572
+ min: 0,
15573
+ step: 10,
15574
+ unit: "px",
15575
+ description: "The camera\u2019s distance from the frame, for layers posed in 3D: smaller is a wider lens and stronger foreshortening; 0 is orthographic, no foreshortening at all."
15576
+ },
14549
15577
  // How the pixels are made. A small LCD has no anti-aliasing and, as
14550
15578
  // often as not, one colour beside its background; the preview and every
14551
15579
  // export read this, so the artboard shows what ships.
@@ -14574,6 +15602,12 @@ var canvasNode = {
14574
15602
  dimWhen: notMono
14575
15603
  }
14576
15604
  ],
15605
+ // The pictures the raster worker makes of posed layers' picture runs,
15606
+ // under the runs' ids: served on the main thread, where the cook below
15607
+ // has no rasteriser (it projects the vectors itself).
15608
+ standIns: (_inputs, multi) => (multi.in ?? []).flatMap(
15609
+ (v) => v.t === "layers" ? v.v.filter((l) => l.pose && l.visible).flatMap((l) => layerPieces(l).filter((p) => p.kind === "picture").map((p) => p.id)) : []
15610
+ ),
14577
15611
  cook(ctx) {
14578
15612
  const layers = [];
14579
15613
  for (const value of ctx.inputList("in")) {
@@ -14602,14 +15636,19 @@ var canvasNode = {
14602
15636
  const sorted = layers.map((l, i) => ({ l, i })).sort((a, b) => a.l.order - b.l.order || a.i - b.i).map(({ l }) => l);
14603
15637
  const mode = ctx.enumOf("display", ["vector", "pixel", "mono"]);
14604
15638
  const display = mode === "vector" ? void 0 : { mode, ink: ctx.color("ink"), threshold: Math.min(1, Math.max(0, ctx.num("threshold"))) };
15639
+ const width = ctx.int("width");
15640
+ const height = ctx.int("height");
15641
+ const origin = ctx.enumOf("origin", ["top-left", "center"]);
15642
+ const camera = { perspective: Math.max(0, ctx.num("perspective")), width, height, origin };
15643
+ const posed = sorted.flatMap((l) => l.pose ? projectLayer(l, camera, ctx, display !== void 0) : [l]);
14605
15644
  return {
14606
15645
  out: sceneVal({
14607
- width: ctx.int("width"),
14608
- height: ctx.int("height"),
15646
+ width,
15647
+ height,
14609
15648
  background: ctx.color("bg"),
14610
- origin: ctx.enumOf("origin", ["top-left", "center"]),
15649
+ origin,
14611
15650
  ...display ? { display } : {},
14612
- layers: sorted
15651
+ layers: posed
14613
15652
  })
14614
15653
  };
14615
15654
  }
@@ -14766,6 +15805,888 @@ var ografNode = {
14766
15805
  return out;
14767
15806
  }
14768
15807
  };
15808
+ function graph() {
15809
+ const nodes = [];
15810
+ const edges = [];
15811
+ const node = (id, type, col, row, params = {}, name) => {
15812
+ const full = type === "text" ? { vAnchor: "baseline", ...params } : params;
15813
+ nodes.push({ id, type, params: full, position: { x: col * 260, y: row * 200 }, ...name ? { name } : {} });
15814
+ return id;
15815
+ };
15816
+ const wire = (from, to, toPort, fromPort = "out") => {
15817
+ edges.push({ id: `e${edges.length + 1}`, from, fromPort, to, toPort });
15818
+ };
15819
+ return { nodes, edges, node, wire };
15820
+ }
15821
+ function glitchRelay() {
15822
+ const { nodes, edges, node, wire } = graph();
15823
+ node("clock", "time", 0, 0);
15824
+ node("degrees", "math", 1, 0, { op: "multiply", a: 0, b: 90 });
15825
+ node("ring", "tick-ring", 0, 1, {
15826
+ cx: 0,
15827
+ cy: 0,
15828
+ radius: 210,
15829
+ count: 60,
15830
+ length: 8,
15831
+ majorEvery: 5,
15832
+ majorLength: 18,
15833
+ majorWidth: 2,
15834
+ align: "inward",
15835
+ stroke: "#7fe8d0",
15836
+ strokeWidth: 1
15837
+ });
15838
+ node("spin", "transform", 1, 1, { pivot: "custom", pivotX: 0, pivotY: 0 });
15839
+ node("hub", "crosshair", 1, 2, { cx: 0, cy: 0, size: 26, gap: 7, stroke: "#5d7285" });
15840
+ node("dialMerge", "merge", 2, 1);
15841
+ node("dial", "layer", 3, 1, { order: 1 }, "Dial");
15842
+ wire("clock", "degrees", "a");
15843
+ wire("degrees", "spin", "rotation");
15844
+ wire("ring", "spin", "in");
15845
+ wire("spin", "dialMerge", "in");
15846
+ wire("hub", "dialMerge", "in");
15847
+ wire("dialMerge", "dial", "in");
15848
+ node("title", "text", 0, 4, { text: "SYS//RELAY 07", x: -90, y: -68, fontSize: 13, fill: "#dbe7f0", fontFamily: "IBM Plex Mono", anchor: "start" });
15849
+ node("code", "random-code", 0, 3, { count: 9, x: -90, y: -50, stepY: 14, fontSize: 11, fill: "#9fd4e8" });
15850
+ node("readMerge", "merge", 1, 3);
15851
+ node("pixels", "rasterize", 2, 3);
15852
+ node("tear", "glitch", 3, 3, { amount: 16, bands: 30, probability: 0.35, rate: 10, channelShift: 4 });
15853
+ node("fringe", "chromatic", 4, 3, { offset: 1.5 });
15854
+ node("crt", "scanlines", 5, 3, { spacing: 3, thickness: 1, strength: 0.3, speed: 24 });
15855
+ node("readout", "layer", 6, 3, { order: 2 }, "Readout");
15856
+ wire("code", "readMerge", "in");
15857
+ wire("title", "readMerge", "in");
15858
+ wire("readMerge", "pixels", "in");
15859
+ wire("pixels", "tear", "in");
15860
+ wire("tear", "fringe", "in");
15861
+ wire("fringe", "crt", "in");
15862
+ wire("crt", "readout", "in");
15863
+ node("out", "canvas", 7, 1, { width: 600, height: 600, bg: "#04070a", origin: "center" });
15864
+ wire("dial", "out", "in");
15865
+ wire("readout", "out", "in");
15866
+ return Project.parse({
15867
+ name: "glitch relay",
15868
+ seed: "relay-07",
15869
+ canvas: { w: 600, h: 600, bg: "#04070a", origin: "center" },
15870
+ timeline: { fps: 30, duration: 2 },
15871
+ graph: assignLayers({ nodes, edges })
15872
+ });
15873
+ }
15874
+ function warningStrip() {
15875
+ const { nodes, edges, node, wire } = graph();
15876
+ node("clock", "time", 0, 0);
15877
+ node("crawl", "math", 1, 0, { op: "multiply", a: 0, b: 40 });
15878
+ wire("clock", "crawl", "a");
15879
+ node("hazard", "stripes", 1, 1, { x: 0, y: -60, w: 420, h: 28, stripe: 14, gap: 14, lean: 30, fill: "#ffb547" });
15880
+ node("frame", "rect", 1, 2, { x: 0, y: -60, w: 420, h: 28, stroke: "#ffb547", strokeWidth: 1 });
15881
+ node("chevrons", "stripes", 1, 3, { x: 0, y: 40, w: 420, h: 40, pattern: "chevron", stripe: 18, gap: 22, point: 90, fill: "#7fe8d0" });
15882
+ node("fade", "stagger", 2, 3, { property: "opacity", from: 0.25, to: 1 });
15883
+ wire("crawl", "hazard", "offset");
15884
+ wire("crawl", "chevrons", "offset");
15885
+ wire("chevrons", "fade", "in");
15886
+ node("label", "text", 1, 4, { text: "CAUTION // LIVE FEED", x: -210, y: 110, fontSize: 12, fill: "#dbe7f0", fontFamily: "IBM Plex Mono", anchor: "start" });
15887
+ node("all", "merge", 3, 2);
15888
+ wire("hazard", "all", "in");
15889
+ wire("frame", "all", "in");
15890
+ wire("fade", "all", "in");
15891
+ wire("label", "all", "in");
15892
+ node("strip", "layer", 4, 2, { order: 1 }, "Strip");
15893
+ wire("all", "strip", "in");
15894
+ node("out", "canvas", 5, 2, { width: 600, height: 300, bg: "#04070a", origin: "center" });
15895
+ wire("strip", "out", "in");
15896
+ return Project.parse({
15897
+ name: "warning strip",
15898
+ seed: "caution-01",
15899
+ canvas: { w: 600, h: 300, bg: "#04070a", origin: "center" },
15900
+ timeline: { fps: 30, duration: 4 },
15901
+ graph: assignLayers({ nodes, edges })
15902
+ });
15903
+ }
15904
+ function panelSplit() {
15905
+ const { nodes, edges, node, wire } = graph();
15906
+ node("frameA", "rect", 0, 0, { x: -220, y: -260, w: 440, h: 230, anchor: "corner", stroke: "#8fd4e8", strokeWidth: 1 });
15907
+ node("header", "subdivide", 1, 0, { axis: "h", count: 2, ratio: 38 });
15908
+ node("lower", "select", 2, 0, { mode: "range", from: 1, to: 1 });
15909
+ node("columns", "subdivide", 3, 0, { axis: "v", count: 3, gap: 8 });
15910
+ node("recipe", "merge", 4, 0);
15911
+ wire("frameA", "header", "in");
15912
+ wire("header", "lower", "in");
15913
+ wire("lower", "columns", "in");
15914
+ wire("lower", "recipe", "in", "rest");
15915
+ wire("columns", "recipe", "in");
15916
+ node("frameB", "rect", 0, 2, { x: -220, y: 30, w: 440, h: 230, anchor: "corner", stroke: "#e8f4ff", strokeWidth: 1 });
15917
+ node("breath", "lfo", 0, 3, { rate: 0.25, amplitude: 12, center: 50 });
15918
+ node("mosaic", "subdivide", 1, 2, { mode: "recursive", depth: 4, rule: "alternate", variation: 0.4, stop: 0.25, gap: 10, salt: 3 });
15919
+ node("fade", "stagger", 2, 2, { property: "opacity", from: 0.3, to: 1 });
15920
+ wire("frameB", "mosaic", "in");
15921
+ wire("breath", "mosaic", "ratio");
15922
+ wire("mosaic", "fade", "in");
15923
+ node("all", "merge", 5, 1);
15924
+ wire("recipe", "all", "in");
15925
+ wire("fade", "all", "in");
15926
+ wire("mosaic", "all", "in", "lines");
15927
+ node("panels", "layer", 6, 1, { order: 1 }, "Panels");
15928
+ wire("all", "panels", "in");
15929
+ node("out", "canvas", 7, 1, { width: 600, height: 600, bg: "#04070a", origin: "center" });
15930
+ wire("panels", "out", "in");
15931
+ return Project.parse({
15932
+ name: "panel split",
15933
+ seed: "panels-01",
15934
+ canvas: { w: 600, h: 600, bg: "#04070a", origin: "center" },
15935
+ timeline: { fps: 30, duration: 4 },
15936
+ graph: assignLayers({ nodes, edges })
15937
+ });
15938
+ }
15939
+ function subdividerStudy() {
15940
+ const { nodes, edges, node, wire } = graph();
15941
+ const ink = "#aab4bf";
15942
+ const frame = (id, col, row, x, y, w, h) => node(id, "rect", col, row, { x, y, w, h, anchor: "corner", stroke: ink, strokeWidth: 1 });
15943
+ frame("f1", 0, 0, 40, 40, 300, 160);
15944
+ node("f1cut", "subdivide", 1, 0, { axis: "h", count: 2, ratio: 48 });
15945
+ node("f1low", "select", 2, 0, { mode: "range", from: 1, to: 1 });
15946
+ node("f1cols", "subdivide", 3, 0, { axis: "v", count: 3, ratios: "30 42 28" });
15947
+ node("f1all", "merge", 4, 0);
15948
+ wire("f1", "f1cut", "in");
15949
+ wire("f1cut", "f1low", "in");
15950
+ wire("f1low", "f1cols", "in");
15951
+ wire("f1low", "f1all", "in", "rest");
15952
+ wire("f1cols", "f1all", "in");
15953
+ frame("f2", 0, 1, 400, 40, 300, 160);
15954
+ node("f2cut", "subdivide", 1, 1, { axis: "h", count: 2, ratio: 70 });
15955
+ node("f2low", "select", 2, 1, { mode: "range", from: 1, to: 1 });
15956
+ node("f2cols", "subdivide", 3, 1, { axis: "v", count: 3, ratios: "30 40 30" });
15957
+ node("f2all", "merge", 4, 1);
15958
+ wire("f2", "f2cut", "in");
15959
+ wire("f2cut", "f2low", "in");
15960
+ wire("f2low", "f2cols", "in");
15961
+ wire("f2low", "f2all", "in", "rest");
15962
+ wire("f2cols", "f2all", "in");
15963
+ frame("f3", 0, 2, 40, 240, 300, 160);
15964
+ node("f3side", "subdivide", 1, 2, { axis: "v", count: 2, ratio: 19 });
15965
+ node("f3foot", "subdivide", 2, 2, { axis: "h", count: 2, ratio: 71 });
15966
+ wire("f3", "f3side", "in");
15967
+ wire("f3side", "f3foot", "in");
15968
+ frame("f4", 0, 3, 400, 240, 150, 160);
15969
+ node("f4cut", "subdivide", 1, 3, { axis: "v", count: 2, ratio: 50 });
15970
+ node("f4right", "select", 2, 3, { mode: "range", from: 1, to: 1 });
15971
+ node("f4strip", "subdivide", 3, 3, { axis: "h", count: 2, ratio: 82 });
15972
+ node("f4all", "merge", 4, 3);
15973
+ wire("f4", "f4cut", "in");
15974
+ wire("f4cut", "f4right", "in");
15975
+ wire("f4right", "f4strip", "in");
15976
+ wire("f4right", "f4all", "in", "rest");
15977
+ wire("f4strip", "f4all", "in");
15978
+ node("sheet", "merge", 5, 1);
15979
+ wire("f1all", "sheet", "in");
15980
+ wire("f2all", "sheet", "in");
15981
+ wire("f3foot", "sheet", "in");
15982
+ wire("f4all", "sheet", "in");
15983
+ node("panels", "layer", 6, 1, { order: 1 }, "Panels");
15984
+ wire("sheet", "panels", "in");
15985
+ node("out", "canvas", 7, 1, { width: 740, height: 440, bg: "#21252b", origin: "top-left" });
15986
+ wire("panels", "out", "in");
15987
+ return Project.parse({
15988
+ name: "subdivider study",
15989
+ seed: "sheet-01",
15990
+ canvas: { w: 740, h: 440, bg: "#21252b", origin: "top-left" },
15991
+ timeline: { fps: 30, duration: 2 },
15992
+ graph: assignLayers({ nodes, edges })
15993
+ });
15994
+ }
15995
+ function litPanels() {
15996
+ const { nodes, edges, node, wire } = graph();
15997
+ node("tokens", "style-tokens", 2, 2, {
15998
+ wear: true,
15999
+ ink: "#d8e6ee",
16000
+ dim: "#4e6a7a",
16001
+ faint: "#22323d",
16002
+ accent: "#5fd3ff",
16003
+ warn: "#ff7a45",
16004
+ bg: "#05090d",
16005
+ stroke: 1,
16006
+ bold: 2,
16007
+ mode: "random",
16008
+ dealInk: true,
16009
+ dealWarn: true,
16010
+ skip: 0.55,
16011
+ salt: 5
16012
+ });
16013
+ node("frame", "rect", 0, 2, { x: -260, y: -160, w: 520, h: 320, anchor: "corner", stroke: "#4e6a7a", strokeWidth: 1 });
16014
+ node("mosaic", "subdivide", 1, 2, { mode: "recursive", depth: 5, rule: "alternate", variation: 0.45, stop: 0.3, gap: 6, minSize: 18, salt: 4 });
16015
+ wire("frame", "mosaic", "in");
16016
+ wire("mosaic", "tokens", "in");
16017
+ node("rules", "style", 2, 3, { setStroke: true, setWidth: true, strokeWidth: 1 });
16018
+ wire("mosaic", "rules", "in", "lines");
16019
+ wire("tokens", "rules", "stroke", "faint");
16020
+ node("fade", "stagger", 3, 2, { property: "opacity", from: 0.4, to: 1 });
16021
+ wire("tokens", "fade", "in");
16022
+ node("label", "text", 2, 4, { text: "SECTOR MAP // 04", x: -260, y: 190, fontSize: 12, fontFamily: "IBM Plex Mono", anchor: "start" });
16023
+ wire("tokens", "label", "fill", "ink");
16024
+ node("all", "merge", 4, 2);
16025
+ wire("fade", "all", "in");
16026
+ wire("rules", "all", "in");
16027
+ wire("label", "all", "in");
16028
+ node("board", "layer", 5, 2, { order: 1 }, "Board");
16029
+ wire("all", "board", "in");
16030
+ node("out", "canvas", 6, 2, { width: 640, height: 440, origin: "center" });
16031
+ wire("tokens", "out", "bg", "bg");
16032
+ wire("board", "out", "in");
16033
+ return Project.parse({
16034
+ name: "lit panels",
16035
+ seed: "sector-04",
16036
+ canvas: { w: 640, h: 440, bg: "#05090d", origin: "center" },
16037
+ timeline: { fps: 30, duration: 3 },
16038
+ graph: assignLayers({ nodes, edges })
16039
+ });
16040
+ }
16041
+ function kiosk() {
16042
+ const { nodes, edges, node, wire } = graph();
16043
+ node("tokens", "style-tokens", 2, 2, {
16044
+ wear: true,
16045
+ ink: "#f3ead3",
16046
+ dim: "#1e2a5a",
16047
+ faint: "#8a1e22",
16048
+ accent: "#2eb1c4",
16049
+ warn: "#f0a030",
16050
+ bg: "#d7262c",
16051
+ stroke: 1,
16052
+ bold: 2,
16053
+ mode: "cycle",
16054
+ dealInk: true,
16055
+ dealDim: true,
16056
+ dealFaint: false,
16057
+ dealAccent: true,
16058
+ dealWarn: true
16059
+ });
16060
+ node("word", "text", 0, 2, { text: "kiosk", fontFamily: "Anton SC", fontSize: 190, x: 0, y: 70, anchor: "middle", fill: "#f3ead3", outlines: true, letterSpacing: -4 });
16061
+ node("bands", "offset", 1, 2, { distance: 11, count: 6, join: "round", keepStyle: true });
16062
+ wire("word", "bands", "in");
16063
+ wire("bands", "tokens", "in");
16064
+ node("all", "merge", 3, 2);
16065
+ wire("tokens", "all", "in");
16066
+ wire("bands", "all", "in", "source");
16067
+ node("poster", "layer", 4, 2, { order: 1 }, "Poster");
16068
+ wire("all", "poster", "in");
16069
+ node("out", "canvas", 5, 2, { width: 640, height: 480, origin: "center" });
16070
+ wire("tokens", "out", "bg", "bg");
16071
+ wire("poster", "out", "in");
16072
+ return Project.parse({
16073
+ name: "kiosk",
16074
+ seed: "kiosk-01",
16075
+ canvas: { w: 640, h: 480, bg: "#d7262c", origin: "center" },
16076
+ timeline: { fps: 30, duration: 2 },
16077
+ graph: assignLayers({ nodes, edges })
16078
+ });
16079
+ }
16080
+ function ionDial() {
16081
+ const { nodes, edges, node, wire } = graph();
16082
+ node("tokens", "style-tokens", 0, 0, {
16083
+ wear: true,
16084
+ ink: "#e8dcc8",
16085
+ dim: "#8a7a5c",
16086
+ faint: "#453a24",
16087
+ accent: "#ffb547",
16088
+ warn: "#ff5b2e",
16089
+ bg: "#0a0703",
16090
+ stroke: 1,
16091
+ bold: 2.5
16092
+ });
16093
+ node("ring", "tick-ring", 1, 1, {
16094
+ cx: 0,
16095
+ cy: 0,
16096
+ radius: 215,
16097
+ count: 72,
16098
+ length: 7,
16099
+ majorEvery: 6,
16100
+ majorLength: 15,
16101
+ majorWidth: 1.8,
16102
+ align: "inward"
16103
+ });
16104
+ node("gauge", "circle", 1, 2, { cx: 0, cy: 0, r: 182, start: 220, sweep: 280 });
16105
+ node("redline", "circle", 1, 3, { cx: 0, cy: 0, r: 182, start: 140, sweep: 28 });
16106
+ node("labels", "radial-labels", 2, 1, {
16107
+ cx: 0,
16108
+ cy: 0,
16109
+ radius: 154,
16110
+ count: 8,
16111
+ start: 220,
16112
+ sweep: 280,
16113
+ from: 0,
16114
+ to: 700,
16115
+ fontSize: 11
16116
+ });
16117
+ node("readout", "text", 2, 2, { text: "PWR // BUS A", x: 0, y: 72, fontSize: 13, anchor: "middle", fontFamily: "IBM Plex Mono" });
16118
+ node("hub", "crosshair", 2, 3, { cx: 0, cy: 0, size: 24, gap: 8 });
16119
+ node("merged", "merge", 3, 1);
16120
+ node("main", "layer", 4, 1, {}, "Dial");
16121
+ node("out", "canvas", 5, 1, { width: 600, height: 600, origin: "center" });
16122
+ for (const id of ["ring", "gauge", "redline", "labels", "readout", "hub"]) wire(id, "merged", "in");
16123
+ wire("merged", "main", "in");
16124
+ wire("main", "out", "in");
16125
+ wire("tokens", "ring", "stroke", "dim");
16126
+ wire("tokens", "gauge", "stroke", "accent");
16127
+ wire("tokens", "gauge", "strokeWidth", "bold");
16128
+ wire("tokens", "redline", "stroke", "warn");
16129
+ wire("tokens", "redline", "strokeWidth", "bold");
16130
+ wire("tokens", "labels", "fill", "ink");
16131
+ wire("tokens", "readout", "fill", "ink");
16132
+ wire("tokens", "hub", "stroke", "faint");
16133
+ wire("tokens", "out", "bg", "bg");
16134
+ return Project.parse({
16135
+ name: "ion dial",
16136
+ seed: "ion-9",
16137
+ canvas: { w: 600, h: 600, bg: "#0a0703", origin: "center" },
16138
+ graph: assignLayers({ nodes, edges })
16139
+ });
16140
+ }
16141
+ function radar() {
16142
+ const { nodes, edges, node, wire } = graph();
16143
+ node("scope", "polar-grid", 0, 1, {
16144
+ cx: 0,
16145
+ cy: 0,
16146
+ radius: 224,
16147
+ innerRadius: 28,
16148
+ rings: 4,
16149
+ spokes: 12,
16150
+ subdivisions: 2,
16151
+ stroke: "#2c3e4a",
16152
+ strokeWidth: 1,
16153
+ subOpacity: 0.4
16154
+ });
16155
+ node("hub", "crosshair", 0, 2, { cx: 0, cy: 0, size: 20, gap: 6, stroke: "#5d7285" });
16156
+ node("edge", "line", 0, 3, { x1: 28, y1: 0, x2: 224, y2: 0, stroke: "#7fe8d0", strokeWidth: 1.6 });
16157
+ node("trail", "circle", 0, 4, { cx: 0, cy: 0, r: 222, start: 64, sweep: 26, stroke: "#7fe8d0", strokeWidth: 2, opacity: 0.5 });
16158
+ node("clock", "time", 1, 0);
16159
+ node("rate", "math", 2, 0, { op: "multiply", a: 0, b: 120 });
16160
+ node("sweepMerge", "merge", 1, 3);
16161
+ node("spin", "transform", 2, 3, { pivot: "custom", pivotX: 0, pivotY: 0 });
16162
+ node("blip", "circle", 1, 5, { cx: 0, cy: 0, r: 2.5, fill: "#7fe8d0", stroke: "none" });
16163
+ node("field", "scatter", 2, 5, { mode: "disc", count: 14, x: 0, y: 0, radius: 205 });
16164
+ node("scopeMerge", "merge", 1, 1);
16165
+ node("contactMerge", "merge", 3, 3);
16166
+ node("scopeLayer", "layer", 3, 1, { order: 1 }, "Scope");
16167
+ node("sweepLayer", "layer", 4, 3, { order: 2 }, "Sweep");
16168
+ node("out", "canvas", 5, 1, { width: 600, height: 600, bg: "#04070a", origin: "center" });
16169
+ wire("clock", "rate", "a");
16170
+ wire("rate", "spin", "rotation");
16171
+ wire("edge", "sweepMerge", "in");
16172
+ wire("trail", "sweepMerge", "in");
16173
+ wire("sweepMerge", "spin", "in");
16174
+ wire("blip", "field", "in");
16175
+ wire("scope", "scopeMerge", "in");
16176
+ wire("hub", "scopeMerge", "in");
16177
+ wire("scopeMerge", "scopeLayer", "in");
16178
+ wire("spin", "contactMerge", "in");
16179
+ wire("field", "contactMerge", "in");
16180
+ wire("contactMerge", "sweepLayer", "in");
16181
+ wire("scopeLayer", "out", "in");
16182
+ wire("sweepLayer", "out", "in");
16183
+ return Project.parse({
16184
+ name: "radar",
16185
+ seed: "contact-4",
16186
+ canvas: { w: 600, h: 600, bg: "#04070a", origin: "center" },
16187
+ // 120°/s over 3s = one full turn: the loop seam is invisible.
16188
+ timeline: { fps: 30, duration: 3 },
16189
+ graph: assignLayers({ nodes, edges })
16190
+ });
16191
+ }
16192
+ function telemetry() {
16193
+ const { nodes, edges, node, wire } = graph();
16194
+ node("paper", "grid", 0, 1, {
16195
+ x: 0,
16196
+ y: 0,
16197
+ w: 520,
16198
+ h: 520,
16199
+ cols: 6,
16200
+ rows: 6,
16201
+ subdivisions: 4,
16202
+ stroke: "#22303c",
16203
+ strokeWidth: 1,
16204
+ subOpacity: 0.35
16205
+ });
16206
+ node("title", "text", 0, 2, { text: "TELEMETRY // LINK 02", x: -252, y: -232, fontSize: 13, fill: "#dbe7f0", fontFamily: "IBM Plex Mono", anchor: "start" });
16207
+ node("code", "random-code", 0, 3, { count: 11, x: -252, y: -200, stepY: 15, fontSize: 10, fill: "#5d7285" });
16208
+ node("breath", "lfo", 1, 0, { shape: "sine", rate: 0.33, amplitude: 0.35, center: 0.55 });
16209
+ node("rx", "bar-meter", 2, 1, { x: 124, y: -197, count: 26, segW: 5, segH: 13, gap: 3, fillColor: "#7fe8d0", emptyColor: "#22303c" });
16210
+ node("tx", "bar-meter", 2, 2, { x: 124, y: -169, count: 26, value: 0.3, segW: 5, segH: 13, gap: 3, fillColor: "#9fd4e8", emptyColor: "#22303c" });
16211
+ node("rxLabel", "text", 1, 1, { text: "RX", x: -4, y: -193, fontSize: 10, fill: "#8a9aa8", fontFamily: "IBM Plex Mono", anchor: "start" });
16212
+ node("txLabel", "text", 1, 2, { text: "TX", x: -4, y: -165, fontSize: 10, fill: "#8a9aa8", fontFamily: "IBM Plex Mono", anchor: "start" });
16213
+ node("ruler", "tick-row", 0, 4, {
16214
+ x: 0,
16215
+ y: 236,
16216
+ span: 504,
16217
+ count: 43,
16218
+ length: 6,
16219
+ majorEvery: 6,
16220
+ majorLength: 12,
16221
+ majorWidth: 1.5,
16222
+ stroke: "#5d7285"
16223
+ });
16224
+ node("merged", "merge", 3, 1);
16225
+ node("main", "layer", 4, 1, {}, "Panel");
16226
+ node("out", "canvas", 5, 1, { width: 600, height: 600, bg: "#04070a", origin: "center" });
16227
+ wire("breath", "rx", "value");
16228
+ for (const id of ["paper", "title", "code", "rx", "tx", "rxLabel", "txLabel", "ruler"]) wire(id, "merged", "in");
16229
+ wire("merged", "main", "in");
16230
+ wire("main", "out", "in");
16231
+ return Project.parse({
16232
+ name: "telemetry",
16233
+ seed: "link-02",
16234
+ canvas: { w: 600, h: 600, bg: "#04070a", origin: "center" },
16235
+ timeline: { fps: 30, duration: 3 },
16236
+ graph: assignLayers({ nodes, edges })
16237
+ });
16238
+ }
16239
+ function bootSequence() {
16240
+ const { nodes, edges, node, wire } = graph();
16241
+ node("boot", "trigger", 0, 0, {
16242
+ value: { base: 1, track: { keyframes: [{ t: 0.2, value: 1, easing: "step" }] } }
16243
+ });
16244
+ node("frameSeq", "clip", 1, 0, {
16245
+ keys: { base: 0, track: { keyframes: [
16246
+ { t: 0, value: 0, easing: "ease-in-out" },
16247
+ { t: 1.4, value: 1, easing: "ease-in-out" }
16248
+ ] } },
16249
+ duration: 1.4
16250
+ });
16251
+ node("textSeq", "clip", 2, 0, {
16252
+ keys: { base: 0, track: { keyframes: [
16253
+ { t: 0, value: 0, easing: "linear" },
16254
+ { t: 0.5, value: 0, easing: "linear" },
16255
+ { t: 2.2, value: 1, easing: "linear" }
16256
+ ] } },
16257
+ duration: 2.2
16258
+ });
16259
+ node("meterSeq", "clip", 3, 0, {
16260
+ keys: { base: 0, track: { keyframes: [
16261
+ { t: 0, value: 0, easing: "linear" },
16262
+ { t: 0.9, value: 0, easing: "ease-in-out" },
16263
+ { t: 2.4, value: 1, easing: "ease-in-out" }
16264
+ ] } },
16265
+ duration: 2.4
16266
+ });
16267
+ wire("boot", "frameSeq", "play");
16268
+ wire("boot", "textSeq", "play");
16269
+ wire("boot", "meterSeq", "play");
16270
+ node("brackets", "corner-brackets", 0, 1, { x: 0, y: 0, w: 470, h: 470, armX: 34, armY: 34, stroke: "#7fe8d0", strokeWidth: 1.5 });
16271
+ node("ring", "tick-ring", 0, 2, {
16272
+ cx: 0,
16273
+ cy: 0,
16274
+ radius: 200,
16275
+ count: 72,
16276
+ length: 6,
16277
+ majorEvery: 6,
16278
+ majorLength: 13,
16279
+ majorWidth: 1.5,
16280
+ align: "inward",
16281
+ stroke: "#5d7285"
16282
+ });
16283
+ node("hub", "crosshair", 0, 3, { cx: 0, cy: 0, size: 22, gap: 7, stroke: "#5d7285" });
16284
+ node("frameMerge", "merge", 1, 2);
16285
+ node("frameReveal", "reveal", 2, 2, { mode: "draw", stagger: 0.5 });
16286
+ node("frameLayer", "layer", 4, 2, { order: 1 }, "Frame");
16287
+ for (const id of ["brackets", "ring", "hub"]) wire(id, "frameMerge", "in");
16288
+ wire("frameMerge", "frameReveal", "in");
16289
+ wire("frameReveal", "frameLayer", "in");
16290
+ wire("frameSeq", "frameReveal", "progress");
16291
+ node("title", "text", 0, 4, { text: "BOOT SEQUENCE 09", x: -205, y: -205, fontSize: 14, letterSpacing: 2, fill: "#dbe7f0", fontFamily: "IBM Plex Mono", anchor: "start" });
16292
+ node("sub", "text", 0, 5, { text: "CORE SYSTEMS // ONLINE", x: -205, y: -184, fontSize: 10, fill: "#8a9aa8", fontFamily: "IBM Plex Mono", anchor: "start" });
16293
+ node("code", "random-code", 1, 4, { count: 8, x: -205, y: 128, stepY: 14, fontSize: 10, fill: "#9fd4e8" });
16294
+ node("meter", "bar-meter", 1, 5, { x: 60, y: -210, count: 22, value: 0.72, segW: 5, segH: 12, gap: 3, fillColor: "#7fe8d0", emptyColor: "#22303c" });
16295
+ node("codeReveal", "reveal", 2, 4, { mode: "wipe", angle: 90, stagger: 0.75 });
16296
+ node("meterReveal", "reveal", 2, 5, { mode: "wipe", angle: 0, stagger: 0.85 });
16297
+ node("readMerge", "merge", 3, 4);
16298
+ node("readLayer", "layer", 4, 4, { order: 2 }, "Readout");
16299
+ wire("textSeq", "title", "reveal");
16300
+ wire("textSeq", "sub", "reveal");
16301
+ wire("code", "codeReveal", "in");
16302
+ wire("meter", "meterReveal", "in");
16303
+ wire("meterSeq", "codeReveal", "progress");
16304
+ wire("meterSeq", "meterReveal", "progress");
16305
+ for (const id of ["title", "sub", "codeReveal", "meterReveal"]) wire(id, "readMerge", "in");
16306
+ wire("readMerge", "readLayer", "in");
16307
+ node("out", "canvas", 5, 3, { width: 600, height: 600, bg: "#04070a", origin: "center" });
16308
+ wire("frameLayer", "out", "in");
16309
+ wire("readLayer", "out", "in");
16310
+ return Project.parse({
16311
+ name: "boot sequence",
16312
+ seed: "boot-09",
16313
+ canvas: { w: 600, h: 600, bg: "#04070a", origin: "center" },
16314
+ // The boot takes ~2.6s; the rest of the loop is the finished HUD holding.
16315
+ timeline: { fps: 30, duration: 4 },
16316
+ graph: assignLayers({ nodes, edges })
16317
+ });
16318
+ }
16319
+ function speedDial() {
16320
+ const { nodes, edges, node, wire } = graph();
16321
+ node("level", "value", 0, 0, { value: 25 });
16322
+ node("levelNext", "math", 1, 0, { op: "add", a: 0, b: 1 });
16323
+ node("outerTicks", "tick-ring", 0, 1, { align: "inward", count: 120, cx: 400, cy: 400, length: 5, majorEvery: 10, majorLength: 11, majorWidth: 1.4, radius: 306, stroke: "#5d7285", strokeWidth: 0.9 });
16324
+ node("outerLabels", "radial-labels", 0, 2, { from: 0, to: 8, anchor: "middle", count: 9, cx: 400, cy: 400, fill: "#dbe7f0", fontSize: 13, radius: 274, start: 220, sweep: 280 });
16325
+ node("outerRing", "circle", 0, 3, { cx: 400, cy: 400, r: 330, stroke: "#9fd4e8", strokeWidth: 1.2, sweep: 360 });
16326
+ node("outerArcs", "partial-outline", 1, 1, { from: 0.02, to: 0.46, offset: 0.13, segments: 2 });
16327
+ node("redRing", "circle", 0, 4, { cx: 400, cy: 400, r: 306, stroke: "#ff5b2e", strokeWidth: 2.4 });
16328
+ node("redArc", "partial-outline", 1, 2, { from: 0, to: 0.115, offset: 0.055, segments: 1 });
16329
+ node("redTick", "tick-ring", 0, 5, { align: "outward", count: 1, cx: 400, cy: 400, length: 22, majorEvery: 0, radius: 296, start: 20, stroke: "#ff5b2e", strokeWidth: 2.4, sweep: 0 });
16330
+ node("innerRing", "circle", 0, 6, { cx: 400, cy: 400, r: 232, stroke: "#ffffff", strokeWidth: 3.2 });
16331
+ node("midRing", "circle", 0, 7, { cx: 400, cy: 400, r: 248, stroke: "#33424f", strokeWidth: 1 });
16332
+ node("gaugeBar", "rect", 0, 8, { w: 22, h: 3.6, x: -11, y: -1.8, fill: "#5d7285", rx: 0, stroke: "none" });
16333
+ node("gaugeRing", "array-radial", 1, 3, { count: 44, cx: 400, cy: 400, mode: "orbit", radius: 202, start: 208, sweep: 304 });
16334
+ node("gaugeLit", "select", 2, 1, { from: 0, to: 25, mode: "range" });
16335
+ node("gaugeLitStyle", "style", 3, 1, { fill: "#dbe7f0", setFill: true, setStroke: false });
16336
+ node("gaugeMid", "select", 2, 2, { from: 26, to: 37, mode: "range" });
16337
+ node("gaugeMidStyle", "style", 3, 2, { fill: "#5d7285", setFill: true, setStroke: false });
16338
+ node("gaugeHot", "select", 2, 3, { from: 38, to: 43, mode: "range" });
16339
+ node("gaugeHotStyle", "style", 3, 3, { fill: "#ff5b2e", setFill: true, setStroke: false });
16340
+ node("innerTicks", "tick-ring", 0, 9, { align: "inward", count: 60, cx: 400, cy: 400, length: 4, majorEvery: 5, majorLength: 8, radius: 178, start: 210, stroke: "#33424f", strokeWidth: 0.8, sweep: 300 });
16341
+ node("centreFrame", "corner-brackets", 0, 10, { w: 300, h: 184, x: 250, y: 308, armX: 34, armY: 26, stroke: "#5d7285", strokeWidth: 1 });
16342
+ node("hairline", "line", 0, 11, { stroke: "#33424f", strokeWidth: 1, x1: 268, x2: 532, y1: 414, y2: 414 });
16343
+ node("marker", "polygon", 0, 12, { cx: 400, cy: 550, radius: 13, rotation: 90, sides: 3, stroke: "#5d7285", strokeWidth: 1.4 });
16344
+ node("speed", "text", 0, 13, { x: 400, y: 378, anchor: "middle", fill: "#ffffff", fontFamily: "Rajdhani", fontSize: 96, letterSpacing: 6, text: "64", fontWeight: 600 });
16345
+ node("unit", "text", 0, 14, { x: 400, y: 402, anchor: "middle", fill: "#5d7285", fontFamily: "IBM Plex Mono", fontSize: 15, letterSpacing: 4, text: "MPH" });
16346
+ node("gear", "text", 0, 15, { x: 400, y: 492, anchor: "middle", fill: "#ffffff", fontFamily: "Rajdhani", fontSize: 62, text: "3", fontWeight: 600 });
16347
+ node("corners", "text", 0, 16, { x: 272, y: 338, anchor: "middle", fill: "#dbe7f0", fontFamily: "IBM Plex Mono", fontSize: 13, text: "F" });
16348
+ node("cornersH", "text", 0, 17, { x: 528, y: 338, anchor: "middle", fill: "#ff5b2e", fontFamily: "IBM Plex Mono", fontSize: 13, text: "H" });
16349
+ node("cornersE", "text", 0, 18, { x: 272, y: 474, anchor: "middle", fill: "#ff5b2e", fontFamily: "IBM Plex Mono", fontSize: 13, text: "E" });
16350
+ node("cornersC", "text", 0, 19, { x: 528, y: 474, anchor: "middle", fill: "#dbe7f0", fontFamily: "IBM Plex Mono", fontSize: 13, text: "C" });
16351
+ node("codes", "random-code", 0, 20, { x: 96, y: 690, count: 3, fill: "#33424f", fontSize: 11, letterSpacing: 1, pattern: "AA#-####", stepY: 17 });
16352
+ node("bgLayerMerge", "merge", 1, 4);
16353
+ node("bgLayer", "layer", 2, 4, { order: 1 }, "bgLayer");
16354
+ node("gaugeLayerMerge", "merge", 4, 1);
16355
+ node("gaugeLayer", "layer", 5, 1, { order: 2 }, "gaugeLayer");
16356
+ node("ringLayerMerge", "merge", 2, 5);
16357
+ node("ringLayer", "layer", 3, 4, { order: 3 }, "ringLayer");
16358
+ node("textLayerMerge", "merge", 1, 5);
16359
+ node("textLayer", "layer", 2, 6, { order: 4 }, "textLayer");
16360
+ node("out", "canvas", 6, 1, { bg: "#04070a", width: 800, height: 800, origin: "top-left" });
16361
+ wire("outerRing", "outerArcs", "in");
16362
+ wire("redRing", "redArc", "in");
16363
+ wire("gaugeBar", "gaugeRing", "in");
16364
+ wire("gaugeRing", "gaugeLit", "in");
16365
+ wire("gaugeLit", "gaugeLitStyle", "in");
16366
+ wire("gaugeRing", "gaugeMid", "in");
16367
+ wire("gaugeMid", "gaugeMidStyle", "in");
16368
+ wire("gaugeRing", "gaugeHot", "in");
16369
+ wire("gaugeHot", "gaugeHotStyle", "in");
16370
+ wire("outerTicks", "bgLayerMerge", "in");
16371
+ wire("outerLabels", "bgLayerMerge", "in");
16372
+ wire("innerTicks", "bgLayerMerge", "in");
16373
+ wire("midRing", "bgLayerMerge", "in");
16374
+ wire("bgLayerMerge", "bgLayer", "in");
16375
+ wire("gaugeLitStyle", "gaugeLayerMerge", "in");
16376
+ wire("gaugeMidStyle", "gaugeLayerMerge", "in");
16377
+ wire("gaugeHotStyle", "gaugeLayerMerge", "in");
16378
+ wire("gaugeLayerMerge", "gaugeLayer", "in");
16379
+ wire("outerArcs", "ringLayerMerge", "in");
16380
+ wire("redArc", "ringLayerMerge", "in");
16381
+ wire("redTick", "ringLayerMerge", "in");
16382
+ wire("innerRing", "ringLayerMerge", "in");
16383
+ wire("ringLayerMerge", "ringLayer", "in");
16384
+ wire("centreFrame", "textLayerMerge", "in");
16385
+ wire("hairline", "textLayerMerge", "in");
16386
+ wire("speed", "textLayerMerge", "in");
16387
+ wire("unit", "textLayerMerge", "in");
16388
+ wire("gear", "textLayerMerge", "in");
16389
+ wire("corners", "textLayerMerge", "in");
16390
+ wire("cornersH", "textLayerMerge", "in");
16391
+ wire("cornersE", "textLayerMerge", "in");
16392
+ wire("cornersC", "textLayerMerge", "in");
16393
+ wire("codes", "textLayerMerge", "in");
16394
+ wire("marker", "textLayerMerge", "in");
16395
+ wire("textLayerMerge", "textLayer", "in");
16396
+ wire("bgLayer", "out", "in");
16397
+ wire("gaugeLayer", "out", "in");
16398
+ wire("ringLayer", "out", "in");
16399
+ wire("textLayer", "out", "in");
16400
+ wire("level", "gaugeLit", "to");
16401
+ wire("level", "levelNext", "a");
16402
+ wire("levelNext", "gaugeMid", "from");
16403
+ return Project.parse({
16404
+ name: "speed dial",
16405
+ seed: "gmunk",
16406
+ canvas: { w: 800, h: 800, bg: "#04070a", origin: "top-left" },
16407
+ timeline: { fps: 30, duration: 4, loop: true },
16408
+ graph: assignLayers({ nodes, edges })
16409
+ });
16410
+ }
16411
+ function cornersStudy() {
16412
+ const { nodes, edges, node, wire } = graph();
16413
+ node("panel", "rect", 0, 0, { w: 300, h: 180, x: 60, y: 80, anchor: "corner", fill: "none", stroke: "#7fe8d0", strokeWidth: 1.5 }, "Panel");
16414
+ node("tab", "rect", 0, 1, { w: 120, h: 70, x: 260, y: 60, anchor: "corner", fill: "none", stroke: "#7fe8d0", strokeWidth: 1.5 }, "Tab cutout");
16415
+ node("panelCut", "boolean", 1, 0, { op: "subtract" });
16416
+ node("panelIn", "corners", 2, 0, { size: 12, style: "round", which: "concave" }, "Inner: round 12");
16417
+ node("panelOut", "corners", 3, 0, { size: 16, style: "chamfer", which: "convex" }, "Outer: chamfer 16");
16418
+ node("panelGhost", "style", 2, 1, { opacity: 0.6, setOpacity: true, setStroke: true, setWidth: true, stroke: "#3a5a55", strokeWidth: 1 }, "Raw cut (ghost, in place)");
16419
+ node("L", "svg", 0, 2, { x: 540, y: 170, fill: "none", size: 180, stroke: "#7fe8d0", strokeWidth: 1.5, svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 160"><path d="M0 0H160V60H60V160H0Z"/></svg>' }, "L profile");
16420
+ node("Lin", "corners", 1, 2, { size: 10, style: "round", which: "concave" }, "Inner: round 10");
16421
+ node("Lout", "corners", 2, 2, { size: 26, style: "round", which: "convex" }, "Outer: round 26");
16422
+ node("starA", "svg", 0, 3, { x: 820, y: 170, fill: "none", size: 180, stroke: "#7fe8d0", strokeWidth: 1.5, svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><path d="M100.00 10.00L122.34 69.26L185.60 72.19L136.14 111.74L152.90 172.81L100.00 138.00L47.10 172.81L63.86 111.74L14.40 72.19L77.66 69.26Z"/></svg>' }, "Star");
16423
+ node("starValleys", "corners", 1, 3, { size: 9, style: "round", which: "concave" }, "Valleys: round 9");
16424
+ node("starB", "svg", 0, 4, { x: 1040, y: 170, fill: "none", size: 180, stroke: "#7fe8d0", strokeWidth: 1.5, svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><path d="M100.00 10.00L122.34 69.26L185.60 72.19L136.14 111.74L152.90 172.81L100.00 138.00L47.10 172.81L63.86 111.74L14.40 72.19L77.66 69.26Z"/></svg>' }, "Star");
16425
+ node("starTips", "corners", 1, 4, { size: 9, style: "round", which: "convex" }, "Tips: round 9");
16426
+ node("bar", "rect", 0, 5, { w: 26, h: 90, x: 80, y: 380, anchor: "corner", fill: "none", stroke: "#7fe8d0", strokeWidth: 1.5 }, "Bar");
16427
+ node("bars", "array-linear", 1, 5, { count: 8, origin: "start", stepX: 40, stepY: 0 });
16428
+ node("base", "rect", 0, 6, { w: 306, h: 40, x: 80, y: 450, anchor: "corner", fill: "none", stroke: "#7fe8d0", strokeWidth: 1.5 }, "Base");
16429
+ node("comb", "boolean", 2, 4, { op: "union" });
16430
+ node("combIn", "corners", 3, 4, { size: 6, style: "chamfer", which: "concave" }, "Gaps: chamfer 6");
16431
+ node("comb2", "boolean", 2, 5, { op: "union" });
16432
+ node("shift2", "transform", 3, 5, { tx: 420, ty: 0 });
16433
+ node("combNotch", "corners", 4, 6, { size: 6, style: "notch", which: "concave" }, "Gaps: notch 6");
16434
+ node("t1", "text", 4, 0, { x: 60, y: 62, anchor: "start", fill: "#7fe8d0", fontSize: 11, letterSpacing: 1.5, stroke: "none", text: "NOTCHED PANEL \xB7 in round 12 / out chamfer 16" });
16435
+ node("main", "layer", 5, 2, {}, "Corners");
16436
+ node("out", "canvas", 6, 2, { bg: "#05070a", width: 1200, height: 700, origin: "top-left" });
16437
+ node("t2", "text", 4, 2, { x: 450, y: 62, anchor: "start", fill: "#7fe8d0", fontSize: 11, letterSpacing: 1.5, stroke: "none", text: "L PROFILE \xB7 in round 10 / out round 26" });
16438
+ node("t3", "text", 4, 3, { x: 730, y: 62, anchor: "start", fill: "#7fe8d0", fontSize: 11, letterSpacing: 1.5, stroke: "none", text: "STAR \xB7 valleys rounded" });
16439
+ node("t3b", "text", 4, 4, { x: 960, y: 62, anchor: "start", fill: "#7fe8d0", fontSize: 11, letterSpacing: 1.5, stroke: "none", text: "STAR \xB7 tips rounded" });
16440
+ node("t4", "text", 4, 5, { x: 80, y: 360, anchor: "start", fill: "#7fe8d0", fontSize: 11, letterSpacing: 1.5, stroke: "none", text: "COMB \xB7 concave chamfer 6" });
16441
+ node("t4b", "text", 4, 7, { x: 500, y: 360, anchor: "start", fill: "#7fe8d0", fontSize: 11, letterSpacing: 1.5, stroke: "none", text: "COMB \xB7 concave notch 6" });
16442
+ node("t1b", "text", 4, 1, { x: 60, y: 285, anchor: "start", fill: "#3a5a55", fontSize: 11, letterSpacing: 1.5, stroke: "none", text: "dim outline: the raw Boolean cut, before Corners" });
16443
+ wire("panel", "panelCut", "in");
16444
+ wire("tab", "panelCut", "with");
16445
+ wire("panelCut", "panelIn", "in");
16446
+ wire("panelIn", "panelOut", "in");
16447
+ wire("panelCut", "panelGhost", "in");
16448
+ wire("L", "Lin", "in");
16449
+ wire("Lin", "Lout", "in");
16450
+ wire("starA", "starValleys", "in");
16451
+ wire("starB", "starTips", "in");
16452
+ wire("bar", "bars", "in");
16453
+ wire("bars", "comb", "in");
16454
+ wire("base", "comb", "with");
16455
+ wire("comb", "combIn", "in");
16456
+ wire("bars", "comb2", "in");
16457
+ wire("base", "comb2", "with");
16458
+ wire("comb2", "shift2", "in");
16459
+ wire("shift2", "combNotch", "in");
16460
+ wire("panelOut", "main", "in");
16461
+ wire("panelGhost", "main", "in");
16462
+ wire("Lout", "main", "in");
16463
+ wire("starValleys", "main", "in");
16464
+ wire("starTips", "main", "in");
16465
+ wire("combIn", "main", "in");
16466
+ wire("combNotch", "main", "in");
16467
+ wire("t1", "main", "in");
16468
+ wire("main", "out", "in");
16469
+ wire("t2", "main", "in");
16470
+ wire("t3", "main", "in");
16471
+ wire("t3b", "main", "in");
16472
+ wire("t4", "main", "in");
16473
+ wire("t4b", "main", "in");
16474
+ wire("t1b", "main", "in");
16475
+ return Project.parse({
16476
+ name: "corners study",
16477
+ seed: "corners",
16478
+ canvas: { w: 1200, h: 700, bg: "#05070a", origin: "top-left" },
16479
+ timeline: { fps: 30, duration: 4, loop: false },
16480
+ graph: assignLayers({ nodes, edges })
16481
+ });
16482
+ }
16483
+ function varyRow() {
16484
+ const { nodes, edges, node, wire } = graph();
16485
+ const teal = "#7fe8d0";
16486
+ const dim = "#3a5a55";
16487
+ const bar = (y, w = 3, fill = teal) => ({ w, h: 80, x: 60, y, anchor: "corner", fill, stroke: "none" });
16488
+ const row = { count: 60, origin: "start", stepX: 18, stepY: 0 };
16489
+ const label = (y, text, fill = teal) => ({ x: 60, y, anchor: "start", fill, fontSize: 11, letterSpacing: 1.5, stroke: "none", text });
16490
+ node("bar1", "rect", 0, 0, bar(70), "Bar 3\xD780");
16491
+ node("row1", "array-linear", 1, 0, row);
16492
+ node("jit1", "jitter", 2, 0, { position: 0, scale: 0.7, scaleAxis: "x" }, "Jitter \xB7 Scale Axis X");
16493
+ node("t1", "text", 3, 0, label(58, "JITTER \xB7 Scale 0.7, Scale Axis X only \u2014 widths scatter, heights hold"));
16494
+ wire("bar1", "row1", "in");
16495
+ wire("row1", "jit1", "in");
16496
+ node("bar2", "rect", 0, 1, bar(195), "Bar 3\xD780");
16497
+ node("row2", "array-linear", 1, 1, row);
16498
+ node("tf2", "transform", 2, 1, { sx: { base: 1, jitter: { amount: 0.6, apply: "mul", mode: "frozen" } } }, "Transform \xB7 Scale X jittered");
16499
+ node("t2", "text", 3, 1, label(183, "TRANSFORM \xB7 right-click Scale X, Add jitter, \xD70.6 \u2014 works on any param of any node after the array"));
16500
+ wire("bar2", "row2", "in");
16501
+ wire("row2", "tf2", "in");
16502
+ node("bar3", "rect", 0, 2, bar(320), "Bar 3\xD780");
16503
+ node("row3", "array-linear", 1, 2, row);
16504
+ node("st3", "stagger", 2, 2, { property: "scale", axis: "x", from: 0.4, to: { base: 2.6, jitter: { amount: 1, mode: "frozen" } } }, "Stagger \xB7 Scale X, To jittered");
16505
+ node("t3", "text", 3, 2, label(308, "STAGGER \xB7 Scale X ramp 0.4 to 2.6, with a jitter on To \u2014 ramp plus noise, louder along the row"));
16506
+ wire("bar3", "row3", "in");
16507
+ wire("row3", "st3", "in");
16508
+ node("bar4a", "rect", 0, 3, bar(445, 2), "Bar 2\xD780");
16509
+ node("bar4b", "rect", 0, 4, bar(445, 5), "Bar 5\xD780");
16510
+ node("bar4c", "rect", 0, 5, bar(445, 10), "Bar 10\xD780");
16511
+ node("mg4", "merge", 1, 4);
16512
+ node("row4", "array-linear", 2, 4, { ...row, deal: true }, "Array Linear \xB7 Deal");
16513
+ node("t4", "text", 3, 4, label(433, "DEAL \xB7 three bars 2 / 5 / 10 merged, Array Linear with Deal on \u2014 cycles them, no randomness"));
16514
+ wire("bar4a", "mg4", "in");
16515
+ wire("bar4b", "mg4", "in");
16516
+ wire("bar4c", "mg4", "in");
16517
+ wire("mg4", "row4", "in");
16518
+ node("bar5", "rect", 0, 6, { ...bar(570, 3, dim), w: { base: 3, jitter: { amount: 0.6, apply: "mul", mode: "frozen" } } }, "Bar \xB7 Width jittered");
16519
+ node("row5", "array-linear", 1, 6, row);
16520
+ node("t5", "text", 3, 6, label(558, "RECT WIDTH JITTERED, THEN ARRAYED \xB7 one shape, one roll: sixty identical bars. Vary after the array, not before.", dim));
16521
+ wire("bar5", "row5", "in");
16522
+ node("main", "layer", 4, 3, {}, "Rows");
16523
+ node("out", "canvas", 5, 3, { bg: "#05070a", width: 1200, height: 700, origin: "top-left" });
16524
+ for (const id of ["jit1", "tf2", "st3", "row4", "row5", "t1", "t2", "t3", "t4", "t5"]) wire(id, "main", "in");
16525
+ wire("main", "out", "in");
16526
+ return Project.parse({
16527
+ name: "vary a row",
16528
+ seed: "vary",
16529
+ canvas: { w: 1200, h: 700, bg: "#05070a", origin: "top-left" },
16530
+ timeline: { fps: 30, duration: 4, loop: false },
16531
+ graph: assignLayers({ nodes, edges })
16532
+ });
16533
+ }
16534
+ function variableFontStudy() {
16535
+ const { nodes, edges, node, wire } = graph();
16536
+ const face = { fontFamily: "Orbitron", fontSize: 64, anchor: "start", fill: "#dbe7f0" };
16537
+ const caption = { fontFamily: "IBM Plex Mono", fontSize: 11, anchor: "start", fill: "#5d7285", letterSpacing: 0.5 };
16538
+ const X = 48;
16539
+ node("breath", "lfo", 0, 0, { shape: "sine", rate: 0.5, center: 650, amplitude: 250 });
16540
+ node("breathe", "text", 1, 0, { ...face, text: "BREATHE", x: X, y: 96, fontWeight: "650" });
16541
+ node("breatheCap", "text", 1, 1, { ...caption, text: "LFO \u2192 WEIGHT \xB7 SINE 0.5 HZ, CENTRE 650, AMPLITUDE 250", x: X, y: 124 });
16542
+ wire("breath", "breathe", "fontWeight");
16543
+ node("clock", "time", 0, 2);
16544
+ node("rise", "ramp", 1, 2, { inMin: 0, inMax: 2.5, outMin: 400, outMax: 900, clamp: true, ease: "smooth" });
16545
+ node("rising", "text", 2, 2, { ...face, text: "MATERIALISE", x: X, y: 218, fontWeight: "400" });
16546
+ node("risingCap", "text", 2, 3, { ...caption, text: "TIME \u2192 RAMP \u2192 WEIGHT \xB7 400 AT 0 S, 900 BY 2.5 S, EASED", x: X, y: 246 });
16547
+ wire("clock", "rise", "value");
16548
+ wire("rise", "rising", "fontWeight");
16549
+ node("wave", "text", 0, 4, { ...face, text: "WAVEFORM", x: X, y: 340, fontWeight: "650", letterSpacing: 6 });
16550
+ node("waveGlyphs", "split-glyphs", 1, 4);
16551
+ node("waveClock", "time", 1, 5);
16552
+ node("wavePhase", "math", 2, 5, { op: "multiply", a: 0, b: 0.5 });
16553
+ node("waveStagger", "stagger", 2, 4, { property: "weight", from: 400, to: 900, curve: "wave", cycles: 1 });
16554
+ node("waveCap", "text", 3, 5, { ...caption, text: "SPLIT GLYPHS \u2192 STAGGER \xB7 FONT WEIGHT 400\u2013900, CURVE WAVE, 1 CYCLE \xB7 TIME \xD7 0.5 \u2192 PHASE", x: X, y: 368 });
16555
+ wire("wave", "waveGlyphs", "in");
16556
+ wire("waveGlyphs", "waveStagger", "in");
16557
+ wire("waveClock", "wavePhase", "a");
16558
+ wire("wavePhase", "waveStagger", "phase");
16559
+ node("scan", "text", 0, 6, { ...face, text: "SCANLINE", x: X, y: 462, fontWeight: "400", letterSpacing: 6 });
16560
+ node("scanGlyphs", "split-glyphs", 1, 6);
16561
+ node("scanSweep", "lfo", 1, 7, { shape: "saw", rate: 0.5, unipolar: true, center: -0.2, amplitude: 1.4 });
16562
+ node("scanStagger", "stagger", 2, 6, { property: "weight", from: 400, to: 900, curve: "peak", width: 0.25 });
16563
+ node("scanCap", "text", 3, 7, { ...caption, text: "SPLIT GLYPHS \u2192 STAGGER \xB7 FONT WEIGHT 400\u2013900, CURVE PEAK, WIDTH 0.25 \xB7 SAW LFO \u22120.2\u20261.2 \u2192 POSITION", x: X, y: 490 });
16564
+ wire("scan", "scanGlyphs", "in");
16565
+ wire("scanGlyphs", "scanStagger", "in");
16566
+ wire("scanSweep", "scanStagger", "position");
16567
+ node("boil", "text", 0, 8, { ...face, text: "DECODE 0x7F3A", x: X, y: 584, fontWeight: "650", letterSpacing: 6 });
16568
+ node("boilGlyphs", "split-glyphs", 1, 8);
16569
+ node("boilStyle", "style", 2, 8, {
16570
+ setWeight: true,
16571
+ weight: { base: 650, jitter: { amount: 250, apply: "add", distribution: "uniform", mode: "animated", noiseFreq: 3 } }
16572
+ });
16573
+ node("boilCap", "text", 2, 9, { ...caption, text: "SPLIT GLYPHS \u2192 STYLE \xB7 SET WEIGHT 650, RIGHT-CLICK JITTER \xB1250, ANIMATED, 3 HZ", x: X, y: 612 });
16574
+ wire("boil", "boilGlyphs", "in");
16575
+ wire("boilGlyphs", "boilStyle", "in");
16576
+ node("squash", "lfo", 0, 10, { shape: "triangle", rate: 0.25, center: 87.5, amplitude: 12.5 });
16577
+ node("squeeze", "text", 1, 10, { ...face, fontFamily: "Tektur", text: "SQUEEZE TO FIT", x: X, y: 706, fontWeight: "600" });
16578
+ node("squeezeCap", "text", 1, 11, { ...caption, text: "TEKTUR \xB7 LFO \u2192 STRETCH \xB7 TRIANGLE 0.25 HZ, 75\u2013100 % \xB7 WEIGHT HOLDS AT 600", x: X, y: 734 });
16579
+ wire("squash", "squeeze", "fontStretch");
16580
+ node("title", "text", 3, 0, { ...caption, text: "VARIABLE FONT STUDY \xB7 ORBITRON 400\u2013900 \xB7 TEKTUR 400\u2013900 \xD7 75\u2013100 % \xB7 ONE FILE, ANY WEIGHT", x: X, y: 36, fill: "#7fe8d0" });
16581
+ node("sheet", "merge", 3, 4);
16582
+ node("sheetLayer", "layer", 4, 4, { order: 1 }, "Study");
16583
+ for (const id of ["title", "breathe", "breatheCap", "rising", "risingCap", "waveStagger", "waveCap", "scanStagger", "scanCap", "boilStyle", "boilCap", "squeeze", "squeezeCap"]) {
16584
+ wire(id, "sheet", "in");
16585
+ }
16586
+ wire("sheet", "sheetLayer", "in");
16587
+ node("out", "canvas", 5, 4, { width: 960, height: 760, bg: "#05070a", origin: "top-left" });
16588
+ wire("sheetLayer", "out", "in");
16589
+ return Project.parse({
16590
+ name: "variable font study",
16591
+ seed: "wght",
16592
+ canvas: { w: 960, h: 760, bg: "#05070a", origin: "top-left" },
16593
+ // Every driver repeats in 2 s or 4 s, so the loop closes on itself.
16594
+ timeline: { fps: 30, duration: 4 },
16595
+ graph: assignLayers({ nodes, edges })
16596
+ });
16597
+ }
16598
+ function depthStack() {
16599
+ const { nodes, edges, node, wire } = graph();
16600
+ const teal = "#7fe8d0";
16601
+ node("frame", "rect", 0, 0, { x: 0, y: 0, w: 380, h: 220, rx: 8, stroke: teal, strokeWidth: 1.5, fill: "#0b161c" }, "Panel");
16602
+ node("cross", "crosshair", 0, 1, { cx: -120, cy: 0, size: 40, gap: 8, stroke: teal, strokeWidth: 1 });
16603
+ node("label", "text", 0, 2, { text: "SECTOR 7 \xB7 ONLINE", x: 40, y: 0, vAnchor: "middle", anchor: "middle", fontSize: 18, letterSpacing: 2, fill: teal, stroke: "none" });
16604
+ node("panel", "merge", 1, 1, { group: true }, "Merge \xB7 Group");
16605
+ node("copies", "array-linear", 2, 1, { count: 5, stepX: 0, stepY: 0 }, "Array \xB7 in place");
16606
+ node("stack", "transform3d", 3, 1, {
16607
+ plane: "shape",
16608
+ order: "depth",
16609
+ // A wide fan: the slide must outrun the shrink, or the far panels
16610
+ // land behind the near one's fill.
16611
+ posX: { base: -380, stagger: { amount: 1100 } },
16612
+ posY: { base: 200, stagger: { amount: -700 } },
16613
+ posZ: { base: 0, stagger: { amount: 2200 } },
16614
+ rotY: -18
16615
+ }, "Transform 3D \xB7 one per shape, Stagger on Z");
16616
+ node("main", "layer", 4, 1, {}, "Stack");
16617
+ node("out", "canvas", 5, 1, { bg: "#05070a", width: 1200, height: 700, origin: "center", perspective: 900 });
16618
+ wire("frame", "panel", "in");
16619
+ wire("cross", "panel", "in");
16620
+ wire("label", "panel", "in");
16621
+ wire("panel", "copies", "in");
16622
+ wire("copies", "stack", "in");
16623
+ wire("stack", "main", "in");
16624
+ wire("main", "out", "in");
16625
+ return Project.parse({
16626
+ name: "depth stack",
16627
+ seed: "stack",
16628
+ canvas: { w: 1200, h: 700, bg: "#05070a", origin: "center" },
16629
+ timeline: { fps: 30, duration: 4, loop: false },
16630
+ graph: assignLayers({ nodes, edges })
16631
+ });
16632
+ }
16633
+ function cardCarousel() {
16634
+ const { nodes, edges, node, wire } = graph();
16635
+ const teal = "#7fe8d0";
16636
+ const dim = "#3a5a55";
16637
+ node("card", "rect", 0, 0, { x: 0, y: 0, w: 160, h: 230, rx: 10, stroke: teal, strokeWidth: 1.5, fill: "#0b161c" }, "Card");
16638
+ node("face", "rect", 0, 1, { x: 0, y: -40, w: 120, h: 110, rx: 4, stroke: dim, strokeWidth: 1, fill: "none" });
16639
+ node("bar", "rect", 0, 2, { x: 0, y: 70, w: 120, h: 10, stroke: "none", fill: teal });
16640
+ node("one", "merge", 1, 1, { group: true }, "Merge \xB7 Group");
16641
+ node("eight", "array-linear", 2, 1, { count: 8, stepX: 0, stepY: 0 }, "Array \xB7 in place");
16642
+ node("clock", "time", 2, 0);
16643
+ node("spin", "math", 3, 0, { op: "multiply", a: 0, b: 30 }, "degrees per second");
16644
+ node("drum", "transform3d", 3, 1, {
16645
+ plane: "shape",
16646
+ order: "depth",
16647
+ hideBack: true,
16648
+ pivot: "shape",
16649
+ rotY: { base: 0, expr: "v + i / n * 360" },
16650
+ posX: { base: 0, expr: "sin(rad(v + i / n * 360)) * 340" },
16651
+ posZ: { base: 0, expr: "(1 - cos(rad(v + i / n * 360))) * 340 + 200" }
16652
+ }, "Transform 3D \xB7 one per shape, on the rim");
16653
+ node("main", "layer", 4, 1, {}, "Carousel");
16654
+ node("out", "canvas", 5, 1, { bg: "#05070a", width: 1200, height: 700, origin: "center", perspective: 1100 });
16655
+ wire("card", "one", "in");
16656
+ wire("face", "one", "in");
16657
+ wire("bar", "one", "in");
16658
+ wire("one", "eight", "in");
16659
+ wire("eight", "drum", "in");
16660
+ wire("clock", "spin", "a");
16661
+ wire("spin", "drum", "rotY");
16662
+ wire("drum", "main", "in");
16663
+ wire("main", "out", "in");
16664
+ return Project.parse({
16665
+ name: "card carousel",
16666
+ seed: "drum",
16667
+ canvas: { w: 1200, h: 700, bg: "#05070a", origin: "center" },
16668
+ timeline: { fps: 30, duration: 12, loop: true },
16669
+ graph: assignLayers({ nodes, edges })
16670
+ });
16671
+ }
16672
+ var PRESETS = [
16673
+ { name: "boot-sequence", label: "Boot Sequence", description: "The power-on: a Trigger starts three Seqs \u2014 the frame draws itself, the title scrambles in, meters wipe across.", build: bootSequence },
16674
+ { name: "glitch-relay", label: "Glitch Relay", description: "The post chain: a spinning dial, its readout rasterized and torn.", build: glitchRelay },
16675
+ { name: "warning-strip", label: "Warning Strip", description: "Stripes: a crawling hazard bar and streaming chevrons, Time wired into Offset.", build: warningStrip },
16676
+ { name: "panel-split", label: "Panel Split", description: "Subdivide: a header-over-three-cells recipe via Select, and a seeded mosaic breathing on an LFO.", build: panelSplit },
16677
+ { name: "subdivider-study", label: "Subdivider Study", description: "Four panel layouts from the reference sheet, each one Subdivide \u2192 Select \u2192 Subdivide \u2192 Merge, Split mode only.", build: subdividerStudy },
16678
+ { name: "lit-panels", label: "Lit Panels", description: "Style Tokens painting a Subdivide: a seeded mosaic dealt the palette, most cells left hollow. Switch the Preset, the board redeals.", build: litPanels },
16679
+ { name: "kiosk", label: "Kiosk", description: "Offset in six bands around a traced word, painted by Style Tokens in Cycle mode \u2014 the retro multi-stroke poster.", build: kiosk },
16680
+ { name: "ion-dial", label: "Ion Dial", description: "Style Tokens driving every stroke \u2014 recolour the palette, the HUD and the UI follow.", build: ionDial },
16681
+ { name: "radar", label: "Radar", description: "Polar grid, a Time-driven sweep, seeded contacts. Loops in 3s.", build: radar },
16682
+ { name: "telemetry", label: "Telemetry", description: "Grid paper, code column, an LFO breathing through a bar meter.", build: telemetry },
16683
+ { name: "speed-dial", label: "Speed Dial", description: "The Oblivion instrument: tick rings, partial arcs, a three-band gauge one Value fills, a bracketed readout.", build: speedDial },
16684
+ { name: "vary-row", label: "Vary a Row", description: "Five ways to give a row of equal bars different widths: Jitter with Scale Axis X, a right-click jitter on Transform Scale X, a Stagger ramp plus noise, Deal cycling three bars \u2014 and why jittering the Rect itself gives sixty identical copies.", build: varyRow },
16685
+ { name: "corners-study", label: "Corners Study", description: "Concave vs convex: a notched panel, an L profile, two stars and a comb, each finished by the Corners node.", build: cornersStudy },
16686
+ { name: "depth-stack", label: "Depth Stack", description: "Panels receding into depth: one panel arrayed in place, Transform 3D with a plane per shape and a Stagger on Pos. Z \u2014 the move an array\u2019s Step cannot make \u2014 drawn nearest-on-top.", build: depthStack },
16687
+ { name: "card-carousel", label: "Card Carousel", description: "Eight cards on a spinning drum: Transform 3D with a plane per shape, Pos. X and Z from sin and cos of each card\u2019s share of the circle, Time into Rot. Y, Hide Back and By depth doing the rest.", build: cardCarousel },
16688
+ { name: "variable-font-study", label: "Variable Font Study", description: "Weight and width as moving quantities, on the bundled variable faces, with nodes alone: an LFO breathing a word, a Ramp materialising one, Stagger\u2019s Wave and Peak curves rolling a wave and a scan along split letters, a jitter boiling them \u2014 and Tektur squeezed along its width axis.", build: variableFontStudy }
16689
+ ];
14769
16690
  var ALL_NODES = [
14770
16691
  ...GENERATORS,
14771
16692
  stripesNode,
@@ -14778,6 +16699,7 @@ var ALL_NODES = [
14778
16699
  splitGlyphsNode,
14779
16700
  textEffectNode,
14780
16701
  ...MODIFIERS,
16702
+ transform3dNode,
14781
16703
  cornersNode,
14782
16704
  bendNode,
14783
16705
  booleanNode,
@@ -14942,10 +16864,16 @@ function fieldRef(p, shape, world, n, defs) {
14942
16864
  defs.push(`<pattern id="${id}" ${attrs.join(" ")}>${image}</pattern>`);
14943
16865
  return `url(#${id})`;
14944
16866
  }
14945
- function styleAttrs(fill, stroke, style, n) {
16867
+ function paintAttr(name, color, n) {
16868
+ const h = color.startsWith("#") ? splitHex(color) : void 0;
16869
+ if (!h) return [`${name}="${color}"`];
16870
+ return h.a < 1 ? [`${name}="${h.rgb}"`, `${name}-opacity="${n(h.a)}"`] : [`${name}="${color}"`];
16871
+ }
16872
+ function styleAttrs(fill, stroke, style, n, split = true) {
14946
16873
  const out = [];
14947
- if (fill !== void 0) out.push(`fill="${fill}"`);
14948
- if (stroke !== void 0 && stroke !== "none") out.push(`stroke="${stroke}"`);
16874
+ const attr = (name, color) => split ? paintAttr(name, color, n) : [`${name}="${color}"`];
16875
+ if (fill !== void 0) out.push(...attr("fill", fill));
16876
+ if (stroke !== void 0 && stroke !== "none") out.push(...attr("stroke", stroke));
14949
16877
  if (style.strokeWidth !== void 0 && stroke !== "none") {
14950
16878
  out.push(`stroke-width="${n(style.strokeWidth)}"`);
14951
16879
  }
@@ -14959,8 +16887,8 @@ function styleAttrs(fill, stroke, style, n) {
14959
16887
  return out;
14960
16888
  }
14961
16889
  function polar2(cx, cy, r, deg) {
14962
- const rad = deg * Math.PI / 180;
14963
- return [cx + r * Math.cos(rad), cy + r * Math.sin(rad)];
16890
+ const rad2 = deg * Math.PI / 180;
16891
+ return [cx + r * Math.cos(rad2), cy + r * Math.sin(rad2)];
14964
16892
  }
14965
16893
  function arcPath(cx, cy, r, a0, a1, n) {
14966
16894
  const sweep = a1 - a0;
@@ -14978,7 +16906,7 @@ function renderShape(shape, opts, depth, pretty, parent = IDENTITY) {
14978
16906
  const defs = [];
14979
16907
  const fill = paintRef(shape.style.fill, shape, world, n, defs);
14980
16908
  const stroke = paintRef(shape.style.stroke, shape, world, n, defs);
14981
- const element = renderElement(shape, opts, depth, pretty, world, styleAttrs(fill, stroke, shape.style, n));
16909
+ const element = renderElement(shape, opts, depth, pretty, world, styleAttrs(fill, stroke, shape.style, n, shape.kind !== "group"));
14982
16910
  if (defs.length === 0) return element;
14983
16911
  return `${pad}<g>${nl}${pad}<defs>${defs.join("")}</defs>${nl}${element}${pad}</g>${nl}`;
14984
16912
  }
@@ -15067,21 +16995,21 @@ function renderSvg(scene, options = {}) {
15067
16995
  const defs = options.defs ? `${pretty ? " " : ""}<defs>${options.defs}</defs>${nl}` : "";
15068
16996
  const ox = scene.origin === "center" ? -scene.width / 2 : 0;
15069
16997
  const oy = scene.origin === "center" ? -scene.height / 2 : 0;
15070
- const bg = scene.background && scene.background !== "none" ? `${pretty ? " " : ""}<rect x="${n(ox)}" y="${n(oy)}" width="${n(scene.width)}" height="${n(scene.height)}" fill="${scene.background}"/>${nl}` : "";
16998
+ const bg = scene.background && scene.background !== "none" ? `${pretty ? " " : ""}<rect x="${n(ox)}" y="${n(oy)}" width="${n(scene.width)}" height="${n(scene.height)}" ${paintAttr("fill", scene.background, n).join(" ")}/>${nl}` : "";
15071
16999
  const view = options.crop ?? { x: ox, y: oy, w: scene.width, h: scene.height };
15072
17000
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${n(view.w)}" height="${n(view.h)}" viewBox="${n(view.x)} ${n(view.y)} ${n(view.w)} ${n(view.h)}">${nl}${defs}${bg}${body}</svg>${nl}`;
15073
17001
  }
15074
17002
 
15075
- // ../../packages/ograf/dist/chunk-MA5YUQEU.js
17003
+ // ../../packages/ograf/dist/chunk-2F3KOQL2.js
15076
17004
  function ografNodeOf(project) {
15077
17005
  return project.graph.nodes.find((n) => n.type === OGRAF_TYPE);
15078
17006
  }
15079
17007
  var COLOR_PATTERN = { rgb: "^#[0-9a-f]{6}$", rgba: "^#[0-9a-f]{8}$" };
15080
- var hex23 = (n) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, "0");
17008
+ var hex24 = (n) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, "0");
15081
17009
  function canonicalColor(value, alpha) {
15082
17010
  const c = parseHex(value);
15083
17011
  if (!c) return void 0;
15084
- return `#${hex23(c[0])}${hex23(c[1])}${hex23(c[2])}${alpha ? hex23(c[3] * 255) : ""}`;
17012
+ return `#${hex24(c[0])}${hex24(c[1])}${hex24(c[2])}${alpha ? hex24(c[3] * 255) : ""}`;
15085
17013
  }
15086
17014
  var clamp3 = (v, min, max) => Math.min(max ?? Infinity, Math.max(min ?? -Infinity, v));
15087
17015
  function controlValue(row, node) {
@@ -15951,6 +17879,11 @@ function buildTemplate(input) {
15951
17879
  `Post effects stay in the studio: ${postRemoved} post ${postRemoved === 1 ? "node is" : "nodes are"} left out, and the shapes they worked on come through as plain vectors. Add the look in Resolve.`
15952
17880
  );
15953
17881
  }
17882
+ for (const node of posedLayersWithPictures(project)) {
17883
+ warnings.push(
17884
+ `Layer "${node.name?.trim() || node.id}" is posed in 3D and holds a picture or a paint: a template has no rasteriser, so Resolve places those flat on the plane instead of warping them. Its shapes project as they do in the studio.`
17885
+ );
17886
+ }
15954
17887
  const assets = {};
15955
17888
  for (const node of project.graph.nodes) {
15956
17889
  if (node.type !== "image") continue;
@@ -16050,10 +17983,6 @@ export {
16050
17983
  monochrome,
16051
17984
  yuvToRgb,
16052
17985
  premultiplyRgba,
16053
- parseBdf,
16054
- bdfOutline,
16055
- bdfGlyphs,
16056
- bdfMeasure,
16057
17986
  FONT_FACES,
16058
17987
  DEFAULT_FONT_FAMILY,
16059
17988
  localFontBytes,
@@ -16062,8 +17991,13 @@ export {
16062
17991
  facesFor,
16063
17992
  fileName,
16064
17993
  fontFaceCss,
17994
+ parseBdf,
17995
+ bdfOutline,
17996
+ bdfGlyphs,
17997
+ bdfMeasure,
16065
17998
  outlineBitmapText,
16066
17999
  usableRows,
18000
+ PRESETS,
16067
18001
  createRegistry,
16068
18002
  renderSvg,
16069
18003
  ografNodeOf,