reframe-video 0.1.2 → 0.2.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 (42) hide show
  1. package/assets/sfx/LICENSE.md +1 -1
  2. package/assets/sfx/bong_001.ogg +0 -0
  3. package/assets/sfx/click_001.ogg +0 -0
  4. package/assets/sfx/confirmation_002.ogg +0 -0
  5. package/assets/sfx/confirmation_003.ogg +0 -0
  6. package/assets/sfx/confirmation_004.ogg +0 -0
  7. package/assets/sfx/glass_001.ogg +0 -0
  8. package/assets/sfx/maximize_001.ogg +0 -0
  9. package/assets/sfx/maximize_002.ogg +0 -0
  10. package/assets/sfx/maximize_005.ogg +0 -0
  11. package/assets/sfx/maximize_009.ogg +0 -0
  12. package/assets/sfx/open_001.ogg +0 -0
  13. package/assets/sfx/pluck_001.ogg +0 -0
  14. package/assets/sfx/pluck_002.ogg +0 -0
  15. package/assets/sfx/select_001.ogg +0 -0
  16. package/assets/sfx/select_002.ogg +0 -0
  17. package/assets/sfx/select_003.ogg +0 -0
  18. package/dist/bin.js +724 -131
  19. package/dist/browserEntry.js +130 -68
  20. package/dist/cli.js +445 -85
  21. package/dist/index.js +674 -86
  22. package/dist/labels.js +606 -0
  23. package/dist/renderer-canvas.js +15 -0
  24. package/dist/trace-cli.js +9 -9
  25. package/dist/types/audio.d.ts +9 -0
  26. package/dist/types/compile.d.ts +1 -0
  27. package/dist/types/compose.d.ts +18 -2
  28. package/dist/types/composeComposition.d.ts +27 -0
  29. package/dist/types/devicePreset.d.ts +65 -0
  30. package/dist/types/dsl.d.ts +12 -1
  31. package/dist/types/evaluate.d.ts +32 -0
  32. package/dist/types/index.d.ts +6 -3
  33. package/dist/types/ir.d.ts +68 -0
  34. package/dist/types/motionOps.d.ts +36 -0
  35. package/dist/types/path.d.ts +7 -3
  36. package/dist/types/validate.d.ts +4 -1
  37. package/guides/edsl-guide.md +2 -1
  38. package/package.json +1 -1
  39. package/preview/index.html +56 -3
  40. package/preview/src/main.ts +1132 -46
  41. package/preview/src/panel.ts +478 -8
  42. package/preview/src/store.ts +323 -6
