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.
@@ -3,9 +3,88 @@
3
3
  // ../core/src/ir.ts
4
4
  var DEFAULT_TO_DURATION = 0.5;
5
5
  var DEFAULT_TWEEN_DURATION = 0.5;
6
+ var DEFAULT_MOTIONPATH_DURATION = 1;
7
+
8
+ // ../core/src/path.ts
9
+ function locate(segCount, u) {
10
+ if (segCount <= 0) return { i: 0, t: 0 };
11
+ const clamped = Math.max(0, Math.min(1, u));
12
+ const scaled = clamped * segCount;
13
+ let i = Math.floor(scaled);
14
+ if (i >= segCount) i = segCount - 1;
15
+ return { i, t: scaled - i };
16
+ }
17
+ function controls(points, closed, i) {
18
+ const n = points.length;
19
+ const at = (k) => {
20
+ if (closed) return points[(k % n + n) % n];
21
+ return points[Math.max(0, Math.min(n - 1, k))];
22
+ };
23
+ return [at(i - 1), at(i), at(i + 1), at(i + 2)];
24
+ }
25
+ function segCountOf(points, closed) {
26
+ const n = points.length;
27
+ if (n < 2) return 0;
28
+ return closed ? n : n - 1;
29
+ }
30
+ function pathPoint(points, closed, u) {
31
+ const n = points.length;
32
+ if (n === 0) return [0, 0];
33
+ if (n === 1) return [points[0][0], points[0][1]];
34
+ const segs = segCountOf(points, closed);
35
+ const { i, t } = locate(segs, u);
36
+ const [p0, p1, p2, p3] = controls(points, closed, i);
37
+ const t2 = t * t;
38
+ const t3 = t2 * t;
39
+ 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);
40
+ return [f(p0[0], p1[0], p2[0], p3[0]), f(p0[1], p1[1], p2[1], p3[1])];
41
+ }
42
+ function pathTangentAngle(points, closed, u) {
43
+ const n = points.length;
44
+ if (n < 2) return 0;
45
+ const segs = segCountOf(points, closed);
46
+ const { i, t } = locate(segs, u);
47
+ const [p0, p1, p2, p3] = controls(points, closed, i);
48
+ const t2 = t * t;
49
+ 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);
50
+ const dx = d(p0[0], p1[0], p2[0], p3[0]);
51
+ const dy = d(p0[1], p1[1], p2[1], p3[1]);
52
+ if (dx === 0 && dy === 0) return 0;
53
+ return Math.atan2(dy, dx) * 180 / Math.PI;
54
+ }
6
55
 
7
56
  // ../core/src/compile.ts
8
57
  var key = (target, prop) => `${target}.${prop}`;
58
+ function scaleTimeline(tl, k) {
59
+ switch (tl.kind) {
60
+ case "seq":
61
+ case "par":
62
+ return { ...tl, children: tl.children.map((c) => scaleTimeline(c, k)) };
63
+ case "stagger":
64
+ return { ...tl, interval: tl.interval * k, children: tl.children.map((c) => scaleTimeline(c, k)) };
65
+ case "wait":
66
+ return { ...tl, duration: tl.duration * k };
67
+ case "tween":
68
+ return { ...tl, duration: (tl.duration ?? DEFAULT_TWEEN_DURATION) * k };
69
+ case "motionPath":
70
+ return { ...tl, duration: (tl.duration ?? DEFAULT_MOTIONPATH_DURATION) * k };
71
+ case "to":
72
+ return {
73
+ ...tl,
74
+ duration: (tl.duration ?? DEFAULT_TO_DURATION) * k,
75
+ ...tl.stagger !== void 0 && { stagger: tl.stagger * k }
76
+ };
77
+ case "beat":
78
+ return {
79
+ ...tl,
80
+ children: tl.children.map((c) => scaleTimeline(c, k)),
81
+ ...tl.gap !== void 0 && { gap: tl.gap * k }
82
+ };
83
+ }
84
+ }
85
+ function orderBeats(children) {
86
+ 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);
87
+ }
9
88
  function compileScene(ir) {
10
89
  const nodeById = /* @__PURE__ */ new Map();
11
90
  const nodeOrder = [];
@@ -34,6 +113,7 @@
34
113
  }
35
114
  }
36
115
  const segments = /* @__PURE__ */ new Map();
