reframe-video 0.1.0 → 0.1.2

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
 
@@ -136,6 +290,8 @@ var PROPS_BY_TYPE = {
136
290
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
137
291
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
138
292
  text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
293
+ image: [...COMMON_PROPS, "src", "width", "height"],
294
+ path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
139
295
  group: COMMON_PROPS
140
296
  };
141
297
  var SceneValidationError = class extends Error {
@@ -189,11 +345,11 @@ function validateScene(ir) {
189
345
  );
190
346
  }
191
347
  const labels = /* @__PURE__ */ new Set();
192
- const checkTimeline = (tl, path) => {
348
+ const checkTimeline = (tl, path2) => {
193
349
  if ("label" in tl && tl.label !== void 0) {
194
350
  if (labels.has(tl.label)) {
195
351
  problems.push(
196
- `${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`
197
353
  );
198
354
  }
199
355
  labels.add(tl.label);
@@ -201,33 +357,63 @@ function validateScene(ir) {
201
357
  switch (tl.kind) {
202
358
  case "seq":
203
359
  case "par":
204
- tl.children.forEach((c, i) => checkTimeline(c, `${path}.${tl.kind}[${i}]`));
360
+ tl.children.forEach((c, i) => checkTimeline(c, `${path2}.${tl.kind}[${i}]`));
205
361
  break;
206
362
  case "stagger":
207
- if (tl.interval < 0) problems.push(`${path}: stagger interval must be >= 0`);
208
- 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}]`));
209
365
  break;
210
366
  case "to":
211
367
  if (!(tl.state in states)) {
212
368
  problems.push(
213
- `${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)"}`
214
370
  );
215
371
  }
216
372
  if (tl.duration !== void 0 && tl.duration <= 0) {
217
- problems.push(`${path}: to("${tl.state}") duration must be > 0`);
373
+ problems.push(`${path2}: to("${tl.state}") duration must be > 0`);
218
374
  }
219
375
  for (const id of tl.filter ?? []) {
220
- 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}"`);
221
377
  }
222
378
  break;
223
379
  case "tween":
224
- checkProps(path, tl.target, tl.props);
380
+ checkProps(path2, tl.target, tl.props);
225
381
  if (tl.duration !== void 0 && tl.duration <= 0) {
226
- problems.push(`${path}: tween duration must be > 0`);
382
+ problems.push(`${path2}: tween duration must be > 0`);
227
383
  }
228
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
+ }
229
400
  case "wait":
