reframe-video 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/assets/sfx/LICENSE.md +2 -1
  2. package/assets/sfx/bong_001.ogg +0 -0
  3. package/assets/sfx/click_001.ogg +0 -0
  4. package/assets/sfx/confirmation_002.ogg +0 -0
  5. package/assets/sfx/confirmation_003.ogg +0 -0
  6. package/assets/sfx/confirmation_004.ogg +0 -0
  7. package/assets/sfx/footstep_001.ogg +0 -0
  8. package/assets/sfx/footstep_002.ogg +0 -0
  9. package/assets/sfx/footstep_003.ogg +0 -0
  10. package/assets/sfx/glass_001.ogg +0 -0
  11. package/assets/sfx/maximize_001.ogg +0 -0
  12. package/assets/sfx/maximize_002.ogg +0 -0
  13. package/assets/sfx/maximize_005.ogg +0 -0
  14. package/assets/sfx/maximize_009.ogg +0 -0
  15. package/assets/sfx/open_001.ogg +0 -0
  16. package/assets/sfx/pluck_001.ogg +0 -0
  17. package/assets/sfx/pluck_002.ogg +0 -0
  18. package/assets/sfx/select_001.ogg +0 -0
  19. package/assets/sfx/select_002.ogg +0 -0
  20. package/assets/sfx/select_003.ogg +0 -0
  21. package/dist/bin.js +271 -49
  22. package/dist/browserEntry.js +179 -68
  23. package/dist/cli.js +445 -85
  24. package/dist/index.js +1187 -116
  25. package/dist/labels.js +606 -0
  26. package/dist/renderer-canvas.js +15 -0
  27. package/dist/trace-cli.js +9 -9
  28. package/dist/types/audio.d.ts +9 -0
  29. package/dist/types/characterPreset.d.ts +39 -0
  30. package/dist/types/compile.d.ts +1 -0
  31. package/dist/types/compose.d.ts +18 -2
  32. package/dist/types/composeComposition.d.ts +27 -0
  33. package/dist/types/devicePreset.d.ts +65 -0
  34. package/dist/types/dsl.d.ts +12 -1
  35. package/dist/types/evaluate.d.ts +32 -0
  36. package/dist/types/figure.d.ts +32 -0
  37. package/dist/types/index.d.ts +9 -3
  38. package/dist/types/interpolate.d.ts +3 -2
  39. package/dist/types/ir.d.ts +68 -0
  40. package/dist/types/motionOps.d.ts +36 -0
  41. package/dist/types/path.d.ts +7 -3
  42. package/dist/types/rig.d.ts +87 -0
  43. package/dist/types/validate.d.ts +4 -1
  44. package/guides/edsl-guide.md +54 -1
  45. package/guides/regen-contract.md +11 -0
  46. package/package.json +1 -1
  47. package/preview/index.html +56 -3
  48. package/preview/src/main.ts +1132 -46
  49. package/preview/src/panel.ts +478 -8
  50. package/preview/src/store.ts +323 -6
@@ -5,11 +5,12 @@ Verified against each asset page's license field on 2026-06-11.
5
5
  | files | source | author | license |
6
6
  |---|---|---|---|
