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/cli.js CHANGED
@@ -1,16 +1,95 @@
1
1
  #!/usr/bin/env tsx
2
2
 
3
3
  // ../render-cli/src/cli.ts
4
- import { mkdtemp as mkdtemp2, readFile as readFile3, rm as rm2 } from "node:fs/promises";
4
+ import { mkdtemp as mkdtemp2, readFile as readFile4, rm as rm2 } from "node:fs/promises";
5
5
  import { tmpdir as tmpdir3 } from "node:os";
6
- import { basename, join as join5, resolve as resolve3 } from "node:path";
6
+ import { basename, dirname as dirname6, join as join5, resolve as resolve4 } from "node:path";
7
7
 
8
8
  // ../core/src/ir.ts
9
9
  var DEFAULT_TO_DURATION = 0.5;
10
10
  var DEFAULT_TWEEN_DURATION = 0.5;
11
+ var DEFAULT_MOTIONPATH_DURATION = 1;
12
+
13
+ // ../core/src/path.ts
14
+ function locate(segCount, u) {
15
+ if (segCount <= 0) return { i: 0, t: 0 };
16
+ const clamped = Math.max(0, Math.min(1, u));
17
+ const scaled = clamped * segCount;
18
+ let i = Math.floor(scaled);
19
+ if (i >= segCount) i = segCount - 1;
20
+ return { i, t: scaled - i };
21
+ }
22
+ function controls(points, closed, i) {
23
+ const n = points.length;
24
+ const at = (k) => {
25
+ if (closed) return points[(k % n + n) % n];
26
+ return points[Math.max(0, Math.min(n - 1, k))];
27
+ };
28
+ return [at(i - 1), at(i), at(i + 1), at(i + 2)];
29
+ }
30
+ function segCountOf(points, closed) {
31
+ const n = points.length;
32
+ if (n < 2) return 0;
33
+ return closed ? n : n - 1;
34
+ }
35
+ function pathPoint(points, closed, u) {
36
+ const n = points.length;
37
+ if (n === 0) return [0, 0];
38
+ if (n === 1) return [points[0][0], points[0][1]];
39
+ const segs = segCountOf(points, closed);
40
+ const { i, t } = locate(segs, u);
41
+ const [p0, p1, p2, p3] = controls(points, closed, i);
42
+ const t2 = t * t;
43
+ const t3 = t2 * t;
44
+ 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);
45
+ return [f(p0[0], p1[0], p2[0], p3[0]), f(p0[1], p1[1], p2[1], p3[1])];
46
+ }
47
+ function pathTangentAngle(points, closed, u) {
48
+ const n = points.length;
49
+ if (n < 2) return 0;
50
+ const segs = segCountOf(points, closed);
51
+ const { i, t } = locate(segs, u);
52
+ const [p0, p1, p2, p3] = controls(points, closed, i);
53
+ const t2 = t * t;
54
+ 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);
55
+ const dx = d(p0[0], p1[0], p2[0], p3[0]);
56
+ const dy = d(p0[1], p1[1], p2[1], p3[1]);
57
+ if (dx === 0 && dy === 0) return 0;
58
+ return Math.atan2(dy, dx) * 180 / Math.PI;
59
+ }
11
60
 
12
61
  // ../core/src/compile.ts
13
62
  var key = (target, prop) => `${target}.${prop}`;
63
+ function scaleTimeline(tl, k) {
64
+ switch (tl.kind) {
65
+ case "seq":
66
+ case "par":
67
+ return { ...tl, children: tl.children.map((c) => scaleTimeline(c, k)) };
68
+ case "stagger":
69
+ return { ...tl, interval: tl.interval * k, children: tl.children.map((c) => scaleTimeline(c, k)) };
70
+ case "wait":
71
+ return { ...tl, duration: tl.duration * k };
72
+ case "tween":
73
+ return { ...tl, duration: (tl.duration ?? DEFAULT_TWEEN_DURATION) * k };
74
+ case "motionPath":
75
+ return { ...tl, duration: (tl.duration ?? DEFAULT_MOTIONPATH_DURATION) * k };
76
+ case "to":
77
+ return {
78
+ ...tl,
79
+ duration: (tl.duration ?? DEFAULT_TO_DURATION) * k,
80
+ ...tl.stagger !== void 0 && { stagger: tl.stagger * k }
81
+ };
82
+ case "beat":
83
+ return {
84
+ ...tl,
85
+ children: tl.children.map((c) => scaleTimeline(c, k)),
86
+ ...tl.gap !== void 0 && { gap: tl.gap * k }
87
+ };
88
+ }
89
+ }
90
+ function orderBeats(children) {
91
+ 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);
92
+ }
14
93
  function compileScene(ir) {
15
94
  const nodeById = /* @__PURE__ */ new Map();
16
95
  const nodeOrder = [];
@@ -39,6 +118,7 @@ function compileScene(ir) {
39
118
  }
40
119
  }
