reframe-video 0.1.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 (41) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +77 -0
  3. package/assets/fonts/inter-400.woff2 +0 -0
  4. package/assets/fonts/inter-700.woff2 +0 -0
  5. package/assets/fonts/inter-800.woff2 +0 -0
  6. package/assets/sfx/LICENSE.md +12 -0
  7. package/assets/sfx/click_002.ogg +0 -0
  8. package/assets/sfx/click_003.ogg +0 -0
  9. package/assets/sfx/click_004.ogg +0 -0
  10. package/assets/sfx/confirmation_001.ogg +0 -0
  11. package/assets/sfx/keypress-001.wav +0 -0
  12. package/assets/sfx/keypress-004.wav +0 -0
  13. package/assets/sfx/keypress-007.wav +0 -0
  14. package/assets/sfx/keypress-010.wav +0 -0
  15. package/assets/sfx/keypress-014.wav +0 -0
  16. package/dist/analyze.js +344 -0
  17. package/dist/bin.js +1677 -0
  18. package/dist/browserEntry.js +532 -0
  19. package/dist/cli.js +1205 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +889 -0
  22. package/dist/renderer-canvas.js +89 -0
  23. package/dist/types/audio.d.ts +53 -0
  24. package/dist/types/behaviors.d.ts +7 -0
  25. package/dist/types/compile.d.ts +38 -0
  26. package/dist/types/compose.d.ts +64 -0
  27. package/dist/types/dsl.d.ts +66 -0
  28. package/dist/types/evaluate.d.ts +59 -0
  29. package/dist/types/index.d.ts +9 -0
  30. package/dist/types/interpolate.d.ts +12 -0
  31. package/dist/types/ir.d.ts +213 -0
  32. package/dist/types/validate.d.ts +12 -0
  33. package/guides/edsl-guide.md +202 -0
  34. package/guides/regen-contract.md +18 -0
  35. package/package.json +55 -0
  36. package/preview/index.html +60 -0
  37. package/preview/src/main.ts +162 -0
  38. package/preview/src/panel.ts +347 -0
  39. package/preview/src/store.ts +220 -0
  40. package/preview/src/virtual.d.ts +4 -0
  41. package/preview/vite.config.ts +52 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1205 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ // ../render-cli/src/cli.ts
