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/cli.js CHANGED
@@ -8,9 +8,88 @@ import { basename, dirname as dirname6, join as join5, resolve as resolve4 } fro
8
8
  // ../core/src/ir.ts
9
9
  var DEFAULT_TO_DURATION = 0.5;
10
10
  var DEFAULT_TWEEN_DURATION = 0.5;
11
+ var DEFAULT_MOTIONPATH_DURATION = 1;
12
+
13
+ // ../core/src/path.ts
14
+ function locate(segCount, u) {
15
+ if (segCount <= 0) return { i: 0, t: 0 };
16
+ const clamped = Math.max(0, Math.min(1, u));
17
+ const scaled = clamped * segCount;
18
+ let i = Math.floor(scaled);
19
+ if (i >= segCount) i = segCount - 1;
20
+ return { i, t: scaled - i };
21
+ }
22
+ function controls(points, closed, i) {
23
+ const n = points.length;
24
+ const at = (k) => {
25
+ if (closed) return points[(k % n + n) % n];
26
+ return points[Math.max(0, Math.min(n - 1, k))];
27
+ };
28
+ return [at(i - 1), at(i), at(i + 1), at(i + 2)];
29
+ }
30
+ function segCountOf(points, closed) {
31
+ const n = points.length;
32
+ if (n < 2) return 0;
33
+ return closed ? n : n - 1;
34
+ }
35
+ function pathPoint(points, closed, u) {
36
+ const n = points.length;
37
+ if (n === 0) return [0, 0];
38
+ if (n === 1) return [points[0][0], points[0][1]];
39
+ const segs = segCountOf(points, closed);
40
+ const { i, t } = locate(segs, u);
41
+ const [p0, p1, p2, p3] = controls(points, closed, i);
42
+ const t2 = t * t;
43
+ const t3 = t2 * t;
44
+ 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);
45
+ return [f(p0[0], p1[0], p2[0], p3[0]), f(p0[1], p1[1], p2[1], p3[1])];
46
+ }
47
+ function pathTangentAngle(points, closed, u) {
48
+ const n = points.length;
49
+ if (n < 2) return 0;
50
+ const segs = segCountOf(points, closed);
51
+ const { i, t } = locate(segs, u);
52
+ const [p0, p1, p2, p3] = controls(points, closed, i);
53
+ const t2 = t * t;
54
+ 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);
55
+ const dx = d(p0[0], p1[0], p2[0], p3[0]);
56
+ const dy = d(p0[1], p1[1], p2[1], p3[1]);
57
+ if (dx === 0 && dy === 0) return 0;
58
+ return Math.atan2(dy, dx) * 180 / Math.PI;
59
+ }
11
60
 
12
61
  // ../core/src/compile.ts
13
62
  var key = (target, prop) => `${target}.${prop}`;
63
+ function scaleTimeline(tl, k) {
64
+ switch (tl.kind) {
65
+ case "seq":
66
+ case "par":
67
+ return { ...tl, children: tl.children.map((c) => scaleTimeline(c, k)) };
68
+ case "stagger":
69
+ return { ...tl, interval: tl.interval * k, children: tl.children.map((c) => scaleTimeline(c, k)) };
70
+ case "wait":
71
+ return { ...tl, duration: tl.duration * k };
72
+ case "tween":
73
+ return { ...tl, duration: (tl.duration ?? DEFAULT_TWEEN_DURATION) * k };
74
+ case "motionPath":
75
+ return { ...tl, duration: (tl.duration ?? DEFAULT_MOTIONPATH_DURATION) * k };
76
+ case "to":
77
+ return {
78
+ ...tl,
79
+ duration: (tl.duration ?? DEFAULT_TO_DURATION) * k,
80
+ ...tl.stagger !== void 0 && { stagger: tl.stagger * k }
81
+ };
82
+ case "beat":
83
+ return {
84
+ ...tl,
85
+ children: tl.children.map((c) => scaleTimeline(c, k)),
86
+ ...tl.gap !== void 0 && { gap: tl.gap * k }
87
+ };
88
+ }
89
+ }
90
+ function orderBeats(children) {
91
+ 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);
92
+ }
14
93
  function compileScene(ir) {
15
94
  const nodeById = /* @__PURE__ */ new Map();
16
95
  const nodeOrder = [];
@@ -39,6 +118,7 @@ function compileScene(ir) {
39
118
  }
40
119
  }
