reframe-video 0.6.13 → 0.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -10,13 +10,14 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // ../core/src/ir.ts
13
- var DEFAULT_TO_DURATION, DEFAULT_TWEEN_DURATION, DEFAULT_MOTIONPATH_DURATION;
13
+ var DEFAULT_TO_DURATION, DEFAULT_TWEEN_DURATION, DEFAULT_MOTIONPATH_DURATION, DEFAULT_STILL_DURATION;
14
14
  var init_ir = __esm({
15
15
  "../core/src/ir.ts"() {
16
16
  "use strict";
17
17
  DEFAULT_TO_DURATION = 0.5;
18
18
  DEFAULT_TWEEN_DURATION = 0.5;
19
19
  DEFAULT_MOTIONPATH_DURATION = 1;
20
+ DEFAULT_STILL_DURATION = 1;
20
21
  }
21
22
  });
22
23
 
@@ -319,14 +320,14 @@ function compileScene(ir) {
319
320
  }
320
321
  }
321
322
  };
322
- const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
323
+ const inferredEnd = (ir.timeline ? walk(ir.timeline, 0) : 0) || 0;
323
324
  for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
324
325
  for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
325
326
  const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
326
327
  const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
327
328
  return {
328
329
  ir,
329
- duration: ir.duration ?? inferredEnd,
330
+ duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
330
331
  segments,
331
332
  motionPaths,
332
333
  initialValues,
@@ -1346,6 +1347,17 @@ var init_behaviors = __esm({
1346
1347
  });
1347
1348
 
1348
1349
  // ../core/src/interpolate.ts
1350
+ function springEase(stiffness, damping, velocity) {
1351
+ const K = 5;
1352
+ const zeta = Math.min(0.999, Math.max(0.05, damping / (2 * Math.sqrt(Math.max(1e-6, stiffness)))));
1353
+ const wd = K / zeta * Math.sqrt(1 - zeta * zeta);
1354
+ const coef = (K - velocity) / wd;
1355
+ return (u) => {
1356
+ if (u <= 0) return 0;
1357
+ if (u >= 1) return 1;
1358
+ return 1 - Math.exp(-K * u) * (Math.cos(wd * u) + coef * Math.sin(wd * u));
1359
+ };
1360
+ }
1349
1361
  function easeOutBounce(u) {
1350
1362
  const n1 = 7.5625;
1351
1363
  const d1 = 2.75;
@@ -1389,7 +1401,11 @@ var init_interpolate = __esm({
1389
1401
  // bounce: drops and bounces to rest (lands without overshoot)
1390
1402
  easeInBounce: (u) => 1 - easeOutBounce(1 - u),
1391
1403
  easeOutBounce,
1392
- easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
1404
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2,
1405
+ // damped-spring presets (ζ from damping/(2√stiffness)): 0.5 / 0.30 / 0.90
1406
+ spring: springEase(100, 10, 0),
1407
+ springBouncy: springEase(180, 8, 0),
1408
+ springStiff: springEase(210, 26, 0)
1393
1409
  };
1394
1410
  EASE_NAMES = Object.keys(EASE_TABLE);
1395
1411
  }
@@ -2843,10 +2859,16 @@ ${USAGE}`);
2843
2859
  }
2844
2860
  preflightFfmpeg();
2845
2861
  const outBase = PACKAGED ? join9(USER_CWD, "out") : join9(ROOT2, "out");
2862
+ const stem = `${basename(input).replace(/\.[^.]+$/, "")}.mp4`;
2846
2863
  let outArgs = args;
2847
- if (!args.includes("-o")) {
2864
+ const oIdx = args.indexOf("-o");
2865
+ if (oIdx === -1) {
2848
2866
  await mkdir4(outBase, { recursive: true });
2849
- outArgs = [...args, "-o", join9(outBase, `${basename(input).replace(/\.[^.]+$/, "")}.mp4`)];
2867
+ outArgs = [...args, "-o", join9(outBase, stem)];
2868
+ } else if (args[oIdx + 1] && !args[oIdx + 1].startsWith("-") && !/\.\w{2,4}$/.test(args[oIdx + 1])) {
2869
+ const dir = userPath(args[oIdx + 1]);
2870
+ await mkdir4(dir, { recursive: true });
2871
+ outArgs = args.map((a, i) => i === oIdx + 1 ? join9(dir, stem) : a);
2850
2872
  }
2851
2873
  outArgs = outArgs.map(
2852
2874
  (a, i) => outArgs[i - 1] === "--overlay" || outArgs[i - 1] === "-o" ? userPath(a) : a
@@ -4,6 +4,7 @@
4
4
  var DEFAULT_TO_DURATION = 0.5;
5
5
  var DEFAULT_TWEEN_DURATION = 0.5;
6
6
  var DEFAULT_MOTIONPATH_DURATION = 1;
7
+ var DEFAULT_STILL_DURATION = 1;
7
8
 
8
9
  // ../core/src/path.ts
9
10
  function pathBBox(d) {
@@ -316,14 +317,14 @@
316
317
  }
317
318
  }
318
319
  };
319
- const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
320
+ const inferredEnd = (ir.timeline ? walk(ir.timeline, 0) : 0) || 0;
320
321
  for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
321
322
  for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
322
323
  const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
323
324
  const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
324
325
  return {
325
326
  ir,
326
- duration: ir.duration ?? inferredEnd,
327
+ duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
327
328
  segments,
328
329
  motionPaths,
329
330
  initialValues,
@@ -408,6 +409,17 @@
408
409
  var BACK_C3 = BACK_C1 + 1;
409
410
  var ELASTIC_C4 = 2 * Math.PI / 3;
410
411
  var ELASTIC_C5 = 2 * Math.PI / 4.5;
412
+ function springEase(stiffness, damping, velocity) {
413
+ const K = 5;
414
+ const zeta = Math.min(0.999, Math.max(0.05, damping / (2 * Math.sqrt(Math.max(1e-6, stiffness)))));
415
+ const wd = K / zeta * Math.sqrt(1 - zeta * zeta);
416
+ const coef = (K - velocity) / wd;
417
+ return (u) => {
418
+ if (u <= 0) return 0;
419
+ if (u >= 1) return 1;
420
+ return 1 - Math.exp(-K * u) * (Math.cos(wd * u) + coef * Math.sin(wd * u));
421
+ };
422
+ }
411
423
  function easeOutBounce(u) {
412
424
  const n1 = 7.5625;
413
425
  const d1 = 2.75;
@@ -442,7 +454,11 @@
442
454
  // bounce: drops and bounces to rest (lands without overshoot)
443
455
  easeInBounce: (u) => 1 - easeOutBounce(1 - u),
444
456
  easeOutBounce,
445
- easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
457
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2,
458
+ // damped-spring presets (ζ from damping/(2√stiffness)): 0.5 / 0.30 / 0.90
459
+ spring: springEase(100, 10, 0),
460
+ springBouncy: springEase(180, 8, 0),
461
+ springStiff: springEase(210, 26, 0)
446
462
  };
447
463
  var EASE_NAMES = Object.keys(EASE_TABLE);
448
464
  function resolveEase(ease) {
@@ -452,6 +468,10 @@
452
468
  if (!fn) throw new Error(`unknown ease "${ease}" \u2014 valid: ${Object.keys(EASE_TABLE).join(", ")}`);
453
469
  return fn;
454
470
  }
471
+ if ("spring" in ease) {
472
+ const { stiffness = 100, damping = 10, velocity = 0 } = ease.spring;
473
+ return springEase(stiffness, damping, velocity);
474
+ }
455
475
  return cubicBezierEase(...ease.cubicBezier);
456
476
  }
457
477
  function cubicBezierEase(x1, y1, x2, y2) {
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ var DEFAULT_CROSSFADE = 0.5;
10
10
  var DEFAULT_TO_DURATION = 0.5;
11
11
  var DEFAULT_TWEEN_DURATION = 0.5;
12
12
  var DEFAULT_MOTIONPATH_DURATION = 1;
13
+ var DEFAULT_STILL_DURATION = 1;
13
14
 
14
15
  // ../core/src/path.ts
15
16
  function locate(segCount, u) {
@@ -306,14 +307,14 @@ function compileScene(ir) {
306
307
  }
307
308
  }
308
309
  };
309
- const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
310
+ const inferredEnd = (ir.timeline ? walk(ir.timeline, 0) : 0) || 0;
310
311
  for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
311
312
  for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
312
313
  const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
313
314
  const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
314
315
  return {
315
316
  ir,
316
- duration: ir.duration ?? inferredEnd,
317
+ duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
317
318
  segments,
318
319
  motionPaths,
319
320
  initialValues,
@@ -1050,6 +1051,17 @@ var BACK_C2 = BACK_C1 * 1.525;
1050
1051
  var BACK_C3 = BACK_C1 + 1;
1051
1052
  var ELASTIC_C4 = 2 * Math.PI / 3;
1052
1053
  var ELASTIC_C5 = 2 * Math.PI / 4.5;
1054
+ function springEase(stiffness, damping, velocity) {
1055
+ const K = 5;
1056
+ const zeta = Math.min(0.999, Math.max(0.05, damping / (2 * Math.sqrt(Math.max(1e-6, stiffness)))));
1057
+ const wd = K / zeta * Math.sqrt(1 - zeta * zeta);
1058
+ const coef = (K - velocity) / wd;
1059
+ return (u) => {
1060
+ if (u <= 0) return 0;
1061
+ if (u >= 1) return 1;
1062
+ return 1 - Math.exp(-K * u) * (Math.cos(wd * u) + coef * Math.sin(wd * u));
1063
+ };
1064
+ }
1053
1065
  function easeOutBounce(u) {
1054
1066
  const n1 = 7.5625;
1055
1067
  const d1 = 2.75;
@@ -1084,7 +1096,11 @@ var EASE_TABLE = {
1084
1096
  // bounce: drops and bounces to rest (lands without overshoot)
1085
1097
  easeInBounce: (u) => 1 - easeOutBounce(1 - u),
1086
1098
  easeOutBounce,
1087
- easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
1099
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2,
1100
+ // damped-spring presets (ζ from damping/(2√stiffness)): 0.5 / 0.30 / 0.90
1101
+ spring: springEase(100, 10, 0),
1102
+ springBouncy: springEase(180, 8, 0),
1103
+ springStiff: springEase(210, 26, 0)
1088
1104
  };
1089
1105
  var EASE_NAMES = Object.keys(EASE_TABLE);
1090
1106
 
package/dist/diff.js CHANGED
@@ -16,6 +16,7 @@ import { fileURLToPath } from "node:url";
16
16
  var DEFAULT_TO_DURATION = 0.5;
17
17
  var DEFAULT_TWEEN_DURATION = 0.5;
18
18
  var DEFAULT_MOTIONPATH_DURATION = 1;
19
+ var DEFAULT_STILL_DURATION = 1;
19
20
 
20
21
  // ../core/src/path.ts
21
22
  function locate(segCount, u) {
@@ -312,14 +313,14 @@ function compileScene(ir) {
312
313
  }
313
314
  }
314
315
  };
315
- const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
316
+ const inferredEnd = (ir.timeline ? walk(ir.timeline, 0) : 0) || 0;
316
317
  for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
317
318
  for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
318
319
  const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
319
320
  const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
320
321
  return {
321
322
  ir,
322
- duration: ir.duration ?? inferredEnd,
323
+ duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
323
324
  segments,
324
325
  motionPaths,
325
326
  initialValues,
@@ -639,6 +640,17 @@ var BACK_C2 = BACK_C1 * 1.525;
639
640
  var BACK_C3 = BACK_C1 + 1;
640
641
  var ELASTIC_C4 = 2 * Math.PI / 3;
641
642
  var ELASTIC_C5 = 2 * Math.PI / 4.5;
643
+ function springEase(stiffness, damping, velocity) {
644
+ const K = 5;
645
+ const zeta = Math.min(0.999, Math.max(0.05, damping / (2 * Math.sqrt(Math.max(1e-6, stiffness)))));
646
+ const wd = K / zeta * Math.sqrt(1 - zeta * zeta);
647
+ const coef = (K - velocity) / wd;
648
+ return (u) => {
649
+ if (u <= 0) return 0;
650
+ if (u >= 1) return 1;
651
+ return 1 - Math.exp(-K * u) * (Math.cos(wd * u) + coef * Math.sin(wd * u));
652
+ };
653
+ }
642
654
  function easeOutBounce(u) {
643
655
  const n1 = 7.5625;
644
656
  const d1 = 2.75;
@@ -673,7 +685,11 @@ var EASE_TABLE = {
673
685
  // bounce: drops and bounces to rest (lands without overshoot)
674
686
  easeInBounce: (u) => 1 - easeOutBounce(1 - u),
675
687
  easeOutBounce,
676
- easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
688
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2,
689
+ // damped-spring presets (ζ from damping/(2√stiffness)): 0.5 / 0.30 / 0.90
690
+ spring: springEase(100, 10, 0),
691
+ springBouncy: springEase(180, 8, 0),
692
+ springStiff: springEase(210, 26, 0)
677
693
  };
678
694
  var EASE_NAMES = Object.keys(EASE_TABLE);
679
695
 
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ var DEFAULT_TO_DURATION = 0.5;
4
4
  var DEFAULT_TWEEN_DURATION = 0.5;
5
5
  var DEFAULT_MOTIONPATH_DURATION = 1;
6
6
  var DEFAULT_FPS = 30;
7
+ var DEFAULT_STILL_DURATION = 1;
7
8
 
8
9
  // ../core/src/path.ts
9
10
  function pathBBox(d) {
@@ -316,14 +317,14 @@ function compileScene(ir) {
316
317
  }
317
318
  }
318
319
  };
319
- const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
320
+ const inferredEnd = (ir.timeline ? walk(ir.timeline, 0) : 0) || 0;
320
321
  for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
321
322
  for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
322
323
  const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
323
324
  const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
324
325
  return {
325
326
  ir,
326
- duration: ir.duration ?? inferredEnd,
327
+ duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
327
328
  segments,
328
329
  motionPaths,
329
330
  initialValues,
@@ -2850,6 +2851,17 @@ var BACK_C2 = BACK_C1 * 1.525;
2850
2851
  var BACK_C3 = BACK_C1 + 1;
2851
2852
  var ELASTIC_C4 = 2 * Math.PI / 3;
2852
2853
  var ELASTIC_C5 = 2 * Math.PI / 4.5;
2854
+ function springEase(stiffness, damping, velocity) {
2855
+ const K3 = 5;
2856
+ const zeta = Math.min(0.999, Math.max(0.05, damping / (2 * Math.sqrt(Math.max(1e-6, stiffness)))));
2857
+ const wd = K3 / zeta * Math.sqrt(1 - zeta * zeta);
2858
+ const coef = (K3 - velocity) / wd;
2859
+ return (u) => {
2860
+ if (u <= 0) return 0;
2861
+ if (u >= 1) return 1;
2862
+ return 1 - Math.exp(-K3 * u) * (Math.cos(wd * u) + coef * Math.sin(wd * u));
2863
+ };
2864
+ }
2853
2865
  function easeOutBounce(u) {
2854
2866
  const n1 = 7.5625;
2855
2867
  const d1 = 2.75;
@@ -2884,7 +2896,11 @@ var EASE_TABLE = {
2884
2896
  // bounce: drops and bounces to rest (lands without overshoot)
2885
2897
  easeInBounce: (u) => 1 - easeOutBounce(1 - u),
2886
2898
  easeOutBounce,
2887
- easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
2899
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2,
2900
+ // damped-spring presets (ζ from damping/(2√stiffness)): 0.5 / 0.30 / 0.90
2901
+ spring: springEase(100, 10, 0),
2902
+ springBouncy: springEase(180, 8, 0),
2903
+ springStiff: springEase(210, 26, 0)
2888
2904
  };
2889
2905
  var EASE_NAMES = Object.keys(EASE_TABLE);
2890
2906
  function resolveEase(ease) {
@@ -2894,6 +2910,10 @@ function resolveEase(ease) {
2894
2910
  if (!fn) throw new Error(`unknown ease "${ease}" \u2014 valid: ${Object.keys(EASE_TABLE).join(", ")}`);
2895
2911
  return fn;
2896
2912
  }
2913
+ if ("spring" in ease) {
2914
+ const { stiffness = 100, damping = 10, velocity = 0 } = ease.spring;
2915
+ return springEase(stiffness, damping, velocity);
2916
+ }
2897
2917
  return cubicBezierEase(...ease.cubicBezier);
2898
2918
  }
2899
2919
  function cubicBezierEase(x1, y1, x2, y2) {
@@ -3505,6 +3525,7 @@ export {
3505
3525
  DEFAULT_CROSSFADE,
3506
3526
  DEFAULT_FPS,
3507
3527
  DEFAULT_MOTIONPATH_DURATION,
3528
+ DEFAULT_STILL_DURATION,
3508
3529
  DEFAULT_TO_DURATION,
3509
3530
  DEFAULT_TWEEN_DURATION,
3510
3531
  DEVICE_PRESET_NAMES,
package/dist/labels.js CHANGED
@@ -4,6 +4,7 @@
4
4
  var DEFAULT_TO_DURATION = 0.5;
5
5
  var DEFAULT_TWEEN_DURATION = 0.5;
6
6
  var DEFAULT_MOTIONPATH_DURATION = 1;
7
+ var DEFAULT_STILL_DURATION = 1;
7
8
 
8
9
  // ../core/src/path.ts
9
10
  function locate(segCount, u) {
@@ -300,14 +301,14 @@ function compileScene(ir) {
300
301
  }
301
302
  }
302
303
  };
303
- const inferredEnd = ir.timeline ? walk(ir.timeline, 0) : 0;
304
+ const inferredEnd = (ir.timeline ? walk(ir.timeline, 0) : 0) || 0;
304
305
  for (const list of segments.values()) list.sort((a, b) => a.t0 - b.t0);
305
306
  for (const list of motionPaths.values()) list.sort((a, b) => a.t0 - b.t0);
306
307
  const hasCamera = !cameraIsNode && (ir.camera !== void 0 || motionPaths.has("camera") || [...segments.keys()].some((k) => k.startsWith("camera.")));
307
308
  const hasPerspective = !cameraIsNode && (ir.camera?.perspective !== void 0 || segments.has("camera.perspective"));
308
309
  return {
309
310
  ir,
310
- duration: ir.duration ?? inferredEnd,
311
+ duration: ir.duration ?? (inferredEnd > 0 ? inferredEnd : DEFAULT_STILL_DURATION),
311
312
  segments,
312
313
  motionPaths,
313
314
  initialValues,
@@ -597,6 +598,17 @@ var BACK_C2 = BACK_C1 * 1.525;
597
598
  var BACK_C3 = BACK_C1 + 1;
598
599
  var ELASTIC_C4 = 2 * Math.PI / 3;
599
600
  var ELASTIC_C5 = 2 * Math.PI / 4.5;
601
+ function springEase(stiffness, damping, velocity) {
602
+ const K = 5;
603
+ const zeta = Math.min(0.999, Math.max(0.05, damping / (2 * Math.sqrt(Math.max(1e-6, stiffness)))));
604
+ const wd = K / zeta * Math.sqrt(1 - zeta * zeta);
605
+ const coef = (K - velocity) / wd;
606
+ return (u) => {
607
+ if (u <= 0) return 0;
608
+ if (u >= 1) return 1;
609
+ return 1 - Math.exp(-K * u) * (Math.cos(wd * u) + coef * Math.sin(wd * u));
610
+ };
611
+ }
600
612
  function easeOutBounce(u) {
601
613
  const n1 = 7.5625;
602
614
  const d1 = 2.75;
@@ -631,7 +643,11 @@ var EASE_TABLE = {
631
643
  // bounce: drops and bounces to rest (lands without overshoot)
632
644
  easeInBounce: (u) => 1 - easeOutBounce(1 - u),
633
645
  easeOutBounce,
634
- easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
646
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2,
647
+ // damped-spring presets (ζ from damping/(2√stiffness)): 0.5 / 0.30 / 0.90
648
+ spring: springEase(100, 10, 0),
649
+ springBouncy: springEase(180, 8, 0),
650
+ springStiff: springEase(210, 26, 0)
635
651
  };
636
652
  var EASE_NAMES = Object.keys(EASE_TABLE);
637
653
 
package/dist/trace-cli.js CHANGED
@@ -42,6 +42,17 @@ var BACK_C2 = BACK_C1 * 1.525;
42
42
  var BACK_C3 = BACK_C1 + 1;
43
43
  var ELASTIC_C4 = 2 * Math.PI / 3;
44
44
  var ELASTIC_C5 = 2 * Math.PI / 4.5;
45
+ function springEase(stiffness, damping, velocity) {
46
+ const K = 5;
47
+ const zeta = Math.min(0.999, Math.max(0.05, damping / (2 * Math.sqrt(Math.max(1e-6, stiffness)))));
48
+ const wd = K / zeta * Math.sqrt(1 - zeta * zeta);
49
+ const coef = (K - velocity) / wd;
50
+ return (u) => {
51
+ if (u <= 0) return 0;
52
+ if (u >= 1) return 1;
53
+ return 1 - Math.exp(-K * u) * (Math.cos(wd * u) + coef * Math.sin(wd * u));
54
+ };
55
+ }
45
56
  function easeOutBounce(u) {
46
57
  const n1 = 7.5625;
47
58
  const d1 = 2.75;
@@ -76,7 +87,11 @@ var EASE_TABLE = {
76
87
  // bounce: drops and bounces to rest (lands without overshoot)
77
88
  easeInBounce: (u) => 1 - easeOutBounce(1 - u),
78
89
  easeOutBounce,
79
- easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
90
+ easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2,
91
+ // damped-spring presets (ζ from damping/(2√stiffness)): 0.5 / 0.30 / 0.90
92
+ spring: springEase(100, 10, 0),
93
+ springBouncy: springEase(180, 8, 0),
94
+ springStiff: springEase(210, 26, 0)
80
95
  };
81
96
  var EASE_NAMES = Object.keys(EASE_TABLE);
82
97
 
@@ -8,10 +8,24 @@
8
8
  * Semantics: a scene is evaluated as a pure function of continuous time
9
9
  * `evaluate(scene, tSeconds) -> DisplayList`. `fps` is a render hint only.
10
10
  */
11
- export type EaseName = "linear" | "easeInQuad" | "easeOutQuad" | "easeInOutQuad" | "easeInCubic" | "easeOutCubic" | "easeInOutCubic" | "easeInQuart" | "easeOutQuart" | "easeInOutQuart" | "easeInExpo" | "easeOutExpo" | "easeInOutExpo" | "easeInBack" | "easeOutBack" | "easeInOutBack" | "easeInElastic" | "easeOutElastic" | "easeInOutElastic" | "easeInBounce" | "easeOutBounce" | "easeInOutBounce";
11
+ export type EaseName = "linear" | "easeInQuad" | "easeOutQuad" | "easeInOutQuad" | "easeInCubic" | "easeOutCubic" | "easeInOutCubic" | "easeInQuart" | "easeOutQuart" | "easeInOutQuart" | "easeInExpo" | "easeOutExpo" | "easeInOutExpo" | "easeInBack" | "easeOutBack" | "easeInOutBack" | "easeInElastic" | "easeOutElastic" | "easeInOutElastic" | "easeInBounce" | "easeOutBounce" | "easeInOutBounce" | "spring" | "springBouncy" | "springStiff";
12
+ /**
13
+ * A custom spring: a damped harmonic oscillator sampled over the tween's normalized
14
+ * 0..1 window (mass = 1). `stiffness`/`damping` set the damping ratio
15
+ * ζ = damping / (2·√stiffness) — the SHAPE knob (low ζ ⇒ bouncy, high ζ ⇒ snappy);
16
+ * `velocity` is an initial launch slope. Defaults: stiffness 100, damping 10
17
+ * (ζ = 0.5), velocity 0.
18
+ */
19
+ export interface SpringEase {
20
+ spring: {
21
+ stiffness?: number;
22
+ damping?: number;
23
+ velocity?: number;
24
+ };
25
+ }
12
26
  export type Ease = EaseName | {
13
27
  cubicBezier: [number, number, number, number];
14
- };
28
+ } | SpringEase;
15
29
  export type Anchor = "top-left" | "top-center" | "top-right" | "center-left" | "center" | "center-right" | "bottom-left" | "bottom-center" | "bottom-right";
16
30
  export interface Size {
17
31
  width: number;
@@ -508,3 +522,6 @@ export declare const DEFAULT_TO_DURATION = 0.5;
508
522
  export declare const DEFAULT_TWEEN_DURATION = 0.5;
509
523
  export declare const DEFAULT_MOTIONPATH_DURATION = 1;
510
524
  export declare const DEFAULT_FPS = 30;
525
+ /** Fallback length (seconds) for a scene with no animating timeline — a static
526
+ * frame still needs a positive duration to render. Override with scene `duration`. */
527
+ export declare const DEFAULT_STILL_DURATION = 1;
@@ -43,6 +43,11 @@ reframe diff ref.png scene.ts --mode blend # 50% overlay — spot drift
43
43
 
44
44
  Loop: `--mode grid` to measure → write the node tree → `--mode side`/`diff` to compare →
45
45
  fix coordinates/sizes/colors → repeat until faithful. Pick the frame with `--t <sec>`.
46
+ The grid is rendered at the reference's **full resolution** — the printed numbers are
47
+ exact scene pixels, so place nodes at the labelled coordinates directly (no scaling).
48
+ `diff`/`blend` are sharpest on hard edges (type, icons, frames); over large **soft
49
+ gradients/glows** they always light up "different" even when close, so tune those by
50
+ eye with `--mode side` rather than chasing the diff to black.
46
51
 
47
52
  ### 3. Apply the cinematic-craft checklist
48
53
 
@@ -68,6 +68,9 @@ Factories return plain data. Every node needs a unique `id`.
68
68
  `"top-left"` (default) | `"top-center"` | `"top-right"` | `"center-left"` |
69
69
  `"center"` | `"center-right"` | `"bottom-left"` | `"bottom-center"` | `"bottom-right"`.
70
70
  Example: a bar that grows upward = `anchor: "bottom-left"` + animate `height`.
71
+ **Text alignment is `anchor`, not a separate `align` prop:** the anchor's horizontal
72
+ half sets the text align — `"…-left"` left-aligns, `"…-center"`/`"center"` centers,
73
+ `"…-right"` right-aligns (a right-aligned wordmark in a corner = `anchor: "bottom-right"`).
71
74
  Font: use `fontFamily: "Inter"` (weights 400/700/800 are available).
72
75
 
73
76
  ### Layout helpers (evenly spacing things)
@@ -132,8 +135,13 @@ Expressive eases for a premium feel: `easeIn/Out/InOutBack` (overshoots past the
132
135
  target then settles — a pop/snap), `easeIn/Out/InOutElastic` (rings around the
133
136
  target — a playful spring), `easeIn/Out/InOutBounce` (drops and bounces to rest).
134
137
  A logo or card "popping" in usually wants `easeOutBack`; a stamp landing,
135
- `easeOutBounce`.
136
- Scene duration is inferred from the timeline.
138
+ `easeOutBounce`. Physical springs settle to rest within the tween's duration:
139
+ `spring` (a natural settle), `springBouncy` (rings more), `springStiff` (snappy,
140
+ barely overshoots) — or tune your own with `{ spring: { stiffness, damping, velocity } }`
141
+ (damping ratio = `damping / (2·√stiffness)`; lower ⇒ bouncier).
142
+ Scene duration is inferred from the timeline. For a **static frame** you can omit
143
+ `timeline` entirely (or set scene `duration: <seconds>`) — a still defaults to a 1s
144
+ render; no throwaway `wait` is needed.
137
145
 
138
146
  ## Behaviors: continuous motion during holds
139
147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reframe-video",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
4
4
  "description": "Declarative motion graphics that AI can write and humans can tweak — human edits survive AI regeneration. Deterministic mp4 renders from a plain-data scene format.",
5
5
  "keywords": [
6
6
  "motion-graphics",