reframe-video 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.
package/dist/bin.js CHANGED
@@ -10,16 +10,100 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // ../core/src/ir.ts
13
- var DEFAULT_TO_DURATION, DEFAULT_TWEEN_DURATION;
13
+ var DEFAULT_TO_DURATION, DEFAULT_TWEEN_DURATION, DEFAULT_MOTIONPATH_DURATION;
14
14
  var init_ir = __esm({
15
15
  "../core/src/ir.ts"() {
16
16
  "use strict";
17
17
  DEFAULT_TO_DURATION = 0.5;
18
18
  DEFAULT_TWEEN_DURATION = 0.5;
19
+ DEFAULT_MOTIONPATH_DURATION = 1;
20
+ }
21
+ });
22
+
23
+ // ../core/src/path.ts
24
+ function locate(segCount, u) {
25
+ if (segCount <= 0) return { i: 0, t: 0 };
26
+ const clamped = Math.max(0, Math.min(1, u));
27
+ const scaled = clamped * segCount;
28
+ let i = Math.floor(scaled);
29
+ if (i >= segCount) i = segCount - 1;
30
+ return { i, t: scaled - i };
31
+ }
32
+ function controls(points, closed, i) {
33
+ const n = points.length;
34
+ const at = (k) => {
35
+ if (closed) return points[(k % n + n) % n];
36
+ return points[Math.max(0, Math.min(n - 1, k))];
37
+ };
38
+ return [at(i - 1), at(i), at(i + 1), at(i + 2)];
39
+ }
40
+ function segCountOf(points, closed) {
41
+ const n = points.length;
42
+ if (n < 2) return 0;
43
+ return closed ? n : n - 1;
44
+ }
45
+ function pathPoint(points, closed, u) {
46
+ const n = points.length;
47
+ if (n === 0) return [0, 0];
48
+ if (n === 1) return [points[0][0], points[0][1]];
49
+ const segs = segCountOf(points, closed);
50
+ const { i, t } = locate(segs, u);
51
+ const [p0, p1, p2, p3] = controls(points, closed, i);
52
+ const t2 = t * t;
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) {
58
+ const n = points.length;
59
+ if (n < 2) return 0;
60
+ const segs = segCountOf(points, closed);
61
+ const { i, t } = locate(segs, u);
62
+ const [p0, p1, p2, p3] = controls(points, closed, i);
63
+ 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]);
67
+ if (dx === 0 && dy === 0) return 0;
68
+ return Math.atan2(dy, dx) * 180 / Math.PI;
69
+ }
70
+ var init_path = __esm({
71
+ "../core/src/path.ts"() {
72
+ "use strict";
19
73
  }
20
74
  });
21
75
 
22
76
  // ../core/src/compile.ts
77
+ function scaleTimeline(tl, k) {
78
+ switch (tl.kind) {
79
+ case "seq":
80
+ case "par":
81
+ return { ...tl, children: tl.children.map((c) => scaleTimeline(c, k)) };
82
+ case "stagger":
83
+ return { ...tl, interval: tl.interval * k, children: tl.children.map((c) => scaleTimeline(c, k)) };
84
+ case "wait":
85
+ return { ...tl, duration: tl.duration * k };
86
+ case "tween":
87
+ return { ...tl, duration: (tl.duration ?? DEFAULT_TWEEN_DURATION) * k };
88
+ case "motionPath":
89
+ return { ...tl, duration: (tl.duration ?? DEFAULT_MOTIONPATH_DURATION) * k };
90
+ case "to":
91
+ return {
92
+ ...tl,
93
+ duration: (tl.duration ?? DEFAULT_TO_DURATION) * k,
94
+ ...tl.stagger !== void 0 && { stagger: tl.stagger * k }
95
+ };
96
+ case "beat":
97
+ return {
98
+ ...tl,
99
+ children: tl.children.map((c) => scaleTimeline(c, k)),
100
+ ...tl.gap !== void 0 && { gap: tl.gap * k }
101
+ };
102
+ }
103
+ }
104
+ function orderBeats(children) {
105
+ return children.map((c, i) => ({ c, i, key: c.kind === "beat" && c.order !== void 0 ? c.order : i })).sort((a, b) => a.key - b.key || a.i - b.i).map((x) => x.c);
106
+ }
23
107
  function compileScene(ir) {
24
108
  const nodeById = /* @__PURE__ */ new Map();
25
109
  const nodeOrder = [];
@@ -48,6 +132,7 @@ function compileScene(ir) {
48
132
  }
49
133
  }
50
134
  const segments = /* @__PURE__ */ new Map();
135
+ const motionPaths = /* @__PURE__ */ new Map();
51
136
  const current = new Map(initialValues);