41
120
  const segments = /* @__PURE__ */ new Map();
121
+ const motionPaths = /* @__PURE__ */ new Map();
42
122
  const current = new Map(initialValues);
43
123
  const pushSegment = (seg) => {
44
124
  const k = key(seg.target, seg.prop);
@@ -55,6 +135,50 @@ function compileScene(ir) {
55
135
  throw new Error(`cannot animate "${prop}" of "${target}": no base value to start from`);
56
136
  };
57
137
  const labelTimes = /* @__PURE__ */ new Map();
138
+ const beatTimes = /* @__PURE__ */ new Map();
139
+ const durationOf = (tl, start) => {
140
+ switch (tl.kind) {
141
+ case "seq": {
142
+ let t = start;
143
+ for (const child of orderBeats(tl.children)) t = durationOf(child, t);
144
+ return t;
145
+ }
146
+ case "par": {
147
+ let end = start;
148
+ for (const child of tl.children) end = Math.max(end, durationOf(child, start));
149
+ return end;
150
+ }
151
+ case "stagger": {
152
+ let end = start;
153
+ tl.children.forEach((child, i) => {
154
+ end = Math.max(end, durationOf(child, start + i * tl.interval));
155
+ });
156
+ return end;
157
+ }
158
+ case "wait":
159
+ return start + tl.duration;
160
+ case "tween":
161
+ return start + (tl.duration ?? DEFAULT_TWEEN_DURATION);
162
+ case "motionPath":
163
+ return start + (tl.duration ?? DEFAULT_MOTIONPATH_DURATION);
164
+ case "to": {
165
+ const override = ir.states?.[tl.state] ?? {};
166
+ const duration = tl.duration ?? DEFAULT_TO_DURATION;
167
+ const si = tl.stagger ?? 0;
168
+ const targets = nodeOrder.filter(
169
+ (id) => id in override && (tl.filter === void 0 || tl.filter.includes(id))
170
+ );
171
+ return start + duration + Math.max(0, targets.length - 1) * si;
172
+ }
173
+ case "beat": {
174
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
175
+ const natural = durationOf(grouping, 0);
176
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, natural) : 1);
177
+ const beatStart = tl.at ?? start + (tl.gap ?? 0);
178
+ return beatStart + k * natural;
179
+ }
180
+ }
181
+ };
58
182
  const walk = (tl, start) => {
59
183
  const end = walkInner(tl, start);
60
184
  if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
@@ -64,9 +188,19 @@ function compileScene(ir) {
64
188
  switch (tl.kind) {
65
189
  case "seq": {
66
190
  let t = start;
67
- for (const child of tl.children) t = walk(child, t);
191
+ for (const child of orderBeats(tl.children)) t = walk(child, t);
68
192
  return t;
69
193
  }
194
+ case "beat": {
195
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
196
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
197
+ const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
198
+ const beatStart = tl.at ?? start + (tl.gap ?? 0);
199
+ const end = walk(inner, beatStart);
200
+ beatTimes.set(tl.name, { t0: beatStart, t1: end });
201
+ labelTimes.set(tl.name, { t0: beatStart, t1: end });
202
+ return end;
203
+ }
70
204
  case "par": {
71
205
  let end = start;
72
206
  for (const child of tl.children) end = Math.max(end, walk(child, start));
@@ -96,6 +230,23 @@ function compileScene(ir) {
96
230
  }
97
231
  return start + duration;
98
232
  }
233
+ case "motionPath": {
234
+ const duration = tl.duration ?? DEFAULT_MOTIONPATH_DURATION;
235
+ const points = tl.points;
236
+ const closed = tl.closed ?? false;
237
+ const autoRotate = tl.autoRotate ?? false;
238
+ const rotateOffset = tl.rotateOffset ?? 0;
239
+ let list = motionPaths.get(tl.target);
240
+ if (!list) motionPaths.set(tl.target, list = []);
241
+ list.push({ t0: start, t1: start + duration, points, closed, autoRotate, rotateOffset, ...tl.ease !== void 0 && { ease: tl.ease } });
242
+ if (points.length > 0) {
243
+ const [ex, ey] = pathPoint(points, closed, 1);
244
+ current.set(key(tl.target, "x"), ex);
245
+ current.set(key(tl.target, "y"), ey);
246
+ if (autoRotate) current.set(key(tl.target, "rotation"), pathTangentAngle(points, closed, 1) + rotateOffset);
247
+ }
248
+ return start + duration;
249
+ }
99
250
  case "to": {
100
251
  const override = ir.states?.[tl.state] ?? {};
101
252
  const duration = tl.duration ?? DEFAULT_TO_DURATION;
@@ -124,14 +275,17 @@ function compileScene(ir) {
124
275
  };
125
276
  const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
126
277
  for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
278
+ for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
127
279
  return {
128
280
  ir,
129
281
  duration: ir.duration ?? inferredEnd,
130
282
  segments,
283
+ motionPaths,
131
284
  initialValues,
132
285
  nodeById,
133
286
  nodeOrder,
134
- labelTimes
287
+ labelTimes,
288
+ beatTimes
135
289
  };
136
290
  }
137
291
 
@@ -143,6 +297,7 @@ var PROPS_BY_TYPE = {
143
297
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
144
298
  text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
145
299
  image: [...COMMON_PROPS, "src", "width", "height"],
300
+ path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
146
301
  group: COMMON_PROPS
147
302
  };
148
303
  var SceneValidationError = class extends Error {
@@ -233,9 +388,39 @@ function validateScene(ir) {
233
388
  problems.push(`${path}: tween duration must be > 0`);
234
389
  }
235
390
  break;
391
+ case "motionPath": {
392
+ const node = nodeById.get(tl.target);
393
+ if (!node) {
394
+ problems.push(
395
+ `${path}: motionPath targets unknown node "${tl.target}" \u2014 known ids: ${[...nodeById.keys()].join(", ")}`
396
+ );
397
+ } else if (node.type === "line") {
398
+ problems.push(`${path}: motionPath cannot target a line (no x/y) \u2014 "${tl.target}"`);
399
+ }
400
+ if (tl.points.length < 1) problems.push(`${path}: motionPath "${tl.target}" needs at least 1 point`);
401
+ if (tl.duration !== void 0 && tl.duration <= 0) {
402
+ problems.push(`${path}: motionPath "${tl.target}" duration must be > 0`);
403
+ }
404
+ break;
405
+ }
236
406
  case "wait":
237
407
  if (tl.duration < 0) problems.push(`${path}: wait duration must be >= 0`);
238
408
  break;
409
+ case "beat":
410
+ if (labels.has(tl.name)) {
411
+ problems.push(
412
+ `${path}: duplicate timeline label "${tl.name}" (beat name) \u2014 labels are overlay addresses and must be unique`
413
+ );
414
+ }
415
+ labels.add(tl.name);
416
+ if (tl.duration !== void 0 && tl.duration <= 0) {
417
+ problems.push(`${path}: beat "${tl.name}" duration must be > 0`);
418
+ }
419
+ if (tl.scale !== void 0 && tl.scale <= 0) {
420
+ problems.push(`${path}: beat "${tl.name}" scale must be > 0`);
421
+ }
422
+ tl.children.forEach((c, i) => checkTimeline(c, `${path}.beat(${tl.name})[${i}]`));
423
+ break;
239
424
  }
240
425
  };
241
426
  if (ir.timeline) checkTimeline(ir.timeline, "timeline");
@@ -399,13 +584,16 @@ function applyOverlay(ir, overlay, layer, report) {
399
584
  const byLabel = /* @__PURE__ */ new Map();
400
585
  const walkTimeline = (tl) => {
401
586
  if ("label" in tl && tl.label !== void 0) byLabel.set(tl.label, tl);
587
+ if (tl.kind === "beat") byLabel.set(tl.name, tl);
402
588
  if ("children" in tl) tl.children.forEach(walkTimeline);
403
589
  };
404
590
  if (ir.timeline) walkTimeline(ir.timeline);
405
591
  const PATCHABLE = {
406
592
  to: ["duration", "ease", "stagger"],
407
593
  tween: ["duration", "ease"],
408
- wait: ["duration"]
594
+ wait: ["duration"],
595
+ motionPath: ["points", "duration", "ease"],
596
+ beat: ["at", "gap", "scale", "duration", "order"]
409
597
  };
410
598
  let timingPatched = false;
411
599
  for (const [label, patch] of Object.entries(overlay.timeline)) {
@@ -429,7 +617,7 @@ function applyOverlay(ir, overlay, layer, report) {
429
617
  }
430
618
  step[key2] = value;
431
619
  applied(`timeline.${label}.${key2}`, "set");
432
- if (key2 === "duration" || key2 === "stagger") timingPatched = true;
620
+ if (["duration", "stagger", "at", "gap", "scale", "order"].includes(key2)) timingPatched = true;
433
621
  }
434
622
  }
435
623
  if (timingPatched && overlay.scene?.duration === void 0) {
@@ -454,6 +642,9 @@ function formatComposeReport(report) {
454
642
  return lines.join("\n");
455
643
  }
456
644
 
645
+ // ../core/src/presets.ts
646
+ var SET = 1 / 120;
647
+
457
648
  // ../core/src/audio.ts
458
649
  var SFX_DURATION = {
459
650
  whoosh: 0.35,
@@ -526,6 +717,19 @@ function resolveAudioPlan(compiled) {
526
717
  }
527
718
 
528
719
  // ../core/src/interpolate.ts
720
+ var BACK_C1 = 1.70158;
721
+ var BACK_C2 = BACK_C1 * 1.525;
722
+ var BACK_C3 = BACK_C1 + 1;
723
+ var ELASTIC_C4 = 2 * Math.PI / 3;
724
+ var ELASTIC_C5 = 2 * Math.PI / 4.5;
725
+ function easeOutBounce(u) {
726
+ const n1 = 7.5625;
727
+ const d1 = 2.75;
728
+ if (u < 1 / d1) return n1 * u * u;
729
+ if (u < 2 / d1) return n1 * (u -= 1.5 / d1) * u + 0.75;
730
+ if (u < 2.5 / d1) return n1 * (u -= 2.25 / d1) * u + 0.9375;
731
+ return n1 * (u -= 2.625 / d1) * u + 0.984375;
732
+ }
529
733
  var EASE_TABLE = {
530
734
  linear: (u) => u,
531
735
  easeInQuad: (u) => u * u,
@@ -539,7 +743,20 @@ var EASE_TABLE = {
539
743
  easeInOutQuart: (u) => u < 0.5 ? 8 * u ** 4 : 1 - (-2 * u + 2) ** 4 / 2,
540
744
  easeInExpo: (u) => u === 0 ? 0 : 2 ** (10 * u - 10),
541
745
  easeOutExpo: (u) => u === 1 ? 1 : 1 - 2 ** (-10 * u),
542
- easeInOutExpo: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? 2 ** (20 * u - 10) / 2 : (2 - 2 ** (-20 * u + 10)) / 2
746
+ easeInOutExpo: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? 2 ** (20 * u - 10) / 2 : (2 - 2 ** (-20 * u + 10)) / 2,
747
+ // --- expressive eases (GSAP's signature feel) — standard Penner equations ---
748
+ // back: overshoots past the target then settles (pop / snap)
749
+ easeInBack: (u) => BACK_C3 * u ** 3 - BACK_C1 * u * u,
750
+ easeOutBack: (u) => 1 + BACK_C3 * (u - 1) ** 3 + BACK_C1 * (u - 1) ** 2,
751
+ 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,
752
+ // elastic: rings around the target before settling (playful spring)
753
+ easeInElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : -(2 ** (10 * u - 10)) * Math.sin((u * 10 - 10.75) * ELASTIC_C4),
754
+ easeOutElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : 2 ** (-10 * u) * Math.sin((u * 10 - 0.75) * ELASTIC_C4) + 1,
755
+ 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,
756
+ // bounce: drops and bounces to rest (lands without overshoot)
757
+ easeInBounce: (u) => 1 - easeOutBounce(1 - u),
758
+ easeOutBounce,
759
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
543
760
  };
544
761
  var EASE_NAMES = Object.keys(EASE_TABLE);
545
762