116
+ const motionPaths = /* @__PURE__ */ new Map();
37
117
  const current = new Map(initialValues);
38
118
  const pushSegment = (seg) => {
39
119
  const k = key(seg.target, seg.prop);
@@ -50,6 +130,50 @@
50
130
  throw new Error(`cannot animate "${prop}" of "${target}": no base value to start from`);
51
131
  };
52
132
  const labelTimes = /* @__PURE__ */ new Map();
133
+ const beatTimes = /* @__PURE__ */ new Map();
134
+ const durationOf = (tl, start) => {
135
+ switch (tl.kind) {
136
+ case "seq": {
137
+ let t = start;
138
+ for (const child of orderBeats(tl.children)) t = durationOf(child, t);
139
+ return t;
140
+ }
141
+ case "par": {
142
+ let end = start;
143
+ for (const child of tl.children) end = Math.max(end, durationOf(child, start));
144
+ return end;
145
+ }
146
+ case "stagger": {
147
+ let end = start;
148
+ tl.children.forEach((child, i) => {
149
+ end = Math.max(end, durationOf(child, start + i * tl.interval));
150
+ });
151
+ return end;
152
+ }
153
+ case "wait":
154
+ return start + tl.duration;
155
+ case "tween":
156
+ return start + (tl.duration ?? DEFAULT_TWEEN_DURATION);
157
+ case "motionPath":
158
+ return start + (tl.duration ?? DEFAULT_MOTIONPATH_DURATION);
159
+ case "to": {
160
+ const override = ir.states?.[tl.state] ?? {};
161
+ const duration = tl.duration ?? DEFAULT_TO_DURATION;
162
+ const si = tl.stagger ?? 0;
163
+ const targets = nodeOrder.filter(
164
+ (id) => id in override && (tl.filter === void 0 || tl.filter.includes(id))
165
+ );
166
+ return start + duration + Math.max(0, targets.length - 1) * si;
167
+ }
168
+ case "beat": {
169
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
170
+ const natural = durationOf(grouping, 0);
171
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, natural) : 1);
172
+ const beatStart = tl.at ?? start + (tl.gap ?? 0);
173
+ return beatStart + k * natural;
174
+ }
175
+ }
176
+ };
53
177
  const walk = (tl, start) => {
54
178
  const end = walkInner(tl, start);
55
179
  if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
@@ -59,9 +183,19 @@
59
183
  switch (tl.kind) {
60
184
  case "seq": {
61
185
  let t = start;
62
- for (const child of tl.children) t = walk(child, t);
186
+ for (const child of orderBeats(tl.children)) t = walk(child, t);
63
187
  return t;
64
188
  }
189
+ case "beat": {
190
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
191
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
192
+ const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
193
+ const beatStart = tl.at ?? start + (tl.gap ?? 0);
194
+ const end = walk(inner, beatStart);
195
+ beatTimes.set(tl.name, { t0: beatStart, t1: end });
196
+ labelTimes.set(tl.name, { t0: beatStart, t1: end });
197
+ return end;
198
+ }
65
199
  case "par": {
66
200
  let end = start;
67
201
  for (const child of tl.children) end = Math.max(end, walk(child, start));
@@ -91,6 +225,23 @@
91
225
  }
92
226
  return start + duration;
93
227
  }
228
+ case "motionPath": {
229
+ const duration = tl.duration ?? DEFAULT_MOTIONPATH_DURATION;
230
+ const points = tl.points;
231
+ const closed = tl.closed ?? false;
232
+ const autoRotate = tl.autoRotate ?? false;
233
+ const rotateOffset = tl.rotateOffset ?? 0;
234
+ let list = motionPaths.get(tl.target);
235
+ if (!list) motionPaths.set(tl.target, list = []);
236
+ list.push({ t0: start, t1: start + duration, points, closed, autoRotate, rotateOffset, ...tl.ease !== void 0 && { ease: tl.ease } });
237
+ if (points.length > 0) {
238
+ const [ex, ey] = pathPoint(points, closed, 1);
239
+ current.set(key(tl.target, "x"), ex);
240
+ current.set(key(tl.target, "y"), ey);
241
+ if (autoRotate) current.set(key(tl.target, "rotation"), pathTangentAngle(points, closed, 1) + rotateOffset);
242
+ }
243
+ return start + duration;
244
+ }
94
245
  case "to": {
95
246
  const override = ir.states?.[tl.state] ?? {};
96
247
  const duration = tl.duration ?? DEFAULT_TO_DURATION;
@@ -119,14 +270,17 @@
119
270
  };
120
271
  const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
121
272
  for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
273
+ for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
122
274
  return {
123
275
  ir,
124
276
  duration: ir.duration ?? inferredEnd,
125
277
  segments,
278
+ motionPaths,
126
279
  initialValues,
127
280
  nodeById,
128
281
  nodeOrder,
129
- labelTimes
282
+ labelTimes,
283
+ beatTimes
130
284
  };
131
285
  }
