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/index.js CHANGED
@@ -1,10 +1,89 @@
1
1
  // ../core/src/ir.ts
2
2
  var DEFAULT_TO_DURATION = 0.5;
3
3
  var DEFAULT_TWEEN_DURATION = 0.5;
4
+ var DEFAULT_MOTIONPATH_DURATION = 1;
4
5
  var DEFAULT_FPS = 30;
5
6
 
7
+ // ../core/src/path.ts
8
+ function locate(segCount, u) {
9
+ if (segCount <= 0) return { i: 0, t: 0 };
10
+ const clamped = Math.max(0, Math.min(1, u));
11
+ const scaled = clamped * segCount;
12
+ let i = Math.floor(scaled);
13
+ if (i >= segCount) i = segCount - 1;
14
+ return { i, t: scaled - i };
15
+ }
16
+ function controls(points, closed, i) {
17
+ const n = points.length;
18
+ const at = (k) => {
19
+ if (closed) return points[(k % n + n) % n];
20
+ return points[Math.max(0, Math.min(n - 1, k))];
21
+ };
22
+ return [at(i - 1), at(i), at(i + 1), at(i + 2)];
23
+ }
24
+ function segCountOf(points, closed) {
25
+ const n = points.length;
26
+ if (n < 2) return 0;
27
+ return closed ? n : n - 1;
28
+ }
29
+ function pathPoint(points, closed, u) {
30
+ const n = points.length;
31
+ if (n === 0) return [0, 0];
32
+ if (n === 1) return [points[0][0], points[0][1]];
33
+ const segs = segCountOf(points, closed);
34
+ const { i, t } = locate(segs, u);
35
+ const [p0, p1, p2, p3] = controls(points, closed, i);
36
+ const t2 = t * t;
37
+ const t3 = t2 * t;
38
+ const f = (a, b, c, d) => 0.5 * (2 * b + (-a + c) * t + (2 * a - 5 * b + 4 * c - d) * t2 + (-a + 3 * b - 3 * c + d) * t3);
39
+ return [f(p0[0], p1[0], p2[0], p3[0]), f(p0[1], p1[1], p2[1], p3[1])];
40
+ }
41
+ function pathTangentAngle(points, closed, u) {
42
+ const n = points.length;
43
+ if (n < 2) return 0;
44
+ const segs = segCountOf(points, closed);
45
+ const { i, t } = locate(segs, u);
46
+ const [p0, p1, p2, p3] = controls(points, closed, i);
47
+ const t2 = t * t;
48
+ const d = (a, b, c, e) => 0.5 * (-a + c + 2 * (2 * a - 5 * b + 4 * c - e) * t + 3 * (-a + 3 * b - 3 * c + e) * t2);
49
+ const dx = d(p0[0], p1[0], p2[0], p3[0]);
50
+ const dy = d(p0[1], p1[1], p2[1], p3[1]);
51
+ if (dx === 0 && dy === 0) return 0;
52
+ return Math.atan2(dy, dx) * 180 / Math.PI;
53
+ }
54
+
6
55
  // ../core/src/compile.ts
7
56
  var key = (target, prop) => `${target}.${prop}`;
57
+ function scaleTimeline(tl, k) {
58
+ switch (tl.kind) {
59
+ case "seq":
60
+ case "par":
61
+ return { ...tl, children: tl.children.map((c) => scaleTimeline(c, k)) };
62
+ case "stagger":
63
+ return { ...tl, interval: tl.interval * k, children: tl.children.map((c) => scaleTimeline(c, k)) };
64
+ case "wait":
65
+ return { ...tl, duration: tl.duration * k };
66
+ case "tween":
67
+ return { ...tl, duration: (tl.duration ?? DEFAULT_TWEEN_DURATION) * k };
68
+ case "motionPath":
69
+ return { ...tl, duration: (tl.duration ?? DEFAULT_MOTIONPATH_DURATION) * k };
70
+ case "to":
71
+ return {
72
+ ...tl,
73
+ duration: (tl.duration ?? DEFAULT_TO_DURATION) * k,
74
+ ...tl.stagger !== void 0 && { stagger: tl.stagger * k }
75
+ };
76
+ case "beat":
77
+ return {
78
+ ...tl,
79
+ children: tl.children.map((c) => scaleTimeline(c, k)),
80
+ ...tl.gap !== void 0 && { gap: tl.gap * k }
81
+ };
82
+ }
83
+ }
84
+ function orderBeats(children) {
85
+ 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);
86
+ }
8
87
  function compileScene(ir) {
9
88
  const nodeById = /* @__PURE__ */ new Map();
10
89
  const nodeOrder = [];
@@ -33,6 +112,7 @@ function compileScene(ir) {
33
112
  }
34
113
  }
