reframe-video 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -10,16 +10,100 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // ../core/src/ir.ts
13
- var DEFAULT_TO_DURATION, DEFAULT_TWEEN_DURATION;
13
+ var DEFAULT_TO_DURATION, DEFAULT_TWEEN_DURATION, DEFAULT_MOTIONPATH_DURATION;
14
14
  var init_ir = __esm({
15
15
  "../core/src/ir.ts"() {
16
16
  "use strict";
17
17
  DEFAULT_TO_DURATION = 0.5;
18
18
  DEFAULT_TWEEN_DURATION = 0.5;
19
+ DEFAULT_MOTIONPATH_DURATION = 1;
20
+ }
21
+ });
22
+
23
+ // ../core/src/path.ts
24
+ function locate(segCount, u) {
25
+ if (segCount <= 0) return { i: 0, t: 0 };
26
+ const clamped = Math.max(0, Math.min(1, u));
27
+ const scaled = clamped * segCount;
28
+ let i = Math.floor(scaled);
29
+ if (i >= segCount) i = segCount - 1;
30
+ return { i, t: scaled - i };
31
+ }
32
+ function controls(points, closed, i) {
33
+ const n = points.length;
34
+ const at = (k) => {
35
+ if (closed) return points[(k % n + n) % n];
36
+ return points[Math.max(0, Math.min(n - 1, k))];
37
+ };
38
+ return [at(i - 1), at(i), at(i + 1), at(i + 2)];
39
+ }
40
+ function segCountOf(points, closed) {
41
+ const n = points.length;
42
+ if (n < 2) return 0;
43
+ return closed ? n : n - 1;
44
+ }
45
+ function pathPoint(points, closed, u) {
46
+ const n = points.length;
47
+ if (n === 0) return [0, 0];
48
+ if (n === 1) return [points[0][0], points[0][1]];
49
+ const segs = segCountOf(points, closed);
50
+ const { i, t } = locate(segs, u);
51
+ const [p0, p1, p2, p3] = controls(points, closed, i);
52
+ const t2 = t * t;
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])];
56
+ }
57
+ function pathTangentAngle(points, closed, u) {
58
+ const n = points.length;
59
+ if (n < 2) return 0;
60
+ const segs = segCountOf(points, closed);
61
+ const { i, t } = locate(segs, u);
62
+ const [p0, p1, p2, p3] = controls(points, closed, i);
63
+ 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]);
67
+ if (dx === 0 && dy === 0) return 0;
68
+ return Math.atan2(dy, dx) * 180 / Math.PI;
69
+ }
70
+ var init_path = __esm({
71
+ "../core/src/path.ts"() {
72
+ "use strict";
19
73
  }
20
74
  });
21
75
 
22
76
  // ../core/src/compile.ts
77
+ function scaleTimeline(tl, k) {
78
+ switch (tl.kind) {
79
+ case "seq":
80
+ case "par":
81
+ return { ...tl, children: tl.children.map((c) => scaleTimeline(c, k)) };
82
+ case "stagger":
83
+ return { ...tl, interval: tl.interval * k, children: tl.children.map((c) => scaleTimeline(c, k)) };
84
+ case "wait":
85
+ return { ...tl, duration: tl.duration * k };
86
+ case "tween":
87
+ return { ...tl, duration: (tl.duration ?? DEFAULT_TWEEN_DURATION) * k };
88
+ case "motionPath":
89
+ return { ...tl, duration: (tl.duration ?? DEFAULT_MOTIONPATH_DURATION) * k };
90
+ case "to":
91
+ return {
92
+ ...tl,
93
+ duration: (tl.duration ?? DEFAULT_TO_DURATION) * k,
94
+ ...tl.stagger !== void 0 && { stagger: tl.stagger * k }
95
+ };
96
+ case "beat":
97
+ return {
98
+ ...tl,
99
+ children: tl.children.map((c) => scaleTimeline(c, k)),
100
+ ...tl.gap !== void 0 && { gap: tl.gap * k }
101
+ };
102
+ }
103
+ }
104
+ function orderBeats(children) {
105
+ return children.map((c, i) => ({ c, i, key: c.kind === "beat" && c.order !== void 0 ? c.order : i })).sort((a, b) => a.key - b.key || a.i - b.i).map((x) => x.c);
106
+ }
23
107
  function compileScene(ir) {
24
108
  const nodeById = /* @__PURE__ */ new Map();
25
109
  const nodeOrder = [];
@@ -48,6 +132,7 @@ function compileScene(ir) {
48
132
  }
49
133
  }
50
134
  const segments = /* @__PURE__ */ new Map();
135
+ const motionPaths = /* @__PURE__ */ new Map();
51
136
  const current = new Map(initialValues);
52
137
  const pushSegment = (seg) => {
53
138
  const k = key(seg.target, seg.prop);
@@ -64,6 +149,50 @@ function compileScene(ir) {
64
149
  throw new Error(`cannot animate "${prop}" of "${target}": no base value to start from`);
65
150
  };
66
151
  const labelTimes = /* @__PURE__ */ new Map();
152
+ const beatTimes = /* @__PURE__ */ new Map();
153
+ const durationOf = (tl, start) => {
154
+ switch (tl.kind) {
155
+ case "seq": {
156
+ let t = start;
157
+ for (const child of orderBeats(tl.children)) t = durationOf(child, t);
158
+ return t;
159
+ }
160
+ case "par": {
161
+ let end = start;
162
+ for (const child of tl.children) end = Math.max(end, durationOf(child, start));
163
+ return end;
164
+ }
165
+ case "stagger": {
166
+ let end = start;
167
+ tl.children.forEach((child, i) => {
168
+ end = Math.max(end, durationOf(child, start + i * tl.interval));
169
+ });
170
+ return end;
171
+ }
172
+ case "wait":
173
+ return start + tl.duration;
174
+ case "tween":
175
+ return start + (tl.duration ?? DEFAULT_TWEEN_DURATION);
176
+ case "motionPath":
177
+ return start + (tl.duration ?? DEFAULT_MOTIONPATH_DURATION);
178
+ case "to": {
179
+ const override = ir.states?.[tl.state] ?? {};
180
+ const duration = tl.duration ?? DEFAULT_TO_DURATION;
181
+ const si = tl.stagger ?? 0;
182
+ const targets = nodeOrder.filter(
183
+ (id) => id in override && (tl.filter === void 0 || tl.filter.includes(id))
184
+ );
185
+ return start + duration + Math.max(0, targets.length - 1) * si;
186
+ }
187
+ case "beat": {
188
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
189
+ const natural = durationOf(grouping, 0);
190
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, natural) : 1);
191
+ const beatStart = tl.at ?? start + (tl.gap ?? 0);
192
+ return beatStart + k * natural;
193
+ }
194
+ }
195
+ };
67
196
  const walk = (tl, start) => {
68
197
  const end = walkInner(tl, start);
69
198
  if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
@@ -73,9 +202,19 @@ function compileScene(ir) {
73
202
  switch (tl.kind) {
74
203
  case "seq": {
75
204
  let t = start;
76
- for (const child of tl.children) t = walk(child, t);
205
+ for (const child of orderBeats(tl.children)) t = walk(child, t);
77
206
  return t;
78
207
  }
208
+ case "beat": {
209
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
210
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
211
+ const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
212
+ const beatStart = tl.at ?? start + (tl.gap ?? 0);
213
+ const end = walk(inner, beatStart);
214
+ beatTimes.set(tl.name, { t0: beatStart, t1: end });
215
+ labelTimes.set(tl.name, { t0: beatStart, t1: end });
216
+ return end;
217
+ }
79
218
  case "par": {
80
219
  let end = start;
81
220
  for (const child of tl.children) end = Math.max(end, walk(child, start));
@@ -105,6 +244,23 @@ function compileScene(ir) {
105
244
  }
106
245
  return start + duration;
107
246
  }
247
+ case "motionPath": {
248
+ const duration = tl.duration ?? DEFAULT_MOTIONPATH_DURATION;
249
+ const points = tl.points;
250
+ const closed = tl.closed ?? false;
251
+ const autoRotate = tl.autoRotate ?? false;
252
+ const rotateOffset = tl.rotateOffset ?? 0;
253
+ let list = motionPaths.get(tl.target);
254
+ 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 } });
256
+ if (points.length > 0) {
257
+ const [ex, ey] = pathPoint(points, closed, 1);
258
+ current.set(key(tl.target, "x"), ex);
259
+ current.set(key(tl.target, "y"), ey);
260
+ if (autoRotate) current.set(key(tl.target, "rotation"), pathTangentAngle(points, closed, 1) + rotateOffset);
261
+ }
262
+ return start + duration;
263
+ }
108
264
  case "to": {
109
265
  const override = ir.states?.[tl.state] ?? {};
110
266
  const duration = tl.duration ?? DEFAULT_TO_DURATION;
@@ -133,14 +289,17 @@ function compileScene(ir) {
133
289
  };
134
290
  const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
135
291
  for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
292
+ for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
136
293
  return {
137
294
  ir,
138
295
  duration: ir.duration ?? inferredEnd,
139
296
  segments,
297
+ motionPaths,
140
298
  initialValues,
141
299
  nodeById,
142
300
  nodeOrder,
143
- labelTimes
301
+ labelTimes,
302
+ beatTimes
144
303
  };
145
304
  }
