reframe-video 0.6.17 → 0.6.19
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/README.md +45 -0
- package/dist/bin.js +120 -34
- package/dist/browserEntry.js +1 -1
- package/dist/cli.js +98 -28
- package/dist/compile-api.d.ts +6 -0
- package/dist/compile-api.js +477 -0
- package/dist/compile.js +477 -0
- package/dist/diff.js +77 -26
- package/dist/index.js +17 -2
- package/dist/labels.js +81 -32
- package/dist/renderer-canvas.d.ts +1 -0
- package/dist/renderer-canvas.js +1 -1
- package/dist/trace-cli.js +1 -1
- package/dist/types/audio.d.ts +10 -0
- package/dist/types/ir.d.ts +10 -0
- package/dist/types-renderer/index.d.ts +34 -0
- package/guides/edsl-guide.md +7 -3
- package/package.json +11 -2
package/dist/compile.js
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
// ../render-cli/src/compile.ts
|
|
4
|
+
import { writeFile } from "node:fs/promises";
|
|
5
|
+
|
|
6
|
+
// ../render-cli/src/loadScene.ts
|
|
7
|
+
import { build } from "esbuild";
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { dirname, resolve } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
// ../core/src/validate.ts
|
|
13
|
+
var FX_PROPS = ["blur", "shadowColor", "shadowBlur", "shadowX", "shadowY", "blend"];
|
|
14
|
+
var BLEND_MODES = /* @__PURE__ */ new Set([
|
|
15
|
+
"normal",
|
|
16
|
+
"multiply",
|
|
17
|
+
"screen",
|
|
18
|
+
"overlay",
|
|
19
|
+
"lighten",
|
|
20
|
+
"darken",
|
|
21
|
+
"add",
|
|
22
|
+
"color-dodge",
|
|
23
|
+
"soft-light",
|
|
24
|
+
"hard-light",
|
|
25
|
+
"difference"
|
|
26
|
+
]);
|
|
27
|
+
var IMAGE_FITS = /* @__PURE__ */ new Set(["fill", "cover"]);
|
|
28
|
+
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "z", "rotateX", "rotateY", "anchor", "fixed", ...FX_PROPS];
|
|
29
|
+
var CAMERA_PROPS = ["x", "y", "zoom", "rotation", "perspective", "focus", "aperture"];
|
|
30
|
+
var PROPS_BY_TYPE = {
|
|
31
|
+
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
32
|
+
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
33
|
+
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
34
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
35
|
+
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
36
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume", "fadeIn", "pan"],
|
|
37
|
+
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
38
|
+
group: COMMON_PROPS
|
|
39
|
+
};
|
|
40
|
+
var SceneValidationError = class extends Error {
|
|
41
|
+
constructor(problems) {
|
|
42
|
+
super(`Scene validation failed:
|
|
43
|
+
${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
44
|
+
this.problems = problems;
|
|
45
|
+
this.name = "SceneValidationError";
|
|
46
|
+
}
|
|
47
|
+
problems;
|
|
48
|
+
};
|
|
49
|
+
function validateScene(ir) {
|
|
50
|
+
const problems = [];
|
|
51
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
52
|
+
const checkPaint = (where, value) => {
|
|
53
|
+
if (typeof value !== "object" || value === null) return;
|
|
54
|
+
const g = value;
|
|
55
|
+
if (g.kind !== "linear" && g.kind !== "radial" && g.kind !== "conic") {
|
|
56
|
+
problems.push(`${where}: a paint object must be a gradient with kind "linear" / "radial" / "conic"`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!Array.isArray(g.stops) || g.stops.length === 0) {
|
|
60
|
+
problems.push(`${where}: gradient "${g.kind}" needs at least one color stop`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
g.stops.forEach((s, i) => {
|
|
64
|
+
const st = s;
|
|
65
|
+
if (typeof st?.color !== "string") problems.push(`${where}: gradient stop ${i} needs a color string`);
|
|
66
|
+
if (typeof st?.offset !== "number" || st.offset < 0 || st.offset > 1) {
|
|
67
|
+
problems.push(`${where}: gradient stop ${i} "offset" must be a number in 0..1`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
const collect = (nodes) => {
|
|
72
|
+
for (const node of nodes) {
|
|
73
|
+
if (nodeById.has(node.id)) {
|
|
74
|
+
problems.push(`duplicate node id "${node.id}" \u2014 every node id must be unique`);
|
|
75
|
+
}
|
|
76
|
+
nodeById.set(node.id, node);
|
|
77
|
+
const props = node.props;
|
|
78
|
+
checkPaint(`node "${node.id}" fill`, props.fill);
|
|
79
|
+
checkPaint(`node "${node.id}" stroke`, props.stroke);
|
|
80
|
+
if (typeof props.blur === "number" && props.blur < 0) problems.push(`node "${node.id}": blur must be >= 0`);
|
|
81
|
+
if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
|
|
82
|
+
if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
|
|
83
|
+
if (typeof props.fit === "string" && !IMAGE_FITS.has(props.fit)) problems.push(`node "${node.id}": unknown fit "${props.fit}" \u2014 use ${[...IMAGE_FITS].join(", ")}`);
|
|
84
|
+
if (node.type === "group") {
|
|
85
|
+
const clip = node.props.clip;
|
|
86
|
+
if (clip) {
|
|
87
|
+
if (clip.kind !== "rect" && clip.kind !== "ellipse") {
|
|
88
|
+
problems.push(`group "${node.id}" clip: unknown kind "${clip.kind}" \u2014 use "rect" or "ellipse"`);
|
|
89
|
+
}
|
|
90
|
+
if (!(clip.width > 0) || !(clip.height > 0)) {
|
|
91
|
+
problems.push(`group "${node.id}" clip: width and height must be > 0`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const matte = node.props.matte;
|
|
95
|
+
if (matte !== void 0) {
|
|
96
|
+
if (matte !== "alpha" && matte !== "luma") {
|
|
97
|
+
problems.push(`group "${node.id}" matte: unknown mode "${String(matte)}" \u2014 use "alpha" or "luma"`);
|
|
98
|
+
} else if (node.children.length < 2) {
|
|
99
|
+
problems.push(`group "${node.id}" matte: needs \u22652 children (first masks the rest)`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
collect(node.children);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
collect(ir.nodes);
|
|
107
|
+
const checkProps = (where, nodeId, props) => {
|
|
108
|
+
if (nodeId === "camera" && !nodeById.has("camera")) {
|
|
109
|
+
for (const key of Object.keys(props)) {
|
|
110
|
+
if (!CAMERA_PROPS.includes(key)) {
|
|
111
|
+
problems.push(`${where}: "${key}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const node = nodeById.get(nodeId);
|
|
117
|
+
if (!node) {
|
|
118
|
+
problems.push(
|
|
119
|
+
`${where} targets unknown node "${nodeId}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const allowed = PROPS_BY_TYPE[node.type];
|
|
124
|
+
for (const key of Object.keys(props)) {
|
|
125
|
+
if (!allowed.includes(key)) {
|
|
126
|
+
problems.push(
|
|
127
|
+
`${where}: "${key}" is not a prop of ${node.type} "${nodeId}" \u2014 valid props: ${allowed.join(", ")}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const states = ir.states ?? {};
|
|
133
|
+
for (const [stateName, overrides] of Object.entries(states)) {
|
|
134
|
+
for (const [nodeId, props] of Object.entries(overrides)) {
|
|
135
|
+
checkProps(`state "${stateName}"`, nodeId, props);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (ir.initial !== void 0 && !(ir.initial in states)) {
|
|
139
|
+
problems.push(
|
|
140
|
+
`initial state "${ir.initial}" is not defined \u2014 defined states: ${Object.keys(states).join(", ") || "(none)"}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const labels = /* @__PURE__ */ new Set();
|
|
144
|
+
const checkTimeline = (tl, path2) => {
|
|
145
|
+
if ("label" in tl && tl.label !== void 0) {
|
|
146
|
+
if (labels.has(tl.label)) {
|
|
147
|
+
problems.push(
|
|
148
|
+
`${path2}: duplicate timeline label "${tl.label}" \u2014 labels are overlay addresses and must be unique`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
labels.add(tl.label);
|
|
152
|
+
}
|
|
153
|
+
switch (tl.kind) {
|
|
154
|
+
case "seq":
|
|
155
|
+
case "par":
|
|
156
|
+
tl.children.forEach((c, i) => checkTimeline(c, `${path2}.${tl.kind}[${i}]`));
|
|
157
|
+
break;
|
|
158
|
+
case "stagger":
|
|
159
|
+
if (tl.interval < 0) problems.push(`${path2}: stagger interval must be >= 0`);
|
|
160
|
+
tl.children.forEach((c, i) => checkTimeline(c, `${path2}.stagger[${i}]`));
|
|
161
|
+
break;
|
|
162
|
+
case "to":
|
|
163
|
+
if (!(tl.state in states)) {
|
|
164
|
+
problems.push(
|
|
165
|
+
`${path2}: to("${tl.state}") references an undefined state \u2014 defined states: ${Object.keys(states).join(", ") || "(none)"}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (tl.duration !== void 0 && tl.duration <= 0) {
|
|
169
|
+
problems.push(`${path2}: to("${tl.state}") duration must be > 0`);
|
|
170
|
+
}
|
|
171
|
+
for (const id of tl.filter ?? []) {
|
|
172
|
+
if (!nodeById.has(id)) problems.push(`${path2}: filter contains unknown node "${id}"`);
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
case "tween":
|
|
176
|
+
checkProps(path2, tl.target, tl.props);
|
|
177
|
+
if (tl.duration !== void 0 && tl.duration <= 0) {
|
|
178
|
+
problems.push(`${path2}: tween duration must be > 0`);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
case "motionPath": {
|
|
182
|
+
const node = nodeById.get(tl.target);
|
|
183
|
+
const isSceneCamera = tl.target === "camera" && !node;
|
|
184
|
+
if (!isSceneCamera) {
|
|
185
|
+
if (!node) {
|
|
186
|
+
problems.push(
|
|
187
|
+
`${path2}: motionPath targets unknown node "${tl.target}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
|
|
188
|
+
);
|
|
189
|
+
} else if (node.type === "line") {
|
|
190
|
+
problems.push(`${path2}: motionPath cannot target a line (no x/y) \u2014 "${tl.target}"`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (tl.points.length < 1) problems.push(`${path2}: motionPath "${tl.target}" needs at least 1 point`);
|
|
194
|
+
if (tl.duration !== void 0 && tl.duration <= 0) {
|
|
195
|
+
problems.push(`${path2}: motionPath "${tl.target}" duration must be > 0`);
|
|
196
|
+
}
|
|
197
|
+
if (tl.curviness !== void 0 && tl.curviness < 0) {
|
|
198
|
+
problems.push(`${path2}: motionPath "${tl.target}" curviness must be >= 0`);
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
case "wait":
|
|
203
|
+
if (tl.duration < 0) problems.push(`${path2}: wait duration must be >= 0`);
|
|
204
|
+
break;
|
|
205
|
+
case "beat":
|
|
206
|
+
if (labels.has(tl.name)) {
|
|
207
|
+
problems.push(
|
|
208
|
+
`${path2}: duplicate timeline label "${tl.name}" (beat name) \u2014 labels are overlay addresses and must be unique`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
labels.add(tl.name);
|
|
212
|
+
if (tl.duration !== void 0 && tl.duration <= 0) {
|
|
213
|
+
problems.push(`${path2}: beat "${tl.name}" duration must be > 0`);
|
|
214
|
+
}
|
|
215
|
+
if (tl.scale !== void 0 && tl.scale <= 0) {
|
|
216
|
+
problems.push(`${path2}: beat "${tl.name}" scale must be > 0`);
|
|
217
|
+
}
|
|
218
|
+
for (const id of tl.nodes ?? []) {
|
|
219
|
+
if (!nodeById.has(id)) {
|
|
220
|
+
problems.push(
|
|
221
|
+
`${path2}: beat "${tl.name}" owns unknown node "${id}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
tl.children.forEach((c, i) => checkTimeline(c, `${path2}.beat(${tl.name})[${i}]`));
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
if (ir.timeline) checkTimeline(ir.timeline, "timeline");
|
|
230
|
+
for (const [i, b] of (ir.behaviors ?? []).entries()) {
|
|
231
|
+
checkProps(`behaviors[${i}]`, b.target, { [b.prop]: 0 });
|
|
232
|
+
}
|
|
233
|
+
if (ir.duration !== void 0 && ir.duration <= 0) {
|
|
234
|
+
problems.push("scene duration must be > 0");
|
|
235
|
+
}
|
|
236
|
+
if (ir.camera) {
|
|
237
|
+
if (nodeById.has("camera")) {
|
|
238
|
+
problems.push(`camera: a node is already named "camera" \u2014 rename that node or drop the scene camera (the id "camera" can't be both)`);
|
|
239
|
+
}
|
|
240
|
+
for (const [key, value] of Object.entries(ir.camera)) {
|
|
241
|
+
if (key === "zSort") {
|
|
242
|
+
if (typeof value !== "boolean") problems.push(`camera.zSort must be a boolean`);
|
|
243
|
+
} else if (!CAMERA_PROPS.includes(key)) {
|
|
244
|
+
problems.push(`camera: "${key}" is not a camera prop \u2014 valid props: ${CAMERA_PROPS.join(", ")}, zSort`);
|
|
245
|
+
} else if (typeof value !== "number") {
|
|
246
|
+
problems.push(`camera.${key} must be a number`);
|
|
247
|
+
} else if (key === "perspective" && value <= 0) {
|
|
248
|
+
problems.push(`camera.perspective must be > 0 (focal distance in px) \u2014 drop it to disable perspective`);
|
|
249
|
+
} else if (key === "aperture" && value < 0) {
|
|
250
|
+
problems.push(`camera.aperture must be >= 0 (blur px per unit depth) \u2014 0 disables depth of field`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
|
|
255
|
+
for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
|
|
256
|
+
if (typeof cue.at === "string" && !labels.has(cue.at)) {
|
|
257
|
+
problems.push(
|
|
258
|
+
`audio.cues[${i}]: unknown timeline label "${cue.at}" \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (typeof cue.at === "number" && cue.at < 0) {
|
|
262
|
+
problems.push(`audio.cues[${i}]: "at" must be >= 0`);
|
|
263
|
+
}
|
|
264
|
+
if (cue.sfx === void 0 === (cue.file === void 0)) {
|
|
265
|
+
problems.push(`audio.cues[${i}]: exactly one of "sfx" or "file" is required`);
|
|
266
|
+
}
|
|
267
|
+
if (cue.sfx !== void 0 && !SFX_NAMES.includes(cue.sfx)) {
|
|
268
|
+
problems.push(`audio.cues[${i}]: unknown sfx "${cue.sfx}" \u2014 valid: ${SFX_NAMES.join(", ")}`);
|
|
269
|
+
}
|
|
270
|
+
if (cue.gain !== void 0 && cue.gain < 0) {
|
|
271
|
+
problems.push(`audio.cues[${i}]: gain must be >= 0`);
|
|
272
|
+
}
|
|
273
|
+
if (cue.fadeIn !== void 0 && cue.fadeIn < 0) {
|
|
274
|
+
problems.push(`audio.cues[${i}]: fadeIn must be >= 0`);
|
|
275
|
+
}
|
|
276
|
+
if (cue.fadeOut !== void 0 && cue.fadeOut < 0) {
|
|
277
|
+
problems.push(`audio.cues[${i}]: fadeOut must be >= 0`);
|
|
278
|
+
}
|
|
279
|
+
if (cue.pan !== void 0 && (cue.pan < -1 || cue.pan > 1)) {
|
|
280
|
+
problems.push(`audio.cues[${i}]: pan must be in [-1, 1] (-1 left \u2026 +1 right)`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const duck = ir.audio?.bgm?.duck;
|
|
284
|
+
if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
|
|
285
|
+
problems.push("audio.bgm.duck.depth must be in [0, 1]");
|
|
286
|
+
}
|
|
287
|
+
if (ir.audio?.bgm?.file !== void 0 && ir.audio.bgm.synth !== void 0) {
|
|
288
|
+
problems.push('audio.bgm: use either "file" or "synth", not both');
|
|
289
|
+
}
|
|
290
|
+
if (problems.length > 0) throw new SceneValidationError(problems);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ../core/src/presets.ts
|
|
294
|
+
var SET = 1 / 120;
|
|
295
|
+
|
|
296
|
+
// ../core/src/interpolate.ts
|
|
297
|
+
var BACK_C1 = 1.70158;
|
|
298
|
+
var BACK_C2 = BACK_C1 * 1.525;
|
|
299
|
+
var BACK_C3 = BACK_C1 + 1;
|
|
300
|
+
var ELASTIC_C4 = 2 * Math.PI / 3;
|
|
301
|
+
var ELASTIC_C5 = 2 * Math.PI / 4.5;
|
|
302
|
+
function springEase(stiffness, damping, velocity) {
|
|
303
|
+
const K = 5;
|
|
304
|
+
const zeta = Math.min(0.999, Math.max(0.05, damping / (2 * Math.sqrt(Math.max(1e-6, stiffness)))));
|
|
305
|
+
const wd = K / zeta * Math.sqrt(1 - zeta * zeta);
|
|
306
|
+
const coef = (K - velocity) / wd;
|
|
307
|
+
return (u) => {
|
|
308
|
+
if (u <= 0) return 0;
|
|
309
|
+
if (u >= 1) return 1;
|
|
310
|
+
return 1 - Math.exp(-K * u) * (Math.cos(wd * u) + coef * Math.sin(wd * u));
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function easeOutBounce(u) {
|
|
314
|
+
const n1 = 7.5625;
|
|
315
|
+
const d1 = 2.75;
|
|
316
|
+
if (u < 1 / d1) return n1 * u * u;
|
|
317
|
+
if (u < 2 / d1) return n1 * (u -= 1.5 / d1) * u + 0.75;
|
|
318
|
+
if (u < 2.5 / d1) return n1 * (u -= 2.25 / d1) * u + 0.9375;
|
|
319
|
+
return n1 * (u -= 2.625 / d1) * u + 0.984375;
|
|
320
|
+
}
|
|
321
|
+
var EASE_TABLE = {
|
|
322
|
+
linear: (u) => u,
|
|
323
|
+
easeInQuad: (u) => u * u,
|
|
324
|
+
easeOutQuad: (u) => 1 - (1 - u) * (1 - u),
|
|
325
|
+
easeInOutQuad: (u) => u < 0.5 ? 2 * u * u : 1 - (-2 * u + 2) ** 2 / 2,
|
|
326
|
+
easeInCubic: (u) => u ** 3,
|
|
327
|
+
easeOutCubic: (u) => 1 - (1 - u) ** 3,
|
|
328
|
+
easeInOutCubic: (u) => u < 0.5 ? 4 * u ** 3 : 1 - (-2 * u + 2) ** 3 / 2,
|
|
329
|
+
easeInQuart: (u) => u ** 4,
|
|
330
|
+
easeOutQuart: (u) => 1 - (1 - u) ** 4,
|
|
331
|
+
easeInOutQuart: (u) => u < 0.5 ? 8 * u ** 4 : 1 - (-2 * u + 2) ** 4 / 2,
|
|
332
|
+
easeInExpo: (u) => u === 0 ? 0 : 2 ** (10 * u - 10),
|
|
333
|
+
easeOutExpo: (u) => u === 1 ? 1 : 1 - 2 ** (-10 * u),
|
|
334
|
+
easeInOutExpo: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? 2 ** (20 * u - 10) / 2 : (2 - 2 ** (-20 * u + 10)) / 2,
|
|
335
|
+
// --- expressive eases (GSAP's signature feel) — standard Penner equations ---
|
|
336
|
+
// back: overshoots past the target then settles (pop / snap)
|
|
337
|
+
easeInBack: (u) => BACK_C3 * u ** 3 - BACK_C1 * u * u,
|
|
338
|
+
easeOutBack: (u) => 1 + BACK_C3 * (u - 1) ** 3 + BACK_C1 * (u - 1) ** 2,
|
|
339
|
+
easeInOutBack: (u) => u < 0.5 ? (2 * u) ** 2 * ((BACK_C2 + 1) * 2 * u - BACK_C2) / 2 : ((2 * u - 2) ** 2 * ((BACK_C2 + 1) * (2 * u - 2) + BACK_C2) + 2) / 2,
|
|
340
|
+
// elastic: rings around the target before settling (playful spring)
|
|
341
|
+
easeInElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : -(2 ** (10 * u - 10)) * Math.sin((u * 10 - 10.75) * ELASTIC_C4),
|
|
342
|
+
easeOutElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : 2 ** (-10 * u) * Math.sin((u * 10 - 0.75) * ELASTIC_C4) + 1,
|
|
343
|
+
easeInOutElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? -(2 ** (20 * u - 10) * Math.sin((20 * u - 11.125) * ELASTIC_C5)) / 2 : 2 ** (-20 * u + 10) * Math.sin((20 * u - 11.125) * ELASTIC_C5) / 2 + 1,
|
|
344
|
+
// bounce: drops and bounces to rest (lands without overshoot)
|
|
345
|
+
easeInBounce: (u) => 1 - easeOutBounce(1 - u),
|
|
346
|
+
easeOutBounce,
|
|
347
|
+
easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2,
|
|
348
|
+
// damped-spring presets (ζ from damping/(2√stiffness)): 0.5 / 0.30 / 0.90
|
|
349
|
+
spring: springEase(100, 10, 0),
|
|
350
|
+
springBouncy: springEase(180, 8, 0),
|
|
351
|
+
springStiff: springEase(210, 26, 0)
|
|
352
|
+
};
|
|
353
|
+
var EASE_NAMES = Object.keys(EASE_TABLE);
|
|
354
|
+
|
|
355
|
+
// ../core/src/evaluate.ts
|
|
356
|
+
var DEG = Math.PI / 180;
|
|
357
|
+
|
|
358
|
+
// ../render-cli/src/loadScene.ts
|
|
359
|
+
var HERE = dirname(fileURLToPath(import.meta.url));
|
|
360
|
+
var CORE_ENTRY = true ? resolve(HERE, "index.js") : resolve(HERE, "..", "..", "core", "src", "index.ts");
|
|
361
|
+
var SceneLoadError = class extends Error {
|
|
362
|
+
kind;
|
|
363
|
+
constructor(kind, message, options) {
|
|
364
|
+
super(message, options);
|
|
365
|
+
this.name = "SceneLoadError";
|
|
366
|
+
this.kind = kind;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
var clean = (err) => (err instanceof Error ? err.message : String(err)).replace(
|
|
370
|
+
/data:text\/javascript;base64,[A-Za-z0-9+/=]+/g,
|
|
371
|
+
"<scene bundle>"
|
|
372
|
+
);
|
|
373
|
+
var ALIAS = { "@reframe/core": CORE_ENTRY, "reframe-video": CORE_ENTRY };
|
|
374
|
+
async function bundle(input) {
|
|
375
|
+
const common = {
|
|
376
|
+
bundle: true,
|
|
377
|
+
format: "esm",
|
|
378
|
+
platform: "neutral",
|
|
379
|
+
write: false,
|
|
380
|
+
logLevel: "silent",
|
|
381
|
+
sourcemap: "inline",
|
|
382
|
+
alias: ALIAS
|
|
383
|
+
};
|
|
384
|
+
try {
|
|
385
|
+
const out = await build(
|
|
386
|
+
"path" in input ? { ...common, entryPoints: [input.path] } : { ...common, stdin: { contents: input.code, resolveDir: input.resolveDir, loader: "ts", sourcefile: "scene.ts" } }
|
|
387
|
+
);
|
|
388
|
+
return out.outputFiles[0].text;
|
|
389
|
+
} catch (err) {
|
|
390
|
+
throw new SceneLoadError("bundle", clean(err), { cause: err });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async function importDefault(code, label) {
|
|
394
|
+
let mod;
|
|
395
|
+
try {
|
|
396
|
+
mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
|
|
397
|
+
} catch (err) {
|
|
398
|
+
const kind = err instanceof Error && err.name === "SceneValidationError" ? "validation" : "eval";
|
|
399
|
+
throw new SceneLoadError(kind, clean(err), { cause: err });
|
|
400
|
+
}
|
|
401
|
+
if (mod.default === void 0) throw new SceneLoadError("eval", `${label} must default-export a scene or composition`);
|
|
402
|
+
return mod.default;
|
|
403
|
+
}
|
|
404
|
+
async function loadDefault(path2) {
|
|
405
|
+
if (path2.endsWith(".json")) {
|
|
406
|
+
try {
|
|
407
|
+
return JSON.parse(await readFile(path2, "utf8"));
|
|
408
|
+
} catch (err) {
|
|
409
|
+
throw new SceneLoadError("eval", `failed to read ${path2}: ${clean(err)}`, { cause: err });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return importDefault(await bundle({ path: path2 }), path2);
|
|
413
|
+
}
|
|
414
|
+
function isComposition(def) {
|
|
415
|
+
return typeof def === "object" && def !== null && Array.isArray(def.scenes);
|
|
416
|
+
}
|
|
417
|
+
function asScene(def, label) {
|
|
418
|
+
if (isComposition(def)) {
|
|
419
|
+
throw new SceneLoadError("validation", `${label} is a composition \u2014 render it directly, not as a single scene`);
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
validateScene(def);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
throw new SceneLoadError("validation", clean(err), { cause: err });
|
|
425
|
+
}
|
|
426
|
+
return def;
|
|
427
|
+
}
|
|
428
|
+
async function loadScene(path2) {
|
|
429
|
+
return asScene(await loadDefault(path2), path2);
|
|
430
|
+
}
|
|
431
|
+
async function loadSceneFromCode(code, resolveDir = process.cwd()) {
|
|
432
|
+
return asScene(await importDefault(await bundle({ code, resolveDir }), "<source>"), "<source>");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ../render-cli/src/compile.ts
|
|
436
|
+
var args = process.argv.slice(2);
|
|
437
|
+
var flag = (n) => args.includes(n);
|
|
438
|
+
var opt = (n) => {
|
|
439
|
+
const i = args.indexOf(n);
|
|
440
|
+
return i !== -1 ? args[i + 1] : void 0;
|
|
441
|
+
};
|
|
442
|
+
var jsonMode = flag("--json");
|
|
443
|
+
async function readStdin() {
|
|
444
|
+
const chunks = [];
|
|
445
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
446
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
447
|
+
}
|
|
448
|
+
function withTimeout(p, ms) {
|
|
449
|
+
return Promise.race([
|
|
450
|
+
p,
|
|
451
|
+
new Promise((_, rej) => {
|
|
452
|
+
const t = setTimeout(() => rej(new SceneLoadError("eval", `compile timed out after ${ms}ms`)), ms);
|
|
453
|
+
t.unref();
|
|
454
|
+
})
|
|
455
|
+
]);
|
|
456
|
+
}
|
|
457
|
+
async function main() {
|
|
458
|
+
const out = opt("-o");
|
|
459
|
+
const timeoutMs = Number(opt("--timeout") ?? 8e3);
|
|
460
|
+
const code = opt("--code");
|
|
461
|
+
const input = args.find((a, i) => !a.startsWith("-") && !["-o", "--code", "--timeout"].includes(args[i - 1] ?? ""));
|
|
462
|
+
const load = code !== void 0 ? loadSceneFromCode(code) : flag("--stdin") ? readStdin().then((src) => loadSceneFromCode(src)) : input ? loadScene(input) : Promise.reject(new SceneLoadError("eval", 'compile needs a scene file, --stdin, or --code "<src>"'));
|
|
463
|
+
const ir = await withTimeout(load, timeoutMs);
|
|
464
|
+
const text2 = JSON.stringify(ir);
|
|
465
|
+
if (out) await writeFile(out, `${text2}
|
|
466
|
+
`);
|
|
467
|
+
else process.stdout.write(`${text2}
|
|
468
|
+
`);
|
|
469
|
+
}
|
|
470
|
+
main().catch((err) => {
|
|
471
|
+
const kind = err instanceof SceneLoadError ? err.kind : "eval";
|
|
472
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
473
|
+
if (jsonMode) process.stdout.write(`${JSON.stringify({ ok: false, error: message, kind })}
|
|
474
|
+
`);
|
|
475
|
+
else console.error(`error: ${message}`);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
});
|
package/dist/diff.js
CHANGED
|
@@ -361,7 +361,7 @@ var PROPS_BY_TYPE = {
|
|
|
361
361
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
362
362
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
363
363
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
364
|
-
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
364
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume", "fadeIn", "pan"],
|
|
365
365
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
366
366
|
group: COMMON_PROPS
|
|
367
367
|
};
|
|
@@ -598,6 +598,15 @@ function validateScene(ir) {
|
|
|
598
598
|
if (cue.gain !== void 0 && cue.gain < 0) {
|
|
599
599
|
problems.push(`audio.cues[${i}]: gain must be >= 0`);
|
|
600
600
|
}
|
|
601
|
+
if (cue.fadeIn !== void 0 && cue.fadeIn < 0) {
|
|
602
|
+
problems.push(`audio.cues[${i}]: fadeIn must be >= 0`);
|
|
603
|
+
}
|
|
604
|
+
if (cue.fadeOut !== void 0 && cue.fadeOut < 0) {
|
|
605
|
+
problems.push(`audio.cues[${i}]: fadeOut must be >= 0`);
|
|
606
|
+
}
|
|
607
|
+
if (cue.pan !== void 0 && (cue.pan < -1 || cue.pan > 1)) {
|
|
608
|
+
problems.push(`audio.cues[${i}]: pan must be in [-1, 1] (-1 left \u2026 +1 right)`);
|
|
609
|
+
}
|
|
601
610
|
}
|
|
602
611
|
const duck = ir.audio?.bgm?.duck;
|
|
603
612
|
if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
|
|
@@ -745,42 +754,84 @@ function collectVideoSrcs(ir) {
|
|
|
745
754
|
// ../render-cli/src/loadScene.ts
|
|
746
755
|
var HERE = dirname(fileURLToPath(import.meta.url));
|
|
747
756
|
var CORE_ENTRY = true ? resolve(HERE, "index.js") : resolve(HERE, "..", "..", "core", "src", "index.ts");
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
757
|
+
var SceneLoadError = class extends Error {
|
|
758
|
+
kind;
|
|
759
|
+
constructor(kind, message, options) {
|
|
760
|
+
super(message, options);
|
|
761
|
+
this.name = "SceneLoadError";
|
|
762
|
+
this.kind = kind;
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
var clean = (err) => (err instanceof Error ? err.message : String(err)).replace(
|
|
766
|
+
/data:text\/javascript;base64,[A-Za-z0-9+/=]+/g,
|
|
767
|
+
"<scene bundle>"
|
|
768
|
+
);
|
|
769
|
+
var ALIAS = { "@reframe/core": CORE_ENTRY, "reframe-video": CORE_ENTRY };
|
|
770
|
+
async function bundle(input) {
|
|
771
|
+
const common = {
|
|
772
|
+
bundle: true,
|
|
773
|
+
format: "esm",
|
|
774
|
+
platform: "neutral",
|
|
775
|
+
write: false,
|
|
776
|
+
logLevel: "silent",
|
|
777
|
+
sourcemap: "inline",
|
|
778
|
+
alias: ALIAS
|
|
779
|
+
};
|
|
751
780
|
try {
|
|
752
|
-
const out = await build(
|
|
753
|
-
entryPoints: [
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
});
|
|
764
|
-
code = out.outputFiles[0].text;
|
|
781
|
+
const out = await build(
|
|
782
|
+
"path" in input ? { ...common, entryPoints: [input.path] } : { ...common, stdin: { contents: input.code, resolveDir: input.resolveDir, loader: "ts", sourcefile: "scene.ts" } }
|
|
783
|
+
);
|
|
784
|
+
return out.outputFiles[0].text;
|
|
785
|
+
} catch (err) {
|
|
786
|
+
throw new SceneLoadError("bundle", clean(err), { cause: err });
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
async function importDefault(code, label) {
|
|
790
|
+
let mod;
|
|
791
|
+
try {
|
|
792
|
+
mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
|
|
765
793
|
} catch (err) {
|
|
766
|
-
|
|
767
|
-
|
|
794
|
+
const kind = err instanceof Error && err.name === "SceneValidationError" ? "validation" : "eval";
|
|
795
|
+
throw new SceneLoadError(kind, clean(err), { cause: err });
|
|
768
796
|
}
|
|
769
|
-
|
|
770
|
-
if (mod.default === void 0) throw new Error(`${path2} must default-export a scene or composition`);
|
|
797
|
+
if (mod.default === void 0) throw new SceneLoadError("eval", `${label} must default-export a scene or composition`);
|
|
771
798
|
return mod.default;
|
|
772
799
|
}
|
|
800
|
+
async function loadDefault(path2) {
|
|
801
|
+
if (path2.endsWith(".json")) {
|
|
802
|
+
try {
|
|
803
|
+
return JSON.parse(await readFile(path2, "utf8"));
|
|
804
|
+
} catch (err) {
|
|
805
|
+
throw new SceneLoadError("eval", `failed to read ${path2}: ${clean(err)}`, { cause: err });
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return importDefault(await bundle({ path: path2 }), path2);
|
|
809
|
+
}
|
|
773
810
|
function isComposition(def) {
|
|
774
811
|
return typeof def === "object" && def !== null && Array.isArray(def.scenes);
|
|
775
812
|
}
|
|
813
|
+
function asScene(def, label) {
|
|
814
|
+
if (isComposition(def)) {
|
|
815
|
+
throw new SceneLoadError("validation", `${label} is a composition \u2014 render it directly, not as a single scene`);
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
validateScene(def);
|
|
819
|
+
} catch (err) {
|
|
820
|
+
throw new SceneLoadError("validation", clean(err), { cause: err });
|
|
821
|
+
}
|
|
822
|
+
return def;
|
|
823
|
+
}
|
|
776
824
|
async function loadModule(path2) {
|
|
777
825
|
const def = await loadDefault(path2);
|
|
778
826
|
if (isComposition(def)) {
|
|
779
|
-
|
|
827
|
+
try {
|
|
828
|
+
validateComposition(def);
|
|
829
|
+
} catch (err) {
|
|
830
|
+
throw new SceneLoadError("validation", clean(err), { cause: err });
|
|
831
|
+
}
|
|
780
832
|
return { kind: "composition", ir: def };
|
|
781
833
|
}
|
|
782
|
-
|
|
783
|
-
return { kind: "scene", ir: def };
|
|
834
|
+
return { kind: "scene", ir: asScene(def, path2) };
|
|
784
835
|
}
|
|
785
836
|
|
|
786
837
|
// ../render-cli/src/frameLoop.ts
|
|
@@ -1046,11 +1097,11 @@ async function renderFrameAt(ir, t, opts = {}) {
|
|
|
1046
1097
|
const assets = await buildImageAssets(ir, sceneDir);
|
|
1047
1098
|
const { fps, duration } = resolveTiming(ir, {});
|
|
1048
1099
|
const videoAssets = await buildVideoFrameAssets(ir, sceneDir, fps, duration);
|
|
1049
|
-
const
|
|
1100
|
+
const bundle2 = await browserBundle();
|
|
1050
1101
|
return withPage(ir.size, async (page) => {
|
|
1051
1102
|
await page.setContent(`<!DOCTYPE html><html><body style="margin:0;background:#000"></body></html>`);
|
|
1052
1103
|
await injectFonts(page);
|
|
1053
|
-
await page.addScriptTag({ content:
|
|
1104
|
+
await page.addScriptTag({ content: bundle2 });
|
|
1054
1105
|
await page.evaluate(
|
|
1055
1106
|
([sceneIr, imageAssets, vAssets]) => window.__reframe.init(sceneIr, imageAssets, vAssets),
|
|
1056
1107
|
[ir, assets, videoAssets]
|
package/dist/index.js
CHANGED
|
@@ -365,7 +365,7 @@ var PROPS_BY_TYPE = {
|
|
|
365
365
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
366
366
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
367
367
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
368
|
-
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
368
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume", "fadeIn", "pan"],
|
|
369
369
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
370
370
|
group: COMMON_PROPS
|
|
371
371
|
};
|
|
@@ -602,6 +602,15 @@ function validateScene(ir) {
|
|
|
602
602
|
if (cue.gain !== void 0 && cue.gain < 0) {
|
|
603
603
|
problems.push(`audio.cues[${i}]: gain must be >= 0`);
|
|
604
604
|
}
|
|
605
|
+
if (cue.fadeIn !== void 0 && cue.fadeIn < 0) {
|
|
606
|
+
problems.push(`audio.cues[${i}]: fadeIn must be >= 0`);
|
|
607
|
+
}
|
|
608
|
+
if (cue.fadeOut !== void 0 && cue.fadeOut < 0) {
|
|
609
|
+
problems.push(`audio.cues[${i}]: fadeOut must be >= 0`);
|
|
610
|
+
}
|
|
611
|
+
if (cue.pan !== void 0 && (cue.pan < -1 || cue.pan > 1)) {
|
|
612
|
+
problems.push(`audio.cues[${i}]: pan must be in [-1, 1] (-1 left \u2026 +1 right)`);
|
|
613
|
+
}
|
|
605
614
|
}
|
|
606
615
|
const duck = ir.audio?.bgm?.duck;
|
|
607
616
|
if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
|
|
@@ -2690,7 +2699,7 @@ function collectClipAudio(ir, duration, warnings) {
|
|
|
2690
2699
|
warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
|
|
2691
2700
|
continue;
|
|
2692
2701
|
}
|
|
2693
|
-
out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain });
|
|
2702
|
+
out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain, fadeIn: node.props.fadeIn ?? 0, pan: node.props.pan ?? 0 });
|
|
2694
2703
|
}
|
|
2695
2704
|
if (node.type === "group") walk(node.children);
|
|
2696
2705
|
}
|
|
@@ -2732,6 +2741,9 @@ function resolveAudioPlan(compiled) {
|
|
|
2732
2741
|
t,
|
|
2733
2742
|
gain: cue.gain ?? 1,
|
|
2734
2743
|
duration: cueDuration,
|
|
2744
|
+
fadeIn: cue.fadeIn ?? 0,
|
|
2745
|
+
fadeOut: cue.fadeOut ?? 0,
|
|
2746
|
+
pan: cue.pan ?? 0,
|
|
2735
2747
|
source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
|
|
2736
2748
|
});
|
|
2737
2749
|
}
|
|
@@ -2809,6 +2821,9 @@ function resolveCompositionAudioPlan(comp) {
|
|
|
2809
2821
|
t,
|
|
2810
2822
|
gain: cue.gain ?? 1,
|
|
2811
2823
|
duration: cueDuration,
|
|
2824
|
+
fadeIn: cue.fadeIn ?? 0,
|
|
2825
|
+
fadeOut: cue.fadeOut ?? 0,
|
|
2826
|
+
pan: cue.pan ?? 0,
|
|
2812
2827
|
source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
|
|
2813
2828
|
});
|
|
2814
2829
|
}
|