132
286
 
@@ -137,9 +291,14 @@
137
291
  ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
138
292
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
139
293
  text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
294
+ image: [...COMMON_PROPS, "src", "width", "height"],
295
+ path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
140
296
  group: COMMON_PROPS
141
297
  };
142
298
 
299
+ // ../core/src/presets.ts
300
+ var SET = 1 / 120;
301
+
143
302
  // ../core/src/behaviors.ts
144
303
  function sampleBehavior(b, t) {
145
304
  switch (b.name) {
@@ -170,6 +329,19 @@
170
329
  }
171
330
 
172
331
  // ../core/src/interpolate.ts
332
+ var BACK_C1 = 1.70158;
333
+ var BACK_C2 = BACK_C1 * 1.525;
334
+ var BACK_C3 = BACK_C1 + 1;
335
+ var ELASTIC_C4 = 2 * Math.PI / 3;
336
+ var ELASTIC_C5 = 2 * Math.PI / 4.5;
337
+ function easeOutBounce(u) {
338
+ const n1 = 7.5625;
339
+ const d1 = 2.75;
340
+ if (u < 1 / d1) return n1 * u * u;
341
+ if (u < 2 / d1) return n1 * (u -= 1.5 / d1) * u + 0.75;
342
+ if (u < 2.5 / d1) return n1 * (u -= 2.25 / d1) * u + 0.9375;
343
+ return n1 * (u -= 2.625 / d1) * u + 0.984375;
344
+ }
173
345
  var EASE_TABLE = {
174
346
  linear: (u) => u,
175
347
  easeInQuad: (u) => u * u,
@@ -183,7 +355,20 @@
183
355
  easeInOutQuart: (u) => u < 0.5 ? 8 * u ** 4 : 1 - (-2 * u + 2) ** 4 / 2,
184
356
  easeInExpo: (u) => u === 0 ? 0 : 2 ** (10 * u - 10),
185
357
  easeOutExpo: (u) => u === 1 ? 1 : 1 - 2 ** (-10 * u),
186
- easeInOutExpo: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? 2 ** (20 * u - 10) / 2 : (2 - 2 ** (-20 * u + 10)) / 2
358
+ easeInOutExpo: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? 2 ** (20 * u - 10) / 2 : (2 - 2 ** (-20 * u + 10)) / 2,
359
+ // --- expressive eases (GSAP's signature feel) — standard Penner equations ---
360
+ // back: overshoots past the target then settles (pop / snap)
361
+ easeInBack: (u) => BACK_C3 * u ** 3 - BACK_C1 * u * u,
362
+ easeOutBack: (u) => 1 + BACK_C3 * (u - 1) ** 3 + BACK_C1 * (u - 1) ** 2,
363
+ 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,
364
+ // elastic: rings around the target before settling (playful spring)
365
+ easeInElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : -(2 ** (10 * u - 10)) * Math.sin((u * 10 - 10.75) * ELASTIC_C4),
366
+ easeOutElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : 2 ** (-10 * u) * Math.sin((u * 10 - 0.75) * ELASTIC_C4) + 1,
367
+ 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,
368
+ // bounce: drops and bounces to rest (lands without overshoot)
369
+ easeInBounce: (u) => 1 - easeOutBounce(1 - u),
370
+ easeOutBounce,
371
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
187
372
  };
188
373
  var EASE_NAMES = Object.keys(EASE_TABLE);
189
374
  function resolveEase(ease) {
@@ -298,6 +483,7 @@
298
483
  const ops = [];
299
484
  const valueAt = (target, prop, fallback) => {
300
485
  let value = compiled2.initialValues.get(`${target}.${prop}`) ?? fallback;
486
+ let segStart = Number.NEGATIVE_INFINITY;
301
487
  const segs = compiled2.segments.get(`${target}.${prop}`);
302
488
  if (segs) {
303
489
  let active;
@@ -306,6 +492,7 @@
306
492
  else break;
307
493
  }
308
494
  if (active) {
495
+ segStart = active.t0;
309
496
  if (t >= active.t1) {
310
497
  value = active.to;
311
498
  } else {
@@ -314,6 +501,23 @@
314
501
  }
315
502
  }
316
503
  }
504
+ if (prop === "x" || prop === "y" || prop === "rotation") {
505
+ const drivers = compiled2.motionPaths.get(target);
506
+ if (drivers) {
507
+ let active;
508
+ for (const d of drivers) {
509
+ if (d.t0 <= t) active = d;
510
+ else break;
511
+ }
512
+ if (active && active.t0 >= segStart && (prop !== "rotation" || active.autoRotate) && active.points.length > 0) {
513
+ const span = active.t1 - active.t0;
514
+ const u = span <= 0 ? 1 : resolveEase(active.ease)(Math.max(0, Math.min(1, (t - active.t0) / span)));
515
+ if (prop === "x") value = pathPoint(active.points, active.closed, u)[0];
516
+ else if (prop === "y") value = pathPoint(active.points, active.closed, u)[1];
517
+ else value = pathTangentAngle(active.points, active.closed, u) + active.rotateOffset;
518
+ }
519
+ }
520
+ }
317
521
  for (const b of compiled2.ir.behaviors ?? []) {
318
522
  if (b.target === target && b.prop === prop && typeof value === "number") {
319
523
  const envelope = behaviorEnvelope(b, t);
@@ -394,6 +598,40 @@
394
598
  });
395
599
  return;
396
600
  }
601
+ case "image": {
602
+ const width = num(id, "width", node.props.width);
603
+ const height = num(id, "height", node.props.height);
604
+ const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
605
+ ops.push({
606
+ type: "image",
607
+ id,
608
+ transform: matrix,
609
+ opacity,
610
+ src: str(id, "src", node.props.src),
611
+ width,
612
+ height,
613
+ offsetX: -width * ax,
614
+ offsetY: -height * ay
615
+ });
616
+ return;
617
+ }
618
+ case "path": {
619
+ const ox = num(id, "originX", node.props.originX ?? 0);
620
+ const oy = num(id, "originY", node.props.originY ?? 0);
621
+ const fill = opt(id, "fill", node.props.fill);
622
+ const stroke = opt(id, "stroke", node.props.stroke);
623
+ ops.push({
624
+ type: "path",
625
+ id,
626
+ transform: ox === 0 && oy === 0 ? matrix : multiply(matrix, [1, 0, 0, 1, -ox, -oy]),
627
+ opacity,
628
+ d: str(id, "d", node.props.d),
629
+ progress: Math.max(0, Math.min(1, num(id, "progress", node.props.progress ?? 1))),
630
+ ...fill !== void 0 && { fill },
631
+ ...stroke !== void 0 && { stroke, strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1) }
632
+ });
633
+ return;
634
+ }
397
635
  case "text": {
398
636
  const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
399
637
  const raw = valueAt(id, "content", node.props.content);
@@ -424,7 +662,7 @@
424
662
  }