146
305
  var key;
@@ -148,6 +307,7 @@ var init_compile = __esm({
148
307
  "../core/src/compile.ts"() {
149
308
  "use strict";
150
309
  init_ir();
310
+ init_path();
151
311
  key = (target, prop) => `${target}.${prop}`;
152
312
  }
153
313
  });
@@ -195,11 +355,11 @@ function validateScene(ir) {
195
355
  );
196
356
  }
197
357
  const labels = /* @__PURE__ */ new Set();
198
- const checkTimeline = (tl, path) => {
358
+ const checkTimeline = (tl, path2) => {
199
359
  if ("label" in tl && tl.label !== void 0) {
200
360
  if (labels.has(tl.label)) {
201
361
  problems.push(
202
- `${path}: duplicate timeline label "${tl.label}" \u2014 labels are overlay addresses and must be unique`
362
+ `${path2}: duplicate timeline label "${tl.label}" \u2014 labels are overlay addresses and must be unique`
203
363
  );
204
364
  }
205
365
  labels.add(tl.label);
@@ -207,33 +367,63 @@ function validateScene(ir) {
207
367
  switch (tl.kind) {
208
368
  case "seq":
209
369
  case "par":
210
- tl.children.forEach((c, i) => checkTimeline(c, `${path}.${tl.kind}[${i}]`));
370
+ tl.children.forEach((c, i) => checkTimeline(c, `${path2}.${tl.kind}[${i}]`));
211
371
  break;
212
372
  case "stagger":
213
- if (tl.interval < 0) problems.push(`${path}: stagger interval must be >= 0`);
214
- tl.children.forEach((c, i) => checkTimeline(c, `${path}.stagger[${i}]`));
373
+ if (tl.interval < 0) problems.push(`${path2}: stagger interval must be >= 0`);
374
+ tl.children.forEach((c, i) => checkTimeline(c, `${path2}.stagger[${i}]`));
215
375
  break;
216
376
  case "to":
217
377
  if (!(tl.state in states)) {
218
378
  problems.push(
219
- `${path}: to("${tl.state}") references an undefined state \u2014 defined states: ${Object.keys(states).join(", ") || "(none)"}`
379
+ `${path2}: to("${tl.state}") references an undefined state \u2014 defined states: ${Object.keys(states).join(", ") || "(none)"}`
220
380
  );
221
381
  }
222
382
  if (tl.duration !== void 0 && tl.duration <= 0) {
223
- problems.push(`${path}: to("${tl.state}") duration must be > 0`);
383
+ problems.push(`${path2}: to("${tl.state}") duration must be > 0`);
224
384
  }
225
385
  for (const id of tl.filter ?? []) {
226
- if (!nodeById.has(id)) problems.push(`${path}: filter contains unknown node "${id}"`);
386
+ if (!nodeById.has(id)) problems.push(`${path2}: filter contains unknown node "${id}"`);
227
387
  }
228
388
  break;
229
389
  case "tween":
230
- checkProps(path, tl.target, tl.props);
390
+ checkProps(path2, tl.target, tl.props);
231
391
  if (tl.duration !== void 0 && tl.duration <= 0) {
232
- problems.push(`${path}: tween duration must be > 0`);
392
+ problems.push(`${path2}: tween duration must be > 0`);
233
393
  }
234
394
  break;
395
+ case "motionPath": {
396
+ const node = nodeById.get(tl.target);
397
+ if (!node) {
398
+ problems.push(
399
+ `${path2}: motionPath targets unknown node "${tl.target}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
400
+ );
401
+ } else if (node.type === "line") {
402
+ problems.push(`${path2}: motionPath cannot target a line (no x/y) \u2014 "${tl.target}"`);
403
+ }
404
+ if (tl.points.length < 1) problems.push(`${path2}: motionPath "${tl.target}" needs at least 1 point`);
405
+ if (tl.duration !== void 0 && tl.duration <= 0) {
406
+ problems.push(`${path2}: motionPath "${tl.target}" duration must be > 0`);
407
+ }
408
+ break;
409
+ }
235
410
  case "wait":
236
- if (tl.duration < 0) problems.push(`${path}: wait duration must be >= 0`);
411
+ if (tl.duration < 0) problems.push(`${path2}: wait duration must be >= 0`);
412
+ break;
413
+ case "beat":
414
+ if (labels.has(tl.name)) {
415
+ problems.push(
416
+ `${path2}: duplicate timeline label "${tl.name}" (beat name) \u2014 labels are overlay addresses and must be unique`
417
+ );
418
+ }
419
+ labels.add(tl.name);
420
+ if (tl.duration !== void 0 && tl.duration <= 0) {
421
+ problems.push(`${path2}: beat "${tl.name}" duration must be > 0`);
422
+ }
423
+ if (tl.scale !== void 0 && tl.scale <= 0) {
424
+ problems.push(`${path2}: beat "${tl.name}" scale must be > 0`);
425
+ }
426
+ tl.children.forEach((c, i) => checkTimeline(c, `${path2}.beat(${tl.name})[${i}]`));
237
427
  break;
238
428
  }
239
429
  };
@@ -284,6 +474,7 @@ var init_validate = __esm({
284
474
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
285
475
  text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
286
476
  image: [...COMMON_PROPS, "src", "width", "height"],
477
+ path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
287
478
  group: COMMON_PROPS
288
479
  };
289
480
  SceneValidationError = class extends Error {
@@ -299,6 +490,51 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
299
490
  });
300
491
 
301
492
  // ../core/src/dsl.ts
493
+ function scene(input) {
494
+ const ir = { version: 1, ...input };
495
+ validateScene(ir);
496
+ if (ir.duration === void 0 && ir.timeline) {
497
+ ir.duration = compileScene(ir).duration;
498
+ }
499
+ return ir;
500
+ }
501
+ function rect(props) {
502
+ const { id, ...rest } = props;
503
+ return { type: "rect", id, props: rest };
504
+ }
505
+ function text(props) {
506
+ const { id, ...rest } = props;
507
+ return { type: "text", id, props: rest };
508
+ }
509
+ function path(props) {
510
+ const { id, ...rest } = props;
511
+ return { type: "path", id, props: rest };
512
+ }
513
+ function group(props, children) {
514
+ const { id, ...rest } = props;
515
+ return { type: "group", id, props: rest, children };
516
+ }
517
+ function seq(...children) {
518
+ return { kind: "seq", children };
519
+ }
520
+ function par(...children) {
521
+ return { kind: "par", children };
522
+ }
523
+ function stagger(interval, ...children) {
524
+ return { kind: "stagger", interval, children };
525
+ }
526
+ function beat(name, opts, children) {
527
+ return { kind: "beat", name, children, ...opts };
528
+ }
529
+ function tween(target, props, opts = {}) {
530
+ return { kind: "tween", target, props, ...opts };
531
+ }
532
+ function wait(duration, label) {
533
+ return { kind: "wait", duration, ...label !== void 0 && { label } };
534
+ }
535
+ function motionPath(target, points, opts = {}) {
536
+ return { kind: "motionPath", target, points, ...opts };
537
+ }
302
538
  var init_dsl = __esm({
303
539
  "../core/src/dsl.ts"() {
304
540
  "use strict";
@@ -430,13 +666,16 @@ function applyOverlay(ir, overlay, layer, report) {
430
666
  const byLabel = /* @__PURE__ */ new Map();
431
667
  const walkTimeline = (tl) => {
432
668
  if ("label" in tl && tl.label !== void 0) byLabel.set(tl.label, tl);
669
+ if (tl.kind === "beat") byLabel.set(tl.name, tl);
433
670
  if ("children" in tl) tl.children.forEach(walkTimeline);
434
671
  };
435
672
  if (ir.timeline) walkTimeline(ir.timeline);
436
673
  const PATCHABLE = {
437
674
  to: ["duration", "ease", "stagger"],
438
675
  tween: ["duration", "ease"],
439
- wait: ["duration"]
676
+ wait: ["duration"],
677
+ motionPath: ["points", "duration", "ease"],
678
+ beat: ["at", "gap", "scale", "duration", "order"]
440
679
  };
441
680
  let timingPatched = false;
442
681
  for (const [label, patch] of Object.entries(overlay.timeline)) {
@@ -460,7 +699,7 @@ function applyOverlay(ir, overlay, layer, report) {
460
699
  }
461
700
  step[key2] = value;
462
701
  applied(`timeline.${label}.${key2}`, "set");
463
- if (key2 === "duration" || key2 === "stagger") timingPatched = true;
702
+ if (["duration", "stagger", "at", "gap", "scale", "order"].includes(key2)) timingPatched = true;
464
703
  }
465
704
  }
466
705
  if (timingPatched && overlay.scene?.duration === void 0) {
@@ -484,6 +723,185 @@ var init_compose = __esm({
484
723
  }
485
724
  });
486
725
 
726
+ // ../core/src/presets.ts
727
+ function makeRng(seed) {
728
+ let a = seed >>> 0 || 2654435769;
729
+ return () => {
730
+ a = a + 1831565813 | 0;
731
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
732
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
733
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
734
+ };
735
+ }
736
+ function ctx(o) {
737
+ const rand = makeRng((o.seed ?? 0) + 1);
738
+ return {
739
+ e: clamp01(o.energy ?? 0.5),
740
+ sp: Math.max(0.25, o.speed ?? 1),
741
+ it: clamp01(o.intensity ?? 0.5),
742
+ from: o.from,
743
+ rand,
744
+ jit: (amp) => (rand() - 0.5) * 2 * amp,
745
+ g: o.target.group,
746
+ cx: o.target.center[0],
747
+ cy: o.target.center[1],
748
+ s: o.target.baseScale,
749
+ fills: o.target.fills,
750
+ inks: o.target.inks
751
+ };
752
+ }
753
+ function settleEase(e) {
754
+ return e < 0.34 ? "easeOutCubic" : e < 0.67 ? "easeOutBack" : "easeOutElastic";
755
+ }
756
+ function fromVec(from, dist) {
757
+ switch (from) {
758
+ case "left":
759
+ return [-dist, 0];
760
+ case "right":
761
+ return [dist, 0];
762
+ case "top":
763
+ return [0, -dist];
764
+ default:
765
+ return [0, dist];
766
+ }
767
+ }
768
+ function fadeFills(c, base = 0.4, gap = 0.06) {
769
+ return stagger(
770
+ gap / c.sp,
771
+ ...c.fills.map(
772
+ (id, i) => tween(id, { opacity: 1 }, { duration: dur(base, c.sp), ease: "easeOutQuad", ...i === 0 && { label: "reveal" } })
773
+ )
774
+ );
775
+ }
776
+ function drawInks(c) {
777
+ return stagger(
778
+ 0.15 / c.sp,
779
+ ...c.inks.map(
780
+ (id, i) => tween(id, { progress: 1 }, { duration: dur(1.3 + c.jit(0.2), c.sp), ease: "easeInOutQuad", ...i === 0 && { label: "draw" } })
781
+ )
782
+ );
783
+ }
784
+ function motionPreset(name, opts) {
785
+ const c = ctx(opts);
786
+ switch (name) {
787
+ case "draw-bloom":
788
+ return beat("draw-bloom", {}, [
789
+ drawInks(c),
790
+ fadeFills(c, 0.45),
791
+ tween(c.g, { scale: c.s * (1.02 + 0.05 * c.e) }, { duration: dur(2.4, c.sp), ease: "easeInOutQuad", label: "settle" })
792
+ ]);
793
+ case "punch-in": {
794
+ const peak = c.s * (1 + 0.06 + 0.24 * c.e + c.jit(0.02));
795
+ return beat("punch-in", {}, [
796
+ par(
797
+ fadeFills(c, 0.25),
798
+ seq(
799
+ tween(c.g, { scale: peak }, { duration: dur(0.45 + c.jit(0.05), c.sp), ease: "easeOutCubic", label: "punch" }),
800
+ tween(c.g, { scale: c.s }, { duration: dur(0.5, c.sp), ease: settleEase(c.e) })
801
+ )
802
+ )
803
+ ]);
804
+ }
805
+ case "rise-settle": {
806
+ const es = 0.65 + c.rand() * 0.7;
807
+ const dist = (220 + 260 * c.it) * es;
808
+ const [dx, dy] = fromVec(c.from ?? "bottom", dist);
809
+ const jx = c.jit(110);
810
+ return beat("rise-settle", {}, [
811
+ par(
812
+ motionPath(
813
+ c.g,
814
+ [
815
+ [c.cx + dx + jx, c.cy + dy],
816
+ [c.cx + dx * 0.4 - jx * 0.6, c.cy + dy * 0.4],
817
+ [c.cx, c.cy]
818
+ ],
819
+ { duration: dur(1.1, c.sp), ease: settleEase(c.e), label: "rise" }
820
+ ),
821
+ fadeFills(c, 0.4)
822
+ )
823
+ ]);
824
+ }
825
+ case "slide-bank": {
826
+ const es = 0.65 + c.rand() * 0.7;
827
+ const dist = (420 + 240 * c.it) * es;
828
+ const [dx, dy] = fromVec(c.from ?? "left", dist);
829
+ const arc = c.jit(140);
830
+ const midx = c.jit(120);
831
+ const move = dur(1.2, c.sp);
832
+ return beat("slide-bank", {}, [
833
+ par(
834
+ motionPath(
835
+ c.g,
836
+ [
837
+ [c.cx + dx, c.cy + dy],
838
+ [c.cx + dx * 0.4 + midx, c.cy + dy * 0.4 - 70 - arc],
839
+ [c.cx, c.cy]
840
+ ],
841
+ { duration: move, ease: settleEase(c.e), autoRotate: true, label: "slide" }
842
+ ),
843
+ // level the bank out once it lands (authored after the path → wins for rotation)
844
+ seq(wait(move), tween(c.g, { rotation: 0 }, { duration: dur(0.5, c.sp), ease: "easeOutCubic" })),
845
+ fadeFills(c, 0.4)
846
+ )
847
+ ]);
848
+ }
849
+ case "reveal-orbit": {
850
+ const es = 0.65 + c.rand() * 0.7;
851
+ const orbit = (180 + 160 * c.it) * es;
852
+ const jx = c.jit(0.4);
853
+ const jy = c.jit(0.4);
854
+ return beat("reveal-orbit", {}, [
855
+ drawInks(c),
856
+ fadeFills(c, 0.45),
857
+ par(
858
+ motionPath(
859
+ c.g,
860
+ [
861
+ [c.cx, c.cy],
862
+ [c.cx - orbit * (1 + jx), c.cy - orbit * 0.8],
863
+ [c.cx + orbit * (1 + jy), c.cy - orbit],
864
+ [c.cx, c.cy]
865
+ ],
866
+ { duration: dur(1.7, c.sp), ease: "easeInOutCubic", label: "orbit" }
867
+ ),
868
+ seq(
869
+ tween(c.g, { scale: c.s * (1.12 + 0.1 * c.e) }, { duration: dur(0.85, c.sp), ease: "easeOutBack" }),
870
+ tween(c.g, { scale: c.s }, { duration: dur(0.85, c.sp), ease: "easeInOutQuad" })
871
+ )
872
+ )
873
+ ]);
874
+ }
875
+ case "spin-forge": {
876
+ const turns = 1 + Math.round(c.it);
877
+ const dir = c.rand() < 0.5 ? -1 : 1;
878
+ const startRot = dir * 360 * turns;
879
+ const peak = c.s * (1 + 0.05 + 0.2 * c.e);
880
+ return beat("spin-forge", {}, [
881
+ par(
882
+ seq(
883
+ tween(c.g, { scale: c.s * 0.2, rotation: startRot }, { duration: SET }),
884
+ // establish (invisible)
885
+ tween(c.g, { scale: peak, rotation: 0 }, { duration: dur(0.9, c.sp), ease: "easeOutBack", label: "spin" }),
886
+ tween(c.g, { scale: c.s }, { duration: dur(0.3, c.sp), ease: "easeInOutQuad" })
887
+ ),
888
+ seq(wait(SET), fadeFills(c, 0.3))
889
+ )
890
+ ]);
891
+ }
892
+ }
893
+ }
894
+ var clamp01, SET, dur;
895
+ var init_presets = __esm({
896
+ "../core/src/presets.ts"() {
897
+ "use strict";
898
+ init_dsl();
899
+ clamp01 = (x) => Math.max(0, Math.min(1, x));
900
+ SET = 1 / 120;
901
+ dur = (base, sp) => base / sp;
902
+ }
903
+ });
904
+
487
905
  // ../core/src/audio.ts
488
906
  function resolveAudioPlan(compiled) {
489
907
  const audio = compiled.ir.audio;
@@ -569,10 +987,23 @@ var init_behaviors = __esm({
569
987
  });
570
988
 
571
989
  // ../core/src/interpolate.ts
572
- var EASE_TABLE, EASE_NAMES;
990
+ function easeOutBounce(u) {
991
+ const n1 = 7.5625;
992
+ const d1 = 2.75;
993
+ if (u < 1 / d1) return n1 * u * u;
994
+ if (u < 2 / d1) return n1 * (u -= 1.5 / d1) * u + 0.75;
995
+ if (u < 2.5 / d1) return n1 * (u -= 2.25 / d1) * u + 0.9375;
996
+ return n1 * (u -= 2.625 / d1) * u + 0.984375;
997
+ }
998
+ var BACK_C1, BACK_C2, BACK_C3, ELASTIC_C4, ELASTIC_C5, EASE_TABLE, EASE_NAMES;
573
999
  var init_interpolate = __esm({
574
1000
  "../core/src/interpolate.ts"() {
575
1001
  "use strict";
1002
+ BACK_C1 = 1.70158;
1003
+ BACK_C2 = BACK_C1 * 1.525;
1004
+ BACK_C3 = BACK_C1 + 1;
1005
+ ELASTIC_C4 = 2 * Math.PI / 3;
1006
+ ELASTIC_C5 = 2 * Math.PI / 4.5;
576
1007
  EASE_TABLE = {
577
1008
  linear: (u) => u,
578
1009
  easeInQuad: (u) => u * u,
@@ -586,7 +1017,20 @@ var init_interpolate = __esm({
586
1017
  easeInOutQuart: (u) => u < 0.5 ? 8 * u ** 4 : 1 - (-2 * u + 2) ** 4 / 2,
587
1018
  easeInExpo: (u) => u === 0 ? 0 : 2 ** (10 * u - 10),
588
1019
  easeOutExpo: (u) => u === 1 ? 1 : 1 - 2 ** (-10 * u),
589
- easeInOutExpo: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? 2 ** (20 * u - 10) / 2 : (2 - 2 ** (-20 * u + 10)) / 2
1020
+ easeInOutExpo: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? 2 ** (20 * u - 10) / 2 : (2 - 2 ** (-20 * u + 10)) / 2,
1021
+ // --- expressive eases (GSAP's signature feel) — standard Penner equations ---
1022
+ // back: overshoots past the target then settles (pop / snap)
1023
+ easeInBack: (u) => BACK_C3 * u ** 3 - BACK_C1 * u * u,
1024
+ easeOutBack: (u) => 1 + BACK_C3 * (u - 1) ** 3 + BACK_C1 * (u - 1) ** 2,
1025
+ easeInOutBack: (u) => u < 0.5 ? (2 * u) ** 2 * ((BACK_C2 + 1) * 2 * u - BACK_C2) / 2 : ((2 * u - 2) ** 2 * ((BACK_C2 + 1) * (2 * u - 2) + BACK_C2) + 2) / 2,
1026
+ // elastic: rings around the target before settling (playful spring)
1027
+ easeInElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : -(2 ** (10 * u - 10)) * Math.sin((u * 10 - 10.75) * ELASTIC_C4),
1028
+ easeOutElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : 2 ** (-10 * u) * Math.sin((u * 10 - 0.75) * ELASTIC_C4) + 1,
1029
+ easeInOutElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? -(2 ** (20 * u - 10) * Math.sin((20 * u - 11.125) * ELASTIC_C5)) / 2 : 2 ** (-20 * u + 10) * Math.sin((20 * u - 11.125) * ELASTIC_C5) / 2 + 1,
1030
+ // bounce: drops and bounces to rest (lands without overshoot)
1031
+ easeInBounce: (u) => 1 - easeOutBounce(1 - u),
1032
+ easeOutBounce,
1033
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
590
1034
  };
591
1035
  EASE_NAMES = Object.keys(EASE_TABLE);
592
1036
  }
@@ -598,6 +1042,7 @@ var init_evaluate = __esm({
598
1042
  "use strict";
599
1043
  init_behaviors();
600
1044
  init_interpolate();
1045
+ init_path();
601
1046
  }
602
1047
  });
603
1048
 
@@ -638,6 +1083,14 @@ var init_assets = __esm({
638
1083
  }
639
1084
  });
640
1085
 
1086
+ // ../core/src/motion.ts
1087
+ var init_motion = __esm({
1088
+ "../core/src/motion.ts"() {
1089
+ "use strict";
1090
+ init_dsl();
1091
+ }
1092
+ });
1093
+
641
1094
  // ../core/src/index.ts
642
1095
  var init_src = __esm({
643
1096
  "../core/src/index.ts"() {
@@ -647,11 +1100,160 @@ var init_src = __esm({
647
1100
  init_validate();
648
1101
  init_compose();
649
1102
  init_compile();
1103
+ init_path();
1104
+ init_presets();
650
1105
  init_audio();
651
1106
  init_evaluate();
652
1107
  init_interpolate();
653
1108
  init_behaviors();
654
1109
  init_assets();
1110
+ init_motion();
1111
+ }
1112
+ });
1113
+
1114
+ // ../render-cli/src/logoSting.ts
1115
+ var logoSting_exports = {};
1116
+ __export(logoSting_exports, {
1117
+ LOGO_PRESETS: () => LOGO_PRESETS,
1118
+ buildLogoSting: () => buildLogoSting,
1119
+ resolveLogo: () => resolveLogo
1120
+ });
1121
+ import { existsSync } from "node:fs";
1122
+ import { readFile } from "node:fs/promises";
1123
+ async function loadSvg(arg) {
1124
+ if (existsSync(arg)) {
1125
+ return { svg: await readFile(arg, "utf8"), name: arg.split("/").pop().replace(/\.svg$/i, "") };
1126
+ }
1127
+ const slug = arg.toLowerCase().replace(/[^a-z0-9]/g, "");
1128
+ const r = await fetch(`https://cdn.simpleicons.org/${slug}`);
1129
+ if (!r.ok) throw new Error(`no local file "${arg}", and simple-icons has no "${slug}" (${r.status})`);
1130
+ return { svg: await r.text(), name: arg };
1131
+ }
1132
+ function tooDark(hex) {
1133
+ const h = hex.replace("#", "");
1134
+ const n = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
1135
+ const r = parseInt(n.slice(0, 2), 16);
1136
+ const g = parseInt(n.slice(2, 4), 16);
1137
+ const b = parseInt(n.slice(4, 6), 16);
1138
+ return 0.299 * r + 0.587 * g + 0.114 * b < 40;
1139
+ }
1140
+ function parseSvg(svg) {
1141
+ let viewBox = { minX: 0, minY: 0, w: 100, h: 100 };
1142
+ const vb = svg.match(/viewBox\s*=\s*"([\d.\-\s]+)"/i);
1143
+ if (vb) {
1144
+ const [a, b, c, d] = vb[1].trim().split(/\s+/).map(Number);
1145
+ viewBox = { minX: a, minY: b, w: c, h: d };
1146
+ } else {
1147
+ const w = svg.match(/\bwidth\s*=\s*"([\d.]+)/i);
1148
+ const h = svg.match(/\bheight\s*=\s*"([\d.]+)/i);
1149
+ if (w && h) viewBox = { minX: 0, minY: 0, w: +w[1], h: +h[1] };
1150
+ }
1151
+ const rootFill = svg.match(/<svg[^>]*\bfill\s*=\s*"(#[0-9a-fA-F]{3,8})"/)?.[1];
1152
+ const fallback = rootFill && !tooDark(rootFill) ? rootFill : "#E6EDF3";
1153
+ const paths = [];
1154
+ const re = /<path\b[^>]*>/g;
1155
+ let m;
1156
+ while (m = re.exec(svg)) {
1157
+ const tag = m[0];
1158
+ const d = tag.match(/\bd\s*=\s*"([^"]+)"/)?.[1];
1159
+ if (!d) continue;
1160
+ let fill = tag.match(/\bfill\s*=\s*"(#[0-9a-fA-F]{3,8})"/)?.[1] ?? fallback;
1161
+ if (tooDark(fill)) fill = fallback;
1162
+ paths.push({ d, fill });
1163
+ }
1164
+ return { paths, viewBox };
1165
+ }
1166
+ async function resolveLogo(arg, displayName, opts) {
1167
+ if (opts.motion && !LOGO_PRESETS.includes(opts.motion)) {
1168
+ throw new Error(`unknown --motion "${opts.motion}". options: ${LOGO_PRESETS.join(", ")}`);
1169
+ }
1170
+ const { svg, name } = await loadSvg(arg);
1171
+ const { paths, viewBox } = parseSvg(svg);
1172
+ if (paths.length === 0) throw new Error("no <path> elements found \u2014 logo stings need a path-based SVG");
1173
+ const from = FROMS.includes(opts.from) ? opts.from : void 0;
1174
+ const data = {
1175
+ name: displayName ?? titleCase(name),
1176
+ paths,
1177
+ viewBox,
1178
+ ...opts.motion && { motion: opts.motion },
1179
+ ...opts.energy !== void 0 && { energy: opts.energy },
1180
+ ...opts.speed !== void 0 && { speed: opts.speed },
1181
+ ...opts.intensity !== void 0 && { intensity: opts.intensity },
1182
+ ...from && { from },
1183
+ ...opts.seed !== void 0 && { seed: opts.seed }
1184
+ };
1185
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "logo";
1186
+ return { data, slug };
1187
+ }
1188
+ function buildLogoSting(d) {
1189
+ const W = 1080;
1190
+ const H = 1080;
1191
+ const CX = 540;
1192
+ const CY = 500;
1193
+ const vcx = d.viewBox.minX + d.viewBox.w / 2;
1194
+ const vcy = d.viewBox.minY + d.viewBox.h / 2;
1195
+ const fit = LOGO_PX / Math.max(d.viewBox.w, d.viewBox.h);
1196
+ const sw = 2.2 / fit;
1197
+ const fills = d.paths.map(
1198
+ (p, i) => path({ id: `fill-${i}`, d: p.d, originX: vcx, originY: vcy, x: 0, y: 0, fill: p.fill, opacity: 0 })
1199
+ );
1200
+ const inks = d.paths.map(
1201
+ (p, i) => path({ id: `ink-${i}`, d: p.d, originX: vcx, originY: vcy, x: 0, y: 0, stroke: p.fill, strokeWidth: sw, progress: 0 })
1202
+ );
1203
+ const rig = {
1204
+ group: "logo",
1205
+ center: [CX, CY],
1206
+ baseScale: fit,
1207
+ fills: fills.map((n) => n.id),
1208
+ inks: inks.map((n) => n.id)
1209
+ };
1210
+ return scene({
1211
+ id: "logo-sting",
1212
+ size: { width: W, height: H },
1213
+ fps: 30,
1214
+ background: BG,
1215
+ nodes: [
1216
+ rect({ id: "bg", x: 0, y: 0, width: W, height: H, fill: BG }),
1217
+ group({ id: "logo", x: CX, y: CY, scale: fit }, [...fills, ...inks]),
1218
+ text({ id: "word", x: CX, y: 905, anchor: "center", content: d.name, fontFamily: "Inter", fontSize: 56, fontWeight: 800, fill: FG, opacity: 0 }),
1219
+ text({ id: "made", x: CX, y: 968, anchor: "center", content: "made with reframe", fontFamily: "Inter", fontSize: 20, fill: MUTED, opacity: 0 })
1220
+ ],
1221
+ timeline: seq(
1222
+ motionPreset(d.motion ?? "reveal-orbit", {
1223
+ target: rig,
1224
+ ...d.energy !== void 0 && { energy: d.energy },
1225
+ ...d.speed !== void 0 && { speed: d.speed },
1226
+ ...d.intensity !== void 0 && { intensity: d.intensity },
1227
+ ...d.from !== void 0 && { from: d.from },
1228
+ ...d.seed !== void 0 && { seed: d.seed }
1229
+ }),
1230
+ par(
1231
+ tween("word", { opacity: 1 }, { duration: 0.5, ease: "easeOutQuad", label: "word" }),
1232
+ seq(wait(0.2), tween("made", { opacity: 1 }, { duration: 0.5, ease: "easeOutQuad" }))
1233
+ ),
1234
+ wait(0.8, "hold")
1235
+ )
1236
+ });
1237
+ }
1238
+ var LOGO_PRESETS, titleCase, FROMS, BG, FG, MUTED, LOGO_PX;
1239
+ var init_logoSting = __esm({
1240
+ "../render-cli/src/logoSting.ts"() {
1241
+ "use strict";
1242
+ init_src();
1243
+ LOGO_PRESETS = [
1244
+ "draw-bloom",
1245
+ "punch-in",
1246
+ "rise-settle",
1247
+ "slide-bank",
1248
+ "reveal-orbit",
1249
+ "spin-forge"
1250
+ ];
1251
+ titleCase = (s) => s.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()).trim();
1252
+ FROMS = ["left", "right", "top", "bottom"];
1253
+ BG = "#0D1117";
1254
+ FG = "#E6EDF3";
1255
+ MUTED = "#8B949E";
1256
+ LOGO_PX = 520;
655
1257
  }
656
1258
  });
657
1259
 
@@ -699,52 +1301,52 @@ function buffer(duration) {
699
1301
  return { out: new Float32Array(n), n };
700
1302
  }
701
1303
  function whoosh(seed) {
702
- const dur = 0.35;
703
- const { out, n } = buffer(dur);
1304
+ const dur2 = 0.35;
1305
+ const { out, n } = buffer(dur2);
704
1306
  let lp = 0;
705
1307
  let lp2 = 0;
706
1308
  for (let i = 0; i < n; i++) {
707
1309
  const t = i / SAMPLE_RATE;
708
- const u = t / dur;
1310
+ const u = t / dur2;
709
1311
  const center = 1200 * Math.pow(300 / 1200, u);
710
1312
  const alpha = Math.min(1, TAU * center / SAMPLE_RATE);
711
1313
  lp += alpha * (noise(i, seed) - lp);
712
1314
  lp2 += alpha * 0.5 * (lp - lp2);
713
- const env = u < 0.3 ? u / 0.3 : expDecay(t - 0.3 * dur, dur * 0.7, 4);
1315
+ const env = u < 0.3 ? u / 0.3 : expDecay(t - 0.3 * dur2, dur2 * 0.7, 4);
714
1316
  out[i] = (lp - lp2) * env * 2.2;
715
1317
  }
716
1318
  return out;
717
1319
  }
718
1320
  function pop(seed) {
719
- const dur = 0.12;
720
- const { out, n } = buffer(dur);
1321
+ const dur2 = 0.12;
1322
+ const { out, n } = buffer(dur2);
721
1323
  let phase = 0;
722
1324
  for (let i = 0; i < n; i++) {
723
1325
  const t = i / SAMPLE_RATE;
724
1326
  const freq = 600 * Math.pow(150 / 600, t / 0.08);
725
1327
  phase += TAU * freq / SAMPLE_RATE;
726
1328
  const transient = t < 2e-3 ? noise(i, seed) * 0.5 : 0;
727
- out[i] = (Math.sin(phase) + transient) * expDecay(t, dur, 6) * 0.8;
1329
+ out[i] = (Math.sin(phase) + transient) * expDecay(t, dur2, 6) * 0.8;
728
1330
  }
729
1331
  return out;
730
1332
  }
731
1333
  function tick(seed) {
732
- const dur = 0.03;
733
- const { out, n } = buffer(dur);
1334
+ const dur2 = 0.03;
1335
+ const { out, n } = buffer(dur2);
734
1336
  for (let i = 0; i < n; i++) {
735
1337
  const t = i / SAMPLE_RATE;
736
1338
  const sine = t < 4e-3 ? Math.sin(TAU * 4e3 * t) : 0;
737
- out[i] = (sine * 0.6 + noise(i, seed) * 0.35) * expDecay(t, dur, 8);
1339
+ out[i] = (sine * 0.6 + noise(i, seed) * 0.35) * expDecay(t, dur2, 8);
738
1340
  }
739
1341
  return out;
740
1342
  }
741
1343
  function rise(seed) {
742
- const dur = 0.5;
743
- const { out, n } = buffer(dur);
1344
+ const dur2 = 0.5;
1345
+ const { out, n } = buffer(dur2);
744
1346
  let phase = 0;
745
1347
  for (let i = 0; i < n; i++) {
746
1348
  const t = i / SAMPLE_RATE;
747
- const u = t / dur;
1349
+ const u = t / dur2;
748
1350
  const freq = 220 * Math.pow(880 / 220, u);
749
1351
  phase += TAU * freq / SAMPLE_RATE;
750
1352
  const env = Math.sin(Math.PI * Math.min(1, u * 1.05)) ** 1.5;
@@ -753,8 +1355,8 @@ function rise(seed) {
753
1355
  return out;
754
1356
  }
755
1357
  function shimmer(seed) {
756
- const dur = 0.9;
757
- const { out, n } = buffer(dur);
1358
+ const dur2 = 0.9;
1359
+ const { out, n } = buffer(dur2);
758
1360
  const partials = Array.from({ length: 5 }, (_, p) => ({
759
1361
  freq: 2e3 + hash01(p, seed + 7) * 2e3,
760
1362
  am: 0.5 + hash01(p, seed + 8) * 1.5,
@@ -762,7 +1364,7 @@ function shimmer(seed) {
762
1364
  }));
763
1365
  for (let i = 0; i < n; i++) {
764
1366
  const t = i / SAMPLE_RATE;
765
- const u = t / dur;
1367
+ const u = t / dur2;
766
1368
  const env = Math.sin(Math.PI * u) ** 1.2;
767
1369
  let s = 0;
768
1370
  for (const part of partials) {
@@ -773,8 +1375,8 @@ function shimmer(seed) {
773
1375
  return out;
774
1376
  }
775
1377
  function thud(seed) {
776
- const dur = 0.25;
777
- const { out, n } = buffer(dur);
1378
+ const dur2 = 0.25;
1379
+ const { out, n } = buffer(dur2);
778
1380
  let phase = 0;
779
1381
  let lp = 0;
780
1382
  for (let i = 0; i < n; i++) {
@@ -783,7 +1385,7 @@ function thud(seed) {
783
1385
  phase += TAU * freq / SAMPLE_RATE;
784
1386
  lp += 0.02 * (noise(i, seed) - lp);
785
1387
  const attack = t < 0.01 ? lp * 3 : 0;
786
- out[i] = (Math.sin(phase) * 0.9 + attack) * expDecay(t, dur, 5);
1388
+ out[i] = (Math.sin(phase) * 0.9 + attack) * expDecay(t, dur2, 5);
787
1389
  }
788
1390
  return out;
789
1391
  }
@@ -818,7 +1420,7 @@ var init_synth = __esm({
818
1420
  init_wav();
819
1421
  noise = (n, seed) => hash01(n, seed) * 2 - 1;
820
1422
  TAU = Math.PI * 2;
821
- expDecay = (t, dur, k = 5) => Math.exp(-k * t / dur);
1423
+ expDecay = (t, dur2, k = 5) => Math.exp(-k * t / dur2);
822
1424
  RECIPES = {
823
1425
  whoosh,
824
1426
  pop,
@@ -832,26 +1434,26 @@ var init_synth = __esm({
832
1434
 
833
1435
  // ../render-cli/src/audio/sfx.ts
834
1436
  import { mkdir, rename, writeFile } from "node:fs/promises";
835
- import { existsSync } from "node:fs";
1437
+ import { existsSync as existsSync2 } from "node:fs";
836
1438
  import { tmpdir } from "node:os";
837
1439
  import { dirname, isAbsolute, join, resolve } from "node:path";
838
1440
  import { fileURLToPath } from "node:url";
839
- function fnv1a(text) {
1441
+ function fnv1a(text2) {
840
1442
  let h = 2166136261;
841
- for (let i = 0; i < text.length; i++) {
842
- h ^= text.charCodeAt(i);
1443
+ for (let i = 0; i < text2.length; i++) {
1444
+ h ^= text2.charCodeAt(i);
843
1445
  h = Math.imul(h, 16777619);
844
1446
  }
845
1447
  return (h >>> 0).toString(16);
846
1448
  }
847
1449
  async function writeCached(key2, make) {
848
- const path = join(CACHE, `${key2}.wav`);
849
- if (existsSync(path)) return path;
1450
+ const path2 = join(CACHE, `${key2}.wav`);
1451
+ if (existsSync2(path2)) return path2;
850
1452
  await mkdir(CACHE, { recursive: true });
851
- const temp = `${path}.${process.pid}.${fnv1a(String(performance.now()))}.tmp`;
1453
+ const temp = `${path2}.${process.pid}.${fnv1a(String(performance.now()))}.tmp`;
852
1454
  await writeFile(temp, encodeWavMono16(make()));
853
- await rename(temp, path);
854
- return path;
1455
+ await rename(temp, path2);
1456
+ return path2;
855
1457
  }
856
1458
  async function resolveCueFile(cue, sceneDir) {
857
1459
  if (cue.source.kind === "file") {
@@ -861,14 +1463,14 @@ async function resolveCueFile(cue, sceneDir) {
861
1463
  resolve(sceneDir, p),
862
1464
  join(VENDORED, p)
863
1465
  ]) {
864
- if (candidate && existsSync(candidate)) return candidate;
1466
+ if (candidate && existsSync2(candidate)) return candidate;
865
1467
  }
866
1468
  throw new Error(
867
1469
  `audio cue file "${p}" not found (tried absolute, scene-relative, assets/sfx/)`
868
1470
  );
869
1471
  }
870
1472
  const vendored = join(VENDORED, `${cue.source.name}.wav`);
871
- if (existsSync(vendored)) return vendored;
1473
+ if (existsSync2(vendored)) return vendored;
872
1474
  const { name, params } = cue.source;
873
1475
  return writeCached(`${name}-${fnv1a(JSON.stringify(params))}`, () => synthSfx(name, params));
874
1476
  }
@@ -876,7 +1478,7 @@ async function resolveBgmFile(source, duration, sceneDir) {
876
1478
  if (source.kind === "file") {
877
1479
  const p = source.path;
878
1480
  for (const candidate of [isAbsolute(p) ? p : null, resolve(sceneDir, p), join(VENDORED, p)]) {
879
- if (candidate && existsSync(candidate)) return candidate;
1481
+ if (candidate && existsSync2(candidate)) return candidate;
880
1482
  }
881
1483
  throw new Error(`bgm file "${p}" not found`);
882
1484
  }
@@ -1046,14 +1648,14 @@ var init_encode = __esm({
1046
1648
  });
1047
1649
 
1048
1650
  // ../render-cli/src/fonts.ts
1049
- import { readFile } from "node:fs/promises";
1651
+ import { readFile as readFile2 } from "node:fs/promises";
1050
1652
  import { dirname as dirname3, join as join3 } from "node:path";
1051
1653
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1052
1654
  async function fontFaceCss() {
1053
1655
  if (cssCache) return cssCache;
1054
1656
  const rules = await Promise.all(
1055
1657
  WEIGHTS.map(async (weight) => {
1056
- const data = await readFile(join3(FONTS_DIR, `inter-${weight}.woff2`));
1658
+ const data = await readFile2(join3(FONTS_DIR, `inter-${weight}.woff2`));
1057
1659
  return `@font-face {
1058
1660
  font-family: "Inter";
1059
1661
  font-style: normal;
@@ -1076,8 +1678,8 @@ var init_fonts = __esm({
1076
1678
  });
1077
1679
 
1078
1680
  // ../render-cli/src/images.ts
1079
- import { readFile as readFile2 } from "node:fs/promises";
1080
- import { existsSync as existsSync2 } from "node:fs";
1681
+ import { readFile as readFile3 } from "node:fs/promises";
1682
+ import { existsSync as existsSync3 } from "node:fs";
1081
1683
  import { extname, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
1082
1684
  async function buildImageAssets(ir, sceneDir) {
1083
1685
  const assets = {};
@@ -1091,11 +1693,11 @@ async function buildImageAssets(ir, sceneDir) {
1091
1693
  const candidates = [isAbsolute2(src) ? src : null, resolve2(sceneDir, src)].filter(
1092
1694
  (c) => c !== null
1093
1695
  );
1094
- const found = candidates.find((c) => existsSync2(c));
1696
+ const found = candidates.find((c) => existsSync3(c));
1095
1697
  if (!found) {
1096
1698
  throw new Error(`image "${src}" not found (tried: ${candidates.join(", ")})`);
1097
1699
  }
1098
- const data = await readFile2(found);
1700
+ const data = await readFile3(found);
1099
1701
  assets[src] = `data:${mime};base64,${data.toString("base64")}`;
1100
1702
  }
1101
1703
  return assets;
@@ -1219,8 +1821,8 @@ async function withPage(size, fn) {
1219
1821
  async function browserBundle() {
1220
1822
  if (bundleCache) return bundleCache;
1221
1823
  if (true) {
1222
- const { readFile: readFile5 } = await import("node:fs/promises");
1223
- bundleCache = await readFile5(
1824
+ const { readFile: readFile6 } = await import("node:fs/promises");
1825
+ bundleCache = await readFile6(
1224
1826
  join4(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.js"),
1225
1827
  "utf8"
1226
1828
  );
@@ -1282,7 +1884,7 @@ __export(batch_exports, {
1282
1884
  parseCsv: () => parseCsv,
1283
1885
  runBatch: () => runBatch
1284
1886
  });
1285
- import { mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile3, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
1887
+ import { mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile4, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
1286
1888
  import { tmpdir as tmpdir3 } from "node:os";
1287
1889
  import { join as join5, dirname as dirname5 } from "node:path";
1288
1890
  function overlayFromFlat(row, name) {
@@ -1321,8 +1923,8 @@ function overlayFromFlat(row, name) {
1321
1923
  }
1322
1924
  return doc;
1323
1925
  }
1324
- function parseCsv(text) {
1325
- const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
1926
+ function parseCsv(text2) {
1927
+ const lines = text2.split(/\r?\n/).filter((l) => l.trim().length > 0);
1326
1928
  if (lines.length < 2) return [];
1327
1929
  const split = (line) => {
1328
1930
  const out = [];
@@ -1357,14 +1959,14 @@ function parseCsv(text) {
1357
1959
  return row;
1358
1960
  });
1359
1961
  }
1360
- async function loadRows(path) {
1361
- const text = await readFile3(path, "utf8");
1362
- if (path.endsWith(".csv")) return parseCsv(text);
1363
- const parsed = JSON.parse(text);
1364
- if (!Array.isArray(parsed)) throw new Error(`${path}: expected a JSON array of row objects`);
1962
+ async function loadRows(path2) {
1963
+ const text2 = await readFile4(path2, "utf8");
1964
+ if (path2.endsWith(".csv")) return parseCsv(text2);
1965
+ const parsed = JSON.parse(text2);
1966
+ if (!Array.isArray(parsed)) throw new Error(`${path2}: expected a JSON array of row objects`);
1365
1967
  return parsed;
1366
1968
  }
1367
- async function runBatch(scene, rows, opts) {
1969
+ async function runBatch(scene2, rows, opts) {
1368
1970
  await mkdir3(opts.outDir, { recursive: true });
1369
1971
  const results = new Array(rows.length);
1370
1972
  let next = 0;
@@ -1377,7 +1979,7 @@ async function runBatch(scene, rows, opts) {
1377
1979
  let result;
1378
1980
  try {
1379
1981
  const rowOverlay = overlayFromFlat(row, name);
1380
- const { ir, report } = composeScene(scene, ...opts.baseOverlays, rowOverlay);
1982
+ const { ir, report } = composeScene(scene2, ...opts.baseOverlays, rowOverlay);
1381
1983
  const framesDir = await mkdtemp2(join5(tmpdir3(), `reframe-batch-${index}-`));
1382
1984
  const output = join5(opts.outDir, `${name}.mp4`);
1383
1985
  const plan = opts.noAudio ? null : resolveAudioPlan(compileScene(ir));
@@ -1443,19 +2045,19 @@ __export(loadScene_exports, {
1443
2045
  loadScene: () => loadScene
1444
2046
  });
1445
2047
  import { build as build2 } from "esbuild";
1446
- import { readFile as readFile4 } from "node:fs/promises";
2048
+ import { readFile as readFile5 } from "node:fs/promises";
1447
2049
  import { dirname as dirname6, resolve as resolve3 } from "node:path";
1448
2050
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1449
- async function loadScene(path) {
1450
- if (path.endsWith(".json")) {
1451
- const ir = JSON.parse(await readFile4(path, "utf8"));
2051
+ async function loadScene(path2) {
2052
+ if (path2.endsWith(".json")) {
2053
+ const ir = JSON.parse(await readFile5(path2, "utf8"));
1452
2054
  validateScene(ir);
1453
2055
  return ir;
1454
2056
  }
1455
2057
  let code;
1456
2058
  try {
1457
2059
  const out = await build2({
1458
- entryPoints: [path],
2060
+ entryPoints: [path2],
1459
2061
  bundle: true,
1460
2062
  format: "esm",
1461
2063
  platform: "neutral",
@@ -1469,12 +2071,12 @@ async function loadScene(path) {
1469
2071
  code = out.outputFiles[0].text;
1470
2072
  } catch (err) {
1471
2073
  throw new Error(
1472
- `failed to bundle ${path}:
2074
+ `failed to bundle ${path2}:
1473
2075
  ${err instanceof Error ? err.message : String(err)}`
1474
2076
  );
1475
2077
  }
1476
2078
  const mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
1477
- if (!mod.default) throw new Error(`${path} must default-export a scene`);
2079
+ if (!mod.default) throw new Error(`${path2} must default-export a scene`);
1478
2080
  return mod.default;
1479
2081
  }
1480
2082
  var HERE, CORE_ENTRY;
@@ -1489,7 +2091,7 @@ var init_loadScene = __esm({
1489
2091
 
1490
2092
  // ../render-cli/src/reframe.ts
1491
2093
  import { spawn as spawn3, spawnSync } from "node:child_process";
1492
- import { existsSync as existsSync3 } from "node:fs";
2094
+ import { existsSync as existsSync4 } from "node:fs";
1493
2095
  import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
1494
2096
  import { basename, isAbsolute as isAbsolute3, join as join6, resolve as resolve4 } from "node:path";
1495
2097
  import { dirname as dirname7 } from "node:path";
@@ -1500,15 +2102,20 @@ var ROOT2 = PACKAGED ? resolve4(HERE2, "..") : resolve4(HERE2, "..", "..", "..")
1500
2102
  var USER_CWD = process.env.INIT_CWD ?? process.cwd();
1501
2103
  var RENDER_CLI = PACKAGED ? join6(ROOT2, "dist", "cli.js") : join6(ROOT2, "packages", "render-cli", "src", "cli.ts");
1502
2104
  var ANALYZE = PACKAGED ? join6(ROOT2, "dist", "analyze.js") : join6(ROOT2, "benchmark", "harness", "motion", "analyze.ts");
2105
+ var TRACE = PACKAGED ? join6(ROOT2, "dist", "trace-cli.js") : join6(ROOT2, "benchmark", "harness", "motion", "trace-cli.ts");
1503
2106
  var CMD = PACKAGED ? "reframe" : "pnpm reframe";
1504
2107
  var USAGE = `reframe \u2014 declarative motion graphics
1505
2108
 
1506
2109
  usage:
1507
2110
  ${CMD} render <scene.ts|.json|.html> [--overlay edits.json]... [-o out.mp4] [--fps N] [--duration S] [--no-audio]
1508
2111
  ${CMD} batch <scene.ts> <data.json|csv> [-o outDir] [--overlay base.json]... [--concurrency N] [--fps N]
2112
+ ${CMD} logo <logo.svg|brand-slug> ["Name"] [--motion <preset>] [--energy 0..1] [--seed N] [-o out.mp4]
2113
+ animate a logo into a sting (presets: draw-bloom, punch-in,
2114
+ rise-settle, slide-bank, reveal-orbit, spin-forge)
1509
2115
  ${CMD} preview open the scrub/edit UI (lists scenes in your directory)
1510
2116
  ${CMD} new <scene-name> scaffold <scene-name>.ts in your directory
1511
2117
  ${CMD} motion <mp4|framesDir> motion-profile a rendered clip
2118
+ ${CMD} trace <ref.mp4> [--apply scene.ts] extract a video's motion structure \u2192 MotionSketch / timeline
1512
2119
  ${CMD} guide [--regen] print the scene-authoring guide (for you or your AI)
1513
2120
  ${CMD} demo run the edit-survival demo (3 mp4s into out/)
1514
2121
  `;
@@ -1531,9 +2138,9 @@ function run(cmd, args, opts = {}) {
1531
2138
  });
1532
2139
  let sawBrowserError = false;
1533
2140
  proc.stderr.on("data", (d) => {
1534
- const text = d.toString();
1535
- if (/Executable doesn't exist|browserType\.launch/.test(text)) sawBrowserError = true;
1536
- process.stderr.write(text);
2141
+ const text2 = d.toString();
2142
+ if (/Executable doesn't exist|browserType\.launch/.test(text2)) sawBrowserError = true;
2143
+ process.stderr.write(text2);
1537
2144
  });
1538
2145
  proc.on("close", (code) => {
1539
2146
  if (code !== 0 && sawBrowserError) {
@@ -1614,7 +2221,7 @@ async function main() {
1614
2221
 
1615
2222
  ${USAGE}`);
1616
2223
  const inputPath = userPath(input);
1617
- if (!existsSync3(inputPath)) fail(`no such file: ${inputPath}`);
2224
+ if (!existsSync4(inputPath)) fail(`no such file: ${inputPath}`);
1618
2225
  const mode = /\.(ts|json)$/.test(input) ? "ir" : /\.html$/.test(input) ? "html" : null;
1619
2226
  if (!mode) {
1620
2227
  fail(`cannot infer render mode from "${input}" \u2014 expected .ts/.json (reframe scene) or .html (GSAP page)`);
@@ -1637,12 +2244,49 @@ ${USAGE}`);
1637
2244
  await (PACKAGED ? run(process.execPath, [RENDER_CLI, mode, inputPath, ...outArgs]) : run("npx", ["tsx", RENDER_CLI, mode, inputPath, ...outArgs]))
1638
2245
  );
1639
2246
  }
2247
+ case "logo": {
2248
+ const positional = [];
2249
+ const flags = {};
2250
+ for (let i = 0; i < rest.length; i++) {
2251
+ const a = rest[i];
2252
+ if (a.startsWith("--")) flags[a.slice(2)] = rest[++i] ?? "";
2253
+ else if (a === "-o") flags.o = rest[++i] ?? "";
2254
+ else positional.push(a);
2255
+ }
2256
+ const arg = positional[0];
2257
+ if (!arg) {
2258
+ 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]`);
2259
+ }
2260
+ preflightFfmpeg();
2261
+ const { tmpdir: tmpdir4 } = await import("node:os");
2262
+ const { resolveLogo: resolveLogo2, buildLogoSting: buildLogoSting2 } = await Promise.resolve().then(() => (init_logoSting(), logoSting_exports));
2263
+ const num = (k) => flags[k] !== void 0 ? Number(flags[k]) : void 0;
2264
+ console.log(`loading logo: ${arg} \u2026`);
2265
+ const { data, slug } = await resolveLogo2(arg, positional[1], {
2266
+ motion: flags.motion,
2267
+ energy: num("energy"),
2268
+ speed: num("speed"),
2269
+ intensity: num("intensity"),
2270
+ from: flags.from,
2271
+ seed: num("seed")
2272
+ });
2273
+ const sceneIR = buildLogoSting2(data);
2274
+ const tmp = join6(tmpdir4(), `reframe-logo-${slug}-${process.pid}.json`);
2275
+ await writeFile5(tmp, JSON.stringify(sceneIR));
2276
+ const outBase = PACKAGED ? join6(USER_CWD, "out") : join6(ROOT2, "out");
2277
+ const out = flags.o ? userPath(flags.o) : join6(outBase, `logo-${slug}.mp4`);
2278
+ await mkdir4(dirname7(out), { recursive: true });
2279
+ console.log(`rendering ${data.name} (${data.paths.length} path${data.paths.length > 1 ? "s" : ""}, motion: ${data.motion ?? "reveal-orbit"}) \u2192 ${out}`);
2280
+ process.exit(
2281
+ await (PACKAGED ? run(process.execPath, [RENDER_CLI, "ir", tmp, "-o", out, "--no-audio"]) : run("npx", ["tsx", RENDER_CLI, "ir", tmp, "-o", out, "--no-audio"]))
2282
+ );
2283
+ }
1640
2284
  case "batch": {
1641
2285
  const [sceneArg, dataArg, ...flags] = rest;
1642
2286
  if (!sceneArg || !dataArg) fail(`usage: ${CMD} batch <scene.ts> <data.json|csv> [...]`);
1643
2287
  const scenePath = userPath(sceneArg);
1644
2288
  const dataPath = userPath(dataArg);
1645
- for (const p of [scenePath, dataPath]) if (!existsSync3(p)) fail(`no such file: ${p}`);
2289
+ for (const p of [scenePath, dataPath]) if (!existsSync4(p)) fail(`no such file: ${p}`);
1646
2290
  preflightFfmpeg();
1647
2291
  let outDir = PACKAGED ? join6(USER_CWD, "out", "batch") : join6(ROOT2, "out", "batch");
1648
2292
  let concurrency = 3;
@@ -1657,15 +2301,15 @@ ${USAGE}`);
1657
2301
  }
1658
2302
  const { loadRows: loadRows2, runBatch: runBatch2 } = await Promise.resolve().then(() => (init_batch(), batch_exports));
1659
2303
  const { loadScene: loadScene2 } = await Promise.resolve().then(() => (init_loadScene(), loadScene_exports));
1660
- const { readFile: readFile5 } = await import("node:fs/promises");
1661
- const scene = await loadScene2(scenePath);
2304
+ const { readFile: readFile6 } = await import("node:fs/promises");
2305
+ const scene2 = await loadScene2(scenePath);
1662
2306
  const baseOverlays = await Promise.all(
1663
- baseOverlayPaths.map(async (p) => JSON.parse(await readFile5(p, "utf8")))
2307
+ baseOverlayPaths.map(async (p) => JSON.parse(await readFile6(p, "utf8")))
1664
2308
  );
1665
2309
  const rows = await loadRows2(dataPath);
1666
2310
  if (rows.length === 0) fail(`${dataPath}: no data rows`);
1667
2311
  console.log(`batch: ${rows.length} rows \xD7 ${concurrency} workers \u2192 ${outDir}`);
1668
- const results = await runBatch2(scene, rows, {
2312
+ const results = await runBatch2(scene2, rows, {
1669
2313
  outDir,
1670
2314
  baseOverlays,
1671
2315
  concurrency,
@@ -1714,7 +2358,7 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
1714
2358
  const targetDir = inRepo ? join6(ROOT2, "examples", "scenes") : USER_CWD;
1715
2359
  const target = join6(targetDir, `${name}.ts`);
1716
2360
  const shown = inRepo ? `examples/scenes/${name}.ts` : `${name}.ts`;
1717
- if (existsSync3(target)) fail(`${shown} already exists`);
2361
+ if (existsSync4(target)) fail(`${shown} already exists`);
1718
2362
  const id = name.split("-")[0] ?? name;
1719
2363
  await writeFile5(target, SCENE_TEMPLATE(name, id));
1720
2364
  console.log(`created ${shown}
@@ -1731,10 +2375,21 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
1731
2375
  await (PACKAGED ? run(process.execPath, [ANALYZE, userPath(input), ...rest.slice(1)]) : run("npx", ["tsx", ANALYZE, userPath(input), ...rest.slice(1)]))
1732
2376
  );
1733
2377
  }
2378
+ case "trace": {
2379
+ const input = rest[0];
2380
+ if (!input || input.startsWith("-")) fail(`usage: ${CMD} trace <ref.mp4> [--apply scene.ts] [-o out.json]`);
2381
+ preflightFfmpeg();
2382
+ const args = rest.slice(1).map(
2383
+ (a, i) => rest.slice(1)[i - 1] === "--apply" || rest.slice(1)[i - 1] === "-o" ? userPath(a) : a
2384
+ );
2385
+ process.exit(
2386
+ await (PACKAGED ? run(process.execPath, [TRACE, userPath(input), ...args]) : run("npx", ["tsx", TRACE, userPath(input), ...args]))
2387
+ );
2388
+ }
1734
2389
  case "guide": {
1735
2390
  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");
1736
- const { readFile: readFile5 } = await import("node:fs/promises");
1737
- process.stdout.write(await readFile5(file, "utf8"));
2391
+ const { readFile: readFile6 } = await import("node:fs/promises");
2392
+ process.stdout.write(await readFile6(file, "utf8"));
1738
2393
  return;
1739
2394
  }
1740
2395
  case "demo":