reframe-video 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/assets/sfx/LICENSE.md +1 -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/glass_001.ogg +0 -0
  8. package/assets/sfx/maximize_001.ogg +0 -0
  9. package/assets/sfx/maximize_002.ogg +0 -0
  10. package/assets/sfx/maximize_005.ogg +0 -0
  11. package/assets/sfx/maximize_009.ogg +0 -0
  12. package/assets/sfx/open_001.ogg +0 -0
  13. package/assets/sfx/pluck_001.ogg +0 -0
  14. package/assets/sfx/pluck_002.ogg +0 -0
  15. package/assets/sfx/select_001.ogg +0 -0
  16. package/assets/sfx/select_002.ogg +0 -0
  17. package/assets/sfx/select_003.ogg +0 -0
  18. package/dist/bin.js +240 -47
  19. package/dist/browserEntry.js +130 -68
  20. package/dist/cli.js +445 -85
  21. package/dist/index.js +674 -86
  22. package/dist/labels.js +606 -0
  23. package/dist/renderer-canvas.js +15 -0
  24. package/dist/trace-cli.js +9 -9
  25. package/dist/types/audio.d.ts +9 -0
  26. package/dist/types/compile.d.ts +1 -0
  27. package/dist/types/compose.d.ts +18 -2
  28. package/dist/types/composeComposition.d.ts +27 -0
  29. package/dist/types/devicePreset.d.ts +65 -0
  30. package/dist/types/dsl.d.ts +12 -1
  31. package/dist/types/evaluate.d.ts +32 -0
  32. package/dist/types/index.d.ts +6 -3
  33. package/dist/types/ir.d.ts +68 -0
  34. package/dist/types/motionOps.d.ts +36 -0
  35. package/dist/types/path.d.ts +7 -3
  36. package/dist/types/validate.d.ts +4 -1
  37. package/guides/edsl-guide.md +2 -1
  38. package/package.json +1 -1
  39. package/preview/index.html +56 -3
  40. package/preview/src/main.ts +1132 -46
  41. package/preview/src/panel.ts +478 -8
  42. package/preview/src/store.ts +323 -6
@@ -5,7 +5,7 @@ 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 |
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,22 @@ 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/motionOps.ts
1048
+ var init_motionOps = __esm({
1049
+ "../core/src/motionOps.ts"() {
1050
+ "use strict";
1051
+ init_dsl();
1052
+ }
1053
+ });
1054
+
905
1055
  // ../core/src/audio.ts
906
1056
  function resolveAudioPlan(compiled) {
907
1057
  const audio = compiled.ir.audio;
@@ -938,6 +1088,15 @@ function resolveAudioPlan(compiled) {
938
1088
  });
939
1089
  }
940
1090
  cues.sort((a, b) => a.t - b.t);
1091
+ return {
1092
+ duration,
1093
+ bgm: resolveBgm(audio.bgm),
1094
+ cues,
1095
+ duckWindows: mergeDuckWindows(cues, duration),
1096
+ warnings
1097
+ };
1098
+ }
1099
+ function mergeDuckWindows(cues, duration) {
941
1100
  const duckWindows = [];
942
1101
  for (const cue of cues) {
943
1102
  const window2 = { t0: cue.t, t1: Math.min(duration, cue.t + cue.duration) };
@@ -945,23 +1104,22 @@ function resolveAudioPlan(compiled) {
945
1104
  if (last && window2.t0 <= last.t1 + 0.1) last.t1 = Math.max(last.t1, window2.t1);
946
1105
  else duckWindows.push(window2);
947
1106
  }
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 };
1107
+ return duckWindows;
1108
+ }
1109
+ function resolveBgm(b) {
1110
+ if (!b) return null;
1111
+ const duck = b.duck === false ? null : {
1112
+ depth: b.duck?.depth ?? 0.5,
1113
+ attack: b.duck?.attack ?? 0.05,
1114
+ release: b.duck?.release ?? 0.25
1115
+ };
1116
+ return {
1117
+ source: b.file ? { kind: "file", path: b.file } : { kind: "synth", name: b.synth ?? "ambient-pad" },
1118
+ gain: b.gain ?? 0.5,
1119
+ fadeIn: b.fadeIn ?? 0,
1120
+ fadeOut: b.fadeOut ?? 0,
1121
+ duck
1122
+ };
965
1123
  }
966
1124
  var SFX_DURATION, FILE_CUE_DURATION;
