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.
- package/LICENSE +24 -0
- package/README.md +77 -0
- package/assets/fonts/inter-400.woff2 +0 -0
- package/assets/fonts/inter-700.woff2 +0 -0
- package/assets/fonts/inter-800.woff2 +0 -0
- package/assets/sfx/LICENSE.md +12 -0
- package/assets/sfx/click_002.ogg +0 -0
- package/assets/sfx/click_003.ogg +0 -0
- package/assets/sfx/click_004.ogg +0 -0
- package/assets/sfx/confirmation_001.ogg +0 -0
- package/assets/sfx/keypress-001.wav +0 -0
- package/assets/sfx/keypress-004.wav +0 -0
- package/assets/sfx/keypress-007.wav +0 -0
- package/assets/sfx/keypress-010.wav +0 -0
- package/assets/sfx/keypress-014.wav +0 -0
- package/dist/analyze.js +344 -0
- package/dist/bin.js +1677 -0
- package/dist/browserEntry.js +532 -0
- package/dist/cli.js +1205 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +889 -0
- package/dist/renderer-canvas.js +89 -0
- package/dist/types/audio.d.ts +53 -0
- package/dist/types/behaviors.d.ts +7 -0
- package/dist/types/compile.d.ts +38 -0
- package/dist/types/compose.d.ts +64 -0
- package/dist/types/dsl.d.ts +66 -0
- package/dist/types/evaluate.d.ts +59 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/interpolate.d.ts +12 -0
- package/dist/types/ir.d.ts +213 -0
- package/dist/types/validate.d.ts +12 -0
- package/guides/edsl-guide.md +202 -0
- package/guides/regen-contract.md +18 -0
- package/package.json +55 -0
- package/preview/index.html +60 -0
- package/preview/src/main.ts +162 -0
- package/preview/src/panel.ts +347 -0
- package/preview/src/store.ts +220 -0
- package/preview/src/virtual.d.ts +4 -0
- package/preview/vite.config.ts +52 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,1677 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ../core/src/ir.ts
|
|
13
|
+
var DEFAULT_TO_DURATION, DEFAULT_TWEEN_DURATION;
|
|
14
|
+
var init_ir = __esm({
|
|
15
|
+
"../core/src/ir.ts"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
DEFAULT_TO_DURATION = 0.5;
|
|
18
|
+
DEFAULT_TWEEN_DURATION = 0.5;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ../core/src/compile.ts
|
|
23
|
+
function compileScene(ir) {
|
|
24
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
25
|
+
const nodeOrder = [];
|
|
26
|
+
const collect = (nodes) => {
|
|
27
|
+
for (const node of nodes) {
|
|
28
|
+
nodeById.set(node.id, node);
|
|
29
|
+
nodeOrder.push(node.id);
|
|
30
|
+
if (node.type === "group") collect(node.children);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
collect(ir.nodes);
|
|
34
|
+
const initialValues = /* @__PURE__ */ new Map();
|
|
35
|
+
for (const [id, node] of nodeById) {
|
|
36
|
+
for (const [prop, value] of Object.entries(node.props)) {
|
|
37
|
+
if (typeof value === "number" || typeof value === "string") {
|
|
38
|
+
initialValues.set(key(id, prop), value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (ir.initial !== void 0) {
|
|
43
|
+
const override = ir.states?.[ir.initial] ?? {};
|
|
44
|
+
for (const [id, props] of Object.entries(override)) {
|
|
45
|
+
for (const [prop, value] of Object.entries(props)) {
|
|
46
|
+
initialValues.set(key(id, prop), value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const segments = /* @__PURE__ */ new Map();
|
|
51
|
+
const current = new Map(initialValues);
|
|
52
|
+
const pushSegment = (seg) => {
|
|
53
|
+
const k = key(seg.target, seg.prop);
|
|
54
|
+
let list = segments.get(k);
|
|
55
|
+
if (!list) segments.set(k, list = []);
|
|
56
|
+
list.push(seg);
|
|
57
|
+
current.set(k, seg.to);
|
|
58
|
+
};
|
|
59
|
+
const currentValue = (target, prop) => {
|
|
60
|
+
const v = current.get(key(target, prop));
|
|
61
|
+
if (v !== void 0) return v;
|
|
62
|
+
if (prop === "opacity" || prop === "scale" || prop === "progress") return 1;
|
|
63
|
+
if (prop === "rotation") return 0;
|
|
64
|
+
throw new Error(`cannot animate "${prop}" of "${target}": no base value to start from`);
|
|
65
|
+
};
|
|
66
|
+
const labelTimes = /* @__PURE__ */ new Map();
|
|
67
|
+
const walk = (tl, start) => {
|
|
68
|
+
const end = walkInner(tl, start);
|
|
69
|
+
if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
|
|
70
|
+
return end;
|
|
71
|
+
};
|
|
72
|
+
const walkInner = (tl, start) => {
|
|
73
|
+
switch (tl.kind) {
|
|
74
|
+
case "seq": {
|
|
75
|
+
let t = start;
|
|
76
|
+
for (const child of tl.children) t = walk(child, t);
|
|
77
|
+
return t;
|
|
78
|
+
}
|
|
79
|
+
case "par": {
|
|
80
|
+
let end = start;
|
|
81
|
+
for (const child of tl.children) end = Math.max(end, walk(child, start));
|
|
82
|
+
return end;
|
|
83
|
+
}
|
|
84
|
+
case "stagger": {
|
|
85
|
+
let end = start;
|
|
86
|
+
tl.children.forEach((child, i) => {
|
|
87
|
+
end = Math.max(end, walk(child, start + i * tl.interval));
|
|
88
|
+
});
|
|
89
|
+
return end;
|
|
90
|
+
}
|
|
91
|
+
case "wait":
|
|
92
|
+
return start + tl.duration;
|
|
93
|
+
case "tween": {
|
|
94
|
+
const duration = tl.duration ?? DEFAULT_TWEEN_DURATION;
|
|
95
|
+
for (const [prop, toValue] of Object.entries(tl.props)) {
|
|
96
|
+
pushSegment({
|
|
97
|
+
target: tl.target,
|
|
98
|
+
prop,
|
|
99
|
+
t0: start,
|
|
100
|
+
t1: start + duration,
|
|
101
|
+
from: currentValue(tl.target, prop),
|
|
102
|
+
to: toValue,
|
|
103
|
+
...tl.ease !== void 0 && { ease: tl.ease }
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return start + duration;
|
|
107
|
+
}
|
|
108
|
+
case "to": {
|
|
109
|
+
const override = ir.states?.[tl.state] ?? {};
|
|
110
|
+
const duration = tl.duration ?? DEFAULT_TO_DURATION;
|
|
111
|
+
const staggerInterval = tl.stagger ?? 0;
|
|
112
|
+
const targets = nodeOrder.filter(
|
|
113
|
+
(id) => id in override && (tl.filter === void 0 || tl.filter.includes(id))
|
|
114
|
+
);
|
|
115
|
+
targets.forEach((id, i) => {
|
|
116
|
+
const t0 = start + i * staggerInterval;
|
|
117
|
+
for (const [prop, toValue] of Object.entries(override[id])) {
|
|
118
|
+
pushSegment({
|
|
119
|
+
target: id,
|
|
120
|
+
prop,
|
|
121
|
+
t0,
|
|
122
|
+
t1: t0 + duration,
|
|
123
|
+
from: currentValue(id, prop),
|
|
124
|
+
to: toValue,
|
|
125
|
+
...tl.ease !== void 0 && { ease: tl.ease }
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
const last = Math.max(0, targets.length - 1);
|
|
130
|
+
return start + duration + last * staggerInterval;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
|
|
135
|
+
for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
|
|
136
|
+
return {
|
|
137
|
+
ir,
|
|
138
|
+
duration: ir.duration ?? inferredEnd,
|
|
139
|
+
segments,
|
|
140
|
+
initialValues,
|
|
141
|
+
nodeById,
|
|
142
|
+
nodeOrder,
|
|
143
|
+
labelTimes
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
var key;
|
|
147
|
+
var init_compile = __esm({
|
|
148
|
+
"../core/src/compile.ts"() {
|
|
149
|
+
"use strict";
|
|
150
|
+
init_ir();
|
|
151
|
+
key = (target, prop) => `${target}.${prop}`;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ../core/src/validate.ts
|
|
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
|
+
var COMMON_PROPS, PROPS_BY_TYPE, SceneValidationError;
|
|
277
|
+
var init_validate = __esm({
|
|
278
|
+
"../core/src/validate.ts"() {
|
|
279
|
+
"use strict";
|
|
280
|
+
COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "anchor"];
|
|
281
|
+
PROPS_BY_TYPE = {
|
|
282
|
+
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
283
|
+
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
284
|
+
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
|
|
285
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
286
|
+
group: COMMON_PROPS
|
|
287
|
+
};
|
|
288
|
+
SceneValidationError = class extends Error {
|
|
289
|
+
constructor(problems) {
|
|
290
|
+
super(`Scene validation failed:
|
|
291
|
+
${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
292
|
+
this.problems = problems;
|
|
293
|
+
this.name = "SceneValidationError";
|
|
294
|
+
}
|
|
295
|
+
problems;
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// ../core/src/dsl.ts
|
|
301
|
+
var init_dsl = __esm({
|
|
302
|
+
"../core/src/dsl.ts"() {
|
|
303
|
+
"use strict";
|
|
304
|
+
init_compile();
|
|
305
|
+
init_validate();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ../core/src/compose.ts
|
|
310
|
+
function composeScene(base, ...overlays) {
|
|
311
|
+
const ir = structuredClone(base);
|
|
312
|
+
const report = { applied: [], orphans: [], warnings: [] };
|
|
313
|
+
overlays.forEach((overlay, index) => {
|
|
314
|
+
const layer = overlay.name ?? `overlay-${index}`;
|
|
315
|
+
if (overlay.target !== void 0 && overlay.target !== ir.id) {
|
|
316
|
+
report.warnings.push(
|
|
317
|
+
`${layer}: authored against scene "${overlay.target}" but composing onto "${ir.id}"`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
applyOverlay(ir, overlay, layer, report);
|
|
321
|
+
});
|
|
322
|
+
validateScene(ir);
|
|
323
|
+
return { ir, report };
|
|
324
|
+
}
|
|
325
|
+
function applyOverlay(ir, overlay, layer, report) {
|
|
326
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
327
|
+
const collect = (nodes) => {
|
|
328
|
+
for (const node of nodes) {
|
|
329
|
+
nodeById.set(node.id, node);
|
|
330
|
+
if (node.type === "group") collect(node.children);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
collect(ir.nodes);
|
|
334
|
+
const knownIds = () => [...nodeById.keys()].join(", ");
|
|
335
|
+
const orphan = (address, reason) => report.orphans.push({ layer, address, reason });
|
|
336
|
+
const applied = (address, action) => report.applied.push({ layer, address, action });
|
|
337
|
+
const patchProps = (address, node, target, patch) => {
|
|
338
|
+
const allowed = PROPS_BY_TYPE[node.type];
|
|
339
|
+
for (const [prop, value] of Object.entries(patch)) {
|
|
340
|
+
if (!allowed.includes(prop)) {
|
|
341
|
+
orphan(
|
|
342
|
+
`${address}.${prop}`,
|
|
343
|
+
`"${prop}" is not a prop of ${node.type} "${node.id}" \u2014 the base may have changed this node's type; valid props: ${allowed.join(", ")}`
|
|
344
|
+
);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (value === null) {
|
|
348
|
+
delete target[prop];
|
|
349
|
+
applied(`${address}.${prop}`, "unset");
|
|
350
|
+
} else {
|
|
351
|
+
target[prop] = value;
|
|
352
|
+
applied(`${address}.${prop}`, "set");
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
if (overlay.scene) {
|
|
357
|
+
for (const key2 of SCENE_PATCHABLE) {
|
|
358
|
+
const value = overlay.scene[key2];
|
|
359
|
+
if (value !== void 0) {
|
|
360
|
+
ir[key2] = value;
|
|
361
|
+
applied(`scene.${key2}`, "set");
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
for (const [id, patch] of Object.entries(overlay.nodes ?? {})) {
|
|
366
|
+
const node = nodeById.get(id);
|
|
367
|
+
if (!node) {
|
|
368
|
+
orphan(
|
|
369
|
+
`nodes.${id}`,
|
|
370
|
+
`unknown node "${id}" \u2014 known ids: ${knownIds()}; did the base regeneration rename it?`
|
|
371
|
+
);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
patchProps(`nodes.${id}`, node, node.props, patch);
|
|
375
|
+
}
|
|
376
|
+
for (const [stateName, statePatch] of Object.entries(overlay.states ?? {})) {
|
|
377
|
+
const state = ir.states?.[stateName];
|
|
378
|
+
if (!state) {
|
|
379
|
+
orphan(
|
|
380
|
+
`states.${stateName}`,
|
|
381
|
+
`unknown state "${stateName}" \u2014 defined states: ${Object.keys(ir.states ?? {}).join(", ") || "(none)"}`
|
|
382
|
+
);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
for (const [id, patch] of Object.entries(statePatch)) {
|
|
386
|
+
const node = nodeById.get(id);
|
|
387
|
+
if (!node) {
|
|
388
|
+
orphan(
|
|
389
|
+
`states.${stateName}.${id}`,
|
|
390
|
+
`unknown node "${id}" \u2014 known ids: ${knownIds()}; did the base regeneration rename it?`
|
|
391
|
+
);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const target = state[id] ??= {};
|
|
395
|
+
patchProps(`states.${stateName}.${id}`, node, target, patch);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (overlay.behaviors?.remove || overlay.behaviors?.set) {
|
|
399
|
+
ir.behaviors ??= [];
|
|
400
|
+
for (const { target, prop } of overlay.behaviors.remove ?? []) {
|
|
401
|
+
const index = ir.behaviors.findIndex((b) => b.target === target && b.prop === prop);
|
|
402
|
+
if (index < 0) {
|
|
403
|
+
orphan(
|
|
404
|
+
`behaviors.remove.${target}.${prop}`,
|
|
405
|
+
`no behavior on "${target}.${prop}" to remove`
|
|
406
|
+
);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
ir.behaviors.splice(index, 1);
|
|
410
|
+
applied(`behaviors.${target}.${prop}`, "behavior-remove");
|
|
411
|
+
}
|
|
412
|
+
for (const behavior of overlay.behaviors.set ?? []) {
|
|
413
|
+
if (!nodeById.has(behavior.target)) {
|
|
414
|
+
orphan(
|
|
415
|
+
`behaviors.set.${behavior.target}.${behavior.prop}`,
|
|
416
|
+
`unknown node "${behavior.target}" \u2014 known ids: ${knownIds()}`
|
|
417
|
+
);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const index = ir.behaviors.findIndex(
|
|
421
|
+
(b) => b.target === behavior.target && b.prop === behavior.prop
|
|
422
|
+
);
|
|
423
|
+
if (index >= 0) ir.behaviors[index] = structuredClone(behavior);
|
|
424
|
+
else ir.behaviors.push(structuredClone(behavior));
|
|
425
|
+
applied(`behaviors.${behavior.target}.${behavior.prop}`, "behavior-set");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (overlay.timeline) {
|
|
429
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
430
|
+
const walkTimeline = (tl) => {
|
|
431
|
+
if ("label" in tl && tl.label !== void 0) byLabel.set(tl.label, tl);
|
|
432
|
+
if ("children" in tl) tl.children.forEach(walkTimeline);
|
|
433
|
+
};
|
|
434
|
+
if (ir.timeline) walkTimeline(ir.timeline);
|
|
435
|
+
const PATCHABLE = {
|
|
436
|
+
to: ["duration", "ease", "stagger"],
|
|
437
|
+
tween: ["duration", "ease"],
|
|
438
|
+
wait: ["duration"]
|
|
439
|
+
};
|
|
440
|
+
let timingPatched = false;
|
|
441
|
+
for (const [label, patch] of Object.entries(overlay.timeline)) {
|
|
442
|
+
const step = byLabel.get(label);
|
|
443
|
+
if (!step) {
|
|
444
|
+
orphan(
|
|
445
|
+
`timeline.${label}`,
|
|
446
|
+
`unknown timeline label "${label}" \u2014 known labels: ${[...byLabel.keys()].join(", ") || "(none)"}; did the base regeneration drop it?`
|
|
447
|
+
);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const allowed = PATCHABLE[step.kind] ?? [];
|
|
451
|
+
for (const [key2, value] of Object.entries(patch)) {
|
|
452
|
+
if (value === void 0) continue;
|
|
453
|
+
if (!allowed.includes(key2)) {
|
|
454
|
+
orphan(
|
|
455
|
+
`timeline.${label}.${key2}`,
|
|
456
|
+
`"${key2}" is not patchable on a ${step.kind} step \u2014 patchable: ${allowed.join(", ")}`
|
|
457
|
+
);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
step[key2] = value;
|
|
461
|
+
applied(`timeline.${label}.${key2}`, "set");
|
|
462
|
+
if (key2 === "duration" || key2 === "stagger") timingPatched = true;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (timingPatched && overlay.scene?.duration === void 0) {
|
|
466
|
+
delete ir.duration;
|
|
467
|
+
ir.duration = compileScene(ir).duration;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
for (const node of overlay.addNodes ?? []) {
|
|
471
|
+
ir.nodes.push(structuredClone(node));
|
|
472
|
+
nodeById.set(node.id, node);
|
|
473
|
+
applied(`addNodes.${node.id}`, "add-node");
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
var SCENE_PATCHABLE;
|
|
477
|
+
var init_compose = __esm({
|
|
478
|
+
"../core/src/compose.ts"() {
|
|
479
|
+
"use strict";
|
|
480
|
+
init_compile();
|
|
481
|
+
init_validate();
|
|
482
|
+
SCENE_PATCHABLE = ["background", "duration", "fps"];
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// ../core/src/audio.ts
|
|
487
|
+
function resolveAudioPlan(compiled) {
|
|
488
|
+
const audio = compiled.ir.audio;
|
|
489
|
+
if (!audio || !audio.bgm && (audio.cues ?? []).length === 0) return null;
|
|
490
|
+
const warnings = [];
|
|
491
|
+
const duration = compiled.duration;
|
|
492
|
+
const cues = [];
|
|
493
|
+
for (const [index, cue] of (audio.cues ?? []).entries()) {
|
|
494
|
+
let anchor;
|
|
495
|
+
if (typeof cue.at === "number") {
|
|
496
|
+
anchor = cue.at;
|
|
497
|
+
} else {
|
|
498
|
+
const span = compiled.labelTimes.get(cue.at);
|
|
499
|
+
if (!span) {
|
|
500
|
+
warnings.push(`cue[${index}]: unknown label "${cue.at}" \u2014 cue dropped`);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
anchor = span.t0;
|
|
504
|
+
}
|
|
505
|
+
const t = Math.max(0, anchor + (cue.offset ?? 0));
|
|
506
|
+
const cueDuration = cue.sfx ? SFX_DURATION[cue.sfx] : FILE_CUE_DURATION;
|
|
507
|
+
if (t >= duration) {
|
|
508
|
+
warnings.push(`cue[${index}] at ${t.toFixed(2)}s starts past the scene end (${duration.toFixed(2)}s) \u2014 dropped`);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (t + cueDuration > duration) {
|
|
512
|
+
warnings.push(`cue[${index}] at ${t.toFixed(2)}s extends past the scene end \u2014 it will be truncated`);
|
|
513
|
+
}
|
|
514
|
+
cues.push({
|
|
515
|
+
t,
|
|
516
|
+
gain: cue.gain ?? 1,
|
|
517
|
+
duration: cueDuration,
|
|
518
|
+
source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
cues.sort((a, b) => a.t - b.t);
|
|
522
|
+
const duckWindows = [];
|
|
523
|
+
for (const cue of cues) {
|
|
524
|
+
const window2 = { t0: cue.t, t1: Math.min(duration, cue.t + cue.duration) };
|
|
525
|
+
const last = duckWindows[duckWindows.length - 1];
|
|
526
|
+
if (last && window2.t0 <= last.t1 + 0.1) last.t1 = Math.max(last.t1, window2.t1);
|
|
527
|
+
else duckWindows.push(window2);
|
|
528
|
+
}
|
|
529
|
+
let bgm = null;
|
|
530
|
+
if (audio.bgm) {
|
|
531
|
+
const b = audio.bgm;
|
|
532
|
+
const duck = b.duck === false ? null : {
|
|
533
|
+
depth: b.duck?.depth ?? 0.5,
|
|
534
|
+
attack: b.duck?.attack ?? 0.05,
|
|
535
|
+
release: b.duck?.release ?? 0.25
|
|
536
|
+
};
|
|
537
|
+
bgm = {
|
|
538
|
+
source: b.file ? { kind: "file", path: b.file } : { kind: "synth", name: b.synth ?? "ambient-pad" },
|
|
539
|
+
gain: b.gain ?? 0.5,
|
|
540
|
+
fadeIn: b.fadeIn ?? 0,
|
|
541
|
+
fadeOut: b.fadeOut ?? 0,
|
|
542
|
+
duck
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
return { duration, bgm, cues, duckWindows, warnings };
|
|
546
|
+
}
|
|
547
|
+
var SFX_DURATION, FILE_CUE_DURATION;
|
|
548
|
+
var init_audio = __esm({
|
|
549
|
+
"../core/src/audio.ts"() {
|
|
550
|
+
"use strict";
|
|
551
|
+
SFX_DURATION = {
|
|
552
|
+
whoosh: 0.35,
|
|
553
|
+
pop: 0.12,
|
|
554
|
+
tick: 0.03,
|
|
555
|
+
rise: 0.5,
|
|
556
|
+
shimmer: 0.9,
|
|
557
|
+
thud: 0.25
|
|
558
|
+
};
|
|
559
|
+
FILE_CUE_DURATION = 0.4;
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// ../core/src/behaviors.ts
|
|
564
|
+
var init_behaviors = __esm({
|
|
565
|
+
"../core/src/behaviors.ts"() {
|
|
566
|
+
"use strict";
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// ../core/src/interpolate.ts
|
|
571
|
+
var EASE_TABLE, EASE_NAMES;
|
|
572
|
+
var init_interpolate = __esm({
|
|
573
|
+
"../core/src/interpolate.ts"() {
|
|
574
|
+
"use strict";
|
|
575
|
+
EASE_TABLE = {
|
|
576
|
+
linear: (u) => u,
|
|
577
|
+
easeInQuad: (u) => u * u,
|
|
578
|
+
easeOutQuad: (u) => 1 - (1 - u) * (1 - u),
|
|
579
|
+
easeInOutQuad: (u) => u < 0.5 ? 2 * u * u : 1 - (-2 * u + 2) ** 2 / 2,
|
|
580
|
+
easeInCubic: (u) => u ** 3,
|
|
581
|
+
easeOutCubic: (u) => 1 - (1 - u) ** 3,
|
|
582
|
+
easeInOutCubic: (u) => u < 0.5 ? 4 * u ** 3 : 1 - (-2 * u + 2) ** 3 / 2,
|
|
583
|
+
easeInQuart: (u) => u ** 4,
|
|
584
|
+
easeOutQuart: (u) => 1 - (1 - u) ** 4,
|
|
585
|
+
easeInOutQuart: (u) => u < 0.5 ? 8 * u ** 4 : 1 - (-2 * u + 2) ** 4 / 2,
|
|
586
|
+
easeInExpo: (u) => u === 0 ? 0 : 2 ** (10 * u - 10),
|
|
587
|
+
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
|
|
589
|
+
};
|
|
590
|
+
EASE_NAMES = Object.keys(EASE_TABLE);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// ../core/src/evaluate.ts
|
|
595
|
+
var init_evaluate = __esm({
|
|
596
|
+
"../core/src/evaluate.ts"() {
|
|
597
|
+
"use strict";
|
|
598
|
+
init_behaviors();
|
|
599
|
+
init_interpolate();
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// ../core/src/index.ts
|
|
604
|
+
var init_src = __esm({
|
|
605
|
+
"../core/src/index.ts"() {
|
|
606
|
+
"use strict";
|
|
607
|
+
init_ir();
|
|
608
|
+
init_dsl();
|
|
609
|
+
init_validate();
|
|
610
|
+
init_compose();
|
|
611
|
+
init_compile();
|
|
612
|
+
init_audio();
|
|
613
|
+
init_evaluate();
|
|
614
|
+
init_interpolate();
|
|
615
|
+
init_behaviors();
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// ../render-cli/src/audio/wav.ts
|
|
620
|
+
function encodeWavMono16(samples, sampleRate = SAMPLE_RATE) {
|
|
621
|
+
const dataBytes = samples.length * 2;
|
|
622
|
+
const buffer2 = Buffer.alloc(44 + dataBytes);
|
|
623
|
+
buffer2.write("RIFF", 0);
|
|
624
|
+
buffer2.writeUInt32LE(36 + dataBytes, 4);
|
|
625
|
+
buffer2.write("WAVE", 8);
|
|
626
|
+
buffer2.write("fmt ", 12);
|
|
627
|
+
buffer2.writeUInt32LE(16, 16);
|
|
628
|
+
buffer2.writeUInt16LE(1, 20);
|
|
629
|
+
buffer2.writeUInt16LE(1, 22);
|
|
630
|
+
buffer2.writeUInt32LE(sampleRate, 24);
|
|
631
|
+
buffer2.writeUInt32LE(sampleRate * 2, 28);
|
|
632
|
+
buffer2.writeUInt16LE(2, 32);
|
|
633
|
+
buffer2.writeUInt16LE(16, 34);
|
|
634
|
+
buffer2.write("data", 36);
|
|
635
|
+
buffer2.writeUInt32LE(dataBytes, 40);
|
|
636
|
+
for (let i = 0; i < samples.length; i++) {
|
|
637
|
+
const s = Math.max(-1, Math.min(1, samples[i]));
|
|
638
|
+
buffer2.writeInt16LE(Math.round(s * 32767), 44 + i * 2);
|
|
639
|
+
}
|
|
640
|
+
return buffer2;
|
|
641
|
+
}
|
|
642
|
+
var SAMPLE_RATE;
|
|
643
|
+
var init_wav = __esm({
|
|
644
|
+
"../render-cli/src/audio/wav.ts"() {
|
|
645
|
+
"use strict";
|
|
646
|
+
SAMPLE_RATE = 44100;
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// ../render-cli/src/audio/synth.ts
|
|
651
|
+
function hash01(n, seed) {
|
|
652
|
+
let h = n * 374761393 + seed * 668265263 | 0;
|
|
653
|
+
h = h ^ h >>> 13 | 0;
|
|
654
|
+
h = Math.imul(h, 1274126177);
|
|
655
|
+
h = (h ^ h >>> 16) >>> 0;
|
|
656
|
+
return h / 4294967295;
|
|
657
|
+
}
|
|
658
|
+
function buffer(duration) {
|
|
659
|
+
const n = Math.round(duration * SAMPLE_RATE);
|
|
660
|
+
return { out: new Float32Array(n), n };
|
|
661
|
+
}
|
|
662
|
+
function whoosh(seed) {
|
|
663
|
+
const dur = 0.35;
|
|
664
|
+
const { out, n } = buffer(dur);
|
|
665
|
+
let lp = 0;
|
|
666
|
+
let lp2 = 0;
|
|
667
|
+
for (let i = 0; i < n; i++) {
|
|
668
|
+
const t = i / SAMPLE_RATE;
|
|
669
|
+
const u = t / dur;
|
|
670
|
+
const center = 1200 * Math.pow(300 / 1200, u);
|
|
671
|
+
const alpha = Math.min(1, TAU * center / SAMPLE_RATE);
|
|
672
|
+
lp += alpha * (noise(i, seed) - lp);
|
|
673
|
+
lp2 += alpha * 0.5 * (lp - lp2);
|
|
674
|
+
const env = u < 0.3 ? u / 0.3 : expDecay(t - 0.3 * dur, dur * 0.7, 4);
|
|
675
|
+
out[i] = (lp - lp2) * env * 2.2;
|
|
676
|
+
}
|
|
677
|
+
return out;
|
|
678
|
+
}
|
|
679
|
+
function pop(seed) {
|
|
680
|
+
const dur = 0.12;
|
|
681
|
+
const { out, n } = buffer(dur);
|
|
682
|
+
let phase = 0;
|
|
683
|
+
for (let i = 0; i < n; i++) {
|
|
684
|
+
const t = i / SAMPLE_RATE;
|
|
685
|
+
const freq = 600 * Math.pow(150 / 600, t / 0.08);
|
|
686
|
+
phase += TAU * freq / SAMPLE_RATE;
|
|
687
|
+
const transient = t < 2e-3 ? noise(i, seed) * 0.5 : 0;
|
|
688
|
+
out[i] = (Math.sin(phase) + transient) * expDecay(t, dur, 6) * 0.8;
|
|
689
|
+
}
|
|
690
|
+
return out;
|
|
691
|
+
}
|
|
692
|
+
function tick(seed) {
|
|
693
|
+
const dur = 0.03;
|
|
694
|
+
const { out, n } = buffer(dur);
|
|
695
|
+
for (let i = 0; i < n; i++) {
|
|
696
|
+
const t = i / SAMPLE_RATE;
|
|
697
|
+
const sine = t < 4e-3 ? Math.sin(TAU * 4e3 * t) : 0;
|
|
698
|
+
out[i] = (sine * 0.6 + noise(i, seed) * 0.35) * expDecay(t, dur, 8);
|
|
699
|
+
}
|
|
700
|
+
return out;
|
|
701
|
+
}
|
|
702
|
+
function rise(seed) {
|
|
703
|
+
const dur = 0.5;
|
|
704
|
+
const { out, n } = buffer(dur);
|
|
705
|
+
let phase = 0;
|
|
706
|
+
for (let i = 0; i < n; i++) {
|
|
707
|
+
const t = i / SAMPLE_RATE;
|
|
708
|
+
const u = t / dur;
|
|
709
|
+
const freq = 220 * Math.pow(880 / 220, u);
|
|
710
|
+
phase += TAU * freq / SAMPLE_RATE;
|
|
711
|
+
const env = Math.sin(Math.PI * Math.min(1, u * 1.05)) ** 1.5;
|
|
712
|
+
out[i] = (Math.sin(phase) + 0.3 * Math.sin(2 * phase)) * env * 0.45;
|
|
713
|
+
}
|
|
714
|
+
return out;
|
|
715
|
+
}
|
|
716
|
+
function shimmer(seed) {
|
|
717
|
+
const dur = 0.9;
|
|
718
|
+
const { out, n } = buffer(dur);
|
|
719
|
+
const partials = Array.from({ length: 5 }, (_, p) => ({
|
|
720
|
+
freq: 2e3 + hash01(p, seed + 7) * 2e3,
|
|
721
|
+
am: 0.5 + hash01(p, seed + 8) * 1.5,
|
|
722
|
+
phase: hash01(p, seed + 9) * TAU
|
|
723
|
+
}));
|
|
724
|
+
for (let i = 0; i < n; i++) {
|
|
725
|
+
const t = i / SAMPLE_RATE;
|
|
726
|
+
const u = t / dur;
|
|
727
|
+
const env = Math.sin(Math.PI * u) ** 1.2;
|
|
728
|
+
let s = 0;
|
|
729
|
+
for (const part of partials) {
|
|
730
|
+
s += Math.sin(TAU * part.freq * t + part.phase) * (0.6 + 0.4 * Math.sin(TAU * part.am * t));
|
|
731
|
+
}
|
|
732
|
+
out[i] = s / 5 * env * 0.5;
|
|
733
|
+
}
|
|
734
|
+
return out;
|
|
735
|
+
}
|
|
736
|
+
function thud(seed) {
|
|
737
|
+
const dur = 0.25;
|
|
738
|
+
const { out, n } = buffer(dur);
|
|
739
|
+
let phase = 0;
|
|
740
|
+
let lp = 0;
|
|
741
|
+
for (let i = 0; i < n; i++) {
|
|
742
|
+
const t = i / SAMPLE_RATE;
|
|
743
|
+
const freq = 90 * Math.pow(45 / 90, t / 0.15);
|
|
744
|
+
phase += TAU * freq / SAMPLE_RATE;
|
|
745
|
+
lp += 0.02 * (noise(i, seed) - lp);
|
|
746
|
+
const attack = t < 0.01 ? lp * 3 : 0;
|
|
747
|
+
out[i] = (Math.sin(phase) * 0.9 + attack) * expDecay(t, dur, 5);
|
|
748
|
+
}
|
|
749
|
+
return out;
|
|
750
|
+
}
|
|
751
|
+
function synthSfx(name, params = {}) {
|
|
752
|
+
const samples = RECIPES[name](params.seed ?? 0);
|
|
753
|
+
if (params.gainDb) {
|
|
754
|
+
const g = Math.pow(10, params.gainDb / 20);
|
|
755
|
+
for (let i = 0; i < samples.length; i++) samples[i] *= g;
|
|
756
|
+
}
|
|
757
|
+
return samples;
|
|
758
|
+
}
|
|
759
|
+
function synthAmbientPad(duration, seed = 0) {
|
|
760
|
+
const { out, n } = buffer(duration);
|
|
761
|
+
const voices = [110, 165, 220].flatMap((f, v) => [
|
|
762
|
+
{ 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 },
|
|
763
|
+
{ 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 }
|
|
764
|
+
]);
|
|
765
|
+
for (let i = 0; i < n; i++) {
|
|
766
|
+
const t = i / SAMPLE_RATE;
|
|
767
|
+
let s = 0;
|
|
768
|
+
for (const voice of voices) {
|
|
769
|
+
s += Math.sin(TAU * voice.freq * t + voice.phase) * (0.75 + 0.25 * Math.sin(TAU * voice.am * t));
|
|
770
|
+
}
|
|
771
|
+
out[i] = s / voices.length * 0.7;
|
|
772
|
+
}
|
|
773
|
+
return out;
|
|
774
|
+
}
|
|
775
|
+
var noise, TAU, expDecay, RECIPES;
|
|
776
|
+
var init_synth = __esm({
|
|
777
|
+
"../render-cli/src/audio/synth.ts"() {
|
|
778
|
+
"use strict";
|
|
779
|
+
init_wav();
|
|
780
|
+
noise = (n, seed) => hash01(n, seed) * 2 - 1;
|
|
781
|
+
TAU = Math.PI * 2;
|
|
782
|
+
expDecay = (t, dur, k = 5) => Math.exp(-k * t / dur);
|
|
783
|
+
RECIPES = {
|
|
784
|
+
whoosh,
|
|
785
|
+
pop,
|
|
786
|
+
tick,
|
|
787
|
+
rise,
|
|
788
|
+
shimmer,
|
|
789
|
+
thud
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// ../render-cli/src/audio/sfx.ts
|
|
795
|
+
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
796
|
+
import { existsSync } from "node:fs";
|
|
797
|
+
import { tmpdir } from "node:os";
|
|
798
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
799
|
+
import { fileURLToPath } from "node:url";
|
|
800
|
+
function fnv1a(text) {
|
|
801
|
+
let h = 2166136261;
|
|
802
|
+
for (let i = 0; i < text.length; i++) {
|
|
803
|
+
h ^= text.charCodeAt(i);
|
|
804
|
+
h = Math.imul(h, 16777619);
|
|
805
|
+
}
|
|
806
|
+
return (h >>> 0).toString(16);
|
|
807
|
+
}
|
|
808
|
+
async function writeCached(key2, make) {
|
|
809
|
+
const path = join(CACHE, `${key2}.wav`);
|
|
810
|
+
if (existsSync(path)) return path;
|
|
811
|
+
await mkdir(CACHE, { recursive: true });
|
|
812
|
+
const temp = `${path}.${process.pid}.${fnv1a(String(performance.now()))}.tmp`;
|
|
813
|
+
await writeFile(temp, encodeWavMono16(make()));
|
|
814
|
+
await rename(temp, path);
|
|
815
|
+
return path;
|
|
816
|
+
}
|
|
817
|
+
async function resolveCueFile(cue, sceneDir) {
|
|
818
|
+
if (cue.source.kind === "file") {
|
|
819
|
+
const p = cue.source.path;
|
|
820
|
+
for (const candidate of [
|
|
821
|
+
isAbsolute(p) ? p : null,
|
|
822
|
+
resolve(sceneDir, p),
|
|
823
|
+
join(VENDORED, p)
|
|
824
|
+
]) {
|
|
825
|
+
if (candidate && existsSync(candidate)) return candidate;
|
|
826
|
+
}
|
|
827
|
+
throw new Error(
|
|
828
|
+
`audio cue file "${p}" not found (tried absolute, scene-relative, assets/sfx/)`
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
const vendored = join(VENDORED, `${cue.source.name}.wav`);
|
|
832
|
+
if (existsSync(vendored)) return vendored;
|
|
833
|
+
const { name, params } = cue.source;
|
|
834
|
+
return writeCached(`${name}-${fnv1a(JSON.stringify(params))}`, () => synthSfx(name, params));
|
|
835
|
+
}
|
|
836
|
+
async function resolveBgmFile(source, duration, sceneDir) {
|
|
837
|
+
if (source.kind === "file") {
|
|
838
|
+
const p = source.path;
|
|
839
|
+
for (const candidate of [isAbsolute(p) ? p : null, resolve(sceneDir, p), join(VENDORED, p)]) {
|
|
840
|
+
if (candidate && existsSync(candidate)) return candidate;
|
|
841
|
+
}
|
|
842
|
+
throw new Error(`bgm file "${p}" not found`);
|
|
843
|
+
}
|
|
844
|
+
return writeCached(`ambient-pad-${duration.toFixed(2)}`, () => synthAmbientPad(duration));
|
|
845
|
+
}
|
|
846
|
+
var ROOT, VENDORED, CACHE;
|
|
847
|
+
var init_sfx = __esm({
|
|
848
|
+
"../render-cli/src/audio/sfx.ts"() {
|
|
849
|
+
"use strict";
|
|
850
|
+
init_synth();
|
|
851
|
+
init_wav();
|
|
852
|
+
ROOT = true ? resolve(dirname(fileURLToPath(import.meta.url)), "..") : resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
|
|
853
|
+
VENDORED = join(ROOT, "assets", "sfx");
|
|
854
|
+
CACHE = join(tmpdir(), "reframe-sfx-cache");
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
// ../render-cli/src/audio/mux.ts
|
|
859
|
+
import { spawn } from "node:child_process";
|
|
860
|
+
import { mkdtemp, rm, writeFile as writeFile2 } from "node:fs/promises";
|
|
861
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
862
|
+
import { join as join2 } from "node:path";
|
|
863
|
+
function buildFilterGraph(plan, inputs) {
|
|
864
|
+
const lines = [];
|
|
865
|
+
const mixIn = ["[anchor]"];
|
|
866
|
+
lines.push(`anullsrc=r=44100:cl=stereo,atrim=duration=${plan.duration.toFixed(3)}[anchor]`);
|
|
867
|
+
let inputIndex = 1;
|
|
868
|
+
if (plan.bgm && inputs.bgmFile) {
|
|
869
|
+
const b = plan.bgm;
|
|
870
|
+
const chain = [FORMAT, `volume=${b.gain}`];
|
|
871
|
+
if (b.fadeIn > 0) chain.push(`afade=t=in:st=0:d=${b.fadeIn}`);
|
|
872
|
+
if (b.fadeOut > 0) {
|
|
873
|
+
chain.push(`afade=t=out:st=${Math.max(0, plan.duration - b.fadeOut).toFixed(3)}:d=${b.fadeOut}`);
|
|
874
|
+
}
|
|
875
|
+
if (b.duck) {
|
|
876
|
+
for (const w of plan.duckWindows) {
|
|
877
|
+
const { attack, release, depth } = b.duck;
|
|
878
|
+
const t0 = (w.t0 - attack).toFixed(3);
|
|
879
|
+
const t1 = (w.t1 + release).toFixed(3);
|
|
880
|
+
chain.push(
|
|
881
|
+
`volume='1-${depth}*max(0\\,min(1\\,min((t-${t0})/${attack}\\,(${t1}-t)/${release})))':eval=frame`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
lines.push(`[${inputIndex}:a]${chain.join(",")}[bgm]`);
|
|
886
|
+
mixIn.push("[bgm]");
|
|
887
|
+
inputIndex++;
|
|
888
|
+
}
|
|
889
|
+
plan.cues.forEach((cue, i) => {
|
|
890
|
+
const delayMs = Math.round(cue.t * 1e3);
|
|
891
|
+
lines.push(`[${inputIndex}:a]${FORMAT},volume=${cue.gain},adelay=${delayMs}:all=1[c${i}]`);
|
|
892
|
+
mixIn.push(`[c${i}]`);
|
|
893
|
+
inputIndex++;
|
|
894
|
+
});
|
|
895
|
+
lines.push(
|
|
896
|
+
`${mixIn.join("")}amix=inputs=${mixIn.length}:duration=first:normalize=0,alimiter=limit=0.891,aresample=async=1:first_pts=0[aout]`
|
|
897
|
+
);
|
|
898
|
+
return lines.join(";\n");
|
|
899
|
+
}
|
|
900
|
+
async function muxAudio(videoIn, plan, inputs, outFile) {
|
|
901
|
+
const work = await mkdtemp(join2(tmpdir2(), "reframe-mux-"));
|
|
902
|
+
try {
|
|
903
|
+
const graphFile = join2(work, "graph.txt");
|
|
904
|
+
await writeFile2(graphFile, buildFilterGraph(plan, inputs));
|
|
905
|
+
const args = [
|
|
906
|
+
"-y",
|
|
907
|
+
"-i",
|
|
908
|
+
videoIn,
|
|
909
|
+
...plan.bgm && inputs.bgmFile ? ["-i", inputs.bgmFile] : [],
|
|
910
|
+
...inputs.cueFiles.flatMap((f) => ["-i", f]),
|
|
911
|
+
"-filter_complex_script",
|
|
912
|
+
graphFile,
|
|
913
|
+
"-map",
|
|
914
|
+
"0:v",
|
|
915
|
+
"-map",
|
|
916
|
+
"[aout]",
|
|
917
|
+
"-c:v",
|
|
918
|
+
"copy",
|
|
919
|
+
"-c:a",
|
|
920
|
+
"aac",
|
|
921
|
+
"-b:a",
|
|
922
|
+
"192k",
|
|
923
|
+
"-ar",
|
|
924
|
+
"44100",
|
|
925
|
+
"-shortest",
|
|
926
|
+
outFile
|
|
927
|
+
];
|
|
928
|
+
await new Promise((resolvePromise, reject) => {
|
|
929
|
+
const proc = spawn("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
930
|
+
let stderr = "";
|
|
931
|
+
proc.stderr.on("data", (d) => stderr += d.toString());
|
|
932
|
+
proc.on("close", (code) => {
|
|
933
|
+
if (code === 0) resolvePromise();
|
|
934
|
+
else reject(new Error(`ffmpeg mux exited ${code}:
|
|
935
|
+
${stderr.slice(-2e3)}`));
|
|
936
|
+
});
|
|
937
|
+
proc.on("error", reject);
|
|
938
|
+
});
|
|
939
|
+
} finally {
|
|
940
|
+
await rm(work, { recursive: true, force: true });
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
var FORMAT;
|
|
944
|
+
var init_mux = __esm({
|
|
945
|
+
"../render-cli/src/audio/mux.ts"() {
|
|
946
|
+
"use strict";
|
|
947
|
+
FORMAT = "aformat=sample_rates=44100:channel_layouts=stereo";
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// ../render-cli/src/audio/index.ts
|
|
952
|
+
import { dirname as dirname2 } from "node:path";
|
|
953
|
+
async function buildAudioTrack(plan, scenePath, videoIn, outFile) {
|
|
954
|
+
const sceneDir = dirname2(scenePath);
|
|
955
|
+
const cueFiles = await Promise.all(plan.cues.map((cue) => resolveCueFile(cue, sceneDir)));
|
|
956
|
+
const bgmFile = plan.bgm ? await resolveBgmFile(plan.bgm.source, plan.duration, sceneDir) : null;
|
|
957
|
+
await muxAudio(videoIn, plan, { cueFiles, bgmFile }, outFile);
|
|
958
|
+
}
|
|
959
|
+
var init_audio2 = __esm({
|
|
960
|
+
"../render-cli/src/audio/index.ts"() {
|
|
961
|
+
"use strict";
|
|
962
|
+
init_sfx();
|
|
963
|
+
init_mux();
|
|
964
|
+
init_mux();
|
|
965
|
+
init_synth();
|
|
966
|
+
init_wav();
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
// ../render-cli/src/encode.ts
|
|
971
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
972
|
+
async function encodeMp4(framesDir, fps, outFile) {
|
|
973
|
+
const args = [
|
|
974
|
+
"-y",
|
|
975
|
+
"-framerate",
|
|
976
|
+
String(fps),
|
|
977
|
+
"-i",
|
|
978
|
+
`${framesDir}/%05d.png`,
|
|
979
|
+
"-c:v",
|
|
980
|
+
"libx264",
|
|
981
|
+
"-preset",
|
|
982
|
+
"slow",
|
|
983
|
+
"-crf",
|
|
984
|
+
"18",
|
|
985
|
+
"-pix_fmt",
|
|
986
|
+
"yuv420p",
|
|
987
|
+
"-movflags",
|
|
988
|
+
"+faststart",
|
|
989
|
+
outFile
|
|
990
|
+
];
|
|
991
|
+
await new Promise((resolve4, reject) => {
|
|
992
|
+
const proc = spawn2("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
993
|
+
let stderr = "";
|
|
994
|
+
proc.stderr.on("data", (d) => stderr += d.toString());
|
|
995
|
+
proc.on("close", (code) => {
|
|
996
|
+
if (code === 0) resolve4();
|
|
997
|
+
else reject(new Error(`ffmpeg exited with ${code}:
|
|
998
|
+
${stderr.slice(-2e3)}`));
|
|
999
|
+
});
|
|
1000
|
+
proc.on("error", reject);
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
var init_encode = __esm({
|
|
1004
|
+
"../render-cli/src/encode.ts"() {
|
|
1005
|
+
"use strict";
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
// ../render-cli/src/fonts.ts
|
|
1010
|
+
import { readFile } from "node:fs/promises";
|
|
1011
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
1012
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1013
|
+
async function fontFaceCss() {
|
|
1014
|
+
if (cssCache) return cssCache;
|
|
1015
|
+
const rules = await Promise.all(
|
|
1016
|
+
WEIGHTS.map(async (weight) => {
|
|
1017
|
+
const data = await readFile(join3(FONTS_DIR, `inter-${weight}.woff2`));
|
|
1018
|
+
return `@font-face {
|
|
1019
|
+
font-family: "Inter";
|
|
1020
|
+
font-style: normal;
|
|
1021
|
+
font-weight: ${weight};
|
|
1022
|
+
src: url(data:font/woff2;base64,${data.toString("base64")}) format("woff2");
|
|
1023
|
+
}`;
|
|
1024
|
+
})
|
|
1025
|
+
);
|
|
1026
|
+
cssCache = rules.join("\n");
|
|
1027
|
+
return cssCache;
|
|
1028
|
+
}
|
|
1029
|
+
var FONTS_DIR, WEIGHTS, cssCache;
|
|
1030
|
+
var init_fonts = __esm({
|
|
1031
|
+
"../render-cli/src/fonts.ts"() {
|
|
1032
|
+
"use strict";
|
|
1033
|
+
FONTS_DIR = true ? join3(dirname3(fileURLToPath2(import.meta.url)), "..", "assets", "fonts") : join3(dirname3(fileURLToPath2(import.meta.url)), "..", "..", "..", "assets", "fonts");
|
|
1034
|
+
WEIGHTS = [400, 700, 800];
|
|
1035
|
+
cssCache = null;
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
// ../render-cli/src/vclock.ts
|
|
1040
|
+
var VCLOCK_SOURCE;
|
|
1041
|
+
var init_vclock = __esm({
|
|
1042
|
+
"../render-cli/src/vclock.ts"() {
|
|
1043
|
+
"use strict";
|
|
1044
|
+
VCLOCK_SOURCE = String.raw`
|
|
1045
|
+
(() => {
|
|
1046
|
+
let now = 0;
|
|
1047
|
+
let nextId = 1;
|
|
1048
|
+
let rafQueue = [];
|
|
1049
|
+
const timers = [];
|
|
1050
|
+
|
|
1051
|
+
Date.now = () => now;
|
|
1052
|
+
performance.now = () => now;
|
|
1053
|
+
|
|
1054
|
+
window.requestAnimationFrame = (cb) => {
|
|
1055
|
+
const id = nextId++;
|
|
1056
|
+
rafQueue.push({ id, cb });
|
|
1057
|
+
return id;
|
|
1058
|
+
};
|
|
1059
|
+
window.cancelAnimationFrame = (id) => {
|
|
1060
|
+
rafQueue = rafQueue.filter((r) => r.id !== id);
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
const addTimer = (cb, delay, args, interval) => {
|
|
1064
|
+
const id = nextId++;
|
|
1065
|
+
timers.push({
|
|
1066
|
+
id,
|
|
1067
|
+
cb: () => cb(...args),
|
|
1068
|
+
due: now + Math.max(Number(delay) || 0, 0),
|
|
1069
|
+
interval: interval ? Math.max(Number(delay) || 0, 1) : undefined,
|
|
1070
|
+
});
|
|
1071
|
+
return id;
|
|
1072
|
+
};
|
|
1073
|
+
const removeTimer = (id) => {
|
|
1074
|
+
const i = timers.findIndex((t) => t.id === id);
|
|
1075
|
+
if (i >= 0) timers.splice(i, 1);
|
|
1076
|
+
};
|
|
1077
|
+
window.setTimeout = (cb, delay = 0, ...args) =>
|
|
1078
|
+
typeof cb === "function" ? addTimer(cb, delay, args, false) : 0;
|
|
1079
|
+
window.setInterval = (cb, delay = 0, ...args) =>
|
|
1080
|
+
typeof cb === "function" ? addTimer(cb, delay, args, true) : 0;
|
|
1081
|
+
window.clearTimeout = removeTimer;
|
|
1082
|
+
window.clearInterval = removeTimer;
|
|
1083
|
+
|
|
1084
|
+
window.__vclock = {
|
|
1085
|
+
now: () => now,
|
|
1086
|
+
advanceTo(targetMs) {
|
|
1087
|
+
// Fire due timers in order, letting fired callbacks schedule new ones.
|
|
1088
|
+
for (;;) {
|
|
1089
|
+
timers.sort((a, b) => a.due - b.due);
|
|
1090
|
+
const next = timers[0];
|
|
1091
|
+
if (!next || next.due > targetMs) break;
|
|
1092
|
+
now = next.due;
|
|
1093
|
+
if (next.interval !== undefined) next.due += next.interval;
|
|
1094
|
+
else timers.shift();
|
|
1095
|
+
next.cb();
|
|
1096
|
+
}
|
|
1097
|
+
now = targetMs;
|
|
1098
|
+
// One rAF batch per frame; callbacks registered during the batch run
|
|
1099
|
+
// on the next advanceTo (matching real browser semantics).
|
|
1100
|
+
const batch = rafQueue;
|
|
1101
|
+
rafQueue = [];
|
|
1102
|
+
for (const { cb } of batch) cb(now);
|
|
1103
|
+
},
|
|
1104
|
+
};
|
|
1105
|
+
})();
|
|
1106
|
+
`;
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
// ../render-cli/src/reframeGlobal.ts
|
|
1111
|
+
var init_reframeGlobal = __esm({
|
|
1112
|
+
"../render-cli/src/reframeGlobal.ts"() {
|
|
1113
|
+
"use strict";
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
// ../render-cli/src/frameLoop.ts
|
|
1118
|
+
import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
|
|
1119
|
+
import { join as join4, dirname as dirname4 } from "node:path";
|
|
1120
|
+
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
1121
|
+
import { build } from "esbuild";
|
|
1122
|
+
import { chromium } from "playwright";
|
|
1123
|
+
async function injectFonts(page) {
|
|
1124
|
+
await page.addStyleTag({ content: await fontFaceCss() });
|
|
1125
|
+
await page.evaluate(async () => {
|
|
1126
|
+
await Promise.all([...document.fonts].map((f) => f.load()));
|
|
1127
|
+
await document.fonts.ready;
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
async function withPage(size, fn) {
|
|
1131
|
+
const browser = await chromium.launch({
|
|
1132
|
+
args: ["--force-color-profile=srgb", "--font-render-hinting=none"]
|
|
1133
|
+
});
|
|
1134
|
+
try {
|
|
1135
|
+
const page = await browser.newPage({ viewport: size, deviceScaleFactor: 1 });
|
|
1136
|
+
return await fn(page);
|
|
1137
|
+
} finally {
|
|
1138
|
+
await browser.close();
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
async function browserBundle() {
|
|
1142
|
+
if (bundleCache) return bundleCache;
|
|
1143
|
+
if (true) {
|
|
1144
|
+
const { readFile: readFile4 } = await import("node:fs/promises");
|
|
1145
|
+
bundleCache = await readFile4(
|
|
1146
|
+
join4(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.js"),
|
|
1147
|
+
"utf8"
|
|
1148
|
+
);
|
|
1149
|
+
return bundleCache;
|
|
1150
|
+
}
|
|
1151
|
+
const entry = join4(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.ts");
|
|
1152
|
+
const result = await build({
|
|
1153
|
+
entryPoints: [entry],
|
|
1154
|
+
bundle: true,
|
|
1155
|
+
write: false,
|
|
1156
|
+
format: "iife",
|
|
1157
|
+
target: "es2022"
|
|
1158
|
+
});
|
|
1159
|
+
bundleCache = result.outputFiles[0].text;
|
|
1160
|
+
return bundleCache;
|
|
1161
|
+
}
|
|
1162
|
+
async function captureIr(ir, opts) {
|
|
1163
|
+
await mkdir2(opts.framesDir, { recursive: true });
|
|
1164
|
+
const bundle = await browserBundle();
|
|
1165
|
+
return withPage(ir.size, async (page) => {
|
|
1166
|
+
await page.setContent(
|
|
1167
|
+
`<!DOCTYPE html><html><body style="margin:0;background:#000"></body></html>`
|
|
1168
|
+
);
|
|
1169
|
+
await injectFonts(page);
|
|
1170
|
+
await page.addScriptTag({ content: bundle });
|
|
1171
|
+
const info = await page.evaluate(
|
|
1172
|
+
(sceneIr) => window.__reframe.init(sceneIr),
|
|
1173
|
+
ir
|
|
1174
|
+
);
|
|
1175
|
+
const fps = opts.fps ?? info.fps;
|
|
1176
|
+
const duration = opts.duration ?? info.duration;
|
|
1177
|
+
const frameCount = Math.max(1, Math.round(duration * fps));
|
|
1178
|
+
for (let f = 0; f < frameCount; f++) {
|
|
1179
|
+
const dataUrl = await page.evaluate((t) => window.__reframe.renderFrame(t), f / fps);
|
|
1180
|
+
await writeFile3(framePath(opts.framesDir, f), Buffer.from(dataUrl.slice(22), "base64"));
|
|
1181
|
+
}
|
|
1182
|
+
return { framesDir: opts.framesDir, frameCount, fps };
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
var framePath, bundleCache;
|
|
1186
|
+
var init_frameLoop = __esm({
|
|
1187
|
+
"../render-cli/src/frameLoop.ts"() {
|
|
1188
|
+
"use strict";
|
|
1189
|
+
init_fonts();
|
|
1190
|
+
init_vclock();
|
|
1191
|
+
init_reframeGlobal();
|
|
1192
|
+
framePath = (dir, i) => join4(dir, `${String(i).padStart(5, "0")}.png`);
|
|
1193
|
+
bundleCache = null;
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
// ../render-cli/src/batch.ts
|
|
1198
|
+
var batch_exports = {};
|
|
1199
|
+
__export(batch_exports, {
|
|
1200
|
+
loadRows: () => loadRows,
|
|
1201
|
+
overlayFromFlat: () => overlayFromFlat,
|
|
1202
|
+
parseCsv: () => parseCsv,
|
|
1203
|
+
runBatch: () => runBatch
|
|
1204
|
+
});
|
|
1205
|
+
import { mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile2, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
|
|
1206
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
1207
|
+
import { join as join5 } from "node:path";
|
|
1208
|
+
function overlayFromFlat(row, name) {
|
|
1209
|
+
const doc = { reframeOverlay: 1, name };
|
|
1210
|
+
for (const [key2, raw] of Object.entries(row)) {
|
|
1211
|
+
if (key2.startsWith("_")) continue;
|
|
1212
|
+
if (raw === null || raw === void 0 || raw === "") continue;
|
|
1213
|
+
const value = raw;
|
|
1214
|
+
const parts = key2.split(".");
|
|
1215
|
+
const [head] = parts;
|
|
1216
|
+
if (head === "nodes" && parts.length === 3) {
|
|
1217
|
+
((doc.nodes ??= {})[parts[1]] ??= {})[parts[2]] = value;
|
|
1218
|
+
} else if (head === "states" && parts.length === 4) {
|
|
1219
|
+
(((doc.states ??= {})[parts[1]] ??= {})[parts[2]] ??= {})[parts[3]] = value;
|
|
1220
|
+
} else if (head === "timeline" && parts.length === 3) {
|
|
1221
|
+
const patchKey = parts[2];
|
|
1222
|
+
if (!["duration", "ease", "stagger"].includes(patchKey)) {
|
|
1223
|
+
throw new Error(
|
|
1224
|
+
`row key "${key2}": timeline patches support duration/ease/stagger, got "${patchKey}"`
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
((doc.timeline ??= {})[parts[1]] ??= {})[patchKey] = value;
|
|
1228
|
+
} else if (head === "scene" && parts.length === 2) {
|
|
1229
|
+
const sceneKey = parts[1];
|
|
1230
|
+
if (!["background", "duration", "fps"].includes(sceneKey)) {
|
|
1231
|
+
throw new Error(
|
|
1232
|
+
`row key "${key2}": scene patches support background/duration/fps, got "${sceneKey}"`
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
(doc.scene ??= {})[sceneKey] = value;
|
|
1236
|
+
} else {
|
|
1237
|
+
throw new Error(
|
|
1238
|
+
`row key "${key2}" is not a valid overlay address \u2014 expected nodes.<id>.<prop>, states.<state>.<id>.<prop>, timeline.<label>.<duration|ease|stagger>, or scene.<background|duration|fps>`
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return doc;
|
|
1243
|
+
}
|
|
1244
|
+
function parseCsv(text) {
|
|
1245
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
1246
|
+
if (lines.length < 2) return [];
|
|
1247
|
+
const split = (line) => {
|
|
1248
|
+
const out = [];
|
|
1249
|
+
let cur = "";
|
|
1250
|
+
let quoted = false;
|
|
1251
|
+
for (let i = 0; i < line.length; i++) {
|
|
1252
|
+
const ch = line[i];
|
|
1253
|
+
if (quoted) {
|
|
1254
|
+
if (ch === '"' && line[i + 1] === '"') {
|
|
1255
|
+
cur += '"';
|
|
1256
|
+
i++;
|
|
1257
|
+
} else if (ch === '"') quoted = false;
|
|
1258
|
+
else cur += ch;
|
|
1259
|
+
} else if (ch === '"') quoted = true;
|
|
1260
|
+
else if (ch === ",") {
|
|
1261
|
+
out.push(cur);
|
|
1262
|
+
cur = "";
|
|
1263
|
+
} else cur += ch;
|
|
1264
|
+
}
|
|
1265
|
+
out.push(cur);
|
|
1266
|
+
return out;
|
|
1267
|
+
};
|
|
1268
|
+
const headers = split(lines[0]).map((h) => h.trim());
|
|
1269
|
+
return lines.slice(1).map((line) => {
|
|
1270
|
+
const cells = split(line);
|
|
1271
|
+
const row = {};
|
|
1272
|
+
headers.forEach((h, i) => {
|
|
1273
|
+
const cell = (cells[i] ?? "").trim();
|
|
1274
|
+
const asNumber = Number(cell);
|
|
1275
|
+
row[h] = cell !== "" && !Number.isNaN(asNumber) ? asNumber : cell;
|
|
1276
|
+
});
|
|
1277
|
+
return row;
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
async function loadRows(path) {
|
|
1281
|
+
const text = await readFile2(path, "utf8");
|
|
1282
|
+
if (path.endsWith(".csv")) return parseCsv(text);
|
|
1283
|
+
const parsed = JSON.parse(text);
|
|
1284
|
+
if (!Array.isArray(parsed)) throw new Error(`${path}: expected a JSON array of row objects`);
|
|
1285
|
+
return parsed;
|
|
1286
|
+
}
|
|
1287
|
+
async function runBatch(scene, rows, opts) {
|
|
1288
|
+
await mkdir3(opts.outDir, { recursive: true });
|
|
1289
|
+
const results = new Array(rows.length);
|
|
1290
|
+
let next = 0;
|
|
1291
|
+
const worker = async () => {
|
|
1292
|
+
for (; ; ) {
|
|
1293
|
+
const index = next++;
|
|
1294
|
+
if (index >= rows.length) return;
|
|
1295
|
+
const row = rows[index];
|
|
1296
|
+
const name = sanitize(String(row._name ?? `row-${index}`));
|
|
1297
|
+
let result;
|
|
1298
|
+
try {
|
|
1299
|
+
const rowOverlay = overlayFromFlat(row, name);
|
|
1300
|
+
const { ir, report } = composeScene(scene, ...opts.baseOverlays, rowOverlay);
|
|
1301
|
+
const framesDir = await mkdtemp2(join5(tmpdir3(), `reframe-batch-${index}-`));
|
|
1302
|
+
const output = join5(opts.outDir, `${name}.mp4`);
|
|
1303
|
+
const plan = opts.noAudio ? null : resolveAudioPlan(compileScene(ir));
|
|
1304
|
+
try {
|
|
1305
|
+
const captured = await captureIr(ir, {
|
|
1306
|
+
framesDir,
|
|
1307
|
+
...opts.fps !== void 0 && { fps: opts.fps }
|
|
1308
|
+
});
|
|
1309
|
+
if (plan) {
|
|
1310
|
+
const videoTmp = `${output}.video.mp4`;
|
|
1311
|
+
await encodeMp4(captured.framesDir, captured.fps, videoTmp);
|
|
1312
|
+
await buildAudioTrack(plan, opts.scenePath ?? output, videoTmp, output);
|
|
1313
|
+
await rm2(videoTmp, { force: true });
|
|
1314
|
+
} else {
|
|
1315
|
+
await encodeMp4(captured.framesDir, captured.fps, output);
|
|
1316
|
+
}
|
|
1317
|
+
} finally {
|
|
1318
|
+
await rm2(framesDir, { recursive: true, force: true });
|
|
1319
|
+
}
|
|
1320
|
+
result = {
|
|
1321
|
+
name,
|
|
1322
|
+
output,
|
|
1323
|
+
applied: report.applied.length,
|
|
1324
|
+
orphans: report.orphans,
|
|
1325
|
+
error: null
|
|
1326
|
+
};
|
|
1327
|
+
} catch (err) {
|
|
1328
|
+
result = {
|
|
1329
|
+
name,
|
|
1330
|
+
output: null,
|
|
1331
|
+
applied: 0,
|
|
1332
|
+
orphans: [],
|
|
1333
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
results[index] = result;
|
|
1337
|
+
opts.onRow?.(result);
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
await Promise.all(Array.from({ length: Math.max(1, opts.concurrency) }, worker));
|
|
1341
|
+
await writeFile4(
|
|
1342
|
+
join5(opts.outDir, "batch-report.json"),
|
|
1343
|
+
JSON.stringify({ rows: results }, null, 2)
|
|
1344
|
+
);
|
|
1345
|
+
return results;
|
|
1346
|
+
}
|
|
1347
|
+
var sanitize;
|
|
1348
|
+
var init_batch = __esm({
|
|
1349
|
+
"../render-cli/src/batch.ts"() {
|
|
1350
|
+
"use strict";
|
|
1351
|
+
init_src();
|
|
1352
|
+
init_audio2();
|
|
1353
|
+
init_encode();
|
|
1354
|
+
init_frameLoop();
|
|
1355
|
+
sanitize = (s) => s.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 80);
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
// ../render-cli/src/loadScene.ts
|
|
1360
|
+
var loadScene_exports = {};
|
|
1361
|
+
__export(loadScene_exports, {
|
|
1362
|
+
loadScene: () => loadScene
|
|
1363
|
+
});
|
|
1364
|
+
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";
|
|
1367
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1368
|
+
async function loadScene(path) {
|
|
1369
|
+
if (path.endsWith(".json")) {
|
|
1370
|
+
const ir = JSON.parse(await readFile3(path, "utf8"));
|
|
1371
|
+
validateScene(ir);
|
|
1372
|
+
return ir;
|
|
1373
|
+
}
|
|
1374
|
+
let code;
|
|
1375
|
+
try {
|
|
1376
|
+
const out = await build2({
|
|
1377
|
+
entryPoints: [path],
|
|
1378
|
+
bundle: true,
|
|
1379
|
+
format: "esm",
|
|
1380
|
+
platform: "neutral",
|
|
1381
|
+
write: false,
|
|
1382
|
+
logLevel: "silent",
|
|
1383
|
+
sourcemap: "inline",
|
|
1384
|
+
// both specifiers accepted: the guide's canonical "@reframe/core" and
|
|
1385
|
+
// the published package name
|
|
1386
|
+
alias: { "@reframe/core": CORE_ENTRY, "reframe-video": CORE_ENTRY }
|
|
1387
|
+
});
|
|
1388
|
+
code = out.outputFiles[0].text;
|
|
1389
|
+
} catch (err) {
|
|
1390
|
+
throw new Error(
|
|
1391
|
+
`failed to bundle ${path}:
|
|
1392
|
+
${err instanceof Error ? err.message : String(err)}`
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
const mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
|
|
1396
|
+
if (!mod.default) throw new Error(`${path} must default-export a scene`);
|
|
1397
|
+
return mod.default;
|
|
1398
|
+
}
|
|
1399
|
+
var HERE, CORE_ENTRY;
|
|
1400
|
+
var init_loadScene = __esm({
|
|
1401
|
+
"../render-cli/src/loadScene.ts"() {
|
|
1402
|
+
"use strict";
|
|
1403
|
+
init_src();
|
|
1404
|
+
HERE = dirname5(fileURLToPath4(import.meta.url));
|
|
1405
|
+
CORE_ENTRY = true ? resolve2(HERE, "index.js") : resolve2(HERE, "..", "..", "core", "src", "index.ts");
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
// ../render-cli/src/reframe.ts
|
|
1410
|
+
import { spawn as spawn3, spawnSync } from "node:child_process";
|
|
1411
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
1412
|
+
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";
|
|
1415
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
1416
|
+
var PACKAGED = true;
|
|
1417
|
+
var HERE2 = dirname6(fileURLToPath5(import.meta.url));
|
|
1418
|
+
var ROOT2 = PACKAGED ? resolve3(HERE2, "..") : resolve3(HERE2, "..", "..", "..");
|
|
1419
|
+
var USER_CWD = process.env.INIT_CWD ?? process.cwd();
|
|
1420
|
+
var RENDER_CLI = PACKAGED ? join6(ROOT2, "dist", "cli.js") : join6(ROOT2, "packages", "render-cli", "src", "cli.ts");
|
|
1421
|
+
var ANALYZE = PACKAGED ? join6(ROOT2, "dist", "analyze.js") : join6(ROOT2, "benchmark", "harness", "motion", "analyze.ts");
|
|
1422
|
+
var CMD = PACKAGED ? "reframe" : "pnpm reframe";
|
|
1423
|
+
var USAGE = `reframe \u2014 declarative motion graphics
|
|
1424
|
+
|
|
1425
|
+
usage:
|
|
1426
|
+
${CMD} render <scene.ts|.json|.html> [--overlay edits.json]... [-o out.mp4] [--fps N] [--duration S] [--no-audio]
|
|
1427
|
+
${CMD} batch <scene.ts> <data.json|csv> [-o outDir] [--overlay base.json]... [--concurrency N] [--fps N]
|
|
1428
|
+
${CMD} preview open the scrub/edit UI (lists scenes in your directory)
|
|
1429
|
+
${CMD} new <scene-name> scaffold <scene-name>.ts in your directory
|
|
1430
|
+
${CMD} motion <mp4|framesDir> motion-profile a rendered clip
|
|
1431
|
+
${CMD} guide [--regen] print the scene-authoring guide (for you or your AI)
|
|
1432
|
+
${CMD} demo run the edit-survival demo (3 mp4s into out/)
|
|
1433
|
+
`;
|
|
1434
|
+
var userPath = (p) => isAbsolute2(p) ? p : resolve3(USER_CWD, p);
|
|
1435
|
+
function fail(message) {
|
|
1436
|
+
console.error(`error: ${message}`);
|
|
1437
|
+
process.exit(2);
|
|
1438
|
+
}
|
|
1439
|
+
function preflightFfmpeg() {
|
|
1440
|
+
if (spawnSync("ffmpeg", ["-version"], { stdio: "ignore" }).error) {
|
|
1441
|
+
fail("ffmpeg not found on PATH \u2014 install it first (macOS: brew install ffmpeg, debian: apt install ffmpeg)");
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
function run(cmd, args, opts = {}) {
|
|
1445
|
+
return new Promise((res) => {
|
|
1446
|
+
const proc = spawn3(cmd, args, {
|
|
1447
|
+
cwd: opts.cwd ?? (PACKAGED ? USER_CWD : ROOT2),
|
|
1448
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
1449
|
+
...opts.env && { env: { ...process.env, ...opts.env } }
|
|
1450
|
+
});
|
|
1451
|
+
let sawBrowserError = false;
|
|
1452
|
+
proc.stderr.on("data", (d) => {
|
|
1453
|
+
const text = d.toString();
|
|
1454
|
+
if (/Executable doesn't exist|browserType\.launch/.test(text)) sawBrowserError = true;
|
|
1455
|
+
process.stderr.write(text);
|
|
1456
|
+
});
|
|
1457
|
+
proc.on("close", (code) => {
|
|
1458
|
+
if (code !== 0 && sawBrowserError) {
|
|
1459
|
+
console.error(
|
|
1460
|
+
`
|
|
1461
|
+
hint: the Playwright browser is not installed yet \u2014 run: ${PACKAGED ? "npx playwright install chromium" : "pnpm exec playwright install chromium"}`
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
res(code ?? 1);
|
|
1465
|
+
});
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
var SCENE_TEMPLATE = (name, id) => `import { scene, group, rect, text, seq, to, wait } from "@reframe/core";
|
|
1469
|
+
|
|
1470
|
+
// Scenes are pure functions of time: no Math.random()/Date \u2014 randomness via
|
|
1471
|
+
// wiggle(seed). Full syntax: ${CMD} guide
|
|
1472
|
+
export default scene({
|
|
1473
|
+
id: "${name}",
|
|
1474
|
+
size: { width: 1920, height: 1080 },
|
|
1475
|
+
fps: 30,
|
|
1476
|
+
background: "#101014",
|
|
1477
|
+
nodes: [
|
|
1478
|
+
// Base props describe the FINISHED design; states override sparsely.
|
|
1479
|
+
group({ id: "${id}", x: 960, y: 540 }, [
|
|
1480
|
+
rect({
|
|
1481
|
+
id: "${id}-card",
|
|
1482
|
+
x: 0,
|
|
1483
|
+
y: 0,
|
|
1484
|
+
width: 560,
|
|
1485
|
+
height: 200,
|
|
1486
|
+
anchor: "center",
|
|
1487
|
+
fill: "#1E2A3A",
|
|
1488
|
+
radius: 24,
|
|
1489
|
+
}),
|
|
1490
|
+
text({
|
|
1491
|
+
id: "${id}-title",
|
|
1492
|
+
x: 0,
|
|
1493
|
+
y: 0,
|
|
1494
|
+
anchor: "center",
|
|
1495
|
+
content: "${name}",
|
|
1496
|
+
fontFamily: "Inter", // bundled weights: 400 / 700 / 800
|
|
1497
|
+
fontSize: 64,
|
|
1498
|
+
fontWeight: 800,
|
|
1499
|
+
fill: "#FFFFFF",
|
|
1500
|
+
}),
|
|
1501
|
+
]),
|
|
1502
|
+
],
|
|
1503
|
+
|
|
1504
|
+
states: {
|
|
1505
|
+
hidden: {
|
|
1506
|
+
"${id}-card": { opacity: 0, scale: 0.9 },
|
|
1507
|
+
"${id}-title": { opacity: 0, y: 24 },
|
|
1508
|
+
},
|
|
1509
|
+
shown: {
|
|
1510
|
+
"${id}-card": { opacity: 1, scale: 1 },
|
|
1511
|
+
"${id}-title": { opacity: 1, y: 0 },
|
|
1512
|
+
},
|
|
1513
|
+
},
|
|
1514
|
+
initial: "hidden",
|
|
1515
|
+
|
|
1516
|
+
// Labels are stable addresses: overlays (and the preview editor) can patch
|
|
1517
|
+
// duration/ease on labeled steps, and the edits survive regeneration.
|
|
1518
|
+
timeline: seq(
|
|
1519
|
+
to("shown", { duration: 0.6, ease: "easeOutCubic", stagger: 0.08, label: "enter" }),
|
|
1520
|
+
wait(2.0, "hold"),
|
|
1521
|
+
to("hidden", { duration: 0.4, ease: "easeInCubic", label: "exit" }),
|
|
1522
|
+
),
|
|
1523
|
+
|
|
1524
|
+
// behaviors: [oscillate("${id}", "y", { amplitude: 6, frequency: 0.4 }, { from: 0.8, until: 2.4 })],
|
|
1525
|
+
});
|
|
1526
|
+
`;
|
|
1527
|
+
async function main() {
|
|
1528
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
1529
|
+
switch (command) {
|
|
1530
|
+
case "render": {
|
|
1531
|
+
const input = rest[0];
|
|
1532
|
+
if (!input || input.startsWith("-")) fail(`render needs an input file
|
|
1533
|
+
|
|
1534
|
+
${USAGE}`);
|
|
1535
|
+
const inputPath = userPath(input);
|
|
1536
|
+
if (!existsSync2(inputPath)) fail(`no such file: ${inputPath}`);
|
|
1537
|
+
const mode = /\.(ts|json)$/.test(input) ? "ir" : /\.html$/.test(input) ? "html" : null;
|
|
1538
|
+
if (!mode) {
|
|
1539
|
+
fail(`cannot infer render mode from "${input}" \u2014 expected .ts/.json (reframe scene) or .html (GSAP page)`);
|
|
1540
|
+
}
|
|
1541
|
+
const args = rest.slice(1);
|
|
1542
|
+
if (mode === "html" && !args.includes("--duration")) {
|
|
1543
|
+
fail("html render requires --duration <seconds> (the page does not declare its own length)");
|
|
1544
|
+
}
|
|
1545
|
+
preflightFfmpeg();
|
|
1546
|
+
const outBase = PACKAGED ? join6(USER_CWD, "out") : join6(ROOT2, "out");
|
|
1547
|
+
let outArgs = args;
|
|
1548
|
+
if (!args.includes("-o")) {
|
|
1549
|
+
await mkdir4(outBase, { recursive: true });
|
|
1550
|
+
outArgs = [...args, "-o", join6(outBase, `${basename(input).replace(/\.[^.]+$/, "")}.mp4`)];
|
|
1551
|
+
}
|
|
1552
|
+
outArgs = outArgs.map(
|
|
1553
|
+
(a, i) => outArgs[i - 1] === "--overlay" || outArgs[i - 1] === "-o" ? userPath(a) : a
|
|
1554
|
+
);
|
|
1555
|
+
process.exit(
|
|
1556
|
+
await (PACKAGED ? run(process.execPath, [RENDER_CLI, mode, inputPath, ...outArgs]) : run("npx", ["tsx", RENDER_CLI, mode, inputPath, ...outArgs]))
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
case "batch": {
|
|
1560
|
+
const [sceneArg, dataArg, ...flags] = rest;
|
|
1561
|
+
if (!sceneArg || !dataArg) fail(`usage: ${CMD} batch <scene.ts> <data.json|csv> [...]`);
|
|
1562
|
+
const scenePath = userPath(sceneArg);
|
|
1563
|
+
const dataPath = userPath(dataArg);
|
|
1564
|
+
for (const p of [scenePath, dataPath]) if (!existsSync2(p)) fail(`no such file: ${p}`);
|
|
1565
|
+
preflightFfmpeg();
|
|
1566
|
+
let outDir = PACKAGED ? join6(USER_CWD, "out", "batch") : join6(ROOT2, "out", "batch");
|
|
1567
|
+
let concurrency = 3;
|
|
1568
|
+
let fps;
|
|
1569
|
+
const baseOverlayPaths = [];
|
|
1570
|
+
for (let i = 0; i < flags.length; i++) {
|
|
1571
|
+
if (flags[i] === "-o") outDir = userPath(flags[++i]);
|
|
1572
|
+
else if (flags[i] === "--overlay") baseOverlayPaths.push(userPath(flags[++i]));
|
|
1573
|
+
else if (flags[i] === "--concurrency") concurrency = Number(flags[++i]);
|
|
1574
|
+
else if (flags[i] === "--fps") fps = Number(flags[++i]);
|
|
1575
|
+
else fail(`unknown flag ${flags[i]}`);
|
|
1576
|
+
}
|
|
1577
|
+
const { loadRows: loadRows2, runBatch: runBatch2 } = await Promise.resolve().then(() => (init_batch(), batch_exports));
|
|
1578
|
+
const { loadScene: loadScene2 } = await Promise.resolve().then(() => (init_loadScene(), loadScene_exports));
|
|
1579
|
+
const { readFile: readFile4 } = await import("node:fs/promises");
|
|
1580
|
+
const scene = await loadScene2(scenePath);
|
|
1581
|
+
const baseOverlays = await Promise.all(
|
|
1582
|
+
baseOverlayPaths.map(async (p) => JSON.parse(await readFile4(p, "utf8")))
|
|
1583
|
+
);
|
|
1584
|
+
const rows = await loadRows2(dataPath);
|
|
1585
|
+
if (rows.length === 0) fail(`${dataPath}: no data rows`);
|
|
1586
|
+
console.log(`batch: ${rows.length} rows \xD7 ${concurrency} workers \u2192 ${outDir}`);
|
|
1587
|
+
const results = await runBatch2(scene, rows, {
|
|
1588
|
+
outDir,
|
|
1589
|
+
baseOverlays,
|
|
1590
|
+
concurrency,
|
|
1591
|
+
scenePath,
|
|
1592
|
+
...fps !== void 0 && { fps },
|
|
1593
|
+
onRow: (r) => {
|
|
1594
|
+
if (r.error) console.error(` \u2717 ${r.name}: ${r.error.split("\n")[0]}`);
|
|
1595
|
+
else if (r.orphans.length > 0) {
|
|
1596
|
+
console.warn(` ! ${r.name}: rendered with ${r.orphans.length} orphaned edit(s)`);
|
|
1597
|
+
for (const o of r.orphans) console.warn(` ${o.address}: ${o.reason}`);
|
|
1598
|
+
} else console.log(` \u2713 ${r.name} (${r.applied} edits)`);
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
const failed = results.filter((r) => r.error).length;
|
|
1602
|
+
const orphaned = results.filter((r) => !r.error && r.orphans.length > 0).length;
|
|
1603
|
+
console.log(
|
|
1604
|
+
`
|
|
1605
|
+
${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed \u2014 report: ${join6(outDir, "batch-report.json")}`
|
|
1606
|
+
);
|
|
1607
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
1608
|
+
}
|
|
1609
|
+
case "preview": {
|
|
1610
|
+
if (PACKAGED) {
|
|
1611
|
+
const { createRequire } = await import("node:module");
|
|
1612
|
+
const vitePkg = createRequire(import.meta.url).resolve("vite/package.json");
|
|
1613
|
+
const viteBin = join6(dirname6(vitePkg), "bin", "vite.js");
|
|
1614
|
+
process.exit(
|
|
1615
|
+
await run(process.execPath, [viteBin, join6(ROOT2, "preview")], {
|
|
1616
|
+
env: { REFRAME_SCENE_DIR: USER_CWD }
|
|
1617
|
+
})
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
process.exit(
|
|
1621
|
+
await run("pnpm", ["--filter", "@reframe/preview", "dev"], {
|
|
1622
|
+
env: { REFRAME_SCENE_DIR: USER_CWD }
|
|
1623
|
+
})
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
case "new": {
|
|
1627
|
+
const name = rest[0];
|
|
1628
|
+
if (!name) fail(`usage: ${CMD} new <scene-name>`);
|
|
1629
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
|
|
1630
|
+
fail(`scene name must be kebab-case (a-z, 0-9, -): got "${name}"`);
|
|
1631
|
+
}
|
|
1632
|
+
const inRepo = USER_CWD === ROOT2 || USER_CWD.startsWith(ROOT2 + "/");
|
|
1633
|
+
const targetDir = inRepo ? join6(ROOT2, "examples", "scenes") : USER_CWD;
|
|
1634
|
+
const target = join6(targetDir, `${name}.ts`);
|
|
1635
|
+
const shown = inRepo ? `examples/scenes/${name}.ts` : `${name}.ts`;
|
|
1636
|
+
if (existsSync2(target)) fail(`${shown} already exists`);
|
|
1637
|
+
const id = name.split("-")[0] ?? name;
|
|
1638
|
+
await writeFile5(target, SCENE_TEMPLATE(name, id));
|
|
1639
|
+
console.log(`created ${shown}
|
|
1640
|
+
preview: ${CMD} preview (pick "${name}" in the dropdown)
|
|
1641
|
+
render: ${CMD} render ${shown}
|
|
1642
|
+
syntax: ${CMD} guide`);
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
case "motion": {
|
|
1646
|
+
const input = rest[0];
|
|
1647
|
+
if (!input) fail(`usage: ${CMD} motion <mp4|framesDir> [...args]`);
|
|
1648
|
+
preflightFfmpeg();
|
|
1649
|
+
process.exit(
|
|
1650
|
+
await (PACKAGED ? run(process.execPath, [ANALYZE, userPath(input), ...rest.slice(1)]) : run("npx", ["tsx", ANALYZE, userPath(input), ...rest.slice(1)]))
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
case "guide": {
|
|
1654
|
+
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"));
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
case "demo":
|
|
1660
|
+
if (PACKAGED) {
|
|
1661
|
+
fail(
|
|
1662
|
+
"the edit-survival demo ships with the repo, not the package \u2014 git clone https://github.com/kiyeonjeon21/reframe && pnpm install && pnpm reframe demo"
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
preflightFfmpeg();
|
|
1666
|
+
process.exit(
|
|
1667
|
+
await run("npx", ["tsx", join6(ROOT2, "examples", "scripts", "demo-edit-survival.ts")])
|
|
1668
|
+
);
|
|
1669
|
+
default:
|
|
1670
|
+
console.log(USAGE);
|
|
1671
|
+
process.exit(command === void 0 || command === "help" ? 0 : 2);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
main().catch((err) => {
|
|
1675
|
+
console.error(err instanceof Error ? err.message : err);
|
|
1676
|
+
process.exit(1);
|
|
1677
|
+
});
|