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/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // ../core/src/ir.ts
2
+ var DEFAULT_CROSSFADE = 0.5;
2
3
  var DEFAULT_TO_DURATION = 0.5;
3
4
  var DEFAULT_TWEEN_DURATION = 0.5;
4
5
  var DEFAULT_MOTIONPATH_DURATION = 1;
@@ -26,7 +27,7 @@ function segCountOf(points, closed) {
26
27
  if (n < 2) return 0;
27
28
  return closed ? n : n - 1;
28
29
  }
29
- function pathPoint(points, closed, u) {
30
+ function pathPoint(points, closed, u, curviness = 1) {
30
31
  const n = points.length;
31
32
  if (n === 0) return [0, 0];
32
33
  if (n === 1) return [points[0][0], points[0][1]];
@@ -35,19 +36,41 @@ function pathPoint(points, closed, u) {
35
36
  const [p0, p1, p2, p3] = controls(points, closed, i);
36
37
  const t2 = t * t;
37
38
  const t3 = t2 * t;
38
- 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);
39
- return [f(p0[0], p1[0], p2[0], p3[0]), f(p0[1], p1[1], p2[1], p3[1])];
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])];
40
50
  }
41
- function pathTangentAngle(points, closed, u) {
51
+ function pathTangentAngle(points, closed, u, curviness = 1) {
42
52
  const n = points.length;
43
53
  if (n < 2) return 0;
44
54
  const segs = segCountOf(points, closed);
45
55
  const { i, t } = locate(segs, u);
46
56
  const [p0, p1, p2, p3] = controls(points, closed, i);
47
57
  const t2 = t * t;
48
- 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);
49
- const dx = d(p0[0], p1[0], p2[0], p3[0]);
50
- const dy = d(p0[1], p1[1], p2[1], p3[1]);
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
+ }
51
74
  if (dx === 0 && dy === 0) return 0;
52
75
  return Math.atan2(dy, dx) * 180 / Math.PI;
53
76
  }
@@ -124,8 +147,8 @@ function compileScene(ir) {
124
147
  const currentValue = (target, prop) => {
125
148
  const v = current.get(key(target, prop));
126
149
  if (v !== void 0) return v;
127
- if (prop === "opacity" || prop === "scale" || prop === "progress") return 1;
128
- if (prop === "rotation") return 0;
150
+ if (prop === "opacity" || prop === "scale" || prop === "progress" || prop === "scaleX" || prop === "scaleY") return 1;
151
+ if (prop === "rotation" || prop === "skewX" || prop === "skewY") return 0;
129
152
  throw new Error(`cannot animate "${prop}" of "${target}": no base value to start from`);
130
153
  };
131
154
  const labelTimes = /* @__PURE__ */ new Map();
@@ -228,16 +251,17 @@ function compileScene(ir) {
228
251
  const duration = tl.duration ?? DEFAULT_MOTIONPATH_DURATION;
229
252
  const points = tl.points;
230
253
  const closed = tl.closed ?? false;
254
+ const curviness = tl.curviness ?? 1;
231
255
  const autoRotate = tl.autoRotate ?? false;
232
256
  const rotateOffset = tl.rotateOffset ?? 0;
233
257
  let list = motionPaths.get(tl.target);
234
258
  if (!list) motionPaths.set(tl.target, list = []);
235
- list.push({ t0: start, t1: start + duration, points, closed, autoRotate, rotateOffset, ...tl.ease !== void 0 && { ease: tl.ease } });
259
+ list.push({ t0: start, t1: start + duration, points, closed, curviness, autoRotate, rotateOffset, ...tl.ease !== void 0 && { ease: tl.ease } });
236
260
  if (points.length > 0) {
237
- const [ex, ey] = pathPoint(points, closed, 1);
261
+ const [ex, ey] = pathPoint(points, closed, 1, curviness);
238
262
  current.set(key(tl.target, "x"), ex);
239
263
  current.set(key(tl.target, "y"), ey);
240
- if (autoRotate) current.set(key(tl.target, "rotation"), pathTangentAngle(points, closed, 1) + rotateOffset);
264
+ if (autoRotate) current.set(key(tl.target, "rotation"), pathTangentAngle(points, closed, 1, curviness) + rotateOffset);
241
265
  }
242
266
  return start + duration;
243
267
  }
@@ -284,7 +308,7 @@ function compileScene(ir) {
284
308
  }
285
309
 
286
310
  // ../core/src/validate.ts
287
- var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "anchor"];
311
+ var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor"];
288
312
  var PROPS_BY_TYPE = {
289
313
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
290
314
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
@@ -312,7 +336,18 @@ function validateScene(ir) {
312
336
  problems.push(`duplicate node id "${node.id}" \u2014 every node id must be unique`);
313
337
  }
314
338
  nodeById.set(node.id, node);
315
- if (node.type === "group") collect(node.children);
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
+ }
316
351
  }
317
352
  };
318
353
  collect(ir.nodes);
@@ -395,6 +430,9 @@ function validateScene(ir) {
395
430
  if (tl.duration !== void 0 && tl.duration <= 0) {
396
431
  problems.push(`${path2}: motionPath "${tl.target}" duration must be > 0`);
397
432
  }
433
+ if (tl.curviness !== void 0 && tl.curviness < 0) {
434
+ problems.push(`${path2}: motionPath "${tl.target}" curviness must be >= 0`);
435
+ }
398
436
  break;
399
437
  }
400
438
  case "wait":