967
1125
  var init_audio = __esm({
@@ -1098,10 +1256,13 @@ var init_src = __esm({
1098
1256
  init_ir();
1099
1257
  init_dsl();
1100
1258
  init_validate();
1259
+ init_composeComposition();
1101
1260
  init_compose();
1102
1261
  init_compile();
1103
1262
  init_path();
1104
1263
  init_presets();
1264
+ init_devicePreset();
1265
+ init_motionOps();
1105
1266
  init_audio();
1106
1267
  init_evaluate();
1107
1268
  init_interpolate();
@@ -2042,18 +2203,16 @@ var init_batch = __esm({
2042
2203
  // ../render-cli/src/loadScene.ts
2043
2204
  var loadScene_exports = {};
2044
2205
  __export(loadScene_exports, {
2206
+ isComposition: () => isComposition,
2207
+ loadModule: () => loadModule,
2045
2208
  loadScene: () => loadScene
2046
2209
  });
2047
2210
  import { build as build2 } from "esbuild";
2048
2211
  import { readFile as readFile5 } from "node:fs/promises";
2049
2212
  import { dirname as dirname6, resolve as resolve3 } from "node:path";
2050
2213
  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
- }
2214
+ async function loadDefault(path2) {
2215
+ if (path2.endsWith(".json")) return JSON.parse(await readFile5(path2, "utf8"));
2057
2216
  let code;
2058
2217
  try {
2059
2218
  const out = await build2({
@@ -2070,15 +2229,33 @@ async function loadScene(path2) {
2070
2229
  });
2071
2230
  code = out.outputFiles[0].text;
2072
2231
  } catch (err) {
2073
- throw new Error(
2074
- `failed to bundle ${path2}:
2075
- ${err instanceof Error ? err.message : String(err)}`
2076
- );
2232
+ throw new Error(`failed to bundle ${path2}:
2233
+ ${err instanceof Error ? err.message : String(err)}`);
2077
2234
  }
2078
2235
  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`);
2236
+ if (mod.default === void 0) throw new Error(`${path2} must default-export a scene or composition`);
2080
2237
  return mod.default;
2081
2238
  }
2239
+ function isComposition(def) {
2240
+ return typeof def === "object" && def !== null && Array.isArray(def.scenes);
2241
+ }
2242
+ async function loadScene(path2) {
2243
+ const def = await loadDefault(path2);
2244
+ if (isComposition(def)) {
2245
+ throw new Error(`${path2} is a composition \u2014 render it directly, not as a single scene`);
2246
+ }
2247
+ validateScene(def);
2248
+ return def;
2249
+ }
2250
+ async function loadModule(path2) {
2251
+ const def = await loadDefault(path2);
2252
+ if (isComposition(def)) {
2253
+ validateComposition(def);
2254
+ return { kind: "composition", ir: def };
2255
+ }
2256
+ validateScene(def);
2257
+ return { kind: "scene", ir: def };
2258
+ }
2082
2259
  var HERE, CORE_ENTRY;
2083
2260
  var init_loadScene = __esm({
2084
2261
  "../render-cli/src/loadScene.ts"() {
@@ -2101,6 +2278,7 @@ var HERE2 = dirname7(fileURLToPath5(import.meta.url));
2101
2278
  var ROOT2 = PACKAGED ? resolve4(HERE2, "..") : resolve4(HERE2, "..", "..", "..");
2102
2279
  var USER_CWD = process.env.INIT_CWD ?? process.cwd();
2103
2280
  var RENDER_CLI = PACKAGED ? join6(ROOT2, "dist", "cli.js") : join6(ROOT2, "packages", "render-cli", "src", "cli.ts");
2281
+ var LABELS = PACKAGED ? join6(ROOT2, "dist", "labels.js") : join6(ROOT2, "packages", "render-cli", "src", "labels.ts");
2104
2282
  var ANALYZE = PACKAGED ? join6(ROOT2, "dist", "analyze.js") : join6(ROOT2, "benchmark", "harness", "motion", "analyze.ts");
2105
2283
  var TRACE = PACKAGED ? join6(ROOT2, "dist", "trace-cli.js") : join6(ROOT2, "benchmark", "harness", "motion", "trace-cli.ts");
2106
2284
  var CMD = PACKAGED ? "reframe" : "pnpm reframe";
@@ -2114,6 +2292,7 @@ usage:
2114
2292
  rise-settle, slide-bank, reveal-orbit, spin-forge)
2115
2293
  ${CMD} preview open the scrub/edit UI (lists scenes in your directory)
2116
2294
  ${CMD} new <scene-name> scaffold <scene-name>.ts in your directory
2295
+ ${CMD} labels <scene.ts|.json> print the event clock (label \u2192 exact seconds; for sound design / timing)
2117
2296
  ${CMD} motion <mp4|framesDir> motion-profile a rendered clip
2118
2297
  ${CMD} trace <ref.mp4> [--apply scene.ts] extract a video's motion structure \u2192 MotionSketch / timeline
2119
2298
  ${CMD} guide [--regen] print the scene-authoring guide (for you or your AI)
@@ -2244,6 +2423,17 @@ ${USAGE}`);
2244
2423
  await (PACKAGED ? run(process.execPath, [RENDER_CLI, mode, inputPath, ...outArgs]) : run("npx", ["tsx", RENDER_CLI, mode, inputPath, ...outArgs]))
2245
2424
  );
2246
2425
  }
2426
+ case "labels": {
2427
+ const input = rest[0];
2428
+ if (!input || input.startsWith("-")) fail(`labels needs a scene file
2429
+
2430
+ ${USAGE}`);
2431
+ const inputPath = userPath(input);
2432
+ if (!existsSync4(inputPath)) fail(`no such file: ${inputPath}`);
2433
+ process.exit(
2434
+ await (PACKAGED ? run(process.execPath, [LABELS, inputPath]) : run("npx", ["tsx", LABELS, inputPath]))
2435
+ );
2436
+ }
2247
2437
  case "logo": {
2248
2438
  const positional = [];
2249
2439
  const flags = {};
@@ -2332,6 +2522,9 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
2332
2522
  process.exit(failed > 0 ? 1 : 0);
2333
2523
  }
2334
2524
  case "preview": {
2525
+ console.log(
2526
+ "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>"
2527
+ );
2335
2528
  if (PACKAGED) {
2336
2529
  const { createRequire } = await import("node:module");
2337
2530
  const vitePkg = createRequire(import.meta.url).resolve("vite/package.json");