7
7
  | keypress-001/004/007/010/014.wav | [Keyboard Soundpack #1](https://opengameart.org/content/keyboard-soundpack-1-typing-and-single-keystrokes) (Cherry KC 1000 recordings) | unicaegames | CC0 |
8
- | click_002/003/004.ogg, confirmation_001.ogg | [Interface Sounds](https://opengameart.org/content/interface-sounds) | Kenney (kenney.nl) | CC0 |
8
+ | click_001/002/003/004.ogg, confirmation_001/002/003/004.ogg, maximize_001/002/005/009.ogg, open_001.ogg, pluck_001/002.ogg, select_001/002/003.ogg, bong_001.ogg, glass_001.ogg | [Interface Sounds](https://kenney.nl/assets/interface-sounds) | Kenney (kenney.nl) | CC0 |
9
9
  | tick.wav (tick_001), pop.wav (drop_002), shimmer.wav (glass_002 + echo tail) | [Interface Sounds](https://opengameart.org/content/interface-sounds) | Kenney (kenney.nl) | CC0 |
10
10
  | whoosh.wav (wind body), rise.wav (reversed slice) | [Air whoosh](https://opengameart.org/content/air-whoosh) | qubodup | CC0 |
11
11
  | whoosh.wav (transient layer: swish-9) | [Swishes Sound Pack](https://opengameart.org/content/swishes-sound-pack) | artisticdude | CC0 |
12
12
  | thud.wav (trimmed) | [Muffled Distant Explosion](https://opengameart.org/content/muffled-distant-explosion) | NenadSimic | CC0 |
13
+ | footstep_001/002/003.ogg (footstep00/03/06) | [RPG Audio](https://kenney.nl/assets/rpg-audio) | Kenney (kenney.nl) | CC0 |
13
14
  | bgm-song21.mp3 | [Mysterious Ambience (song21)](https://opengameart.org/content/mysterious-ambience-song21) | cynicmusic (pixelsphere.org) | multi-licensed; used under its CC0 option |
14
15
 
15
16
  CC0 requires no attribution; this file records provenance anyway.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/dist/bin.js CHANGED
@@ -42,7 +42,7 @@ function segCountOf(points, closed) {
42
42
  if (n < 2) return 0;
43
43
  return closed ? n : n - 1;
44
44
  }
45
- function pathPoint(points, closed, u) {
45
+ function pathPoint(points, closed, u, curviness = 1) {
46
46
  const n = points.length;
47
47
  if (n === 0) return [0, 0];
48
48
  if (n === 1) return [points[0][0], points[0][1]];
@@ -51,19 +51,41 @@ function pathPoint(points, closed, u) {
51
51
  const [p0, p1, p2, p3] = controls(points, closed, i);
52
52
  const t2 = t * t;
53
53
  const t3 = t2 * t;
54
- const f = (a, b, c, d) => 0.5 * (2 * b + (-a + c) * t + (2 * a - 5 * b + 4 * c - d) * t2 + (-a + 3 * b - 3 * c + d) * t3);
55
- return [f(p0[0], p1[0], p2[0], p3[0]), f(p0[1], p1[1], p2[1], p3[1])];
56
- }
57
- function pathTangentAngle(points, closed, u) {
54
+ if (curviness === 1) {
55
+ const f = (a, b, c, d) => 0.5 * (2 * b + (-a + c) * t + (2 * a - 5 * b + 4 * c - d) * t2 + (-a + 3 * b - 3 * c + d) * t3);
56
+ return [f(p0[0], p1[0], p2[0], p3[0]), f(p0[1], p1[1], p2[1], p3[1])];
57
+ }
58
+ const h00 = 2 * t3 - 3 * t2 + 1;
59
+ const h10 = t3 - 2 * t2 + t;
60
+ const h01 = -2 * t3 + 3 * t2;
61
+ const h11 = t3 - t2;
62
+ const k = curviness * 0.5;
63
+ const H = (a, b, c, d) => h00 * b + h10 * k * (c - a) + h01 * c + h11 * k * (d - b);
64
+ return [H(p0[0], p1[0], p2[0], p3[0]), H(p0[1], p1[1], p2[1], p3[1])];
65
+ }
66
+ function pathTangentAngle(points, closed, u, curviness = 1) {
58
67
  const n = points.length;
59
68
  if (n < 2) return 0;
60
69
  const segs = segCountOf(points, closed);
61
70
  const { i, t } = locate(segs, u);
62
71
  const [p0, p1, p2, p3] = controls(points, closed, i);
63
72
  const t2 = t * t;
64
- const d = (a, b, c, e) => 0.5 * (-a + c + 2 * (2 * a - 5 * b + 4 * c - e) * t + 3 * (-a + 3 * b - 3 * c + e) * t2);
65
- const dx = d(p0[0], p1[0], p2[0], p3[0]);
66
- const dy = d(p0[1], p1[1], p2[1], p3[1]);
73
+ let dx;
74
+ let dy;
75
+ if (curviness === 1) {
76
+ const d = (a, b, c, e) => 0.5 * (-a + c + 2 * (2 * a - 5 * b + 4 * c - e) * t + 3 * (-a + 3 * b - 3 * c + e) * t2);
77
+ dx = d(p0[0], p1[0], p2[0], p3[0]);
78
+ dy = d(p0[1], p1[1], p2[1], p3[1]);
79
+ } else {
80
+ const g00 = 6 * t2 - 6 * t;
81
+ const g10 = 3 * t2 - 4 * t + 1;
82
+ const g01 = -6 * t2 + 6 * t;
83
+ const g11 = 3 * t2 - 2 * t;
84
+ const k = curviness * 0.5;
85
+ const D = (a, b, c, e) => g00 * b + g10 * k * (c - a) + g01 * c + g11 * k * (e - b);
86
+ dx = D(p0[0], p1[0], p2[0], p3[0]);
87
+ dy = D(p0[1], p1[1], p2[1], p3[1]);
88
+ }
67
89
  if (dx === 0 && dy === 0) return 0;
68
90
  return Math.atan2(dy, dx) * 180 / Math.PI;
69
91
  }
@@ -144,8 +166,8 @@ function compileScene(ir) {
144
166
  const currentValue = (target, prop) => {
145
167
  const v = current.get(key(target, prop));
146
168
  if (v !== void 0) return v;
147
- if (prop === "opacity" || prop === "scale" || prop === "progress") return 1;
148
- if (prop === "rotation") return 0;
169
+ if (prop === "opacity" || prop === "scale" || prop === "progress" || prop === "scaleX" || prop === "scaleY") return 1;
170
+ if (prop === "rotation" || prop === "skewX" || prop === "skewY") return 0;
149
171
  throw new Error(`cannot animate "${prop}" of "${target}": no base value to start from`);
150
172
  };
151
173
  const labelTimes = /* @__PURE__ */ new Map();
@@ -248,16 +270,17 @@ function compileScene(ir) {
248
270
  const duration = tl.duration ?? DEFAULT_MOTIONPATH_DURATION;
249
271
  const points = tl.points;
250
272
  const closed = tl.closed ?? false;
273
+ const curviness = tl.curviness ?? 1;
251
274
  const autoRotate = tl.autoRotate ?? false;
252
275
  const rotateOffset = tl.rotateOffset ?? 0;
253
276
  let list = motionPaths.get(tl.target);
254
277
  if (!list) motionPaths.set(tl.target, list = []);
255
- list.push({ t0: start, t1: start + duration, points, closed, autoRotate, rotateOffset, ...tl.ease !== void 0 && { ease: tl.ease } });
278
+ list.push({ t0: start, t1: start + duration, points, closed, curviness, autoRotate, rotateOffset, ...tl.ease !== void 0 && { ease: tl.ease } });
256
279
  if (points.length > 0) {
257
- const [ex, ey] = pathPoint(points, closed, 1);
280
+ const [ex, ey] = pathPoint(points, closed, 1, curviness);
258
281
  current.set(key(tl.target, "x"), ex);
259
282
  current.set(key(tl.target, "y"), ey);
260
- if (autoRotate) current.set(key(tl.target, "rotation"), pathTangentAngle(points, closed, 1) + rotateOffset);
283
+ if (autoRotate) current.set(key(tl.target, "rotation"), pathTangentAngle(points, closed, 1, curviness) + rotateOffset);
261
284
  }
262
285
  return start + duration;
263
286
  }
@@ -322,7 +345,18 @@ function validateScene(ir) {
322
345
  problems.push(`duplicate node id "${node.id}" \u2014 every node id must be unique`);
323
346
  }
324
347
  nodeById.set(node.id, node);
325
- if (node.type === "group") collect(node.children);
348
+ if (node.type === "group") {
349
+ const clip = node.props.clip;
350
+ if (clip) {
351
+ if (clip.kind !== "rect" && clip.kind !== "ellipse") {
352
+ problems.push(`group "${node.id}" clip: unknown kind "${clip.kind}" \u2014 use "rect" or "ellipse"`);
353
+ }
354
+ if (!(clip.width > 0) || !(clip.height > 0)) {
355
+ problems.push(`group "${node.id}" clip: width and height must be > 0`);
356
+ }
357
+ }
358
+ collect(node.children);
359
+ }
326
360
  }
327
361
  };
328
362
  collect(ir.nodes);
@@ -405,6 +439,9 @@ function validateScene(ir) {
405
439
  if (tl.duration !== void 0 && tl.duration <= 0) {
406
440
  problems.push(`${path2}: motionPath "${tl.target}" duration must be > 0`);
407
441
  }
442
+ if (tl.curviness !== void 0 && tl.curviness < 0) {
443
+ problems.push(`${path2}: motionPath "${tl.target}" curviness must be >= 0`);
444
+ }
408
445
  break;
409
446
  }
410
447
  case "wait":
@@ -423,6 +460,13 @@ function validateScene(ir) {
423
460
  if (tl.scale !== void 0 && tl.scale <= 0) {
424
461
  problems.push(`${path2}: beat "${tl.name}" scale must be > 0`);
425
462
  }
463
+ for (const id of tl.nodes ?? []) {
464
+ if (!nodeById.has(id)) {
465
+ problems.push(
466
+ `${path2}: beat "${tl.name}" owns unknown node "${id}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
467
+ );
468
+ }
469
+ }
426
470
  tl.children.forEach((c, i) => checkTimeline(c, `${path2}.beat(${tl.name})[${i}]`));
427
471
  break;
428
472
  }
@@ -463,11 +507,40 @@ function validateScene(ir) {
463
507
  }
464
508
  if (problems.length > 0) throw new SceneValidationError(problems);
465
509
  }
466
- var COMMON_PROPS, PROPS_BY_TYPE, SceneValidationError;
510
+ function validateComposition(comp) {
511
+ const problems = [];
512
+ if (comp.scenes.length === 0) problems.push("composition has no scenes");
513
+ const seen = /* @__PURE__ */ new Set();
514
+ for (const [i, entry] of comp.scenes.entries()) {
515
+ const where = `scenes[${i}]`;
516
+ try {
517
+ validateScene(entry.scene);
518
+ } catch (err) {
519
+ if (err instanceof SceneValidationError) {
520
+ for (const p of err.problems) problems.push(`${where} (scene "${entry.scene.id}"): ${p}`);
521
+ } else throw err;
522
+ }
523
+ if (seen.has(entry.scene.id)) {
524
+ problems.push(`${where}: duplicate scene id "${entry.scene.id}" \u2014 scene ids must be unique in a composition`);
525
+ }
526
+ seen.add(entry.scene.id);
527
+ if (entry.transition !== void 0 && !TRANSITIONS.includes(entry.transition)) {
528
+ problems.push(`${where}: unknown transition "${entry.transition}" \u2014 valid: ${TRANSITIONS.join(", ")}`);
529
+ }
530
+ if (typeof entry.at === "string" && Number.isNaN(Number(entry.at))) {
531
+ problems.push(`${where}: "at" string "${entry.at}" is not a number (use "-0.5"/"+0.5" or a number)`);
532
+ }
533
+ if (typeof entry.at === "number" && entry.at < 0) {
534
+ problems.push(`${where}: absolute "at" must be >= 0`);
535
+ }
536
+ }
537
+ if (problems.length > 0) throw new SceneValidationError(problems);
538
+ }
539
+ var COMMON_PROPS, PROPS_BY_TYPE, SceneValidationError, TRANSITIONS;
467
540
  var init_validate = __esm({
468
541
  "../core/src/validate.ts"() {
469
542
  "use strict";
470
- COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "anchor"];
543
+ COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor"];
471
544
  PROPS_BY_TYPE = {
472
545
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
473
546
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
@@ -486,6 +559,7 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
486
559
  }
487
560
  problems;
488
561
  };
562
+ TRANSITIONS = ["cut", "crossfade"];
489
563
  }
490
564
  });
491
565
 
@@ -543,10 +617,27 @@ var init_dsl = __esm({
543
617
  }
544
618
  });
545
619
 
620
+ // ../core/src/composeComposition.ts
621
+ var init_composeComposition = __esm({
622
+ "../core/src/composeComposition.ts"() {
623
+ "use strict";
624
+ init_compile();
625
+ init_ir();
626
+ }
627
+ });
628
+
546
629
  // ../core/src/compose.ts
547
630
  function composeScene(base, ...overlays) {
548
631
  const ir = structuredClone(base);
549
632
  const report = { applied: [], orphans: [], warnings: [] };
633
+ const baseNodeIds = /* @__PURE__ */ new Set();
634
+ const collectBase = (nodes) => {
635
+ for (const node of nodes) {
636
+ baseNodeIds.add(node.id);
637
+ if (node.type === "group") collectBase(node.children);
638
+ }
639
+ };
640
+ collectBase(base.nodes);
550
641
  overlays.forEach((overlay, index) => {
551
642
  const layer = overlay.name ?? `overlay-${index}`;
552
643
  if (overlay.target !== void 0 && overlay.target !== ir.id) {
@@ -554,12 +645,12 @@ function composeScene(base, ...overlays) {
554
645
  `${layer}: authored against scene "${overlay.target}" but composing onto "${ir.id}"`
555
646
  );
556
647
  }
557
- applyOverlay(ir, overlay, layer, report);
648
+ applyOverlay(ir, overlay, layer, report, baseNodeIds);
558
649
  });
559
650
  validateScene(ir);
560
651
  return { ir, report };
561
652
  }
562
- function applyOverlay(ir, overlay, layer, report) {
653
+ function applyOverlay(ir, overlay, layer, report, baseNodeIds) {
563
654
  const nodeById = /* @__PURE__ */ new Map();
564
655
  const collect = (nodes) => {
565
656
  for (const node of nodes) {
@@ -674,7 +765,7 @@ function applyOverlay(ir, overlay, layer, report) {
674
765
  to: ["duration", "ease", "stagger"],
675
766
  tween: ["duration", "ease"],
676
767
  wait: ["duration"],
677
- motionPath: ["points", "duration", "ease"],
768
+ motionPath: ["points", "duration", "ease", "curviness", "autoRotate"],
678
769
  beat: ["at", "gap", "scale", "duration", "order"]
679
770
  };
680
771
  let timingPatched = false;
@@ -712,6 +803,49 @@ function applyOverlay(ir, overlay, layer, report) {
712
803
  nodeById.set(node.id, node);
713
804
  applied(`addNodes.${node.id}`, "add-node");
714
805
  }
806
+ for (const id of overlay.removeNodes ?? []) {
807
+ if (baseNodeIds.has(id)) {
808
+ orphan(
809
+ `removeNodes.${id}`,
810
+ `"${id}" is a base scene node \u2014 the scene owns it; hide it with opacity: 0 instead of removing`
811
+ );
812
+ continue;
813
+ }
814
+ const index = ir.nodes.findIndex((n) => n.id === id);
815
+ if (index < 0) {
816
+ orphan(
817
+ `removeNodes.${id}`,
818
+ `unknown overlay-added node "${id}" \u2014 nothing to remove`
819
+ );
820
+ continue;
821
+ }
822
+ ir.nodes.splice(index, 1);
823
+ nodeById.delete(id);
824
+ applied(`removeNodes.${id}`, "remove-node");
825
+ }
826
+ if (overlay.addTimeline && overlay.addTimeline.length > 0) {
827
+ const collectTargets = (tl, out) => {
828
+ if (tl.kind === "tween" || tl.kind === "motionPath") out.add(tl.target);
829
+ if ("children" in tl) tl.children.forEach((c) => collectTargets(c, out));
830
+ };
831
+ const valid = [];
832
+ overlay.addTimeline.forEach((frag, i) => {
833
+ const targets = /* @__PURE__ */ new Set();
834
+ collectTargets(frag, targets);
835
+ const missing = [...targets].filter((id) => !nodeById.has(id));
836
+ if (missing.length > 0) {
837
+ orphan(`addTimeline[${i}]`, `targets unknown node(s) ${missing.join(", ")} \u2014 known ids: ${knownIds()}`);
838
+ return;
839
+ }
840
+ valid.push(structuredClone(frag));
841
+ applied(`addTimeline[${i}]`, "add-timeline");
842
+ });
843
+ if (valid.length > 0) {
844
+ ir.timeline = ir.timeline ? { kind: "par", children: [ir.timeline, ...valid] } : valid.length === 1 ? valid[0] : { kind: "par", children: valid };
845
+ delete ir.duration;
846
+ ir.duration = compileScene(ir).duration;
847
+ }
848
+ }
715
849
  }
716
850
  var SCENE_PATCHABLE;
717
851
  var init_compose = __esm({
@@ -902,6 +1036,48 @@ var init_presets = __esm({
902
1036
  }
903
1037
  });
904
1038
 
1039
+ // ../core/src/devicePreset.ts
1040
+ var init_devicePreset = __esm({
1041
+ "../core/src/devicePreset.ts"() {
1042
+ "use strict";
1043
+ init_dsl();
1044
+ }
1045
+ });
1046
+
1047
+ // ../core/src/rig.ts
1048
+ var init_rig = __esm({
1049
+ "../core/src/rig.ts"() {
1050
+ "use strict";
1051
+ init_dsl();
1052
+ }
1053
+ });
1054
+
1055
+ // ../core/src/characterPreset.ts
1056
+ var init_characterPreset = __esm({
1057
+ "../core/src/characterPreset.ts"() {
1058
+ "use strict";
1059
+ init_dsl();
1060
+ init_rig();
1061
+ }
1062
+ });
1063
+
1064
+ // ../core/src/figure.ts
1065
+ var init_figure = __esm({
1066
+ "../core/src/figure.ts"() {
1067
+ "use strict";
1068
+ init_dsl();
1069
+ init_rig();
1070
+ }
1071
+ });
1072
+
1073
+ // ../core/src/motionOps.ts
1074
+ var init_motionOps = __esm({
1075
+ "../core/src/motionOps.ts"() {
1076
+ "use strict";
1077
+ init_dsl();
1078
+ }
1079
+ });
1080
+
905
1081
  // ../core/src/audio.ts
906
1082
  function resolveAudioPlan(compiled) {
907
1083
  const audio = compiled.ir.audio;
@@ -938,6 +1114,15 @@ function resolveAudioPlan(compiled) {
938
1114
  });
939
1115
  }
940
1116
  cues.sort((a, b) => a.t - b.t);
1117
+ return {
1118
+ duration,
1119
+ bgm: resolveBgm(audio.bgm),
1120
+ cues,
1121
+ duckWindows: mergeDuckWindows(cues, duration),
1122
+ warnings
1123
+ };
1124
+ }
1125
+ function mergeDuckWindows(cues, duration) {
941
1126
  const duckWindows = [];
942
1127
  for (const cue of cues) {
943
1128
  const window2 = { t0: cue.t, t1: Math.min(duration, cue.t + cue.duration) };
@@ -945,23 +1130,22 @@ function resolveAudioPlan(compiled) {
945
1130
  if (last && window2.t0 <= last.t1 + 0.1) last.t1 = Math.max(last.t1, window2.t1);
946
1131
  else duckWindows.push(window2);
947
1132
  }
948
- let bgm = null;
949
- if (audio.bgm) {
950
- const b = audio.bgm;
951
- const duck = b.duck === false ? null : {
952
- depth: b.duck?.depth ?? 0.5,
953
- attack: b.duck?.attack ?? 0.05,
954
- release: b.duck?.release ?? 0.25
955
- };
956
- bgm = {
957
- source: b.file ? { kind: "file", path: b.file } : { kind: "synth", name: b.synth ?? "ambient-pad" },
958
- gain: b.gain ?? 0.5,
959
- fadeIn: b.fadeIn ?? 0,
960
- fadeOut: b.fadeOut ?? 0,
961
- duck
962
- };
963
- }
964
- return { duration, bgm, cues, duckWindows, warnings };
1133
+ return duckWindows;
1134
+ }
1135
+ function resolveBgm(b) {
1136
+ if (!b) return null;
1137
+ const duck = b.duck === false ? null : {
1138
+ depth: b.duck?.depth ?? 0.5,
1139
+ attack: b.duck?.attack ?? 0.05,
1140
+ release: b.duck?.release ?? 0.25
1141
+ };
1142
+ return {
1143
+ source: b.file ? { kind: "file", path: b.file } : { kind: "synth", name: b.synth ?? "ambient-pad" },
1144
+ gain: b.gain ?? 0.5,
1145
+ fadeIn: b.fadeIn ?? 0,
1146
+ fadeOut: b.fadeOut ?? 0,
1147
+ duck
1148
+ };
965
1149
  }
966
1150
  var SFX_DURATION, FILE_CUE_DURATION;
967
1151
  var init_audio = __esm({
@@ -1098,10 +1282,16 @@ var init_src = __esm({
1098
1282
  init_ir();
1099
1283
  init_dsl();
1100
1284
  init_validate();
1285
+ init_composeComposition();
1101
1286
  init_compose();
1102
1287
  init_compile();
1103
1288
  init_path();
1104
1289
  init_presets();
1290
+ init_devicePreset();
1291
+ init_rig();
1292
+ init_characterPreset();
1293
+ init_figure();
1294
+ init_motionOps();
1105
1295
  init_audio();
1106
1296
  init_evaluate();
1107
1297
  init_interpolate();
@@ -1200,7 +1390,7 @@ function buildLogoSting(d) {
1200
1390
  const inks = d.paths.map(
1201
1391
  (p, i) => path({ id: `ink-${i}`, d: p.d, originX: vcx, originY: vcy, x: 0, y: 0, stroke: p.fill, strokeWidth: sw, progress: 0 })
1202
1392
  );
1203
- const rig = {
1393
+ const rig2 = {
1204
1394
  group: "logo",
1205
1395
  center: [CX, CY],
1206
1396
  baseScale: fit,
@@ -1220,7 +1410,7 @@ function buildLogoSting(d) {
1220
1410
  ],
1221
1411
  timeline: seq(
1222
1412
  motionPreset(d.motion ?? "reveal-orbit", {
1223
- target: rig,
1413
+ target: rig2,
1224
1414
  ...d.energy !== void 0 && { energy: d.energy },
1225
1415
  ...d.speed !== void 0 && { speed: d.speed },
1226
1416
  ...d.intensity !== void 0 && { intensity: d.intensity },
@@ -2042,18 +2232,16 @@ var init_batch = __esm({
2042
2232
  // ../render-cli/src/loadScene.ts
2043
2233
  var loadScene_exports = {};
2044
2234
  __export(loadScene_exports, {
2235
+ isComposition: () => isComposition,
2236
+ loadModule: () => loadModule,
2045
2237
  loadScene: () => loadScene
2046
2238
  });
2047
2239
  import { build as build2 } from "esbuild";
2048
2240
  import { readFile as readFile5 } from "node:fs/promises";
2049
2241
  import { dirname as dirname6, resolve as resolve3 } from "node:path";
2050
2242
  import { fileURLToPath as fileURLToPath4 } from "node:url";
2051
- async function loadScene(path2) {
2052
- if (path2.endsWith(".json")) {
2053
- const ir = JSON.parse(await readFile5(path2, "utf8"));
2054
- validateScene(ir);
2055
- return ir;
2056
- }
2243
+ async function loadDefault(path2) {
2244
+ if (path2.endsWith(".json")) return JSON.parse(await readFile5(path2, "utf8"));
2057
2245
  let code;
2058
2246
  try {
2059
2247
  const out = await build2({
@@ -2070,15 +2258,33 @@ async function loadScene(path2) {
2070
2258
  });
2071
2259
  code = out.outputFiles[0].text;
2072
2260
  } catch (err) {
2073
- throw new Error(
2074
- `failed to bundle ${path2}:
2075
- ${err instanceof Error ? err.message : String(err)}`
2076
- );
2261
+ throw new Error(`failed to bundle ${path2}:
2262
+ ${err instanceof Error ? err.message : String(err)}`);
2077
2263
  }
2078
2264
  const mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
2079
- if (!mod.default) throw new Error(`${path2} must default-export a scene`);
2265
+ if (mod.default === void 0) throw new Error(`${path2} must default-export a scene or composition`);
2080
2266
  return mod.default;
2081
2267
  }
2268
+ function isComposition(def) {
2269
+ return typeof def === "object" && def !== null && Array.isArray(def.scenes);
2270
+ }
2271
+ async function loadScene(path2) {
2272
+ const def = await loadDefault(path2);
2273
+ if (isComposition(def)) {
2274
+ throw new Error(`${path2} is a composition \u2014 render it directly, not as a single scene`);
2275
+ }
2276
+ validateScene(def);
2277
+ return def;
2278
+ }
2279
+ async function loadModule(path2) {
2280
+ const def = await loadDefault(path2);
2281
+ if (isComposition(def)) {
2282
+ validateComposition(def);
2283
+ return { kind: "composition", ir: def };
2284
+ }
2285
+ validateScene(def);
2286
+ return { kind: "scene", ir: def };
2287
+ }
2082
2288
  var HERE, CORE_ENTRY;
2083
2289
  var init_loadScene = __esm({
2084
2290
  "../render-cli/src/loadScene.ts"() {
@@ -2101,6 +2307,7 @@ var HERE2 = dirname7(fileURLToPath5(import.meta.url));
2101
2307
  var ROOT2 = PACKAGED ? resolve4(HERE2, "..") : resolve4(HERE2, "..", "..", "..");
2102
2308
  var USER_CWD = process.env.INIT_CWD ?? process.cwd();
2103
2309
  var RENDER_CLI = PACKAGED ? join6(ROOT2, "dist", "cli.js") : join6(ROOT2, "packages", "render-cli", "src", "cli.ts");
2310
+ var LABELS = PACKAGED ? join6(ROOT2, "dist", "labels.js") : join6(ROOT2, "packages", "render-cli", "src", "labels.ts");
2104
2311
  var ANALYZE = PACKAGED ? join6(ROOT2, "dist", "analyze.js") : join6(ROOT2, "benchmark", "harness", "motion", "analyze.ts");
2105
2312
  var TRACE = PACKAGED ? join6(ROOT2, "dist", "trace-cli.js") : join6(ROOT2, "benchmark", "harness", "motion", "trace-cli.ts");
2106
2313
  var CMD = PACKAGED ? "reframe" : "pnpm reframe";
@@ -2114,6 +2321,7 @@ usage:
2114
2321
  rise-settle, slide-bank, reveal-orbit, spin-forge)
2115
2322
  ${CMD} preview open the scrub/edit UI (lists scenes in your directory)
2116
2323
  ${CMD} new <scene-name> scaffold <scene-name>.ts in your directory
2324
+ ${CMD} labels <scene.ts|.json> print the event clock (label \u2192 exact seconds; for sound design / timing)
2117
2325
  ${CMD} motion <mp4|framesDir> motion-profile a rendered clip
2118
2326
  ${CMD} trace <ref.mp4> [--apply scene.ts] extract a video's motion structure \u2192 MotionSketch / timeline
2119
2327
  ${CMD} guide [--regen] print the scene-authoring guide (for you or your AI)
@@ -2244,6 +2452,17 @@ ${USAGE}`);
2244
2452
  await (PACKAGED ? run(process.execPath, [RENDER_CLI, mode, inputPath, ...outArgs]) : run("npx", ["tsx", RENDER_CLI, mode, inputPath, ...outArgs]))
2245
2453
  );
2246
2454
  }
2455
+ case "labels": {
2456
+ const input = rest[0];
2457
+ if (!input || input.startsWith("-")) fail(`labels needs a scene file
2458
+
2459
+ ${USAGE}`);
2460
+ const inputPath = userPath(input);
2461
+ if (!existsSync4(inputPath)) fail(`no such file: ${inputPath}`);
2462
+ process.exit(
2463
+ await (PACKAGED ? run(process.execPath, [LABELS, inputPath]) : run("npx", ["tsx", LABELS, inputPath]))
2464
+ );
2465
+ }
2247
2466
  case "logo": {
2248
2467
  const positional = [];
2249
2468
  const flags = {};
@@ -2332,6 +2551,9 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
2332
2551
  process.exit(failed > 0 ? 1 : 0);
2333
2552
  }
2334
2553
  case "preview": {
2554
+ console.log(
2555
+ "preview: drag waypoints/nodes; double-click a path to add a waypoint or a handle to remove it;\n \u270E reshapes an ease curve; 'vary \xD74' proposes motion variants.\ndeep-link a scene + time: http://localhost:5173/?scene=<scene-name>&t=<seconds>"
2556
+ );
2335
2557
  if (PACKAGED) {
2336
2558
  const { createRequire } = await import("node:module");
2337
2559
  const vitePkg = createRequire(import.meta.url).resolve("vite/package.json");