425
663
 
426
664
  // ../renderer-canvas/src/index.ts
427
- function renderFrame(ctx2, compiled2, t) {
665
+ function renderFrame(ctx2, compiled2, t, images2) {
428
666
  const { size, background } = compiled2.ir;
429
667
  ctx2.setTransform(1, 0, 0, 1, 0, 0);
430
668
  ctx2.clearRect(0, 0, size.width, size.height);
@@ -432,9 +670,9 @@
432
670
  ctx2.fillStyle = background;
433
671
  ctx2.fillRect(0, 0, size.width, size.height);
434
672
  }
435
- drawDisplayList(ctx2, evaluate(compiled2, t));
673
+ drawDisplayList(ctx2, evaluate(compiled2, t), images2);
436
674
  }
437
- function drawDisplayList(ctx2, ops) {
675
+ function drawDisplayList(ctx2, ops, images2) {
438
676
  for (const op of ops) {
439
677
  ctx2.save();
440
678
  ctx2.setTransform(...op.transform);
@@ -490,6 +728,48 @@
490
728
  ctx2.stroke();
491
729
  break;
492
730
  }
731
+ case "image": {
732
+ const img = images2?.get(op.src);
733
+ if (img) {
734
+ ctx2.drawImage(img, op.offsetX, op.offsetY, op.width, op.height);
735
+ } else {
736
+ ctx2.fillStyle = "#2A2A30";
737
+ ctx2.fillRect(op.offsetX, op.offsetY, op.width, op.height);
738
+ ctx2.strokeStyle = "#FF00FF";
739
+ ctx2.lineWidth = 2;
740
+ ctx2.strokeRect(op.offsetX, op.offsetY, op.width, op.height);
741
+ ctx2.beginPath();
742
+ ctx2.moveTo(op.offsetX, op.offsetY);
743
+ ctx2.lineTo(op.offsetX + op.width, op.offsetY + op.height);
744
+ ctx2.moveTo(op.offsetX + op.width, op.offsetY);
745
+ ctx2.lineTo(op.offsetX, op.offsetY + op.height);
746
+ ctx2.stroke();
747
+ }
748
+ break;
749
+ }
750
+ case "path": {
751
+ const p = new Path2D(op.d);
752
+ if (op.fill) {
753
+ ctx2.fillStyle = op.fill;
754
+ ctx2.fill(p);
755
+ }
756
+ if (op.stroke && (op.strokeWidth ?? 1) > 0) {
757
+ ctx2.strokeStyle = op.stroke;
758
+ ctx2.lineWidth = op.strokeWidth ?? 1;
759
+ ctx2.lineJoin = "round";
760
+ ctx2.lineCap = "round";
761
+ if (op.progress < 1) {
762
+ const len = pathLength(op.d);
763
+ if (len > 0) {
764
+ ctx2.setLineDash([len, len]);
765
+ ctx2.lineDashOffset = len * (1 - op.progress);
766
+ }
767
+ }
768
+ ctx2.stroke(p);
769
+ ctx2.setLineDash([]);
770
+ }
771
+ break;
772
+ }
493
773
  case "text": {
494
774
  ctx2.font = `${op.fontWeight} ${op.fontSize}px ${quoteFamily(op.fontFamily)}`;
495
775
  if (op.letterSpacing) ctx2.letterSpacing = `${op.letterSpacing}px`;
@@ -507,13 +787,28 @@
507
787
  function quoteFamily(family) {
508
788
  return family.includes(" ") && !family.includes('"') ? `"${family}"` : family;
509
789
  }
790
+ var pathLengthCache = /* @__PURE__ */ new Map();
791
+ function pathLength(d) {
792
+ const hit = pathLengthCache.get(d);
793
+ if (hit !== void 0) return hit;
794
+ let len = 0;
795
+ if (typeof document !== "undefined") {
796
+ const el = document.createElementNS("http://www.w3.org/2000/svg", "path");
797
+ el.setAttribute("d", d);
798
+ len = el.getTotalLength();
799
+ }
800
+ pathLengthCache.set(d, len);
801
+ return len;
802
+ }
510
803
 
511
804
  // ../render-cli/src/browserEntry.ts
512
805
  var compiled = null;
513
806
  var ctx = null;
514
807
  var canvas = null;
808
+ var images = /* @__PURE__ */ new Map();
515
809
  window.__reframe = {
516
- init(ir) {
810
+ // fully decode every image before the first frame — renderFrame is sync
811
+ async init(ir, assets = {}) {
517
812
  compiled = compileScene(ir);
518
813
  canvas = document.createElement("canvas");
519
814
  canvas.width = ir.size.width;
@@ -521,11 +816,19 @@
521
816
  document.body.appendChild(canvas);
522
817
  ctx = canvas.getContext("2d", { willReadFrequently: true });
523
818
  if (!ctx) throw new Error("could not create 2d context");
819
+ await Promise.all(
820
+ Object.entries(assets).map(async ([src, dataUrl]) => {
821
+ const img = new Image();
822
+ img.src = dataUrl;
823
+ await img.decode();
824
+ images.set(src, img);
825
+ })
826
+ );
524
827
  return { duration: compiled.duration, fps: ir.fps ?? 30 };
525
828
  },
526
829
  renderFrame(t) {
527
830
  if (!compiled || !ctx || !canvas) throw new Error("init() not called");
528
- renderFrame(ctx, compiled, t);
831
+ renderFrame(ctx, compiled, t, images);
529
832
  return canvas.toDataURL("image/png");
530
833
  }
531
834
  };