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/bin.js CHANGED
@@ -42,7 +42,7 @@ function segCountOf(points, closed) {
42
42
  if (n < 2) return 0;
43
43
  return closed ? n : n - 1;
44
44
  }
45
- function pathPoint(points, closed, u) {
45
+ function pathPoint(points, closed, u, curviness = 1) {
46
46
  const n = points.length;
47
47
  if (n === 0) return [0, 0];
48
48
  if (n === 1) return [points[0][0], points[0][1]];
@@ -51,19 +51,41 @@ function pathPoint(points, closed, u) {
51
51
  const [p0, p1, p2, p3] = controls(points, closed, i);
52
52
  const t2 = t * t;
53
53
  const t3 = t2 * t;
54
- 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);
55
- return [f(p0[0], p1[0], p2[0], p3[0]), f(p0[1], p1[1], p2[1], p3[1])];
54
+ if (curviness === 1) {
55
+ 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);
56
+ return [f(p0[0], p1[0], p2[0], p3[0]), f(p0[1], p1[1], p2[1], p3[1])];
57
+ }
58
+ const h00 = 2 * t3 - 3 * t2 + 1;
59
+ const h10 = t3 - 2 * t2 + t;
60
+ const h01 = -2 * t3 + 3 * t2;
61
+ const h11 = t3 - t2;
62
+ const k = curviness * 0.5;
63
+ const H = (a, b, c, d) => h00 * b + h10 * k * (c - a) + h01 * c + h11 * k * (d - b);
64
+ return [H(p0[0], p1[0], p2[0], p3[0]), H(p0[1], p1[1], p2[1], p3[1])];
56
65
  }
57
- function pathTangentAngle(points, closed, u) {
66
+ function pathTangentAngle(points, closed, u, curviness = 1) {
58
67
  const n = points.length;
59
68
  if (n < 2) return 0;
60
69
  const segs = segCountOf(points, closed);
61
70
  const { i, t } = locate(segs, u);
62
71
  const [p0, p1, p2, p3] = controls(points, closed, i);
63
72
  const t2 = t * t;
64
- 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);
65
- const dx = d(p0[0], p1[0], p2[0], p3[0]);
66
- const dy = d(p0[1], p1[1], p2[1], p3[1]);
73
+ let dx;
74
+ let dy;
75
+ if (curviness === 1) {
76
+ 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);
77
+ dx = d(p0[0], p1[0], p2[0], p3[0]);
78
+ dy = d(p0[1], p1[1], p2[1], p3[1]);
79
+ } else {
80
+ const g00 = 6 * t2 - 6 * t;
81
+ const g10 = 3 * t2 - 4 * t + 1;
82
+ const g01 = -6 * t2 + 6 * t;
83
+ const g11 = 3 * t2 - 2 * t;
84
+ const k = curviness * 0.5;
85
+ const D = (a, b, c, e) => g00 * b + g10 * k * (c - a) + g01 * c + g11 * k * (e - b);
86
+ dx = D(p0[0], p1[0], p2[0], p3[0]);
87
+ dy = D(p0[1], p1[1], p2[1], p3[1]);
88
+ }
67
89
  if (dx === 0 && dy === 0) return 0;
68
90
  return Math.atan2(dy, dx) * 180 / Math.PI;
69
91
  }
@@ -144,8 +166,8 @@ function compileScene(ir) {
144
166
  const currentValue = (target, prop) => {
145
167
  const v = current.get(key(target, prop));
146
168
  if (v !== void 0) return v;
147
- if (prop === "opacity" || prop === "scale" || prop === "progress") return 1;
148
- if (prop === "rotation") return 0;
169
+ if (prop === "opacity" || prop === "scale" || prop === "progress" || prop === "scaleX" || prop === "scaleY") return 1;
170
+ if (prop === "rotation" || prop === "skewX" || prop === "skewY") return 0;
149
171
  throw new Error(`cannot animate "${prop}" of "${target}": no base value to start from`);
150
172
  };
151
173
  const labelTimes = /* @__PURE__ */ new Map();
@@ -248,16 +270,17 @@ function compileScene(ir) {
248
270
  const duration = tl.duration ?? DEFAULT_MOTIONPATH_DURATION;
249
271
  const points = tl.points;
250
272
  const closed = tl.closed ?? false;
273
+ const curviness = tl.curviness ?? 1;
251
274
  const autoRotate = tl.autoRotate ?? false;
252
275
  const rotateOffset = tl.rotateOffset ?? 0;
253
276
  let list = motionPaths.get(tl.target);
254
277
  if (!list) motionPaths.set(tl.target, list = []);
255
- list.push({ t0: start, t1: start + duration, points, closed, autoRotate, rotateOffset, ...tl.ease !== void 0 && { ease: tl.ease } });
278
+ list.push({ t0: start, t1: start + duration, points, closed, curviness, autoRotate, rotateOffset, ...tl.ease !== void 0 && { ease: tl.ease } });
256
279
  if (points.length > 0) {
257
- const [ex, ey] = pathPoint(points, closed, 1);
280
+ const [ex, ey] = pathPoint(points, closed, 1, curviness);
258
281
  current.set(key(tl.target, "x"), ex);
259
282
  current.set(key(tl.target, "y"), ey);
260
- if (autoRotate) current.set(key(tl.target, "rotation"), pathTangentAngle(points, closed, 1) + rotateOffset);
283
+ if (autoRotate) current.set(key(tl.target, "rotation"), pathTangentAngle(points, closed, 1, curviness) + rotateOffset);
261
284
  }
262
285
  return start + duration;
263
286
  }
@@ -322,7 +345,18 @@ function validateScene(ir) {
322
345
  problems.push(`duplicate node id "${node.id}" \u2014 every node id must be unique`);
323
346
  }
324
347
  nodeById.set(node.id, node);
325
- if (node.type === "group") collect(node.children);
348
+ if (node.type === "group") {
349
+ const clip = node.props.clip;
350
+ if (clip) {
351
+ if (clip.kind !== "rect" && clip.kind !== "ellipse") {
352
+ problems.push(`group "${node.id}" clip: unknown kind "${clip.kind}" \u2014 use "rect" or "ellipse"`);
353
+ }
354
+ if (!(clip.width > 0) || !(clip.height > 0)) {
355
+ problems.push(`group "${node.id}" clip: width and height must be > 0`);
356
+ }
357
+ }
358
+ collect(node.children);
359
+ }
326
360
  }
327
361
  };
328
362
  collect(ir.nodes);
@@ -355,11 +389,11 @@ function validateScene(ir) {
355
389
  );
356
390
  }
357
391
  const labels = /* @__PURE__ */ new Set();