package/dist/labels.js ADDED
@@ -0,0 +1,606 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ // ../core/src/ir.ts
4
+ var DEFAULT_TO_DURATION = 0.5;
5
+ var DEFAULT_TWEEN_DURATION = 0.5;
6
+ var DEFAULT_MOTIONPATH_DURATION = 1;
7
+
8
+ // ../core/src/path.ts
9
+ function locate(segCount, u) {
10
+ if (segCount <= 0) return { i: 0, t: 0 };
11
+ const clamped = Math.max(0, Math.min(1, u));
12
+ const scaled = clamped * segCount;
13
+ let i = Math.floor(scaled);
14
+ if (i >= segCount) i = segCount - 1;
15
+ return { i, t: scaled - i };
16
+ }
17
+ function controls(points, closed, i) {
18
+ const n = points.length;
19
+ const at = (k) => {
20
+ if (closed) return points[(k % n + n) % n];
21
+ return points[Math.max(0, Math.min(n - 1, k))];
22
+ };
23
+ return [at(i - 1), at(i), at(i + 1), at(i + 2)];
24
+ }
25
+ function segCountOf(points, closed) {
26
+ const n = points.length;
27
+ if (n < 2) return 0;
28
+ return closed ? n : n - 1;
29
+ }
30
+ function pathPoint(points, closed, u, curviness = 1) {
31
+ const n = points.length;
32
+ if (n === 0) return [0, 0];
33
+ if (n === 1) return [points[0][0], points[0][1]];
34
+ const segs = segCountOf(points, closed);
35
+ const { i, t } = locate(segs, u);
36
+ const [p0, p1, p2, p3] = controls(points, closed, i);
37
+ const t2 = t * t;
38
+ const t3 = t2 * t;
39
+ if (curviness === 1) {
40
+ const f = (a, b, c, d) => 0.5 * (2 * b + (-a + c) * t + (2 * a - 5 * b + 4 * c - d) * t2 + (-a + 3 * b - 3 * c + d) * t3);
41
+ return [f(p0[0], p1[0], p2[0], p3[0]), f(p0[1], p1[1], p2[1], p3[1])];
42
+ }
43
+ const h00 = 2 * t3 - 3 * t2 + 1;
44
+ const h10 = t3 - 2 * t2 + t;
45
+ const h01 = -2 * t3 + 3 * t2;
46
+ const h11 = t3 - t2;
47
+ const k = curviness * 0.5;
48
+ const H = (a, b, c, d) => h00 * b + h10 * k * (c - a) + h01 * c + h11 * k * (d - b);
49
+ return [H(p0[0], p1[0], p2[0], p3[0]), H(p0[1], p1[1], p2[1], p3[1])];
50
+ }
51
+ function pathTangentAngle(points, closed, u, curviness = 1) {
52
+ const n = points.length;
53
+ if (n < 2) return 0;
54
+ const segs = segCountOf(points, closed);
55
+ const { i, t } = locate(segs, u);
56
+ const [p0, p1, p2, p3] = controls(points, closed, i);
57
+ const t2 = t * t;
58
+ let dx;
59
+ let dy;
60
+ if (curviness === 1) {
61
+ const d = (a, b, c, e) => 0.5 * (-a + c + 2 * (2 * a - 5 * b + 4 * c - e) * t + 3 * (-a + 3 * b - 3 * c + e) * t2);
62
+ dx = d(p0[0], p1[0], p2[0], p3[0]);
63
+ dy = d(p0[1], p1[1], p2[1], p3[1]);
64
+ } else {
65
+ const g00 = 6 * t2 - 6 * t;
66
+ const g10 = 3 * t2 - 4 * t + 1;
67
+ const g01 = -6 * t2 + 6 * t;
68
+ const g11 = 3 * t2 - 2 * t;
69
+ const k = curviness * 0.5;
70
+ const D = (a, b, c, e) => g00 * b + g10 * k * (c - a) + g01 * c + g11 * k * (e - b);
71
+ dx = D(p0[0], p1[0], p2[0], p3[0]);
72
+ dy = D(p0[1], p1[1], p2[1], p3[1]);
73
+ }
74
+ if (dx === 0 && dy === 0) return 0;
75
+ return Math.atan2(dy, dx) * 180 / Math.PI;
76
+ }
77
+
78
+ // ../core/src/compile.ts
79
+ var key = (target, prop) => `${target}.${prop}`;
80
+ function scaleTimeline(tl, k) {
81
+ switch (tl.kind) {
82
+ case "seq":
83
+ case "par":
84
+ return { ...tl, children: tl.children.map((c) => scaleTimeline(c, k)) };
85
+ case "stagger":
86
+ return { ...tl, interval: tl.interval * k, children: tl.children.map((c) => scaleTimeline(c, k)) };
87
+ case "wait":
88
+ return { ...tl, duration: tl.duration * k };
89
+ case "tween":
90
+ return { ...tl, duration: (tl.duration ?? DEFAULT_TWEEN_DURATION) * k };
91
+ case "motionPath":
92
+ return { ...tl, duration: (tl.duration ?? DEFAULT_MOTIONPATH_DURATION) * k };
93
+ case "to":
94
+ return {
95
+ ...tl,
96
+ duration: (tl.duration ?? DEFAULT_TO_DURATION) * k,
97
+ ...tl.stagger !== void 0 && { stagger: tl.stagger * k }
98
+ };
99
+ case "beat":
100
+ return {
101
+ ...tl,
102
+ children: tl.children.map((c) => scaleTimeline(c, k)),
103
+ ...tl.gap !== void 0 && { gap: tl.gap * k }
104
+ };
105
+ }
106
+ }
107
+ function orderBeats(children) {
108
+ return children.map((c, i) => ({ c, i, key: c.kind === "beat" && c.order !== void 0 ? c.order : i })).sort((a, b) => a.key - b.key || a.i - b.i).map((x) => x.c);
109
+ }
110
+ function compileScene(ir) {
111
+ const nodeById = /* @__PURE__ */ new Map();
112
+ const nodeOrder = [];
113
+ const collect = (nodes) => {
114
+ for (const node of nodes) {
115
+ nodeById.set(node.id, node);
116
+ nodeOrder.push(node.id);
117
+ if (node.type === "group") collect(node.children);
118
+ }
119
+ };
120
+ collect(ir.nodes);
121
+ const initialValues = /* @__PURE__ */ new Map();
122
+ for (const [id, node] of nodeById) {
123
+ for (const [prop, value] of Object.entries(node.props)) {
124
+ if (typeof value === "number" || typeof value === "string") {
125
+ initialValues.set(key(id, prop), value);
126
+ }
127
+ }
128
+ }
129
+ if (ir.initial !== void 0) {
130
+ const override = ir.states?.[ir.initial] ?? {};
131
+ for (const [id, props] of Object.entries(override)) {
132
+ for (const [prop, value] of Object.entries(props)) {
133
+ initialValues.set(key(id, prop), value);
134
+ }
135
+ }
136
+ }
137
+ const segments = /* @__PURE__ */ new Map();
138
+ const motionPaths = /* @__PURE__ */ new Map();
139
+ const current = new Map(initialValues);
140
+ const pushSegment = (seg) => {
141
+ const k = key(seg.target, seg.prop);
142
+ let list = segments.get(k);
143
+ if (!list) segments.set(k, list = []);
144
+ list.push(seg);
145
+ current.set(k, seg.to);
146
+ };
147
+ const currentValue = (target, prop) => {
148
+ const v = current.get(key(target, prop));
149
+ if (v !== void 0) return v;
150
+ if (prop === "opacity" || prop === "scale" || prop === "progress" || prop === "scaleX" || prop === "scaleY") return 1;
151
+ if (prop === "rotation" || prop === "skewX" || prop === "skewY") return 0;
152
+ throw new Error(`cannot animate "${prop}" of "${target}": no base value to start from`);
153
+ };
154
+ const labelTimes = /* @__PURE__ */ new Map();
155
+ const beatTimes = /* @__PURE__ */ new Map();
156
+ const durationOf = (tl, start) => {
157
+ switch (tl.kind) {
158
+ case "seq": {
159
+ let t = start;
160
+ for (const child of orderBeats(tl.children)) t = durationOf(child, t);
161
+ return t;
162
+ }
163
+ case "par": {
164
+ let end = start;
165
+ for (const child of tl.children) end = Math.max(end, durationOf(child, start));
166
+ return end;
167
+ }
168
+ case "stagger": {
169
+ let end = start;
170
+ tl.children.forEach((child, i) => {
171
+ end = Math.max(end, durationOf(child, start + i * tl.interval));
172
+ });
173
+ return end;
174
+ }
175
+ case "wait":
176
+ return start + tl.duration;
177
+ case "tween":
178
+ return start + (tl.duration ?? DEFAULT_TWEEN_DURATION);
179
+ case "motionPath":
180
+ return start + (tl.duration ?? DEFAULT_MOTIONPATH_DURATION);
181
+ case "to": {
182
+ const override = ir.states?.[tl.state] ?? {};
183
+ const duration = tl.duration ?? DEFAULT_TO_DURATION;
184
+ const si = tl.stagger ?? 0;
185
+ const targets = nodeOrder.filter(
186
+ (id) => id in override && (tl.filter === void 0 || tl.filter.includes(id))
187
+ );
188
+ return start + duration + Math.max(0, targets.length - 1) * si;
189
+ }
190
+ case "beat": {
191
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
192
+ const natural = durationOf(grouping, 0);
193
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, natural) : 1);
194
+ const beatStart = tl.at ?? start + (tl.gap ?? 0);
195
+ return beatStart + k * natural;
196
+ }
197
+ }
198
+ };
199
+ const walk = (tl, start) => {
200
+ const end = walkInner(tl, start);
201
+ if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
202
+ return end;
203
+ };
204
+ const walkInner = (tl, start) => {
205
+ switch (tl.kind) {
206
+ case "seq": {
207
+ let t = start;
208
+ for (const child of orderBeats(tl.children)) t = walk(child, t);
209
+ return t;
210
+ }
211
+ case "beat": {
212
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
213
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
214
+ const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
215
+ const beatStart = tl.at ?? start + (tl.gap ?? 0);
216
+ const end = walk(inner, beatStart);
217
+ beatTimes.set(tl.name, { t0: beatStart, t1: end });
218
+ labelTimes.set(tl.name, { t0: beatStart, t1: end });
219
+ return end;
220
+ }
221
+ case "par": {
222
+ let end = start;
223
+ for (const child of tl.children) end = Math.max(end, walk(child, start));
224
+ return end;
225
+ }
226
+ case "stagger": {
227
+ let end = start;
228
+ tl.children.forEach((child, i) => {
229
+ end = Math.max(end, walk(child, start + i * tl.interval));
230
+ });
231
+ return end;
232
+ }
233
+ case "wait":
234
+ return start + tl.duration;
235
+ case "tween": {
236
+ const duration = tl.duration ?? DEFAULT_TWEEN_DURATION;
237
+ for (const [prop, toValue] of Object.entries(tl.props)) {
238
+ pushSegment({
239
+ target: tl.target,
240
+ prop,
241
+ t0: start,
242
+ t1: start + duration,
243
+ from: currentValue(tl.target, prop),
244
+ to: toValue,
245
+ ...tl.ease !== void 0 && { ease: tl.ease }
246
+ });
247
+ }
248
+ return start + duration;
249
+ }
250
+ case "motionPath": {
251
+ const duration = tl.duration ?? DEFAULT_MOTIONPATH_DURATION;
252
+ const points = tl.points;
253
+ const closed = tl.closed ?? false;
254
+ const curviness = tl.curviness ?? 1;
255
+ const autoRotate = tl.autoRotate ?? false;
256
+ const rotateOffset = tl.rotateOffset ?? 0;
257
+ let list = motionPaths.get(tl.target);
258
+ if (!list) motionPaths.set(tl.target, list = []);
259
+ list.push({ t0: start, t1: start + duration, points, closed, curviness, autoRotate, rotateOffset, ...tl.ease !== void 0 && { ease: tl.ease } });
260
+ if (points.length > 0) {
261
+ const [ex, ey] = pathPoint(points, closed, 1, curviness);
262
+ current.set(key(tl.target, "x"), ex);
263
+ current.set(key(tl.target, "y"), ey);
264
+ if (autoRotate) current.set(key(tl.target, "rotation"), pathTangentAngle(points, closed, 1, curviness) + rotateOffset);
265
+ }
266
+ return start + duration;
267
+ }
268
+ case "to": {
269
+ const override = ir.states?.[tl.state] ?? {};
270
+ const duration = tl.duration ?? DEFAULT_TO_DURATION;
271
+ const staggerInterval = tl.stagger ?? 0;
272
+ const targets = nodeOrder.filter(
273
+ (id) => id in override && (tl.filter === void 0 || tl.filter.includes(id))
274
+ );
275
+ targets.forEach((id, i) => {
276
+ const t0 = start + i * staggerInterval;
277
+ for (const [prop, toValue] of Object.entries(override[id])) {
278
+ pushSegment({
279
+ target: id,
280
+ prop,
281
+ t0,
282
+ t1: t0 + duration,
283
+ from: currentValue(id, prop),
284
+ to: toValue,
285
+ ...tl.ease !== void 0 && { ease: tl.ease }
286
+ });
287
+ }
288
+ });
289
+ const last = Math.max(0, targets.length - 1);
290
+ return start + duration + last * staggerInterval;
291
+ }
292
+ }
293
+ };
294
+ const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
295
+ for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
296
+ for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
297
+ return {
298
+ ir,
299
+ duration: ir.duration ?? inferredEnd,
300
+ segments,
301
+ motionPaths,
302
+ initialValues,
303
+ nodeById,
304
+ nodeOrder,
305
+ labelTimes,
306
+ beatTimes
307
+ };
308
+ }
309
+
310
+ // ../core/src/validate.ts
311
+ var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor"];
312
+ var PROPS_BY_TYPE = {
313
+ rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
314
+ ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
315
+ line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
316
+ text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
317
+ image: [...COMMON_PROPS, "src", "width", "height"],
318
+ path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
319
+ group: COMMON_PROPS
320
+ };
321
+ var SceneValidationError = class extends Error {
322
+ constructor(problems) {
323
+ super(`Scene validation failed:
324
+ ${problems.map((p) => ` - ${p}`).join("\n")}`);
325
+ this.problems = problems;
326
+ this.name = "SceneValidationError";
327
+ }
328
+ problems;
329
+ };
330
+ function validateScene(ir) {
331
+ const problems = [];
332
+ const nodeById = /* @__PURE__ */ new Map();
333
+ const collect = (nodes) => {
334
+ for (const node of nodes) {
335
+ if (nodeById.has(node.id)) {
336
+ problems.push(`duplicate node id "${node.id}" \u2014 every node id must be unique`);
337
+ }
338
+ nodeById.set(node.id, node);
339
+ if (node.type === "group") {
340
+ const clip = node.props.clip;
341
+ if (clip) {
342
+ if (clip.kind !== "rect" && clip.kind !== "ellipse") {
343
+ problems.push(`group "${node.id}" clip: unknown kind "${clip.kind}" \u2014 use "rect" or "ellipse"`);
344
+ }
345
+ if (!(clip.width > 0) || !(clip.height > 0)) {
346
+ problems.push(`group "${node.id}" clip: width and height must be > 0`);
347
+ }
348
+ }
349
+ collect(node.children);
350
+ }
351
+ }
352
+ };
353
+ collect(ir.nodes);
354
+ const checkProps = (where, nodeId, props) => {
355
+ const node = nodeById.get(nodeId);
356
+ if (!node) {
357
+ problems.push(
358
+ `${where} targets unknown node "${nodeId}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
359
+ );
360
+ return;
361
+ }
362
+ const allowed = PROPS_BY_TYPE[node.type];
363
+ for (const key2 of Object.keys(props)) {
364
+ if (!allowed.includes(key2)) {
365
+ problems.push(
366
+ `${where}: "${key2}" is not a prop of ${node.type} "${nodeId}" \u2014 valid props: ${allowed.join(", ")}`
367
+ );
368
+ }
369
+ }
370
+ };
371
+ const states = ir.states ?? {};
372
+ for (const [stateName, overrides] of Object.entries(states)) {
373
+ for (const [nodeId, props] of Object.entries(overrides)) {
374
+ checkProps(`state "${stateName}"`, nodeId, props);
375
+ }
376
+ }
377
+ if (ir.initial !== void 0 && !(ir.initial in states)) {
378
+ problems.push(
379
+ `initial state "${ir.initial}" is not defined \u2014 defined states: ${Object.keys(states).join(", ") || "(none)"}`
380
+ );
381
+ }
382
+ const labels = /* @__PURE__ */ new Set();
383
+ const checkTimeline = (tl, path3) => {
384
+ if ("label" in tl && tl.label !== void 0) {
385
+ if (labels.has(tl.label)) {
386
+ problems.push(
387
+ `${path3}: duplicate timeline label "${tl.label}" \u2014 labels are overlay addresses and must be unique`
388
+ );
389
+ }
390
+ labels.add(tl.label);
391
+ }
392
+ switch (tl.kind) {
393
+ case "seq":
394
+ case "par":
395
+ tl.children.forEach((c, i) => checkTimeline(c, `${path3}.${tl.kind}[${i}]`));
396
+ break;
397
+ case "stagger":
398
+ if (tl.interval < 0) problems.push(`${path3}: stagger interval must be >= 0`);
399
+ tl.children.forEach((c, i) => checkTimeline(c, `${path3}.stagger[${i}]`));
400
+ break;
401
+ case "to":
402
+ if (!(tl.state in states)) {
403
+ problems.push(
404
+ `${path3}: to("${tl.state}") references an undefined state \u2014 defined states: ${Object.keys(states).join(", ") || "(none)"}`
405
+ );
406
+ }
407
+ if (tl.duration !== void 0 && tl.duration <= 0) {
408
+ problems.push(`${path3}: to("${tl.state}") duration must be > 0`);
409
+ }
410
+ for (const id of tl.filter ?? []) {
411
+ if (!nodeById.has(id)) problems.push(`${path3}: filter contains unknown node "${id}"`);
412
+ }
413
+ break;
414
+ case "tween":
415
+ checkProps(path3, tl.target, tl.props);
416
+ if (tl.duration !== void 0 && tl.duration <= 0) {
417
+ problems.push(`${path3}: tween duration must be > 0`);
418
+ }
419
+ break;
420
+ case "motionPath": {
421
+ const node = nodeById.get(tl.target);
422
+ if (!node) {
423
+ problems.push(
424
+ `${path3}: motionPath targets unknown node "${tl.target}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
425
+ );
426
+ } else if (node.type === "line") {
427
+ problems.push(`${path3}: motionPath cannot target a line (no x/y) \u2014 "${tl.target}"`);
428
+ }
429
+ if (tl.points.length < 1) problems.push(`${path3}: motionPath "${tl.target}" needs at least 1 point`);
430
+ if (tl.duration !== void 0 && tl.duration <= 0) {
431
+ problems.push(`${path3}: motionPath "${tl.target}" duration must be > 0`);
432
+ }
433
+ if (tl.curviness !== void 0 && tl.curviness < 0) {
434
+ problems.push(`${path3}: motionPath "${tl.target}" curviness must be >= 0`);
435
+ }
436
+ break;
437
+ }
438
+ case "wait":
439
+ if (tl.duration < 0) problems.push(`${path3}: wait duration must be >= 0`);
440
+ break;
441
+ case "beat":
442
+ if (labels.has(tl.name)) {
443
+ problems.push(
444
+ `${path3}: duplicate timeline label "${tl.name}" (beat name) \u2014 labels are overlay addresses and must be unique`
445
+ );
446
+ }
447
+ labels.add(tl.name);
448
+ if (tl.duration !== void 0 && tl.duration <= 0) {
449
+ problems.push(`${path3}: beat "${tl.name}" duration must be > 0`);
450
+ }
451
+ if (tl.scale !== void 0 && tl.scale <= 0) {
452
+ problems.push(`${path3}: beat "${tl.name}" scale must be > 0`);
453
+ }
454
+ for (const id of tl.nodes ?? []) {
455
+ if (!nodeById.has(id)) {
456
+ problems.push(
457
+ `${path3}: beat "${tl.name}" owns unknown node "${id}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
458
+ );
459
+ }
460
+ }
461
+ tl.children.forEach((c, i) => checkTimeline(c, `${path3}.beat(${tl.name})[${i}]`));
462
+ break;
463
+ }
464
+ };
465
+ if (ir.timeline) checkTimeline(ir.timeline, "timeline");
466
+ for (const [i, b] of (ir.behaviors ?? []).entries()) {
467
+ checkProps(`behaviors[${i}]`, b.target, { [b.prop]: 0 });
468
+ }
469
+ if (ir.duration !== void 0 && ir.duration <= 0) {
470
+ problems.push("scene duration must be > 0");
471
+ }
472
+ const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
473
+ for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
474
+ if (typeof cue.at === "string" && !labels.has(cue.at)) {
475
+ problems.push(
476
+ `audio.cues[${i}]: unknown timeline label "${cue.at}" \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
477
+ );
478
+ }
479
+ if (typeof cue.at === "number" && cue.at < 0) {
480
+ problems.push(`audio.cues[${i}]: "at" must be >= 0`);
481
+ }
482
+ if (cue.sfx === void 0 === (cue.file === void 0)) {
483
+ problems.push(`audio.cues[${i}]: exactly one of "sfx" or "file" is required`);
484
+ }
485
+ if (cue.sfx !== void 0 && !SFX_NAMES.includes(cue.sfx)) {
486
+ problems.push(`audio.cues[${i}]: unknown sfx "${cue.sfx}" \u2014 valid: ${SFX_NAMES.join(", ")}`);
487
+ }
488
+ if (cue.gain !== void 0 && cue.gain < 0) {
489
+ problems.push(`audio.cues[${i}]: gain must be >= 0`);
490
+ }
491
+ }
492
+ const duck = ir.audio?.bgm?.duck;
493
+ if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
494
+ problems.push("audio.bgm.duck.depth must be in [0, 1]");
495
+ }
496
+ if (ir.audio?.bgm?.file !== void 0 && ir.audio.bgm.synth !== void 0) {
497
+ problems.push('audio.bgm: use either "file" or "synth", not both');
498
+ }
499
+ if (problems.length > 0) throw new SceneValidationError(problems);
500
+ }
501
+
502
+ // ../core/src/presets.ts
503
+ var SET = 1 / 120;
504
+
505
+ // ../core/src/interpolate.ts
506
+ var BACK_C1 = 1.70158;
507
+ var BACK_C2 = BACK_C1 * 1.525;
508
+ var BACK_C3 = BACK_C1 + 1;
509
+ var ELASTIC_C4 = 2 * Math.PI / 3;
510
+ var ELASTIC_C5 = 2 * Math.PI / 4.5;
511
+ function easeOutBounce(u) {
512
+ const n1 = 7.5625;
513
+ const d1 = 2.75;
514
+ if (u < 1 / d1) return n1 * u * u;
515
+ if (u < 2 / d1) return n1 * (u -= 1.5 / d1) * u + 0.75;
516
+ if (u < 2.5 / d1) return n1 * (u -= 2.25 / d1) * u + 0.9375;
517
+ return n1 * (u -= 2.625 / d1) * u + 0.984375;
518
+ }
519
+ var EASE_TABLE = {
520
+ linear: (u) => u,
521
+ easeInQuad: (u) => u * u,
522
+ easeOutQuad: (u) => 1 - (1 - u) * (1 - u),
523
+ easeInOutQuad: (u) => u < 0.5 ? 2 * u * u : 1 - (-2 * u + 2) ** 2 / 2,
524
+ easeInCubic: (u) => u ** 3,
525
+ easeOutCubic: (u) => 1 - (1 - u) ** 3,
526
+ easeInOutCubic: (u) => u < 0.5 ? 4 * u ** 3 : 1 - (-2 * u + 2) ** 3 / 2,
527
+ easeInQuart: (u) => u ** 4,
528
+ easeOutQuart: (u) => 1 - (1 - u) ** 4,
529
+ easeInOutQuart: (u) => u < 0.5 ? 8 * u ** 4 : 1 - (-2 * u + 2) ** 4 / 2,
530
+ easeInExpo: (u) => u === 0 ? 0 : 2 ** (10 * u - 10),
531
+ easeOutExpo: (u) => u === 1 ? 1 : 1 - 2 ** (-10 * u),
532
+ easeInOutExpo: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? 2 ** (20 * u - 10) / 2 : (2 - 2 ** (-20 * u + 10)) / 2,
533
+ // --- expressive eases (GSAP's signature feel) — standard Penner equations ---
534
+ // back: overshoots past the target then settles (pop / snap)
535
+ easeInBack: (u) => BACK_C3 * u ** 3 - BACK_C1 * u * u,
536
+ easeOutBack: (u) => 1 + BACK_C3 * (u - 1) ** 3 + BACK_C1 * (u - 1) ** 2,
537
+ 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,
538
+ // elastic: rings around the target before settling (playful spring)
539
+ easeInElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : -(2 ** (10 * u - 10)) * Math.sin((u * 10 - 10.75) * ELASTIC_C4),
540
+ easeOutElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : 2 ** (-10 * u) * Math.sin((u * 10 - 0.75) * ELASTIC_C4) + 1,
541
+ 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,
542
+ // bounce: drops and bounces to rest (lands without overshoot)
543
+ easeInBounce: (u) => 1 - easeOutBounce(1 - u),
544
+ easeOutBounce,
545
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
546
+ };
547
+ var EASE_NAMES = Object.keys(EASE_TABLE);
548
+
549
+ // ../render-cli/src/loadScene.ts
550
+ import { build } from "esbuild";
551
+ import { readFile } from "node:fs/promises";
552
+ import { dirname, resolve } from "node:path";
553
+ import { fileURLToPath } from "node:url";
554
+ var HERE = dirname(fileURLToPath(import.meta.url));
555
+ var CORE_ENTRY = true ? resolve(HERE, "index.js") : resolve(HERE, "..", "..", "core", "src", "index.ts");
556
+ async function loadDefault(path3) {
557
+ if (path3.endsWith(".json")) return JSON.parse(await readFile(path3, "utf8"));
558
+ let code;
559
+ try {
560
+ const out = await build({
561
+ entryPoints: [path3],
562
+ bundle: true,
563
+ format: "esm",
564
+ platform: "neutral",
565
+ write: false,
566
+ logLevel: "silent",
567
+ sourcemap: "inline",
568
+ // both specifiers accepted: the guide's canonical "@reframe/core" and
569
+ // the published package name
570
+ alias: { "@reframe/core": CORE_ENTRY, "reframe-video": CORE_ENTRY }
571
+ });
572
+ code = out.outputFiles[0].text;
573
+ } catch (err) {
574
+ throw new Error(`failed to bundle ${path3}:
575
+ ${err instanceof Error ? err.message : String(err)}`);
576
+ }
577
+ const mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
578
+ if (mod.default === void 0) throw new Error(`${path3} must default-export a scene or composition`);
579
+ return mod.default;
580
+ }
581
+ function isComposition(def) {
582
+ return typeof def === "object" && def !== null && Array.isArray(def.scenes);
583
+ }
584
+ async function loadScene(path3) {
585
+ const def = await loadDefault(path3);
586
+ if (isComposition(def)) {
587
+ throw new Error(`${path3} is a composition \u2014 render it directly, not as a single scene`);
588
+ }
589
+ validateScene(def);
590
+ return def;
591
+ }
592
+
593
+ // ../render-cli/src/labels.ts
594
+ var path2 = process.argv[2];
595
+ if (!path2) {
596
+ console.error("usage: reframe labels <scene.ts|.json>");
597
+ process.exit(1);
598
+ }
599
+ var scene = await loadScene(path2);
600
+ var compiled = compileScene(scene);
601
+ var rows = [...compiled.labelTimes.entries()].sort((a, b) => a[1].t0 - b[1].t0 || a[0].localeCompare(b[0]));
602
+ console.log(`# ${scene.id} \u2014 ${rows.length} labels \xB7 ${compiled.duration.toFixed(2)}s @ ${scene.fps ?? 30}fps`);
603
+ console.log(`# ${"start".padStart(7)} ${"end".padStart(7)} label`);
604
+ for (const [name, { t0, t1 }] of rows) {
605
+ console.log(`${`${t0.toFixed(2)}s`.padStart(8)} ${`${t1.toFixed(2)}s`.padStart(8)} ${name}`);
606
+ }
@@ -13,6 +13,21 @@ function renderFrame(ctx, compiled, t, images) {
13
13
  function drawDisplayList(ctx, ops, images) {
14
14
  for (const op of ops) {
15
15
  ctx.save();
16
+ if (op.clips) {
17
+ for (const clip of op.clips) {
18
+ ctx.setTransform(...clip.transform);
19
+ ctx.beginPath();
20
+ const { shape } = clip;
21
+ if (shape.kind === "ellipse") {
22
+ ctx.ellipse(shape.x + shape.width / 2, shape.y + shape.height / 2, Math.abs(shape.width / 2), Math.abs(shape.height / 2), 0, 0, Math.PI * 2);
23
+ } else if (shape.radius && shape.radius > 0) {
24
+ ctx.roundRect(shape.x, shape.y, shape.width, shape.height, shape.radius);
25
+ } else {
26
+ ctx.rect(shape.x, shape.y, shape.width, shape.height);
27
+ }
28
+ ctx.clip();
29
+ }
30
+ }
16
31
  ctx.setTransform(...op.transform);
17
32
  ctx.globalAlpha = Math.max(0, Math.min(1, op.opacity));
18
33
  switch (op.type) {
package/dist/trace-cli.js CHANGED
@@ -6,7 +6,7 @@ import { resolve as resolve2 } from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
7
 
8
8
  // ../core/src/validate.ts
9
- var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "anchor"];
9
+ var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor"];
10
10
  var PROPS_BY_TYPE = {
11
11
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
12
12
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
@@ -243,10 +243,10 @@ function analyzePair(prev, next, opts) {
243
243
  import { spawn } from "node:child_process";
244
244
  import { stat } from "node:fs/promises";
245
245
  import { join } from "node:path";
246
- async function resolveSource(path) {
247
- const s = await stat(path);
248
- if (s.isDirectory()) return { kind: "png", input: join(path, "%05d.png") };
249
- return { kind: "mp4", input: path };
246
+ async function resolveSource(path2) {
247
+ const s = await stat(path2);
248
+ if (s.isDirectory()) return { kind: "png", input: join(path2, "%05d.png") };
249
+ return { kind: "mp4", input: path2 };
250
250
  }
251
251
  async function* frameStream(source, opts) {
252
252
  const frameBytes = opts.width * opts.height;
@@ -588,10 +588,10 @@ function extractMotionSketch(profile) {
588
588
  const hi = Math.max(HI_FLOOR, backgroundLevel(diff) + NOISE_MARGIN);
589
589
  const lo = hi * HI_LO_RATIO;
590
590
  const groups = groupRuns(findRuns(diff, nCells, hi, lo), spec.cols);
591
- const events = groups.map((group) => {
592
- const cells = [...new Set(group.map((r) => r.cell))];
593
- const p0 = Math.min(...group.map((r) => r.p0));
594
- const p1 = Math.max(...group.map((r) => r.p1));
591
+ const events = groups.map((group2) => {
592
+ const cells = [...new Set(group2.map((r) => r.cell))];
593
+ const p0 = Math.min(...group2.map((r) => r.p0));
594
+ const p1 = Math.max(...group2.map((r) => r.p1));
595
595
  const cols = cells.map((c) => c % spec.cols);
596
596
  const rows = cells.map((c) => Math.floor(c / spec.cols));
597
597
  const minC = Math.min(...cols);
@@ -7,6 +7,7 @@
7
7
  * AI regenerate the scene, and the sound design follows.
8
8
  */
9
9
  import type { CompiledScene } from "./compile.js";
10
+ import type { CompiledComposition } from "./composeComposition.js";
10
11
  import type { SfxName } from "./ir.js";
11
12
  /** Nominal cue lengths (s) for duck-window math; file cues use a default. */
12
13
  export declare const SFX_DURATION: Record<SfxName, number>;
@@ -51,3 +52,11 @@ export interface AudioPlan {
51
52
  warnings: string[];
52
53
  }
53
54
  export declare function resolveAudioPlan(compiled: CompiledScene): AudioPlan | null;
55
+ /**
56
+ * Composition-level AudioPlan: each scene's cues offset by that scene's start,
57
+ * plus composition-level absolute-time cues, under a composition bed (e.g.
58
+ * kokoro narration) that spans all scenes. Per-scene bgm is ignored (warned) —
59
+ * the bed lives at the composition level. Same determinism boundary as
60
+ * resolveAudioPlan (plan + WAV bytes, not AAC-in-mp4).
61
+ */
62
+ export declare function resolveCompositionAudioPlan(comp: CompiledComposition): AudioPlan | null;