230
- 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}]`));
231
417
  break;
232
418
  }
233
419
  };
@@ -293,6 +479,14 @@ function text(props) {
293
479
  const { id, ...rest } = props;
294
480
  return { type: "text", id, props: rest };
295
481
  }
482
+ function image(props) {
483
+ const { id, ...rest } = props;
484
+ return { type: "image", id, props: rest };
485
+ }
486
+ function path(props) {
487
+ const { id, ...rest } = props;
488
+ return { type: "path", id, props: rest };
489
+ }
296
490
  function group(props, children) {
297
491
  const { id, ...rest } = props;
298
492
  return { type: "group", id, props: rest, children };
@@ -306,6 +500,9 @@ function par(...children) {
306
500
  function stagger(interval, ...children) {
307
501
  return { kind: "stagger", interval, children };
308
502
  }
503
+ function beat(name, opts, children) {
504
+ return { kind: "beat", name, children, ...opts };
505
+ }
309
506
  function to(state, opts = {}) {
310
507
  return { kind: "to", state, ...opts };
311
508
  }
@@ -315,6 +512,9 @@ function tween(target, props, opts = {}) {
315
512
  function wait(duration, label) {
316
513
  return { kind: "wait", duration, ...label !== void 0 && { label } };
317
514
  }
515
+ function motionPath(target, points, opts = {}) {
516
+ return { kind: "motionPath", target, points, ...opts };
517
+ }
318
518
  function oscillate(target, prop, params, window = {}) {
319
519
  return { target, prop, ...window, behavior: { kind: "named", name: "oscillate", params } };
320
520
  }
@@ -446,13 +646,16 @@ function applyOverlay(ir, overlay, layer, report) {
446
646
  const byLabel = /* @__PURE__ */ new Map();
447
647
  const walkTimeline = (tl) => {
448
648
  if ("label" in tl && tl.label !== void 0) byLabel.set(tl.label, tl);
649
+ if (tl.kind === "beat") byLabel.set(tl.name, tl);
449
650
  if ("children" in tl) tl.children.forEach(walkTimeline);
450
651
  };
451
652
  if (ir.timeline) walkTimeline(ir.timeline);
452
653
  const PATCHABLE = {
453
654
  to: ["duration", "ease", "stagger"],
454
655
  tween: ["duration", "ease"],
455
- wait: ["duration"]
656
+ wait: ["duration"],
657
+ motionPath: ["points", "duration", "ease"],
658
+ beat: ["at", "gap", "scale", "duration", "order"]
456
659
  };
457
660
  let timingPatched = false;
458
661
  for (const [label, patch] of Object.entries(overlay.timeline)) {
@@ -476,7 +679,7 @@ function applyOverlay(ir, overlay, layer, report) {
476
679
  }
477
680
  step[key2] = value;
478
681
  applied(`timeline.${label}.${key2}`, "set");
479
- if (key2 === "duration" || key2 === "stagger") timingPatched = true;
682
+ if (["duration", "stagger", "at", "gap", "scale", "order"].includes(key2)) timingPatched = true;
480
683
  }
481
684
  }
482
685
  if (timingPatched && overlay.scene?.duration === void 0) {
@@ -501,6 +704,186 @@ function formatComposeReport(report) {
501
704
  return lines.join("\n");
502
705
  }
503
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
+
504
887
  // ../core/src/audio.ts
505
888
  var SFX_DURATION = {
506
889
  whoosh: 0.35,
@@ -602,6 +985,19 @@ function hash01(n, seed) {
602
985
  }
603
986
 
604
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
+ }
605
1001
  var EASE_TABLE = {
606
1002
  linear: (u) => u,
607
1003
  easeInQuad: (u) => u * u,
@@ -615,7 +1011,20 @@ var EASE_TABLE = {
615
1011
  easeInOutQuart: (u) => u < 0.5 ? 8 * u ** 4 : 1 - (-2 * u + 2) ** 4 / 2,
616
1012
  easeInExpo: (u) => u === 0 ? 0 : 2 ** (10 * u - 10),
617
1013
  easeOutExpo: (u) => u === 1 ? 1 : 1 - 2 ** (-10 * u),
618
- 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
619
1028
  };
620
1029
  var EASE_NAMES = Object.keys(EASE_TABLE);
621
1030
  function resolveEase(ease) {
@@ -730,6 +1139,7 @@ function evaluate(compiled, t) {
730
1139
  const ops = [];
731
1140
  const valueAt = (target, prop, fallback) => {
732
1141
  let value = compiled.initialValues.get(`${target}.${prop}`) ?? fallback;
1142
+ let segStart = Number.NEGATIVE_INFINITY;
733
1143
  const segs = compiled.segments.get(`${target}.${prop}`);
734
1144
  if (segs) {
735
1145
  let active;
@@ -738,6 +1148,7 @@ function evaluate(compiled, t) {
738
1148
  else break;
739
1149
  }
740
1150
  if (active) {
1151
+ segStart = active.t0;
741
1152
  if (t >= active.t1) {
742
1153
  value = active.to;
743
1154
  } else {
@@ -746,6 +1157,23 @@ function evaluate(compiled, t) {
746
1157
  }
747
1158
  }
748
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
+ }
749
1177
  for (const b of compiled.ir.behaviors ?? []) {
750
1178
  if (b.target === target && b.prop === prop && typeof value === "number") {
751
1179
  const envelope = behaviorEnvelope(b, t);
@@ -826,6 +1254,40 @@ function evaluate(compiled, t) {
826
1254
  });
827
1255
  return;
828
1256
  }
1257
+ case "image": {
1258
+ const width = num(id, "width", node.props.width);
1259
+ const height = num(id, "height", node.props.height);
1260
+ const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
1261
+ ops.push({
1262
+ type: "image",
1263
+ id,
1264
+ transform: matrix,
1265
+ opacity,
1266
+ src: str(id, "src", node.props.src),
1267
+ width,
1268
+ height,
1269
+ offsetX: -width * ax,
1270
+ offsetY: -height * ay
1271
+ });
1272
+ return;
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
+ }
829
1291
  case "text": {
830
1292
  const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
831
1293
  const raw = valueAt(id, "content", node.props.content);
@@ -854,31 +1316,119 @@ function evaluate(compiled, t) {
854
1316
  for (const node of compiled.ir.nodes) walk(node, IDENTITY, 1);
855
1317
  return ops;
856
1318
  }
1319
+
1320
+ // ../core/src/assets.ts
1321
+ function collectImageSrcs(ir) {
1322
+ const srcs = /* @__PURE__ */ new Set();
1323
+ const imageIds = /* @__PURE__ */ new Set();
1324
+ const walkNodes = (nodes) => {
1325
+ for (const node of nodes) {
1326
+ if (node.type === "image") {
1327
+ imageIds.add(node.id);
1328
+ srcs.add(node.props.src);
1329
+ }
1330
+ if (node.type === "group") walkNodes(node.children);
1331
+ }
1332
+ };
1333
+ walkNodes(ir.nodes);
1334
+ for (const overrides of Object.values(ir.states ?? {})) {
1335
+ for (const [nodeId, props] of Object.entries(overrides)) {
1336
+ if (imageIds.has(nodeId) && typeof props.src === "string") srcs.add(props.src);
1337
+ }
1338
+ }
1339
+ const walkTimeline = (step) => {
1340
+ if (!step) return;
1341
+ if (step.kind === "seq" || step.kind === "par" || step.kind === "stagger") {
1342
+ for (const child of step.children) walkTimeline(child);
1343
+ } else if (step.kind === "tween" && imageIds.has(step.target)) {
1344
+ const src = step.props.src;
1345
+ if (typeof src === "string") srcs.add(src);
1346
+ }
1347
+ };
1348
+ walkTimeline(ir.timeline);
1349
+ return [...srcs];
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
+ }
857
1396
  export {
858
1397
  DEFAULT_FPS,
1398
+ DEFAULT_MOTIONPATH_DURATION,
859
1399
  DEFAULT_TO_DURATION,
860
1400
  DEFAULT_TWEEN_DURATION,
861
1401
  EASE_NAMES,
1402
+ PRESET_NAMES,
862
1403
  PROPS_BY_TYPE,
863
1404
  SFX_DURATION,
864
1405
  SceneValidationError,
1406
+ beat,
1407
+ collectImageSrcs,
865
1408
  compileScene,
866
1409
  composeScene,
867
1410
  ellipse,
868
1411
  evaluate,
869
1412
  formatComposeReport,
870
1413
  group,
1414
+ image,
871
1415
  isColor,
872
1416
  lerpValue,
873
1417
  line,
1418
+ motionPath,
1419
+ motionPreset,
874
1420
  oscillate,
875
1421
  par,
1422
+ path,
1423
+ pathPoint,
1424
+ pathTangentAngle,
876
1425
  rect,
877
1426
  resolveAudioPlan,
878
1427
  resolveEase,
879
1428
  sampleBehavior,
880
1429
  scene,
881
1430
  seq,
1431
+ sketchToTimeline,
882
1432
  stagger,
883
1433
  text,
884
1434
  to,