52
137
  const pushSegment = (seg) => {
53
138
  const k = key(seg.target, seg.prop);
@@ -64,6 +149,50 @@ function compileScene(ir) {
64
149
  throw new Error(`cannot animate "${prop}" of "${target}": no base value to start from`);
65
150
  };
66
151
  const labelTimes = /* @__PURE__ */ new Map();
152
+ const beatTimes = /* @__PURE__ */ new Map();
153
+ const durationOf = (tl, start) => {
154
+ switch (tl.kind) {
155
+ case "seq": {
156
+ let t = start;
157
+ for (const child of orderBeats(tl.children)) t = durationOf(child, t);
158
+ return t;
159
+ }
160
+ case "par": {
161
+ let end = start;
162
+ for (const child of tl.children) end = Math.max(end, durationOf(child, start));
163
+ return end;
164
+ }
165
+ case "stagger": {
166
+ let end = start;
167
+ tl.children.forEach((child, i) => {
168
+ end = Math.max(end, durationOf(child, start + i * tl.interval));
169
+ });
170
+ return end;
171
+ }
172
+ case "wait":
173
+ return start + tl.duration;
174
+ case "tween":
175
+ return start + (tl.duration ?? DEFAULT_TWEEN_DURATION);
176
+ case "motionPath":
177
+ return start + (tl.duration ?? DEFAULT_MOTIONPATH_DURATION);
178
+ case "to": {
179
+ const override = ir.states?.[tl.state] ?? {};
180
+ const duration = tl.duration ?? DEFAULT_TO_DURATION;
181
+ const si = tl.stagger ?? 0;
182
+ const targets = nodeOrder.filter(
183
+ (id) => id in override && (tl.filter === void 0 || tl.filter.includes(id))
184
+ );
185
+ return start + duration + Math.max(0, targets.length - 1) * si;
186
+ }
187
+ case "beat": {
188
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
189
+ const natural = durationOf(grouping, 0);
190
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, natural) : 1);
191
+ const beatStart = tl.at ?? start + (tl.gap ?? 0);
192
+ return beatStart + k * natural;
193
+ }
194
+ }
195
+ };
67
196
  const walk = (tl, start) => {
68
197
  const end = walkInner(tl, start);
69
198
  if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
@@ -73,9 +202,19 @@ function compileScene(ir) {
73
202
  switch (tl.kind) {
74
203
  case "seq": {
75
204
  let t = start;
76
- for (const child of tl.children) t = walk(child, t);
205
+ for (const child of orderBeats(tl.children)) t = walk(child, t);
77
206
  return t;
78
207
  }
208
+ case "beat": {
209
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
210
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
211
+ const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
212
+ const beatStart = tl.at ?? start + (tl.gap ?? 0);
213
+ const end = walk(inner, beatStart);
214
+ beatTimes.set(tl.name, { t0: beatStart, t1: end });
215
+ labelTimes.set(tl.name, { t0: beatStart, t1: end });
216
+ return end;
217
+ }
79
218
  case "par": {
80
219
  let end = start;
81
220
  for (const child of tl.children) end = Math.max(end, walk(child, start));
@@ -105,6 +244,23 @@ function compileScene(ir) {
105
244
  }
106
245
  return start + duration;
107
246
  }
247
+ case "motionPath": {
248
+ const duration = tl.duration ?? DEFAULT_MOTIONPATH_DURATION;
249
+ const points = tl.points;
250
+ const closed = tl.closed ?? false;
251
+ const autoRotate = tl.autoRotate ?? false;
252
+ const rotateOffset = tl.rotateOffset ?? 0;
253
+ let list = motionPaths.get(tl.target);
254
+ 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 } });
256
+ if (points.length > 0) {
257
+ const [ex, ey] = pathPoint(points, closed, 1);
258
+ current.set(key(tl.target, "x"), ex);
259
+ current.set(key(tl.target, "y"), ey);
260
+ if (autoRotate) current.set(key(tl.target, "rotation"), pathTangentAngle(points, closed, 1) + rotateOffset);
261
+ }
262
+ return start + duration;
263
+ }
108
264
  case "to": {
109
265
  const override = ir.states?.[tl.state] ?? {};
110
266
  const duration = tl.duration ?? DEFAULT_TO_DURATION;
@@ -133,14 +289,17 @@ function compileScene(ir) {
133
289
  };
134
290
  const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
135
291
  for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
292
+ for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
136
293
  return {
137
294
  ir,
138
295
  duration: ir.duration ?? inferredEnd,
139
296
  segments,
297
+ motionPaths,
140
298
  initialValues,
141
299
  nodeById,
142
300
  nodeOrder,
143
- labelTimes
301
+ labelTimes,
302
+ beatTimes
144
303
  };
145
304
  }