35
114
  const segments = /* @__PURE__ */ new Map();
115
+ const motionPaths = /* @__PURE__ */ new Map();
36
116
  const current = new Map(initialValues);
37
117
  const pushSegment = (seg) => {
38
118
  const k = key(seg.target, seg.prop);
@@ -49,6 +129,50 @@ function compileScene(ir) {
49
129
  throw new Error(`cannot animate "${prop}" of "${target}": no base value to start from`);
50
130
  };
51
131
  const labelTimes = /* @__PURE__ */ new Map();
132
+ const beatTimes = /* @__PURE__ */ new Map();
133
+ const durationOf = (tl, start) => {
134
+ switch (tl.kind) {
135
+ case "seq": {
136
+ let t = start;
137
+ for (const child of orderBeats(tl.children)) t = durationOf(child, t);
138
+ return t;
139
+ }
140
+ case "par": {
141
+ let end = start;
142
+ for (const child of tl.children) end = Math.max(end, durationOf(child, start));
143
+ return end;
144
+ }
145
+ case "stagger": {
146
+ let end = start;
147
+ tl.children.forEach((child, i) => {
148
+ end = Math.max(end, durationOf(child, start + i * tl.interval));
149
+ });
150
+ return end;
151
+ }
152
+ case "wait":
153
+ return start + tl.duration;
154
+ case "tween":
155
+ return start + (tl.duration ?? DEFAULT_TWEEN_DURATION);
156
+ case "motionPath":
157
+ return start + (tl.duration ?? DEFAULT_MOTIONPATH_DURATION);
158
+ case "to": {
159
+ const override = ir.states?.[tl.state] ?? {};
160
+ const duration = tl.duration ?? DEFAULT_TO_DURATION;
161
+ const si = tl.stagger ?? 0;
162
+ const targets = nodeOrder.filter(
163
+ (id) => id in override && (tl.filter === void 0 || tl.filter.includes(id))
164
+ );
165
+ return start + duration + Math.max(0, targets.length - 1) * si;
166
+ }
167
+ case "beat": {
168
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
169
+ const natural = durationOf(grouping, 0);
170
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, natural) : 1);
171
+ const beatStart = tl.at ?? start + (tl.gap ?? 0);
172
+ return beatStart + k * natural;
173
+ }
174
+ }
175
+ };
52
176
  const walk = (tl, start) => {
53
177
  const end = walkInner(tl, start);
54
178
  if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
@@ -58,9 +182,19 @@ function compileScene(ir) {
58
182
  switch (tl.kind) {
59
183
  case "seq": {
60
184
  let t = start;
61
- for (const child of tl.children) t = walk(child, t);
185
+ for (const child of orderBeats(tl.children)) t = walk(child, t);
62
186
  return t;
63
187
  }
188
+ case "beat": {
189
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
190
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
191
+ const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
192
+ const beatStart = tl.at ?? start + (tl.gap ?? 0);
193
+ const end = walk(inner, beatStart);
194
+ beatTimes.set(tl.name, { t0: beatStart, t1: end });
195
+ labelTimes.set(tl.name, { t0: beatStart, t1: end });
196
+ return end;
197
+ }
64
198
  case "par": {
65
199
  let end = start;
66
200
  for (const child of tl.children) end = Math.max(end, walk(child, start));
@@ -90,6 +224,23 @@ function compileScene(ir) {
90
224
  }
91
225
  return start + duration;
92
226
  }
227
+ case "motionPath": {
228
+ const duration = tl.duration ?? DEFAULT_MOTIONPATH_DURATION;
229
+ const points = tl.points;
230
+ const closed = tl.closed ?? false;
231
+ const autoRotate = tl.autoRotate ?? false;
232
+ const rotateOffset = tl.rotateOffset ?? 0;
233
+ let list = motionPaths.get(tl.target);
234
+ if (!list) motionPaths.set(tl.target, list = []);
235
+ list.push({ t0: start, t1: start + duration, points, closed, autoRotate, rotateOffset, ...tl.ease !== void 0 && { ease: tl.ease } });
236
+ if (points.length > 0) {
237
+ const [ex, ey] = pathPoint(points, closed, 1);
238
+ current.set(key(tl.target, "x"), ex);
239
+ current.set(key(tl.target, "y"), ey);
240
+ if (autoRotate) current.set(key(tl.target, "rotation"), pathTangentAngle(points, closed, 1) + rotateOffset);
241
+ }
242
+ return start + duration;
243
+ }
93
244
  case "to": {
94
245
  const override = ir.states?.[tl.state] ?? {};
95
246
  const duration = tl.duration ?? DEFAULT_TO_DURATION;
@@ -118,14 +269,17 @@ function compileScene(ir) {
118
269
  };
119
270
  const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
120
271
  for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
272
+ for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
121
273
  return {
122
274
  ir,
123
275
  duration: ir.duration ?? inferredEnd,
124
276
  segments,
277
+ motionPaths,
125
278
  initialValues,
126
279
  nodeById,
127
280
  nodeOrder,
128
- labelTimes
281
+ labelTimes,
282
+ beatTimes
129
283
  };
130
284
  }
131
285
 
@@ -137,6 +291,7 @@ var PROPS_BY_TYPE = {
137
291
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
138
292
  text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
139
293
  image: [...COMMON_PROPS, "src", "width", "height"],
294
+ path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
140
295
  group: COMMON_PROPS
141
296
  };
142
297
  var SceneValidationError = class extends Error {
@@ -190,11 +345,11 @@ function validateScene(ir) {
190
345
  );
191
346
  }
192
347
  const labels = /* @__PURE__ */ new Set();
193
- const checkTimeline = (tl, path) => {
348
+ const checkTimeline = (tl, path2) => {
194
349
  if ("label" in tl && tl.label !== void 0) {
195
350
  if (labels.has(tl.label)) {
196
351
  problems.push(
197
- `${path}: duplicate timeline label "${tl.label}" \u2014 labels are overlay addresses and must be unique`
352
+ `${path2}: duplicate timeline label "${tl.label}" \u2014 labels are overlay addresses and must be unique`
198
353
  );
199
354
  }
200
355
  labels.add(tl.label);
@@ -202,33 +357,63 @@ function validateScene(ir) {
202
357
  switch (tl.kind) {
203
358
  case "seq":
204
359
  case "par":
205
- tl.children.forEach((c, i) => checkTimeline(c, `${path}.${tl.kind}[${i}]`));
360
+ tl.children.forEach((c, i) => checkTimeline(c, `${path2}.${tl.kind}[${i}]`));
206
361
  break;
207
362
  case "stagger":
208
- if (tl.interval < 0) problems.push(`${path}: stagger interval must be >= 0`);
209
- tl.children.forEach((c, i) => checkTimeline(c, `${path}.stagger[${i}]`));
363
+ if (tl.interval < 0) problems.push(`${path2}: stagger interval must be >= 0`);
364
+ tl.children.forEach((c, i) => checkTimeline(c, `${path2}.stagger[${i}]`));
210
365
  break;
211
366
  case "to":
212
367
  if (!(tl.state in states)) {
213
368
  problems.push(
214
- `${path}: to("${tl.state}") references an undefined state \u2014 defined states: ${Object.keys(states).join(", ") || "(none)"}`
369
+ `${path2}: to("${tl.state}") references an undefined state \u2014 defined states: ${Object.keys(states).join(", ") || "(none)"}`
215
370
  );
216
371
  }
217
372
  if (tl.duration !== void 0 && tl.duration <= 0) {
218
- problems.push(`${path}: to("${tl.state}") duration must be > 0`);
373
+ problems.push(`${path2}: to("${tl.state}") duration must be > 0`);
219
374
  }
220
375
  for (const id of tl.filter ?? []) {
221
- if (!nodeById.has(id)) problems.push(`${path}: filter contains unknown node "${id}"`);
376
+ if (!nodeById.has(id)) problems.push(`${path2}: filter contains unknown node "${id}"`);
222
377
  }
223
378
  break;
224
379
  case "tween":
225
- checkProps(path, tl.target, tl.props);
380
+ checkProps(path2, tl.target, tl.props);
226
381
  if (tl.duration !== void 0 && tl.duration <= 0) {
227
- problems.push(`${path}: tween duration must be > 0`);
382
+ problems.push(`${path2}: tween duration must be > 0`);
228
383
  }
229
384
  break;
385
+ case "motionPath": {
386
+ const node = nodeById.get(tl.target);
387
+ if (!node) {
388
+ problems.push(
389
+ `${path2}: motionPath targets unknown node "${tl.target}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
390
+ );
391
+ } else if (node.type === "line") {
392
+ problems.push(`${path2}: motionPath cannot target a line (no x/y) \u2014 "${tl.target}"`);
393
+ }
394
+ if (tl.points.length < 1) problems.push(`${path2}: motionPath "${tl.target}" needs at least 1 point`);
395
+ if (tl.duration !== void 0 && tl.duration <= 0) {
396
+ problems.push(`${path2}: motionPath "${tl.target}" duration must be > 0`);
397
+ }
398
+ break;
399
+ }
230
400
  case "wait":
231
- if (tl.duration < 0) problems.push(`${path}: wait duration must be >= 0`);
401
+ if (tl.duration < 0) problems.push(`${path2}: wait duration must be >= 0`);
402
+ break;
403
+ case "beat":
404
+ if (labels.has(tl.name)) {
405
+ problems.push(
406
+ `${path2}: duplicate timeline label "${tl.name}" (beat name) \u2014 labels are overlay addresses and must be unique`
407
+ );
408
+ }
409
+ labels.add(tl.name);
410
+ if (tl.duration !== void 0 && tl.duration <= 0) {
411
+ problems.push(`${path2}: beat "${tl.name}" duration must be > 0`);
412
+ }
413
+ if (tl.scale !== void 0 && tl.scale <= 0) {
414
+ problems.push(`${path2}: beat "${tl.name}" scale must be > 0`);
415
+ }
416
+ tl.children.forEach((c, i) => checkTimeline(c, `${path2}.beat(${tl.name})[${i}]`));
232
417
  break;
233
418
  }
234
419
  };
@@ -298,6 +483,10 @@ function image(props) {
298
483
  const { id, ...rest } = props;
299
484
  return { type: "image", id, props: rest };
300
485
  }
486
+ function path(props) {
487
+ const { id, ...rest } = props;
488
+ return { type: "path", id, props: rest };
489
+ }
301
490
  function group(props, children) {
302
491
  const { id, ...rest } = props;
303
492
  return { type: "group", id, props: rest, children };
@@ -311,6 +500,9 @@ function par(...children) {
311
500
  function stagger(interval, ...children) {
312
501
  return { kind: "stagger", interval, children };
313
502
  }
503
+ function beat(name, opts, children) {
504
+ return { kind: "beat", name, children, ...opts };
505
+ }
314
506
  function to(state, opts = {}) {
315
507
  return { kind: "to", state, ...opts };
316
508
  }
@@ -320,6 +512,9 @@ function tween(target, props, opts = {}) {
320
512
  function wait(duration, label) {
321
513
  return { kind: "wait", duration, ...label !== void 0 && { label } };
322
514
  }
515
+ function motionPath(target, points, opts = {}) {
516
+ return { kind: "motionPath", target, points, ...opts };
517
+ }
323
518
  function oscillate(target, prop, params, window = {}) {
324
519
  return { target, prop, ...window, behavior: { kind: "named", name: "oscillate", params } };
325
520
  }
@@ -451,13 +646,16 @@ function applyOverlay(ir, overlay, layer, report) {
451
646
  const byLabel = /* @__PURE__ */ new Map();
452
647
  const walkTimeline = (tl) => {
453
648
  if ("label" in tl && tl.label !== void 0) byLabel.set(tl.label, tl);
649
+ if (tl.kind === "beat") byLabel.set(tl.name, tl);
454
650
  if ("children" in tl) tl.children.forEach(walkTimeline);
455
651
  };
456
652
  if (ir.timeline) walkTimeline(ir.timeline);
457
653
  const PATCHABLE = {
458
654
  to: ["duration", "ease", "stagger"],
459
655
  tween: ["duration", "ease"],
460
- wait: ["duration"]
656
+ wait: ["duration"],
657
+ motionPath: ["points", "duration", "ease"],
658
+ beat: ["at", "gap", "scale", "duration", "order"]
461
659
  };
462
660
  let timingPatched = false;
463
661
  for (const [label, patch] of Object.entries(overlay.timeline)) {
@@ -481,7 +679,7 @@ function applyOverlay(ir, overlay, layer, report) {
481
679
  }
482
680
  step[key2] = value;
483
681
  applied(`timeline.${label}.${key2}`, "set");
484
- if (key2 === "duration" || key2 === "stagger") timingPatched = true;
682
+ if (["duration", "stagger", "at", "gap", "scale", "order"].includes(key2)) timingPatched = true;
485
683
  }
486
684
  }
487
685
  if (timingPatched && overlay.scene?.duration === void 0) {
@@ -506,6 +704,186 @@ function formatComposeReport(report) {
506
704
  return lines.join("\n");
507
705
  }
508
706
 
707
+ // ../core/src/presets.ts
708
+ var PRESET_NAMES = [
709
+ "draw-bloom",
710
+ "punch-in",
711
+ "rise-settle",
712
+ "slide-bank",
713
+ "reveal-orbit",
714
+ "spin-forge"
715
+ ];
716
+ function makeRng(seed) {
717
+ let a = seed >>> 0 || 2654435769;
718
+ return () => {
719
+ a = a + 1831565813 | 0;
720
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
721
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
722
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
723
+ };
724
+ }
725
+ var clamp01 = (x) => Math.max(0, Math.min(1, x));
726
+ var SET = 1 / 120;
727
+ function ctx(o) {
728
+ const rand = makeRng((o.seed ?? 0) + 1);
729
+ return {
730
+ e: clamp01(o.energy ?? 0.5),
731
+ sp: Math.max(0.25, o.speed ?? 1),
732
+ it: clamp01(o.intensity ?? 0.5),
733
+ from: o.from,
734
+ rand,
735
+ jit: (amp) => (rand() - 0.5) * 2 * amp,
736
+ g: o.target.group,
737
+ cx: o.target.center[0],
738
+ cy: o.target.center[1],
739
+ s: o.target.baseScale,
740
+ fills: o.target.fills,
741
+ inks: o.target.inks
742
+ };
743
+ }
744
+ var dur = (base, sp) => base / sp;
745
+ function settleEase(e) {
746
+ return e < 0.34 ? "easeOutCubic" : e < 0.67 ? "easeOutBack" : "easeOutElastic";
747
+ }
748
+ function fromVec(from, dist) {
749
+ switch (from) {
750
+ case "left":
751
+ return [-dist, 0];
752
+ case "right":
753
+ return [dist, 0];
754
+ case "top":
755
+ return [0, -dist];
756
+ default:
757
+ return [0, dist];
758
+ }
759
+ }
760
+ function fadeFills(c, base = 0.4, gap = 0.06) {
761
+ return stagger(
762
+ gap / c.sp,
763
+ ...c.fills.map(
764
+ (id, i) => tween(id, { opacity: 1 }, { duration: dur(base, c.sp), ease: "easeOutQuad", ...i === 0 && { label: "reveal" } })
765
+ )
766
+ );
767
+ }
768
+ function drawInks(c) {
769
+ return stagger(
770
+ 0.15 / c.sp,
771
+ ...c.inks.map(
772
+ (id, i) => tween(id, { progress: 1 }, { duration: dur(1.3 + c.jit(0.2), c.sp), ease: "easeInOutQuad", ...i === 0 && { label: "draw" } })
773
+ )
774
+ );
775
+ }
776
+ function motionPreset(name, opts) {
777
+ const c = ctx(opts);
778
+ switch (name) {
779
+ case "draw-bloom":
780
+ return beat("draw-bloom", {}, [
781
+ drawInks(c),
782
+ fadeFills(c, 0.45),
783
+ tween(c.g, { scale: c.s * (1.02 + 0.05 * c.e) }, { duration: dur(2.4, c.sp), ease: "easeInOutQuad", label: "settle" })
784
+ ]);
785
+ case "punch-in": {
786
+ const peak = c.s * (1 + 0.06 + 0.24 * c.e + c.jit(0.02));
787
+ return beat("punch-in", {}, [
788
+ par(
789
+ fadeFills(c, 0.25),
790
+ seq(
791
+ tween(c.g, { scale: peak }, { duration: dur(0.45 + c.jit(0.05), c.sp), ease: "easeOutCubic", label: "punch" }),
792
+ tween(c.g, { scale: c.s }, { duration: dur(0.5, c.sp), ease: settleEase(c.e) })
793
+ )
794
+ )
795
+ ]);
796
+ }
797
+ case "rise-settle": {
798
+ const es = 0.65 + c.rand() * 0.7;
799
+ const dist = (220 + 260 * c.it) * es;
800
+ const [dx, dy] = fromVec(c.from ?? "bottom", dist);
801
+ const jx = c.jit(110);
802
+ return beat("rise-settle", {}, [
803
+ par(
804
+ motionPath(
805
+ c.g,
806
+ [
807
+ [c.cx + dx + jx, c.cy + dy],
808
+ [c.cx + dx * 0.4 - jx * 0.6, c.cy + dy * 0.4],
809
+ [c.cx, c.cy]
810
+ ],
811
+ { duration: dur(1.1, c.sp), ease: settleEase(c.e), label: "rise" }
812
+ ),
813
+ fadeFills(c, 0.4)
814
+ )
815
+ ]);
816
+ }
817
+ case "slide-bank": {
818
+ const es = 0.65 + c.rand() * 0.7;
819
+ const dist = (420 + 240 * c.it) * es;
820
+ const [dx, dy] = fromVec(c.from ?? "left", dist);
821
+ const arc = c.jit(140);
822
+ const midx = c.jit(120);
823
+ const move = dur(1.2, c.sp);
824
+ return beat("slide-bank", {}, [
825
+ par(
826
+ motionPath(
827
+ c.g,
828
+ [
829
+ [c.cx + dx, c.cy + dy],
830
+ [c.cx + dx * 0.4 + midx, c.cy + dy * 0.4 - 70 - arc],
831
+ [c.cx, c.cy]
832
+ ],
833
+ { duration: move, ease: settleEase(c.e), autoRotate: true, label: "slide" }
834
+ ),
835
+ // level the bank out once it lands (authored after the path → wins for rotation)
836
+ seq(wait(move), tween(c.g, { rotation: 0 }, { duration: dur(0.5, c.sp), ease: "easeOutCubic" })),
837
+ fadeFills(c, 0.4)
838
+ )
839
+ ]);
840
+ }
841
+ case "reveal-orbit": {
842
+ const es = 0.65 + c.rand() * 0.7;
843
+ const orbit = (180 + 160 * c.it) * es;
844
+ const jx = c.jit(0.4);
845
+ const jy = c.jit(0.4);
846
+ return beat("reveal-orbit", {}, [
847
+ drawInks(c),
848
+ fadeFills(c, 0.45),
849
+ par(
850
+ motionPath(
851
+ c.g,
852
+ [
853
+ [c.cx, c.cy],
854
+ [c.cx - orbit * (1 + jx), c.cy - orbit * 0.8],
855
+ [c.cx + orbit * (1 + jy), c.cy - orbit],
856
+ [c.cx, c.cy]
857
+ ],
858
+ { duration: dur(1.7, c.sp), ease: "easeInOutCubic", label: "orbit" }
859
+ ),
860
+ seq(
861
+ tween(c.g, { scale: c.s * (1.12 + 0.1 * c.e) }, { duration: dur(0.85, c.sp), ease: "easeOutBack" }),
862
+ tween(c.g, { scale: c.s }, { duration: dur(0.85, c.sp), ease: "easeInOutQuad" })
863
+ )
864
+ )
865
+ ]);
866
+ }
867
+ case "spin-forge": {
868
+ const turns = 1 + Math.round(c.it);
869
+ const dir = c.rand() < 0.5 ? -1 : 1;
870
+ const startRot = dir * 360 * turns;
871
+ const peak = c.s * (1 + 0.05 + 0.2 * c.e);
872
+ return beat("spin-forge", {}, [
873
+ par(
874
+ seq(
875
+ tween(c.g, { scale: c.s * 0.2, rotation: startRot }, { duration: SET }),
876
+ // establish (invisible)
877
+ tween(c.g, { scale: peak, rotation: 0 }, { duration: dur(0.9, c.sp), ease: "easeOutBack", label: "spin" }),
878
+ tween(c.g, { scale: c.s }, { duration: dur(0.3, c.sp), ease: "easeInOutQuad" })
879
+ ),
880
+ seq(wait(SET), fadeFills(c, 0.3))
881
+ )
882
+ ]);
883
+ }
884
+ }
885
+ }
886
+
509
887
  // ../core/src/audio.ts
510
888
  var SFX_DURATION = {
511
889
  whoosh: 0.35,
@@ -607,6 +985,19 @@ function hash01(n, seed) {
607
985
  }
608
986
 
609
987
  // ../core/src/interpolate.ts
988
+ var BACK_C1 = 1.70158;
989
+ var BACK_C2 = BACK_C1 * 1.525;
990
+ var BACK_C3 = BACK_C1 + 1;
991
+ var ELASTIC_C4 = 2 * Math.PI / 3;
992
+ var ELASTIC_C5 = 2 * Math.PI / 4.5;
993
+ function easeOutBounce(u) {
994
+ const n1 = 7.5625;
995
+ const d1 = 2.75;
996
+ if (u < 1 / d1) return n1 * u * u;
997
+ if (u < 2 / d1) return n1 * (u -= 1.5 / d1) * u + 0.75;
998
+ if (u < 2.5 / d1) return n1 * (u -= 2.25 / d1) * u + 0.9375;
999
+ return n1 * (u -= 2.625 / d1) * u + 0.984375;
1000
+ }
610
1001
  var EASE_TABLE = {
611
1002
  linear: (u) => u,
612
1003
  easeInQuad: (u) => u * u,
@@ -620,7 +1011,20 @@ var EASE_TABLE = {
620
1011
  easeInOutQuart: (u) => u < 0.5 ? 8 * u ** 4 : 1 - (-2 * u + 2) ** 4 / 2,
621
1012
  easeInExpo: (u) => u === 0 ? 0 : 2 ** (10 * u - 10),
622
1013
  easeOutExpo: (u) => u === 1 ? 1 : 1 - 2 ** (-10 * u),
623
- easeInOutExpo: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? 2 ** (20 * u - 10) / 2 : (2 - 2 ** (-20 * u + 10)) / 2
1014
+ easeInOutExpo: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? 2 ** (20 * u - 10) / 2 : (2 - 2 ** (-20 * u + 10)) / 2,
1015
+ // --- expressive eases (GSAP's signature feel) — standard Penner equations ---
1016
+ // back: overshoots past the target then settles (pop / snap)
1017
+ easeInBack: (u) => BACK_C3 * u ** 3 - BACK_C1 * u * u,
1018
+ easeOutBack: (u) => 1 + BACK_C3 * (u - 1) ** 3 + BACK_C1 * (u - 1) ** 2,
1019
+ 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,
1020
+ // elastic: rings around the target before settling (playful spring)
1021
+ easeInElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : -(2 ** (10 * u - 10)) * Math.sin((u * 10 - 10.75) * ELASTIC_C4),
1022
+ easeOutElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : 2 ** (-10 * u) * Math.sin((u * 10 - 0.75) * ELASTIC_C4) + 1,
1023
+ 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,
1024
+ // bounce: drops and bounces to rest (lands without overshoot)
1025
+ easeInBounce: (u) => 1 - easeOutBounce(1 - u),
1026
+ easeOutBounce,
1027
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
624
1028
  };
625
1029
  var EASE_NAMES = Object.keys(EASE_TABLE);
626
1030
  function resolveEase(ease) {
@@ -735,6 +1139,7 @@ function evaluate(compiled, t) {
735
1139
  const ops = [];
736
1140
  const valueAt = (target, prop, fallback) => {
737
1141
  let value = compiled.initialValues.get(`${target}.${prop}`) ?? fallback;
1142
+ let segStart = Number.NEGATIVE_INFINITY;
738
1143
  const segs = compiled.segments.get(`${target}.${prop}`);
739
1144
  if (segs) {
740
1145
  let active;
@@ -743,6 +1148,7 @@ function evaluate(compiled, t) {
743
1148
  else break;
744
1149
  }
745
1150
  if (active) {
1151
+ segStart = active.t0;
746
1152
  if (t >= active.t1) {
747
1153
  value = active.to;
748
1154
  } else {
@@ -751,6 +1157,23 @@ function evaluate(compiled, t) {
751
1157
  }
752
1158
  }
753
1159
  }
1160
+ if (prop === "x" || prop === "y" || prop === "rotation") {
1161
+ const drivers = compiled.motionPaths.get(target);
1162
+ if (drivers) {
1163
+ let active;
1164
+ for (const d of drivers) {
1165
+ if (d.t0 <= t) active = d;
1166
+ else break;
1167
+ }
1168
+ if (active && active.t0 >= segStart && (prop !== "rotation" || active.autoRotate) && active.points.length > 0) {
1169
+ const span = active.t1 - active.t0;
1170
+ const u = span <= 0 ? 1 : resolveEase(active.ease)(Math.max(0, Math.min(1, (t - active.t0) / span)));
1171
+ if (prop === "x") value = pathPoint(active.points, active.closed, u)[0];
1172
+ else if (prop === "y") value = pathPoint(active.points, active.closed, u)[1];
1173
+ else value = pathTangentAngle(active.points, active.closed, u) + active.rotateOffset;
1174
+ }
1175
+ }
1176
+ }
754
1177
  for (const b of compiled.ir.behaviors ?? []) {
755
1178
  if (b.target === target && b.prop === prop && typeof value === "number") {
756
1179
  const envelope = behaviorEnvelope(b, t);
@@ -848,6 +1271,23 @@ function evaluate(compiled, t) {
848
1271
  });
849
1272
  return;
850
1273
  }
1274
+ case "path": {
1275
+ const ox = num(id, "originX", node.props.originX ?? 0);
1276
+ const oy = num(id, "originY", node.props.originY ?? 0);
1277
+ const fill = opt(id, "fill", node.props.fill);
1278
+ const stroke = opt(id, "stroke", node.props.stroke);
1279
+ ops.push({
1280
+ type: "path",
1281
+ id,
1282
+ transform: ox === 0 && oy === 0 ? matrix : multiply(matrix, [1, 0, 0, 1, -ox, -oy]),
1283
+ opacity,
1284
+ d: str(id, "d", node.props.d),
1285
+ progress: Math.max(0, Math.min(1, num(id, "progress", node.props.progress ?? 1))),
1286
+ ...fill !== void 0 && { fill },
1287
+ ...stroke !== void 0 && { stroke, strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1) }
1288
+ });
1289
+ return;
1290
+ }
851
1291
  case "text": {
852
1292
  const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
853
1293
  const raw = valueAt(id, "content", node.props.content);
@@ -908,14 +1348,62 @@ function collectImageSrcs(ir) {
908
1348
  walkTimeline(ir.timeline);
909
1349
  return [...srcs];
910
1350
  }
1351
+
1352
+ // ../core/src/motion.ts
1353
+ var EASE_BY_CLASS = {
1354
+ accelerating: "easeInCubic",
1355
+ decelerating: "easeOutCubic",
1356
+ linear: "linear"
1357
+ };
1358
+ function easeFor(easing) {
1359
+ return EASE_BY_CLASS[easing.class] ?? "easeOutCubic";
1360
+ }
1361
+ function sketchToTimeline(sketch, nodeIds) {
1362
+ if (nodeIds.length === 0) return seq();
1363
+ const events = [...sketch.events].sort((a, b) => a.t0 - b.t0);
1364
+ const steps = [];
1365
+ events.forEach((ev, i) => {
1366
+ const node = nodeIds[i % nodeIds.length];
1367
+ const dur2 = Math.max(0.05, ev.t1 - ev.t0);
1368
+ const ease = easeFor(ev.easing);
1369
+ let motion;
1370
+ switch (ev.kind) {
1371
+ case "enter":
1372
+ motion = tween(node, { opacity: 1 }, { duration: dur2, ease });
1373
+ break;
1374
+ case "exit":
1375
+ motion = tween(node, { opacity: 0 }, { duration: dur2, ease });
1376
+ break;
1377
+ case "emphasis": {
1378
+ const peak = 1 + Math.max(0.08, Math.min(0.5, ev.magnitude));
1379
+ motion = seq(
1380
+ tween(node, { scale: peak }, { duration: dur2 / 2, ease: "easeOutCubic" }),
1381
+ tween(node, { scale: 1 }, { duration: dur2 / 2, ease: "easeInOutQuad" })
1382
+ );
1383
+ break;
1384
+ }
1385
+ case "scale":
1386
+ motion = tween(node, { scale: 1 + Math.max(-0.5, Math.min(0.5, ev.magnitude)) }, { duration: dur2, ease });
1387
+ break;
1388
+ case "move":
1389
+ motion = tween(node, { opacity: 1 }, { duration: dur2, ease });
1390
+ break;
1391
+ }
1392
+ steps.push(ev.t0 > 0 ? seq(wait(ev.t0), motion) : motion);
1393
+ });
1394
+ return par(...steps);
1395
+ }
911
1396
  export {
912
1397
  DEFAULT_FPS,
1398
+ DEFAULT_MOTIONPATH_DURATION,
913
1399
  DEFAULT_TO_DURATION,
914
1400
  DEFAULT_TWEEN_DURATION,
915
1401
  EASE_NAMES,
1402
+ PRESET_NAMES,
916
1403
  PROPS_BY_TYPE,
917
1404
  SFX_DURATION,
918
1405
  SceneValidationError,
1406
+ beat,
919
1407
  collectImageSrcs,
920
1408
  compileScene,
921
1409
  composeScene,
@@ -927,14 +1415,20 @@ export {
927
1415
  isColor,
928
1416
  lerpValue,
929
1417
  line,
1418
+ motionPath,
1419
+ motionPreset,
930
1420
  oscillate,
931
1421
  par,
1422
+ path,
1423
+ pathPoint,
1424
+ pathTangentAngle,
932
1425
  rect,
933
1426
  resolveAudioPlan,
934
1427
  resolveEase,
935
1428
  sampleBehavior,
936
1429
  scene,
937
1430
  seq,
1431
+ sketchToTimeline,
938
1432
  stagger,
939
1433
  text,
940
1434
  to,