reframe-video 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +77 -0
  3. package/assets/fonts/inter-400.woff2 +0 -0
  4. package/assets/fonts/inter-700.woff2 +0 -0
  5. package/assets/fonts/inter-800.woff2 +0 -0
  6. package/assets/sfx/LICENSE.md +12 -0
  7. package/assets/sfx/click_002.ogg +0 -0
  8. package/assets/sfx/click_003.ogg +0 -0
  9. package/assets/sfx/click_004.ogg +0 -0
  10. package/assets/sfx/confirmation_001.ogg +0 -0
  11. package/assets/sfx/keypress-001.wav +0 -0
  12. package/assets/sfx/keypress-004.wav +0 -0
  13. package/assets/sfx/keypress-007.wav +0 -0
  14. package/assets/sfx/keypress-010.wav +0 -0
  15. package/assets/sfx/keypress-014.wav +0 -0
  16. package/dist/analyze.js +344 -0
  17. package/dist/bin.js +1677 -0
  18. package/dist/browserEntry.js +532 -0
  19. package/dist/cli.js +1205 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +889 -0
  22. package/dist/renderer-canvas.js +89 -0
  23. package/dist/types/audio.d.ts +53 -0
  24. package/dist/types/behaviors.d.ts +7 -0
  25. package/dist/types/compile.d.ts +38 -0
  26. package/dist/types/compose.d.ts +64 -0
  27. package/dist/types/dsl.d.ts +66 -0
  28. package/dist/types/evaluate.d.ts +59 -0
  29. package/dist/types/index.d.ts +9 -0
  30. package/dist/types/interpolate.d.ts +12 -0
  31. package/dist/types/ir.d.ts +213 -0
  32. package/dist/types/validate.d.ts +12 -0
  33. package/guides/edsl-guide.md +202 -0
  34. package/guides/regen-contract.md +18 -0
  35. package/package.json +55 -0
  36. package/preview/index.html +60 -0
  37. package/preview/src/main.ts +162 -0
  38. package/preview/src/panel.ts +347 -0
  39. package/preview/src/store.ts +220 -0
  40. package/preview/src/virtual.d.ts +4 -0
  41. package/preview/vite.config.ts +52 -0