146
305
  var key;
@@ -148,6 +307,7 @@ var init_compile = __esm({
148
307
  "../core/src/compile.ts"() {
149
308
  "use strict";
150
309
  init_ir();
310
+ init_path();
151
311
  key = (target, prop) => `${target}.${prop}`;
152
312
  }
153
313
  });
@@ -232,9 +392,39 @@ function validateScene(ir) {
232
392
  problems.push(`${path}: tween duration must be > 0`);
233
393
  }
234
394
  break;
395
+ case "motionPath": {
396
+ const node = nodeById.get(tl.target);
397
+ if (!node) {
398
+ problems.push(
399
+ `${path}: motionPath targets unknown node "${tl.target}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
400
+ );
401
+ } else if (node.type === "line") {
402
+ problems.push(`${path}: motionPath cannot target a line (no x/y) \u2014 "${tl.target}"`);
403
+ }
404
+ if (tl.points.length < 1) problems.push(`${path}: motionPath "${tl.target}" needs at least 1 point`);
405
+ if (tl.duration !== void 0 && tl.duration <= 0) {
406
+ problems.push(`${path}: motionPath "${tl.target}" duration must be > 0`);
407
+ }
408
+ break;
409
+ }
235
410
  case "wait":
236
411
  if (tl.duration < 0) problems.push(`${path}: wait duration must be >= 0`);
237
412
  break;
413
+ case "beat":
414
+ if (labels.has(tl.name)) {
415
+ problems.push(
416
+ `${path}: duplicate timeline label "${tl.name}" (beat name) \u2014 labels are overlay addresses and must be unique`
417
+ );
418
+ }
419
+ labels.add(tl.name);
420
+ if (tl.duration !== void 0 && tl.duration <= 0) {
421
+ problems.push(`${path}: beat "${tl.name}" duration must be > 0`);
422
+ }
423
+ if (tl.scale !== void 0 && tl.scale <= 0) {
424
+ problems.push(`${path}: beat "${tl.name}" scale must be > 0`);
425
+ }
426
+ tl.children.forEach((c, i) => checkTimeline(c, `${path}.beat(${tl.name})[${i}]`));
427
+ break;
238
428
  }
239
429
  };
240
430
  if (ir.timeline) checkTimeline(ir.timeline, "timeline");
@@ -283,6 +473,8 @@ var init_validate = __esm({
283
473
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
284
474
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
285
475
  text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
476
+ image: [...COMMON_PROPS, "src", "width", "height"],
477
+ path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
286
478
  group: COMMON_PROPS
287
479
  };
288
480
  SceneValidationError = class extends Error {
@@ -429,13 +621,16 @@ function applyOverlay(ir, overlay, layer, report) {
429
621
  const byLabel = /* @__PURE__ */ new Map();
430
622
  const walkTimeline = (tl) => {
431
623
  if ("label" in tl && tl.label !== void 0) byLabel.set(tl.label, tl);
624
+ if (tl.kind === "beat") byLabel.set(tl.name, tl);
432
625
  if ("children" in tl) tl.children.forEach(walkTimeline);
433
626
  };
434
627
  if (ir.timeline) walkTimeline(ir.timeline);
435
628
  const PATCHABLE = {
436
629
  to: ["duration", "ease", "stagger"],
437
630
  tween: ["duration", "ease"],
438
- wait: ["duration"]
631
+ wait: ["duration"],
632
+ motionPath: ["points", "duration", "ease"],
633
+ beat: ["at", "gap", "scale", "duration", "order"]
439
634
  };
440
635
  let timingPatched = false;
441
636
  for (const [label, patch] of Object.entries(overlay.timeline)) {
@@ -459,7 +654,7 @@ function applyOverlay(ir, overlay, layer, report) {
459
654
  }
460
655
  step[key2] = value;
461
656
  applied(`timeline.${label}.${key2}`, "set");
462
- if (key2 === "duration" || key2 === "stagger") timingPatched = true;
657
+ if (["duration", "stagger", "at", "gap", "scale", "order"].includes(key2)) timingPatched = true;
463
658
  }
464
659
  }
465
660
  if (timingPatched && overlay.scene?.duration === void 0) {
@@ -483,6 +678,16 @@ var init_compose = __esm({
483
678
  }
484
679
  });
485
680
 
681
+ // ../core/src/presets.ts
682
+ var SET;
683
+ var init_presets = __esm({
684
+ "../core/src/presets.ts"() {
685
+ "use strict";
686
+ init_dsl();
687
+ SET = 1 / 120;
688
+ }
689
+ });
690
+
486
691
  // ../core/src/audio.ts
487
692
  function resolveAudioPlan(compiled) {
488
693
  const audio = compiled.ir.audio;
@@ -568,10 +773,23 @@ var init_behaviors = __esm({
568
773
  });
569
774
 
570
775
  // ../core/src/interpolate.ts
571
- var EASE_TABLE, EASE_NAMES;
776
+ function easeOutBounce(u) {
777
+ const n1 = 7.5625;
778
+ const d1 = 2.75;
779
+ if (u < 1 / d1) return n1 * u * u;
780
+ if (u < 2 / d1) return n1 * (u -= 1.5 / d1) * u + 0.75;
781
+ if (u < 2.5 / d1) return n1 * (u -= 2.25 / d1) * u + 0.9375;
782
+ return n1 * (u -= 2.625 / d1) * u + 0.984375;
783
+ }
784
+ var BACK_C1, BACK_C2, BACK_C3, ELASTIC_C4, ELASTIC_C5, EASE_TABLE, EASE_NAMES;
572
785
  var init_interpolate = __esm({
573
786
  "../core/src/interpolate.ts"() {
574
787
  "use strict";
788
+ BACK_C1 = 1.70158;
789
+ BACK_C2 = BACK_C1 * 1.525;
790
+ BACK_C3 = BACK_C1 + 1;
791
+ ELASTIC_C4 = 2 * Math.PI / 3;
792
+ ELASTIC_C5 = 2 * Math.PI / 4.5;
575
793
  EASE_TABLE = {
576
794
  linear: (u) => u,
577
795
  easeInQuad: (u) => u * u,
@@ -585,7 +803,20 @@ var init_interpolate = __esm({
585
803
  easeInOutQuart: (u) => u < 0.5 ? 8 * u ** 4 : 1 - (-2 * u + 2) ** 4 / 2,
586
804
  easeInExpo: (u) => u === 0 ? 0 : 2 ** (10 * u - 10),
587
805
  easeOutExpo: (u) => u === 1 ? 1 : 1 - 2 ** (-10 * u),
588
- easeInOutExpo: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? 2 ** (20 * u - 10) / 2 : (2 - 2 ** (-20 * u + 10)) / 2
806
+ easeInOutExpo: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? 2 ** (20 * u - 10) / 2 : (2 - 2 ** (-20 * u + 10)) / 2,
807
+ // --- expressive eases (GSAP's signature feel) — standard Penner equations ---
808
+ // back: overshoots past the target then settles (pop / snap)
809
+ easeInBack: (u) => BACK_C3 * u ** 3 - BACK_C1 * u * u,
810
+ easeOutBack: (u) => 1 + BACK_C3 * (u - 1) ** 3 + BACK_C1 * (u - 1) ** 2,
811
+ easeInOutBack: (u) => u < 0.5 ? (2 * u) ** 2 * ((BACK_C2 + 1) * 2 * u - BACK_C2) / 2 : ((2 * u - 2) ** 2 * ((BACK_C2 + 1) * (2 * u - 2) + BACK_C2) + 2) / 2,
812
+ // elastic: rings around the target before settling (playful spring)
813
+ easeInElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : -(2 ** (10 * u - 10)) * Math.sin((u * 10 - 10.75) * ELASTIC_C4),
814
+ easeOutElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : 2 ** (-10 * u) * Math.sin((u * 10 - 0.75) * ELASTIC_C4) + 1,
815
+ easeInOutElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? -(2 ** (20 * u - 10) * Math.sin((20 * u - 11.125) * ELASTIC_C5)) / 2 : 2 ** (-20 * u + 10) * Math.sin((20 * u - 11.125) * ELASTIC_C5) / 2 + 1,
816
+ // bounce: drops and bounces to rest (lands without overshoot)
817
+ easeInBounce: (u) => 1 - easeOutBounce(1 - u),
818
+ easeOutBounce,
819
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
589
820
  };
590
821
  EASE_NAMES = Object.keys(EASE_TABLE);
591
822
  }
@@ -597,6 +828,52 @@ var init_evaluate = __esm({
597
828
  "use strict";
598
829
  init_behaviors();
599
830
  init_interpolate();
831
+ init_path();
832
+ }
833
+ });
834
+
835
+ // ../core/src/assets.ts
836
+ function collectImageSrcs(ir) {
837
+ const srcs = /* @__PURE__ */ new Set();
838
+ const imageIds = /* @__PURE__ */ new Set();
839
+ const walkNodes = (nodes) => {
840
+ for (const node of nodes) {
841
+ if (node.type === "image") {
842
+ imageIds.add(node.id);
843
+ srcs.add(node.props.src);
844
+ }
845
+ if (node.type === "group") walkNodes(node.children);
846
+ }
847
+ };
848
+ walkNodes(ir.nodes);
849
+ for (const overrides of Object.values(ir.states ?? {})) {
850
+ for (const [nodeId, props] of Object.entries(overrides)) {
851
+ if (imageIds.has(nodeId) && typeof props.src === "string") srcs.add(props.src);
852
+ }
853
+ }
854
+ const walkTimeline = (step) => {
855
+ if (!step) return;
856
+ if (step.kind === "seq" || step.kind === "par" || step.kind === "stagger") {
857
+ for (const child of step.children) walkTimeline(child);
858
+ } else if (step.kind === "tween" && imageIds.has(step.target)) {
859
+ const src = step.props.src;
860
+ if (typeof src === "string") srcs.add(src);
861
+ }
862
+ };
863
+ walkTimeline(ir.timeline);
864
+ return [...srcs];
865
+ }
866
+ var init_assets = __esm({
867
+ "../core/src/assets.ts"() {
868
+ "use strict";
869
+ }
870
+ });
871
+
872
+ // ../core/src/motion.ts
873
+ var init_motion = __esm({
874
+ "../core/src/motion.ts"() {
875
+ "use strict";
876
+ init_dsl();
600
877
  }
601
878
  });
602
879
 
@@ -609,10 +886,14 @@ var init_src = __esm({
609
886
  init_validate();
610
887
  init_compose();
611
888
  init_compile();
889
+ init_path();
890
+ init_presets();
612
891
  init_audio();
613
892
  init_evaluate();
614
893
  init_interpolate();
615
894
  init_behaviors();
895
+ init_assets();
896
+ init_motion();
616
897
  }
617
898
  });
618
899
 
@@ -988,12 +1269,12 @@ async function encodeMp4(framesDir, fps, outFile) {
988
1269
  "+faststart",
989
1270
  outFile
990
1271
  ];
991
- await new Promise((resolve4, reject) => {
1272
+ await new Promise((resolve5, reject) => {
992
1273
  const proc = spawn2("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
993
1274
  let stderr = "";
994
1275
  proc.stderr.on("data", (d) => stderr += d.toString());
995
1276
  proc.on("close", (code) => {
996
- if (code === 0) resolve4();
1277
+ if (code === 0) resolve5();
997
1278
  else reject(new Error(`ffmpeg exited with ${code}:
998
1279
  ${stderr.slice(-2e3)}`));
999
1280
  });
@@ -1036,6 +1317,45 @@ var init_fonts = __esm({
1036
1317
  }
1037
1318
  });
1038
1319
 
1320
+ // ../render-cli/src/images.ts
1321
+ import { readFile as readFile2 } from "node:fs/promises";
1322
+ import { existsSync as existsSync2 } from "node:fs";
1323
+ import { extname, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
1324
+ async function buildImageAssets(ir, sceneDir) {
1325
+ const assets = {};
1326
+ for (const src of collectImageSrcs(ir)) {
1327
+ const mime = MIME[extname(src).toLowerCase()];
1328
+ if (!mime) {
1329
+ throw new Error(
1330
+ `image "${src}": unsupported format "${extname(src)}" \u2014 supported: ${Object.keys(MIME).join(" ")}`
1331
+ );
1332
+ }
1333
+ const candidates = [isAbsolute2(src) ? src : null, resolve2(sceneDir, src)].filter(
1334
+ (c) => c !== null
1335
+ );
1336
+ const found = candidates.find((c) => existsSync2(c));
1337
+ if (!found) {
1338
+ throw new Error(`image "${src}" not found (tried: ${candidates.join(", ")})`);
1339
+ }
1340
+ const data = await readFile2(found);
1341
+ assets[src] = `data:${mime};base64,${data.toString("base64")}`;
1342
+ }
1343
+ return assets;
1344
+ }
1345
+ var MIME;
1346
+ var init_images = __esm({
1347
+ "../render-cli/src/images.ts"() {
1348
+ "use strict";
1349
+ init_src();
1350
+ MIME = {
1351
+ ".png": "image/png",
1352
+ ".jpg": "image/jpeg",
1353
+ ".jpeg": "image/jpeg",
1354
+ ".webp": "image/webp"
1355
+ };
1356
+ }
1357
+ });
1358
+
1039
1359
  // ../render-cli/src/vclock.ts
1040
1360
  var VCLOCK_SOURCE;
1041
1361
  var init_vclock = __esm({
@@ -1141,8 +1461,8 @@ async function withPage(size, fn) {
1141
1461
  async function browserBundle() {
1142
1462
  if (bundleCache) return bundleCache;
1143
1463
  if (true) {
1144
- const { readFile: readFile4 } = await import("node:fs/promises");
1145
- bundleCache = await readFile4(
1464
+ const { readFile: readFile5 } = await import("node:fs/promises");
1465
+ bundleCache = await readFile5(
1146
1466
  join4(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.js"),
1147
1467
  "utf8"
1148
1468
  );
@@ -1161,6 +1481,7 @@ async function browserBundle() {
1161
1481
  }
1162
1482
  async function captureIr(ir, opts) {
1163
1483
  await mkdir2(opts.framesDir, { recursive: true });
1484
+ const assets = await buildImageAssets(ir, opts.sceneDir ?? process.cwd());
1164
1485
  const bundle = await browserBundle();
1165
1486
  return withPage(ir.size, async (page) => {
1166
1487
  await page.setContent(
@@ -1169,8 +1490,8 @@ async function captureIr(ir, opts) {
1169
1490
  await injectFonts(page);
1170
1491
  await page.addScriptTag({ content: bundle });
1171
1492
  const info = await page.evaluate(
1172
- (sceneIr) => window.__reframe.init(sceneIr),
1173
- ir
1493
+ ([sceneIr, imageAssets]) => window.__reframe.init(sceneIr, imageAssets),
1494
+ [ir, assets]
1174
1495
  );
1175
1496
  const fps = opts.fps ?? info.fps;
1176
1497
  const duration = opts.duration ?? info.duration;
@@ -1187,6 +1508,7 @@ var init_frameLoop = __esm({
1187
1508
  "../render-cli/src/frameLoop.ts"() {
1188
1509
  "use strict";
1189
1510
  init_fonts();
1511
+ init_images();
1190
1512
  init_vclock();
1191
1513
  init_reframeGlobal();
1192
1514
  framePath = (dir, i) => join4(dir, `${String(i).padStart(5, "0")}.png`);
@@ -1202,9 +1524,9 @@ __export(batch_exports, {
1202
1524
  parseCsv: () => parseCsv,
1203
1525
  runBatch: () => runBatch
1204
1526
  });
1205
- import { mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile2, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
1527
+ import { mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile3, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
1206
1528
  import { tmpdir as tmpdir3 } from "node:os";
1207
- import { join as join5 } from "node:path";
1529
+ import { join as join5, dirname as dirname5 } from "node:path";
1208
1530
  function overlayFromFlat(row, name) {
1209
1531
  const doc = { reframeOverlay: 1, name };
1210
1532
  for (const [key2, raw] of Object.entries(row)) {
@@ -1278,7 +1600,7 @@ function parseCsv(text) {
1278
1600
  });
1279
1601
  }
1280
1602
  async function loadRows(path) {
1281
- const text = await readFile2(path, "utf8");
1603
+ const text = await readFile3(path, "utf8");
1282
1604
  if (path.endsWith(".csv")) return parseCsv(text);
1283
1605
  const parsed = JSON.parse(text);
1284
1606
  if (!Array.isArray(parsed)) throw new Error(`${path}: expected a JSON array of row objects`);
@@ -1304,6 +1626,7 @@ async function runBatch(scene, rows, opts) {
1304
1626
  try {
1305
1627
  const captured = await captureIr(ir, {
1306
1628
  framesDir,
1629
+ ...opts.scenePath !== void 0 && { sceneDir: dirname5(opts.scenePath) },
1307
1630
  ...opts.fps !== void 0 && { fps: opts.fps }
1308
1631
  });
1309
1632
  if (plan) {
@@ -1362,12 +1685,12 @@ __export(loadScene_exports, {
1362
1685
  loadScene: () => loadScene
1363
1686
  });
1364
1687
  import { build as build2 } from "esbuild";
1365
- import { readFile as readFile3 } from "node:fs/promises";
1366
- import { dirname as dirname5, resolve as resolve2 } from "node:path";
1688
+ import { readFile as readFile4 } from "node:fs/promises";
1689
+ import { dirname as dirname6, resolve as resolve3 } from "node:path";
1367
1690
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1368
1691
  async function loadScene(path) {
1369
1692
  if (path.endsWith(".json")) {
1370
- const ir = JSON.parse(await readFile3(path, "utf8"));
1693
+ const ir = JSON.parse(await readFile4(path, "utf8"));
1371
1694
  validateScene(ir);
1372
1695
  return ir;
1373
1696
  }
@@ -1401,24 +1724,25 @@ var init_loadScene = __esm({
1401
1724
  "../render-cli/src/loadScene.ts"() {
1402
1725
  "use strict";
1403
1726
  init_src();
1404
- HERE = dirname5(fileURLToPath4(import.meta.url));
1405
- CORE_ENTRY = true ? resolve2(HERE, "index.js") : resolve2(HERE, "..", "..", "core", "src", "index.ts");
1727
+ HERE = dirname6(fileURLToPath4(import.meta.url));
1728
+ CORE_ENTRY = true ? resolve3(HERE, "index.js") : resolve3(HERE, "..", "..", "core", "src", "index.ts");
1406
1729
  }
1407
1730
  });
1408
1731
 
1409
1732
  // ../render-cli/src/reframe.ts
1410
1733
  import { spawn as spawn3, spawnSync } from "node:child_process";
1411
- import { existsSync as existsSync2 } from "node:fs";
1734
+ import { existsSync as existsSync3 } from "node:fs";
1412
1735
  import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
1413
- import { basename, isAbsolute as isAbsolute2, join as join6, resolve as resolve3 } from "node:path";
1414
- import { dirname as dirname6 } from "node:path";
1736
+ import { basename, isAbsolute as isAbsolute3, join as join6, resolve as resolve4 } from "node:path";
1737
+ import { dirname as dirname7 } from "node:path";
1415
1738
  import { fileURLToPath as fileURLToPath5 } from "node:url";
1416
1739
  var PACKAGED = true;
1417
- var HERE2 = dirname6(fileURLToPath5(import.meta.url));
1418
- var ROOT2 = PACKAGED ? resolve3(HERE2, "..") : resolve3(HERE2, "..", "..", "..");
1740
+ var HERE2 = dirname7(fileURLToPath5(import.meta.url));
1741
+ var ROOT2 = PACKAGED ? resolve4(HERE2, "..") : resolve4(HERE2, "..", "..", "..");
1419
1742
  var USER_CWD = process.env.INIT_CWD ?? process.cwd();
1420
1743
  var RENDER_CLI = PACKAGED ? join6(ROOT2, "dist", "cli.js") : join6(ROOT2, "packages", "render-cli", "src", "cli.ts");
1421
1744
  var ANALYZE = PACKAGED ? join6(ROOT2, "dist", "analyze.js") : join6(ROOT2, "benchmark", "harness", "motion", "analyze.ts");
1745
+ var TRACE = PACKAGED ? join6(ROOT2, "dist", "trace-cli.js") : join6(ROOT2, "benchmark", "harness", "motion", "trace-cli.ts");
1422
1746
  var CMD = PACKAGED ? "reframe" : "pnpm reframe";
1423
1747
  var USAGE = `reframe \u2014 declarative motion graphics
1424
1748
 
@@ -1428,10 +1752,11 @@ usage:
1428
1752
  ${CMD} preview open the scrub/edit UI (lists scenes in your directory)
1429
1753
  ${CMD} new <scene-name> scaffold <scene-name>.ts in your directory
1430
1754
  ${CMD} motion <mp4|framesDir> motion-profile a rendered clip
1755
+ ${CMD} trace <ref.mp4> [--apply scene.ts] extract a video's motion structure \u2192 MotionSketch / timeline
1431
1756
  ${CMD} guide [--regen] print the scene-authoring guide (for you or your AI)
1432
1757
  ${CMD} demo run the edit-survival demo (3 mp4s into out/)
1433
1758
  `;
1434
- var userPath = (p) => isAbsolute2(p) ? p : resolve3(USER_CWD, p);
1759
+ var userPath = (p) => isAbsolute3(p) ? p : resolve4(USER_CWD, p);
1435
1760
  function fail(message) {
1436
1761
  console.error(`error: ${message}`);
1437
1762
  process.exit(2);
@@ -1533,7 +1858,7 @@ async function main() {
1533
1858
 
1534
1859
  ${USAGE}`);
1535
1860
  const inputPath = userPath(input);
1536
- if (!existsSync2(inputPath)) fail(`no such file: ${inputPath}`);
1861
+ if (!existsSync3(inputPath)) fail(`no such file: ${inputPath}`);
1537
1862
  const mode = /\.(ts|json)$/.test(input) ? "ir" : /\.html$/.test(input) ? "html" : null;
1538
1863
  if (!mode) {
1539
1864
  fail(`cannot infer render mode from "${input}" \u2014 expected .ts/.json (reframe scene) or .html (GSAP page)`);
@@ -1561,7 +1886,7 @@ ${USAGE}`);
1561
1886
  if (!sceneArg || !dataArg) fail(`usage: ${CMD} batch <scene.ts> <data.json|csv> [...]`);
1562
1887
  const scenePath = userPath(sceneArg);
1563
1888
  const dataPath = userPath(dataArg);
1564
- for (const p of [scenePath, dataPath]) if (!existsSync2(p)) fail(`no such file: ${p}`);
1889
+ for (const p of [scenePath, dataPath]) if (!existsSync3(p)) fail(`no such file: ${p}`);
1565
1890
  preflightFfmpeg();
1566
1891
  let outDir = PACKAGED ? join6(USER_CWD, "out", "batch") : join6(ROOT2, "out", "batch");
1567
1892
  let concurrency = 3;
@@ -1576,10 +1901,10 @@ ${USAGE}`);
1576
1901
  }
1577
1902
  const { loadRows: loadRows2, runBatch: runBatch2 } = await Promise.resolve().then(() => (init_batch(), batch_exports));
1578
1903
  const { loadScene: loadScene2 } = await Promise.resolve().then(() => (init_loadScene(), loadScene_exports));
1579
- const { readFile: readFile4 } = await import("node:fs/promises");
1904
+ const { readFile: readFile5 } = await import("node:fs/promises");
1580
1905
  const scene = await loadScene2(scenePath);
1581
1906
  const baseOverlays = await Promise.all(
1582
- baseOverlayPaths.map(async (p) => JSON.parse(await readFile4(p, "utf8")))
1907
+ baseOverlayPaths.map(async (p) => JSON.parse(await readFile5(p, "utf8")))
1583
1908
  );
1584
1909
  const rows = await loadRows2(dataPath);
1585
1910
  if (rows.length === 0) fail(`${dataPath}: no data rows`);
@@ -1610,7 +1935,7 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
1610
1935
  if (PACKAGED) {
1611
1936
  const { createRequire } = await import("node:module");
1612
1937
  const vitePkg = createRequire(import.meta.url).resolve("vite/package.json");
1613
- const viteBin = join6(dirname6(vitePkg), "bin", "vite.js");
1938
+ const viteBin = join6(dirname7(vitePkg), "bin", "vite.js");
1614
1939
  process.exit(
1615
1940
  await run(process.execPath, [viteBin, join6(ROOT2, "preview")], {
1616
1941
  env: { REFRAME_SCENE_DIR: USER_CWD }
@@ -1633,7 +1958,7 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
1633
1958
  const targetDir = inRepo ? join6(ROOT2, "examples", "scenes") : USER_CWD;
1634
1959
  const target = join6(targetDir, `${name}.ts`);
1635
1960
  const shown = inRepo ? `examples/scenes/${name}.ts` : `${name}.ts`;
1636
- if (existsSync2(target)) fail(`${shown} already exists`);
1961
+ if (existsSync3(target)) fail(`${shown} already exists`);
1637
1962
  const id = name.split("-")[0] ?? name;
1638
1963
  await writeFile5(target, SCENE_TEMPLATE(name, id));
1639
1964
  console.log(`created ${shown}
@@ -1650,10 +1975,21 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
1650
1975
  await (PACKAGED ? run(process.execPath, [ANALYZE, userPath(input), ...rest.slice(1)]) : run("npx", ["tsx", ANALYZE, userPath(input), ...rest.slice(1)]))
1651
1976
  );
1652
1977
  }
1978
+ case "trace": {
1979
+ const input = rest[0];
1980
+ if (!input || input.startsWith("-")) fail(`usage: ${CMD} trace <ref.mp4> [--apply scene.ts] [-o out.json]`);
1981
+ preflightFfmpeg();
1982
+ const args = rest.slice(1).map(
1983
+ (a, i) => rest.slice(1)[i - 1] === "--apply" || rest.slice(1)[i - 1] === "-o" ? userPath(a) : a
1984
+ );
1985
+ process.exit(
1986
+ await (PACKAGED ? run(process.execPath, [TRACE, userPath(input), ...args]) : run("npx", ["tsx", TRACE, userPath(input), ...args]))
1987
+ );
1988
+ }
1653
1989
  case "guide": {
1654
1990
  const file = rest.includes("--regen") ? PACKAGED ? join6(ROOT2, "guides", "regen-contract.md") : join6(ROOT2, "docs", "regen-contract.md") : PACKAGED ? join6(ROOT2, "guides", "edsl-guide.md") : join6(ROOT2, "benchmark", "guides", "edsl-guide.md");
1655
- const { readFile: readFile4 } = await import("node:fs/promises");
1656
- process.stdout.write(await readFile4(file, "utf8"));
1991
+ const { readFile: readFile5 } = await import("node:fs/promises");
1992
+ process.stdout.write(await readFile5(file, "utf8"));
1657
1993
  return;
1658
1994
  }
1659
1995
  case "demo":