4
+ import { mkdtemp as mkdtemp2, readFile as readFile3, rm as rm2 } from "node:fs/promises";
5
+ import { tmpdir as tmpdir3 } from "node:os";
6
+ import { basename, join as join5, resolve as resolve3 } from "node:path";
7
+
8
+ // ../core/src/ir.ts
9
+ var DEFAULT_TO_DURATION = 0.5;
10
+ var DEFAULT_TWEEN_DURATION = 0.5;
11
+
12
+ // ../core/src/compile.ts
13
+ var key = (target, prop) => `${target}.${prop}`;
14
+ function compileScene(ir) {
15
+ const nodeById = /* @__PURE__ */ new Map();
16
+ const nodeOrder = [];
17
+ const collect = (nodes) => {
18
+ for (const node of nodes) {
19
+ nodeById.set(node.id, node);
20
+ nodeOrder.push(node.id);
21
+ if (node.type === "group") collect(node.children);
22
+ }
23
+ };
24
+ collect(ir.nodes);
25
+ const initialValues = /* @__PURE__ */ new Map();
26
+ for (const [id, node] of nodeById) {
27
+ for (const [prop, value] of Object.entries(node.props)) {
28
+ if (typeof value === "number" || typeof value === "string") {
29
+ initialValues.set(key(id, prop), value);
30
+ }
31
+ }
32
+ }
33
+ if (ir.initial !== void 0) {
34
+ const override = ir.states?.[ir.initial] ?? {};
35
+ for (const [id, props] of Object.entries(override)) {
36
+ for (const [prop, value] of Object.entries(props)) {
37
+ initialValues.set(key(id, prop), value);
38
+ }
39
+ }
40
+ }
41
+ const segments = /* @__PURE__ */ new Map();
42
+ const current = new Map(initialValues);
43
+ const pushSegment = (seg) => {
44
+ const k = key(seg.target, seg.prop);
45
+ let list = segments.get(k);
46
+ if (!list) segments.set(k, list = []);
47
+ list.push(seg);
48
+ current.set(k, seg.to);
49
+ };
50
+ const currentValue = (target, prop) => {
51
+ const v = current.get(key(target, prop));
52
+ if (v !== void 0) return v;
53
+ if (prop === "opacity" || prop === "scale" || prop === "progress") return 1;
54
+ if (prop === "rotation") return 0;
55
+ throw new Error(`cannot animate "${prop}" of "${target}": no base value to start from`);
56
+ };
57
+ const labelTimes = /* @__PURE__ */ new Map();
58
+ const walk = (tl, start) => {
59
+ const end = walkInner(tl, start);
60
+ if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
61
+ return end;
62
+ };
63
+ const walkInner = (tl, start) => {
64
+ switch (tl.kind) {
65
+ case "seq": {
66
+ let t = start;
67
+ for (const child of tl.children) t = walk(child, t);
68
+ return t;
69
+ }
70
+ case "par": {
71
+ let end = start;
72
+ for (const child of tl.children) end = Math.max(end, walk(child, start));
73
+ return end;
74
+ }
75
+ case "stagger": {
76
+ let end = start;
77
+ tl.children.forEach((child, i) => {
78
+ end = Math.max(end, walk(child, start + i * tl.interval));
79
+ });
80
+ return end;
81
+ }
82
+ case "wait":
83
+ return start + tl.duration;
84
+ case "tween": {
85
+ const duration = tl.duration ?? DEFAULT_TWEEN_DURATION;
86
+ for (const [prop, toValue] of Object.entries(tl.props)) {
87
+ pushSegment({
88
+ target: tl.target,
89
+ prop,
90
+ t0: start,
91
+ t1: start + duration,
92
+ from: currentValue(tl.target, prop),
93
+ to: toValue,
94
+ ...tl.ease !== void 0 && { ease: tl.ease }
95
+ });
96
+ }
97
+ return start + duration;
98
+ }
99
+ case "to": {
100
+ const override = ir.states?.[tl.state] ?? {};
101
+ const duration = tl.duration ?? DEFAULT_TO_DURATION;
102
+ const staggerInterval = tl.stagger ?? 0;
103
+ const targets = nodeOrder.filter(
104
+ (id) => id in override && (tl.filter === void 0 || tl.filter.includes(id))
105
+ );
106
+ targets.forEach((id, i) => {
107
+ const t0 = start + i * staggerInterval;
108
+ for (const [prop, toValue] of Object.entries(override[id])) {
109
+ pushSegment({
110
+ target: id,
111
+ prop,
112
+ t0,
113
+ t1: t0 + duration,
114
+ from: currentValue(id, prop),
115
+ to: toValue,
116
+ ...tl.ease !== void 0 && { ease: tl.ease }
117
+ });
118
+ }
119
+ });
120
+ const last = Math.max(0, targets.length - 1);
121
+ return start + duration + last * staggerInterval;
122
+ }
123
+ }
124
+ };
125
+ const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
126
+ for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
127
+ return {
128
+ ir,
129
+ duration: ir.duration ?? inferredEnd,
130
+ segments,
131
+ initialValues,
132
+ nodeById,
133
+ nodeOrder,
134
+ labelTimes
135
+ };
136
+ }
137
+
138
+ // ../core/src/validate.ts
139
+ var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "anchor"];
140
+ var PROPS_BY_TYPE = {
141
+ rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
142
+ ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
143
+ line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
144
+ text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
145
+ group: COMMON_PROPS
146
+ };
147
+ var SceneValidationError = class extends Error {
148
+ constructor(problems) {
149
+ super(`Scene validation failed:
150
+ ${problems.map((p) => ` - ${p}`).join("\n")}`);
151
+ this.problems = problems;
152
+ this.name = "SceneValidationError";
153
+ }
154
+ problems;
155
+ };
156
+ function validateScene(ir) {
157
+ const problems = [];
158
+ const nodeById = /* @__PURE__ */ new Map();
159
+ const collect = (nodes) => {
160
+ for (const node of nodes) {
161
+ if (nodeById.has(node.id)) {
162
+ problems.push(`duplicate node id "${node.id}" \u2014 every node id must be unique`);
163
+ }
164
+ nodeById.set(node.id, node);
165
+ if (node.type === "group") collect(node.children);
166
+ }
167
+ };
168
+ collect(ir.nodes);
169
+ const checkProps = (where, nodeId, props) => {
170
+ const node = nodeById.get(nodeId);
171
+ if (!node) {
172
+ problems.push(
173
+ `${where} targets unknown node "${nodeId}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
174
+ );
175
+ return;
176
+ }
177
+ const allowed = PROPS_BY_TYPE[node.type];
178
+ for (const key2 of Object.keys(props)) {
179
+ if (!allowed.includes(key2)) {
180
+ problems.push(
181
+ `${where}: "${key2}" is not a prop of ${node.type} "${nodeId}" \u2014 valid props: ${allowed.join(", ")}`
182
+ );
183
+ }
184
+ }
185
+ };
186
+ const states = ir.states ?? {};
187
+ for (const [stateName, overrides] of Object.entries(states)) {
188
+ for (const [nodeId, props] of Object.entries(overrides)) {
189
+ checkProps(`state "${stateName}"`, nodeId, props);
190
+ }
191
+ }
192
+ if (ir.initial !== void 0 && !(ir.initial in states)) {
193
+ problems.push(
194
+ `initial state "${ir.initial}" is not defined \u2014 defined states: ${Object.keys(states).join(", ") || "(none)"}`
195
+ );
196
+ }
197
+ const labels = /* @__PURE__ */ new Set();
198
+ const checkTimeline = (tl, path) => {
199
+ if ("label" in tl && tl.label !== void 0) {
200
+ if (labels.has(tl.label)) {
201
+ problems.push(
202
+ `${path}: duplicate timeline label "${tl.label}" \u2014 labels are overlay addresses and must be unique`
203
+ );
204
+ }
205
+ labels.add(tl.label);
206
+ }
207
+ switch (tl.kind) {
208
+ case "seq":
209
+ case "par":
210
+ tl.children.forEach((c, i) => checkTimeline(c, `${path}.${tl.kind}[${i}]`));
211
+ break;
212
+ case "stagger":
213
+ if (tl.interval < 0) problems.push(`${path}: stagger interval must be >= 0`);
214
+ tl.children.forEach((c, i) => checkTimeline(c, `${path}.stagger[${i}]`));
215
+ break;
216
+ case "to":
217
+ if (!(tl.state in states)) {
218
+ problems.push(
219
+ `${path}: to("${tl.state}") references an undefined state \u2014 defined states: ${Object.keys(states).join(", ") || "(none)"}`
220
+ );
221
+ }
222
+ if (tl.duration !== void 0 && tl.duration <= 0) {
223
+ problems.push(`${path}: to("${tl.state}") duration must be > 0`);
224
+ }
225
+ for (const id of tl.filter ?? []) {
226
+ if (!nodeById.has(id)) problems.push(`${path}: filter contains unknown node "${id}"`);
227
+ }
228
+ break;
229
+ case "tween":
230
+ checkProps(path, tl.target, tl.props);
231
+ if (tl.duration !== void 0 && tl.duration <= 0) {
232
+ problems.push(`${path}: tween duration must be > 0`);
233
+ }
234
+ break;
235
+ case "wait":
236
+ if (tl.duration < 0) problems.push(`${path}: wait duration must be >= 0`);
237
+ break;
238
+ }
239
+ };
240
+ if (ir.timeline) checkTimeline(ir.timeline, "timeline");
241
+ for (const [i, b] of (ir.behaviors ?? []).entries()) {
242
+ checkProps(`behaviors[${i}]`, b.target, { [b.prop]: 0 });
243
+ }
244
+ if (ir.duration !== void 0 && ir.duration <= 0) {
245
+ problems.push("scene duration must be > 0");
246
+ }
247
+ const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
248
+ for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
249
+ if (typeof cue.at === "string" && !labels.has(cue.at)) {
250
+ problems.push(
251
+ `audio.cues[${i}]: unknown timeline label "${cue.at}" \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
252
+ );
253
+ }
254
+ if (typeof cue.at === "number" && cue.at < 0) {
255
+ problems.push(`audio.cues[${i}]: "at" must be >= 0`);
256
+ }
257
+ if (cue.sfx === void 0 === (cue.file === void 0)) {
258
+ problems.push(`audio.cues[${i}]: exactly one of "sfx" or "file" is required`);
259
+ }
260
+ if (cue.sfx !== void 0 && !SFX_NAMES.includes(cue.sfx)) {
261
+ problems.push(`audio.cues[${i}]: unknown sfx "${cue.sfx}" \u2014 valid: ${SFX_NAMES.join(", ")}`);
262
+ }
263
+ if (cue.gain !== void 0 && cue.gain < 0) {
264
+ problems.push(`audio.cues[${i}]: gain must be >= 0`);
265
+ }
266
+ }
267
+ const duck = ir.audio?.bgm?.duck;
268
+ if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
269
+ problems.push("audio.bgm.duck.depth must be in [0, 1]");
270
+ }
271
+ if (ir.audio?.bgm?.file !== void 0 && ir.audio.bgm.synth !== void 0) {
272
+ problems.push('audio.bgm: use either "file" or "synth", not both');
273
+ }
274
+ if (problems.length > 0) throw new SceneValidationError(problems);
275
+ }
276
+
277
+ // ../core/src/compose.ts
278
+ var SCENE_PATCHABLE = ["background", "duration", "fps"];
279
+ function composeScene(base, ...overlays) {
280
+ const ir = structuredClone(base);
281
+ const report = { applied: [], orphans: [], warnings: [] };
282
+ overlays.forEach((overlay, index) => {
283
+ const layer = overlay.name ?? `overlay-${index}`;
284
+ if (overlay.target !== void 0 && overlay.target !== ir.id) {
285
+ report.warnings.push(
286
+ `${layer}: authored against scene "${overlay.target}" but composing onto "${ir.id}"`
287
+ );
288
+ }
289
+ applyOverlay(ir, overlay, layer, report);
290
+ });
291
+ validateScene(ir);
292
+ return { ir, report };
293
+ }
294
+ function applyOverlay(ir, overlay, layer, report) {
295
+ const nodeById = /* @__PURE__ */ new Map();
296
+ const collect = (nodes) => {
297
+ for (const node of nodes) {
298
+ nodeById.set(node.id, node);
299
+ if (node.type === "group") collect(node.children);
300
+ }
301
+ };
302
+ collect(ir.nodes);
303
+ const knownIds = () => [...nodeById.keys()].join(", ");
304
+ const orphan = (address, reason) => report.orphans.push({ layer, address, reason });
305
+ const applied = (address, action) => report.applied.push({ layer, address, action });
306
+ const patchProps = (address, node, target, patch) => {
307
+ const allowed = PROPS_BY_TYPE[node.type];
308
+ for (const [prop, value] of Object.entries(patch)) {
309
+ if (!allowed.includes(prop)) {
310
+ orphan(
311
+ `${address}.${prop}`,
312
+ `"${prop}" is not a prop of ${node.type} "${node.id}" \u2014 the base may have changed this node's type; valid props: ${allowed.join(", ")}`
313
+ );
314
+ continue;
315
+ }
316
+ if (value === null) {
317
+ delete target[prop];
318
+ applied(`${address}.${prop}`, "unset");
319
+ } else {
320
+ target[prop] = value;
321
+ applied(`${address}.${prop}`, "set");
322
+ }
323
+ }
324
+ };
325
+ if (overlay.scene) {
326
+ for (const key2 of SCENE_PATCHABLE) {
327
+ const value = overlay.scene[key2];
328
+ if (value !== void 0) {
329
+ ir[key2] = value;
330
+ applied(`scene.${key2}`, "set");
331
+ }
332
+ }
333
+ }
334
+ for (const [id, patch] of Object.entries(overlay.nodes ?? {})) {
335
+ const node = nodeById.get(id);
336
+ if (!node) {
337
+ orphan(
338
+ `nodes.${id}`,
339
+ `unknown node "${id}" \u2014 known ids: ${knownIds()}; did the base regeneration rename it?`
340
+ );
341
+ continue;
342
+ }
343
+ patchProps(`nodes.${id}`, node, node.props, patch);
344
+ }
345
+ for (const [stateName, statePatch] of Object.entries(overlay.states ?? {})) {
346
+ const state = ir.states?.[stateName];
347
+ if (!state) {
348
+ orphan(
349
+ `states.${stateName}`,
350
+ `unknown state "${stateName}" \u2014 defined states: ${Object.keys(ir.states ?? {}).join(", ") || "(none)"}`
351
+ );
352
+ continue;
353
+ }
354
+ for (const [id, patch] of Object.entries(statePatch)) {
355
+ const node = nodeById.get(id);
356
+ if (!node) {
357
+ orphan(
358
+ `states.${stateName}.${id}`,
359
+ `unknown node "${id}" \u2014 known ids: ${knownIds()}; did the base regeneration rename it?`
360
+ );
361
+ continue;
362
+ }
363
+ const target = state[id] ??= {};
364
+ patchProps(`states.${stateName}.${id}`, node, target, patch);
365
+ }
366
+ }
367
+ if (overlay.behaviors?.remove || overlay.behaviors?.set) {
368
+ ir.behaviors ??= [];
369
+ for (const { target, prop } of overlay.behaviors.remove ?? []) {
370
+ const index = ir.behaviors.findIndex((b) => b.target === target && b.prop === prop);
371
+ if (index < 0) {
372
+ orphan(
373
+ `behaviors.remove.${target}.${prop}`,
374
+ `no behavior on "${target}.${prop}" to remove`
375
+ );
376
+ continue;
377
+ }
378
+ ir.behaviors.splice(index, 1);
379
+ applied(`behaviors.${target}.${prop}`, "behavior-remove");
380
+ }
381
+ for (const behavior of overlay.behaviors.set ?? []) {
382
+ if (!nodeById.has(behavior.target)) {
383
+ orphan(
384
+ `behaviors.set.${behavior.target}.${behavior.prop}`,
385
+ `unknown node "${behavior.target}" \u2014 known ids: ${knownIds()}`
386
+ );
387
+ continue;
388
+ }
389
+ const index = ir.behaviors.findIndex(
390
+ (b) => b.target === behavior.target && b.prop === behavior.prop
391
+ );
392
+ if (index >= 0) ir.behaviors[index] = structuredClone(behavior);
393
+ else ir.behaviors.push(structuredClone(behavior));
394
+ applied(`behaviors.${behavior.target}.${behavior.prop}`, "behavior-set");
395
+ }
396
+ }
397
+ if (overlay.timeline) {
398
+ const byLabel = /* @__PURE__ */ new Map();
399
+ const walkTimeline = (tl) => {
400
+ if ("label" in tl && tl.label !== void 0) byLabel.set(tl.label, tl);
401
+ if ("children" in tl) tl.children.forEach(walkTimeline);
402
+ };
403
+ if (ir.timeline) walkTimeline(ir.timeline);
404
+ const PATCHABLE = {
405
+ to: ["duration", "ease", "stagger"],
406
+ tween: ["duration", "ease"],
407
+ wait: ["duration"]
408
+ };
409
+ let timingPatched = false;
410
+ for (const [label, patch] of Object.entries(overlay.timeline)) {
411
+ const step = byLabel.get(label);
412
+ if (!step) {
413
+ orphan(
414
+ `timeline.${label}`,
415
+ `unknown timeline label "${label}" \u2014 known labels: ${[...byLabel.keys()].join(", ") || "(none)"}; did the base regeneration drop it?`
416
+ );
417
+ continue;
418
+ }
419
+ const allowed = PATCHABLE[step.kind] ?? [];
420
+ for (const [key2, value] of Object.entries(patch)) {
421
+ if (value === void 0) continue;
422
+ if (!allowed.includes(key2)) {
423
+ orphan(
424
+ `timeline.${label}.${key2}`,
425
+ `"${key2}" is not patchable on a ${step.kind} step \u2014 patchable: ${allowed.join(", ")}`
426
+ );
427
+ continue;
428
+ }
429
+ step[key2] = value;
430
+ applied(`timeline.${label}.${key2}`, "set");
431
+ if (key2 === "duration" || key2 === "stagger") timingPatched = true;
432
+ }
433
+ }
434
+ if (timingPatched && overlay.scene?.duration === void 0) {
435
+ delete ir.duration;
436
+ ir.duration = compileScene(ir).duration;
437
+ }
438
+ }
439
+ for (const node of overlay.addNodes ?? []) {
440
+ ir.nodes.push(structuredClone(node));
441
+ nodeById.set(node.id, node);
442
+ applied(`addNodes.${node.id}`, "add-node");
443
+ }
444
+ }
445
+ function formatComposeReport(report) {
446
+ const lines = [];
447
+ lines.push(
448
+ `compose: ${report.applied.length} applied, ${report.orphans.length} orphaned, ${report.warnings.length} warnings`
449
+ );
450
+ for (const a of report.applied) lines.push(` \u2713 [${a.layer}] ${a.address} (${a.action})`);
451
+ for (const o of report.orphans) lines.push(` \u2717 [${o.layer}] ${o.address}: ${o.reason}`);
452
+ for (const w of report.warnings) lines.push(` ! ${w}`);
453
+ return lines.join("\n");
454
+ }
455
+
456
+ // ../core/src/audio.ts
457
+ var SFX_DURATION = {
458
+ whoosh: 0.35,
459
+ pop: 0.12,
460
+ tick: 0.03,
461
+ rise: 0.5,
462
+ shimmer: 0.9,
463
+ thud: 0.25
464
+ };
465
+ var FILE_CUE_DURATION = 0.4;
466
+ function resolveAudioPlan(compiled) {
467
+ const audio = compiled.ir.audio;
468
+ if (!audio || !audio.bgm && (audio.cues ?? []).length === 0) return null;
469
+ const warnings = [];
470
+ const duration = compiled.duration;
471
+ const cues = [];
472
+ for (const [index, cue] of (audio.cues ?? []).entries()) {
473
+ let anchor;
474
+ if (typeof cue.at === "number") {
475
+ anchor = cue.at;
476
+ } else {
477
+ const span = compiled.labelTimes.get(cue.at);
478
+ if (!span) {
479
+ warnings.push(`cue[${index}]: unknown label "${cue.at}" \u2014 cue dropped`);
480
+ continue;
481
+ }
482
+ anchor = span.t0;
483
+ }
484
+ const t = Math.max(0, anchor + (cue.offset ?? 0));
485
+ const cueDuration = cue.sfx ? SFX_DURATION[cue.sfx] : FILE_CUE_DURATION;
486
+ if (t >= duration) {
487
+ warnings.push(`cue[${index}] at ${t.toFixed(2)}s starts past the scene end (${duration.toFixed(2)}s) \u2014 dropped`);
488
+ continue;
489
+ }
490
+ if (t + cueDuration > duration) {
491
+ warnings.push(`cue[${index}] at ${t.toFixed(2)}s extends past the scene end \u2014 it will be truncated`);
492
+ }
493
+ cues.push({
494
+ t,
495
+ gain: cue.gain ?? 1,
496
+ duration: cueDuration,
497
+ source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
498
+ });
499
+ }
500
+ cues.sort((a, b) => a.t - b.t);
501
+ const duckWindows = [];
502
+ for (const cue of cues) {
503
+ const window2 = { t0: cue.t, t1: Math.min(duration, cue.t + cue.duration) };
504
+ const last = duckWindows[duckWindows.length - 1];
505
+ if (last && window2.t0 <= last.t1 + 0.1) last.t1 = Math.max(last.t1, window2.t1);
506
+ else duckWindows.push(window2);
507
+ }
508
+ let bgm = null;
509
+ if (audio.bgm) {
510
+ const b = audio.bgm;
511
+ const duck = b.duck === false ? null : {
512
+ depth: b.duck?.depth ?? 0.5,
513
+ attack: b.duck?.attack ?? 0.05,
514
+ release: b.duck?.release ?? 0.25
515
+ };
516
+ bgm = {
517
+ source: b.file ? { kind: "file", path: b.file } : { kind: "synth", name: b.synth ?? "ambient-pad" },
518
+ gain: b.gain ?? 0.5,
519
+ fadeIn: b.fadeIn ?? 0,
520
+ fadeOut: b.fadeOut ?? 0,
521
+ duck
522
+ };
523
+ }
524
+ return { duration, bgm, cues, duckWindows, warnings };
525
+ }
526
+
527
+ // ../core/src/interpolate.ts
528
+ var EASE_TABLE = {
529
+ linear: (u) => u,
530
+ easeInQuad: (u) => u * u,
531
+ easeOutQuad: (u) => 1 - (1 - u) * (1 - u),
532
+ easeInOutQuad: (u) => u < 0.5 ? 2 * u * u : 1 - (-2 * u + 2) ** 2 / 2,
533
+ easeInCubic: (u) => u ** 3,
534
+ easeOutCubic: (u) => 1 - (1 - u) ** 3,
535
+ easeInOutCubic: (u) => u < 0.5 ? 4 * u ** 3 : 1 - (-2 * u + 2) ** 3 / 2,
536
+ easeInQuart: (u) => u ** 4,
537
+ easeOutQuart: (u) => 1 - (1 - u) ** 4,
538
+ easeInOutQuart: (u) => u < 0.5 ? 8 * u ** 4 : 1 - (-2 * u + 2) ** 4 / 2,
539
+ easeInExpo: (u) => u === 0 ? 0 : 2 ** (10 * u - 10),
540
+ 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
542
+ };
543
+ var EASE_NAMES = Object.keys(EASE_TABLE);
544
+
545
+ // ../render-cli/src/audio/index.ts
546
+ import { dirname as dirname2 } from "node:path";
547
+
548
+ // ../render-cli/src/audio/sfx.ts
549
+ import { mkdir, rename, writeFile } from "node:fs/promises";
550
+ import { existsSync } from "node:fs";
551
+ import { tmpdir } from "node:os";
552
+ import { dirname, isAbsolute, join, resolve } from "node:path";
553
+ import { fileURLToPath } from "node:url";
554
+
555
+ // ../render-cli/src/audio/wav.ts
556
+ var SAMPLE_RATE = 44100;
557
+ function encodeWavMono16(samples, sampleRate = SAMPLE_RATE) {
558
+ const dataBytes = samples.length * 2;
559
+ const buffer2 = Buffer.alloc(44 + dataBytes);
560
+ buffer2.write("RIFF", 0);
561
+ buffer2.writeUInt32LE(36 + dataBytes, 4);
562
+ buffer2.write("WAVE", 8);
563
+ buffer2.write("fmt ", 12);
564
+ buffer2.writeUInt32LE(16, 16);
565
+ buffer2.writeUInt16LE(1, 20);
566
+ buffer2.writeUInt16LE(1, 22);
567
+ buffer2.writeUInt32LE(sampleRate, 24);
568
+ buffer2.writeUInt32LE(sampleRate * 2, 28);
569
+ buffer2.writeUInt16LE(2, 32);
570
+ buffer2.writeUInt16LE(16, 34);
571
+ buffer2.write("data", 36);
572
+ buffer2.writeUInt32LE(dataBytes, 40);
573
+ for (let i = 0; i < samples.length; i++) {
574
+ const s = Math.max(-1, Math.min(1, samples[i]));
575
+ buffer2.writeInt16LE(Math.round(s * 32767), 44 + i * 2);
576
+ }
577
+ return buffer2;
578
+ }
579
+
580
+ // ../render-cli/src/audio/synth.ts
581
+ function hash01(n, seed) {
582
+ let h = n * 374761393 + seed * 668265263 | 0;
583
+ h = h ^ h >>> 13 | 0;
584
+ h = Math.imul(h, 1274126177);
585
+ h = (h ^ h >>> 16) >>> 0;
586
+ return h / 4294967295;
587
+ }
588
+ var noise = (n, seed) => hash01(n, seed) * 2 - 1;
589
+ var TAU = Math.PI * 2;
590
+ var expDecay = (t, dur, k = 5) => Math.exp(-k * t / dur);
591
+ function buffer(duration) {
592
+ const n = Math.round(duration * SAMPLE_RATE);
593
+ return { out: new Float32Array(n), n };
594
+ }
595
+ function whoosh(seed) {
596
+ const dur = 0.35;
597
+ const { out, n } = buffer(dur);
598
+ let lp = 0;
599
+ let lp2 = 0;
600
+ for (let i = 0; i < n; i++) {
601
+ const t = i / SAMPLE_RATE;
602
+ const u = t / dur;
603
+ const center = 1200 * Math.pow(300 / 1200, u);
604
+ const alpha = Math.min(1, TAU * center / SAMPLE_RATE);
605
+ lp += alpha * (noise(i, seed) - lp);
606
+ lp2 += alpha * 0.5 * (lp - lp2);
607
+ const env = u < 0.3 ? u / 0.3 : expDecay(t - 0.3 * dur, dur * 0.7, 4);
608
+ out[i] = (lp - lp2) * env * 2.2;
609
+ }
610
+ return out;
611
+ }
612
+ function pop(seed) {
613
+ const dur = 0.12;
614
+ const { out, n } = buffer(dur);
615
+ let phase = 0;
616
+ for (let i = 0; i < n; i++) {
617
+ const t = i / SAMPLE_RATE;
618
+ const freq = 600 * Math.pow(150 / 600, t / 0.08);
619
+ phase += TAU * freq / SAMPLE_RATE;
620
+ const transient = t < 2e-3 ? noise(i, seed) * 0.5 : 0;
621
+ out[i] = (Math.sin(phase) + transient) * expDecay(t, dur, 6) * 0.8;
622
+ }
623
+ return out;
624
+ }
625
+ function tick(seed) {
626
+ const dur = 0.03;
627
+ const { out, n } = buffer(dur);
628
+ for (let i = 0; i < n; i++) {
629
+ const t = i / SAMPLE_RATE;
630
+ const sine = t < 4e-3 ? Math.sin(TAU * 4e3 * t) : 0;
631
+ out[i] = (sine * 0.6 + noise(i, seed) * 0.35) * expDecay(t, dur, 8);
632
+ }
633
+ return out;
634
+ }
635
+ function rise(seed) {
636
+ const dur = 0.5;
637
+ const { out, n } = buffer(dur);
638
+ let phase = 0;
639
+ for (let i = 0; i < n; i++) {
640
+ const t = i / SAMPLE_RATE;
641
+ const u = t / dur;
642
+ const freq = 220 * Math.pow(880 / 220, u);
643
+ phase += TAU * freq / SAMPLE_RATE;
644
+ const env = Math.sin(Math.PI * Math.min(1, u * 1.05)) ** 1.5;
645
+ out[i] = (Math.sin(phase) + 0.3 * Math.sin(2 * phase)) * env * 0.45;
646
+ }
647
+ return out;
648
+ }
649
+ function shimmer(seed) {
650
+ const dur = 0.9;
651
+ const { out, n } = buffer(dur);
652
+ const partials = Array.from({ length: 5 }, (_, p) => ({
653
+ freq: 2e3 + hash01(p, seed + 7) * 2e3,
654
+ am: 0.5 + hash01(p, seed + 8) * 1.5,
655
+ phase: hash01(p, seed + 9) * TAU
656
+ }));
657
+ for (let i = 0; i < n; i++) {
658
+ const t = i / SAMPLE_RATE;
659
+ const u = t / dur;
660
+ const env = Math.sin(Math.PI * u) ** 1.2;
661
+ let s = 0;
662
+ for (const part of partials) {
663
+ s += Math.sin(TAU * part.freq * t + part.phase) * (0.6 + 0.4 * Math.sin(TAU * part.am * t));
664
+ }
665
+ out[i] = s / 5 * env * 0.5;
666
+ }
667
+ return out;
668
+ }
669
+ function thud(seed) {
670
+ const dur = 0.25;
671
+ const { out, n } = buffer(dur);
672
+ let phase = 0;
673
+ let lp = 0;
674
+ for (let i = 0; i < n; i++) {
675
+ const t = i / SAMPLE_RATE;
676
+ const freq = 90 * Math.pow(45 / 90, t / 0.15);
677
+ phase += TAU * freq / SAMPLE_RATE;
678
+ lp += 0.02 * (noise(i, seed) - lp);
679
+ const attack = t < 0.01 ? lp * 3 : 0;
680
+ out[i] = (Math.sin(phase) * 0.9 + attack) * expDecay(t, dur, 5);
681
+ }
682
+ return out;
683
+ }
684
+ var RECIPES = {
685
+ whoosh,
686
+ pop,
687
+ tick,
688
+ rise,
689
+ shimmer,
690
+ thud
691
+ };
692
+ function synthSfx(name, params = {}) {
693
+ const samples = RECIPES[name](params.seed ?? 0);
694
+ if (params.gainDb) {
695
+ const g = Math.pow(10, params.gainDb / 20);
696
+ for (let i = 0; i < samples.length; i++) samples[i] *= g;
697
+ }
698
+ return samples;
699
+ }
700
+ function synthAmbientPad(duration, seed = 0) {
701
+ const { out, n } = buffer(duration);
702
+ const voices = [110, 165, 220].flatMap((f, v) => [
703
+ { freq: f * (1 + (hash01(v, seed + 3) - 0.5) * 4e-3), am: 0.05 + hash01(v, seed + 4) * 0.08, phase: hash01(v, seed + 5) * TAU },
704
+ { freq: f * (1 - (hash01(v, seed + 6) - 0.5) * 4e-3), am: 0.05 + hash01(v, seed + 7) * 0.08, phase: hash01(v, seed + 8) * TAU }
705
+ ]);
706
+ for (let i = 0; i < n; i++) {
707
+ const t = i / SAMPLE_RATE;
708
+ let s = 0;
709
+ for (const voice of voices) {
710
+ s += Math.sin(TAU * voice.freq * t + voice.phase) * (0.75 + 0.25 * Math.sin(TAU * voice.am * t));
711
+ }
712
+ out[i] = s / voices.length * 0.7;
713
+ }
714
+ return out;
715
+ }
716
+
717
+ // ../render-cli/src/audio/sfx.ts
718
+ var ROOT = true ? resolve(dirname(fileURLToPath(import.meta.url)), "..") : resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
719
+ var VENDORED = join(ROOT, "assets", "sfx");
720
+ var CACHE = join(tmpdir(), "reframe-sfx-cache");
721
+ function fnv1a(text) {
722
+ let h = 2166136261;
723
+ for (let i = 0; i < text.length; i++) {
724
+ h ^= text.charCodeAt(i);
725
+ h = Math.imul(h, 16777619);
726
+ }
727
+ return (h >>> 0).toString(16);
728
+ }
729
+ async function writeCached(key2, make) {
730
+ const path = join(CACHE, `${key2}.wav`);
731
+ if (existsSync(path)) return path;
732
+ await mkdir(CACHE, { recursive: true });
733
+ const temp = `${path}.${process.pid}.${fnv1a(String(performance.now()))}.tmp`;
734
+ await writeFile(temp, encodeWavMono16(make()));
735
+ await rename(temp, path);
736
+ return path;
737
+ }
738
+ async function resolveCueFile(cue, sceneDir) {
739
+ if (cue.source.kind === "file") {
740
+ const p = cue.source.path;
741
+ for (const candidate of [
742
+ isAbsolute(p) ? p : null,
743
+ resolve(sceneDir, p),
744
+ join(VENDORED, p)
745
+ ]) {
746
+ if (candidate && existsSync(candidate)) return candidate;
747
+ }
748
+ throw new Error(
749
+ `audio cue file "${p}" not found (tried absolute, scene-relative, assets/sfx/)`
750
+ );
751
+ }
752
+ const vendored = join(VENDORED, `${cue.source.name}.wav`);
753
+ if (existsSync(vendored)) return vendored;
754
+ const { name, params } = cue.source;
755
+ return writeCached(`${name}-${fnv1a(JSON.stringify(params))}`, () => synthSfx(name, params));
756
+ }
757
+ async function resolveBgmFile(source, duration, sceneDir) {
758
+ if (source.kind === "file") {
759
+ const p = source.path;
760
+ for (const candidate of [isAbsolute(p) ? p : null, resolve(sceneDir, p), join(VENDORED, p)]) {
761
+ if (candidate && existsSync(candidate)) return candidate;
762
+ }
763
+ throw new Error(`bgm file "${p}" not found`);
764
+ }
765
+ return writeCached(`ambient-pad-${duration.toFixed(2)}`, () => synthAmbientPad(duration));
766
+ }
767
+
768
+ // ../render-cli/src/audio/mux.ts
769
+ import { spawn } from "node:child_process";
770
+ import { mkdtemp, rm, writeFile as writeFile2 } from "node:fs/promises";
771
+ import { tmpdir as tmpdir2 } from "node:os";
772
+ import { join as join2 } from "node:path";
773
+ var FORMAT = "aformat=sample_rates=44100:channel_layouts=stereo";
774
+ function buildFilterGraph(plan, inputs) {
775
+ const lines = [];
776
+ const mixIn = ["[anchor]"];
777
+ lines.push(`anullsrc=r=44100:cl=stereo,atrim=duration=${plan.duration.toFixed(3)}[anchor]`);
778
+ let inputIndex = 1;
779
+ if (plan.bgm && inputs.bgmFile) {
780
+ const b = plan.bgm;
781
+ const chain = [FORMAT, `volume=${b.gain}`];
782
+ if (b.fadeIn > 0) chain.push(`afade=t=in:st=0:d=${b.fadeIn}`);
783
+ if (b.fadeOut > 0) {
784
+ chain.push(`afade=t=out:st=${Math.max(0, plan.duration - b.fadeOut).toFixed(3)}:d=${b.fadeOut}`);
785
+ }
786
+ if (b.duck) {
787
+ for (const w of plan.duckWindows) {
788
+ const { attack, release, depth } = b.duck;
789
+ const t0 = (w.t0 - attack).toFixed(3);
790
+ const t1 = (w.t1 + release).toFixed(3);
791
+ chain.push(
792
+ `volume='1-${depth}*max(0\\,min(1\\,min((t-${t0})/${attack}\\,(${t1}-t)/${release})))':eval=frame`
793
+ );
794
+ }
795
+ }
796
+ lines.push(`[${inputIndex}:a]${chain.join(",")}[bgm]`);
797
+ mixIn.push("[bgm]");
798
+ inputIndex++;
799
+ }
800
+ plan.cues.forEach((cue, i) => {
801
+ const delayMs = Math.round(cue.t * 1e3);
802
+ lines.push(`[${inputIndex}:a]${FORMAT},volume=${cue.gain},adelay=${delayMs}:all=1[c${i}]`);
803
+ mixIn.push(`[c${i}]`);
804
+ inputIndex++;
805
+ });
806
+ lines.push(
807
+ `${mixIn.join("")}amix=inputs=${mixIn.length}:duration=first:normalize=0,alimiter=limit=0.891,aresample=async=1:first_pts=0[aout]`
808
+ );
809
+ return lines.join(";\n");
810
+ }
811
+ async function muxAudio(videoIn, plan, inputs, outFile) {
812
+ const work = await mkdtemp(join2(tmpdir2(), "reframe-mux-"));
813
+ try {
814
+ const graphFile = join2(work, "graph.txt");
815
+ await writeFile2(graphFile, buildFilterGraph(plan, inputs));
816
+ const args = [
817
+ "-y",
818
+ "-i",
819
+ videoIn,
820
+ ...plan.bgm && inputs.bgmFile ? ["-i", inputs.bgmFile] : [],
821
+ ...inputs.cueFiles.flatMap((f) => ["-i", f]),
822
+ "-filter_complex_script",
823
+ graphFile,
824
+ "-map",
825
+ "0:v",
826
+ "-map",
827
+ "[aout]",
828
+ "-c:v",
829
+ "copy",
830
+ "-c:a",
831
+ "aac",
832
+ "-b:a",
833
+ "192k",
834
+ "-ar",
835
+ "44100",
836
+ "-shortest",
837
+ outFile
838
+ ];
839
+ await new Promise((resolvePromise, reject) => {
840
+ const proc = spawn("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
841
+ let stderr = "";
842
+ proc.stderr.on("data", (d) => stderr += d.toString());
843
+ proc.on("close", (code) => {
844
+ if (code === 0) resolvePromise();
845
+ else reject(new Error(`ffmpeg mux exited ${code}:
846
+ ${stderr.slice(-2e3)}`));
847
+ });
848
+ proc.on("error", reject);
849
+ });
850
+ } finally {
851
+ await rm(work, { recursive: true, force: true });
852
+ }
853
+ }
854
+
855
+ // ../render-cli/src/audio/index.ts
856
+ async function buildAudioTrack(plan, scenePath, videoIn, outFile) {
857
+ const sceneDir = dirname2(scenePath);
858
+ const cueFiles = await Promise.all(plan.cues.map((cue) => resolveCueFile(cue, sceneDir)));
859
+ const bgmFile = plan.bgm ? await resolveBgmFile(plan.bgm.source, plan.duration, sceneDir) : null;
860
+ await muxAudio(videoIn, plan, { cueFiles, bgmFile }, outFile);
861
+ }
862
+
863
+ // ../render-cli/src/encode.ts
864
+ import { spawn as spawn2 } from "node:child_process";
865
+ async function encodeMp4(framesDir, fps, outFile) {
866
+ const args = [
867
+ "-y",
868
+ "-framerate",
869
+ String(fps),
870
+ "-i",
871
+ `${framesDir}/%05d.png`,
872
+ "-c:v",
873
+ "libx264",
874
+ "-preset",
875
+ "slow",
876
+ "-crf",
877
+ "18",
878
+ "-pix_fmt",
879
+ "yuv420p",
880
+ "-movflags",
881
+ "+faststart",
882
+ outFile
883
+ ];
884
+ await new Promise((resolve4, reject) => {
885
+ const proc = spawn2("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
886
+ let stderr = "";
887
+ proc.stderr.on("data", (d) => stderr += d.toString());
888
+ proc.on("close", (code) => {
889
+ if (code === 0) resolve4();
890
+ else reject(new Error(`ffmpeg exited with ${code}:
891
+ ${stderr.slice(-2e3)}`));
892
+ });
893
+ proc.on("error", reject);
894
+ });
895
+ }
896
+
897
+ // ../render-cli/src/frameLoop.ts
898
+ import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
899
+ import { join as join4, dirname as dirname4 } from "node:path";
900
+ import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
901
+ import { build } from "esbuild";
902
+ import { chromium } from "playwright";
903
+
904
+ // ../render-cli/src/fonts.ts
905
+ import { readFile } from "node:fs/promises";
906
+ import { dirname as dirname3, join as join3 } from "node:path";
907
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
908
+ var FONTS_DIR = true ? join3(dirname3(fileURLToPath2(import.meta.url)), "..", "assets", "fonts") : join3(dirname3(fileURLToPath2(import.meta.url)), "..", "..", "..", "assets", "fonts");
909
+ var WEIGHTS = [400, 700, 800];
910
+ var cssCache = null;
911
+ async function fontFaceCss() {
912
+ if (cssCache) return cssCache;
913
+ const rules = await Promise.all(
914
+ WEIGHTS.map(async (weight) => {
915
+ const data = await readFile(join3(FONTS_DIR, `inter-${weight}.woff2`));
916
+ return `@font-face {
917
+ font-family: "Inter";
918
+ font-style: normal;
919
+ font-weight: ${weight};
920
+ src: url(data:font/woff2;base64,${data.toString("base64")}) format("woff2");
921
+ }`;
922
+ })
923
+ );
924
+ cssCache = rules.join("\n");
925
+ return cssCache;
926
+ }
927
+
928
+ // ../render-cli/src/vclock.ts
929
+ var VCLOCK_SOURCE = String.raw`
930
+ (() => {
931
+ let now = 0;
932
+ let nextId = 1;
933
+ let rafQueue = [];
934
+ const timers = [];
935
+
936
+ Date.now = () => now;
937
+ performance.now = () => now;
938
+
939
+ window.requestAnimationFrame = (cb) => {
940
+ const id = nextId++;
941
+ rafQueue.push({ id, cb });
942
+ return id;
943
+ };
944
+ window.cancelAnimationFrame = (id) => {
945
+ rafQueue = rafQueue.filter((r) => r.id !== id);
946
+ };
947
+
948
+ const addTimer = (cb, delay, args, interval) => {
949
+ const id = nextId++;
950
+ timers.push({
951
+ id,
952
+ cb: () => cb(...args),
953
+ due: now + Math.max(Number(delay) || 0, 0),
954
+ interval: interval ? Math.max(Number(delay) || 0, 1) : undefined,
955
+ });
956
+ return id;
957
+ };
958
+ const removeTimer = (id) => {
959
+ const i = timers.findIndex((t) => t.id === id);
960
+ if (i >= 0) timers.splice(i, 1);
961
+ };
962
+ window.setTimeout = (cb, delay = 0, ...args) =>
963
+ typeof cb === "function" ? addTimer(cb, delay, args, false) : 0;
964
+ window.setInterval = (cb, delay = 0, ...args) =>
965
+ typeof cb === "function" ? addTimer(cb, delay, args, true) : 0;
966
+ window.clearTimeout = removeTimer;
967
+ window.clearInterval = removeTimer;
968
+
969
+ window.__vclock = {
970
+ now: () => now,
971
+ advanceTo(targetMs) {
972
+ // Fire due timers in order, letting fired callbacks schedule new ones.
973
+ for (;;) {
974
+ timers.sort((a, b) => a.due - b.due);
975
+ const next = timers[0];
976
+ if (!next || next.due > targetMs) break;
977
+ now = next.due;
978
+ if (next.interval !== undefined) next.due += next.interval;
979
+ else timers.shift();
980
+ next.cb();
981
+ }
982
+ now = targetMs;
983
+ // One rAF batch per frame; callbacks registered during the batch run
984
+ // on the next advanceTo (matching real browser semantics).
985
+ const batch = rafQueue;
986
+ rafQueue = [];
987
+ for (const { cb } of batch) cb(now);
988
+ },
989
+ };
990
+ })();
991
+ `;
992
+
993
+ // ../render-cli/src/frameLoop.ts
994
+ async function injectFonts(page) {
995
+ await page.addStyleTag({ content: await fontFaceCss() });
996
+ await page.evaluate(async () => {
997
+ await Promise.all([...document.fonts].map((f) => f.load()));
998
+ await document.fonts.ready;
999
+ });
1000
+ }
1001
+ var framePath = (dir, i) => join4(dir, `${String(i).padStart(5, "0")}.png`);
1002
+ async function withPage(size, fn) {
1003
+ const browser = await chromium.launch({
1004
+ args: ["--force-color-profile=srgb", "--font-render-hinting=none"]
1005
+ });
1006
+ try {
1007
+ const page = await browser.newPage({ viewport: size, deviceScaleFactor: 1 });
1008
+ return await fn(page);
1009
+ } finally {
1010
+ await browser.close();
1011
+ }
1012
+ }
1013
+ var bundleCache = null;
1014
+ async function browserBundle() {
1015
+ if (bundleCache) return bundleCache;
1016
+ if (true) {
1017
+ const { readFile: readFile4 } = await import("node:fs/promises");
1018
+ bundleCache = await readFile4(
1019
+ join4(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.js"),
1020
+ "utf8"
1021
+ );
1022
+ return bundleCache;
1023
+ }
1024
+ const entry = join4(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.ts");
1025
+ const result = await build({
1026
+ entryPoints: [entry],
1027
+ bundle: true,
1028
+ write: false,
1029
+ format: "iife",
1030
+ target: "es2022"
1031
+ });
1032
+ bundleCache = result.outputFiles[0].text;
1033
+ return bundleCache;
1034
+ }
1035
+ async function captureIr(ir, opts) {
1036
+ await mkdir2(opts.framesDir, { recursive: true });
1037
+ const bundle = await browserBundle();
1038
+ return withPage(ir.size, async (page) => {
1039
+ await page.setContent(
1040
+ `<!DOCTYPE html><html><body style="margin:0;background:#000"></body></html>`
1041
+ );
1042
+ await injectFonts(page);
1043
+ await page.addScriptTag({ content: bundle });
1044
+ const info = await page.evaluate(
1045
+ (sceneIr) => window.__reframe.init(sceneIr),
1046
+ ir
1047
+ );
1048
+ const fps = opts.fps ?? info.fps;
1049
+ const duration = opts.duration ?? info.duration;
1050
+ const frameCount = Math.max(1, Math.round(duration * fps));
1051
+ for (let f = 0; f < frameCount; f++) {
1052
+ const dataUrl = await page.evaluate((t) => window.__reframe.renderFrame(t), f / fps);
1053
+ await writeFile3(framePath(opts.framesDir, f), Buffer.from(dataUrl.slice(22), "base64"));
1054
+ }
1055
+ return { framesDir: opts.framesDir, frameCount, fps };
1056
+ });
1057
+ }
1058
+ async function captureHtml(htmlPath, opts) {
1059
+ await mkdir2(opts.framesDir, { recursive: true });
1060
+ const size = { width: opts.width ?? 1920, height: opts.height ?? 1080 };
1061
+ return withPage(size, async (page) => {
1062
+ await page.addInitScript(VCLOCK_SOURCE);
1063
+ await page.goto(pathToFileURL(htmlPath).href);
1064
+ await injectFonts(page);
1065
+ const stage = page.locator("#stage");
1066
+ const hasStage = await stage.count() > 0;
1067
+ const frameCount = Math.max(1, Math.round(opts.duration * opts.fps));
1068
+ for (let f = 0; f < frameCount; f++) {
1069
+ await page.evaluate(
1070
+ (ms) => window.__vclock.advanceTo(ms),
1071
+ f / opts.fps * 1e3
1072
+ );
1073
+ const path = framePath(opts.framesDir, f);
1074
+ if (hasStage) await stage.screenshot({ path, animations: "allow" });
1075
+ else await page.screenshot({ path, animations: "allow" });
1076
+ }
1077
+ return { framesDir: opts.framesDir, frameCount, fps: opts.fps };
1078
+ });
1079
+ }
1080
+
1081
+ // ../render-cli/src/loadScene.ts
1082
+ 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";
1085
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
1086
+ var HERE = dirname5(fileURLToPath4(import.meta.url));
1087
+ var CORE_ENTRY = true ? resolve2(HERE, "index.js") : resolve2(HERE, "..", "..", "core", "src", "index.ts");
1088
+ async function loadScene(path) {
1089
+ if (path.endsWith(".json")) {
1090
+ const ir = JSON.parse(await readFile2(path, "utf8"));
1091
+ validateScene(ir);
1092
+ return ir;
1093
+ }
1094
+ let code;
1095
+ try {
1096
+ const out = await build2({
1097
+ entryPoints: [path],
1098
+ bundle: true,
1099
+ format: "esm",
1100
+ platform: "neutral",
1101
+ write: false,
1102
+ logLevel: "silent",
1103
+ sourcemap: "inline",
1104
+ // both specifiers accepted: the guide's canonical "@reframe/core" and
1105
+ // the published package name
1106
+ alias: { "@reframe/core": CORE_ENTRY, "reframe-video": CORE_ENTRY }
1107
+ });
1108
+ code = out.outputFiles[0].text;
1109
+ } catch (err) {
1110
+ throw new Error(
1111
+ `failed to bundle ${path}:
1112
+ ${err instanceof Error ? err.message : String(err)}`
1113
+ );
1114
+ }
1115
+ const mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
1116
+ if (!mod.default) throw new Error(`${path} must default-export a scene`);
1117
+ return mod.default;
1118
+ }
1119
+
1120
+ // ../render-cli/src/cli.ts
1121
+ function parseArgs(argv) {
1122
+ const [mode, input, ...rest] = argv;
1123
+ if (mode !== "ir" && mode !== "html" || !input) {
1124
+ console.error(
1125
+ "usage: reframe-render ir <scene.ts|json> [-o out.mp4] [--fps N] [--keep-frames] [--frames-dir d]\n reframe-render html <page.html> --duration S [-o out.mp4] [--fps N] [--keep-frames] [--frames-dir d]"
1126
+ );
1127
+ process.exit(2);
1128
+ }
1129
+ const args = {
1130
+ mode,
1131
+ input: resolve3(input),
1132
+ out: `${basename(input).replace(/\.[^.]+$/, "")}.mp4`,
1133
+ keepFrames: false,
1134
+ overlays: [],
1135
+ noAudio: false
1136
+ };
1137
+ for (let i = 0; i < rest.length; i++) {
1138
+ const a = rest[i];
1139
+ if (a === "-o") args.out = rest[++i];
1140
+ else if (a === "--fps") args.fps = Number(rest[++i]);
1141
+ else if (a === "--duration") args.duration = Number(rest[++i]);
1142
+ 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]));
1145
+ else if (a === "--no-audio") args.noAudio = true;
1146
+ else {
1147
+ console.error(`unknown flag ${a}`);
1148
+ process.exit(2);
1149
+ }
1150
+ }
1151
+ return args;
1152
+ }
1153
+ async function main() {
1154
+ const args = parseArgs(process.argv.slice(2));
1155
+ const framesDir = args.framesDir ?? await mkdtemp2(join5(tmpdir3(), "reframe-frames-"));
1156
+ let result;
1157
+ let audioJob = null;
1158
+ if (args.mode === "ir") {
1159
+ let ir = await loadScene(args.input);
1160
+ if (args.overlays.length > 0) {
1161
+ const docs = await Promise.all(
1162
+ args.overlays.map(async (p) => JSON.parse(await readFile3(p, "utf8")))
1163
+ );
1164
+ const composed = composeScene(ir, ...docs);
1165
+ console.error(formatComposeReport(composed.report));
1166
+ ir = composed.ir;
1167
+ }
1168
+ if (!args.noAudio) {
1169
+ const plan = resolveAudioPlan(compileScene(ir));
1170
+ if (plan) {
1171
+ for (const w of plan.warnings) console.error(`audio: ${w}`);
1172
+ audioJob = { plan, videoOut: `${args.out}.video.mp4` };
1173
+ }
1174
+ }
1175
+ result = await captureIr(ir, {
1176
+ framesDir,
1177
+ ...args.fps !== void 0 && { fps: args.fps },
1178
+ ...args.duration !== void 0 && { duration: args.duration }
1179
+ });
1180
+ } else {
1181
+ if (args.duration === void 0 || Number.isNaN(args.duration)) {
1182
+ throw new Error("html mode requires --duration <seconds>");
1183
+ }
1184
+ result = await captureHtml(args.input, {
1185
+ framesDir,
1186
+ fps: args.fps ?? 30,
1187
+ duration: args.duration
1188
+ });
1189
+ }
1190
+ await encodeMp4(result.framesDir, result.fps, audioJob ? audioJob.videoOut : args.out);
1191
+ if (audioJob) {
1192
+ await buildAudioTrack(audioJob.plan, args.input, audioJob.videoOut, args.out);
1193
+ await rm2(audioJob.videoOut, { force: true });
1194
+ }
1195
+ if (!args.keepFrames && args.framesDir === void 0) {
1196
+ await rm2(framesDir, { recursive: true, force: true });
1197
+ }
1198
+ console.log(
1199
+ `${args.out} (${result.frameCount} frames @ ${result.fps}fps${audioJob ? `, ${audioJob.plan.cues.length} audio cues` : ""})`
1200
+ );
1201
+ }
1202
+ main().catch((err) => {
1203
+ console.error(err instanceof Error ? err.message : err);
1204
+ process.exit(1);
1205
+ });