package/dist/index.js ADDED
@@ -0,0 +1,889 @@
1
+ // ../core/src/ir.ts
2
+ var DEFAULT_TO_DURATION = 0.5;
3
+ var DEFAULT_TWEEN_DURATION = 0.5;
4
+ var DEFAULT_FPS = 30;
5
+
6
+ // ../core/src/compile.ts
7
+ var key = (target, prop) => `${target}.${prop}`;
8
+ function compileScene(ir) {
9
+ const nodeById = /* @__PURE__ */ new Map();
10
+ const nodeOrder = [];
11
+ const collect = (nodes) => {
12
+ for (const node of nodes) {
13
+ nodeById.set(node.id, node);
14
+ nodeOrder.push(node.id);
15
+ if (node.type === "group") collect(node.children);
16
+ }
17
+ };
18
+ collect(ir.nodes);
19
+ const initialValues = /* @__PURE__ */ new Map();
20
+ for (const [id, node] of nodeById) {
21
+ for (const [prop, value] of Object.entries(node.props)) {
22
+ if (typeof value === "number" || typeof value === "string") {
23
+ initialValues.set(key(id, prop), value);
24
+ }
25
+ }
26
+ }
27
+ if (ir.initial !== void 0) {
28
+ const override = ir.states?.[ir.initial] ?? {};
29
+ for (const [id, props] of Object.entries(override)) {
30
+ for (const [prop, value] of Object.entries(props)) {
31
+ initialValues.set(key(id, prop), value);
32
+ }
33
+ }
34
+ }
35
+ const segments = /* @__PURE__ */ new Map();
36
+ const current = new Map(initialValues);
37
+ const pushSegment = (seg) => {
38
+ const k = key(seg.target, seg.prop);
39
+ let list = segments.get(k);
40
+ if (!list) segments.set(k, list = []);
41
+ list.push(seg);
42
+ current.set(k, seg.to);
43
+ };
44
+ const currentValue = (target, prop) => {
45
+ const v = current.get(key(target, prop));
46
+ if (v !== void 0) return v;
47
+ if (prop === "opacity" || prop === "scale" || prop === "progress") return 1;
48
+ if (prop === "rotation") return 0;
49
+ throw new Error(`cannot animate "${prop}" of "${target}": no base value to start from`);
50
+ };
51
+ const labelTimes = /* @__PURE__ */ new Map();
52
+ const walk = (tl, start) => {
53
+ const end = walkInner(tl, start);
54
+ if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
55
+ return end;
56
+ };
57
+ const walkInner = (tl, start) => {
58
+ switch (tl.kind) {
59
+ case "seq": {
60
+ let t = start;
61
+ for (const child of tl.children) t = walk(child, t);
62
+ return t;
63
+ }
64
+ case "par": {
65
+ let end = start;
66
+ for (const child of tl.children) end = Math.max(end, walk(child, start));
67
+ return end;
68
+ }
69
+ case "stagger": {
70
+ let end = start;
71
+ tl.children.forEach((child, i) => {
72
+ end = Math.max(end, walk(child, start + i * tl.interval));
73
+ });
74
+ return end;
75
+ }
76
+ case "wait":
77
+ return start + tl.duration;
78
+ case "tween": {
79
+ const duration = tl.duration ?? DEFAULT_TWEEN_DURATION;
80
+ for (const [prop, toValue] of Object.entries(tl.props)) {
81
+ pushSegment({
82
+ target: tl.target,
83
+ prop,
84
+ t0: start,
85
+ t1: start + duration,
86
+ from: currentValue(tl.target, prop),
87
+ to: toValue,
88
+ ...tl.ease !== void 0 && { ease: tl.ease }
89
+ });
90
+ }
91
+ return start + duration;
92
+ }
93
+ case "to": {
94
+ const override = ir.states?.[tl.state] ?? {};
95
+ const duration = tl.duration ?? DEFAULT_TO_DURATION;
96
+ const staggerInterval = tl.stagger ?? 0;
97
+ const targets = nodeOrder.filter(
98
+ (id) => id in override && (tl.filter === void 0 || tl.filter.includes(id))
99
+ );
100
+ targets.forEach((id, i) => {
101
+ const t0 = start + i * staggerInterval;
102
+ for (const [prop, toValue] of Object.entries(override[id])) {
103
+ pushSegment({
104
+ target: id,
105
+ prop,
106
+ t0,
107
+ t1: t0 + duration,
108
+ from: currentValue(id, prop),
109
+ to: toValue,
110
+ ...tl.ease !== void 0 && { ease: tl.ease }
111
+ });
112
+ }
113
+ });
114
+ const last = Math.max(0, targets.length - 1);
115
+ return start + duration + last * staggerInterval;
116
+ }
117
+ }
118
+ };
119
+ const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
120
+ for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
121
+ return {
122
+ ir,
123
+ duration: ir.duration ?? inferredEnd,
124
+ segments,
125
+ initialValues,
126
+ nodeById,
127
+ nodeOrder,
128
+ labelTimes
129
+ };
130
+ }
131
+
132
+ // ../core/src/validate.ts
133
+ var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "anchor"];
134
+ var PROPS_BY_TYPE = {
135
+ rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
136
+ ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
137
+ line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
138
+ text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
139
+ group: COMMON_PROPS
140
+ };
141
+ var SceneValidationError = class extends Error {
142
+ constructor(problems) {
143
+ super(`Scene validation failed:
144
+ ${problems.map((p) => ` - ${p}`).join("\n")}`);
145
+ this.problems = problems;
146
+ this.name = "SceneValidationError";
147
+ }
148
+ problems;
149
+ };
150
+ function validateScene(ir) {
151
+ const problems = [];
152
+ const nodeById = /* @__PURE__ */ new Map();
153
+ const collect = (nodes) => {
154
+ for (const node of nodes) {
155
+ if (nodeById.has(node.id)) {
156
+ problems.push(`duplicate node id "${node.id}" \u2014 every node id must be unique`);
157
+ }
158
+ nodeById.set(node.id, node);
159
+ if (node.type === "group") collect(node.children);
160
+ }
161
+ };
162
+ collect(ir.nodes);
163
+ const checkProps = (where, nodeId, props) => {
164
+ const node = nodeById.get(nodeId);
165
+ if (!node) {
166
+ problems.push(
167
+ `${where} targets unknown node "${nodeId}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
168
+ );
169
+ return;
170
+ }
171
+ const allowed = PROPS_BY_TYPE[node.type];
172
+ for (const key2 of Object.keys(props)) {
173
+ if (!allowed.includes(key2)) {
174
+ problems.push(
175
+ `${where}: "${key2}" is not a prop of ${node.type} "${nodeId}" \u2014 valid props: ${allowed.join(", ")}`
176
+ );
177
+ }
178
+ }
179
+ };
180
+ const states = ir.states ?? {};
181
+ for (const [stateName, overrides] of Object.entries(states)) {
182
+ for (const [nodeId, props] of Object.entries(overrides)) {
183
+ checkProps(`state "${stateName}"`, nodeId, props);
184
+ }
185
+ }
186
+ if (ir.initial !== void 0 && !(ir.initial in states)) {
187
+ problems.push(
188
+ `initial state "${ir.initial}" is not defined \u2014 defined states: ${Object.keys(states).join(", ") || "(none)"}`
189
+ );
190
+ }
191
+ const labels = /* @__PURE__ */ new Set();
192
+ const checkTimeline = (tl, path) => {
193
+ if ("label" in tl && tl.label !== void 0) {
194
+ if (labels.has(tl.label)) {
195
+ problems.push(
196
+ `${path}: duplicate timeline label "${tl.label}" \u2014 labels are overlay addresses and must be unique`
197
+ );
198
+ }
199
+ labels.add(tl.label);
200
+ }
201
+ switch (tl.kind) {
202
+ case "seq":
203
+ case "par":
204
+ tl.children.forEach((c, i) => checkTimeline(c, `${path}.${tl.kind}[${i}]`));
205
+ break;
206
+ case "stagger":
207
+ if (tl.interval < 0) problems.push(`${path}: stagger interval must be >= 0`);
208
+ tl.children.forEach((c, i) => checkTimeline(c, `${path}.stagger[${i}]`));
209
+ break;
210
+ case "to":
211
+ if (!(tl.state in states)) {
212
+ problems.push(
213
+ `${path}: to("${tl.state}") references an undefined state \u2014 defined states: ${Object.keys(states).join(", ") || "(none)"}`
214
+ );
215
+ }
216
+ if (tl.duration !== void 0 && tl.duration <= 0) {
217
+ problems.push(`${path}: to("${tl.state}") duration must be > 0`);
218
+ }
219
+ for (const id of tl.filter ?? []) {
220
+ if (!nodeById.has(id)) problems.push(`${path}: filter contains unknown node "${id}"`);
221
+ }
222
+ break;
223
+ case "tween":
224
+ checkProps(path, tl.target, tl.props);
225
+ if (tl.duration !== void 0 && tl.duration <= 0) {
226
+ problems.push(`${path}: tween duration must be > 0`);
227
+ }
228
+ break;
229
+ case "wait":
230
+ if (tl.duration < 0) problems.push(`${path}: wait duration must be >= 0`);
231
+ break;
232
+ }
233
+ };
234
+ if (ir.timeline) checkTimeline(ir.timeline, "timeline");
235
+ for (const [i, b] of (ir.behaviors ?? []).entries()) {
236
+ checkProps(`behaviors[${i}]`, b.target, { [b.prop]: 0 });
237
+ }
238
+ if (ir.duration !== void 0 && ir.duration <= 0) {
239
+ problems.push("scene duration must be > 0");
240
+ }
241
+ const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
242
+ for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
243
+ if (typeof cue.at === "string" && !labels.has(cue.at)) {
244
+ problems.push(
245
+ `audio.cues[${i}]: unknown timeline label "${cue.at}" \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
246
+ );
247
+ }
248
+ if (typeof cue.at === "number" && cue.at < 0) {
249
+ problems.push(`audio.cues[${i}]: "at" must be >= 0`);
250
+ }
251
+ if (cue.sfx === void 0 === (cue.file === void 0)) {
252
+ problems.push(`audio.cues[${i}]: exactly one of "sfx" or "file" is required`);
253
+ }
254
+ if (cue.sfx !== void 0 && !SFX_NAMES.includes(cue.sfx)) {
255
+ problems.push(`audio.cues[${i}]: unknown sfx "${cue.sfx}" \u2014 valid: ${SFX_NAMES.join(", ")}`);
256
+ }
257
+ if (cue.gain !== void 0 && cue.gain < 0) {
258
+ problems.push(`audio.cues[${i}]: gain must be >= 0`);
259
+ }
260
+ }
261
+ const duck = ir.audio?.bgm?.duck;
262
+ if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
263
+ problems.push("audio.bgm.duck.depth must be in [0, 1]");
264
+ }
265
+ if (ir.audio?.bgm?.file !== void 0 && ir.audio.bgm.synth !== void 0) {
266
+ problems.push('audio.bgm: use either "file" or "synth", not both');
267
+ }
268
+ if (problems.length > 0) throw new SceneValidationError(problems);
269
+ }
270
+
271
+ // ../core/src/dsl.ts
272
+ function scene(input) {
273
+ const ir = { version: 1, ...input };
274
+ validateScene(ir);
275
+ if (ir.duration === void 0 && ir.timeline) {
276
+ ir.duration = compileScene(ir).duration;
277
+ }
278
+ return ir;
279
+ }
280
+ function rect(props) {
281
+ const { id, ...rest } = props;
282
+ return { type: "rect", id, props: rest };
283
+ }
284
+ function ellipse(props) {
285
+ const { id, ...rest } = props;
286
+ return { type: "ellipse", id, props: rest };
287
+ }
288
+ function line(props) {
289
+ const { id, ...rest } = props;
290
+ return { type: "line", id, props: rest };
291
+ }
292
+ function text(props) {
293
+ const { id, ...rest } = props;
294
+ return { type: "text", id, props: rest };
295
+ }
296
+ function group(props, children) {
297
+ const { id, ...rest } = props;
298
+ return { type: "group", id, props: rest, children };
299
+ }
300
+ function seq(...children) {
301
+ return { kind: "seq", children };
302
+ }
303
+ function par(...children) {
304
+ return { kind: "par", children };
305
+ }
306
+ function stagger(interval, ...children) {
307
+ return { kind: "stagger", interval, children };
308
+ }
309
+ function to(state, opts = {}) {
310
+ return { kind: "to", state, ...opts };
311
+ }
312
+ function tween(target, props, opts = {}) {
313
+ return { kind: "tween", target, props, ...opts };
314
+ }
315
+ function wait(duration, label) {
316
+ return { kind: "wait", duration, ...label !== void 0 && { label } };
317
+ }
318
+ function oscillate(target, prop, params, window = {}) {
319
+ return { target, prop, ...window, behavior: { kind: "named", name: "oscillate", params } };
320
+ }
321
+ function wiggle(target, prop, params, window = {}) {
322
+ return { target, prop, ...window, behavior: { kind: "named", name: "wiggle", params } };
323
+ }
324
+
325
+ // ../core/src/compose.ts
326
+ var SCENE_PATCHABLE = ["background", "duration", "fps"];
327
+ function composeScene(base, ...overlays) {
328
+ const ir = structuredClone(base);
329
+ const report = { applied: [], orphans: [], warnings: [] };
330
+ overlays.forEach((overlay, index) => {
331
+ const layer = overlay.name ?? `overlay-${index}`;
332
+ if (overlay.target !== void 0 && overlay.target !== ir.id) {
333
+ report.warnings.push(
334
+ `${layer}: authored against scene "${overlay.target}" but composing onto "${ir.id}"`
335
+ );
336
+ }
337
+ applyOverlay(ir, overlay, layer, report);
338
+ });
339
+ validateScene(ir);
340
+ return { ir, report };
341
+ }
342
+ function applyOverlay(ir, overlay, layer, report) {
343
+ const nodeById = /* @__PURE__ */ new Map();
344
+ const collect = (nodes) => {
345
+ for (const node of nodes) {
346
+ nodeById.set(node.id, node);
347
+ if (node.type === "group") collect(node.children);
348
+ }
349
+ };
350
+ collect(ir.nodes);
351
+ const knownIds = () => [...nodeById.keys()].join(", ");
352
+ const orphan = (address, reason) => report.orphans.push({ layer, address, reason });
353
+ const applied = (address, action) => report.applied.push({ layer, address, action });
354
+ const patchProps = (address, node, target, patch) => {
355
+ const allowed = PROPS_BY_TYPE[node.type];
356
+ for (const [prop, value] of Object.entries(patch)) {
357
+ if (!allowed.includes(prop)) {
358
+ orphan(
359
+ `${address}.${prop}`,
360
+ `"${prop}" is not a prop of ${node.type} "${node.id}" \u2014 the base may have changed this node's type; valid props: ${allowed.join(", ")}`
361
+ );
362
+ continue;
363
+ }
364
+ if (value === null) {
365
+ delete target[prop];
366
+ applied(`${address}.${prop}`, "unset");
367
+ } else {
368
+ target[prop] = value;
369
+ applied(`${address}.${prop}`, "set");
370
+ }
371
+ }
372
+ };
373
+ if (overlay.scene) {
374
+ for (const key2 of SCENE_PATCHABLE) {
375
+ const value = overlay.scene[key2];
376
+ if (value !== void 0) {
377
+ ir[key2] = value;
378
+ applied(`scene.${key2}`, "set");
379
+ }
380
+ }
381
+ }
382
+ for (const [id, patch] of Object.entries(overlay.nodes ?? {})) {
383
+ const node = nodeById.get(id);
384
+ if (!node) {
385
+ orphan(
386
+ `nodes.${id}`,
387
+ `unknown node "${id}" \u2014 known ids: ${knownIds()}; did the base regeneration rename it?`
388
+ );
389
+ continue;
390
+ }
391
+ patchProps(`nodes.${id}`, node, node.props, patch);
392
+ }
393
+ for (const [stateName, statePatch] of Object.entries(overlay.states ?? {})) {
394
+ const state = ir.states?.[stateName];
395
+ if (!state) {
396
+ orphan(
397
+ `states.${stateName}`,
398
+ `unknown state "${stateName}" \u2014 defined states: ${Object.keys(ir.states ?? {}).join(", ") || "(none)"}`
399
+ );
400
+ continue;
401
+ }
402
+ for (const [id, patch] of Object.entries(statePatch)) {
403
+ const node = nodeById.get(id);
404
+ if (!node) {
405
+ orphan(
406
+ `states.${stateName}.${id}`,
407
+ `unknown node "${id}" \u2014 known ids: ${knownIds()}; did the base regeneration rename it?`
408
+ );
409
+ continue;
410
+ }
411
+ const target = state[id] ??= {};
412
+ patchProps(`states.${stateName}.${id}`, node, target, patch);
413
+ }
414
+ }
415
+ if (overlay.behaviors?.remove || overlay.behaviors?.set) {
416
+ ir.behaviors ??= [];
417
+ for (const { target, prop } of overlay.behaviors.remove ?? []) {
418
+ const index = ir.behaviors.findIndex((b) => b.target === target && b.prop === prop);
419
+ if (index < 0) {
420
+ orphan(
421
+ `behaviors.remove.${target}.${prop}`,
422
+ `no behavior on "${target}.${prop}" to remove`
423
+ );
424
+ continue;
425
+ }
426
+ ir.behaviors.splice(index, 1);
427
+ applied(`behaviors.${target}.${prop}`, "behavior-remove");
428
+ }
429
+ for (const behavior of overlay.behaviors.set ?? []) {
430
+ if (!nodeById.has(behavior.target)) {
431
+ orphan(
432
+ `behaviors.set.${behavior.target}.${behavior.prop}`,
433
+ `unknown node "${behavior.target}" \u2014 known ids: ${knownIds()}`
434
+ );
435
+ continue;
436
+ }
437
+ const index = ir.behaviors.findIndex(
438
+ (b) => b.target === behavior.target && b.prop === behavior.prop
439
+ );
440
+ if (index >= 0) ir.behaviors[index] = structuredClone(behavior);
441
+ else ir.behaviors.push(structuredClone(behavior));
442
+ applied(`behaviors.${behavior.target}.${behavior.prop}`, "behavior-set");
443
+ }
444
+ }
445
+ if (overlay.timeline) {
446
+ const byLabel = /* @__PURE__ */ new Map();
447
+ const walkTimeline = (tl) => {
448
+ if ("label" in tl && tl.label !== void 0) byLabel.set(tl.label, tl);
449
+ if ("children" in tl) tl.children.forEach(walkTimeline);
450
+ };
451
+ if (ir.timeline) walkTimeline(ir.timeline);
452
+ const PATCHABLE = {
453
+ to: ["duration", "ease", "stagger"],
454
+ tween: ["duration", "ease"],
455
+ wait: ["duration"]
456
+ };
457
+ let timingPatched = false;
458
+ for (const [label, patch] of Object.entries(overlay.timeline)) {
459
+ const step = byLabel.get(label);
460
+ if (!step) {
461
+ orphan(
462
+ `timeline.${label}`,
463
+ `unknown timeline label "${label}" \u2014 known labels: ${[...byLabel.keys()].join(", ") || "(none)"}; did the base regeneration drop it?`
464
+ );
465
+ continue;
466
+ }
467
+ const allowed = PATCHABLE[step.kind] ?? [];
468
+ for (const [key2, value] of Object.entries(patch)) {
469
+ if (value === void 0) continue;
470
+ if (!allowed.includes(key2)) {
471
+ orphan(
472
+ `timeline.${label}.${key2}`,
473
+ `"${key2}" is not patchable on a ${step.kind} step \u2014 patchable: ${allowed.join(", ")}`
474
+ );
475
+ continue;
476
+ }
477
+ step[key2] = value;
478
+ applied(`timeline.${label}.${key2}`, "set");
479
+ if (key2 === "duration" || key2 === "stagger") timingPatched = true;
480
+ }
481
+ }
482
+ if (timingPatched && overlay.scene?.duration === void 0) {
483
+ delete ir.duration;
484
+ ir.duration = compileScene(ir).duration;
485
+ }
486
+ }
487
+ for (const node of overlay.addNodes ?? []) {
488
+ ir.nodes.push(structuredClone(node));
489
+ nodeById.set(node.id, node);
490
+ applied(`addNodes.${node.id}`, "add-node");
491
+ }
492
+ }
493
+ function formatComposeReport(report) {
494
+ const lines = [];
495
+ lines.push(
496
+ `compose: ${report.applied.length} applied, ${report.orphans.length} orphaned, ${report.warnings.length} warnings`
497
+ );
498
+ for (const a of report.applied) lines.push(` \u2713 [${a.layer}] ${a.address} (${a.action})`);
499
+ for (const o of report.orphans) lines.push(` \u2717 [${o.layer}] ${o.address}: ${o.reason}`);
500
+ for (const w of report.warnings) lines.push(` ! ${w}`);
501
+ return lines.join("\n");
502
+ }
503
+
504
+ // ../core/src/audio.ts
505
+ var SFX_DURATION = {
506
+ whoosh: 0.35,
507
+ pop: 0.12,
508
+ tick: 0.03,
509
+ rise: 0.5,
510
+ shimmer: 0.9,
511
+ thud: 0.25
512
+ };
513
+ var FILE_CUE_DURATION = 0.4;
514
+ function resolveAudioPlan(compiled) {
515
+ const audio = compiled.ir.audio;
516
+ if (!audio || !audio.bgm && (audio.cues ?? []).length === 0) return null;
517
+ const warnings = [];
518
+ const duration = compiled.duration;
519
+ const cues = [];
520
+ for (const [index, cue] of (audio.cues ?? []).entries()) {
521
+ let anchor;
522
+ if (typeof cue.at === "number") {
523
+ anchor = cue.at;
524
+ } else {
525
+ const span = compiled.labelTimes.get(cue.at);
526
+ if (!span) {
527
+ warnings.push(`cue[${index}]: unknown label "${cue.at}" \u2014 cue dropped`);
528
+ continue;
529
+ }
530
+ anchor = span.t0;
531
+ }
532
+ const t = Math.max(0, anchor + (cue.offset ?? 0));
533
+ const cueDuration = cue.sfx ? SFX_DURATION[cue.sfx] : FILE_CUE_DURATION;
534
+ if (t >= duration) {
535
+ warnings.push(`cue[${index}] at ${t.toFixed(2)}s starts past the scene end (${duration.toFixed(2)}s) \u2014 dropped`);
536
+ continue;
537
+ }
538
+ if (t + cueDuration > duration) {
539
+ warnings.push(`cue[${index}] at ${t.toFixed(2)}s extends past the scene end \u2014 it will be truncated`);
540
+ }
541
+ cues.push({
542
+ t,
543
+ gain: cue.gain ?? 1,
544
+ duration: cueDuration,
545
+ source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
546
+ });
547
+ }
548
+ cues.sort((a, b) => a.t - b.t);
549
+ const duckWindows = [];
550
+ for (const cue of cues) {
551
+ const window = { t0: cue.t, t1: Math.min(duration, cue.t + cue.duration) };
552
+ const last = duckWindows[duckWindows.length - 1];
553
+ if (last && window.t0 <= last.t1 + 0.1) last.t1 = Math.max(last.t1, window.t1);
554
+ else duckWindows.push(window);
555
+ }
556
+ let bgm = null;
557
+ if (audio.bgm) {
558
+ const b = audio.bgm;
559
+ const duck = b.duck === false ? null : {
560
+ depth: b.duck?.depth ?? 0.5,
561
+ attack: b.duck?.attack ?? 0.05,
562
+ release: b.duck?.release ?? 0.25
563
+ };
564
+ bgm = {
565
+ source: b.file ? { kind: "file", path: b.file } : { kind: "synth", name: b.synth ?? "ambient-pad" },
566
+ gain: b.gain ?? 0.5,
567
+ fadeIn: b.fadeIn ?? 0,
568
+ fadeOut: b.fadeOut ?? 0,
569
+ duck
570
+ };
571
+ }
572
+ return { duration, bgm, cues, duckWindows, warnings };
573
+ }
574
+
575
+ // ../core/src/behaviors.ts
576
+ function sampleBehavior(b, t) {
577
+ switch (b.name) {
578
+ case "oscillate": {
579
+ const { amplitude, frequency, phase = 0 } = b.params;
580
+ return amplitude * Math.sin(2 * Math.PI * frequency * t + phase);
581
+ }
582
+ case "wiggle": {
583
+ const { amplitude, frequency, seed } = b.params;
584
+ return amplitude * valueNoise(t * frequency, seed);
585
+ }
586
+ }
587
+ }
588
+ function valueNoise(x, seed) {
589
+ const i = Math.floor(x);
590
+ const f = x - i;
591
+ const u = f * f * (3 - 2 * f);
592
+ const a = hash01(i, seed) * 2 - 1;
593
+ const b = hash01(i + 1, seed) * 2 - 1;
594
+ return a + (b - a) * u;
595
+ }
596
+ function hash01(n, seed) {
597
+ let h = n * 374761393 + seed * 668265263 | 0;
598
+ h = h ^ h >>> 13 | 0;
599
+ h = Math.imul(h, 1274126177);
600
+ h = (h ^ h >>> 16) >>> 0;
601
+ return h / 4294967295;
602
+ }
603
+
604
+ // ../core/src/interpolate.ts
605
+ var EASE_TABLE = {
606
+ linear: (u) => u,
607
+ easeInQuad: (u) => u * u,
608
+ easeOutQuad: (u) => 1 - (1 - u) * (1 - u),
609
+ easeInOutQuad: (u) => u < 0.5 ? 2 * u * u : 1 - (-2 * u + 2) ** 2 / 2,
610
+ easeInCubic: (u) => u ** 3,
611
+ easeOutCubic: (u) => 1 - (1 - u) ** 3,
612
+ easeInOutCubic: (u) => u < 0.5 ? 4 * u ** 3 : 1 - (-2 * u + 2) ** 3 / 2,
613
+ easeInQuart: (u) => u ** 4,
614
+ easeOutQuart: (u) => 1 - (1 - u) ** 4,
615
+ easeInOutQuart: (u) => u < 0.5 ? 8 * u ** 4 : 1 - (-2 * u + 2) ** 4 / 2,
616
+ easeInExpo: (u) => u === 0 ? 0 : 2 ** (10 * u - 10),
617
+ easeOutExpo: (u) => u === 1 ? 1 : 1 - 2 ** (-10 * u),
618
+ easeInOutExpo: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? 2 ** (20 * u - 10) / 2 : (2 - 2 ** (-20 * u + 10)) / 2
619
+ };
620
+ var EASE_NAMES = Object.keys(EASE_TABLE);
621
+ function resolveEase(ease) {
622
+ if (ease === void 0) return EASE_TABLE.linear;
623
+ if (typeof ease === "string") {
624
+ const fn = EASE_TABLE[ease];
625
+ if (!fn) throw new Error(`unknown ease "${ease}" \u2014 valid: ${Object.keys(EASE_TABLE).join(", ")}`);
626
+ return fn;
627
+ }
628
+ return cubicBezierEase(...ease.cubicBezier);
629
+ }
630
+ function cubicBezierEase(x1, y1, x2, y2) {
631
+ const bez = (a, b) => (t) => 3 * a * t * (1 - t) ** 2 + 3 * b * t * t * (1 - t) + t ** 3;
632
+ const bx = bez(x1, x2);
633
+ const by = bez(y1, y2);
634
+ const dbx = (t) => 3 * x1 * (1 - t) * (1 - 3 * t) + 3 * x2 * t * (2 - 3 * t) + 3 * t * t;
635
+ return (u) => {
636
+ if (u <= 0) return 0;
637
+ if (u >= 1) return 1;
638
+ let t = u;
639
+ for (let i = 0; i < 8; i++) {
640
+ const err = bx(t) - u;
641
+ if (Math.abs(err) < 1e-6) return by(t);
642
+ const d = dbx(t);
643
+ if (Math.abs(d) < 1e-6) break;
644
+ t -= err / d;
645
+ }
646
+ let lo = 0;
647
+ let hi = 1;
648
+ t = u;
649
+ while (hi - lo > 1e-6) {
650
+ if (bx(t) < u) lo = t;
651
+ else hi = t;
652
+ t = (lo + hi) / 2;
653
+ }
654
+ return by(t);
655
+ };
656
+ }
657
+ var HEX_COLOR = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
658
+ function isColor(v) {
659
+ return typeof v === "string" && HEX_COLOR.test(v);
660
+ }
661
+ function parseColor(hex) {
662
+ let h = hex.slice(1);
663
+ if (h.length <= 4) h = [...h].map((c) => c + c).join("");
664
+ const n = parseInt(h.padEnd(8, "f"), 16);
665
+ return [n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255];
666
+ }
667
+ function formatColor([r, g, b, a]) {
668
+ const hex = (v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0");
669
+ return a >= 255 ? `#${hex(r)}${hex(g)}${hex(b)}` : `#${hex(r)}${hex(g)}${hex(b)}${hex(a)}`;
670
+ }
671
+ function lerpValue(from, to2, u) {
672
+ if (typeof from === "number" && typeof to2 === "number") {
673
+ return from + (to2 - from) * u;
674
+ }
675
+ if (isColor(from) && isColor(to2)) {
676
+ const a = parseColor(from);
677
+ const b = parseColor(to2);
678
+ return formatColor([
679
+ a[0] + (b[0] - a[0]) * u,
680
+ a[1] + (b[1] - a[1]) * u,
681
+ a[2] + (b[2] - a[2]) * u,
682
+ a[3] + (b[3] - a[3]) * u
683
+ ]);
684
+ }
685
+ return to2;
686
+ }
687
+
688
+ // ../core/src/evaluate.ts
689
+ var IDENTITY = [1, 0, 0, 1, 0, 0];
690
+ function multiply(m, n) {
691
+ return [
692
+ m[0] * n[0] + m[2] * n[1],
693
+ m[1] * n[0] + m[3] * n[1],
694
+ m[0] * n[2] + m[2] * n[3],
695
+ m[1] * n[2] + m[3] * n[3],
696
+ m[0] * n[4] + m[2] * n[5] + m[4],
697
+ m[1] * n[4] + m[3] * n[5] + m[5]
698
+ ];
699
+ }
700
+ function localMatrix(x, y, rotationDeg, scale) {
701
+ const r = rotationDeg * Math.PI / 180;
702
+ const cos = Math.cos(r) * scale;
703
+ const sin = Math.sin(r) * scale;
704
+ return [cos, sin, -sin, cos, x, y];
705
+ }
706
+ var ANCHOR_FACTORS = {
707
+ "top-left": [0, 0],
708
+ "top-center": [0.5, 0],
709
+ "top-right": [1, 0],
710
+ "center-left": [0, 0.5],
711
+ center: [0.5, 0.5],
712
+ "center-right": [1, 0.5],
713
+ "bottom-left": [0, 1],
714
+ "bottom-center": [0.5, 1],
715
+ "bottom-right": [1, 1]
716
+ };
717
+ var TEXT_ALIGN = { 0: "left", 0.5: "center", 1: "right" };
718
+ var TEXT_BASELINE = { 0: "top", 0.5: "middle", 1: "bottom" };
719
+ function behaviorEnvelope(b, t) {
720
+ const from = b.from ?? Number.NEGATIVE_INFINITY;
721
+ const until = b.until ?? Number.POSITIVE_INFINITY;
722
+ if (t < from || t > until) return 0;
723
+ const ramp = b.ramp ?? 0.2;
724
+ let envelope = 1;
725
+ if (Number.isFinite(from) && ramp > 0) envelope = Math.min(envelope, (t - from) / ramp);
726
+ if (Number.isFinite(until) && ramp > 0) envelope = Math.min(envelope, (until - t) / ramp);
727
+ return Math.max(0, Math.min(1, envelope));
728
+ }
729
+ function evaluate(compiled, t) {
730
+ const ops = [];
731
+ const valueAt = (target, prop, fallback) => {
732
+ let value = compiled.initialValues.get(`${target}.${prop}`) ?? fallback;
733
+ const segs = compiled.segments.get(`${target}.${prop}`);
734
+ if (segs) {
735
+ let active;
736
+ for (const seg of segs) {
737
+ if (seg.t0 <= t) active = seg;
738
+ else break;
739
+ }
740
+ if (active) {
741
+ if (t >= active.t1) {
742
+ value = active.to;
743
+ } else {
744
+ const u = resolveEase(active.ease)((t - active.t0) / (active.t1 - active.t0));
745
+ value = lerpValue(active.from, active.to, u);
746
+ }
747
+ }
748
+ }
749
+ for (const b of compiled.ir.behaviors ?? []) {
750
+ if (b.target === target && b.prop === prop && typeof value === "number") {
751
+ const envelope = behaviorEnvelope(b, t);
752
+ if (envelope > 0) value = value + envelope * sampleBehavior(b.behavior, t);
753
+ }
754
+ }
755
+ return value;
756
+ };
757
+ const num = (target, prop, fallback) => {
758
+ const v = valueAt(target, prop, fallback);
759
+ return typeof v === "number" ? v : fallback;
760
+ };
761
+ const str = (target, prop, fallback) => {
762
+ const v = valueAt(target, prop, fallback);
763
+ return typeof v === "string" ? v : String(v);
764
+ };
765
+ const opt = (target, prop, base) => {
766
+ const v = valueAt(target, prop, base ?? "");
767
+ return v === "" && base === void 0 ? void 0 : String(v);
768
+ };
769
+ const walk = (node, parent, parentOpacity) => {
770
+ const id = node.id;
771
+ if (node.type === "line") {
772
+ const opacity2 = parentOpacity * num(id, "opacity", node.props.opacity ?? 1);
773
+ if (opacity2 <= 0) return;
774
+ const progress = Math.max(0, Math.min(1, num(id, "progress", node.props.progress ?? 1)));
775
+ const x1 = num(id, "x1", node.props.x1);
776
+ const y1 = num(id, "y1", node.props.y1);
777
+ ops.push({
778
+ type: "line",
779
+ id,
780
+ transform: parent,
781
+ opacity: opacity2,
782
+ x1,
783
+ y1,
784
+ x2: x1 + (num(id, "x2", node.props.x2) - x1) * progress,
785
+ y2: y1 + (num(id, "y2", node.props.y2) - y1) * progress,
786
+ stroke: str(id, "stroke", node.props.stroke),
787
+ strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1)
788
+ });
789
+ return;
790
+ }
791
+ const opacity = parentOpacity * num(id, "opacity", node.props.opacity ?? 1);
792
+ if (opacity <= 0) return;
793
+ const matrix = multiply(
794
+ parent,
795
+ localMatrix(
796
+ num(id, "x", node.props.x),
797
+ num(id, "y", node.props.y),
798
+ num(id, "rotation", node.props.rotation ?? 0),
799
+ num(id, "scale", node.props.scale ?? 1)
800
+ )
801
+ );
802
+ switch (node.type) {
803
+ case "group":
804
+ for (const child of node.children) walk(child, matrix, opacity);
805
+ return;
806
+ case "rect":
807
+ case "ellipse": {
808
+ const width = num(id, "width", node.props.width);
809
+ const height = num(id, "height", node.props.height);
810
+ const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
811
+ const strokeWidth = num(id, "strokeWidth", node.props.strokeWidth ?? 1);
812
+ const fill = opt(id, "fill", node.props.fill);
813
+ const stroke = opt(id, "stroke", node.props.stroke);
814
+ ops.push({
815
+ type: node.type,
816
+ id,
817
+ transform: matrix,
818
+ opacity,
819
+ width,
820
+ height,
821
+ offsetX: -width * ax,
822
+ offsetY: -height * ay,
823
+ ...fill !== void 0 && { fill },
824
+ ...stroke !== void 0 && { stroke, strokeWidth },
825
+ ...node.type === "rect" && { radius: num(id, "radius", node.props.radius ?? 0) }
826
+ });
827
+ return;
828
+ }
829
+ case "text": {
830
+ const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
831
+ const raw = valueAt(id, "content", node.props.content);
832
+ const decimals = Math.max(
833
+ 0,
834
+ Math.round(num(id, "contentDecimals", node.props.contentDecimals ?? 0))
835
+ );
836
+ ops.push({
837
+ type: "text",
838
+ id,
839
+ transform: matrix,
840
+ opacity,
841
+ content: typeof raw === "number" ? raw.toFixed(decimals) : raw,
842
+ fontFamily: str(id, "fontFamily", node.props.fontFamily),
843
+ fontSize: num(id, "fontSize", node.props.fontSize),
844
+ fontWeight: num(id, "fontWeight", node.props.fontWeight ?? 400),
845
+ fill: str(id, "fill", node.props.fill ?? "#ffffff"),
846
+ letterSpacing: num(id, "letterSpacing", node.props.letterSpacing ?? 0),
847
+ align: TEXT_ALIGN[ax] ?? "left",
848
+ baseline: TEXT_BASELINE[ay] ?? "top"
849
+ });
850
+ return;
851
+ }
852
+ }
853
+ };
854
+ for (const node of compiled.ir.nodes) walk(node, IDENTITY, 1);
855
+ return ops;
856
+ }
857
+ export {
858
+ DEFAULT_FPS,
859
+ DEFAULT_TO_DURATION,
860
+ DEFAULT_TWEEN_DURATION,
861
+ EASE_NAMES,
862
+ PROPS_BY_TYPE,
863
+ SFX_DURATION,
864
+ SceneValidationError,
865
+ compileScene,
866
+ composeScene,
867
+ ellipse,
868
+ evaluate,
869
+ formatComposeReport,
870
+ group,
871
+ isColor,
872
+ lerpValue,
873
+ line,
874
+ oscillate,
875
+ par,
876
+ rect,
877
+ resolveAudioPlan,
878
+ resolveEase,
879
+ sampleBehavior,
880
+ scene,
881
+ seq,
882
+ stagger,
883
+ text,
884
+ to,
885
+ tween,
886
+ validateScene,
887
+ wait,
888
+ wiggle
889
+ };