358
- const checkTimeline = (tl, path) => {
392
+ const checkTimeline = (tl, path2) => {
359
393
  if ("label" in tl && tl.label !== void 0) {
360
394
  if (labels.has(tl.label)) {
361
395
  problems.push(
362
- `${path}: duplicate timeline label "${tl.label}" \u2014 labels are overlay addresses and must be unique`
396
+ `${path2}: duplicate timeline label "${tl.label}" \u2014 labels are overlay addresses and must be unique`
363
397
  );
364
398
  }
365
399
  labels.add(tl.label);
@@ -367,63 +401,73 @@ function validateScene(ir) {
367
401
  switch (tl.kind) {
368
402
  case "seq":
369
403
  case "par":
370
- tl.children.forEach((c, i) => checkTimeline(c, `${path}.${tl.kind}[${i}]`));
404
+ tl.children.forEach((c, i) => checkTimeline(c, `${path2}.${tl.kind}[${i}]`));
371
405
  break;
372
406
  case "stagger":
373
- if (tl.interval < 0) problems.push(`${path}: stagger interval must be >= 0`);
374
- tl.children.forEach((c, i) => checkTimeline(c, `${path}.stagger[${i}]`));
407
+ if (tl.interval < 0) problems.push(`${path2}: stagger interval must be >= 0`);
408
+ tl.children.forEach((c, i) => checkTimeline(c, `${path2}.stagger[${i}]`));
375
409
  break;
376
410
  case "to":
377
411
  if (!(tl.state in states)) {
378
412
  problems.push(
379
- `${path}: to("${tl.state}") references an undefined state \u2014 defined states: ${Object.keys(states).join(", ") || "(none)"}`
413
+ `${path2}: to("${tl.state}") references an undefined state \u2014 defined states: ${Object.keys(states).join(", ") || "(none)"}`
380
414
  );
381
415
  }
382
416
  if (tl.duration !== void 0 && tl.duration <= 0) {
383
- problems.push(`${path}: to("${tl.state}") duration must be > 0`);
417
+ problems.push(`${path2}: to("${tl.state}") duration must be > 0`);
384
418
  }
385
419
  for (const id of tl.filter ?? []) {
386
- if (!nodeById.has(id)) problems.push(`${path}: filter contains unknown node "${id}"`);
420
+ if (!nodeById.has(id)) problems.push(`${path2}: filter contains unknown node "${id}"`);
387
421
  }
388
422
  break;
389
423
  case "tween":
390
- checkProps(path, tl.target, tl.props);
424
+ checkProps(path2, tl.target, tl.props);
391
425
  if (tl.duration !== void 0 && tl.duration <= 0) {
392
- problems.push(`${path}: tween duration must be > 0`);
426
+ problems.push(`${path2}: tween duration must be > 0`);
393
427
  }
394
428
  break;
395
429
  case "motionPath": {
396
430
  const node = nodeById.get(tl.target);
397
431
  if (!node) {
398
432
  problems.push(
399
- `${path}: motionPath targets unknown node "${tl.target}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
433
+ `${path2}: motionPath targets unknown node "${tl.target}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
400
434
  );
401
435
  } else if (node.type === "line") {
402
- problems.push(`${path}: motionPath cannot target a line (no x/y) \u2014 "${tl.target}"`);
436
+ problems.push(`${path2}: motionPath cannot target a line (no x/y) \u2014 "${tl.target}"`);
403
437
  }
404
- if (tl.points.length < 1) problems.push(`${path}: motionPath "${tl.target}" needs at least 1 point`);
438
+ if (tl.points.length < 1) problems.push(`${path2}: motionPath "${tl.target}" needs at least 1 point`);
405
439
  if (tl.duration !== void 0 && tl.duration <= 0) {
406
- problems.push(`${path}: motionPath "${tl.target}" duration must be > 0`);
440
+ problems.push(`${path2}: motionPath "${tl.target}" duration must be > 0`);
441
+ }
442
+ if (tl.curviness !== void 0 && tl.curviness < 0) {
443
+ problems.push(`${path2}: motionPath "${tl.target}" curviness must be >= 0`);
407
444
  }
408
445
  break;
409
446
  }
410
447
  case "wait":
411
- if (tl.duration < 0) problems.push(`${path}: wait duration must be >= 0`);
448
+ if (tl.duration < 0) problems.push(`${path2}: wait duration must be >= 0`);
412
449
  break;
413
450
  case "beat":
414
451
  if (labels.has(tl.name)) {
415
452
  problems.push(
416
- `${path}: duplicate timeline label "${tl.name}" (beat name) \u2014 labels are overlay addresses and must be unique`
453
+ `${path2}: duplicate timeline label "${tl.name}" (beat name) \u2014 labels are overlay addresses and must be unique`
417
454
  );
418
455
  }
419
456
  labels.add(tl.name);
420
457
  if (tl.duration !== void 0 && tl.duration <= 0) {
421
- problems.push(`${path}: beat "${tl.name}" duration must be > 0`);
458
+ problems.push(`${path2}: beat "${tl.name}" duration must be > 0`);
422
459
  }
423
460
  if (tl.scale !== void 0 && tl.scale <= 0) {
424
- problems.push(`${path}: beat "${tl.name}" scale must be > 0`);
461
+ problems.push(`${path2}: beat "${tl.name}" scale must be > 0`);
425
462
  }
426
- tl.children.forEach((c, i) => checkTimeline(c, `${path}.beat(${tl.name})[${i}]`));
463
+ for (const id of tl.nodes ?? []) {
464
+ if (!nodeById.has(id)) {
465
+ problems.push(
466
+ `${path2}: beat "${tl.name}" owns unknown node "${id}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
467
+ );
468
+ }
469
+ }
470
+ tl.children.forEach((c, i) => checkTimeline(c, `${path2}.beat(${tl.name})[${i}]`));
427
471
  break;
428
472
  }
429
473
  };
@@ -463,11 +507,40 @@ function validateScene(ir) {
463
507
  }
464
508
  if (problems.length > 0) throw new SceneValidationError(problems);
465
509
  }
466
- var COMMON_PROPS, PROPS_BY_TYPE, SceneValidationError;
510
+ function validateComposition(comp) {
511
+ const problems = [];
512
+ if (comp.scenes.length === 0) problems.push("composition has no scenes");
513
+ const seen = /* @__PURE__ */ new Set();
514
+ for (const [i, entry] of comp.scenes.entries()) {
515
+ const where = `scenes[${i}]`;
516
+ try {
517
+ validateScene(entry.scene);
518
+ } catch (err) {
519
+ if (err instanceof SceneValidationError) {
520
+ for (const p of err.problems) problems.push(`${where} (scene "${entry.scene.id}"): ${p}`);
521
+ } else throw err;
522
+ }
523
+ if (seen.has(entry.scene.id)) {
524
+ problems.push(`${where}: duplicate scene id "${entry.scene.id}" \u2014 scene ids must be unique in a composition`);
525
+ }
526
+ seen.add(entry.scene.id);
527
+ if (entry.transition !== void 0 && !TRANSITIONS.includes(entry.transition)) {
528
+ problems.push(`${where}: unknown transition "${entry.transition}" \u2014 valid: ${TRANSITIONS.join(", ")}`);
529
+ }
530
+ if (typeof entry.at === "string" && Number.isNaN(Number(entry.at))) {
531
+ problems.push(`${where}: "at" string "${entry.at}" is not a number (use "-0.5"/"+0.5" or a number)`);
532
+ }
533
+ if (typeof entry.at === "number" && entry.at < 0) {
534
+ problems.push(`${where}: absolute "at" must be >= 0`);
535
+ }
536
+ }
537
+ if (problems.length > 0) throw new SceneValidationError(problems);
538
+ }
539
+ var COMMON_PROPS, PROPS_BY_TYPE, SceneValidationError, TRANSITIONS;
467
540
  var init_validate = __esm({
468
541
  "../core/src/validate.ts"() {
469
542
  "use strict";
470
- COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "anchor"];
543
+ COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "scaleX", "scaleY", "skewX", "skewY", "anchor"];
471
544
  PROPS_BY_TYPE = {
472
545
  rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
473
546
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
@@ -486,10 +559,56 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
486
559
  }
487
560
  problems;
488
561
  };
562
+ TRANSITIONS = ["cut", "crossfade"];
489
563
  }
490
564
  });
491
565
 
492
566
  // ../core/src/dsl.ts
567
+ function scene(input) {
568
+ const ir = { version: 1, ...input };
569
+ validateScene(ir);
570
+ if (ir.duration === void 0 && ir.timeline) {
571
+ ir.duration = compileScene(ir).duration;
572
+ }
573
+ return ir;
574
+ }
575
+ function rect(props) {
576
+ const { id, ...rest } = props;
577
+ return { type: "rect", id, props: rest };
578
+ }
579
+ function text(props) {
580
+ const { id, ...rest } = props;
581
+ return { type: "text", id, props: rest };
582
+ }
583
+ function path(props) {
584
+ const { id, ...rest } = props;
585
+ return { type: "path", id, props: rest };
586
+ }
587
+ function group(props, children) {
588
+ const { id, ...rest } = props;
589
+ return { type: "group", id, props: rest, children };
590
+ }
591
+ function seq(...children) {
592
+ return { kind: "seq", children };
593
+ }
594
+ function par(...children) {
595
+ return { kind: "par", children };
596
+ }
597
+ function stagger(interval, ...children) {
598
+ return { kind: "stagger", interval, children };
599
+ }
600
+ function beat(name, opts, children) {
601
+ return { kind: "beat", name, children, ...opts };
602
+ }
603
+ function tween(target, props, opts = {}) {
604
+ return { kind: "tween", target, props, ...opts };
605
+ }
606
+ function wait(duration, label) {
607
+ return { kind: "wait", duration, ...label !== void 0 && { label } };
608
+ }
609
+ function motionPath(target, points, opts = {}) {
610
+ return { kind: "motionPath", target, points, ...opts };
611
+ }
493
612
  var init_dsl = __esm({
494
613
  "../core/src/dsl.ts"() {
495
614
  "use strict";
@@ -498,10 +617,27 @@ var init_dsl = __esm({
498
617
  }
499
618
  });
500
619
 
620
+ // ../core/src/composeComposition.ts
621
+ var init_composeComposition = __esm({
622
+ "../core/src/composeComposition.ts"() {
623
+ "use strict";
624
+ init_compile();
625
+ init_ir();
626
+ }
627
+ });
628
+
501
629
  // ../core/src/compose.ts
502
630
  function composeScene(base, ...overlays) {
503
631
  const ir = structuredClone(base);
504
632
  const report = { applied: [], orphans: [], warnings: [] };
633
+ const baseNodeIds = /* @__PURE__ */ new Set();
634
+ const collectBase = (nodes) => {
635
+ for (const node of nodes) {
636
+ baseNodeIds.add(node.id);
637
+ if (node.type === "group") collectBase(node.children);
638
+ }
639
+ };
640
+ collectBase(base.nodes);
505
641
  overlays.forEach((overlay, index) => {
506
642
  const layer = overlay.name ?? `overlay-${index}`;
507
643
  if (overlay.target !== void 0 && overlay.target !== ir.id) {
@@ -509,12 +645,12 @@ function composeScene(base, ...overlays) {
509
645
  `${layer}: authored against scene "${overlay.target}" but composing onto "${ir.id}"`
510
646
  );
511
647
  }
512
- applyOverlay(ir, overlay, layer, report);
648
+ applyOverlay(ir, overlay, layer, report, baseNodeIds);
513
649
  });
514
650
  validateScene(ir);
515
651
  return { ir, report };
516
652
  }
517
- function applyOverlay(ir, overlay, layer, report) {
653
+ function applyOverlay(ir, overlay, layer, report, baseNodeIds) {
518
654
  const nodeById = /* @__PURE__ */ new Map();
519
655
  const collect = (nodes) => {
520
656
  for (const node of nodes) {
@@ -629,7 +765,7 @@ function applyOverlay(ir, overlay, layer, report) {
629
765
  to: ["duration", "ease", "stagger"],
630
766
  tween: ["duration", "ease"],
631
767
  wait: ["duration"],
632
- motionPath: ["points", "duration", "ease"],
768
+ motionPath: ["points", "duration", "ease", "curviness", "autoRotate"],
633
769
  beat: ["at", "gap", "scale", "duration", "order"]
634
770
  };
635
771
  let timingPatched = false;
@@ -667,6 +803,49 @@ function applyOverlay(ir, overlay, layer, report) {
667
803
  nodeById.set(node.id, node);
668
804
  applied(`addNodes.${node.id}`, "add-node");
669
805
  }
806
+ for (const id of overlay.removeNodes ?? []) {
807
+ if (baseNodeIds.has(id)) {
808
+ orphan(
809
+ `removeNodes.${id}`,
810
+ `"${id}" is a base scene node \u2014 the scene owns it; hide it with opacity: 0 instead of removing`
811
+ );
812
+ continue;
813
+ }
814
+ const index = ir.nodes.findIndex((n) => n.id === id);
815
+ if (index < 0) {
816
+ orphan(
817
+ `removeNodes.${id}`,
818
+ `unknown overlay-added node "${id}" \u2014 nothing to remove`
819
+ );
820
+ continue;
821
+ }
822
+ ir.nodes.splice(index, 1);
823
+ nodeById.delete(id);
824
+ applied(`removeNodes.${id}`, "remove-node");
825
+ }
826
+ if (overlay.addTimeline && overlay.addTimeline.length > 0) {
827
+ const collectTargets = (tl, out) => {
828
+ if (tl.kind === "tween" || tl.kind === "motionPath") out.add(tl.target);
829
+ if ("children" in tl) tl.children.forEach((c) => collectTargets(c, out));
830
+ };
831
+ const valid = [];
832
+ overlay.addTimeline.forEach((frag, i) => {
833
+ const targets = /* @__PURE__ */ new Set();
834
+ collectTargets(frag, targets);
835
+ const missing = [...targets].filter((id) => !nodeById.has(id));
836
+ if (missing.length > 0) {
837
+ orphan(`addTimeline[${i}]`, `targets unknown node(s) ${missing.join(", ")} \u2014 known ids: ${knownIds()}`);
838
+ return;
839
+ }
840
+ valid.push(structuredClone(frag));
841
+ applied(`addTimeline[${i}]`, "add-timeline");
842
+ });
843
+ if (valid.length > 0) {
844
+ ir.timeline = ir.timeline ? { kind: "par", children: [ir.timeline, ...valid] } : valid.length === 1 ? valid[0] : { kind: "par", children: valid };
845
+ delete ir.duration;
846
+ ir.duration = compileScene(ir).duration;
847
+ }
848
+ }
670
849
  }
671
850
  var SCENE_PATCHABLE;
672
851
  var init_compose = __esm({
@@ -679,12 +858,197 @@ var init_compose = __esm({
679
858
  });
680
859
 
681
860
  // ../core/src/presets.ts
682
- var SET;
861
+ function makeRng(seed) {
862
+ let a = seed >>> 0 || 2654435769;
863
+ return () => {
864
+ a = a + 1831565813 | 0;
865
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
866
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
867
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
868
+ };
869
+ }
870
+ function ctx(o) {
871
+ const rand = makeRng((o.seed ?? 0) + 1);
872
+ return {
873
+ e: clamp01(o.energy ?? 0.5),
874
+ sp: Math.max(0.25, o.speed ?? 1),
875
+ it: clamp01(o.intensity ?? 0.5),
876
+ from: o.from,
877
+ rand,
878
+ jit: (amp) => (rand() - 0.5) * 2 * amp,
879
+ g: o.target.group,
880
+ cx: o.target.center[0],
881
+ cy: o.target.center[1],
882
+ s: o.target.baseScale,
883
+ fills: o.target.fills,
884
+ inks: o.target.inks
885
+ };
886
+ }
887
+ function settleEase(e) {
888
+ return e < 0.34 ? "easeOutCubic" : e < 0.67 ? "easeOutBack" : "easeOutElastic";
889
+ }
890
+ function fromVec(from, dist) {
891
+ switch (from) {
892
+ case "left":
893
+ return [-dist, 0];
894
+ case "right":
895
+ return [dist, 0];
896
+ case "top":
897
+ return [0, -dist];
898
+ default:
899
+ return [0, dist];
900
+ }
901
+ }
902
+ function fadeFills(c, base = 0.4, gap = 0.06) {
903
+ return stagger(
904
+ gap / c.sp,
905
+ ...c.fills.map(
906
+ (id, i) => tween(id, { opacity: 1 }, { duration: dur(base, c.sp), ease: "easeOutQuad", ...i === 0 && { label: "reveal" } })
907
+ )
908
+ );
909
+ }
910
+ function drawInks(c) {
911
+ return stagger(
912
+ 0.15 / c.sp,
913
+ ...c.inks.map(
914
+ (id, i) => tween(id, { progress: 1 }, { duration: dur(1.3 + c.jit(0.2), c.sp), ease: "easeInOutQuad", ...i === 0 && { label: "draw" } })
915
+ )
916
+ );
917
+ }
918
+ function motionPreset(name, opts) {
919
+ const c = ctx(opts);
920
+ switch (name) {
921
+ case "draw-bloom":
922
+ return beat("draw-bloom", {}, [
923
+ drawInks(c),
924
+ fadeFills(c, 0.45),
925
+ tween(c.g, { scale: c.s * (1.02 + 0.05 * c.e) }, { duration: dur(2.4, c.sp), ease: "easeInOutQuad", label: "settle" })
926
+ ]);
927
+ case "punch-in": {
928
+ const peak = c.s * (1 + 0.06 + 0.24 * c.e + c.jit(0.02));
929
+ return beat("punch-in", {}, [
930
+ par(
931
+ fadeFills(c, 0.25),
932
+ seq(
933
+ tween(c.g, { scale: peak }, { duration: dur(0.45 + c.jit(0.05), c.sp), ease: "easeOutCubic", label: "punch" }),
934
+ tween(c.g, { scale: c.s }, { duration: dur(0.5, c.sp), ease: settleEase(c.e) })
935
+ )
936
+ )
937
+ ]);
938
+ }
939
+ case "rise-settle": {
940
+ const es = 0.65 + c.rand() * 0.7;
941
+ const dist = (220 + 260 * c.it) * es;
942
+ const [dx, dy] = fromVec(c.from ?? "bottom", dist);
943
+ const jx = c.jit(110);
944
+ return beat("rise-settle", {}, [
945
+ par(
946
+ motionPath(
947
+ c.g,
948
+ [
949
+ [c.cx + dx + jx, c.cy + dy],
950
+ [c.cx + dx * 0.4 - jx * 0.6, c.cy + dy * 0.4],
951
+ [c.cx, c.cy]
952
+ ],
953
+ { duration: dur(1.1, c.sp), ease: settleEase(c.e), label: "rise" }
954
+ ),
955
+ fadeFills(c, 0.4)
956
+ )
957
+ ]);
958
+ }
959
+ case "slide-bank": {
960
+ const es = 0.65 + c.rand() * 0.7;
961
+ const dist = (420 + 240 * c.it) * es;
962
+ const [dx, dy] = fromVec(c.from ?? "left", dist);
963
+ const arc = c.jit(140);
964
+ const midx = c.jit(120);
965
+ const move = dur(1.2, c.sp);
966
+ return beat("slide-bank", {}, [
967
+ par(
968
+ motionPath(
969
+ c.g,
970
+ [
971
+ [c.cx + dx, c.cy + dy],
972
+ [c.cx + dx * 0.4 + midx, c.cy + dy * 0.4 - 70 - arc],
973
+ [c.cx, c.cy]
974
+ ],
975
+ { duration: move, ease: settleEase(c.e), autoRotate: true, label: "slide" }
976
+ ),
977
+ // level the bank out once it lands (authored after the path → wins for rotation)
978
+ seq(wait(move), tween(c.g, { rotation: 0 }, { duration: dur(0.5, c.sp), ease: "easeOutCubic" })),
979
+ fadeFills(c, 0.4)
980
+ )
981
+ ]);
982
+ }
983
+ case "reveal-orbit": {
984
+ const es = 0.65 + c.rand() * 0.7;
985
+ const orbit = (180 + 160 * c.it) * es;
986
+ const jx = c.jit(0.4);
987
+ const jy = c.jit(0.4);
988
+ return beat("reveal-orbit", {}, [
989
+ drawInks(c),
990
+ fadeFills(c, 0.45),
991
+ par(
992
+ motionPath(
993
+ c.g,
994
+ [
995
+ [c.cx, c.cy],
996
+ [c.cx - orbit * (1 + jx), c.cy - orbit * 0.8],
997
+ [c.cx + orbit * (1 + jy), c.cy - orbit],
998
+ [c.cx, c.cy]
999
+ ],
1000
+ { duration: dur(1.7, c.sp), ease: "easeInOutCubic", label: "orbit" }
1001
+ ),
1002
+ seq(
1003
+ tween(c.g, { scale: c.s * (1.12 + 0.1 * c.e) }, { duration: dur(0.85, c.sp), ease: "easeOutBack" }),
1004
+ tween(c.g, { scale: c.s }, { duration: dur(0.85, c.sp), ease: "easeInOutQuad" })
1005
+ )
1006
+ )
1007
+ ]);
1008
+ }
1009
+ case "spin-forge": {
1010
+ const turns = 1 + Math.round(c.it);
1011
+ const dir = c.rand() < 0.5 ? -1 : 1;
1012
+ const startRot = dir * 360 * turns;
1013
+ const peak = c.s * (1 + 0.05 + 0.2 * c.e);
1014
+ return beat("spin-forge", {}, [
1015
+ par(
1016
+ seq(
1017
+ tween(c.g, { scale: c.s * 0.2, rotation: startRot }, { duration: SET }),
1018
+ // establish (invisible)
1019
+ tween(c.g, { scale: peak, rotation: 0 }, { duration: dur(0.9, c.sp), ease: "easeOutBack", label: "spin" }),
1020
+ tween(c.g, { scale: c.s }, { duration: dur(0.3, c.sp), ease: "easeInOutQuad" })
1021
+ ),
1022
+ seq(wait(SET), fadeFills(c, 0.3))
1023
+ )
1024
+ ]);
1025
+ }
1026
+ }
1027
+ }
1028
+ var clamp01, SET, dur;
683
1029
  var init_presets = __esm({
684
1030
  "../core/src/presets.ts"() {
685
1031
  "use strict";
686
1032
  init_dsl();
1033
+ clamp01 = (x) => Math.max(0, Math.min(1, x));
687
1034
  SET = 1 / 120;
1035
+ dur = (base, sp) => base / sp;
1036
+ }
1037
+ });
1038
+
1039
+ // ../core/src/devicePreset.ts
1040
+ var init_devicePreset = __esm({
1041
+ "../core/src/devicePreset.ts"() {
1042
+ "use strict";
1043
+ init_dsl();
1044
+ }
1045
+ });
1046
+
1047
+ // ../core/src/motionOps.ts
1048
+ var init_motionOps = __esm({
1049
+ "../core/src/motionOps.ts"() {
1050
+ "use strict";
1051
+ init_dsl();
688
1052
  }
689
1053
  });
690
1054
 
@@ -724,6 +1088,15 @@ function resolveAudioPlan(compiled) {
724
1088
  });
725
1089
  }
726
1090
  cues.sort((a, b) => a.t - b.t);
1091
+ return {
1092
+ duration,
1093
+ bgm: resolveBgm(audio.bgm),
1094
+ cues,
1095
+ duckWindows: mergeDuckWindows(cues, duration),
1096
+ warnings
1097
+ };
1098
+ }
1099
+ function mergeDuckWindows(cues, duration) {
727
1100
  const duckWindows = [];
728
1101
  for (const cue of cues) {
729
1102
  const window2 = { t0: cue.t, t1: Math.min(duration, cue.t + cue.duration) };
@@ -731,23 +1104,22 @@ function resolveAudioPlan(compiled) {
731
1104
  if (last && window2.t0 <= last.t1 + 0.1) last.t1 = Math.max(last.t1, window2.t1);
732
1105
  else duckWindows.push(window2);
733
1106
  }
734
- let bgm = null;
735
- if (audio.bgm) {
736
- const b = audio.bgm;
737
- const duck = b.duck === false ? null : {
738
- depth: b.duck?.depth ?? 0.5,
739
- attack: b.duck?.attack ?? 0.05,
740
- release: b.duck?.release ?? 0.25
741
- };
742
- bgm = {
743
- source: b.file ? { kind: "file", path: b.file } : { kind: "synth", name: b.synth ?? "ambient-pad" },
744
- gain: b.gain ?? 0.5,
745
- fadeIn: b.fadeIn ?? 0,
746
- fadeOut: b.fadeOut ?? 0,
747
- duck
748
- };
749
- }
750
- return { duration, bgm, cues, duckWindows, warnings };
1107
+ return duckWindows;
1108
+ }
1109
+ function resolveBgm(b) {
1110
+ if (!b) return null;
1111
+ const duck = b.duck === false ? null : {
1112
+ depth: b.duck?.depth ?? 0.5,
1113
+ attack: b.duck?.attack ?? 0.05,
1114
+ release: b.duck?.release ?? 0.25
1115
+ };
1116
+ return {
1117
+ source: b.file ? { kind: "file", path: b.file } : { kind: "synth", name: b.synth ?? "ambient-pad" },
1118
+ gain: b.gain ?? 0.5,
1119
+ fadeIn: b.fadeIn ?? 0,
1120
+ fadeOut: b.fadeOut ?? 0,
1121
+ duck
1122
+ };
751
1123
  }
752
1124
  var SFX_DURATION, FILE_CUE_DURATION;
753
1125
  var init_audio = __esm({
@@ -884,10 +1256,13 @@ var init_src = __esm({
884
1256
  init_ir();
885
1257
  init_dsl();
886
1258
  init_validate();
1259
+ init_composeComposition();
887
1260
  init_compose();
888
1261
  init_compile();
889
1262
  init_path();
890
1263
  init_presets();
1264
+ init_devicePreset();
1265
+ init_motionOps();
891
1266
  init_audio();
892
1267
  init_evaluate();
893
1268
  init_interpolate();
@@ -897,6 +1272,152 @@ var init_src = __esm({
897
1272
  }
898
1273
  });
899
1274
 
1275
+ // ../render-cli/src/logoSting.ts
1276
+ var logoSting_exports = {};
1277
+ __export(logoSting_exports, {
1278
+ LOGO_PRESETS: () => LOGO_PRESETS,
1279
+ buildLogoSting: () => buildLogoSting,
1280
+ resolveLogo: () => resolveLogo
1281
+ });
1282
+ import { existsSync } from "node:fs";
1283
+ import { readFile } from "node:fs/promises";
1284
+ async function loadSvg(arg) {
1285
+ if (existsSync(arg)) {
1286
+ return { svg: await readFile(arg, "utf8"), name: arg.split("/").pop().replace(/\.svg$/i, "") };
1287
+ }
1288
+ const slug = arg.toLowerCase().replace(/[^a-z0-9]/g, "");
1289
+ const r = await fetch(`https://cdn.simpleicons.org/${slug}`);
1290
+ if (!r.ok) throw new Error(`no local file "${arg}", and simple-icons has no "${slug}" (${r.status})`);
1291
+ return { svg: await r.text(), name: arg };
1292
+ }
1293
+ function tooDark(hex) {
1294
+ const h = hex.replace("#", "");
1295
+ const n = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
1296
+ const r = parseInt(n.slice(0, 2), 16);
1297
+ const g = parseInt(n.slice(2, 4), 16);
1298
+ const b = parseInt(n.slice(4, 6), 16);
1299
+ return 0.299 * r + 0.587 * g + 0.114 * b < 40;
1300
+ }
1301
+ function parseSvg(svg) {
1302
+ let viewBox = { minX: 0, minY: 0, w: 100, h: 100 };
1303
+ const vb = svg.match(/viewBox\s*=\s*"([\d.\-\s]+)"/i);
1304
+ if (vb) {
1305
+ const [a, b, c, d] = vb[1].trim().split(/\s+/).map(Number);
1306
+ viewBox = { minX: a, minY: b, w: c, h: d };
1307
+ } else {
1308
+ const w = svg.match(/\bwidth\s*=\s*"([\d.]+)/i);
1309
+ const h = svg.match(/\bheight\s*=\s*"([\d.]+)/i);
1310
+ if (w && h) viewBox = { minX: 0, minY: 0, w: +w[1], h: +h[1] };
1311
+ }
1312
+ const rootFill = svg.match(/<svg[^>]*\bfill\s*=\s*"(#[0-9a-fA-F]{3,8})"/)?.[1];
1313
+ const fallback = rootFill && !tooDark(rootFill) ? rootFill : "#E6EDF3";
1314
+ const paths = [];
1315
+ const re = /<path\b[^>]*>/g;
1316
+ let m;
1317
+ while (m = re.exec(svg)) {
1318
+ const tag = m[0];
1319
+ const d = tag.match(/\bd\s*=\s*"([^"]+)"/)?.[1];
1320
+ if (!d) continue;
1321
+ let fill = tag.match(/\bfill\s*=\s*"(#[0-9a-fA-F]{3,8})"/)?.[1] ?? fallback;
1322
+ if (tooDark(fill)) fill = fallback;
1323
+ paths.push({ d, fill });
1324
+ }
1325
+ return { paths, viewBox };
1326
+ }
1327
+ async function resolveLogo(arg, displayName, opts) {
1328
+ if (opts.motion && !LOGO_PRESETS.includes(opts.motion)) {
1329
+ throw new Error(`unknown --motion "${opts.motion}". options: ${LOGO_PRESETS.join(", ")}`);
1330
+ }
1331
+ const { svg, name } = await loadSvg(arg);
1332
+ const { paths, viewBox } = parseSvg(svg);
1333
+ if (paths.length === 0) throw new Error("no <path> elements found \u2014 logo stings need a path-based SVG");
1334
+ const from = FROMS.includes(opts.from) ? opts.from : void 0;
1335
+ const data = {
1336
+ name: displayName ?? titleCase(name),
1337
+ paths,
1338
+ viewBox,
1339
+ ...opts.motion && { motion: opts.motion },
1340
+ ...opts.energy !== void 0 && { energy: opts.energy },
1341
+ ...opts.speed !== void 0 && { speed: opts.speed },
1342
+ ...opts.intensity !== void 0 && { intensity: opts.intensity },
1343
+ ...from && { from },
1344
+ ...opts.seed !== void 0 && { seed: opts.seed }
1345
+ };
1346
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "logo";
1347
+ return { data, slug };
1348
+ }
1349
+ function buildLogoSting(d) {
1350
+ const W = 1080;
1351
+ const H = 1080;
1352
+ const CX = 540;
1353
+ const CY = 500;
1354
+ const vcx = d.viewBox.minX + d.viewBox.w / 2;
1355
+ const vcy = d.viewBox.minY + d.viewBox.h / 2;
1356
+ const fit = LOGO_PX / Math.max(d.viewBox.w, d.viewBox.h);
1357
+ const sw = 2.2 / fit;
1358
+ const fills = d.paths.map(
1359
+ (p, i) => path({ id: `fill-${i}`, d: p.d, originX: vcx, originY: vcy, x: 0, y: 0, fill: p.fill, opacity: 0 })
1360
+ );
1361
+ const inks = d.paths.map(
1362
+ (p, i) => path({ id: `ink-${i}`, d: p.d, originX: vcx, originY: vcy, x: 0, y: 0, stroke: p.fill, strokeWidth: sw, progress: 0 })
1363
+ );
1364
+ const rig = {
1365
+ group: "logo",
1366
+ center: [CX, CY],
1367
+ baseScale: fit,
1368
+ fills: fills.map((n) => n.id),
1369
+ inks: inks.map((n) => n.id)
1370
+ };
1371
+ return scene({
1372
+ id: "logo-sting",
1373
+ size: { width: W, height: H },
1374
+ fps: 30,
1375
+ background: BG,
1376
+ nodes: [
1377
+ rect({ id: "bg", x: 0, y: 0, width: W, height: H, fill: BG }),
1378
+ group({ id: "logo", x: CX, y: CY, scale: fit }, [...fills, ...inks]),
1379
+ text({ id: "word", x: CX, y: 905, anchor: "center", content: d.name, fontFamily: "Inter", fontSize: 56, fontWeight: 800, fill: FG, opacity: 0 }),
1380
+ text({ id: "made", x: CX, y: 968, anchor: "center", content: "made with reframe", fontFamily: "Inter", fontSize: 20, fill: MUTED, opacity: 0 })
1381
+ ],
1382
+ timeline: seq(
1383
+ motionPreset(d.motion ?? "reveal-orbit", {
1384
+ target: rig,
1385
+ ...d.energy !== void 0 && { energy: d.energy },
1386
+ ...d.speed !== void 0 && { speed: d.speed },
1387
+ ...d.intensity !== void 0 && { intensity: d.intensity },
1388
+ ...d.from !== void 0 && { from: d.from },
1389
+ ...d.seed !== void 0 && { seed: d.seed }
1390
+ }),
1391
+ par(
1392
+ tween("word", { opacity: 1 }, { duration: 0.5, ease: "easeOutQuad", label: "word" }),
1393
+ seq(wait(0.2), tween("made", { opacity: 1 }, { duration: 0.5, ease: "easeOutQuad" }))
1394
+ ),
1395
+ wait(0.8, "hold")
1396
+ )
1397
+ });
1398
+ }
1399
+ var LOGO_PRESETS, titleCase, FROMS, BG, FG, MUTED, LOGO_PX;
1400
+ var init_logoSting = __esm({
1401
+ "../render-cli/src/logoSting.ts"() {
1402
+ "use strict";
1403
+ init_src();
1404
+ LOGO_PRESETS = [
1405
+ "draw-bloom",
1406
+ "punch-in",
1407
+ "rise-settle",
1408
+ "slide-bank",
1409
+ "reveal-orbit",
1410
+ "spin-forge"
1411
+ ];
1412
+ titleCase = (s) => s.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()).trim();
1413
+ FROMS = ["left", "right", "top", "bottom"];
1414
+ BG = "#0D1117";
1415
+ FG = "#E6EDF3";
1416
+ MUTED = "#8B949E";
1417
+ LOGO_PX = 520;
1418
+ }
1419
+ });
1420
+
900
1421
  // ../render-cli/src/audio/wav.ts
901
1422
  function encodeWavMono16(samples, sampleRate = SAMPLE_RATE) {
902
1423
  const dataBytes = samples.length * 2;
@@ -941,52 +1462,52 @@ function buffer(duration) {
941
1462
  return { out: new Float32Array(n), n };
942
1463
  }
943
1464
  function whoosh(seed) {
944
- const dur = 0.35;
945
- const { out, n } = buffer(dur);
1465
+ const dur2 = 0.35;
1466
+ const { out, n } = buffer(dur2);
946
1467
  let lp = 0;
947
1468
  let lp2 = 0;
948
1469
  for (let i = 0; i < n; i++) {
949
1470
  const t = i / SAMPLE_RATE;
950
- const u = t / dur;
1471
+ const u = t / dur2;
951
1472
  const center = 1200 * Math.pow(300 / 1200, u);
952
1473
  const alpha = Math.min(1, TAU * center / SAMPLE_RATE);
953
1474
  lp += alpha * (noise(i, seed) - lp);
954
1475
  lp2 += alpha * 0.5 * (lp - lp2);
955
- const env = u < 0.3 ? u / 0.3 : expDecay(t - 0.3 * dur, dur * 0.7, 4);
1476
+ const env = u < 0.3 ? u / 0.3 : expDecay(t - 0.3 * dur2, dur2 * 0.7, 4);
956
1477
  out[i] = (lp - lp2) * env * 2.2;
957
1478
  }
958
1479
  return out;
959
1480
  }
960
1481
  function pop(seed) {
961
- const dur = 0.12;
962
- const { out, n } = buffer(dur);
1482
+ const dur2 = 0.12;
1483
+ const { out, n } = buffer(dur2);
963
1484
  let phase = 0;
964
1485
  for (let i = 0; i < n; i++) {
965
1486
  const t = i / SAMPLE_RATE;
966
1487
  const freq = 600 * Math.pow(150 / 600, t / 0.08);
967
1488
  phase += TAU * freq / SAMPLE_RATE;
968
1489
  const transient = t < 2e-3 ? noise(i, seed) * 0.5 : 0;
969
- out[i] = (Math.sin(phase) + transient) * expDecay(t, dur, 6) * 0.8;
1490
+ out[i] = (Math.sin(phase) + transient) * expDecay(t, dur2, 6) * 0.8;
970
1491
  }
971
1492
  return out;
972
1493
  }
973
1494
  function tick(seed) {
974
- const dur = 0.03;
975
- const { out, n } = buffer(dur);
1495
+ const dur2 = 0.03;
1496
+ const { out, n } = buffer(dur2);
976
1497
  for (let i = 0; i < n; i++) {
977
1498
  const t = i / SAMPLE_RATE;
978
1499
  const sine = t < 4e-3 ? Math.sin(TAU * 4e3 * t) : 0;
979
- out[i] = (sine * 0.6 + noise(i, seed) * 0.35) * expDecay(t, dur, 8);
1500
+ out[i] = (sine * 0.6 + noise(i, seed) * 0.35) * expDecay(t, dur2, 8);
980
1501
  }
981
1502
  return out;
982
1503
  }
983
1504
  function rise(seed) {
984
- const dur = 0.5;
985
- const { out, n } = buffer(dur);
1505
+ const dur2 = 0.5;
1506
+ const { out, n } = buffer(dur2);
986
1507
  let phase = 0;
987
1508
  for (let i = 0; i < n; i++) {
988
1509
  const t = i / SAMPLE_RATE;
989
- const u = t / dur;
1510
+ const u = t / dur2;
990
1511
  const freq = 220 * Math.pow(880 / 220, u);
991
1512
  phase += TAU * freq / SAMPLE_RATE;
992
1513
  const env = Math.sin(Math.PI * Math.min(1, u * 1.05)) ** 1.5;
@@ -995,8 +1516,8 @@ function rise(seed) {
995
1516
  return out;
996
1517
  }
997
1518
  function shimmer(seed) {
998
- const dur = 0.9;
999
- const { out, n } = buffer(dur);
1519
+ const dur2 = 0.9;
1520
+ const { out, n } = buffer(dur2);
1000
1521
  const partials = Array.from({ length: 5 }, (_, p) => ({
1001
1522
  freq: 2e3 + hash01(p, seed + 7) * 2e3,
1002
1523
  am: 0.5 + hash01(p, seed + 8) * 1.5,
@@ -1004,7 +1525,7 @@ function shimmer(seed) {
1004
1525
  }));
1005
1526
  for (let i = 0; i < n; i++) {
1006
1527
  const t = i / SAMPLE_RATE;
1007
- const u = t / dur;
1528
+ const u = t / dur2;
1008
1529
  const env = Math.sin(Math.PI * u) ** 1.2;
1009
1530
  let s = 0;
1010
1531
  for (const part of partials) {
@@ -1015,8 +1536,8 @@ function shimmer(seed) {
1015
1536
  return out;
1016
1537
  }
1017
1538
  function thud(seed) {
1018
- const dur = 0.25;
1019
- const { out, n } = buffer(dur);
1539
+ const dur2 = 0.25;
1540
+ const { out, n } = buffer(dur2);
1020
1541
  let phase = 0;
1021
1542
  let lp = 0;
1022
1543
  for (let i = 0; i < n; i++) {
@@ -1025,7 +1546,7 @@ function thud(seed) {
1025
1546
  phase += TAU * freq / SAMPLE_RATE;
1026
1547
  lp += 0.02 * (noise(i, seed) - lp);
1027
1548
  const attack = t < 0.01 ? lp * 3 : 0;
1028
- out[i] = (Math.sin(phase) * 0.9 + attack) * expDecay(t, dur, 5);
1549
+ out[i] = (Math.sin(phase) * 0.9 + attack) * expDecay(t, dur2, 5);
1029
1550
  }
1030
1551
  return out;
1031
1552
  }
@@ -1060,7 +1581,7 @@ var init_synth = __esm({
1060
1581
  init_wav();
1061
1582
  noise = (n, seed) => hash01(n, seed) * 2 - 1;
1062
1583
  TAU = Math.PI * 2;
1063
- expDecay = (t, dur, k = 5) => Math.exp(-k * t / dur);
1584
+ expDecay = (t, dur2, k = 5) => Math.exp(-k * t / dur2);
1064
1585
  RECIPES = {
1065
1586
  whoosh,
1066
1587
  pop,
@@ -1074,26 +1595,26 @@ var init_synth = __esm({
1074
1595
 
1075
1596
  // ../render-cli/src/audio/sfx.ts
1076
1597
  import { mkdir, rename, writeFile } from "node:fs/promises";
1077
- import { existsSync } from "node:fs";
1598
+ import { existsSync as existsSync2 } from "node:fs";
1078
1599
  import { tmpdir } from "node:os";
1079
1600
  import { dirname, isAbsolute, join, resolve } from "node:path";
1080
1601
  import { fileURLToPath } from "node:url";
1081
- function fnv1a(text) {
1602
+ function fnv1a(text2) {
1082
1603
  let h = 2166136261;
1083
- for (let i = 0; i < text.length; i++) {
1084
- h ^= text.charCodeAt(i);
1604
+ for (let i = 0; i < text2.length; i++) {
1605
+ h ^= text2.charCodeAt(i);
1085
1606
  h = Math.imul(h, 16777619);
1086
1607
  }
1087
1608
  return (h >>> 0).toString(16);
1088
1609
  }
1089
1610
  async function writeCached(key2, make) {
1090
- const path = join(CACHE, `${key2}.wav`);
1091
- if (existsSync(path)) return path;
1611
+ const path2 = join(CACHE, `${key2}.wav`);
1612
+ if (existsSync2(path2)) return path2;
1092
1613
  await mkdir(CACHE, { recursive: true });
1093
- const temp = `${path}.${process.pid}.${fnv1a(String(performance.now()))}.tmp`;
1614
+ const temp = `${path2}.${process.pid}.${fnv1a(String(performance.now()))}.tmp`;
1094
1615
  await writeFile(temp, encodeWavMono16(make()));
1095
- await rename(temp, path);
1096
- return path;
1616
+ await rename(temp, path2);
1617
+ return path2;
1097
1618
  }
1098
1619
  async function resolveCueFile(cue, sceneDir) {
1099
1620
  if (cue.source.kind === "file") {
@@ -1103,14 +1624,14 @@ async function resolveCueFile(cue, sceneDir) {
1103
1624
  resolve(sceneDir, p),
1104
1625
  join(VENDORED, p)
1105
1626
  ]) {
1106
- if (candidate && existsSync(candidate)) return candidate;
1627
+ if (candidate && existsSync2(candidate)) return candidate;
1107
1628
  }
1108
1629
  throw new Error(
1109
1630
  `audio cue file "${p}" not found (tried absolute, scene-relative, assets/sfx/)`
1110
1631
  );
1111
1632
  }
1112
1633
  const vendored = join(VENDORED, `${cue.source.name}.wav`);
1113
- if (existsSync(vendored)) return vendored;
1634
+ if (existsSync2(vendored)) return vendored;
1114
1635
  const { name, params } = cue.source;
1115
1636
  return writeCached(`${name}-${fnv1a(JSON.stringify(params))}`, () => synthSfx(name, params));
1116
1637
  }
@@ -1118,7 +1639,7 @@ async function resolveBgmFile(source, duration, sceneDir) {
1118
1639
  if (source.kind === "file") {
1119
1640
  const p = source.path;
1120
1641
  for (const candidate of [isAbsolute(p) ? p : null, resolve(sceneDir, p), join(VENDORED, p)]) {
1121
- if (candidate && existsSync(candidate)) return candidate;
1642
+ if (candidate && existsSync2(candidate)) return candidate;
1122
1643
  }
1123
1644
  throw new Error(`bgm file "${p}" not found`);
1124
1645
  }
@@ -1288,14 +1809,14 @@ var init_encode = __esm({
1288
1809
  });
1289
1810
 
1290
1811
  // ../render-cli/src/fonts.ts
1291
- import { readFile } from "node:fs/promises";
1812
+ import { readFile as readFile2 } from "node:fs/promises";
1292
1813
  import { dirname as dirname3, join as join3 } from "node:path";
1293
1814
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1294
1815
  async function fontFaceCss() {
1295
1816
  if (cssCache) return cssCache;
1296
1817
  const rules = await Promise.all(
1297
1818
  WEIGHTS.map(async (weight) => {
1298
- const data = await readFile(join3(FONTS_DIR, `inter-${weight}.woff2`));
1819
+ const data = await readFile2(join3(FONTS_DIR, `inter-${weight}.woff2`));
1299
1820
  return `@font-face {
1300
1821
  font-family: "Inter";
1301
1822
  font-style: normal;
@@ -1318,8 +1839,8 @@ var init_fonts = __esm({
1318
1839
  });
1319
1840
 
1320
1841
  // ../render-cli/src/images.ts
1321
- import { readFile as readFile2 } from "node:fs/promises";
1322
- import { existsSync as existsSync2 } from "node:fs";
1842
+ import { readFile as readFile3 } from "node:fs/promises";
1843
+ import { existsSync as existsSync3 } from "node:fs";
1323
1844
  import { extname, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
1324
1845
  async function buildImageAssets(ir, sceneDir) {
1325
1846
  const assets = {};
@@ -1333,11 +1854,11 @@ async function buildImageAssets(ir, sceneDir) {
1333
1854
  const candidates = [isAbsolute2(src) ? src : null, resolve2(sceneDir, src)].filter(
1334
1855
  (c) => c !== null
1335
1856
  );
1336
- const found = candidates.find((c) => existsSync2(c));
1857
+ const found = candidates.find((c) => existsSync3(c));
1337
1858
  if (!found) {
1338
1859
  throw new Error(`image "${src}" not found (tried: ${candidates.join(", ")})`);
1339
1860
  }
1340
- const data = await readFile2(found);
1861
+ const data = await readFile3(found);
1341
1862
  assets[src] = `data:${mime};base64,${data.toString("base64")}`;
1342
1863
  }
1343
1864
  return assets;
@@ -1461,8 +1982,8 @@ async function withPage(size, fn) {
1461
1982
  async function browserBundle() {
1462
1983
  if (bundleCache) return bundleCache;
1463
1984
  if (true) {
1464
- const { readFile: readFile5 } = await import("node:fs/promises");
1465
- bundleCache = await readFile5(
1985
+ const { readFile: readFile6 } = await import("node:fs/promises");
1986
+ bundleCache = await readFile6(
1466
1987
  join4(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.js"),
1467
1988
  "utf8"
1468
1989
  );
@@ -1524,7 +2045,7 @@ __export(batch_exports, {
1524
2045
  parseCsv: () => parseCsv,
1525
2046
  runBatch: () => runBatch
1526
2047
  });
1527
- import { mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile3, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
2048
+ import { mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile4, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
1528
2049
  import { tmpdir as tmpdir3 } from "node:os";
1529
2050
  import { join as join5, dirname as dirname5 } from "node:path";
1530
2051
  function overlayFromFlat(row, name) {
@@ -1563,8 +2084,8 @@ function overlayFromFlat(row, name) {
1563
2084
  }
1564
2085
  return doc;
1565
2086
  }
1566
- function parseCsv(text) {
1567
- const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
2087
+ function parseCsv(text2) {
2088
+ const lines = text2.split(/\r?\n/).filter((l) => l.trim().length > 0);
1568
2089
  if (lines.length < 2) return [];
1569
2090
  const split = (line) => {
1570
2091
  const out = [];
@@ -1599,14 +2120,14 @@ function parseCsv(text) {
1599
2120
  return row;
1600
2121
  });
1601
2122
  }
1602
- async function loadRows(path) {
1603
- const text = await readFile3(path, "utf8");
1604
- if (path.endsWith(".csv")) return parseCsv(text);
1605
- const parsed = JSON.parse(text);
1606
- if (!Array.isArray(parsed)) throw new Error(`${path}: expected a JSON array of row objects`);
2123
+ async function loadRows(path2) {
2124
+ const text2 = await readFile4(path2, "utf8");
2125
+ if (path2.endsWith(".csv")) return parseCsv(text2);
2126
+ const parsed = JSON.parse(text2);
2127
+ if (!Array.isArray(parsed)) throw new Error(`${path2}: expected a JSON array of row objects`);
1607
2128
  return parsed;
1608
2129
  }
1609
- async function runBatch(scene, rows, opts) {
2130
+ async function runBatch(scene2, rows, opts) {
1610
2131
  await mkdir3(opts.outDir, { recursive: true });
1611
2132
  const results = new Array(rows.length);
1612
2133
  let next = 0;
@@ -1619,7 +2140,7 @@ async function runBatch(scene, rows, opts) {
1619
2140
  let result;
1620
2141
  try {
1621
2142
  const rowOverlay = overlayFromFlat(row, name);
1622
- const { ir, report } = composeScene(scene, ...opts.baseOverlays, rowOverlay);
2143
+ const { ir, report } = composeScene(scene2, ...opts.baseOverlays, rowOverlay);
1623
2144
  const framesDir = await mkdtemp2(join5(tmpdir3(), `reframe-batch-${index}-`));
1624
2145
  const output = join5(opts.outDir, `${name}.mp4`);
1625
2146
  const plan = opts.noAudio ? null : resolveAudioPlan(compileScene(ir));
@@ -1682,22 +2203,20 @@ var init_batch = __esm({
1682
2203
  // ../render-cli/src/loadScene.ts
1683
2204
  var loadScene_exports = {};
1684
2205
  __export(loadScene_exports, {
2206
+ isComposition: () => isComposition,
2207
+ loadModule: () => loadModule,
1685
2208
  loadScene: () => loadScene
1686
2209
  });
1687
2210
  import { build as build2 } from "esbuild";
1688
- import { readFile as readFile4 } from "node:fs/promises";
2211
+ import { readFile as readFile5 } from "node:fs/promises";
1689
2212
  import { dirname as dirname6, resolve as resolve3 } from "node:path";
1690
2213
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1691
- async function loadScene(path) {
1692
- if (path.endsWith(".json")) {
1693
- const ir = JSON.parse(await readFile4(path, "utf8"));
1694
- validateScene(ir);
1695
- return ir;
1696
- }
2214
+ async function loadDefault(path2) {
2215
+ if (path2.endsWith(".json")) return JSON.parse(await readFile5(path2, "utf8"));
1697
2216
  let code;
1698
2217
  try {
1699
2218
  const out = await build2({
1700
- entryPoints: [path],
2219
+ entryPoints: [path2],
1701
2220
  bundle: true,
1702
2221
  format: "esm",
1703
2222
  platform: "neutral",
@@ -1710,15 +2229,33 @@ async function loadScene(path) {
1710
2229
  });
1711
2230
  code = out.outputFiles[0].text;
1712
2231
  } catch (err) {
1713
- throw new Error(
1714
- `failed to bundle ${path}:
1715
- ${err instanceof Error ? err.message : String(err)}`
1716
- );
2232
+ throw new Error(`failed to bundle ${path2}:
2233
+ ${err instanceof Error ? err.message : String(err)}`);
1717
2234
  }
1718
2235
  const mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
1719
- if (!mod.default) throw new Error(`${path} must default-export a scene`);
2236
+ if (mod.default === void 0) throw new Error(`${path2} must default-export a scene or composition`);
1720
2237
  return mod.default;
1721
2238
  }
2239
+ function isComposition(def) {
2240
+ return typeof def === "object" && def !== null && Array.isArray(def.scenes);
2241
+ }
2242
+ async function loadScene(path2) {
2243
+ const def = await loadDefault(path2);
2244
+ if (isComposition(def)) {
2245
+ throw new Error(`${path2} is a composition \u2014 render it directly, not as a single scene`);
2246
+ }
2247
+ validateScene(def);
2248
+ return def;
2249
+ }
2250
+ async function loadModule(path2) {
2251
+ const def = await loadDefault(path2);
2252
+ if (isComposition(def)) {
2253
+ validateComposition(def);
2254
+ return { kind: "composition", ir: def };
2255
+ }
2256
+ validateScene(def);
2257
+ return { kind: "scene", ir: def };
2258
+ }
1722
2259
  var HERE, CORE_ENTRY;
1723
2260
  var init_loadScene = __esm({
1724
2261
  "../render-cli/src/loadScene.ts"() {
@@ -1731,7 +2268,7 @@ var init_loadScene = __esm({
1731
2268
 
1732
2269
  // ../render-cli/src/reframe.ts
1733
2270
  import { spawn as spawn3, spawnSync } from "node:child_process";
1734
- import { existsSync as existsSync3 } from "node:fs";
2271
+ import { existsSync as existsSync4 } from "node:fs";
1735
2272
  import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
1736
2273
  import { basename, isAbsolute as isAbsolute3, join as join6, resolve as resolve4 } from "node:path";
1737
2274
  import { dirname as dirname7 } from "node:path";
@@ -1741,6 +2278,7 @@ var HERE2 = dirname7(fileURLToPath5(import.meta.url));
1741
2278
  var ROOT2 = PACKAGED ? resolve4(HERE2, "..") : resolve4(HERE2, "..", "..", "..");
1742
2279
  var USER_CWD = process.env.INIT_CWD ?? process.cwd();
1743
2280
  var RENDER_CLI = PACKAGED ? join6(ROOT2, "dist", "cli.js") : join6(ROOT2, "packages", "render-cli", "src", "cli.ts");
2281
+ var LABELS = PACKAGED ? join6(ROOT2, "dist", "labels.js") : join6(ROOT2, "packages", "render-cli", "src", "labels.ts");
1744
2282
  var ANALYZE = PACKAGED ? join6(ROOT2, "dist", "analyze.js") : join6(ROOT2, "benchmark", "harness", "motion", "analyze.ts");
1745
2283
  var TRACE = PACKAGED ? join6(ROOT2, "dist", "trace-cli.js") : join6(ROOT2, "benchmark", "harness", "motion", "trace-cli.ts");
1746
2284
  var CMD = PACKAGED ? "reframe" : "pnpm reframe";
@@ -1749,8 +2287,12 @@ var USAGE = `reframe \u2014 declarative motion graphics
1749
2287
  usage:
1750
2288
  ${CMD} render <scene.ts|.json|.html> [--overlay edits.json]... [-o out.mp4] [--fps N] [--duration S] [--no-audio]
1751
2289
  ${CMD} batch <scene.ts> <data.json|csv> [-o outDir] [--overlay base.json]... [--concurrency N] [--fps N]
2290
+ ${CMD} logo <logo.svg|brand-slug> ["Name"] [--motion <preset>] [--energy 0..1] [--seed N] [-o out.mp4]
2291
+ animate a logo into a sting (presets: draw-bloom, punch-in,
2292
+ rise-settle, slide-bank, reveal-orbit, spin-forge)
1752
2293
  ${CMD} preview open the scrub/edit UI (lists scenes in your directory)
1753
2294
  ${CMD} new <scene-name> scaffold <scene-name>.ts in your directory
2295
+ ${CMD} labels <scene.ts|.json> print the event clock (label \u2192 exact seconds; for sound design / timing)
1754
2296
  ${CMD} motion <mp4|framesDir> motion-profile a rendered clip
1755
2297
  ${CMD} trace <ref.mp4> [--apply scene.ts] extract a video's motion structure \u2192 MotionSketch / timeline
1756
2298
  ${CMD} guide [--regen] print the scene-authoring guide (for you or your AI)
@@ -1775,9 +2317,9 @@ function run(cmd, args, opts = {}) {
1775
2317
  });
1776
2318
  let sawBrowserError = false;
1777
2319
  proc.stderr.on("data", (d) => {
1778
- const text = d.toString();
1779
- if (/Executable doesn't exist|browserType\.launch/.test(text)) sawBrowserError = true;
1780
- process.stderr.write(text);
2320
+ const text2 = d.toString();
2321
+ if (/Executable doesn't exist|browserType\.launch/.test(text2)) sawBrowserError = true;
2322
+ process.stderr.write(text2);
1781
2323
  });
1782
2324
  proc.on("close", (code) => {
1783
2325
  if (code !== 0 && sawBrowserError) {
@@ -1858,7 +2400,7 @@ async function main() {
1858
2400
 
1859
2401
  ${USAGE}`);
1860
2402
  const inputPath = userPath(input);
1861
- if (!existsSync3(inputPath)) fail(`no such file: ${inputPath}`);
2403
+ if (!existsSync4(inputPath)) fail(`no such file: ${inputPath}`);
1862
2404
  const mode = /\.(ts|json)$/.test(input) ? "ir" : /\.html$/.test(input) ? "html" : null;
1863
2405
  if (!mode) {
1864
2406
  fail(`cannot infer render mode from "${input}" \u2014 expected .ts/.json (reframe scene) or .html (GSAP page)`);
@@ -1881,12 +2423,60 @@ ${USAGE}`);
1881
2423
  await (PACKAGED ? run(process.execPath, [RENDER_CLI, mode, inputPath, ...outArgs]) : run("npx", ["tsx", RENDER_CLI, mode, inputPath, ...outArgs]))
1882
2424
  );
1883
2425
  }
2426
+ case "labels": {
2427
+ const input = rest[0];
2428
+ if (!input || input.startsWith("-")) fail(`labels needs a scene file
2429
+
2430
+ ${USAGE}`);
2431
+ const inputPath = userPath(input);
2432
+ if (!existsSync4(inputPath)) fail(`no such file: ${inputPath}`);
2433
+ process.exit(
2434
+ await (PACKAGED ? run(process.execPath, [LABELS, inputPath]) : run("npx", ["tsx", LABELS, inputPath]))
2435
+ );
2436
+ }
2437
+ case "logo": {
2438
+ const positional = [];
2439
+ const flags = {};
2440
+ for (let i = 0; i < rest.length; i++) {
2441
+ const a = rest[i];
2442
+ if (a.startsWith("--")) flags[a.slice(2)] = rest[++i] ?? "";
2443
+ else if (a === "-o") flags.o = rest[++i] ?? "";
2444
+ else positional.push(a);
2445
+ }
2446
+ const arg = positional[0];
2447
+ if (!arg) {
2448
+ fail(`usage: ${CMD} logo <logo.svg | brand-slug> ["Display Name"] [--motion <preset>] [--energy 0..1] [--speed n] [--intensity 0..1] [--from left|right|top|bottom] [--seed n] [-o out.mp4]`);
2449
+ }
2450
+ preflightFfmpeg();
2451
+ const { tmpdir: tmpdir4 } = await import("node:os");
2452
+ const { resolveLogo: resolveLogo2, buildLogoSting: buildLogoSting2 } = await Promise.resolve().then(() => (init_logoSting(), logoSting_exports));
2453
+ const num = (k) => flags[k] !== void 0 ? Number(flags[k]) : void 0;
2454
+ console.log(`loading logo: ${arg} \u2026`);
2455
+ const { data, slug } = await resolveLogo2(arg, positional[1], {
2456
+ motion: flags.motion,
2457
+ energy: num("energy"),
2458
+ speed: num("speed"),
2459
+ intensity: num("intensity"),
2460
+ from: flags.from,
2461
+ seed: num("seed")
2462
+ });
2463
+ const sceneIR = buildLogoSting2(data);
2464
+ const tmp = join6(tmpdir4(), `reframe-logo-${slug}-${process.pid}.json`);
2465
+ await writeFile5(tmp, JSON.stringify(sceneIR));
2466
+ const outBase = PACKAGED ? join6(USER_CWD, "out") : join6(ROOT2, "out");
2467
+ const out = flags.o ? userPath(flags.o) : join6(outBase, `logo-${slug}.mp4`);
2468
+ await mkdir4(dirname7(out), { recursive: true });
2469
+ console.log(`rendering ${data.name} (${data.paths.length} path${data.paths.length > 1 ? "s" : ""}, motion: ${data.motion ?? "reveal-orbit"}) \u2192 ${out}`);
2470
+ process.exit(
2471
+ await (PACKAGED ? run(process.execPath, [RENDER_CLI, "ir", tmp, "-o", out, "--no-audio"]) : run("npx", ["tsx", RENDER_CLI, "ir", tmp, "-o", out, "--no-audio"]))
2472
+ );
2473
+ }
1884
2474
  case "batch": {
1885
2475
  const [sceneArg, dataArg, ...flags] = rest;
1886
2476
  if (!sceneArg || !dataArg) fail(`usage: ${CMD} batch <scene.ts> <data.json|csv> [...]`);
1887
2477
  const scenePath = userPath(sceneArg);
1888
2478
  const dataPath = userPath(dataArg);
1889
- for (const p of [scenePath, dataPath]) if (!existsSync3(p)) fail(`no such file: ${p}`);
2479
+ for (const p of [scenePath, dataPath]) if (!existsSync4(p)) fail(`no such file: ${p}`);
1890
2480
  preflightFfmpeg();
1891
2481
  let outDir = PACKAGED ? join6(USER_CWD, "out", "batch") : join6(ROOT2, "out", "batch");
1892
2482
  let concurrency = 3;
@@ -1901,15 +2491,15 @@ ${USAGE}`);
1901
2491
  }
1902
2492
  const { loadRows: loadRows2, runBatch: runBatch2 } = await Promise.resolve().then(() => (init_batch(), batch_exports));
1903
2493
  const { loadScene: loadScene2 } = await Promise.resolve().then(() => (init_loadScene(), loadScene_exports));
1904
- const { readFile: readFile5 } = await import("node:fs/promises");
1905
- const scene = await loadScene2(scenePath);
2494
+ const { readFile: readFile6 } = await import("node:fs/promises");
2495
+ const scene2 = await loadScene2(scenePath);
1906
2496
  const baseOverlays = await Promise.all(
1907
- baseOverlayPaths.map(async (p) => JSON.parse(await readFile5(p, "utf8")))
2497
+ baseOverlayPaths.map(async (p) => JSON.parse(await readFile6(p, "utf8")))
1908
2498
  );
1909
2499
  const rows = await loadRows2(dataPath);
1910
2500
  if (rows.length === 0) fail(`${dataPath}: no data rows`);
1911
2501
  console.log(`batch: ${rows.length} rows \xD7 ${concurrency} workers \u2192 ${outDir}`);
1912
- const results = await runBatch2(scene, rows, {
2502
+ const results = await runBatch2(scene2, rows, {
1913
2503
  outDir,
1914
2504
  baseOverlays,
1915
2505
  concurrency,
@@ -1932,6 +2522,9 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
1932
2522
  process.exit(failed > 0 ? 1 : 0);
1933
2523
  }
1934
2524
  case "preview": {
2525
+ console.log(
2526
+ "preview: drag waypoints/nodes; double-click a path to add a waypoint or a handle to remove it;\n \u270E reshapes an ease curve; 'vary \xD74' proposes motion variants.\ndeep-link a scene + time: http://localhost:5173/?scene=<scene-name>&t=<seconds>"
2527
+ );
1935
2528
  if (PACKAGED) {
1936
2529
  const { createRequire } = await import("node:module");
1937
2530
  const vitePkg = createRequire(import.meta.url).resolve("vite/package.json");
@@ -1958,7 +2551,7 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
1958
2551
  const targetDir = inRepo ? join6(ROOT2, "examples", "scenes") : USER_CWD;
1959
2552
  const target = join6(targetDir, `${name}.ts`);
1960
2553
  const shown = inRepo ? `examples/scenes/${name}.ts` : `${name}.ts`;
1961
- if (existsSync3(target)) fail(`${shown} already exists`);
2554
+ if (existsSync4(target)) fail(`${shown} already exists`);
1962
2555
  const id = name.split("-")[0] ?? name;
1963
2556
  await writeFile5(target, SCENE_TEMPLATE(name, id));
1964
2557
  console.log(`created ${shown}
@@ -1988,8 +2581,8 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
1988
2581
  }
1989
2582
  case "guide": {
1990
2583
  const file = rest.includes("--regen") ? PACKAGED ? join6(ROOT2, "guides", "regen-contract.md") : join6(ROOT2, "docs", "regen-contract.md") : PACKAGED ? join6(ROOT2, "guides", "edsl-guide.md") : join6(ROOT2, "benchmark", "guides", "edsl-guide.md");
1991
- const { readFile: readFile5 } = await import("node:fs/promises");
1992
- process.stdout.write(await readFile5(file, "utf8"));
2584
+ const { readFile: readFile6 } = await import("node:fs/promises");
2585
+ process.stdout.write(await readFile6(file, "utf8"));
1993
2586
  return;
1994
2587
  }
1995
2588
  case "demo":