@@ -413,6 +451,13 @@ function validateScene(ir) {
413
451
  if (tl.scale !== void 0 && tl.scale <= 0) {
414
452
  problems.push(`${path2}: beat "${tl.name}" scale must be > 0`);
415
453
  }
454
+ for (const id of tl.nodes ?? []) {
455
+ if (!nodeById.has(id)) {
456
+ problems.push(
457
+ `${path2}: beat "${tl.name}" owns unknown node "${id}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
458
+ );
459
+ }
460
+ }
416
461
  tl.children.forEach((c, i) => checkTimeline(c, `${path2}.beat(${tl.name})[${i}]`));
417
462
  break;
418
463
  }
@@ -453,6 +498,36 @@ function validateScene(ir) {
453
498
  }
454
499
  if (problems.length > 0) throw new SceneValidationError(problems);
455
500
  }
501
+ var TRANSITIONS = ["cut", "crossfade"];
502
+ function validateComposition(comp) {
503
+ const problems = [];
504
+ if (comp.scenes.length === 0) problems.push("composition has no scenes");
505
+ const seen = /* @__PURE__ */ new Set();
506
+ for (const [i, entry] of comp.scenes.entries()) {
507
+ const where = `scenes[${i}]`;
508
+ try {
509
+ validateScene(entry.scene);
510
+ } catch (err) {
511
+ if (err instanceof SceneValidationError) {
512
+ for (const p of err.problems) problems.push(`${where} (scene "${entry.scene.id}"): ${p}`);
513
+ } else throw err;
514
+ }
515
+ if (seen.has(entry.scene.id)) {
516
+ problems.push(`${where}: duplicate scene id "${entry.scene.id}" \u2014 scene ids must be unique in a composition`);
517
+ }
518
+ seen.add(entry.scene.id);
519
+ if (entry.transition !== void 0 && !TRANSITIONS.includes(entry.transition)) {
520
+ problems.push(`${where}: unknown transition "${entry.transition}" \u2014 valid: ${TRANSITIONS.join(", ")}`);
521
+ }
522
+ if (typeof entry.at === "string" && Number.isNaN(Number(entry.at))) {
523
+ problems.push(`${where}: "at" string "${entry.at}" is not a number (use "-0.5"/"+0.5" or a number)`);
524
+ }
525
+ if (typeof entry.at === "number" && entry.at < 0) {
526
+ problems.push(`${where}: absolute "at" must be >= 0`);
527
+ }
528
+ }
529
+ if (problems.length > 0) throw new SceneValidationError(problems);
530
+ }
456
531
 
457
532
  // ../core/src/dsl.ts
458
533
  function scene(input) {
@@ -463,6 +538,11 @@ function scene(input) {
463
538
  }
464
539
  return ir;
465
540
  }
541
+ function composition(input) {
542
+ const ir = { version: 1, ...input };
543
+ validateComposition(ir);
544
+ return ir;
545
+ }
466
546
  function rect(props) {
467
547
  const { id, ...rest } = props;
468
548
  return { type: "rect", id, props: rest };
@@ -522,11 +602,47 @@ function wiggle(target, prop, params, window = {}) {
522
602
  return { target, prop, ...window, behavior: { kind: "named", name: "wiggle", params } };
523
603
  }
524
604
 
605
+ // ../core/src/composeComposition.ts
606
+ function compileComposition(comp) {
607
+ const scenes = [];
608
+ let prevEnd = 0;
609
+ comp.scenes.forEach((entry, i) => {
610
+ const compiled = compileScene(entry.scene);
611
+ const duration2 = compiled.duration;
612
+ const transition = entry.transition ?? "cut";
613
+ const append = i === 0 ? 0 : prevEnd;
614
+ let start;
615
+ if (typeof entry.at === "number") {
616
+ start = entry.at;
617
+ } else if (typeof entry.at === "string") {
618
+ start = append + Number(entry.at);
619
+ } else if (transition === "crossfade" && i > 0) {
620
+ start = append - DEFAULT_CROSSFADE;
621
+ } else {
622
+ start = append;
623
+ }
624
+ start = Math.max(0, start);
625
+ const overlap = i > 0 ? Math.max(0, prevEnd - start) : 0;
626
+ scenes.push({ id: entry.scene.id, scene: entry.scene, compiled, start, duration: duration2, transition, overlap });
627
+ prevEnd = start + duration2;
628
+ });
629
+ const duration = scenes.reduce((max, s) => Math.max(max, s.start + s.duration), 0);
630
+ return { ir: comp, scenes, duration };
631
+ }
632
+
525
633
  // ../core/src/compose.ts
526
634
  var SCENE_PATCHABLE = ["background", "duration", "fps"];
527
635
  function composeScene(base, ...overlays) {
528
636
  const ir = structuredClone(base);
529
637
  const report = { applied: [], orphans: [], warnings: [] };
638
+ const baseNodeIds = /* @__PURE__ */ new Set();
639
+ const collectBase = (nodes) => {
640
+ for (const node of nodes) {
641
+ baseNodeIds.add(node.id);
642
+ if (node.type === "group") collectBase(node.children);
643
+ }
644
+ };
645
+ collectBase(base.nodes);
530
646
  overlays.forEach((overlay, index) => {
531
647
  const layer = overlay.name ?? `overlay-${index}`;
532
648
  if (overlay.target !== void 0 && overlay.target !== ir.id) {
@@ -534,12 +650,12 @@ function composeScene(base, ...overlays) {
534
650
  `${layer}: authored against scene "${overlay.target}" but composing onto "${ir.id}"`
535
651
  );
536
652
  }
537
- applyOverlay(ir, overlay, layer, report);
653
+ applyOverlay(ir, overlay, layer, report, baseNodeIds);
538
654
  });
539
655
  validateScene(ir);
540
656
  return { ir, report };
541
657
  }
542
- function applyOverlay(ir, overlay, layer, report) {
658
+ function applyOverlay(ir, overlay, layer, report, baseNodeIds) {
543
659
  const nodeById = /* @__PURE__ */ new Map();
544
660
  const collect = (nodes) => {
545
661
  for (const node of nodes) {
@@ -654,7 +770,7 @@ function applyOverlay(ir, overlay, layer, report) {
654
770
  to: ["duration", "ease", "stagger"],
655
771
  tween: ["duration", "ease"],
656
772
  wait: ["duration"],
657
- motionPath: ["points", "duration", "ease"],
773
+ motionPath: ["points", "duration", "ease", "curviness", "autoRotate"],
658
774
  beat: ["at", "gap", "scale", "duration", "order"]
659
775
  };
660
776
  let timingPatched = false;
@@ -692,6 +808,49 @@ function applyOverlay(ir, overlay, layer, report) {
692
808
  nodeById.set(node.id, node);
693
809
  applied(`addNodes.${node.id}`, "add-node");
694
810
  }
811
+ for (const id of overlay.removeNodes ?? []) {
812
+ if (baseNodeIds.has(id)) {
813
+ orphan(
814
+ `removeNodes.${id}`,
815
+ `"${id}" is a base scene node \u2014 the scene owns it; hide it with opacity: 0 instead of removing`
816
+ );
817
+ continue;
818
+ }
819
+ const index = ir.nodes.findIndex((n) => n.id === id);
820
+ if (index < 0) {
821
+ orphan(
822
+ `removeNodes.${id}`,
823
+ `unknown overlay-added node "${id}" \u2014 nothing to remove`
824
+ );
825
+ continue;
826
+ }
827
+ ir.nodes.splice(index, 1);
828
+ nodeById.delete(id);
829
+ applied(`removeNodes.${id}`, "remove-node");
830
+ }
831
+ if (overlay.addTimeline && overlay.addTimeline.length > 0) {
832
+ const collectTargets = (tl, out) => {
833
+ if (tl.kind === "tween" || tl.kind === "motionPath") out.add(tl.target);
834
+ if ("children" in tl) tl.children.forEach((c) => collectTargets(c, out));
835
+ };
836
+ const valid = [];
837
+ overlay.addTimeline.forEach((frag, i) => {
838
+ const targets = /* @__PURE__ */ new Set();
839
+ collectTargets(frag, targets);
840
+ const missing = [...targets].filter((id) => !nodeById.has(id));
841
+ if (missing.length > 0) {
842
+ orphan(`addTimeline[${i}]`, `targets unknown node(s) ${missing.join(", ")} \u2014 known ids: ${knownIds()}`);
843
+ return;
844
+ }
845
+ valid.push(structuredClone(frag));
846
+ applied(`addTimeline[${i}]`, "add-timeline");
847
+ });
848
+ if (valid.length > 0) {
849
+ ir.timeline = ir.timeline ? { kind: "par", children: [ir.timeline, ...valid] } : valid.length === 1 ? valid[0] : { kind: "par", children: valid };
850
+ delete ir.duration;
851
+ ir.duration = compileScene(ir).duration;
852
+ }
853
+ }
695
854
  }
696
855
  function formatComposeReport(report) {
697
856
  const lines = [];
@@ -884,6 +1043,310 @@ function motionPreset(name, opts) {
884
1043
  }
885
1044
  }
886
1045
 
1046
+ // ../core/src/devicePreset.ts
1047
+ var DEVICE_PRESET_NAMES = ["phone", "tablet", "laptop", "browser", "watch", "monitor", "tv", "foldable", "terminal", "car"];
1048
+ var DARK = { body: "#15161C", bodyStroke: "#2A2D38", screen: "#0E0F15", detail: "#3A3D48", chrome: "#1B1D24", chromeText: "#9AA0AD" };
1049
+ var LIGHT = { body: "#E7E9EE", bodyStroke: "#C3C7D1", screen: "#FFFFFF", detail: "#AEB3C0", chrome: "#F2F3F6", chromeText: "#5B606C" };
1050
+ var SCREENS = {
1051
+ phone: { width: 352, height: 736, radius: 38 },
1052
+ tablet: { width: 544, height: 764, radius: 18 },
1053
+ laptop: { width: 840, height: 520, radius: 8, cy: -150 },
1054
+ browser: { width: 984, height: 568, radius: 6, cy: 24 },
1055
+ watch: { width: 184, height: 224, radius: 44 },
1056
+ monitor: { width: 1056, height: 600, radius: 6 },
1057
+ tv: { width: 1280, height: 720, radius: 8, cy: -24 },
1058
+ foldable: { width: 760, height: 560, radius: 20 },
1059
+ terminal: { width: 900, height: 560, radius: 6, cy: 18 },
1060
+ car: { width: 1e3, height: 520, radius: 24 }
1061
+ };
1062
+ var BOUNDS = {
1063
+ phone: { width: 392, height: 812 },
1064
+ tablet: { width: 600, height: 820 },
1065
+ laptop: { width: 1100, height: 650 },
1066
+ browser: { width: 1e3, height: 660 },
1067
+ watch: { width: 220, height: 300 },
1068
+ monitor: { width: 1120, height: 860 },
1069
+ tv: { width: 1340, height: 920 },
1070
+ foldable: { width: 800, height: 600 },
1071
+ terminal: { width: 916, height: 636 },
1072
+ car: { width: 1060, height: 600 }
1073
+ };
1074
+ var isLandscape = (name, o) => (name === "phone" || name === "tablet") && o.orientation === "landscape";
1075
+ function screenDims(name, o) {
1076
+ const d = SCREENS[name];
1077
+ const base = { cx: d.cx ?? 0, cy: d.cy ?? 0 };
1078
+ return isLandscape(name, o) ? { width: d.height, height: d.width, radius: d.radius, ...base } : { width: d.width, height: d.height, radius: d.radius, ...base };
1079
+ }
1080
+ function deviceScreen(name, opts = {}) {
1081
+ const d = screenDims(name, opts);
1082
+ return { x: 0, y: 0, width: d.width, height: d.height, radius: d.radius };
1083
+ }
1084
+ function deviceScreenCenter(name, opts = {}) {
1085
+ const d = screenDims(name, opts);
1086
+ return { x: d.cx, y: d.cy };
1087
+ }
1088
+ function deviceBounds(name, opts = {}) {
1089
+ const b = BOUNDS[name];
1090
+ return isLandscape(name, opts) ? { width: b.height, height: b.width } : { ...b };
1091
+ }
1092
+ function screenGroup(id, p, o, cx, cy, dims, content) {
1093
+ return group({ id: `${id}-screen`, x: cx, y: cy, clip: { kind: "rect", x: -dims.width / 2, y: -dims.height / 2, width: dims.width, height: dims.height, radius: dims.radius } }, [
1094
+ rect({ id: `${id}-screenbg`, x: 0, y: 0, anchor: "center", width: dims.width, height: dims.height, fill: o.screen ?? p.screen }),
1095
+ group({ id: `${id}-content`, x: 0, y: 0 }, content)
1096
+ ]);
1097
+ }
1098
+ function buildDevice(name, id, p, o, content) {
1099
+ const dims = screenDims(name, o);
1100
+ const sw = dims.width;
1101
+ const sh = dims.height;
1102
+ const screen = () => screenGroup(id, p, o, dims.cx, dims.cy, dims, content);
1103
+ switch (name) {
1104
+ case "phone":
1105
+ case "tablet": {
1106
+ const bezel = name === "phone" ? 20 : 28;
1107
+ const bodyW = sw + bezel * 2;
1108
+ const bodyH = sh + bezel * 2;
1109
+ const bodyR = name === "phone" ? 54 : 34;
1110
+ const land = isLandscape(name, o);
1111
+ const nodes = [
1112
+ rect({ id: `${id}-body`, x: 0, y: 0, anchor: "center", width: bodyW, height: bodyH, fill: p.body, stroke: p.bodyStroke, strokeWidth: 2, radius: bodyR }),
1113
+ screen()
1114
+ ];
1115
+ if (name === "phone") {
1116
+ nodes.push(
1117
+ land ? rect({ id: `${id}-notch`, x: -sw / 2 + 16, y: 0, anchor: "center", width: 30, height: 96, fill: "#000000", radius: 15 }) : rect({ id: `${id}-notch`, x: 0, y: -sh / 2 + 16, anchor: "center", width: 96, height: 30, fill: "#000000", radius: 15 }),
1118
+ land ? rect({ id: `${id}-home`, x: sw / 2 - 4, y: 0, anchor: "center", width: 5, height: 120, fill: p.detail, radius: 3 }) : rect({ id: `${id}-home`, x: 0, y: sh / 2 - 18, anchor: "center", width: 120, height: 5, fill: p.detail, radius: 3 })
1119
+ );
1120
+ if (!land) {
1121
+ nodes.push(
1122
+ rect({ id: `${id}-pwr`, x: bodyW / 2, y: -bodyH * 0.1, anchor: "center", width: 4, height: 78, fill: p.detail, radius: 2 }),
1123
+ rect({ id: `${id}-volup`, x: -bodyW / 2, y: -bodyH * 0.16, anchor: "center", width: 4, height: 48, fill: p.detail, radius: 2 }),
1124
+ rect({ id: `${id}-voldn`, x: -bodyW / 2, y: -bodyH * 0.16 + 60, anchor: "center", width: 4, height: 48, fill: p.detail, radius: 2 })
1125
+ );
1126
+ }
1127
+ } else {
1128
+ nodes.push(
1129
+ rect({ id: `${id}-camera`, x: land ? -sw / 2 - 14 : 0, y: land ? 0 : -sh / 2 - 14, anchor: "center", width: 8, height: 8, fill: p.detail, radius: 4 }),
1130
+ rect({ id: `${id}-pwr`, x: land ? -bodyW * 0.18 : bodyW * 0.18, y: land ? -bodyH / 2 : -bodyH / 2, anchor: "center", width: 60, height: 4, fill: p.detail, radius: 2 })
1131
+ );
1132
+ }
1133
+ return nodes;
1134
+ }
1135
+ case "laptop": {
1136
+ const lidTop = dims.cy - (sh + 40) / 2;
1137
+ const keyRows = [0, 1, 2, 3].map(
1138
+ (r) => rect({ id: `${id}-keys${r}`, x: 0, y: 150 + r * 11, anchor: "center", width: 640 + r * 50, height: 6, fill: p.chrome, radius: 3 })
1139
+ );
1140
+ return [
1141
+ path({ id: `${id}-base`, x: 0, y: 0, d: "M -450 140 L 450 140 L 520 196 L -520 196 Z", fill: p.body, stroke: p.bodyStroke, strokeWidth: 2 }),
1142
+ rect({ id: `${id}-foot-l`, x: -360, y: 198, anchor: "center", width: 70, height: 5, fill: p.detail, radius: 3 }),
1143
+ rect({ id: `${id}-foot-r`, x: 360, y: 198, anchor: "center", width: 70, height: 5, fill: p.detail, radius: 3 }),
1144
+ ...keyRows,
1145
+ rect({ id: `${id}-trackpad`, x: 0, y: 184, anchor: "center", width: 150, height: 8, fill: p.detail, radius: 4 }),
1146
+ rect({ id: `${id}-hinge`, x: 0, y: 134, anchor: "center", width: 900, height: 10, fill: p.detail, radius: 5 }),
1147
+ screen(),
1148
+ ellipse({ id: `${id}-webcam`, x: 0, y: lidTop + 14, anchor: "center", width: 6, height: 6, fill: p.detail }),
1149
+ rect({ id: `${id}-lid`, x: 0, y: dims.cy, anchor: "center", width: sw + 40, height: sh + 40, stroke: p.bodyStroke, strokeWidth: 2, radius: 18 })
1150
+ ];
1151
+ }
1152
+ case "browser": {
1153
+ const winW = sw + 16;
1154
+ const winH = sh + 92;
1155
+ const barY = -winH / 2 + 24;
1156
+ return [
1157
+ rect({ id: `${id}-win`, x: 0, y: 0, anchor: "center", width: winW, height: winH, fill: p.chrome, stroke: p.bodyStroke, strokeWidth: 1.5, radius: 14 }),
1158
+ ellipse({ id: `${id}-dot1`, x: -winW / 2 + 30, y: barY, anchor: "center", width: 13, height: 13, fill: "#FF5F57" }),
1159
+ ellipse({ id: `${id}-dot2`, x: -winW / 2 + 54, y: barY, anchor: "center", width: 13, height: 13, fill: "#FEBC2E" }),
1160
+ ellipse({ id: `${id}-dot3`, x: -winW / 2 + 78, y: barY, anchor: "center", width: 13, height: 13, fill: "#28C840" }),
1161
+ // an active tab tucked under the lights
1162
+ rect({ id: `${id}-tab`, x: -winW / 2 + 230, y: barY, anchor: "center", width: 190, height: 30, fill: o.screen ?? p.screen, radius: 8 }),
1163
+ text({ id: `${id}-tabtext`, x: -winW / 2 + 156, y: barY, anchor: "center-left", content: "Overview", fontFamily: "Inter", fontSize: 13, fill: p.chromeText }),
1164
+ rect({ id: `${id}-urlpill`, x: 96, y: barY, anchor: "center", width: 700, height: 26, fill: o.screen ?? p.screen, stroke: p.bodyStroke, strokeWidth: 1, radius: 13 }),
1165
+ rect({ id: `${id}-lock`, x: 96 - 330, y: barY, anchor: "center", width: 8, height: 10, fill: p.chromeText, radius: 2 }),
1166
+ text({ id: `${id}-urltext`, x: 96 - 312, y: barY, anchor: "center-left", content: urlText(o.url), fontFamily: "Inter", fontSize: 14, fill: p.chromeText }),
1167
+ screen()
1168
+ ];
1169
+ }
1170
+ case "watch": {
1171
+ const bw = sw + 36;
1172
+ const bh = sh + 36;
1173
+ return [
1174
+ // straps (drawn behind the body) flaring out top & bottom
1175
+ path({ id: `${id}-bandtop`, x: 0, y: -bh / 2 + 4, d: "M -78 0 L 78 0 L 64 -86 L -64 -86 Z", fill: p.body, stroke: p.bodyStroke, strokeWidth: 2 }),
1176
+ path({ id: `${id}-bandbot`, x: 0, y: bh / 2 - 4, d: "M -78 0 L 78 0 L 64 86 L -64 86 Z", fill: p.body, stroke: p.bodyStroke, strokeWidth: 2 }),
1177
+ rect({ id: `${id}-body`, x: 0, y: 0, anchor: "center", width: bw, height: bh, fill: p.body, stroke: p.bodyStroke, strokeWidth: 3, radius: 60 }),
1178
+ screen(),
1179
+ rect({ id: `${id}-crown`, x: bw / 2, y: -20, anchor: "center", width: 14, height: 40, fill: p.detail, radius: 6 }),
1180
+ rect({ id: `${id}-button`, x: bw / 2 - 2, y: 40, anchor: "center", width: 8, height: 34, fill: p.detail, radius: 4 })
1181
+ ];
1182
+ }
1183
+ case "monitor": {
1184
+ const panelW = sw + 44;
1185
+ const panelH = sh + 60;
1186
+ return [
1187
+ rect({ id: `${id}-panel`, x: 0, y: 0, anchor: "center", width: panelW, height: panelH, fill: p.body, stroke: p.bodyStroke, strokeWidth: 2, radius: 16 }),
1188
+ screen(),
1189
+ ellipse({ id: `${id}-led`, x: panelW / 2 - 26, y: panelH / 2 - 16, anchor: "center", width: 6, height: 6, fill: "#28C840" }),
1190
+ rect({ id: `${id}-neck`, x: 0, y: panelH / 2 + 60, anchor: "center", width: 60, height: 120, fill: p.body }),
1191
+ path({ id: `${id}-stand`, x: 0, y: panelH / 2 + 60, d: "M -160 50 L 160 50 L 220 80 L -220 80 Z", fill: p.body, stroke: p.bodyStroke, strokeWidth: 2 })
1192
+ ];
1193
+ }
1194
+ case "tv": {
1195
+ const panelW = sw + 44;
1196
+ const panelH = sh + 48;
1197
+ const panelBottom = dims.cy + panelH / 2;
1198
+ return [
1199
+ rect({ id: `${id}-panel`, x: 0, y: dims.cy, anchor: "center", width: panelW, height: panelH, fill: p.body, stroke: p.bodyStroke, strokeWidth: 2, radius: 12 }),
1200
+ screen(),
1201
+ ellipse({ id: `${id}-brand`, x: 0, y: panelBottom - 12, anchor: "center", width: 6, height: 6, fill: p.detail }),
1202
+ rect({ id: `${id}-neck`, x: 0, y: panelBottom + 48, anchor: "center", width: 64, height: 96, fill: p.body }),
1203
+ path({ id: `${id}-stand`, x: 0, y: panelBottom + 96, d: "M -210 0 L 210 0 L 270 34 L -270 34 Z", fill: p.body, stroke: p.bodyStroke, strokeWidth: 2 })
1204
+ ];
1205
+ }
1206
+ case "foldable": {
1207
+ const bodyW = sw + 40;
1208
+ const bodyH = sh + 40;
1209
+ return [
1210
+ rect({ id: `${id}-hinge-l`, x: -bodyW / 2, y: 0, anchor: "center", width: 8, height: bodyH * 0.5, fill: p.detail, radius: 4 }),
1211
+ rect({ id: `${id}-hinge-r`, x: bodyW / 2, y: 0, anchor: "center", width: 8, height: bodyH * 0.5, fill: p.detail, radius: 4 }),
1212
+ rect({ id: `${id}-body`, x: 0, y: 0, anchor: "center", width: bodyW, height: bodyH, fill: p.body, stroke: p.bodyStroke, strokeWidth: 2, radius: 28 }),
1213
+ screen(),
1214
+ rect({ id: `${id}-crease`, x: 0, y: 0, anchor: "center", width: 4, height: sh, fill: p.bodyStroke, radius: 2, opacity: 0.5 }),
1215
+ ellipse({ id: `${id}-cam1`, x: -10, y: -sh / 2 + 18, anchor: "center", width: 8, height: 8, fill: p.detail }),
1216
+ ellipse({ id: `${id}-cam2`, x: 10, y: -sh / 2 + 18, anchor: "center", width: 8, height: 8, fill: p.detail })
1217
+ ];
1218
+ }
1219
+ case "terminal": {
1220
+ const winW = sw + 16;
1221
+ const winH = sh + 76;
1222
+ return [
1223
+ rect({ id: `${id}-win`, x: 0, y: 0, anchor: "center", width: winW, height: winH, fill: p.chrome, stroke: p.bodyStroke, strokeWidth: 1.5, radius: 12 }),
1224
+ ellipse({ id: `${id}-dot1`, x: -winW / 2 + 28, y: -winH / 2 + 22, anchor: "center", width: 12, height: 12, fill: "#FF5F57" }),
1225
+ ellipse({ id: `${id}-dot2`, x: -winW / 2 + 50, y: -winH / 2 + 22, anchor: "center", width: 12, height: 12, fill: "#FEBC2E" }),
1226
+ ellipse({ id: `${id}-dot3`, x: -winW / 2 + 72, y: -winH / 2 + 22, anchor: "center", width: 12, height: 12, fill: "#28C840" }),
1227
+ rect({ id: `${id}-tab`, x: -winW / 2 + 170, y: -winH / 2 + 22, anchor: "center", width: 130, height: 24, fill: o.screen ?? p.screen, radius: 6 }),
1228
+ text({ id: `${id}-title`, x: -winW / 2 + 170, y: -winH / 2 + 22, anchor: "center", content: urlText(o.url ?? "zsh"), fontFamily: "Inter", fontSize: 13, fill: p.chromeText }),
1229
+ screen()
1230
+ ];
1231
+ }
1232
+ case "car": {
1233
+ const bodyW = sw + 60;
1234
+ const bodyH = sh + 60;
1235
+ return [
1236
+ rect({ id: `${id}-body`, x: 0, y: 0, anchor: "center", width: bodyW, height: bodyH, fill: p.body, stroke: p.bodyStroke, strokeWidth: 2, radius: 40 }),
1237
+ ellipse({ id: `${id}-knob`, x: -bodyW / 2 + 18, y: 0, anchor: "center", width: 22, height: 22, fill: p.body, stroke: p.detail, strokeWidth: 3 }),
1238
+ screen(),
1239
+ ellipse({ id: `${id}-btn1`, x: -44, y: sh / 2 + 16, anchor: "center", width: 12, height: 12, fill: p.detail }),
1240
+ ellipse({ id: `${id}-btn2`, x: 0, y: sh / 2 + 16, anchor: "center", width: 12, height: 12, fill: p.detail }),
1241
+ ellipse({ id: `${id}-btn3`, x: 44, y: sh / 2 + 16, anchor: "center", width: 12, height: 12, fill: p.detail })
1242
+ ];
1243
+ }
1244
+ }
1245
+ }
1246
+ var urlText = (url) => {
1247
+ const u = url ?? "reframe.video";
1248
+ return u.length > 70 ? `${u.slice(0, 67)}\u2026` : u;
1249
+ };
1250
+ function devicePreset(name, opts = {}) {
1251
+ const id = opts.id ?? "device";
1252
+ const p = opts.color === "light" ? LIGHT : DARK;
1253
+ const children = buildDevice(name, id, p, opts, opts.content ?? []);
1254
+ return group(
1255
+ {
1256
+ id,
1257
+ x: opts.x ?? 0,
1258
+ y: opts.y ?? 0,
1259
+ ...opts.scale !== void 0 && opts.scale !== 1 && { scale: opts.scale },
1260
+ ...opts.opacity !== void 0 && opts.opacity !== 1 && { opacity: opts.opacity }
1261
+ },
1262
+ children
1263
+ );
1264
+ }
1265
+
1266
+ // ../core/src/motionOps.ts
1267
+ var MOTION_OPS = ["rotate", "zoom", "ken-burns", "slide-in", "fade", "draw-on", "pulse"];
1268
+ var clamp012 = (n) => Math.max(0, Math.min(1, n));
1269
+ function settleEase2(e) {
1270
+ return e < 0.34 ? "easeOutCubic" : e < 0.67 ? "easeOutBack" : "easeOutElastic";
1271
+ }
1272
+ function fromVec2(from, dist) {
1273
+ switch (from) {
1274
+ case "right":
1275
+ return [dist, 0];
1276
+ case "top":
1277
+ return [0, -dist];
1278
+ case "bottom":
1279
+ return [0, dist];
1280
+ default:
1281
+ return [-dist, 0];
1282
+ }
1283
+ }
1284
+ var motionOpLabel = (name, target) => `op-${name}-${target}`;
1285
+ function motionOp(name, target, opts = {}) {
1286
+ const e = clamp012(opts.energy ?? 0.5);
1287
+ const sp = Math.max(0.25, opts.speed ?? 1);
1288
+ const amt = opts.amount ?? 1;
1289
+ const b = { scale: 1, x: 0, y: 0, rotation: 0, ...opts.base };
1290
+ const d = (base) => base / sp;
1291
+ const label = motionOpLabel(name, target);
1292
+ switch (name) {
1293
+ case "rotate":
1294
+ return { timeline: beat(label, {}, [tween(target, { rotation: b.rotation + 360 * amt }, { duration: d(1), ease: settleEase2(e) })]) };
1295
+ case "zoom": {
1296
+ const peak = b.scale * (1 + 0.22 * amt);
1297
+ return {
1298
+ timeline: beat(label, {}, [
1299
+ seq(
1300
+ tween(target, { scale: peak }, { duration: d(0.4), ease: "easeOutBack" }),
1301
+ tween(target, { scale: b.scale }, { duration: d(0.45), ease: "easeInOutQuad" })
1302
+ )
1303
+ ])
1304
+ };
1305
+ }
1306
+ case "ken-burns":
1307
+ return {
1308
+ timeline: beat(label, {}, [
1309
+ par(
1310
+ tween(target, { scale: b.scale * (1 + 0.1 * amt) }, { duration: d(3), ease: "easeInOutQuad" }),
1311
+ tween(target, { x: b.x + 26 * amt, y: b.y - 16 * amt }, { duration: d(3), ease: "easeInOutQuad" })
1312
+ )
1313
+ ])
1314
+ };
1315
+ case "slide-in": {
1316
+ const [dx, dy] = fromVec2(opts.from ?? "left", 320 * amt);
1317
+ return {
1318
+ setup: { [target]: { x: b.x + dx, y: b.y + dy, opacity: 0 } },
1319
+ timeline: beat(label, {}, [
1320
+ par(
1321
+ tween(target, { x: b.x, y: b.y }, { duration: d(0.7), ease: settleEase2(e) }),
1322
+ tween(target, { opacity: 1 }, { duration: d(0.4), ease: "easeOutQuad" })
1323
+ )
1324
+ ])
1325
+ };
1326
+ }
1327
+ case "fade":
1328
+ return {
1329
+ setup: { [target]: { opacity: 0 } },
1330
+ timeline: beat(label, {}, [tween(target, { opacity: 1 }, { duration: d(0.6), ease: "easeOutQuad" })])
1331
+ };
1332
+ case "draw-on":
1333
+ return {
1334
+ setup: { [target]: { progress: 0 } },
1335
+ timeline: beat(label, {}, [tween(target, { progress: 1 }, { duration: d(1.3), ease: "easeInOutQuad" })])
1336
+ };
1337
+ case "pulse": {
1338
+ const hi = b.scale * (1 + 0.12 * amt);
1339
+ const pulses = 2 + Math.round(amt);
1340
+ const steps = [];
1341
+ for (let i = 0; i < pulses; i++) {
1342
+ steps.push(tween(target, { scale: hi }, { duration: d(0.22), ease: "easeOutQuad" }));
1343
+ steps.push(tween(target, { scale: b.scale }, { duration: d(0.22), ease: "easeInQuad" }));
1344
+ }
1345
+ return { timeline: beat(label, {}, [seq(...steps)]) };
1346
+ }
1347
+ }
1348
+ }
1349
+
887
1350
  // ../core/src/audio.ts
888
1351
  var SFX_DURATION = {
889
1352
  whoosh: 0.35,
@@ -929,6 +1392,15 @@ function resolveAudioPlan(compiled) {
929
1392
  });
930
1393
  }
931
1394
  cues.sort((a, b) => a.t - b.t);
1395
+ return {
1396
+ duration,
1397
+ bgm: resolveBgm(audio.bgm),
1398
+ cues,
1399
+ duckWindows: mergeDuckWindows(cues, duration),
1400
+ warnings
1401
+ };
1402
+ }
1403
+ function mergeDuckWindows(cues, duration) {
932
1404
  const duckWindows = [];
933
1405
  for (const cue of cues) {
934
1406
  const window = { t0: cue.t, t1: Math.min(duration, cue.t + cue.duration) };
@@ -936,23 +1408,68 @@ function resolveAudioPlan(compiled) {
936
1408
  if (last && window.t0 <= last.t1 + 0.1) last.t1 = Math.max(last.t1, window.t1);
937
1409
  else duckWindows.push(window);
938
1410
  }
939
- let bgm = null;
940
- if (audio.bgm) {
941
- const b = audio.bgm;
942
- const duck = b.duck === false ? null : {
943
- depth: b.duck?.depth ?? 0.5,
944
- attack: b.duck?.attack ?? 0.05,
945
- release: b.duck?.release ?? 0.25
946
- };
947
- bgm = {
948
- source: b.file ? { kind: "file", path: b.file } : { kind: "synth", name: b.synth ?? "ambient-pad" },
949
- gain: b.gain ?? 0.5,
950
- fadeIn: b.fadeIn ?? 0,
951
- fadeOut: b.fadeOut ?? 0,
952
- duck
953
- };
1411
+ return duckWindows;
1412
+ }
1413
+ function resolveBgm(b) {
1414
+ if (!b) return null;
1415
+ const duck = b.duck === false ? null : {
1416
+ depth: b.duck?.depth ?? 0.5,
1417
+ attack: b.duck?.attack ?? 0.05,
1418
+ release: b.duck?.release ?? 0.25
1419
+ };
1420
+ return {
1421
+ source: b.file ? { kind: "file", path: b.file } : { kind: "synth", name: b.synth ?? "ambient-pad" },
1422
+ gain: b.gain ?? 0.5,
1423
+ fadeIn: b.fadeIn ?? 0,
1424
+ fadeOut: b.fadeOut ?? 0,
1425
+ duck
1426
+ };
1427
+ }
1428
+ function resolveCompositionAudioPlan(comp) {
1429
+ const audio = comp.ir.audio;
1430
+ const duration = comp.duration;
1431
+ const warnings = [];
1432
+ const cues = [];
1433
+ for (const placement of comp.scenes) {
1434
+ const plan = resolveAudioPlan(placement.compiled);
1435
+ if (!plan) continue;
1436
+ if (plan.bgm) {
1437
+ warnings.push(`scene "${placement.id}": per-scene bgm ignored \u2014 set bgm at the composition level`);
1438
+ }
1439
+ for (const w of plan.warnings) warnings.push(`scene "${placement.id}": ${w}`);
1440
+ for (const cue of plan.cues) {
1441
+ const t = cue.t + placement.start;
1442
+ if (t >= duration) continue;
1443
+ cues.push({ ...cue, t });
1444
+ }
1445
+ }
1446
+ for (const [index, cue] of (audio?.cues ?? []).entries()) {
1447
+ if (typeof cue.at !== "number") {
1448
+ warnings.push(`composition cue[${index}]: "at" must be an absolute number (no composition labels) \u2014 dropped`);
1449
+ continue;
1450
+ }
1451
+ const t = Math.max(0, cue.at + (cue.offset ?? 0));
1452
+ const cueDuration = cue.sfx ? SFX_DURATION[cue.sfx] : FILE_CUE_DURATION;
1453
+ if (t >= duration) {
1454
+ warnings.push(`composition cue[${index}] at ${t.toFixed(2)}s past the composition end \u2014 dropped`);
1455
+ continue;
1456
+ }
1457
+ cues.push({
1458
+ t,
1459
+ gain: cue.gain ?? 1,
1460
+ duration: cueDuration,
1461
+ source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
1462
+ });
954
1463
  }
955
- return { duration, bgm, cues, duckWindows, warnings };
1464
+ if (!audio?.bgm && cues.length === 0) return null;
1465
+ cues.sort((a, b) => a.t - b.t);
1466
+ return {
1467
+ duration,
1468
+ bgm: resolveBgm(audio?.bgm),
1469
+ cues,
1470
+ duckWindows: mergeDuckWindows(cues, duration),
1471
+ warnings
1472
+ };
956
1473
  }
957
1474
 
958
1475
  // ../core/src/behaviors.ts
@@ -1106,11 +1623,22 @@ function multiply(m, n) {
1106
1623
  m[1] * n[4] + m[3] * n[5] + m[5]
1107
1624
  ];
1108
1625
  }
1109
- function localMatrix(x, y, rotationDeg, scale) {
1626
+ function localMatrix(x, y, rotationDeg, scale, scaleX = 1, scaleY = 1, skewXDeg = 0, skewYDeg = 0) {
1110
1627
  const r = rotationDeg * Math.PI / 180;
1111
- const cos = Math.cos(r) * scale;
1112
- const sin = Math.sin(r) * scale;
1113
- return [cos, sin, -sin, cos, x, y];
1628
+ if (scaleX === 1 && scaleY === 1 && skewXDeg === 0 && skewYDeg === 0) {
1629
+ const cos = Math.cos(r) * scale;
1630
+ const sin = Math.sin(r) * scale;
1631
+ return [cos, sin, -sin, cos, x, y];
1632
+ }
1633
+ const c = Math.cos(r);
1634
+ const s = Math.sin(r);
1635
+ const tx = Math.tan(skewXDeg * Math.PI / 180);
1636
+ const ty = Math.tan(skewYDeg * Math.PI / 180);
1637
+ const R = [c, s, -s, c, 0, 0];
1638
+ const K = [1, ty, tx, 1, 0, 0];
1639
+ const S = [scale * scaleX, 0, 0, scale * scaleY, 0, 0];
1640
+ const m = multiply(R, multiply(K, S));
1641
+ return [m[0], m[1], m[2], m[3], x, y];
1114
1642
  }
1115
1643
  var ANCHOR_FACTORS = {
1116
1644
  "top-left": [0, 0],
@@ -1135,53 +1663,86 @@ function behaviorEnvelope(b, t) {
1135
1663
  if (Number.isFinite(until) && ramp > 0) envelope = Math.min(envelope, (until - t) / ramp);
1136
1664
  return Math.max(0, Math.min(1, envelope));
1137
1665
  }
1138
- function evaluate(compiled, t) {
1139
- const ops = [];
1140
- const valueAt = (target, prop, fallback) => {
1141
- let value = compiled.initialValues.get(`${target}.${prop}`) ?? fallback;
1142
- let segStart = Number.NEGATIVE_INFINITY;
1143
- const segs = compiled.segments.get(`${target}.${prop}`);
1144
- if (segs) {
1666
+ function sampleProp(compiled, t, target, prop, fallback) {
1667
+ let value = compiled.initialValues.get(`${target}.${prop}`) ?? fallback;
1668
+ let segStart = Number.NEGATIVE_INFINITY;
1669
+ const segs = compiled.segments.get(`${target}.${prop}`);
1670
+ if (segs) {
1671
+ let active;
1672
+ for (const seg of segs) {
1673
+ if (seg.t0 <= t) active = seg;
1674
+ else break;
1675
+ }
1676
+ if (active) {
1677
+ segStart = active.t0;
1678
+ if (t >= active.t1) {
1679
+ value = active.to;
1680
+ } else {
1681
+ const u = resolveEase(active.ease)((t - active.t0) / (active.t1 - active.t0));
1682
+ value = lerpValue(active.from, active.to, u);
1683
+ }
1684
+ }
1685
+ }
1686
+ if (prop === "x" || prop === "y" || prop === "rotation") {
1687
+ const drivers = compiled.motionPaths.get(target);
1688
+ if (drivers) {
1145
1689
  let active;
1146
- for (const seg of segs) {
1147
- if (seg.t0 <= t) active = seg;
1690
+ for (const d of drivers) {
1691
+ if (d.t0 <= t) active = d;
1148
1692
  else break;
1149
1693
  }
1150
- if (active) {
1151
- segStart = active.t0;
1152
- if (t >= active.t1) {
1153
- value = active.to;
1154
- } else {
1155
- const u = resolveEase(active.ease)((t - active.t0) / (active.t1 - active.t0));
1156
- value = lerpValue(active.from, active.to, u);
1157
- }
1694
+ if (active && active.t0 >= segStart && (prop !== "rotation" || active.autoRotate) && active.points.length > 0) {
1695
+ const span = active.t1 - active.t0;
1696
+ const u = span <= 0 ? 1 : resolveEase(active.ease)(Math.max(0, Math.min(1, (t - active.t0) / span)));
1697
+ if (prop === "x") value = pathPoint(active.points, active.closed, u, active.curviness)[0];
1698
+ else if (prop === "y") value = pathPoint(active.points, active.closed, u, active.curviness)[1];
1699
+ else value = pathTangentAngle(active.points, active.closed, u, active.curviness) + active.rotateOffset;
1158
1700
  }
1159
1701
  }
1160
- if (prop === "x" || prop === "y" || prop === "rotation") {
1161
- const drivers = compiled.motionPaths.get(target);
1162
- if (drivers) {
1163
- let active;
1164
- for (const d of drivers) {
1165
- if (d.t0 <= t) active = d;
1166
- else break;
1167
- }
1168
- if (active && active.t0 >= segStart && (prop !== "rotation" || active.autoRotate) && active.points.length > 0) {
1169
- const span = active.t1 - active.t0;
1170
- const u = span <= 0 ? 1 : resolveEase(active.ease)(Math.max(0, Math.min(1, (t - active.t0) / span)));
1171
- if (prop === "x") value = pathPoint(active.points, active.closed, u)[0];
1172
- else if (prop === "y") value = pathPoint(active.points, active.closed, u)[1];
1173
- else value = pathTangentAngle(active.points, active.closed, u) + active.rotateOffset;
1174
- }
1175
- }
1702
+ }
1703
+ for (const b of compiled.ir.behaviors ?? []) {
1704
+ if (b.target === target && b.prop === prop && typeof value === "number") {
1705
+ const envelope = behaviorEnvelope(b, t);
1706
+ if (envelope > 0) value = value + envelope * sampleBehavior(b.behavior, t);
1176
1707
  }
1177
- for (const b of compiled.ir.behaviors ?? []) {
1178
- if (b.target === target && b.prop === prop && typeof value === "number") {
1179
- const envelope = behaviorEnvelope(b, t);
1180
- if (envelope > 0) value = value + envelope * sampleBehavior(b.behavior, t);
1181
- }
1708
+ }
1709
+ return value;
1710
+ }
1711
+ function nodeParentMatrix(compiled, id, t) {
1712
+ const num = (target, prop, fallback) => {
1713
+ const v = sampleProp(compiled, t, target, prop, fallback);
1714
+ return typeof v === "number" ? v : fallback;
1715
+ };
1716
+ let result = null;
1717
+ const walk = (node, parent) => {
1718
+ if (node.id === id) {
1719
+ result = parent;
1720
+ return true;
1721
+ }
1722
+ if (node.type === "group") {
1723
+ const m = multiply(
1724
+ parent,
1725
+ localMatrix(
1726
+ num(node.id, "x", node.props.x),
1727
+ num(node.id, "y", node.props.y),
1728
+ num(node.id, "rotation", node.props.rotation ?? 0),
1729
+ num(node.id, "scale", node.props.scale ?? 1),
1730
+ num(node.id, "scaleX", node.props.scaleX ?? 1),
1731
+ num(node.id, "scaleY", node.props.scaleY ?? 1),
1732
+ num(node.id, "skewX", node.props.skewX ?? 0),
1733
+ num(node.id, "skewY", node.props.skewY ?? 0)
1734
+ )
1735
+ );
1736
+ for (const child of node.children) if (walk(child, m)) return true;
1182
1737
  }
1183
- return value;
1738
+ return false;
1184
1739
  };
1740
+ for (const node of compiled.ir.nodes) if (walk(node, IDENTITY)) break;
1741
+ return result;
1742
+ }
1743
+ function evaluate(compiled, t) {
1744
+ const ops = [];
1745
+ const valueAt = (target, prop, fallback) => sampleProp(compiled, t, target, prop, fallback);
1185
1746
  const num = (target, prop, fallback) => {
1186
1747
  const v = valueAt(target, prop, fallback);
1187
1748
  return typeof v === "number" ? v : fallback;
@@ -1194,8 +1755,9 @@ function evaluate(compiled, t) {
1194
1755
  const v = valueAt(target, prop, base ?? "");
1195
1756
  return v === "" && base === void 0 ? void 0 : String(v);
1196
1757
  };
1197
- const walk = (node, parent, parentOpacity) => {
1758
+ const walk = (node, parent, parentOpacity, clips) => {
1198
1759
  const id = node.id;
1760
+ const clipSpread = clips.length > 0 ? { clips } : void 0;
1199
1761
  if (node.type === "line") {
1200
1762
  const opacity2 = parentOpacity * num(id, "opacity", node.props.opacity ?? 1);
1201
1763
  if (opacity2 <= 0) return;
@@ -1212,7 +1774,8 @@ function evaluate(compiled, t) {
1212
1774
  x2: x1 + (num(id, "x2", node.props.x2) - x1) * progress,
1213
1775
  y2: y1 + (num(id, "y2", node.props.y2) - y1) * progress,
1214
1776
  stroke: str(id, "stroke", node.props.stroke),
1215
- strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1)
1777
+ strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1),
1778
+ ...clipSpread
1216
1779
  });
1217
1780
  return;
1218
1781
  }
@@ -1224,13 +1787,19 @@ function evaluate(compiled, t) {
1224
1787
  num(id, "x", node.props.x),
1225
1788
  num(id, "y", node.props.y),
1226
1789
  num(id, "rotation", node.props.rotation ?? 0),
1227
- num(id, "scale", node.props.scale ?? 1)
1790
+ num(id, "scale", node.props.scale ?? 1),
1791
+ num(id, "scaleX", node.props.scaleX ?? 1),
1792
+ num(id, "scaleY", node.props.scaleY ?? 1),
1793
+ num(id, "skewX", node.props.skewX ?? 0),
1794
+ num(id, "skewY", node.props.skewY ?? 0)
1228
1795
  )
1229
1796
  );
1230
1797
  switch (node.type) {
1231
- case "group":
1232
- for (const child of node.children) walk(child, matrix, opacity);
1798
+ case "group": {
1799
+ const childClips = node.props.clip ? [...clips, { transform: matrix, shape: node.props.clip }] : clips;
1800
+ for (const child of node.children) walk(child, matrix, opacity, childClips);
1233
1801
  return;
1802
+ }
1234
1803
  case "rect":
1235
1804
  case "ellipse": {
1236
1805
  const width = num(id, "width", node.props.width);
@@ -1250,7 +1819,8 @@ function evaluate(compiled, t) {
1250
1819
  offsetY: -height * ay,
1251
1820
  ...fill !== void 0 && { fill },
1252
1821
  ...stroke !== void 0 && { stroke, strokeWidth },
1253
- ...node.type === "rect" && { radius: num(id, "radius", node.props.radius ?? 0) }
1822
+ ...node.type === "rect" && { radius: num(id, "radius", node.props.radius ?? 0) },
1823
+ ...clipSpread
1254
1824
  });
1255
1825
  return;
1256
1826
  }
@@ -1267,7 +1837,8 @@ function evaluate(compiled, t) {
1267
1837
  width,
1268
1838
  height,
1269
1839
  offsetX: -width * ax,
1270
- offsetY: -height * ay
1840
+ offsetY: -height * ay,
1841
+ ...clipSpread
1271
1842
  });
1272
1843
  return;
1273
1844
  }
@@ -1284,7 +1855,8 @@ function evaluate(compiled, t) {
1284
1855
  d: str(id, "d", node.props.d),
1285
1856
  progress: Math.max(0, Math.min(1, num(id, "progress", node.props.progress ?? 1))),
1286
1857
  ...fill !== void 0 && { fill },
1287
- ...stroke !== void 0 && { stroke, strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1) }
1858
+ ...stroke !== void 0 && { stroke, strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1) },
1859
+ ...clipSpread
1288
1860
  });
1289
1861
  return;
1290
1862
  }
@@ -1307,13 +1879,14 @@ function evaluate(compiled, t) {
1307
1879
  fill: str(id, "fill", node.props.fill ?? "#ffffff"),
1308
1880
  letterSpacing: num(id, "letterSpacing", node.props.letterSpacing ?? 0),
1309
1881
  align: TEXT_ALIGN[ax] ?? "left",
1310
- baseline: TEXT_BASELINE[ay] ?? "top"
1882
+ baseline: TEXT_BASELINE[ay] ?? "top",
1883
+ ...clipSpread
1311
1884
  });
1312
1885
  return;
1313
1886
  }
1314
1887
  }
1315
1888
  };
1316
- for (const node of compiled.ir.nodes) walk(node, IDENTITY, 1);
1889
+ for (const node of compiled.ir.nodes) walk(node, IDENTITY, 1, []);
1317
1890
  return ops;
1318
1891
  }
1319
1892
 
@@ -1394,19 +1967,28 @@ function sketchToTimeline(sketch, nodeIds) {
1394
1967
  return par(...steps);
1395
1968
  }
1396
1969
  export {
1970
+ DEFAULT_CROSSFADE,
1397
1971
  DEFAULT_FPS,
1398
1972
  DEFAULT_MOTIONPATH_DURATION,
1399
1973
  DEFAULT_TO_DURATION,
1400
1974
  DEFAULT_TWEEN_DURATION,
1975
+ DEVICE_PRESET_NAMES,
1401
1976
  EASE_NAMES,
1977
+ MOTION_OPS,
1402
1978
  PRESET_NAMES,
1403
1979
  PROPS_BY_TYPE,
1404
1980
  SFX_DURATION,
1405
1981
  SceneValidationError,
1406
1982
  beat,
1407
1983
  collectImageSrcs,
1984
+ compileComposition,
1408
1985
  compileScene,
1409
1986
  composeScene,
1987
+ composition,
1988
+ deviceBounds,
1989
+ devicePreset,
1990
+ deviceScreen,
1991
+ deviceScreenCenter,
1410
1992
  ellipse,
1411
1993
  evaluate,
1412
1994
  formatComposeReport,
@@ -1415,8 +1997,11 @@ export {
1415
1997
  isColor,
1416
1998
  lerpValue,
1417
1999
  line,
2000
+ motionOp,
2001
+ motionOpLabel,
1418
2002
  motionPath,
1419
2003
  motionPreset,
2004
+ nodeParentMatrix,
1420
2005
  oscillate,
1421
2006
  par,
1422
2007
  path,
@@ -1424,8 +2009,10 @@ export {
1424
2009
  pathTangentAngle,
1425
2010
  rect,
1426
2011
  resolveAudioPlan,
2012
+ resolveCompositionAudioPlan,
1427
2013
  resolveEase,
1428
2014
  sampleBehavior,
2015
+ sampleProp,
1429
2016
  scene,
1430
2017
  seq,
1431
2018
  sketchToTimeline,
@@ -1433,6 +2020,7 @@ export {
1433
2020
  text,
1434
2021
  to,
1435
2022
  tween,
2023
+ validateComposition,
1436
2024
  validateScene,
1437
2025
  wait,
1438
2026
  wiggle