41
120
  const segments = /* @__PURE__ */ new Map();
121
+ const motionPaths = /* @__PURE__ */ new Map();
42
122
  const current = new Map(initialValues);
43
123
  const pushSegment = (seg) => {
44
124
  const k = key(seg.target, seg.prop);
@@ -55,6 +135,50 @@ function compileScene(ir) {
55
135
  throw new Error(`cannot animate "${prop}" of "${target}": no base value to start from`);
56
136
  };
57
137
  const labelTimes = /* @__PURE__ */ new Map();
138
+ const beatTimes = /* @__PURE__ */ new Map();
139
+ const durationOf = (tl, start) => {
140
+ switch (tl.kind) {
141
+ case "seq": {
142
+ let t = start;
143
+ for (const child of orderBeats(tl.children)) t = durationOf(child, t);
144
+ return t;
145
+ }
146
+ case "par": {
147
+ let end = start;
148
+ for (const child of tl.children) end = Math.max(end, durationOf(child, start));
149
+ return end;
150
+ }
151
+ case "stagger": {
152
+ let end = start;
153
+ tl.children.forEach((child, i) => {
154
+ end = Math.max(end, durationOf(child, start + i * tl.interval));
155
+ });
156
+ return end;
157
+ }
158
+ case "wait":
159
+ return start + tl.duration;
160
+ case "tween":
161
+ return start + (tl.duration ?? DEFAULT_TWEEN_DURATION);
162
+ case "motionPath":
163
+ return start + (tl.duration ?? DEFAULT_MOTIONPATH_DURATION);
164
+ case "to": {
165
+ const override = ir.states?.[tl.state] ?? {};
166
+ const duration = tl.duration ?? DEFAULT_TO_DURATION;
167
+ const si = tl.stagger ?? 0;
168
+ const targets = nodeOrder.filter(
169
+ (id) => id in override && (tl.filter === void 0 || tl.filter.includes(id))
170
+ );
171
+ return start + duration + Math.max(0, targets.length - 1) * si;
172
+ }
173
+ case "beat": {
174
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
175
+ const natural = durationOf(grouping, 0);
176
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, natural) : 1);
177
+ const beatStart = tl.at ?? start + (tl.gap ?? 0);
178
+ return beatStart + k * natural;
179
+ }
180
+ }
181
+ };
58
182
  const walk = (tl, start) => {
59
183
  const end = walkInner(tl, start);
60
184
  if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
@@ -64,9 +188,19 @@ function compileScene(ir) {
64
188
  switch (tl.kind) {
65
189
  case "seq": {
66
190
  let t = start;
67
- for (const child of tl.children) t = walk(child, t);
191
+ for (const child of orderBeats(tl.children)) t = walk(child, t);
68
192
  return t;
69
193
  }
194
+ case "beat": {
195
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
196
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
197
+ const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
198
+ const beatStart = tl.at ?? start + (tl.gap ?? 0);
199
+ const end = walk(inner, beatStart);
200
+ beatTimes.set(tl.name, { t0: beatStart, t1: end });
201
+ labelTimes.set(tl.name, { t0: beatStart, t1: end });
202
+ return end;
203
+ }
70
204
  case "par": {
71
205
  let end = start;
72
206
  for (const child of tl.children) end = Math.max(end, walk(child, start));
@@ -96,6 +230,23 @@ function compileScene(ir) {
96
230
  }
97
231
  return start + duration;
98
232
  }
233
+ case "motionPath": {
234
+ const duration = tl.duration ?? DEFAULT_MOTIONPATH_DURATION;
235
+ const points = tl.points;
236
+ const closed = tl.closed ?? false;
237
+ const autoRotate = tl.autoRotate ?? false;
238
+ const rotateOffset = tl.rotateOffset ?? 0;
239
+ let list = motionPaths.get(tl.target);
240
+ if (!list) motionPaths.set(tl.target, list = []);
241
+ list.push({ t0: start, t1: start + duration, points, closed, autoRotate, rotateOffset, ...tl.ease !== void 0 && { ease: tl.ease } });
242
+ if (points.length > 0) {
243
+ const [ex, ey] = pathPoint(points, closed, 1);
244
+ current.set(key(tl.target, "x"), ex);
245
+ current.set(key(tl.target, "y"), ey);
246
+ if (autoRotate) current.set(key(tl.target, "rotation"), pathTangentAngle(points, closed, 1) + rotateOffset);
247
+ }
248
+ return start + duration;
249
+ }
99
250
  case "to": {
100
251
  const override = ir.states?.[tl.state] ?? {};
101
252
  const duration = tl.duration ?? DEFAULT_TO_DURATION;
@@ -124,14 +275,17 @@ function compileScene(ir) {
124
275
  };
125
276
  const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
126
277
  for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
278
+ for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
127
279
  return {
128
280
  ir,
129
281
  duration: ir.duration ?? inferredEnd,
130
282
  segments,
283
+ motionPaths,
131
284
  initialValues,
132
285
  nodeById,
133
286
  nodeOrder,
134
- labelTimes
287
+ labelTimes,
288
+ beatTimes
135
289
  };
136
290
  }
137
291
 
@@ -142,6 +296,8 @@ var PROPS_BY_TYPE = {
142
296
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
143
297
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
144
298
  text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
299
+ image: [...COMMON_PROPS, "src", "width", "height"],
300
+ path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
145
301
  group: COMMON_PROPS
146
302
  };
147
303
  var SceneValidationError = class extends Error {
@@ -232,9 +388,39 @@ function validateScene(ir) {
232
388
  problems.push(`${path}: tween duration must be > 0`);
233
389
  }
234
390
  break;
391
+ case "motionPath": {
392
+ const node = nodeById.get(tl.target);
393
+ if (!node) {
394
+ problems.push(
395
+ `${path}: motionPath targets unknown node "${tl.target}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
396
+ );
397
+ } else if (node.type === "line") {
398
+ problems.push(`${path}: motionPath cannot target a line (no x/y) \u2014 "${tl.target}"`);
399
+ }
400
+ if (tl.points.length < 1) problems.push(`${path}: motionPath "${tl.target}" needs at least 1 point`);
401
+ if (tl.duration !== void 0 && tl.duration <= 0) {
402
+ problems.push(`${path}: motionPath "${tl.target}" duration must be > 0`);
403
+ }
404
+ break;
405
+ }
235
406
  case "wait":
236
407
  if (tl.duration < 0) problems.push(`${path}: wait duration must be >= 0`);
237
408
  break;
409
+ case "beat":
410
+ if (labels.has(tl.name)) {
411
+ problems.push(
412
+ `${path}: duplicate timeline label "${tl.name}" (beat name) \u2014 labels are overlay addresses and must be unique`
413
+ );
414
+ }
415
+ labels.add(tl.name);
416
+ if (tl.duration !== void 0 && tl.duration <= 0) {
417
+ problems.push(`${path}: beat "${tl.name}" duration must be > 0`);
418
+ }
419
+ if (tl.scale !== void 0 && tl.scale <= 0) {
420
+ problems.push(`${path}: beat "${tl.name}" scale must be > 0`);
421
+ }
422
+ tl.children.forEach((c, i) => checkTimeline(c, `${path}.beat(${tl.name})[${i}]`));
423
+ break;
238
424
  }
239
425
  };
240
426
  if (ir.timeline) checkTimeline(ir.timeline, "timeline");
@@ -398,13 +584,16 @@ function applyOverlay(ir, overlay, layer, report) {
398
584
  const byLabel = /* @__PURE__ */ new Map();
399
585
  const walkTimeline = (tl) => {
400
586
  if ("label" in tl && tl.label !== void 0) byLabel.set(tl.label, tl);
587
+ if (tl.kind === "beat") byLabel.set(tl.name, tl);
401
588
  if ("children" in tl) tl.children.forEach(walkTimeline);
402
589
  };
403
590
  if (ir.timeline) walkTimeline(ir.timeline);
404
591
  const PATCHABLE = {
405
592
  to: ["duration", "ease", "stagger"],
406
593
  tween: ["duration", "ease"],
407
- wait: ["duration"]
594
+ wait: ["duration"],
595
+ motionPath: ["points", "duration", "ease"],
596
+ beat: ["at", "gap", "scale", "duration", "order"]
408
597
  };
409
598
  let timingPatched = false;
410
599
  for (const [label, patch] of Object.entries(overlay.timeline)) {
@@ -428,7 +617,7 @@ function applyOverlay(ir, overlay, layer, report) {
428
617
  }
429
618
  step[key2] = value;
430
619
  applied(`timeline.${label}.${key2}`, "set");
431
- if (key2 === "duration" || key2 === "stagger") timingPatched = true;
620
+ if (["duration", "stagger", "at", "gap", "scale", "order"].includes(key2)) timingPatched = true;
432
621
  }
433
622
  }
434
623
  if (timingPatched && overlay.scene?.duration === void 0) {
@@ -453,6 +642,9 @@ function formatComposeReport(report) {
453
642
  return lines.join("\n");
454
643
  }
455
644
 
645
+ // ../core/src/presets.ts
646
+ var SET = 1 / 120;
647
+
456
648
  // ../core/src/audio.ts
457
649
  var SFX_DURATION = {
458
650
  whoosh: 0.35,
@@ -525,6 +717,19 @@ function resolveAudioPlan(compiled) {
525
717
  }
526
718
 
527
719
  // ../core/src/interpolate.ts
720
+ var BACK_C1 = 1.70158;
721
+ var BACK_C2 = BACK_C1 * 1.525;
722
+ var BACK_C3 = BACK_C1 + 1;
723
+ var ELASTIC_C4 = 2 * Math.PI / 3;
724
+ var ELASTIC_C5 = 2 * Math.PI / 4.5;
725
+ function easeOutBounce(u) {
726
+ const n1 = 7.5625;
727
+ const d1 = 2.75;
728
+ if (u < 1 / d1) return n1 * u * u;
729
+ if (u < 2 / d1) return n1 * (u -= 1.5 / d1) * u + 0.75;
730
+ if (u < 2.5 / d1) return n1 * (u -= 2.25 / d1) * u + 0.9375;
731
+ return n1 * (u -= 2.625 / d1) * u + 0.984375;
732
+ }
528
733
  var EASE_TABLE = {
529
734
  linear: (u) => u,
530
735
  easeInQuad: (u) => u * u,
@@ -538,10 +743,55 @@ var EASE_TABLE = {
538
743
  easeInOutQuart: (u) => u < 0.5 ? 8 * u ** 4 : 1 - (-2 * u + 2) ** 4 / 2,
539
744
  easeInExpo: (u) => u === 0 ? 0 : 2 ** (10 * u - 10),
540
745
  easeOutExpo: (u) => u === 1 ? 1 : 1 - 2 ** (-10 * u),
541
- easeInOutExpo: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? 2 ** (20 * u - 10) / 2 : (2 - 2 ** (-20 * u + 10)) / 2
746
+ easeInOutExpo: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? 2 ** (20 * u - 10) / 2 : (2 - 2 ** (-20 * u + 10)) / 2,
747
+ // --- expressive eases (GSAP's signature feel) — standard Penner equations ---
748
+ // back: overshoots past the target then settles (pop / snap)
749
+ easeInBack: (u) => BACK_C3 * u ** 3 - BACK_C1 * u * u,
750
+ easeOutBack: (u) => 1 + BACK_C3 * (u - 1) ** 3 + BACK_C1 * (u - 1) ** 2,
751
+ 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,
752
+ // elastic: rings around the target before settling (playful spring)
753
+ easeInElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : -(2 ** (10 * u - 10)) * Math.sin((u * 10 - 10.75) * ELASTIC_C4),
754
+ easeOutElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : 2 ** (-10 * u) * Math.sin((u * 10 - 0.75) * ELASTIC_C4) + 1,
755
+ 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,
756
+ // bounce: drops and bounces to rest (lands without overshoot)
757
+ easeInBounce: (u) => 1 - easeOutBounce(1 - u),
758
+ easeOutBounce,
759
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
542
760
  };
543
761
  var EASE_NAMES = Object.keys(EASE_TABLE);
544
762
 
763
+ // ../core/src/assets.ts
764
+ function collectImageSrcs(ir) {
765
+ const srcs = /* @__PURE__ */ new Set();
766
+ const imageIds = /* @__PURE__ */ new Set();
767
+ const walkNodes = (nodes) => {
768
+ for (const node of nodes) {
769
+ if (node.type === "image") {
770
+ imageIds.add(node.id);
771
+ srcs.add(node.props.src);
772
+ }
773
+ if (node.type === "group") walkNodes(node.children);
774
+ }
775
+ };
776
+ walkNodes(ir.nodes);
777
+ for (const overrides of Object.values(ir.states ?? {})) {
778
+ for (const [nodeId, props] of Object.entries(overrides)) {
779
+ if (imageIds.has(nodeId) && typeof props.src === "string") srcs.add(props.src);
780
+ }
781
+ }
782
+ const walkTimeline = (step) => {
783
+ if (!step) return;
784
+ if (step.kind === "seq" || step.kind === "par" || step.kind === "stagger") {
785
+ for (const child of step.children) walkTimeline(child);
786
+ } else if (step.kind === "tween" && imageIds.has(step.target)) {
787
+ const src = step.props.src;
788
+ if (typeof src === "string") srcs.add(src);
789
+ }
790
+ };
791
+ walkTimeline(ir.timeline);
792
+ return [...srcs];
793
+ }
794
+
545
795
  // ../render-cli/src/audio/index.ts
546
796
  import { dirname as dirname2 } from "node:path";
547
797
 
@@ -881,12 +1131,12 @@ async function encodeMp4(framesDir, fps, outFile) {
881
1131
  "+faststart",
882
1132
  outFile
883
1133
  ];
884
- await new Promise((resolve4, reject) => {
1134
+ await new Promise((resolve5, reject) => {
885
1135
  const proc = spawn2("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
886
1136
  let stderr = "";
887
1137
  proc.stderr.on("data", (d) => stderr += d.toString());
888
1138
  proc.on("close", (code) => {
889
- if (code === 0) resolve4();
1139
+ if (code === 0) resolve5();
890
1140
  else reject(new Error(`ffmpeg exited with ${code}:
891
1141
  ${stderr.slice(-2e3)}`));
892
1142
  });
@@ -925,6 +1175,38 @@ async function fontFaceCss() {
925
1175
  return cssCache;
926
1176
  }
927
1177
 
1178
+ // ../render-cli/src/images.ts
1179
+ import { readFile as readFile2 } from "node:fs/promises";
1180
+ import { existsSync as existsSync2 } from "node:fs";
1181
+ import { extname, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
1182
+ var MIME = {
1183
+ ".png": "image/png",
1184
+ ".jpg": "image/jpeg",
1185
+ ".jpeg": "image/jpeg",
1186
+ ".webp": "image/webp"
1187
+ };
1188
+ async function buildImageAssets(ir, sceneDir) {
1189
+ const assets = {};
1190
+ for (const src of collectImageSrcs(ir)) {
1191
+ const mime = MIME[extname(src).toLowerCase()];
1192
+ if (!mime) {
1193
+ throw new Error(
1194
+ `image "${src}": unsupported format "${extname(src)}" \u2014 supported: ${Object.keys(MIME).join(" ")}`
1195
+ );
1196
+ }
1197
+ const candidates = [isAbsolute2(src) ? src : null, resolve2(sceneDir, src)].filter(
1198
+ (c) => c !== null
1199
+ );
1200
+ const found = candidates.find((c) => existsSync2(c));
1201
+ if (!found) {
1202
+ throw new Error(`image "${src}" not found (tried: ${candidates.join(", ")})`);
1203
+ }
1204
+ const data = await readFile2(found);
1205
+ assets[src] = `data:${mime};base64,${data.toString("base64")}`;
1206
+ }
1207
+ return assets;
1208
+ }
1209
+
928
1210
  // ../render-cli/src/vclock.ts
929
1211
  var VCLOCK_SOURCE = String.raw`
930
1212
  (() => {
@@ -1014,8 +1296,8 @@ var bundleCache = null;
1014
1296
  async function browserBundle() {
1015
1297
  if (bundleCache) return bundleCache;
1016
1298
  if (true) {
1017
- const { readFile: readFile4 } = await import("node:fs/promises");
1018
- bundleCache = await readFile4(
1299
+ const { readFile: readFile5 } = await import("node:fs/promises");
1300
+ bundleCache = await readFile5(
1019
1301
  join4(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.js"),
1020
1302
  "utf8"
1021
1303
  );
@@ -1034,6 +1316,7 @@ async function browserBundle() {
1034
1316
  }
1035
1317
  async function captureIr(ir, opts) {
1036
1318
  await mkdir2(opts.framesDir, { recursive: true });
1319
+ const assets = await buildImageAssets(ir, opts.sceneDir ?? process.cwd());
1037
1320
  const bundle = await browserBundle();
1038
1321
  return withPage(ir.size, async (page) => {
1039
1322
  await page.setContent(
@@ -1042,8 +1325,8 @@ async function captureIr(ir, opts) {
1042
1325
  await injectFonts(page);
1043
1326
  await page.addScriptTag({ content: bundle });
1044
1327
  const info = await page.evaluate(
1045
- (sceneIr) => window.__reframe.init(sceneIr),
1046
- ir
1328
+ ([sceneIr, imageAssets]) => window.__reframe.init(sceneIr, imageAssets),
1329
+ [ir, assets]
1047
1330
  );
1048
1331
  const fps = opts.fps ?? info.fps;
1049
1332
  const duration = opts.duration ?? info.duration;
@@ -1080,14 +1363,14 @@ async function captureHtml(htmlPath, opts) {
1080
1363
 
1081
1364
  // ../render-cli/src/loadScene.ts
1082
1365
  import { build as build2 } from "esbuild";
1083
- import { readFile as readFile2 } from "node:fs/promises";
1084
- import { dirname as dirname5, resolve as resolve2 } from "node:path";
1366
+ import { readFile as readFile3 } from "node:fs/promises";
1367
+ import { dirname as dirname5, resolve as resolve3 } from "node:path";
1085
1368
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1086
1369
  var HERE = dirname5(fileURLToPath4(import.meta.url));
1087
- var CORE_ENTRY = true ? resolve2(HERE, "index.js") : resolve2(HERE, "..", "..", "core", "src", "index.ts");
1370
+ var CORE_ENTRY = true ? resolve3(HERE, "index.js") : resolve3(HERE, "..", "..", "core", "src", "index.ts");
1088
1371
  async function loadScene(path) {
1089
1372
  if (path.endsWith(".json")) {
1090
- const ir = JSON.parse(await readFile2(path, "utf8"));
1373
+ const ir = JSON.parse(await readFile3(path, "utf8"));
1091
1374
  validateScene(ir);
1092
1375
  return ir;
1093
1376
  }
@@ -1128,7 +1411,7 @@ function parseArgs(argv) {
1128
1411
  }
1129
1412
  const args = {
1130
1413
  mode,
1131
- input: resolve3(input),
1414
+ input: resolve4(input),
1132
1415
  out: `${basename(input).replace(/\.[^.]+$/, "")}.mp4`,
1133
1416
  keepFrames: false,
1134
1417
  overlays: [],
@@ -1140,8 +1423,8 @@ function parseArgs(argv) {
1140
1423
  else if (a === "--fps") args.fps = Number(rest[++i]);
1141
1424
  else if (a === "--duration") args.duration = Number(rest[++i]);
1142
1425
  else if (a === "--keep-frames") args.keepFrames = true;
1143
- else if (a === "--frames-dir") args.framesDir = resolve3(rest[++i]);
1144
- else if (a === "--overlay") args.overlays.push(resolve3(rest[++i]));
1426
+ else if (a === "--frames-dir") args.framesDir = resolve4(rest[++i]);
1427
+ else if (a === "--overlay") args.overlays.push(resolve4(rest[++i]));
1145
1428
  else if (a === "--no-audio") args.noAudio = true;
1146
1429
  else {
1147
1430
  console.error(`unknown flag ${a}`);
@@ -1159,7 +1442,7 @@ async function main() {
1159
1442
  let ir = await loadScene(args.input);
1160
1443
  if (args.overlays.length > 0) {
1161
1444
  const docs = await Promise.all(
1162
- args.overlays.map(async (p) => JSON.parse(await readFile3(p, "utf8")))
1445
+ args.overlays.map(async (p) => JSON.parse(await readFile4(p, "utf8")))
1163
1446
  );
1164
1447
  const composed = composeScene(ir, ...docs);
1165
1448
  console.error(formatComposeReport(composed.report));
@@ -1174,6 +1457,7 @@ async function main() {
1174
1457
  }
1175
1458
  result = await captureIr(ir, {
1176
1459
  framesDir,
1460
+ sceneDir: dirname6(args.input),
1177
1461
  ...args.fps !== void 0 && { fps: args.fps },
1178
1462
  ...args.duration !== void 0 && { duration: args.duration }
1179
1463
  });