greenlight-motion 0.1.5 → 0.2.1

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.
@@ -34,15 +34,6 @@ var init_define_import_meta_env = __esm({
34
34
  }
35
35
  });
36
36
 
37
- // stand-in:three
38
- var require_three = __commonJS({
39
- "stand-in:three"(exports, module) {
40
- init_define_import_meta_env();
41
- var f = () => void 0;
42
- module.exports = new Proxy(f, { get: () => f });
43
- }
44
- });
45
-
46
37
  // node_modules/mp4box/dist/mp4box.all.js
47
38
  var require_mp4box_all = __commonJS({
48
39
  "node_modules/mp4box/dist/mp4box.all.js"(exports) {
@@ -6700,8 +6691,8 @@ var require_mp4box_all = __commonJS({
6700
6691
  var require_advancedTransitions = __commonJS({
6701
6692
  "stand-in:./advancedTransitions"(exports, module) {
6702
6693
  init_define_import_meta_env();
6703
- var f = () => void 0;
6704
- module.exports = new Proxy(f, { get: () => f });
6694
+ var f2 = () => void 0;
6695
+ module.exports = new Proxy(f2, { get: () => f2 });
6705
6696
  }
6706
6697
  });
6707
6698
 
@@ -6718,13 +6709,14 @@ init_define_import_meta_env();
6718
6709
  init_define_import_meta_env();
6719
6710
  var TXA = (function() {
6720
6711
  "use strict";
6721
- const clamp3 = (v, a, b2) => Math.min(b2, Math.max(a, v));
6712
+ const clamp6 = (v, a, b2) => Math.min(b2, Math.max(a, v));
6722
6713
  const lerp = (a, b2, t) => a + (b2 - a) * t;
6723
- const num4 = (label, def, min, max, step, unit = "", kf = true, title = "") => ({ type: "num", label, default: def, min, max, step, unit, kf, title });
6714
+ const num8 = (label, def, min, max, step, unit = "", kf = true, title = "") => ({ type: "num", label, default: def, min, max, step, unit, kf, title });
6724
6715
  const en = (label, def, labels, title = "") => ({ type: "enum", label, default: def, values: Object.keys(labels), labels, title });
6725
6716
  const bool = (label, def, title = "") => ({ type: "bool", label, default: def, title });
6726
6717
  const col = (label, def) => ({ type: "color", label, default: def });
6727
6718
  const cols = (label, def) => ({ type: "colors", label, default: def });
6719
+ const bez = (label, title = "") => ({ type: "bezier", label, default: null, title });
6728
6720
  const CURVE_NAMES2 = [
6729
6721
  "Linear",
6730
6722
  "Ease In",
@@ -6788,12 +6780,19 @@ var TXA = (function() {
6788
6780
  "In: units arrive from the effects. Out: they leave into them at the layer end. While its turn: the effects light each unit in turn. From its turn on: they stay on"
6789
6781
  ),
6790
6782
  order: en("Order", "forward", { forward: "Forward", backward: "Backward", center: "From centre", edges: "From edges", random: "Random" }),
6791
- delay: num4("Delay", 0, -60, 600, 0.01, "s", false, "In: after the layer starts. Out: before the layer ends. Voice: after the word is spoken (negative = before)"),
6792
- stagger: num4("Stagger", 0.06, 0, 10, 5e-3, "s", false, "Time between one unit's turn and the next"),
6793
- duration: num4("Duration", 0.6, 0, 20, 0.01, "s", false, "How long each unit takes"),
6794
- curve: en("Curve", "Ease Out", Object.fromEntries(CURVE_NAMES2.map((n2) => [n2, n2])), "Each unit's own easing: Back, Bounce and Overshoot land past their rest and settle"),
6795
- hold: num4("Hold", 0, 0, 600, 0.01, "s", false, "How long a unit stays lit. 0 = until the next unit's turn"),
6796
- fadeOut: num4("Fade out", 0, 0, 20, 0.01, "s", false),
6783
+ delay: num8("Delay", 0, -60, 600, 0.01, "s", false, "In: after the layer starts. Out: before the layer ends. Voice: after the word is spoken (negative = before)"),
6784
+ stagger: num8("Stagger", 0.06, 0, 10, 5e-3, "s", false, "Time between one unit's turn and the next"),
6785
+ duration: num8("Duration", 0.6, 0, 20, 0.01, "s", false, "How long each unit takes"),
6786
+ // Custom plays `bezier` (the app's keyframe Custom: a curve shaped by its handles).
6787
+ curve: en(
6788
+ "Curve",
6789
+ "Ease Out",
6790
+ { ...Object.fromEntries(CURVE_NAMES2.map((n2) => [n2, n2])), Custom: "Custom" },
6791
+ "Each unit's own easing: Back, Bounce and Overshoot land past their rest and settle"
6792
+ ),
6793
+ bezier: bez("Custom curve", "The Custom curve's handles, x1 y1 x2 y2 (x 0\u20261, y \u22123\u20264)"),
6794
+ hold: num8("Hold", 0, 0, 600, 0.01, "s", false, "How long a unit stays lit. 0 = until the next unit's turn"),
6795
+ fadeOut: num8("Fade out", 0, 0, 20, 0.01, "s", false),
6797
6796
  keepLast: bool("Keep last on", false, "The last unit stays lit"),
6798
6797
  spread: bool("Letters follow the voice", true, "Letters of a word take their turns across the time it is spoken, so they type at speaking pace"),
6799
6798
  basedOn: en("Based on", "chars", { chars: "Characters", charsNoSpace: "Characters, no spaces", words: "Words", lines: "Lines" }),
@@ -6805,27 +6804,27 @@ var TXA = (function() {
6805
6804
  "The unit this animator moves, scales and turns. Same as Based on: a word range turns whole words"
6806
6805
  ),
6807
6806
  units: en("Units", "percent", { percent: "%", index: "Index" }, "Index = count units (Start 2 = the third unit)"),
6808
- start: num4("Start", 0, -1e3, 1e3, 1),
6809
- end: num4("End", 100, -1e3, 1e3, 1),
6810
- offset: num4("Offset", 0, -1e3, 1e3, 1),
6807
+ start: num8("Start", 0, -1e3, 1e3, 1),
6808
+ end: num8("End", 100, -1e3, 1e3, 1),
6809
+ offset: num8("Offset", 0, -1e3, 1e3, 1),
6811
6810
  shape: en("Shape", "square", { square: "Square", rampUp: "Ramp up", rampDown: "Ramp down", triangle: "Triangle", round: "Round", smooth: "Smooth" }),
6812
- smooth: num4("Smoothness", 100, 0, 100, 1, "%", true, "Width of the edge fade, in units: 100 % = one unit"),
6813
- easeHigh: num4("Ease high", 0, -100, 100, 1, "%"),
6814
- easeLow: num4("Ease low", 0, -100, 100, 1, "%"),
6815
- amount: num4("Amount", 100, -100, 100, 1, "%", true, "Negative amounts push every effect the other way"),
6811
+ smooth: num8("Smoothness", 100, 0, 100, 1, "%", true, "Width of the edge fade, in units: 100 % = one unit"),
6812
+ easeHigh: num8("Ease high", 0, -100, 100, 1, "%"),
6813
+ easeLow: num8("Ease low", 0, -100, 100, 1, "%"),
6814
+ amount: num8("Amount", 100, -100, 100, 1, "%", true, "Negative amounts push every effect the other way"),
6816
6815
  randomize: bool("Random order", false),
6817
- seed: num4("Seed", 1, 0, 9999, 1, "", false),
6816
+ seed: num8("Seed", 1, 0, 9999, 1, "", false),
6818
6817
  // Keyframe-free travel: Offset advances this much per second (the Stroke
6819
6818
  // options "Flow" idea). Integrated by time, so not keyframable. Without Loop a
6820
6819
  // range parked off the text lands at Offset 0 and stays (see rangeEdges).
6821
- flow: num4("Flow", 0, -400, 400, 1, "/s", false, "Moves the whole range (its Offset) this much per second, no keyframes. From a negative Offset the range travels in and stops in place; from 0 it travels off the text"),
6820
+ flow: num8("Flow", 0, -400, 400, 1, "/s", false, "Moves the whole range (its Offset) this much per second, no keyframes. From a negative Offset the range travels in and stops in place; from 0 it travels off the text"),
6822
6821
  loop: bool("Loop", false, "The range wraps around the ends"),
6823
6822
  // Units are cut off at the edge of their own resting box (Transform around's
6824
6823
  // unit), so a move slides them in from behind it — Text Animation's Mask.
6825
6824
  mask: bool("Mask", false, "Each unit is cut off at the edge of its own box, so it slides in from behind it")
6826
6825
  };
6827
6826
  const RANGE_ONLY2 = { units: 1, start: 1, end: 1, offset: 1, shape: 1, smooth: 1, easeHigh: 1, easeLow: 1, flow: 1 };
6828
- const DRIVE_ONLY2 = { animate: 1, order: 1, delay: 1, stagger: 1, duration: 1, curve: 1, hold: 1, fadeOut: 1, keepLast: 1, spread: 1 };
6827
+ const DRIVE_ONLY2 = { animate: 1, order: 1, delay: 1, stagger: 1, duration: 1, curve: 1, bezier: 1, hold: 1, fadeOut: 1, keepLast: 1, spread: 1 };
6829
6828
  function rangeShows2(an, key, { voiced = false } = {}) {
6830
6829
  const r = an && an.range || {};
6831
6830
  const driven = r.drive === "time" || r.drive === "voice";
@@ -6852,17 +6851,17 @@ var TXA = (function() {
6852
6851
  const WIGGLE_FIELDS2 = {
6853
6852
  enabled: bool("Wiggle", false),
6854
6853
  mode: en("Mode", "intersect", { add: "Add", subtract: "Subtract", intersect: "Intersect", min: "Min", max: "Max", difference: "Difference" }),
6855
- max: num4("Max amount", 100, -100, 100, 1, "%"),
6856
- min: num4("Min amount", -100, -100, 100, 1, "%"),
6857
- rate: num4("Wiggles / s", 2, 0, 30, 0.1, "/s", false),
6858
- corr: num4("Correlation", 0, 0, 100, 1, "%"),
6859
- tPhase: num4("Temporal phase", 0, -360, 360, 1, "\xB0", false),
6860
- sPhase: num4("Spatial phase", 0, -360, 360, 1, "\xB0", false),
6861
- seed: num4("Seed", 1, 0, 9999, 1, "", false)
6854
+ max: num8("Max amount", 100, -100, 100, 1, "%"),
6855
+ min: num8("Min amount", -100, -100, 100, 1, "%"),
6856
+ rate: num8("Wiggles / s", 2, 0, 30, 0.1, "/s", false),
6857
+ corr: num8("Correlation", 0, 0, 100, 1, "%"),
6858
+ tPhase: num8("Temporal phase", 0, -360, 360, 1, "\xB0", false),
6859
+ sPhase: num8("Spatial phase", 0, -360, 360, 1, "\xB0", false),
6860
+ seed: num8("Seed", 1, 0, 9999, 1, "", false)
6862
6861
  };
6863
6862
  const LAYER_FIELDS2 = {
6864
- pivotX: num4("Pivot X", 0, -100, 100, 1, "%", false, "% of the unit from its centre: \u2212100 = left edge, 100 = right edge"),
6865
- pivotY: num4("Pivot Y", 0, -100, 100, 1, "%", false, "% of the unit from its centre: \u2212100 = top, 100 = bottom")
6863
+ pivotX: num8("Pivot X", 0, -100, 100, 1, "%", false, "% of the unit from its centre: \u2212100 = left edge, 100 = right edge"),
6864
+ pivotY: num8("Pivot Y", 0, -100, 100, 1, "%", false, "% of the unit from its centre: \u2212100 = top, 100 = bottom")
6866
6865
  };
6867
6866
  const LAYER_ITEM_KEYS2 = { pivotX: "textAnimatorPivotX", pivotY: "textAnimatorPivotY" };
6868
6867
  const EFFECTS2 = [
@@ -6871,21 +6870,21 @@ var TXA = (function() {
6871
6870
  label: "Position",
6872
6871
  group: "Transform",
6873
6872
  desc: "Move the selected units",
6874
- fields: { x: num4("X", 0, -2e3, 2e3, 1, "px"), y: num4("Y", -80, -2e3, 2e3, 1, "px") }
6873
+ fields: { x: num8("X", 0, -2e3, 2e3, 1, "px"), y: num8("Y", -80, -2e3, 2e3, 1, "px") }
6875
6874
  },
6876
6875
  {
6877
6876
  key: "scale",
6878
6877
  label: "Scale",
6879
6878
  group: "Transform",
6880
6879
  desc: "Grow or shrink each unit",
6881
- fields: { scale: num4("Scale", 160, -1e3, 1e3, 1, "%"), x: num4("Width", 100, -1e3, 1e3, 1, "%"), y: num4("Height", 100, -1e3, 1e3, 1, "%") }
6880
+ fields: { scale: num8("Scale", 160, -1e3, 1e3, 1, "%"), x: num8("Width", 100, -1e3, 1e3, 1, "%"), y: num8("Height", 100, -1e3, 1e3, 1, "%") }
6882
6881
  },
6883
6882
  {
6884
6883
  key: "rotation",
6885
6884
  label: "Rotation",
6886
6885
  group: "Transform",
6887
6886
  desc: "Spin each unit around its pivot",
6888
- fields: { angle: num4("Angle", 45, -3600, 3600, 1, "\xB0") }
6887
+ fields: { angle: num8("Angle", 45, -3600, 3600, 1, "\xB0") }
6889
6888
  },
6890
6889
  {
6891
6890
  key: "rotate3d",
@@ -6893,9 +6892,9 @@ var TXA = (function() {
6893
6892
  group: "Transform",
6894
6893
  desc: "Flip around X or Y, in perspective",
6895
6894
  fields: {
6896
- x: num4("Rotate X", 90, -720, 720, 1, "\xB0", true, "Positive tips the top away from you"),
6897
- y: num4("Rotate Y", 0, -720, 720, 1, "\xB0", true, "Positive turns the right side away from you"),
6898
- perspective: num4("Perspective", 800, 0, 2e4, 10, "px", true, "Camera distance from the unit: smaller = deeper 3D. 0 = flat, no depth")
6895
+ x: num8("Rotate X", 90, -720, 720, 1, "\xB0", true, "Positive tips the top away from you"),
6896
+ y: num8("Rotate Y", 0, -720, 720, 1, "\xB0", true, "Positive turns the right side away from you"),
6897
+ perspective: num8("Perspective", 800, 0, 2e4, 10, "px", true, "Camera distance from the unit: smaller = deeper 3D. 0 = flat, no depth")
6899
6898
  }
6900
6899
  },
6901
6900
  {
@@ -6903,70 +6902,70 @@ var TXA = (function() {
6903
6902
  label: "Skew",
6904
6903
  group: "Transform",
6905
6904
  desc: "Slant along an axis",
6906
- fields: { skew: num4("Skew", 25, -85, 85, 1, "\xB0"), axis: num4("Axis", 0, -180, 180, 1, "\xB0") }
6905
+ fields: { skew: num8("Skew", 25, -85, 85, 1, "\xB0"), axis: num8("Axis", 0, -180, 180, 1, "\xB0") }
6907
6906
  },
6908
6907
  {
6909
6908
  key: "tracking",
6910
6909
  label: "Tracking",
6911
6910
  group: "Transform",
6912
6911
  desc: "Space the units out \u2014 the line reflows",
6913
- fields: { amount: num4("Amount", 40, -200, 600, 1, "px") }
6912
+ fields: { amount: num8("Amount", 40, -200, 600, 1, "px") }
6914
6913
  },
6915
6914
  {
6916
6915
  key: "opacity",
6917
6916
  label: "Opacity",
6918
6917
  group: "Colour",
6919
6918
  desc: "Fade the selected units",
6920
- fields: { opacity: num4("Opacity", 0, 0, 100, 1, "%") }
6919
+ fields: { opacity: num8("Opacity", 0, 0, 100, 1, "%") }
6921
6920
  },
6922
6921
  {
6923
6922
  key: "fill",
6924
6923
  label: "Fill colour",
6925
6924
  group: "Colour",
6926
6925
  desc: "Recolour the selection",
6927
- fields: { color: col("Colour", "#ffd84d"), mix: num4("Mix", 100, 0, 100, 1, "%") }
6926
+ fields: { color: col("Colour", "#ffd84d"), mix: num8("Mix", 100, 0, 100, 1, "%") }
6928
6927
  },
6929
6928
  {
6930
6929
  key: "gradient",
6931
6930
  label: "Gradient",
6932
6931
  group: "Colour",
6933
6932
  desc: "Paint the selection with a gradient",
6934
- fields: { colors: cols("Colours", ["#7cf5ff", "#b36bff", "#ff5fa2"]), angle: num4("Angle", 0, -360, 360, 1, "\xB0"), span: en("Spans", "text", { text: "Text", unit: "Unit" }), shift: num4("Shift", 0, -1e3, 1e3, 1, "%"), repeat: bool("Repeat", false), mix: num4("Mix", 100, 0, 100, 1, "%") }
6933
+ fields: { colors: cols("Colours", ["#7cf5ff", "#b36bff", "#ff5fa2"]), angle: num8("Angle", 0, -360, 360, 1, "\xB0"), span: en("Spans", "text", { text: "Text", unit: "Unit" }), shift: num8("Shift", 0, -1e3, 1e3, 1, "%"), repeat: bool("Repeat", false), mix: num8("Mix", 100, 0, 100, 1, "%") }
6935
6934
  },
6936
6935
  {
6937
6936
  key: "hue",
6938
6937
  label: "Hue shift",
6939
6938
  group: "Colour",
6940
6939
  desc: "Rotate hue, saturation and brightness",
6941
- fields: { hue: num4("Hue", 180, -720, 720, 1, "\xB0"), saturation: num4("Saturation", 0, -100, 100, 1, "%"), brightness: num4("Brightness", 0, -100, 100, 1, "%") }
6940
+ fields: { hue: num8("Hue", 180, -720, 720, 1, "\xB0"), saturation: num8("Saturation", 0, -100, 100, 1, "%"), brightness: num8("Brightness", 0, -100, 100, 1, "%") }
6942
6941
  },
6943
6942
  {
6944
6943
  key: "stroke",
6945
6944
  label: "Stroke",
6946
6945
  group: "Colour",
6947
6946
  desc: "Outline around the glyphs",
6948
- fields: { color: col("Colour", "#ffffff"), width: num4("Width", 4, 0, 60, 0.5, "px"), outline: bool("Outline only", false) }
6947
+ fields: { color: col("Colour", "#ffffff"), width: num8("Width", 4, 0, 60, 0.5, "px"), outline: bool("Outline only", false) }
6949
6948
  },
6950
6949
  {
6951
6950
  key: "glow",
6952
6951
  label: "Glow",
6953
6952
  group: "Light",
6954
6953
  desc: "Soft light around the glyphs",
6955
- fields: { color: col("Colour", "#5ef2ff"), size: num4("Size", 30, 0, 200, 1, "px"), strength: num4("Strength", 120, 0, 300, 1, "%") }
6954
+ fields: { color: col("Colour", "#5ef2ff"), size: num8("Size", 30, 0, 200, 1, "px"), strength: num8("Strength", 120, 0, 300, 1, "%") }
6956
6955
  },
6957
6956
  {
6958
6957
  key: "shadow",
6959
6958
  label: "Drop shadow",
6960
6959
  group: "Light",
6961
6960
  desc: "Offset shadow under the glyphs",
6962
- fields: { color: col("Colour", "#000000"), x: num4("X", 0, -400, 400, 1, "px"), y: num4("Y", 18, -400, 400, 1, "px"), blur: num4("Blur", 20, 0, 200, 1, "px"), opacity: num4("Opacity", 70, 0, 100, 1, "%") }
6961
+ fields: { color: col("Colour", "#000000"), x: num8("X", 0, -400, 400, 1, "px"), y: num8("Y", 18, -400, 400, 1, "px"), blur: num8("Blur", 20, 0, 200, 1, "px"), opacity: num8("Opacity", 70, 0, 100, 1, "%") }
6963
6962
  },
6964
6963
  {
6965
6964
  key: "blur",
6966
6965
  label: "Blur",
6967
6966
  group: "Light",
6968
6967
  desc: "Defocus the selection",
6969
- fields: { amount: num4("Blur", 18, 0, 120, 0.5, "px") }
6968
+ fields: { amount: num8("Blur", 18, 0, 120, 0.5, "px") }
6970
6969
  },
6971
6970
  {
6972
6971
  key: "box",
@@ -6977,10 +6976,10 @@ var TXA = (function() {
6977
6976
  color: col("Colour", "#33efab"),
6978
6977
  behind: en("Behind", "selected", { selected: "Selected", revealed: "Revealed" }, "Revealed = the text this animator lets through. On a typing animator the box grows with the typing"),
6979
6978
  shape: en("Shape", "line", { line: "Per line", unit: "Per unit", block: "Block" }, "Per line = one bar per run of selected units in a row. Per unit = a bar behind each unit on its own. Block = one shape around all of it, growing a line at a time"),
6980
- padX: num4("Padding X", 12, 0, 200, 1, "px"),
6981
- padY: num4("Padding Y", 4, -60, 200, 1, "px"),
6982
- radius: num4("Radius", 10, 0, 200, 1, "px"),
6983
- opacity: num4("Opacity", 100, 0, 100, 1, "%"),
6979
+ padX: num8("Padding X", 12, 0, 200, 1, "px"),
6980
+ padY: num8("Padding Y", 4, -60, 200, 1, "px"),
6981
+ radius: num8("Radius", 10, 0, 200, 1, "px"),
6982
+ opacity: num8("Opacity", 100, 0, 100, 1, "%"),
6984
6983
  grow: bool("Grow with range", true),
6985
6984
  withText: bool("Fade with text", false, "Each bar is only as visible as its unit: a word that has not appeared yet has no bar")
6986
6985
  }
@@ -6996,10 +6995,10 @@ var TXA = (function() {
6996
6995
  style: en("Style", "bar", { bar: "Bar", underscore: "Underscore", block: "Block" }),
6997
6996
  ownColor: bool("Own colour", false, "Off = the text colour"),
6998
6997
  color: col("Colour", "#ffffff"),
6999
- width: num4("Width", 0, 0, 80, 0.5, "px", false, "0 = automatic"),
7000
- blink: num4("Blinks / s", 1.1, 0, 10, 0.1, "/s", false, "While the text is not typing. 0 = solid"),
6998
+ width: num8("Width", 0, 0, 80, 0.5, "px", false, "0 = automatic"),
6999
+ blink: num8("Blinks / s", 1.1, 0, 10, 0.1, "/s", false, "While the text is not typing. 0 = solid"),
7001
7000
  hide: bool("Hide when typed", false),
7002
- hideAfter: num4("Hide after", 0, 0, 600, 0.1, "s", false, "Seconds after the typing ends")
7001
+ hideAfter: num8("Hide after", 0, 0, 600, 0.1, "s", false, "Seconds after the typing ends")
7003
7002
  }
7004
7003
  },
7005
7004
  {
@@ -7007,14 +7006,14 @@ var TXA = (function() {
7007
7006
  label: "Scramble",
7008
7007
  group: "Characters",
7009
7008
  desc: "Random glyphs that settle into the text",
7010
- fields: { charset: en("Characters", "upper", { upper: "A\u2013Z", lower: "a\u2013z", digits: "0\u20139", alnum: "A\u2013Z 0\u20139", symbols: "Symbols", binary: "Binary", blocks: "Blocks" }), rate: num4("Changes / s", 18, 0, 60, 1, "/s", false), seed: num4("Seed", 1, 0, 9999, 1, "", false) }
7009
+ fields: { charset: en("Characters", "upper", { upper: "A\u2013Z", lower: "a\u2013z", digits: "0\u20139", alnum: "A\u2013Z 0\u20139", symbols: "Symbols", binary: "Binary", blocks: "Blocks" }), rate: num8("Changes / s", 18, 0, 60, 1, "/s", false), seed: num8("Seed", 1, 0, 9999, 1, "", false) }
7011
7010
  },
7012
7011
  {
7013
7012
  key: "charOffset",
7014
7013
  label: "Character offset",
7015
7014
  group: "Characters",
7016
7015
  desc: "Shift letters through the alphabet",
7017
- fields: { offset: num4("Offset", 6, -60, 60, 1, "") }
7016
+ fields: { offset: num8("Offset", 6, -60, 60, 1, "") }
7018
7017
  }
7019
7018
  ];
7020
7019
  const EFFECT_BY_KEY2 = Object.fromEntries(EFFECTS2.map((e2) => [e2.key, e2]));
@@ -7032,33 +7031,42 @@ var TXA = (function() {
7032
7031
  };
7033
7032
  function defaultsOf2(fields) {
7034
7033
  const out = {};
7035
- for (const [k, f] of Object.entries(fields)) out[k] = Array.isArray(f.default) ? f.default.slice() : f.default;
7034
+ for (const [k, f2] of Object.entries(fields)) out[k] = Array.isArray(f2.default) ? f2.default.slice() : f2.default;
7036
7035
  return out;
7037
7036
  }
7038
- function sanitizeField(f, v) {
7039
- switch (f.type) {
7037
+ function sanitizeField(f2, v) {
7038
+ switch (f2.type) {
7040
7039
  case "num": {
7041
7040
  const n2 = Number(v);
7042
- return Number.isFinite(n2) ? clamp3(n2, f.min, f.max) : f.default;
7041
+ return Number.isFinite(n2) ? clamp6(n2, f2.min, f2.max) : f2.default;
7043
7042
  }
7044
7043
  case "enum":
7045
- return f.values.includes(v) ? v : f.default;
7044
+ return f2.values.includes(v) ? v : f2.default;
7046
7045
  case "bool":
7047
- return typeof v === "boolean" ? v : f.default;
7046
+ return typeof v === "boolean" ? v : f2.default;
7048
7047
  case "color":
7049
- return typeof v === "string" && /^#[0-9a-f]{6}$/i.test(v) ? v.toLowerCase() : f.default;
7048
+ return typeof v === "string" && /^#[0-9a-f]{6}$/i.test(v) ? v.toLowerCase() : f2.default;
7050
7049
  case "colors": {
7051
7050
  const list = Array.isArray(v) ? v.filter((c) => typeof c === "string" && /^#[0-9a-f]{6}$/i.test(c)).slice(0, 6) : [];
7052
- return list.length >= 2 ? list.map((c) => c.toLowerCase()) : f.default.slice();
7051
+ return list.length >= 2 ? list.map((c) => c.toLowerCase()) : f2.default.slice();
7052
+ }
7053
+ // The app's sanitizeBezier (easingCurves.js): x stays in 0…1 so the solver
7054
+ // converges, y may overshoot; 4 dp so a dragged handle stays short.
7055
+ case "bezier": {
7056
+ if (!Array.isArray(v) || v.length !== 4) return f2.default;
7057
+ const n2 = v.map(Number);
7058
+ if (n2.some((x) => !Number.isFinite(x))) return f2.default;
7059
+ const r42 = (x) => Math.round(x * 1e4) / 1e4;
7060
+ return [r42(clamp6(n2[0], 0, 1)), r42(clamp6(n2[1], -3, 4)), r42(clamp6(n2[2], 0, 1)), r42(clamp6(n2[3], -3, 4))];
7053
7061
  }
7054
7062
  default:
7055
- return f.default;
7063
+ return f2.default;
7056
7064
  }
7057
7065
  }
7058
7066
  function sanitizeGroup(fields, raw) {
7059
7067
  const out = {};
7060
7068
  const src = raw && typeof raw === "object" ? raw : {};
7061
- for (const [k, f] of Object.entries(fields)) out[k] = sanitizeField(f, src[k]);
7069
+ for (const [k, f2] of Object.entries(fields)) out[k] = sanitizeField(f2, src[k]);
7062
7070
  return out;
7063
7071
  }
7064
7072
  const ID_RE = /^[A-Za-z0-9_-]{1,24}$/;
@@ -7071,11 +7079,14 @@ var TXA = (function() {
7071
7079
  if (!raw || typeof raw !== "object" || !ID_RE.test(String(raw.aid || ""))) return null;
7072
7080
  const seen = /* @__PURE__ */ new Set();
7073
7081
  const effects = (Array.isArray(raw.effects) ? raw.effects : []).map(sanitizeEffect).filter((e2) => e2 && !seen.has(e2.pid) && seen.add(e2.pid)).slice(0, MAX_EFFECTS2);
7082
+ const range = sanitizeGroup(RANGE_FIELDS2, raw.range);
7083
+ if (range.curve !== "Custom") range.bezier = null;
7084
+ else if (!range.bezier) range.curve = RANGE_FIELDS2.curve.default;
7074
7085
  return {
7075
7086
  aid: raw.aid,
7076
7087
  name: typeof raw.name === "string" && raw.name.trim() ? raw.name.trim().slice(0, 40) : `Animator ${i + 1}`,
7077
7088
  enabled: raw.enabled !== false,
7078
- range: sanitizeGroup(RANGE_FIELDS2, raw.range),
7089
+ range,
7079
7090
  wiggle: sanitizeGroup(WIGGLE_FIELDS2, raw.wiggle),
7080
7091
  effects
7081
7092
  };
@@ -7153,22 +7164,22 @@ var TXA = (function() {
7153
7164
  const an = (animators || []).find((a) => a && a.aid === aid);
7154
7165
  if (!an) return null;
7155
7166
  if (chan.startsWith("wig.")) {
7156
- const f2 = WIGGLE_FIELDS2[chan.slice(4)];
7157
- return f2 && f2.kf ? { an, spec: f2, label: `Wiggle ${f2.label.toLowerCase()}` } : null;
7167
+ const f3 = WIGGLE_FIELDS2[chan.slice(4)];
7168
+ return f3 && f3.kf ? { an, spec: f3, label: `Wiggle ${f3.label.toLowerCase()}` } : null;
7158
7169
  }
7159
7170
  const dot = chan.indexOf(".");
7160
7171
  if (dot > 0) {
7161
7172
  const fx = (an.effects || []).find((e2) => e2 && e2.pid === chan.slice(0, dot));
7162
7173
  const def = fx && EFFECT_BY_KEY2[fx.type];
7163
- const f2 = def && def.fields[chan.slice(dot + 1)];
7164
- if (!f2 || f2.type !== "num" || !f2.kf) return null;
7174
+ const f3 = def && def.fields[chan.slice(dot + 1)];
7175
+ if (!f3 || f3.type !== "num" || !f3.kf) return null;
7165
7176
  const numeric = Object.values(def.fields).filter((x) => x.type === "num" && x.kf).length;
7166
- return { an, fx, spec: f2, label: numeric === 1 || f2.label === def.label ? def.label : `${def.label} ${f2.label}` };
7177
+ return { an, fx, spec: f3, label: numeric === 1 || f3.label === def.label ? def.label : `${def.label} ${f3.label}` };
7167
7178
  }
7168
- const f = RANGE_FIELDS2[chan];
7169
- if (!f || !f.kf) return null;
7170
- const unit = chan === "start" || chan === "end" || chan === "offset" ? an.range?.units === "index" ? "" : "%" : f.unit;
7171
- return { an, spec: { ...f, unit }, label: f.label };
7179
+ const f2 = RANGE_FIELDS2[chan];
7180
+ if (!f2 || !f2.kf) return null;
7181
+ const unit = chan === "start" || chan === "end" || chan === "offset" ? an.range?.units === "index" ? "" : "%" : f2.unit;
7182
+ return { an, spec: { ...f2, unit }, label: f2.label };
7172
7183
  }
7173
7184
  function trackLabel2(animators, key) {
7174
7185
  const p = parseTrackKey2(key);
@@ -7196,20 +7207,20 @@ var TXA = (function() {
7196
7207
  for (const an of list) {
7197
7208
  if (!an || !an.aid) continue;
7198
7209
  const driven = !!an.range?.drive && an.range.drive !== "range";
7199
- for (const [k, f] of Object.entries(RANGE_FIELDS2)) {
7200
- if (!f.kf) continue;
7210
+ for (const [k, f2] of Object.entries(RANGE_FIELDS2)) {
7211
+ if (!f2.kf) continue;
7201
7212
  if (driven && RANGE_ONLY2[k]) continue;
7202
7213
  if (k === "smooth" && an.range?.shape !== "square") continue;
7203
7214
  if ((k === "easeHigh" || k === "easeLow") && an.range?.shape === "square") continue;
7204
7215
  row(an, k);
7205
7216
  }
7206
7217
  if (an.wiggle && an.wiggle.enabled) {
7207
- for (const [k, f] of Object.entries(WIGGLE_FIELDS2)) if (f.kf) row(an, `wig.${k}`);
7218
+ for (const [k, f2] of Object.entries(WIGGLE_FIELDS2)) if (f2.kf) row(an, `wig.${k}`);
7208
7219
  }
7209
7220
  for (const fx of an.effects || []) {
7210
7221
  const def = EFFECT_BY_KEY2[fx.type];
7211
7222
  if (!def) continue;
7212
- for (const [k, f] of Object.entries(def.fields)) if (f.type === "num" && f.kf) row(an, `${fx.pid}.${k}`);
7223
+ for (const [k, f2] of Object.entries(def.fields)) if (f2.type === "num" && f2.kf) row(an, `${fx.pid}.${k}`);
7213
7224
  }
7214
7225
  }
7215
7226
  return out;
@@ -7249,10 +7260,10 @@ var TXA = (function() {
7249
7260
  const hash01 = (a, b2) => hashU32(a, b2) / 4294967295;
7250
7261
  function noise1(x, seed) {
7251
7262
  const i = Math.floor(x);
7252
- const f = x - i;
7263
+ const f2 = x - i;
7253
7264
  const a = hash01(i, seed) * 2 - 1;
7254
7265
  const b2 = hash01(i + 1, seed) * 2 - 1;
7255
- const s = f * f * (3 - 2 * f);
7266
+ const s = f2 * f2 * (3 - 2 * f2);
7256
7267
  return a + (b2 - a) * s;
7257
7268
  }
7258
7269
  function mulberry32(seed) {
@@ -7282,43 +7293,43 @@ var TXA = (function() {
7282
7293
  permCache.set(key, p);
7283
7294
  return p;
7284
7295
  }
7285
- function easeShape2(f, low, high) {
7286
- if (!low && !high) return f;
7296
+ function easeShape2(f2, low, high) {
7297
+ if (!low && !high) return f2;
7287
7298
  const m0 = 1 - low / 100;
7288
7299
  const m1 = 1 - high / 100;
7289
- const f2 = f * f, f3 = f2 * f;
7290
- return clamp3((f3 - 2 * f2 + f) * m0 + (-2 * f3 + 3 * f2) + (f3 - f2) * m1, 0, 1);
7300
+ const f22 = f2 * f2, f3 = f22 * f2;
7301
+ return clamp6((f3 - 2 * f22 + f2) * m0 + (-2 * f3 + 3 * f22) + (f3 - f22) * m1, 0, 1);
7291
7302
  }
7292
- function shapeAt2(c, S, E, r, zeroOutside) {
7303
+ function shapeAt2(c, S, E2, r, zeroOutside) {
7293
7304
  if (r.shape === "square") {
7294
7305
  const w = Math.max(1e-4, r.smooth / 100);
7295
- return Math.min(clamp3((c - S) / w + 0.5, 0, 1), clamp3((E - c) / w + 0.5, 0, 1));
7306
+ return Math.min(clamp6((c - S) / w + 0.5, 0, 1), clamp6((E2 - c) / w + 0.5, 0, 1));
7296
7307
  }
7297
- const width = E - S;
7308
+ const width = E2 - S;
7298
7309
  if (width <= 1e-6) return 0;
7299
7310
  const u = (c - S) / width;
7300
7311
  if (u < 0) return !zeroOutside && r.shape === "rampDown" ? 1 : 0;
7301
7312
  if (u > 1) return !zeroOutside && r.shape === "rampUp" ? 1 : 0;
7302
- let f;
7313
+ let f2;
7303
7314
  switch (r.shape) {
7304
7315
  case "rampUp":
7305
- f = u;
7316
+ f2 = u;
7306
7317
  break;
7307
7318
  case "rampDown":
7308
- f = 1 - u;
7319
+ f2 = 1 - u;
7309
7320
  break;
7310
7321
  case "triangle":
7311
- f = 1 - Math.abs(2 * u - 1);
7322
+ f2 = 1 - Math.abs(2 * u - 1);
7312
7323
  break;
7313
7324
  case "round": {
7314
7325
  const q = 2 * u - 1;
7315
- f = Math.sqrt(Math.max(0, 1 - q * q));
7326
+ f2 = Math.sqrt(Math.max(0, 1 - q * q));
7316
7327
  break;
7317
7328
  }
7318
7329
  default:
7319
- f = 0.5 - 0.5 * Math.cos(2 * Math.PI * u);
7330
+ f2 = 0.5 - 0.5 * Math.cos(2 * Math.PI * u);
7320
7331
  }
7321
- return easeShape2(f, r.easeLow, r.easeHigh);
7332
+ return easeShape2(f2, r.easeLow, r.easeHigh);
7322
7333
  }
7323
7334
  function combine(a, b2, mode) {
7324
7335
  switch (mode) {
@@ -7339,8 +7350,8 @@ var TXA = (function() {
7339
7350
  function rangeEdges2(r, N, time) {
7340
7351
  let s = r.start, e2 = r.end;
7341
7352
  if (r.units === "percent") {
7342
- s = clamp3(s, 0, 100);
7343
- e2 = clamp3(e2, 0, 100);
7353
+ s = clamp6(s, 0, 100);
7354
+ e2 = clamp6(e2, 0, 100);
7344
7355
  }
7345
7356
  if (s > e2) {
7346
7357
  const t = s;
@@ -7363,24 +7374,24 @@ var TXA = (function() {
7363
7374
  function selection2(r, w, N, time) {
7364
7375
  const out = new Float32Array(Math.max(0, N));
7365
7376
  if (N <= 0) return { amounts: out, S: 0, E: 0 };
7366
- const { S, E } = rangeEdges2(r, N, time);
7377
+ const { S, E: E2 } = rangeEdges2(r, N, time);
7367
7378
  const perm = r.randomize ? seededPerm(N, r.seed) : null;
7368
7379
  const amt = r.amount / 100;
7369
7380
  for (let i = 0; i < N; i += 1) {
7370
7381
  const c = (perm ? perm[i] : i) + 0.5;
7371
- let f = r.loop ? Math.max(shapeAt2(c - N, S, E, r, true), shapeAt2(c, S, E, r, true), shapeAt2(c + N, S, E, r, true)) : shapeAt2(c, S, E, r, false);
7372
- f *= amt;
7382
+ let f2 = r.loop ? Math.max(shapeAt2(c - N, S, E2, r, true), shapeAt2(c, S, E2, r, true), shapeAt2(c + N, S, E2, r, true)) : shapeAt2(c, S, E2, r, false);
7383
+ f2 *= amt;
7373
7384
  if (w) {
7374
7385
  const t = time * w.rate + w.tPhase / 360;
7375
7386
  const nu = noise1(t + i * (w.sPhase / 360), hashU32(w.seed, i + 1));
7376
7387
  const ng = noise1(t, hashU32(w.seed, 0));
7377
7388
  const v = lerp(nu, ng, w.corr / 100);
7378
7389
  const wv = (w.min + (v + 1) / 2 * (w.max - w.min)) / 100;
7379
- f = combine(f, wv, w.mode);
7390
+ f2 = combine(f2, wv, w.mode);
7380
7391
  }
7381
- out[i] = clamp3(f, -1, 1);
7392
+ out[i] = clamp6(f2, -1, 1);
7382
7393
  }
7383
- return { amounts: out, S, E };
7394
+ return { amounts: out, S, E: E2 };
7384
7395
  }
7385
7396
  const powIn = (n2) => (p) => Math.pow(p, n2);
7386
7397
  const outOf = (fin) => (p) => 1 - fin(1 - p);
@@ -7401,7 +7412,7 @@ var TXA = (function() {
7401
7412
  const bx = (t) => 3 * p1x * t * (1 - t) * (1 - t) + 3 * p2x * t * t * (1 - t) + t * t * t;
7402
7413
  const by = (t) => 3 * p1y * t * (1 - t) * (1 - t) + 3 * p2y * t * t * (1 - t) + t * t * t;
7403
7414
  return (x) => {
7404
- x = clamp3(x, 0, 1);
7415
+ x = clamp6(x, 0, 1);
7405
7416
  let lo = 0, hi = 1, t = x;
7406
7417
  for (let i = 0; i < 24; i += 1) {
7407
7418
  const cx = bx(t);
@@ -7461,7 +7472,15 @@ var TXA = (function() {
7461
7472
  "Glide Easing Stop": bezier(0, 0.9125, 1, -0.0338)
7462
7473
  };
7463
7474
  })();
7464
- const curveFn2 = (name) => CURVE_FNS[name] || CURVE_FNS["Ease Out"];
7475
+ let lastBez = null;
7476
+ const curveFn2 = (name, handles) => {
7477
+ if (name === "Custom" && Array.isArray(handles) && handles.length === 4) {
7478
+ const key = handles.join(",");
7479
+ if (!lastBez || lastBez.key !== key) lastBez = { key, fn: bezier(...handles) };
7480
+ return lastBez.fn;
7481
+ }
7482
+ return CURVE_FNS[name] || CURVE_FNS["Ease Out"];
7483
+ };
7465
7484
  function ranksOf2(order, N, seed) {
7466
7485
  const ranks = new Float32Array(Math.max(0, N));
7467
7486
  const c = (N - 1) / 2;
@@ -7552,9 +7571,9 @@ var TXA = (function() {
7552
7571
  const amounts = new Float32Array(Math.max(0, N));
7553
7572
  const res = { amounts, voiced: false, spans: [], inEnd: 0, outStart: Infinity };
7554
7573
  if (N <= 0) return res;
7555
- const E = curveFn2(r.curve);
7574
+ const E2 = curveFn2(r.curve, r.bezier);
7556
7575
  const d = Math.max(0, r.duration);
7557
- const ramp = (t, t0, len) => len > 1e-6 ? clamp3((t - t0) / len, 0, 1) : t >= t0 ? 1 : 0;
7576
+ const ramp = (t, t0, len) => len > 1e-6 ? clamp6((t - t0) / len, 0, 1) : t >= t0 ? 1 : 0;
7558
7577
  const time = Number(ctx.time) || 0;
7559
7578
  const clip = Math.max(0, Number(ctx.clipSec) || 0);
7560
7579
  const vt = r.drive === "voice" ? voiceTurns2(r, N, ctx.units, ctx.counts, ctx.voice) : null;
@@ -7583,24 +7602,24 @@ var TXA = (function() {
7583
7602
  let a;
7584
7603
  switch (r.animate) {
7585
7604
  case "out":
7586
- a = E(ramp(time, outT0(i), d));
7605
+ a = E2(ramp(time, outT0(i), d));
7587
7606
  break;
7588
7607
  case "inOut":
7589
- a = 1 - E(ramp(tc, t0, d)) + E(ramp(time, outT0(i), d));
7608
+ a = 1 - E2(ramp(tc, t0, d)) + E2(ramp(time, outT0(i), d));
7590
7609
  break;
7591
7610
  case "stay":
7592
- a = E(ramp(tc, t0, d));
7611
+ a = E2(ramp(tc, t0, d));
7593
7612
  break;
7594
7613
  case "active": {
7595
7614
  const isLast = vt ? i === N - 1 : ranks[i] === max;
7596
7615
  const off = isLast && r.keepLast && !looping ? Infinity : r.hold > 0 ? t0 + r.hold : t1;
7597
- a = E(ramp(tc, t0, d));
7598
- if (tc >= off) a = Math.min(a, r.fadeOut > 0 ? 1 - E(ramp(tc, off, r.fadeOut)) : 0);
7616
+ a = E2(ramp(tc, t0, d));
7617
+ if (tc >= off) a = Math.min(a, r.fadeOut > 0 ? 1 - E2(ramp(tc, off, r.fadeOut)) : 0);
7599
7618
  a1 = Math.max(a1, off === Infinity ? t0 + d : off + r.fadeOut);
7600
7619
  break;
7601
7620
  }
7602
7621
  default:
7603
- a = 1 - E(ramp(tc, t0, d));
7622
+ a = 1 - E2(ramp(tc, t0, d));
7604
7623
  }
7605
7624
  if (r.animate !== "active") a1 = Math.max(a1, t0 + d);
7606
7625
  a0 = Math.min(a0, t0);
@@ -7621,15 +7640,15 @@ var TXA = (function() {
7621
7640
  const out = res.amounts;
7622
7641
  const amt = r.amount / 100;
7623
7642
  for (let i = 0; i < N; i += 1) {
7624
- let f = out[i] * amt;
7643
+ let f2 = out[i] * amt;
7625
7644
  if (w) {
7626
7645
  const t = time * w.rate + w.tPhase / 360;
7627
7646
  const nu = noise1(t + i * (w.sPhase / 360), hashU32(w.seed, i + 1));
7628
7647
  const ng = noise1(t, hashU32(w.seed, 0));
7629
7648
  const v = lerp(nu, ng, w.corr / 100);
7630
- f = combine(f, (w.min + (v + 1) / 2 * (w.max - w.min)) / 100, w.mode);
7649
+ f2 = combine(f2, (w.min + (v + 1) / 2 * (w.max - w.min)) / 100, w.mode);
7631
7650
  }
7632
- out[i] = clamp3(f, -2, 2);
7651
+ out[i] = clamp6(f2, -2, 2);
7633
7652
  }
7634
7653
  return { amounts: out, S: null, E: null, drive: res };
7635
7654
  }
@@ -7693,7 +7712,7 @@ var TXA = (function() {
7693
7712
  if (s === 0) return [l * 255, l * 255, l * 255];
7694
7713
  const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
7695
7714
  const p = 2 * l - q;
7696
- const f = (t) => {
7715
+ const f2 = (t) => {
7697
7716
  if (t < 0) t += 1;
7698
7717
  if (t > 1) t -= 1;
7699
7718
  if (t < 1 / 6) return p + (q - p) * 6 * t;
@@ -7701,13 +7720,13 @@ var TXA = (function() {
7701
7720
  if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
7702
7721
  return p;
7703
7722
  };
7704
- return [f(h + 1 / 3) * 255, f(h) * 255, f(h - 1 / 3) * 255];
7723
+ return [f2(h + 1 / 3) * 255, f2(h) * 255, f2(h - 1 / 3) * 255];
7705
7724
  }
7706
7725
  function shiftColor(c, dh, ds, db) {
7707
7726
  const [h, s, l] = rgbToHsl(c);
7708
- const s2 = clamp3(s * (1 + ds), 0, 1);
7727
+ const s2 = clamp6(s * (1 + ds), 0, 1);
7709
7728
  const l2 = db >= 0 ? l + (1 - l) * db : l * (1 + db);
7710
- return hslToRgb([h + dh, s2, clamp3(l2, 0, 1)]);
7729
+ return hslToRgb([h + dh, s2, clamp6(l2, 0, 1)]);
7711
7730
  }
7712
7731
  const rgbCss2 = (c, a = 1) => `rgba(${Math.round(c[0])},${Math.round(c[1])},${Math.round(c[2])},${a})`;
7713
7732
  function gradientStops(colors, shiftPct, repeat) {
@@ -7715,7 +7734,7 @@ var TXA = (function() {
7715
7734
  if (!shiftPct && !repeat) return rgb.map((c, i) => ({ p: rgb.length > 1 ? i / (rgb.length - 1) : 0, c }));
7716
7735
  const list = repeat ? rgb.concat([rgb[0]]) : rgb;
7717
7736
  const at = (q) => {
7718
- const x = (repeat ? (q % 1 + 1) % 1 : clamp3(q, 0, 1)) * (list.length - 1);
7737
+ const x = (repeat ? (q % 1 + 1) % 1 : clamp6(q, 0, 1)) * (list.length - 1);
7719
7738
  const i = Math.min(Math.floor(x), list.length - 2);
7720
7739
  return mixRgb2(list[i], list[i + 1], x - i);
7721
7740
  };
@@ -7833,8 +7852,8 @@ var TXA = (function() {
7833
7852
  function rangeAt2(an, val) {
7834
7853
  const r = { ...an.range };
7835
7854
  const driven = r.drive === "time" || r.drive === "voice";
7836
- for (const [k, f] of Object.entries(RANGE_FIELDS2)) {
7837
- if (f.kf && !(driven && RANGE_ONLY2[k])) r[k] = clamp3(val(an.aid, k, r[k]), f.min, f.max);
7855
+ for (const [k, f2] of Object.entries(RANGE_FIELDS2)) {
7856
+ if (f2.kf && !(driven && RANGE_ONLY2[k])) r[k] = clamp6(val(an.aid, k, r[k]), f2.min, f2.max);
7838
7857
  }
7839
7858
  return r;
7840
7859
  }
@@ -7857,7 +7876,7 @@ var TXA = (function() {
7857
7876
  let w = null;
7858
7877
  if (an.wiggle && an.wiggle.enabled) {
7859
7878
  w = { ...an.wiggle };
7860
- for (const [k, f] of Object.entries(WIGGLE_FIELDS2)) if (f.kf) w[k] = clamp3(val(an.aid, `wig.${k}`, w[k]), f.min, f.max);
7879
+ for (const [k, f2] of Object.entries(WIGGLE_FIELDS2)) if (f2.kf) w[k] = clamp6(val(an.aid, `wig.${k}`, w[k]), f2.min, f2.max);
7861
7880
  }
7862
7881
  const N = counts[r.basedOn] || 0;
7863
7882
  const level = levelOf2(r);
@@ -7872,9 +7891,9 @@ var TXA = (function() {
7872
7891
  for (const fx of effects) {
7873
7892
  const def = EFFECT_BY_KEY2[fx.type];
7874
7893
  const v = {};
7875
- for (const [k, f] of Object.entries(def.fields)) {
7876
- const stored = fx[k] === void 0 ? f.default : fx[k];
7877
- v[k] = f.type === "num" && f.kf ? clamp3(val(an.aid, `${fx.pid}.${k}`, stored), f.min, f.max) : stored;
7894
+ for (const [k, f2] of Object.entries(def.fields)) {
7895
+ const stored = fx[k] === void 0 ? f2.default : fx[k];
7896
+ v[k] = f2.type === "num" && f2.kf ? clamp6(val(an.aid, `${fx.pid}.${k}`, stored), f2.min, f2.max) : stored;
7878
7897
  }
7879
7898
  if (fx.type === "box") {
7880
7899
  const groups = v.shape === "unit" ? Int32Array.from(units, (u) => {
@@ -7900,7 +7919,7 @@ var TXA = (function() {
7900
7919
  }
7901
7920
  }
7902
7921
  for (const st of states) {
7903
- st.alpha = clamp3(st.alpha, 0, 1);
7922
+ st.alpha = clamp6(st.alpha, 0, 1);
7904
7923
  st.blur = Math.max(0, st.blur);
7905
7924
  }
7906
7925
  for (const b2 of boxes) if (b2.v.withText && b2.v.shape === "unit") b2.amt = Float32Array.from(b2.amt, (a, i) => a * (states[i] ? states[i].alpha : 1));
@@ -7912,7 +7931,7 @@ var TXA = (function() {
7912
7931
  return xf[level] || (xf[level] = { tx: 0, ty: 0, s: 1, sx: 1, sy: 1, rot: 0, rx: 0, ry: 0, skew: 0, axis: 0, persp: 0 });
7913
7932
  }
7914
7933
  function applyEffect(st, type, v, k, i, time, g, fillRgb, level = "char") {
7915
- const kp = clamp3(k, 0, 1);
7934
+ const kp = clamp6(k, 0, 1);
7916
7935
  switch (type) {
7917
7936
  case "position": {
7918
7937
  const e2 = xfOf(st, level);
@@ -8058,7 +8077,7 @@ var TXA = (function() {
8058
8077
  const sy = e2.s * e2.sy;
8059
8078
  let k00 = 1, k01 = 0, k10 = 0, k11 = 1;
8060
8079
  if (e2.skew) {
8061
- const t = Math.tan(clamp3(e2.skew, -85, 85) * Math.PI / 180);
8080
+ const t = Math.tan(clamp6(e2.skew, -85, 85) * Math.PI / 180);
8062
8081
  const ca = Math.cos(e2.axis * Math.PI / 180);
8063
8082
  const sa = Math.sin(e2.axis * Math.PI / 180);
8064
8083
  k00 = 1 + t * ca * sa;
@@ -8117,6 +8136,77 @@ var TXA = (function() {
8117
8136
  }
8118
8137
  return xf || st.xf;
8119
8138
  }
8139
+ function m4Mul2(A, B) {
8140
+ const C = new Array(16);
8141
+ for (let r = 0; r < 4; r += 1) {
8142
+ for (let c = 0; c < 4; c += 1) {
8143
+ C[r * 4 + c] = A[r * 4] * B[c] + A[r * 4 + 1] * B[4 + c] + A[r * 4 + 2] * B[8 + c] + A[r * 4 + 3] * B[12 + c];
8144
+ }
8145
+ }
8146
+ return C;
8147
+ }
8148
+ function levelM4(e2, box2, pivot) {
8149
+ const cx = (box2.x0 + box2.x1) / 2;
8150
+ const cy = (box2.y0 + box2.y1) / 2;
8151
+ const px = cx + pivot.x * (box2.x1 - box2.x0) / 2;
8152
+ const py = cy + pivot.y * (box2.y1 - box2.y0) / 2;
8153
+ const sx = e2.s * e2.sx;
8154
+ const sy = e2.s * e2.sy;
8155
+ let k00 = 1, k01 = 0, k10 = 0, k11 = 1;
8156
+ if (e2.skew) {
8157
+ const t = Math.tan(clamp6(e2.skew, -85, 85) * Math.PI / 180);
8158
+ const ca = Math.cos(e2.axis * Math.PI / 180);
8159
+ const sa = Math.sin(e2.axis * Math.PI / 180);
8160
+ k00 = 1 + t * ca * sa;
8161
+ k01 = -t * ca * ca;
8162
+ k10 = t * sa * sa;
8163
+ k11 = 1 - t * sa * ca;
8164
+ }
8165
+ const a00 = k00 * sx, a01 = k01 * sy, a10 = k10 * sx, a11 = k11 * sy;
8166
+ const az = e2.s;
8167
+ const th = e2.rx * Math.PI / 180;
8168
+ const ph = e2.ry * Math.PI / 180;
8169
+ const ct = Math.cos(th), st = Math.sin(th), cp = Math.cos(ph), sp = Math.sin(ph);
8170
+ const R = [cp, sp * st, -sp * ct, 0, ct, st, sp, -cp * st, cp * ct];
8171
+ const rad = e2.rot * Math.PI / 180;
8172
+ const cr = Math.cos(rad), sr = Math.sin(rad);
8173
+ const RA = [
8174
+ R[0] * a00 + R[1] * a10,
8175
+ R[0] * a01 + R[1] * a11,
8176
+ R[2] * az,
8177
+ R[3] * a00 + R[4] * a10,
8178
+ R[3] * a01 + R[4] * a11,
8179
+ R[5] * az,
8180
+ R[6] * a00 + R[7] * a10,
8181
+ R[6] * a01 + R[7] * a11,
8182
+ R[8] * az
8183
+ ];
8184
+ const L = [
8185
+ cr * RA[0] - sr * RA[3],
8186
+ cr * RA[1] - sr * RA[4],
8187
+ cr * RA[2] - sr * RA[5],
8188
+ sr * RA[0] + cr * RA[3],
8189
+ sr * RA[1] + cr * RA[4],
8190
+ sr * RA[2] + cr * RA[5],
8191
+ RA[6],
8192
+ RA[7],
8193
+ RA[8]
8194
+ ];
8195
+ const tx = px + e2.tx - (L[0] * px + L[1] * py);
8196
+ const ty = py + e2.ty - (L[3] * px + L[4] * py);
8197
+ const tz = -(L[6] * px + L[7] * py);
8198
+ return [L[0], L[1], L[2], tx, L[3], L[4], L[5], ty, L[6], L[7], L[8], tz, 0, 0, 0, 1];
8199
+ }
8200
+ function unitTransform3d2(xf, boxOf, pivot) {
8201
+ let M = null;
8202
+ for (const lv of LEVELS2) {
8203
+ const e2 = xf && xf[lv];
8204
+ if (!e2) continue;
8205
+ const r = levelM4(e2, boxOf(lv), pivot);
8206
+ M = M ? m4Mul2(r, M) : r;
8207
+ }
8208
+ return M;
8209
+ }
8120
8210
  function unitTransform2(xf, boxOf, pivot) {
8121
8211
  let H = null;
8122
8212
  let projective = false;
@@ -8256,12 +8346,12 @@ var TXA = (function() {
8256
8346
  const gv = Math.max(Math.abs(w01 - w00), Math.abs(w11 - w10)) / wMin / Lv;
8257
8347
  const K = 2 * (gu + gv) + R * (gu * gu + gv * gv);
8258
8348
  const cell = K > 1e-12 ? Math.sqrt(4 * WARP_TOL / K) : Infinity;
8259
- let nu = clamp3(Math.ceil(Lu / cell), 1, 64);
8260
- let nv = clamp3(Math.ceil(Lv / cell), 1, 64);
8349
+ let nu = clamp6(Math.ceil(Lu / cell), 1, 64);
8350
+ let nv = clamp6(Math.ceil(Lv / cell), 1, 64);
8261
8351
  if (nu * nv > WARP_CELLS) {
8262
- const f = Math.sqrt(WARP_CELLS / (nu * nv));
8263
- nu = Math.max(1, Math.round(nu * f));
8264
- nv = Math.max(1, Math.round(nv * f));
8352
+ const f2 = Math.sqrt(WARP_CELLS / (nu * nv));
8353
+ nu = Math.max(1, Math.round(nu * f2));
8354
+ nv = Math.max(1, Math.round(nv * f2));
8265
8355
  }
8266
8356
  const src = [];
8267
8357
  const dst = [];
@@ -8329,7 +8419,7 @@ var TXA = (function() {
8329
8419
  }
8330
8420
  function revealedOf2(amt) {
8331
8421
  const out = new Float32Array(amt.length);
8332
- for (let i = 0; i < amt.length; i += 1) out[i] = Number.isNaN(amt[i]) ? NaN : 1 - clamp3(amt[i], 0, 1);
8422
+ for (let i = 0; i < amt.length; i += 1) out[i] = Number.isNaN(amt[i]) ? NaN : 1 - clamp6(amt[i], 0, 1);
8333
8423
  return out;
8334
8424
  }
8335
8425
  const BOX_MIN = 1e-4;
@@ -8356,10 +8446,10 @@ var TXA = (function() {
8356
8446
  peak2 = Math.max(peak2, amt[j]);
8357
8447
  }
8358
8448
  }
8359
- const f = cells[k], l = cells[last];
8360
- const x02 = f.x - v.padX, x12 = l.x + l.ink + v.padX, y02 = f.top - v.padY, y12 = f.bottom + v.padY;
8449
+ const f2 = cells[k], l = cells[last];
8450
+ const x02 = f2.x - v.padX, x12 = l.x + l.ink + v.padX, y02 = f2.top - v.padY, y12 = f2.bottom + v.padY;
8361
8451
  if (x12 - x02 > 0.5 && y12 - y02 > 0.5) {
8362
- out.push({ x0: x02, y0: y02, x1: x12, y1: y12, r: Math.max(0, Math.min(v.radius, (y12 - y02) / 2, (x12 - x02) / 2)), alpha: opacity2 * clamp3(peak2, 0, 1) });
8452
+ out.push({ x0: x02, y0: y02, x1: x12, y1: y12, r: Math.max(0, Math.min(v.radius, (y12 - y02) / 2, (x12 - x02) / 2)), alpha: opacity2 * clamp6(peak2, 0, 1) });
8363
8453
  }
8364
8454
  k = j + 1;
8365
8455
  }
@@ -8381,10 +8471,10 @@ var TXA = (function() {
8381
8471
  j += 1;
8382
8472
  }
8383
8473
  const last = j - 1;
8384
- const f = cells[i];
8474
+ const f2 = cells[i];
8385
8475
  const l = cells[last];
8386
- const share = (k) => v.grow ? clamp3(amt[k] / peak2, 0, 1) : 1;
8387
- let x02 = f.x;
8476
+ const share = (k) => v.grow ? clamp6(amt[k] / peak2, 0, 1) : 1;
8477
+ let x02 = f2.x;
8388
8478
  let x12 = l.x + l.ink;
8389
8479
  if (v.grow && i < last) {
8390
8480
  const sl = share(last);
@@ -8395,7 +8485,7 @@ var TXA = (function() {
8395
8485
  for (let k = i; k < last && share(k) < 1; k += 1) x02 += (1 - share(k)) * adv(k);
8396
8486
  if (x12 < x02) x12 = x02;
8397
8487
  }
8398
- runs.push({ row: f.row, x0: x02, x1: x12, top: f.top, bottom: f.bottom, peak: peak2, covered: x12 - x02 });
8488
+ runs.push({ row: f2.row, x0: x02, x1: x12, top: f2.top, bottom: f2.bottom, peak: peak2, covered: x12 - x02 });
8399
8489
  i = j;
8400
8490
  }
8401
8491
  if (!runs.length) return [];
@@ -8406,7 +8496,7 @@ var TXA = (function() {
8406
8496
  y02 -= v.padY;
8407
8497
  y12 += v.padY;
8408
8498
  if (!(x12 - x02 > 0.5 && y12 - y02 > 0.5) || !(a > 0)) return null;
8409
- return { x0: x02, y0: y02, x1: x12, y1: y12, r: Math.max(0, Math.min(v.radius, (y12 - y02) / 2, (x12 - x02) / 2)), alpha: opacity * clamp3(a, 0, 1) };
8499
+ return { x0: x02, y0: y02, x1: x12, y1: y12, r: Math.max(0, Math.min(v.radius, (y12 - y02) / 2, (x12 - x02) / 2)), alpha: opacity * clamp6(a, 0, 1) };
8410
8500
  };
8411
8501
  if (v.shape !== "block") return runs.map((q) => rect(q.x0, q.top, q.x1, q.bottom, q.peak)).filter(Boolean);
8412
8502
  const rows = /* @__PURE__ */ new Map();
@@ -8436,7 +8526,7 @@ var TXA = (function() {
8436
8526
  for (const r of list) {
8437
8527
  const sp = span.get(r.row);
8438
8528
  const width = sp && sp.b > sp.a ? sp.b - sp.a : 0;
8439
- const grown = v.grow ? clamp3(r.covered / Math.max(1e-6, Math.min(width, r.bottom - r.top)), 0, 1) : 1;
8529
+ const grown = v.grow ? clamp6(r.covered / Math.max(1e-6, Math.min(width, r.bottom - r.top)), 0, 1) : 1;
8440
8530
  const s = grown * grown * (3 - 2 * grown);
8441
8531
  r.h = s * (peak > 0 ? r.peak / peak : 0);
8442
8532
  }
@@ -8462,10 +8552,10 @@ var TXA = (function() {
8462
8552
  const { v, amt } = c;
8463
8553
  const n2 = Math.min(amt.length, cells.length);
8464
8554
  if (!n2 || time >= c.outStart) return null;
8465
- let f = 0;
8466
- for (let k = 0; k < n2; k += 1) f += Number.isNaN(amt[k]) ? 0 : clamp3(amt[k], 0, 1);
8467
- const done = f >= n2 - 1e-3;
8468
- const typing = f > 1e-3 && !done;
8555
+ let f2 = 0;
8556
+ for (let k = 0; k < n2; k += 1) f2 += Number.isNaN(amt[k]) ? 0 : clamp6(amt[k], 0, 1);
8557
+ const done = f2 >= n2 - 1e-3;
8558
+ const typing = f2 > 1e-3 && !done;
8469
8559
  const end = Number.isFinite(c.inEnd) ? c.inEnd : null;
8470
8560
  if (v.hide && done && (end === null || time >= end + v.hideAfter)) return null;
8471
8561
  if (!typing && v.blink > 0) {
@@ -8479,10 +8569,10 @@ var TXA = (function() {
8479
8569
  cell = cells[k];
8480
8570
  x = cell.x + cell.ink;
8481
8571
  } else {
8482
- const k = Math.min(n2 - 1, Math.floor(f));
8572
+ const k = Math.min(n2 - 1, Math.floor(f2));
8483
8573
  cell = cells[k];
8484
8574
  const adv = k + 1 < n2 && cells[k + 1].row === cell.row ? Math.max(0, cells[k + 1].x - cell.x) : cell.w;
8485
- x = cell.x + (f - k) * adv;
8575
+ x = cell.x + (f2 - k) * adv;
8486
8576
  }
8487
8577
  const fs = Math.max(1, cell.bottom - cell.top);
8488
8578
  const y = cell.top;
@@ -8558,6 +8648,8 @@ var TXA = (function() {
8558
8648
  groupBoxes: groupBoxes2,
8559
8649
  maskedXf: maskedXf2,
8560
8650
  unitTransform: unitTransform2,
8651
+ unitTransform3d: unitTransform3d2,
8652
+ m4Mul: m4Mul2,
8561
8653
  applyH: applyH2,
8562
8654
  hToCanvas: hToCanvas2,
8563
8655
  warpBegin: warpBegin2,
@@ -8629,6 +8721,8 @@ var {
8629
8721
  groupBoxes,
8630
8722
  maskedXf,
8631
8723
  unitTransform,
8724
+ unitTransform3d,
8725
+ m4Mul,
8632
8726
  applyH,
8633
8727
  hToCanvas,
8634
8728
  warpBegin,
@@ -8753,7 +8847,7 @@ var factory = function(T) {
8753
8847
  }
8754
8848
  ]
8755
8849
  });
8756
- const PRESETS2 = [
8850
+ const PRESETS3 = [
8757
8851
  // ── Sweep Reveal ──
8758
8852
  GLIDE_TEXT,
8759
8853
  glideThrough(
@@ -8762,6 +8856,63 @@ var factory = function(T) {
8762
8856
  "Smooth Glide",
8763
8857
  "Letters type in under a chasing blue band as the word keeps gliding through the whole layer, slow in the middle, then slides off to the left"
8764
8858
  ),
8859
+ // Smart Overshoot = the owner's "Glow Select — text preset" project (board Vibe Motion,
8860
+ // 2026-10-27), third in the list on request. Built on a 54 px layer, so its lengths are
8861
+ // px / 54 em, to 4 places (exact on 54 px, and the skill's × size / 100 rule agrees on
8862
+ // any size). Words rise out of a blur and overshoot into place, glowing hot green and
8863
+ // cooling to white behind the reveal, while the whole line slides back to centre; a soft
8864
+ // glow stays and one key word stays lit green. The project picked by index (word 3); here
8865
+ // the Range is in %, so other text gets the same share: words 60–85 % is exactly that
8866
+ // word on the 5-word sample and still finds a word near the end of a 2- to 8-word line.
8867
+ // The selection box that grew over the end is gone (owner, 2026-10-29): with no keyframe
8868
+ // left, the plate stops repainting once the last word lands.
8869
+ {
8870
+ id: "smart-overshoot",
8871
+ name: "Smart Overshoot",
8872
+ cat: "sweep",
8873
+ sides: "in",
8874
+ anchor: "start",
8875
+ sample: "Just the best stablecoin strategies",
8876
+ tile: "Smart overshoot",
8877
+ labLayer: { size: 54, weight: 400, stage: "#0c0e0d" },
8878
+ description: "Words spring in out of a hot green glow and overshoot into place as the line recentres, and a key word stays lit",
8879
+ animators: [
8880
+ {
8881
+ name: "Recenter",
8882
+ range: kIn("words", "all", { stagger: 0, duration: 1.8, curve: "Power3 Out" }),
8883
+ effects: [["position", { x: em(4.6296), y: 0 }]]
8884
+ },
8885
+ {
8886
+ name: "Spring in",
8887
+ range: kIn("words", "word", { stagger: 0.15, duration: 1.1, curve: "Overshoot" }),
8888
+ effects: [["position", { x: em(1.2963), y: em(1.5741) }], ["scale", { scale: 78 }]]
8889
+ },
8890
+ {
8891
+ name: "Focus in",
8892
+ range: kIn("words", "auto", { stagger: 0.15, duration: 0.38, curve: "Power2 Out" }),
8893
+ effects: [["opacity", { opacity: 0 }], ["blur", { amount: em(0.2593) }]]
8894
+ },
8895
+ {
8896
+ name: "Hot glow",
8897
+ range: kIn("words", "auto", { stagger: 0.15, duration: 0.65, curve: "Sine Smooth" }),
8898
+ effects: [["fill", { color: "#6dff86" }], ["glow", { color: "#3dff6a", size: em(0.6296), strength: 230 }]]
8899
+ },
8900
+ {
8901
+ name: "Soft glow",
8902
+ range: { basedOn: "chars" },
8903
+ effects: [["glow", { color: "#b8ffc6", size: em(0.2222), strength: 30 }]]
8904
+ },
8905
+ {
8906
+ name: "Key word",
8907
+ range: { basedOn: "words", start: 60, end: 85, smooth: 0 },
8908
+ effects: [
8909
+ ["fill", { color: "#6dff86" }],
8910
+ ["stroke", { color: "#6dff86", width: em(0.0241) }],
8911
+ ["glow", { color: "#3dff6a", size: em(0.2963), strength: 60 }]
8912
+ ]
8913
+ }
8914
+ ]
8915
+ },
8765
8916
  glideThrough(
8766
8917
  "glide-soft-stop",
8767
8918
  "Glide Soft Stop",
@@ -10061,7 +10212,7 @@ var factory = function(T) {
10061
10212
  }]
10062
10213
  }
10063
10214
  ];
10064
- for (const p of PRESETS2) for (const a of p.animators) a.range = { basedOn: "charsNoSpace", ...a.range };
10215
+ for (const p of PRESETS3) for (const a of p.animators) a.range = { basedOn: "charsNoSpace", ...a.range };
10065
10216
  const anchorOf2 = (p, a) => a && a.anchor || p.anchor;
10066
10217
  function presetSpan2(p) {
10067
10218
  if (p.anchor !== "start" && p.anchor !== "end") return 0;
@@ -10169,7 +10320,7 @@ var factory = function(T) {
10169
10320
  }]
10170
10321
  };
10171
10322
  }
10172
- const api = { CATEGORIES: CATEGORIES2, PRESETS: PRESETS2, anchorOf: anchorOf2, presetSpan: presetSpan2, isTimed: isTimed2, resolveEm: resolveEm2, build: build2, fakeVoice: fakeVoice2, usesVoice: usesVoice2, hasExit: hasExit2, withExit: withExit2 };
10323
+ const api = { CATEGORIES: CATEGORIES2, PRESETS: PRESETS3, anchorOf: anchorOf2, presetSpan: presetSpan2, isTimed: isTimed2, resolveEm: resolveEm2, build: build2, fakeVoice: fakeVoice2, usesVoice: usesVoice2, hasExit: hasExit2, withExit: withExit2 };
10173
10324
  return api;
10174
10325
  };
10175
10326
  var PRESETS_API = factory(textAnimators_default);
@@ -12439,6 +12590,40 @@ init_define_import_meta_env();
12439
12590
 
12440
12591
  // src/components/videoEditor/types.js
12441
12592
  init_define_import_meta_env();
12593
+ var CONTAINER_MEDIA_TYPES = /* @__PURE__ */ new Set(["sequence", "grid"]);
12594
+ var isContainerClip = (clip) => !!clip && CONTAINER_MEDIA_TYPES.has(clip.mediaType) && !!clip.sequence;
12595
+ var TransitionType = {
12596
+ NONE: "None",
12597
+ FADE: "Cross Fade",
12598
+ // One type per move, the way is `transitionOptions.direction` (see
12599
+ // DIRECTIONAL_TRANSITIONS). Push absorbed Slide Left/Right and Push Up/Down —
12600
+ // the same move (both clips travel together) on two axes.
12601
+ PUSH: "Push",
12602
+ PARALLAX: "Parallax",
12603
+ WIPE: "Wipe",
12604
+ ZOOM_IN: "Zoom In",
12605
+ ZOOM_OUT: "Zoom Out",
12606
+ // 'Zoom Blur' was retired in 9.0.6 — 'Advanced Zoom' does the same thing with a
12607
+ // pivot you can choose, so it was removed rather than kept as a duplicate.
12608
+ // Renamed from 'Zoom to Point'/'Move to Point' when they grew the fisheye and
12609
+ // wiggle groups: they are no longer only about aiming at a point. Every project
12610
+ // saved before that still holds the old string — see TRANSITION_TYPE_ALIASES.
12611
+ ZOOM_POINT: "Advanced Zoom",
12612
+ MOVE_POINT: "Advanced Movement",
12613
+ CAROUSEL_3D: "3D Carousel",
12614
+ LOUVER_FLIP: "Louver Flip",
12615
+ CIRCULAR_WIPE: "Circular Wipe",
12616
+ PIXEL_BLOCKS: "Pixel Blocks",
12617
+ CAMERA_WHIP: "Camera Whip",
12618
+ ZOOM_RULER: "Zoom Ruler",
12619
+ GLOW_STACK: "Gradient Glow Stack",
12620
+ GRADIENT_MAP: "Gradient Map",
12621
+ SCALE_SWAP: "Scale Swap",
12622
+ SIDE_BY_SIDE: "Side by Side",
12623
+ DIP_TO_BLACK: "Dip to Black",
12624
+ RADIAL_WIPE: "Radial Wipe",
12625
+ CUSTOM: "Custom"
12626
+ };
12442
12627
  var TRANSITION_TYPE_ALIASES = Object.freeze({
12443
12628
  "Zoom to Point": "Advanced Zoom",
12444
12629
  "Move to Point": "Advanced Movement",
@@ -12611,7 +12796,11 @@ var TRANSITION_EASING_OPTIONS = Object.values(TransitionEasing).filter((e2) => !
12611
12796
 
12612
12797
  // src/components/videoEditor/gsapTransitions.js
12613
12798
  init_define_import_meta_env();
12614
- var THREE = __toESM(require_three(), 1);
12799
+
12800
+ // stand-in:three
12801
+ init_define_import_meta_env();
12802
+ function f() {
12803
+ }
12615
12804
 
12616
12805
  // src/components/videoEditor/keyframeProps.js
12617
12806
  init_define_import_meta_env();
@@ -12880,8 +13069,8 @@ var KF_PROPS = [
12880
13069
  { label: "motionPathOffsetZ", displayLabel: "Path Offset Z", group: "Transform", placeholder: "0", unit: "px", type: "number", min: -1e4, max: 1e4, step: 5 },
12881
13070
  { label: "letterSpacing", group: "Transform", placeholder: "0", unit: "px", type: "number", min: -80, max: 200, step: 1 },
12882
13071
  // Text on Path (text layers with textPathEnabled). Progress reuses motionPathT
12883
- // above; these animate the glyph distribution itself — keyed trims give a
12884
- // write-on/write-off along the curve. No-ops elsewhere, like letterSpacing.
13072
+ // above; these animate the glyph distribution itself — keyed trims move the
13073
+ // window the run is laid out in (letters are not hidden past it). No-ops elsewhere, like letterSpacing.
12885
13074
  { label: "textPathSpread", displayLabel: "Text Path Spread", group: "Transform", placeholder: "100", unit: "%", type: "number", min: 10, max: 400, step: 1 },
12886
13075
  { label: "textPathSpacing", displayLabel: "Text Path Spacing", group: "Transform", placeholder: "0", unit: "px", type: "number", min: -200, max: 500, step: 1 },
12887
13076
  { label: "textPathTrimStart", displayLabel: "Text Path Trim Start", group: "Transform", placeholder: "0", unit: "%", type: "number", min: 0, max: 100, step: 0.5 },
@@ -12999,6 +13188,9 @@ var KF_PROPS = [
12999
13188
  { label: "frWindow", displayLabel: "Rim Depth Window", group: "Effects", placeholder: "14", unit: "%", type: "number", min: 1, max: 60, step: 0.5 },
13000
13189
  { label: "frBody", displayLabel: "Rim Body Light", group: "Effects", placeholder: "100", unit: "%", type: "number", min: 0, max: 100, step: 1 },
13001
13190
  { label: "frAmount", displayLabel: "Rim Amount", group: "Effects", placeholder: "100", unit: "%", type: "number", min: 0, max: 100, step: 1 },
13191
+ // Posterize Time (per-layer clock — posterizeTime.js). The rate is integrated,
13192
+ // so a keyed ramp speeds the steps up smoothly.
13193
+ { label: "ptFps", displayLabel: "Posterize Frame Rate", group: "Effects", placeholder: "12", unit: "fps", type: "number", min: 1, max: 60, step: 1 },
13002
13194
  // Motion Blur (per-layer, transform-velocity driven — motionBlurLayer.js).
13003
13195
  { label: "mblurShutter", group: "Effects", placeholder: "180", unit: "deg", type: "number", min: 0, max: 720, step: 5 },
13004
13196
  { label: "mblurAmount", group: "Effects", placeholder: "100", unit: "%", type: "number", min: 0, max: 100, step: 1 },
@@ -13256,6 +13448,15 @@ var KF_PROPS = [
13256
13448
  { label: "bwL2Y", displayLabel: "Bezier Left 2 Y", group: "Effects", placeholder: "33", unit: "%", type: "number", min: -100, max: 200, step: 0.5 }
13257
13449
  ];
13258
13450
  var KF_PROP_LABELS = KF_PROPS.map((p) => p.label);
13451
+ var KF_COLOR_PROPS = /* @__PURE__ */ new Set(["color", "backgroundColor", "textColor", "textBgColor", "shapeFill", "textOutlineColor", "borderColor"]);
13452
+ var MOTION_KF_COLOR_PREFIX = "mpc:";
13453
+ var MASK_PATH_KF_RE = /^msk:[A-Za-z0-9_-]{1,24}:path$/;
13454
+ function isMaskPathTrackKey(prop) {
13455
+ return typeof prop === "string" && MASK_PATH_KF_RE.test(prop);
13456
+ }
13457
+ function isColorTrackKey(prop) {
13458
+ return KF_COLOR_PROPS.has(prop) || STROKE_COLOR_KF_LABELS.has(prop) || typeof prop === "string" && prop.startsWith(MOTION_KF_COLOR_PREFIX);
13459
+ }
13259
13460
 
13260
13461
  // src/utils/cssGradient.js
13261
13462
  init_define_import_meta_env();
@@ -13356,10 +13557,138 @@ function buildGradient(type = "linear", angle = 90, stops = []) {
13356
13557
 
13357
13558
  // src/components/videoEditor/shapePath.js
13358
13559
  init_define_import_meta_env();
13560
+
13561
+ // src/components/videoEditor/objectMotionPath.js
13562
+ init_define_import_meta_env();
13563
+ var OBJECT_MOTION_PATH_AIMS = ["tangent", "points", "free"];
13564
+ var num = (value, fallback = 0) => {
13565
+ const parsed = Number.parseFloat(value);
13566
+ return Number.isFinite(parsed) ? parsed : fallback;
13567
+ };
13568
+ var vec3 = (value) => {
13569
+ if (!value || typeof value !== "object") return null;
13570
+ if (![value.x, value.y, value.z].every((v) => Number.isFinite(Number(v)))) return null;
13571
+ return { x: num(value.x), y: num(value.y), z: num(value.z) };
13572
+ };
13573
+ function sanitizeObjectMotionPath(path) {
13574
+ if (!path || typeof path !== "object") return null;
13575
+ const points = [];
13576
+ for (const raw of Array.isArray(path.points) ? path.points : []) {
13577
+ const p = vec3(raw);
13578
+ if (!p) continue;
13579
+ const point = p;
13580
+ const hIn = vec3(raw.hIn);
13581
+ const hOut = vec3(raw.hOut);
13582
+ if (hIn) point.hIn = hIn;
13583
+ if (hOut) point.hOut = hOut;
13584
+ if (raw.manual === true) point.manual = true;
13585
+ const rx = num(raw.rx);
13586
+ const ry = num(raw.ry);
13587
+ const rz = num(raw.rz);
13588
+ if (rx || ry || rz) {
13589
+ point.rx = rx;
13590
+ point.ry = ry;
13591
+ point.rz = rz;
13592
+ }
13593
+ points.push(point);
13594
+ }
13595
+ if (!points.length) return null;
13596
+ return {
13597
+ points,
13598
+ closed: path.closed === true,
13599
+ follow: path.follow === true,
13600
+ aim: OBJECT_MOTION_PATH_AIMS.includes(path.aim) ? path.aim : "tangent",
13601
+ keyframesPerPoint: path.keyframesPerPoint === true,
13602
+ ...path.keyframesPerPoint === true ? {
13603
+ keyframesPerPointCount: Math.max(0, Math.round(num(path.keyframesPerPointCount))),
13604
+ keyframesPerPointClosed: path.keyframesPerPointClosed === true
13605
+ } : {}
13606
+ };
13607
+ }
13608
+ var DEG2RAD = Math.PI / 180;
13609
+ var MOTION_PATH_KF_KEY = "motionPathPoints";
13610
+ var isMotionPathTrackKey = (prop) => prop === MOTION_PATH_KF_KEY;
13611
+ var decodedMotionPaths = /* @__PURE__ */ new Map();
13612
+ var DECODED_MOTION_PATH_LIMIT = 4096;
13613
+ function decodeMotionPathValue(value) {
13614
+ if (Array.isArray(value)) return sanitizeObjectMotionPath({ points: value })?.points || null;
13615
+ if (typeof value !== "string" || !value.trim()) return null;
13616
+ const cached = decodedMotionPaths.get(value);
13617
+ if (cached !== void 0) return cached;
13618
+ let points = null;
13619
+ try {
13620
+ points = sanitizeObjectMotionPath({ points: JSON.parse(value) })?.points || null;
13621
+ } catch {
13622
+ points = null;
13623
+ }
13624
+ if (decodedMotionPaths.size >= DECODED_MOTION_PATH_LIMIT) decodedMotionPaths.clear();
13625
+ decodedMotionPaths.set(value, points);
13626
+ return points;
13627
+ }
13628
+ function interpolateMotionPaths(a, b2, t) {
13629
+ if (!Array.isArray(a) || !a.length) return Array.isArray(b2) ? b2 : [];
13630
+ if (!Array.isArray(b2) || !b2.length) return a;
13631
+ if (!Number.isFinite(t) || t <= 0) return a;
13632
+ if (t >= 1) return b2;
13633
+ const lerp = (p, q) => p + (q - p) * t;
13634
+ const lerpVec = (p, q) => ({
13635
+ x: lerp(num(p?.x), num(q?.x)),
13636
+ y: lerp(num(p?.y), num(q?.y)),
13637
+ z: lerp(num(p?.z), num(q?.z))
13638
+ });
13639
+ const n2 = Math.min(a.length, b2.length);
13640
+ const out = new Array(n2);
13641
+ for (let i = 0; i < n2; i += 1) {
13642
+ const p = a[i];
13643
+ const q = b2[i];
13644
+ const point = {
13645
+ x: lerp(p.x, q.x),
13646
+ y: lerp(p.y, q.y),
13647
+ z: lerp(p.z, q.z),
13648
+ hIn: lerpVec(p.hIn, q.hIn),
13649
+ hOut: lerpVec(p.hOut, q.hOut),
13650
+ manual: true
13651
+ };
13652
+ if (p.rx || p.ry || p.rz || q.rx || q.ry || q.rz) {
13653
+ point.rx = lerp(num(p.rx), num(q.rx));
13654
+ point.ry = lerp(num(p.ry), num(q.ry));
13655
+ point.rz = lerp(num(p.rz), num(q.rz));
13656
+ }
13657
+ out[i] = point;
13658
+ }
13659
+ return out;
13660
+ }
13661
+
13662
+ // src/components/videoEditor/shapePath.js
13663
+ var num2 = (value, fallback = 0) => {
13664
+ const parsed = Number.parseFloat(value);
13665
+ return Number.isFinite(parsed) ? parsed : fallback;
13666
+ };
13359
13667
  var round4 = (n2) => Math.round(n2 * 1e4) / 1e4;
13360
13668
  var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
13361
13669
  var KAPPA = 0.5522847498307936 * 0.5;
13670
+ var SHAPE_PATH_KF_KEY = "shapePath";
13362
13671
  var SHAPE_PATH_KF_GROUP = "Shape";
13672
+ var vec2 = (v) => {
13673
+ if (!v || typeof v !== "object") return null;
13674
+ return { x: num2(v.x), y: num2(v.y) };
13675
+ };
13676
+ function sanitizeShapePathPoints(points) {
13677
+ if (!Array.isArray(points)) return [];
13678
+ const clean = [];
13679
+ for (const p of points) {
13680
+ if (!p || typeof p !== "object") continue;
13681
+ if (!Number.isFinite(Number(p.x)) || !Number.isFinite(Number(p.y))) continue;
13682
+ const point = { x: clamp(num2(p.x), -8, 8), y: clamp(num2(p.y), -8, 8) };
13683
+ const hIn = vec2(p.hIn);
13684
+ const hOut = vec2(p.hOut);
13685
+ if (hIn) point.hIn = { x: clamp(hIn.x, -8, 8), y: clamp(hIn.y, -8, 8) };
13686
+ if (hOut) point.hOut = { x: clamp(hOut.x, -8, 8), y: clamp(hOut.y, -8, 8) };
13687
+ if (p.manual === true) point.manual = true;
13688
+ clean.push(point);
13689
+ }
13690
+ return clean;
13691
+ }
13363
13692
  var corner = (x, y) => ({ x, y, hIn: { x: 0, y: 0 }, hOut: { x: 0, y: 0 }, manual: true });
13364
13693
  function ellipsePoints() {
13365
13694
  return [
@@ -13561,6 +13890,50 @@ var LINE_KF_PROPS = [
13561
13890
  })
13562
13891
  ];
13563
13892
  var LINE_KF_KEYS = LINE_KF_PROPS.map((p) => p.label);
13893
+ function interpolateShapePaths(a, b2, t) {
13894
+ if (!Array.isArray(a) || !a.length) return Array.isArray(b2) ? b2 : [];
13895
+ if (!Array.isArray(b2) || !b2.length) return a;
13896
+ if (!Number.isFinite(t) || t <= 0) return a;
13897
+ if (t >= 1) return b2;
13898
+ const n2 = Math.min(a.length, b2.length);
13899
+ const lerp = (p, q) => p + (q - p) * t;
13900
+ const lerpVec = (p, q) => ({
13901
+ x: lerp(p?.x || 0, q?.x || 0),
13902
+ y: lerp(p?.y || 0, q?.y || 0)
13903
+ });
13904
+ const out = new Array(n2);
13905
+ for (let i = 0; i < n2; i += 1) {
13906
+ out[i] = {
13907
+ x: lerp(a[i].x, b2[i].x),
13908
+ y: lerp(a[i].y, b2[i].y),
13909
+ hIn: lerpVec(a[i].hIn, b2[i].hIn),
13910
+ hOut: lerpVec(a[i].hOut, b2[i].hOut),
13911
+ // The blended handles are explicit geometry — never let an auto-tangent
13912
+ // pass downstream decide it knows better.
13913
+ manual: true
13914
+ };
13915
+ }
13916
+ return out;
13917
+ }
13918
+ function decodeShapePathValue(value) {
13919
+ if (Array.isArray(value)) return sanitizeShapePathPoints(value);
13920
+ if (typeof value !== "string" || !value.trim()) return null;
13921
+ try {
13922
+ const points = sanitizeShapePathPoints(JSON.parse(value));
13923
+ return points.length ? points : null;
13924
+ } catch {
13925
+ return null;
13926
+ }
13927
+ }
13928
+ function isPathTrackKey(prop) {
13929
+ return prop === SHAPE_PATH_KF_KEY || isMaskPathTrackKey(prop) || isMotionPathTrackKey(prop);
13930
+ }
13931
+ function decodePathTrackValue(prop, value) {
13932
+ return isMotionPathTrackKey(prop) ? decodeMotionPathValue(value) : decodeShapePathValue(value);
13933
+ }
13934
+ function interpolatePathTrackValues(prop, a, b2, t) {
13935
+ return isMotionPathTrackKey(prop) ? interpolateMotionPaths(a, b2, t) : interpolateShapePaths(a, b2, t);
13936
+ }
13564
13937
 
13565
13938
  // src/components/videoEditor/motionTemplates/mtl.js
13566
13939
  init_define_import_meta_env();
@@ -14389,25 +14762,588 @@ init_define_import_meta_env();
14389
14762
 
14390
14763
  // src/components/videoEditor/crossTransitionOrder.js
14391
14764
  init_define_import_meta_env();
14765
+ var PREV_OVER_NEXT = "Previous over next";
14766
+ function movesPreviousClip(order) {
14767
+ return order === PREV_OVER_NEXT;
14768
+ }
14392
14769
 
14393
14770
  // src/components/videoEditor/speedUtils.js
14394
14771
  var SPEED_MIN = 0.1;
14395
14772
  var SPEED_MAX = 20;
14396
14773
  var AUTO_HANDLE = 1 / 3;
14774
+ var clamp2 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
14775
+ var num3 = (value, fallback = 0) => {
14776
+ const parsed = Number(value);
14777
+ return Number.isFinite(parsed) ? parsed : fallback;
14778
+ };
14397
14779
  var LOG_SLOW = -Math.log(SPEED_MIN);
14398
14780
  var LOG_FAST = Math.log(SPEED_MAX);
14781
+ var cubic = (a, b2, c, d, t) => {
14782
+ const u = 1 - t;
14783
+ return u * u * u * a + 3 * u * u * t * b2 + 3 * u * t * t * c + t * t * t * d;
14784
+ };
14785
+ var cubicDeriv = (a, b2, c, d, t) => {
14786
+ const u = 1 - t;
14787
+ return 3 * u * u * (b2 - a) + 6 * u * t * (c - b2) + 3 * t * t * (d - c);
14788
+ };
14789
+ var speedCurveCache = /* @__PURE__ */ new WeakMap();
14790
+ function resolveSpeedCurve(points) {
14791
+ if (!Array.isArray(points) || points.length === 0) return null;
14792
+ const cached = speedCurveCache.get(points);
14793
+ if (cached) return cached;
14794
+ const sorted = points.filter((p) => p && Number.isFinite(Number(p.x)) && Number.isFinite(Number(p.y))).map((p) => ({
14795
+ x: num3(p.x),
14796
+ y: clamp2(num3(p.y, 1), SPEED_MIN, SPEED_MAX),
14797
+ manual: p.manual === true,
14798
+ hIn: p.hIn ? { x: num3(p.hIn.x), y: num3(p.hIn.y) } : null,
14799
+ hOut: p.hOut ? { x: num3(p.hOut.x), y: num3(p.hOut.y) } : null
14800
+ })).sort((a, b2) => a.x - b2.x);
14801
+ if (sorted.length === 0) return null;
14802
+ const anchors = sorted.map((p, i) => {
14803
+ const prev = sorted[i - 1];
14804
+ const next = sorted[i + 1];
14805
+ const shape = (raw, neighbour) => {
14806
+ if (!neighbour) return null;
14807
+ const dx = neighbour.x - p.x;
14808
+ const auto = { x: dx * AUTO_HANDLE, y: (neighbour.y - p.y) * AUTO_HANDLE };
14809
+ const h = p.manual && raw ? raw : auto;
14810
+ return {
14811
+ // Inside the segment only: x must never run backwards past its anchor or
14812
+ // forwards past the next one, or the curve stops being a function of time.
14813
+ x: dx >= 0 ? clamp2(h.x, 0, dx) : clamp2(h.x, dx, 0),
14814
+ // Convex hull: a legal control point means a legal curve.
14815
+ y: clamp2(p.y + h.y, SPEED_MIN, SPEED_MAX) - p.y
14816
+ };
14817
+ };
14818
+ return { x: p.x, y: p.y, manual: p.manual, hIn: shape(p.hIn, prev), hOut: shape(p.hOut, next) };
14819
+ });
14820
+ const segments = [];
14821
+ for (let i = 0; i < anchors.length - 1; i += 1) {
14822
+ const a = anchors[i];
14823
+ const b2 = anchors[i + 1];
14824
+ const ah = a.hOut || { x: 0, y: 0 };
14825
+ const bh = b2.hIn || { x: 0, y: 0 };
14826
+ const seg = {
14827
+ x0: a.x,
14828
+ x1: a.x + ah.x,
14829
+ x2: b2.x + bh.x,
14830
+ x3: b2.x,
14831
+ y0: a.y,
14832
+ y1: a.y + ah.y,
14833
+ y2: b2.y + bh.y,
14834
+ y3: b2.y
14835
+ };
14836
+ seg.linear = isLinearSegment(seg);
14837
+ seg.area = segmentArea(seg, 1);
14838
+ segments.push(seg);
14839
+ }
14840
+ const curve = {
14841
+ anchors,
14842
+ segments,
14843
+ area: segments.reduce((sum, s) => sum + s.area, 0),
14844
+ first: anchors[0],
14845
+ last: anchors[anchors.length - 1]
14846
+ };
14847
+ speedCurveCache.set(points, curve);
14848
+ return curve;
14849
+ }
14850
+ function isLinearSegment(seg) {
14851
+ const dx = seg.x3 - seg.x0;
14852
+ const dy = seg.y3 - seg.y0;
14853
+ const eps = 1e-9 * Math.max(1, Math.abs(dx), Math.abs(dy));
14854
+ return Math.abs(seg.x1 - (seg.x0 + dx / 3)) <= eps && Math.abs(seg.x2 - (seg.x0 + 2 * dx / 3)) <= eps && Math.abs(seg.y1 - (seg.y0 + dy / 3)) <= eps && Math.abs(seg.y2 - (seg.y0 + 2 * dy / 3)) <= eps;
14855
+ }
14856
+ function solveSegmentT(seg, x) {
14857
+ const span = seg.x3 - seg.x0;
14858
+ if (span <= 1e-9) return 0;
14859
+ let t = clamp2((x - seg.x0) / span, 0, 1);
14860
+ if (seg.linear) return t;
14861
+ for (let i = 0; i < 8; i += 1) {
14862
+ const err = cubic(seg.x0, seg.x1, seg.x2, seg.x3, t) - x;
14863
+ if (Math.abs(err) < 1e-7) return t;
14864
+ const d = cubicDeriv(seg.x0, seg.x1, seg.x2, seg.x3, t);
14865
+ if (Math.abs(d) < 1e-7) break;
14866
+ t = clamp2(t - err / d, 0, 1);
14867
+ }
14868
+ let lo = 0;
14869
+ let hi = 1;
14870
+ for (let i = 0; i < 24; i += 1) {
14871
+ const mid = (lo + hi) * 0.5;
14872
+ if (cubic(seg.x0, seg.x1, seg.x2, seg.x3, mid) < x) lo = mid;
14873
+ else hi = mid;
14874
+ }
14875
+ return (lo + hi) * 0.5;
14876
+ }
14877
+ var GAUSS = [
14878
+ [-0.8611363115940526, 0.3478548451374538],
14879
+ [-0.3399810435848563, 0.6521451548625461],
14880
+ [0.3399810435848563, 0.6521451548625461],
14881
+ [0.8611363115940526, 0.3478548451374538]
14882
+ ];
14883
+ function segmentArea(seg, tEnd) {
14884
+ if (tEnd <= 0) return 0;
14885
+ if (seg.linear) {
14886
+ const width = (seg.x3 - seg.x0) * tEnd;
14887
+ const yAtT = seg.y0 + (seg.y3 - seg.y0) * tEnd;
14888
+ return width * (seg.y0 + yAtT) / 2;
14889
+ }
14890
+ const half = tEnd * 0.5;
14891
+ let sum = 0;
14892
+ for (const [node, weight] of GAUSS) {
14893
+ const t = half * (node + 1);
14894
+ sum += weight * cubic(seg.y0, seg.y1, seg.y2, seg.y3, t) * cubicDeriv(seg.x0, seg.x1, seg.x2, seg.x3, t);
14895
+ }
14896
+ return sum * half;
14897
+ }
14898
+ var getAverageSpeed = (points) => {
14899
+ const curve = resolveSpeedCurve(points);
14900
+ if (!curve) return 1;
14901
+ if (curve.segments.length === 0) return curve.first.y || 1;
14902
+ return curve.area || 1;
14903
+ };
14904
+ var mapTimelineToSource = (points, t) => {
14905
+ const curve = resolveSpeedCurve(points);
14906
+ if (!curve || curve.segments.length === 0) return t;
14907
+ const totalArea = curve.area;
14908
+ if (totalArea === 0) return 0;
14909
+ let currentArea = 0;
14910
+ for (const seg of curve.segments) {
14911
+ if (seg.x3 <= t) {
14912
+ currentArea += seg.area;
14913
+ } else if (seg.x0 < t) {
14914
+ currentArea += segmentArea(seg, solveSegmentT(seg, t));
14915
+ break;
14916
+ } else {
14917
+ break;
14918
+ }
14919
+ }
14920
+ return currentArea / totalArea;
14921
+ };
14922
+ var getTimelineDuration = (sourceDuration, points) => {
14923
+ const avgSpeed = getAverageSpeed(points);
14924
+ return sourceDuration / avgSpeed;
14925
+ };
14399
14926
  var UNSNAPPED_TOUCH_EPSILON = 1 / 120;
14927
+ var slideEarlierClipsLater = (layout, anchorEnds, index, amount) => {
14928
+ if (!(amount > 0)) return;
14929
+ for (let j = index; j >= 0; j--) {
14930
+ layout[j].startTime += amount;
14931
+ layout[j].endTime += amount;
14932
+ if (anchorEnds[j] != null) anchorEnds[j] += amount;
14933
+ }
14934
+ };
14935
+ var getTimelineLayout = (timeline, { unsnapped } = {}) => {
14936
+ const layout = [];
14937
+ let currentCursor = 0;
14938
+ const anchorEnds = [];
14939
+ for (let i = 0; i < timeline.length; i++) {
14940
+ const item = timeline[i];
14941
+ const sourceDuration = item.trimEnd - item.trimStart;
14942
+ const tlDuration = getTimelineDuration(sourceDuration, item.speedPoints);
14943
+ if (unsnapped && item.freeStart != null) {
14944
+ const prevAnchorEnd = i > 0 ? anchorEnds[i - 1] : null;
14945
+ const touchingPrev = prevAnchorEnd != null && Math.abs(prevAnchorEnd - item.freeStart) <= UNSNAPPED_TOUCH_EPSILON;
14946
+ let overlap = 0;
14947
+ if (touchingPrev && item.transition !== TransitionType.NONE) {
14948
+ overlap = Math.min(layout[i - 1].duration, tlDuration, item.transitionDuration);
14949
+ }
14950
+ const previousMoves = overlap > 0 && movesPreviousClip(item.transitionOrder);
14951
+ const startTime = overlap > 0 ? previousMoves ? layout[i - 1].endTime : layout[i - 1].endTime - overlap : item.freeStart;
14952
+ const endTime = startTime + tlDuration;
14953
+ layout.push({ startTime, endTime, duration: tlDuration, overlapIn: overlap, touchingPrev });
14954
+ anchorEnds.push(item.freeStart + tlDuration);
14955
+ currentCursor = endTime;
14956
+ if (previousMoves) slideEarlierClipsLater(layout, anchorEnds, i - 1, overlap);
14957
+ } else {
14958
+ let overlap = 0;
14959
+ if (i > 0 && item.transition !== TransitionType.NONE) {
14960
+ const prevLayout = layout[i - 1];
14961
+ const maxOverlap = Math.min(prevLayout.duration, tlDuration, item.transitionDuration);
14962
+ overlap = maxOverlap;
14963
+ }
14964
+ const previousMoves = overlap > 0 && movesPreviousClip(item.transitionOrder);
14965
+ const startTime = previousMoves ? currentCursor : currentCursor - overlap;
14966
+ const endTime = startTime + tlDuration;
14967
+ layout.push({ startTime, endTime, duration: tlDuration, overlapIn: overlap, touchingPrev: i > 0 });
14968
+ anchorEnds.push(endTime);
14969
+ currentCursor = endTime;
14970
+ if (previousMoves) slideEarlierClipsLater(layout, anchorEnds, i - 1, overlap);
14971
+ }
14972
+ }
14973
+ return layout;
14974
+ };
14400
14975
 
14401
14976
  // src/components/videoEditor/keyframeFx.js
14402
14977
  init_define_import_meta_env();
14403
14978
 
14404
14979
  // src/components/videoEditor/keyframeExpr.js
14405
14980
  init_define_import_meta_env();
14981
+ var ExprError = class extends Error {
14982
+ constructor(message, pos = 0) {
14983
+ super(message);
14984
+ this.name = "ExprError";
14985
+ this.pos = pos;
14986
+ }
14987
+ };
14988
+ var MAX_NODES = 600;
14989
+ var MAX_DEPTH = 40;
14990
+ var MAX_STEPS = 2e4;
14406
14991
  var MAX_EXPR_LENGTH = 4e3;
14407
- var num = (v) => {
14992
+ var PUNCT = ["===", "!==", "==", "!=", "<=", ">=", "&&", "||", "**", "(", ")", "[", "]", ",", ".", "+", "-", "*", "/", "%", "^", "<", ">", "?", ":", "!"];
14993
+ function tokenize(src) {
14994
+ const tokens = [];
14995
+ let i = 0;
14996
+ const n2 = src.length;
14997
+ while (i < n2) {
14998
+ const c = src[i];
14999
+ if (c === " " || c === " " || c === "\n" || c === "\r") {
15000
+ i += 1;
15001
+ continue;
15002
+ }
15003
+ if (c === "/" && src[i + 1] === "/") {
15004
+ while (i < n2 && src[i] !== "\n") i += 1;
15005
+ continue;
15006
+ }
15007
+ if (c === "/" && src[i + 1] === "*") {
15008
+ const end = src.indexOf("*/", i + 2);
15009
+ i = end === -1 ? n2 : end + 2;
15010
+ continue;
15011
+ }
15012
+ if (c >= "0" && c <= "9" || c === "." && src[i + 1] >= "0" && src[i + 1] <= "9") {
15013
+ const start = i;
15014
+ while (i < n2 && src[i] >= "0" && src[i] <= "9") i += 1;
15015
+ if (src[i] === ".") {
15016
+ i += 1;
15017
+ while (i < n2 && src[i] >= "0" && src[i] <= "9") i += 1;
15018
+ }
15019
+ if (src[i] === "e" || src[i] === "E") {
15020
+ let j = i + 1;
15021
+ if (src[j] === "+" || src[j] === "-") j += 1;
15022
+ if (src[j] >= "0" && src[j] <= "9") {
15023
+ i = j;
15024
+ while (i < n2 && src[i] >= "0" && src[i] <= "9") i += 1;
15025
+ }
15026
+ }
15027
+ tokens.push({ type: "num", value: Number.parseFloat(src.slice(start, i)), pos: start });
15028
+ continue;
15029
+ }
15030
+ if (c === '"' || c === "'") {
15031
+ const start = i;
15032
+ const quote = c;
15033
+ i += 1;
15034
+ let out = "";
15035
+ while (i < n2 && src[i] !== quote) {
15036
+ if (src[i] === "\\" && i + 1 < n2) {
15037
+ const esc = src[i + 1];
15038
+ out += esc === "n" ? "\n" : esc === "t" ? " " : esc;
15039
+ i += 2;
15040
+ continue;
15041
+ }
15042
+ out += src[i];
15043
+ i += 1;
15044
+ }
15045
+ if (i >= n2) throw new ExprError("Unterminated string", start);
15046
+ i += 1;
15047
+ tokens.push({ type: "str", value: out, pos: start });
15048
+ continue;
15049
+ }
15050
+ if (/[A-Za-z_$]/.test(c)) {
15051
+ const start = i;
15052
+ while (i < n2 && /[A-Za-z0-9_$]/.test(src[i])) i += 1;
15053
+ tokens.push({ type: "ident", value: src.slice(start, i), pos: start });
15054
+ continue;
15055
+ }
15056
+ const punct = PUNCT.find((p) => src.startsWith(p, i));
15057
+ if (punct) {
15058
+ tokens.push({ type: "punct", value: punct, pos: i });
15059
+ i += punct.length;
15060
+ continue;
15061
+ }
15062
+ throw new ExprError(`Unexpected character "${c}"`, i);
15063
+ }
15064
+ tokens.push({ type: "eof", value: "", pos: n2 });
15065
+ return tokens;
15066
+ }
15067
+ var BINARY_PRECEDENCE = {
15068
+ "||": 1,
15069
+ "&&": 2,
15070
+ "==": 3,
15071
+ "!=": 3,
15072
+ "===": 3,
15073
+ "!==": 3,
15074
+ "<": 4,
15075
+ ">": 4,
15076
+ "<=": 4,
15077
+ ">=": 4,
15078
+ "+": 5,
15079
+ "-": 5,
15080
+ "*": 6,
15081
+ "/": 6,
15082
+ "%": 6,
15083
+ "^": 8,
15084
+ "**": 8
15085
+ };
15086
+ var RIGHT_ASSOC = /* @__PURE__ */ new Set(["^", "**"]);
15087
+ var Parser = class {
15088
+ constructor(tokens) {
15089
+ this.tokens = tokens;
15090
+ this.i = 0;
15091
+ this.nodes = 0;
15092
+ }
15093
+ peek() {
15094
+ return this.tokens[this.i];
15095
+ }
15096
+ next() {
15097
+ return this.tokens[this.i++];
15098
+ }
15099
+ isPunct(v) {
15100
+ const t = this.peek();
15101
+ return t.type === "punct" && t.value === v;
15102
+ }
15103
+ expect(v) {
15104
+ if (!this.isPunct(v)) throw new ExprError(`Expected "${v}"`, this.peek().pos);
15105
+ return this.next();
15106
+ }
15107
+ node(obj) {
15108
+ this.nodes += 1;
15109
+ if (this.nodes > MAX_NODES) throw new ExprError("Expression is too long", 0);
15110
+ return obj;
15111
+ }
15112
+ parseExpression(depth = 0) {
15113
+ if (depth > MAX_DEPTH) throw new ExprError("Expression is nested too deeply", this.peek().pos);
15114
+ const test = this.parseBinary(0, depth + 1);
15115
+ if (this.isPunct("?")) {
15116
+ this.next();
15117
+ const then = this.parseExpression(depth + 1);
15118
+ this.expect(":");
15119
+ const other = this.parseExpression(depth + 1);
15120
+ return this.node({ kind: "cond", test, then, other });
15121
+ }
15122
+ return test;
15123
+ }
15124
+ parseBinary(minPrec, depth) {
15125
+ if (depth > MAX_DEPTH) throw new ExprError("Expression is nested too deeply", this.peek().pos);
15126
+ let left = this.parseUnary(depth + 1);
15127
+ for (; ; ) {
15128
+ const t = this.peek();
15129
+ if (t.type !== "punct") break;
15130
+ const prec = BINARY_PRECEDENCE[t.value];
15131
+ if (prec === void 0 || prec < minPrec) break;
15132
+ this.next();
15133
+ const nextMin = RIGHT_ASSOC.has(t.value) ? prec : prec + 1;
15134
+ const right = this.parseBinary(nextMin, depth + 1);
15135
+ left = this.node({ kind: "bin", op: t.value, left, right, pos: t.pos });
15136
+ }
15137
+ return left;
15138
+ }
15139
+ parseUnary(depth) {
15140
+ const t = this.peek();
15141
+ if (t.type === "punct" && (t.value === "-" || t.value === "+" || t.value === "!")) {
15142
+ this.next();
15143
+ return this.node({ kind: "un", op: t.value, arg: this.parseUnary(depth + 1), pos: t.pos });
15144
+ }
15145
+ return this.parsePostfix(depth);
15146
+ }
15147
+ parsePostfix(depth) {
15148
+ let target = this.parsePrimary(depth);
15149
+ for (; ; ) {
15150
+ if (this.isPunct(".")) {
15151
+ this.next();
15152
+ const name = this.next();
15153
+ if (name.type !== "ident") throw new ExprError('Expected a property name after "."', name.pos);
15154
+ target = this.node({ kind: "member", target, name: name.value, pos: name.pos });
15155
+ } else if (this.isPunct("[")) {
15156
+ const open = this.next();
15157
+ const index = this.parseExpression(depth + 1);
15158
+ this.expect("]");
15159
+ target = this.node({ kind: "index", target, index, pos: open.pos });
15160
+ } else if (this.isPunct("(")) {
15161
+ const open = this.next();
15162
+ const args = [];
15163
+ if (!this.isPunct(")")) {
15164
+ for (; ; ) {
15165
+ args.push(this.parseExpression(depth + 1));
15166
+ if (this.isPunct(",")) {
15167
+ this.next();
15168
+ continue;
15169
+ }
15170
+ break;
15171
+ }
15172
+ }
15173
+ this.expect(")");
15174
+ target = this.node({ kind: "call", callee: target, args, pos: open.pos });
15175
+ } else break;
15176
+ }
15177
+ return target;
15178
+ }
15179
+ parsePrimary(depth) {
15180
+ const t = this.next();
15181
+ if (t.type === "num") return this.node({ kind: "num", value: t.value });
15182
+ if (t.type === "str") return this.node({ kind: "str", value: t.value });
15183
+ if (t.type === "ident") return this.node({ kind: "ident", name: t.value, pos: t.pos });
15184
+ if (t.type === "punct" && t.value === "(") {
15185
+ const inner = this.parseExpression(depth + 1);
15186
+ this.expect(")");
15187
+ return inner;
15188
+ }
15189
+ if (t.type === "punct" && t.value === "[") {
15190
+ const items = [];
15191
+ if (!this.isPunct("]")) {
15192
+ for (; ; ) {
15193
+ items.push(this.parseExpression(depth + 1));
15194
+ if (this.isPunct(",")) {
15195
+ this.next();
15196
+ continue;
15197
+ }
15198
+ break;
15199
+ }
15200
+ }
15201
+ this.expect("]");
15202
+ return this.node({ kind: "array", items });
15203
+ }
15204
+ throw new ExprError(t.type === "eof" ? "Expression is incomplete" : `Unexpected "${t.value}"`, t.pos);
15205
+ }
15206
+ };
15207
+ function parseExpression(src) {
15208
+ const text = String(src ?? "");
15209
+ if (text.length > MAX_EXPR_LENGTH) throw new ExprError("Expression is too long");
15210
+ const trimmed = text.trim().replace(/;+\s*$/, "");
15211
+ if (!trimmed) throw new ExprError("Expression is empty");
15212
+ const parser = new Parser(tokenize(trimmed));
15213
+ const ast = parser.parseExpression();
15214
+ const tail = parser.peek();
15215
+ if (tail.type !== "eof") throw new ExprError(`Unexpected "${tail.value}"`, tail.pos);
15216
+ return ast;
15217
+ }
15218
+ var astCache = /* @__PURE__ */ new Map();
15219
+ var AST_CACHE_MAX = 200;
15220
+ function compileExpression(src) {
15221
+ const key = String(src ?? "");
15222
+ const hit = astCache.get(key);
15223
+ if (hit) return hit;
15224
+ let entry;
15225
+ try {
15226
+ entry = { ast: parseExpression(key), error: null };
15227
+ } catch (err) {
15228
+ entry = { ast: null, error: err instanceof ExprError ? err : new ExprError(String(err?.message || err)) };
15229
+ }
15230
+ if (astCache.size >= AST_CACHE_MAX) astCache.delete(astCache.keys().next().value);
15231
+ astCache.set(key, entry);
15232
+ return entry;
15233
+ }
15234
+ function latticeNoise(i, seed) {
15235
+ let h = Math.imul(i | 0, 374761393) + Math.imul(seed | 0, 668265263);
15236
+ h = Math.imul(h ^ h >>> 13, 1274126177);
15237
+ h ^= h >>> 16;
15238
+ return (h & 2147483647) / 1073741823 - 1;
15239
+ }
15240
+ function stringSeed(str) {
15241
+ let h = 2166136261;
15242
+ const s = String(str || "");
15243
+ for (let i = 0; i < s.length; i += 1) {
15244
+ h ^= s.charCodeAt(i);
15245
+ h = Math.imul(h, 16777619);
15246
+ }
15247
+ return h | 0;
15248
+ }
15249
+ function smoothNoise(x, seed) {
15250
+ const i = Math.floor(x);
15251
+ const f2 = x - i;
15252
+ const u = f2 * f2 * (3 - 2 * f2);
15253
+ return latticeNoise(i, seed) * (1 - u) + latticeNoise(i + 1, seed) * u;
15254
+ }
15255
+ function fractalNoise(x, seed, octaves) {
15256
+ let sum = 0;
15257
+ let amp = 1;
15258
+ let norm = 0;
15259
+ let freq = 1;
15260
+ const n2 = Math.max(1, Math.min(4, Math.round(octaves)));
15261
+ for (let o = 0; o < n2; o += 1) {
15262
+ sum += smoothNoise(x * freq, seed + o * 1013) * amp;
15263
+ norm += amp;
15264
+ amp *= 0.5;
15265
+ freq *= 2;
15266
+ }
15267
+ return norm > 0 ? sum / norm : 0;
15268
+ }
15269
+ var LOOP_TYPES = /* @__PURE__ */ new Set(["cycle", "pingpong", "offset", "continue"]);
15270
+ function loopSegment(keyTimes, n2, isOut, byDuration) {
15271
+ const first = keyTimes[0];
15272
+ const last = keyTimes[keyTimes.length - 1];
15273
+ if (byDuration) {
15274
+ const d = Math.abs(n2);
15275
+ if (!(d > 0)) return [first, last];
15276
+ return isOut ? [Math.max(first, last - d), last] : [first, Math.min(last, first + d)];
15277
+ }
15278
+ const count = Math.max(0, Math.round(n2));
15279
+ if (count <= 0) return [first, last];
15280
+ const idx = isOut ? Math.max(0, keyTimes.length - 1 - count) : Math.min(keyTimes.length - 1, count);
15281
+ return isOut ? [keyTimes[idx], last] : [first, keyTimes[idx]];
15282
+ }
15283
+ function loopValue(ctx, isOut, typeRaw, n2, byDuration) {
15284
+ const keyTimes = ctx.keyTimes;
15285
+ const valueAt = ctx.valueAt;
15286
+ if (typeof valueAt !== "function" || !Array.isArray(keyTimes) || keyTimes.length < 2) return ctx.value;
15287
+ const type = String(typeRaw ?? "cycle").toLowerCase();
15288
+ if (!LOOP_TYPES.has(type)) throw new ExprError(`Unknown loop type "${typeRaw}" \u2014 use cycle, pingpong, offset or continue`);
15289
+ const t = num4(ctx.time);
15290
+ const [segStart, segEnd] = loopSegment(keyTimes, num4(n2), isOut, byDuration);
15291
+ const edge = isOut ? keyTimes[keyTimes.length - 1] : keyTimes[0];
15292
+ const delta = isOut ? t - edge : edge - t;
15293
+ if (delta <= 0) return ctx.value;
15294
+ const span = segEnd - segStart;
15295
+ if (!(span > 0)) return valueAt(edge);
15296
+ if (type === "continue") {
15297
+ const h = Math.min(span * 0.25, 1 / 60);
15298
+ const vEdge = valueAt(edge);
15299
+ const slope = isOut ? (vEdge - valueAt(edge - h)) / h : (valueAt(edge + h) - vEdge) / h;
15300
+ return vEdge + slope * (isOut ? delta : -delta);
15301
+ }
15302
+ const cycles = Math.floor(delta / span);
15303
+ const phase = delta - cycles * span;
15304
+ if (type === "pingpong") {
15305
+ const backwards = cycles % 2 === 0;
15306
+ const at2 = isOut ? backwards ? segEnd - phase : segStart + phase : backwards ? segStart + phase : segEnd - phase;
15307
+ return valueAt(at2);
15308
+ }
15309
+ const at = isOut ? segStart + phase : segEnd - phase;
15310
+ const v = valueAt(at);
15311
+ if (type === "cycle") return v;
15312
+ const rise = valueAt(segEnd) - valueAt(segStart);
15313
+ return v + rise * (cycles + 1) * (isOut ? 1 : -1);
15314
+ }
15315
+ var GLIDE_FAST_TAPS = 48;
15316
+ var GLIDE_SLOW_TAPS = 64;
15317
+ function glideValue(ctx, fastArg, slowArg, weightArg, delayArg) {
15318
+ const valueAt = ctx.valueAt;
15319
+ if (typeof valueAt !== "function") return num4(ctx.value);
15320
+ const fast = clampNumber(fastArg === void 0 ? 0.1 : num4(fastArg), 5e-3, 3);
15321
+ const slow = clampNumber(slowArg === void 0 ? 0.6 : num4(slowArg), 0.01, 8);
15322
+ const weight = clampNumber(weightArg === void 0 ? 0.25 : num4(weightArg), 0, 1);
15323
+ const delay = clampNumber(delayArg === void 0 ? 0 : num4(delayArg), 0, 3);
15324
+ const t = num4(ctx.time);
15325
+ let acc = 0;
15326
+ let total = 0;
15327
+ const part = (share, tau0, timeConstant, taps) => {
15328
+ if (share <= 0) return;
15329
+ const span = 4 * timeConstant;
15330
+ const step = span / taps;
15331
+ for (let i = 0; i <= taps; i += 1) {
15332
+ const tau = i * step;
15333
+ const w = share * Math.exp(-tau / timeConstant) * step / timeConstant;
15334
+ acc += w * valueAt(t - tau0 - tau);
15335
+ total += w;
15336
+ }
15337
+ };
15338
+ part(1 - weight, 0, fast, GLIDE_FAST_TAPS);
15339
+ part(weight, delay, slow, GLIDE_SLOW_TAPS);
15340
+ return total > 0 ? acc / total : num4(ctx.value);
15341
+ }
15342
+ var num4 = (v) => {
14408
15343
  const n2 = typeof v === "number" ? v : Number.parseFloat(v);
14409
15344
  return Number.isFinite(n2) ? n2 : 0;
14410
15345
  };
15346
+ var truthy = (v) => typeof v === "number" ? v !== 0 : !!v;
14411
15347
  function clampNumber(v, lo, hi) {
14412
15348
  return Math.min(hi, Math.max(lo, v));
14413
15349
  }
@@ -14418,17 +15354,17 @@ function remap(args, shape) {
14418
15354
  let v1;
14419
15355
  let v2;
14420
15356
  if (args.length <= 3) {
14421
- t = num(args[0]);
15357
+ t = num4(args[0]);
14422
15358
  tMin = 0;
14423
15359
  tMax = 1;
14424
- v1 = num(args[1]);
14425
- v2 = num(args[2]);
15360
+ v1 = num4(args[1]);
15361
+ v2 = num4(args[2]);
14426
15362
  } else {
14427
- t = num(args[0]);
14428
- tMin = num(args[1]);
14429
- tMax = num(args[2]);
14430
- v1 = num(args[3]);
14431
- v2 = num(args[4]);
15363
+ t = num4(args[0]);
15364
+ tMin = num4(args[1]);
15365
+ tMax = num4(args[2]);
15366
+ v1 = num4(args[3]);
15367
+ v2 = num4(args[4]);
14432
15368
  }
14433
15369
  if (tMax === tMin) return t <= tMin ? v1 : v2;
14434
15370
  const raw = clampNumber((t - tMin) / (tMax - tMin), 0, 1);
@@ -14443,12 +15379,12 @@ var EASE_SHAPES = {
14443
15379
  function makeMathNamespace() {
14444
15380
  const ns = /* @__PURE__ */ Object.create(null);
14445
15381
  const unary = ["abs", "floor", "ceil", "round", "sqrt", "cbrt", "exp", "log", "log2", "log10", "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh", "sign", "trunc"];
14446
- for (const name of unary) ns[name] = (...a) => Math[name](num(a[0]));
14447
- ns.pow = (a, b2) => Math.pow(num(a), num(b2));
14448
- ns.atan2 = (a, b2) => Math.atan2(num(a), num(b2));
14449
- ns.hypot = (...a) => Math.hypot(...a.map(num));
14450
- ns.min = (...a) => a.length ? Math.min(...a.map(num)) : 0;
14451
- ns.max = (...a) => a.length ? Math.max(...a.map(num)) : 0;
15382
+ for (const name of unary) ns[name] = (...a) => Math[name](num4(a[0]));
15383
+ ns.pow = (a, b2) => Math.pow(num4(a), num4(b2));
15384
+ ns.atan2 = (a, b2) => Math.atan2(num4(a), num4(b2));
15385
+ ns.hypot = (...a) => Math.hypot(...a.map(num4));
15386
+ ns.min = (...a) => a.length ? Math.min(...a.map(num4)) : 0;
15387
+ ns.max = (...a) => a.length ? Math.max(...a.map(num4)) : 0;
14452
15388
  ns.PI = Math.PI;
14453
15389
  ns.E = Math.E;
14454
15390
  return ns;
@@ -14462,17 +15398,187 @@ var STATIC_GLOBALS = (() => {
14462
15398
  for (const name of ["abs", "floor", "ceil", "round", "sqrt", "exp", "log", "sin", "cos", "tan", "asin", "acos", "atan", "atan2", "pow", "min", "max", "sign", "hypot"]) {
14463
15399
  g[name] = MATH_NS[name];
14464
15400
  }
14465
- g.clamp = (v, lo, hi) => clampNumber(num(v), num(lo), num(hi));
14466
- g.lerp = (a, b2, t) => num(a) + (num(b2) - num(a)) * num(t);
15401
+ g.clamp = (v, lo, hi) => clampNumber(num4(v), num4(lo), num4(hi));
15402
+ g.lerp = (a, b2, t) => num4(a) + (num4(b2) - num4(a)) * num4(t);
14467
15403
  g.linear = (...a) => remap(a, EASE_SHAPES.linear);
14468
15404
  g.ease = (...a) => remap(a, EASE_SHAPES.ease);
14469
15405
  g.easeIn = (...a) => remap(a, EASE_SHAPES.easeIn);
14470
15406
  g.easeOut = (...a) => remap(a, EASE_SHAPES.easeOut);
14471
- g.degrees = (r) => num(r) * 180 / Math.PI;
14472
- g.radians = (d) => num(d) * Math.PI / 180;
15407
+ g.degrees = (r) => num4(r) * 180 / Math.PI;
15408
+ g.radians = (d) => num4(d) * Math.PI / 180;
14473
15409
  g.seedRandom = () => 0;
14474
15410
  return g;
14475
15411
  })();
15412
+ function evaluateAst(ast, ctx) {
15413
+ const state = { steps: 0 };
15414
+ let randomCounter = 0;
15415
+ const seed = ctx.seed | 0;
15416
+ const timeSeed = () => Math.floor(num4(ctx.time) * 1e3);
15417
+ const layerRef = (ref) => {
15418
+ if (!ref) return null;
15419
+ const obj = /* @__PURE__ */ Object.create(null);
15420
+ obj.__layer = ref;
15421
+ return obj;
15422
+ };
15423
+ const globals = Object.create(STATIC_GLOBALS);
15424
+ globals.value = ctx.value;
15425
+ globals.time = ctx.time;
15426
+ globals.frame = ctx.frame;
15427
+ globals.fps = ctx.fps;
15428
+ globals.duration = ctx.duration;
15429
+ globals.progress = ctx.progress;
15430
+ globals.index = ctx.index;
15431
+ globals.thisProperty = ctx.value;
15432
+ globals.noise = (t, octaves) => fractalNoise(num4(t), seed, octaves === void 0 ? 1 : num4(octaves));
15433
+ globals.wiggle = (freq, amp, octaves) => num4(ctx.value) + num4(amp) * fractalNoise(num4(ctx.time) * num4(freq), seed, octaves === void 0 ? 2 : num4(octaves));
15434
+ globals.random = (...a) => {
15435
+ randomCounter += 1;
15436
+ const r = (latticeNoise(timeSeed() + randomCounter * 7919, seed) + 1) / 2;
15437
+ if (a.length === 0) return r;
15438
+ if (a.length === 1) return r * num4(a[0]);
15439
+ return num4(a[0]) + r * (num4(a[1]) - num4(a[0]));
15440
+ };
15441
+ globals.loopOut = (type, n2) => loopValue(ctx, true, type, n2, false);
15442
+ globals.loopIn = (type, n2) => loopValue(ctx, false, type, n2, false);
15443
+ globals.loopOutDuration = (type, d) => loopValue(ctx, true, type, d, true);
15444
+ globals.loopInDuration = (type, d) => loopValue(ctx, false, type, d, true);
15445
+ globals.numKeys = Array.isArray(ctx.keyTimes) ? ctx.keyTimes.length : 0;
15446
+ globals.valueAtTime = (t) => typeof ctx.valueAt === "function" ? ctx.valueAt(num4(t)) : ctx.value;
15447
+ globals.glide = (fast, slow, weight, delay) => glideValue(ctx, fast, slow, weight, delay);
15448
+ globals.layer = (name) => {
15449
+ const ref = ctx.resolveLayer?.(typeof name === "string" ? name : num4(name));
15450
+ if (!ref) throw new ExprError(`No layer named ${JSON.stringify(String(name))} overlaps this clip`);
15451
+ return layerRef(ref);
15452
+ };
15453
+ globals.thisLayer = layerRef(ctx.resolveSelf?.());
15454
+ globals.thisComp = (() => {
15455
+ const ns = /* @__PURE__ */ Object.create(null);
15456
+ ns.layer = globals.layer;
15457
+ ns.duration = ctx.duration;
15458
+ ns.frameDuration = ctx.fps > 0 ? 1 / ctx.fps : 1 / 30;
15459
+ return ns;
15460
+ })();
15461
+ const walk = (node) => {
15462
+ state.steps += 1;
15463
+ if (state.steps > MAX_STEPS) throw new ExprError("Expression is too slow to evaluate");
15464
+ switch (node.kind) {
15465
+ case "num":
15466
+ case "str":
15467
+ return node.value;
15468
+ case "array":
15469
+ return node.items.map(walk);
15470
+ case "ident": {
15471
+ if (!(node.name in globals)) throw new ExprError(`Unknown name "${node.name}"`, node.pos);
15472
+ return globals[node.name];
15473
+ }
15474
+ case "un": {
15475
+ const v = walk(node.arg);
15476
+ if (node.op === "-") return -num4(v);
15477
+ if (node.op === "+") return num4(v);
15478
+ return truthy(v) ? 0 : 1;
15479
+ }
15480
+ case "bin": {
15481
+ const op = node.op;
15482
+ if (op === "&&") {
15483
+ const l2 = walk(node.left);
15484
+ return truthy(l2) ? walk(node.right) : l2;
15485
+ }
15486
+ if (op === "||") {
15487
+ const l2 = walk(node.left);
15488
+ return truthy(l2) ? l2 : walk(node.right);
15489
+ }
15490
+ const l = walk(node.left);
15491
+ const r = walk(node.right);
15492
+ switch (op) {
15493
+ // `+` concatenates when either side is a string, like JS.
15494
+ case "+":
15495
+ return typeof l === "string" || typeof r === "string" ? `${l}${r}` : num4(l) + num4(r);
15496
+ case "-":
15497
+ return num4(l) - num4(r);
15498
+ case "*":
15499
+ return num4(l) * num4(r);
15500
+ case "/": {
15501
+ const d = num4(r);
15502
+ return d === 0 ? 0 : num4(l) / d;
15503
+ }
15504
+ case "%": {
15505
+ const d = num4(r);
15506
+ return d === 0 ? 0 : num4(l) % d;
15507
+ }
15508
+ case "^":
15509
+ case "**":
15510
+ return Math.pow(num4(l), num4(r));
15511
+ case "<":
15512
+ return num4(l) < num4(r) ? 1 : 0;
15513
+ case ">":
15514
+ return num4(l) > num4(r) ? 1 : 0;
15515
+ case "<=":
15516
+ return num4(l) <= num4(r) ? 1 : 0;
15517
+ case ">=":
15518
+ return num4(l) >= num4(r) ? 1 : 0;
15519
+ case "==":
15520
+ case "===":
15521
+ return l === r ? 1 : 0;
15522
+ case "!=":
15523
+ case "!==":
15524
+ return l !== r ? 1 : 0;
15525
+ default:
15526
+ throw new ExprError(`Unknown operator "${op}"`, node.pos);
15527
+ }
15528
+ }
15529
+ case "cond":
15530
+ return truthy(walk(node.test)) ? walk(node.then) : walk(node.other);
15531
+ case "index": {
15532
+ const target = walk(node.target);
15533
+ const idx = walk(node.index);
15534
+ if (Array.isArray(target)) return target[Math.trunc(num4(idx))] ?? 0;
15535
+ if (target && typeof target === "object" && target.__layer) return target.__layer.get(String(idx));
15536
+ throw new ExprError("Only layers and lists can be indexed", node.pos);
15537
+ }
15538
+ case "member": {
15539
+ const target = walk(node.target);
15540
+ if (target && typeof target === "object") {
15541
+ if (target.__layer) {
15542
+ if (node.name === "layer") {
15543
+ return (inner) => {
15544
+ const child = target.__layer.child?.(String(inner));
15545
+ if (!child) throw new ExprError(`Cannot read layers inside ${JSON.stringify(String(inner))} here`, node.pos);
15546
+ return layerRef(child);
15547
+ };
15548
+ }
15549
+ return target.__layer.get(node.name);
15550
+ }
15551
+ if (node.name in target) return target[node.name];
15552
+ throw new ExprError(`Unknown property "${node.name}"`, node.pos);
15553
+ }
15554
+ if (Array.isArray(target)) return node.name === "length" ? target.length : 0;
15555
+ throw new ExprError(`"${node.name}" is not available here`, node.pos);
15556
+ }
15557
+ case "call": {
15558
+ const fn = walk(node.callee);
15559
+ if (typeof fn !== "function") throw new ExprError("Not a function", node.pos);
15560
+ const args = node.args.map(walk);
15561
+ return fn(...args);
15562
+ }
15563
+ default:
15564
+ throw new ExprError("Unsupported expression", node.pos);
15565
+ }
15566
+ };
15567
+ const out = walk(ast);
15568
+ return out;
15569
+ }
15570
+ function runExpression(src, ctx) {
15571
+ const { ast, error } = compileExpression(src);
15572
+ if (error) return { value: ctx.value, error: error.message };
15573
+ try {
15574
+ const raw = evaluateAst(ast, ctx);
15575
+ const v = typeof raw === "number" ? raw : Number.parseFloat(raw);
15576
+ if (!Number.isFinite(v)) return { value: ctx.value, error: "Result is not a number" };
15577
+ return { value: v, error: null };
15578
+ } catch (err) {
15579
+ return { value: ctx.value, error: err instanceof ExprError ? err.message : String(err?.message || err) };
15580
+ }
15581
+ }
14476
15582
 
14477
15583
  // src/components/videoEditor/textPlaceholders.js
14478
15584
  init_define_import_meta_env();
@@ -14493,14 +15599,116 @@ var COUNTER_DEFAULTS = Object.freeze({
14493
15599
  textCounterDirection: "auto",
14494
15600
  textCounterBlur: 35
14495
15601
  });
15602
+ var MAX_COLUMNS = 15;
14496
15603
  var RANGE_SEP = "\\s*(?:\\.\\.|\u2192|\u2013|\u2014|to|-)\\s*";
14497
15604
  var NUM_SRC = "[-\u2212]?\\d[\\d,]*(?:\\.\\d+)?";
14498
15605
  var COUNTER_RE = new RegExp(`^(${NUM_SRC})(?:${RANGE_SEP}(${NUM_SRC}))?$`, "i");
14499
15606
  var TIME_SRC = "\\d+(?::\\d+){0,2}(?:\\.\\d+)?";
14500
15607
  var TIMER_RE = new RegExp(`^(${TIME_SRC})(?:${RANGE_SEP}(${TIME_SRC}))?$`, "i");
15608
+ function digitCount(n2) {
15609
+ return String(Math.floor(Math.abs(n2))).length;
15610
+ }
15611
+ function parseLiteral(raw) {
15612
+ const s = raw.replace(/−/g, "-");
15613
+ const neg = s[0] === "-";
15614
+ const body = neg ? s.slice(1) : s;
15615
+ const dot = body.indexOf(".");
15616
+ const intRaw = dot >= 0 ? body.slice(0, dot) : body;
15617
+ const frac = dot >= 0 ? body.slice(dot + 1) : "";
15618
+ const intDigits = intRaw.replace(/,/g, "") || "0";
15619
+ return {
15620
+ value: Number(`${neg ? "-" : ""}${intDigits}${frac ? `.${frac}` : ""}`),
15621
+ decimals: frac.length,
15622
+ sep: intRaw.includes(","),
15623
+ pad: intDigits.length > 1 && intDigits[0] === "0" ? intDigits.length : 0
15624
+ };
15625
+ }
15626
+ function parseCounterRange(body) {
15627
+ const m = COUNTER_RE.exec(String(body ?? "").trim());
15628
+ if (!m) return null;
15629
+ const single = m[2] == null;
15630
+ const a = single ? { value: 0, decimals: 0, sep: false, pad: 0 } : parseLiteral(m[1]);
15631
+ const b2 = parseLiteral(single ? m[1] : m[2]);
15632
+ if (!Number.isFinite(a.value) || !Number.isFinite(b2.value)) return null;
15633
+ const decimals = Math.min(4, Math.max(a.decimals, b2.decimals));
15634
+ const scale = 10 ** decimals;
15635
+ const pad = Math.max(a.pad, b2.pad);
15636
+ const from = Math.round(a.value * scale);
15637
+ const to = Math.round(b2.value * scale);
15638
+ const intCols = Math.max(1, pad, digitCount(Math.abs(from) / scale), digitCount(Math.abs(to) / scale));
15639
+ if (intCols + decimals > MAX_COLUMNS) return null;
15640
+ const cols = [];
15641
+ for (let j = 0; j < intCols + decimals; j += 1) {
15642
+ cols.push({ place: 10 ** j, radix: 10, lead: j - decimals >= Math.max(1, pad) });
15643
+ }
15644
+ const sepRight = {};
15645
+ if (decimals > 0) sepRight[decimals] = ".";
15646
+ if (a.sep || b2.sep) for (let i = 3; i < intCols; i += 3) sepRight[decimals + i] = ",";
15647
+ return { kind: "counter", from, to, scale, cols, sepRight, signed: from < 0 || to < 0 };
15648
+ }
15649
+ function parseTime(raw) {
15650
+ const dot = raw.indexOf(".");
15651
+ const main = dot >= 0 ? raw.slice(0, dot) : raw;
15652
+ const frac = dot >= 0 ? raw.slice(dot + 1) : "";
15653
+ const parts = main.split(":");
15654
+ for (let i = 1; i < parts.length; i += 1) {
15655
+ if (parts[i].length > 2 || Number(parts[i]) >= 60) return null;
15656
+ }
15657
+ let seconds = 0;
15658
+ for (const p of parts) seconds = seconds * 60 + Number(p);
15659
+ return { parts, frac, seconds: seconds + (frac ? Number(`0.${frac}`) : 0) };
15660
+ }
15661
+ function parseTimerRange(body) {
15662
+ const m = TIMER_RE.exec(String(body ?? "").trim());
15663
+ if (!m) return null;
15664
+ const a = parseTime(m[1]);
15665
+ const b2 = m[2] != null ? parseTime(m[2]) : { parts: ["0"], frac: "", seconds: 0 };
15666
+ if (!a || !b2) return null;
15667
+ const fields = Math.max(a.parts.length, b2.parts.length);
15668
+ const decimals = Math.min(3, Math.max(a.frac.length, b2.frac.length));
15669
+ const S = 10 ** decimals;
15670
+ const from = Math.round(a.seconds * S);
15671
+ const to = Math.round(b2.seconds * S);
15672
+ const topPlace = fields === 3 ? 3600 * S : fields === 2 ? 60 * S : S;
15673
+ const typedTop = [a, b2].filter((t) => t.parts.length === fields).map((t) => t.parts[0]);
15674
+ const pad = Math.max(0, ...typedTop.map((p) => p.length > 1 && p[0] === "0" ? p.length : 0));
15675
+ const topDigits = Math.max(1, pad, digitCount(Math.floor(from / topPlace)), digitCount(Math.floor(to / topPlace)));
15676
+ const cols = [];
15677
+ for (let k = 0; k < decimals; k += 1) cols.push({ place: 10 ** k, radix: 10, lead: false });
15678
+ const clockField = (place) => {
15679
+ cols.push({ place, radix: 10, lead: false });
15680
+ cols.push({ place: place * 10, radix: 6, lead: false });
15681
+ };
15682
+ if (fields >= 2) clockField(S);
15683
+ if (fields === 3) clockField(60 * S);
15684
+ for (let k = 0; k < topDigits; k += 1) {
15685
+ cols.push({ place: topPlace * 10 ** k, radix: 10, lead: k >= Math.max(1, pad) });
15686
+ }
15687
+ if (cols.length > MAX_COLUMNS) return null;
15688
+ const sepRight = {};
15689
+ if (decimals > 0) sepRight[decimals] = ".";
15690
+ if (fields >= 2) sepRight[decimals + 2] = ":";
15691
+ if (fields === 3) sepRight[decimals + 4] = ":";
15692
+ return { kind: "timer", from, to, scale: S, cols, sepRight, signed: false };
15693
+ }
15694
+ function formatUnits(units, spec) {
15695
+ let out = "";
15696
+ for (let j = spec.cols.length - 1; j >= 0; j -= 1) {
15697
+ const c = spec.cols[j];
15698
+ if (c.lead && units < c.place) continue;
15699
+ out += String(Math.floor(units / c.place) % c.radix);
15700
+ if (j > 0 && spec.sepRight[j]) out += spec.sepRight[j];
15701
+ }
15702
+ return out;
15703
+ }
15704
+ function formatCounterValue(units, spec) {
15705
+ const u = Math.abs(Math.round(units));
15706
+ return `${units < 0 && u > 0 ? "-" : ""}${formatUnits(u, spec)}`;
15707
+ }
14501
15708
  var SHUTTER_SEC = 1 / 60;
14502
15709
 
14503
15710
  // src/components/videoEditor/textPlaceholders.js
15711
+ var TOKEN_RE = /\{\{\{\s*([A-Za-z]+)\s*(?::([^{}]*?))?\s*\}\}\}/g;
14504
15712
  var PLACEHOLDER_CATALOG = Object.freeze([
14505
15713
  {
14506
15714
  id: "COUNTER",
@@ -14551,6 +15759,7 @@ var PLACEHOLDER_CATALOG = Object.freeze([
14551
15759
  description: "The current frame number. {{{FRAME:clip}}} counts from the layer's start."
14552
15760
  }
14553
15761
  ]);
15762
+ var clamp3 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
14554
15763
  var CYCLE_DEFAULTS = Object.freeze({
14555
15764
  textCycleStyle: "roll",
14556
15765
  textCycleDelay: 0,
@@ -14576,6 +15785,9 @@ var SCRAMBLE_DEFAULTS = Object.freeze({
14576
15785
  textScrambleOrder: "ltr",
14577
15786
  textScrambleRate: 18
14578
15787
  });
15788
+ var TIMECODE_FORMATS = ["hh:mm:ss:ff", "hh:mm:ss", "mm:ss:ff", "mm:ss", "ss:ff", "ss"];
15789
+ var EASING_VALUE_RE = /^[a-z][a-z0-9_]{0,31}$/;
15790
+ var KF_ID_RE = /^[a-z0-9]{1,8}$/;
14579
15791
  var PLACEHOLDER_OPTIONS = Object.freeze({
14580
15792
  number: {
14581
15793
  style: { values: ["odometer", "roll", "count"] },
@@ -14610,6 +15822,81 @@ var PLACEHOLDER_OPTIONS = Object.freeze({
14610
15822
  },
14611
15823
  readout: {}
14612
15824
  });
15825
+ function cleanOption(schema, raw) {
15826
+ const v = String(raw ?? "").trim().toLowerCase();
15827
+ if (!schema || !v) return null;
15828
+ if (schema.values) return schema.values.includes(v) ? v : null;
15829
+ if (schema.easing) return EASING_VALUE_RE.test(v) ? v : null;
15830
+ if (schema.kf) return KF_ID_RE.test(v) ? v : null;
15831
+ const n2 = Number(v);
15832
+ if (!Number.isFinite(n2)) return null;
15833
+ const c = clamp3(n2, schema.num[0], schema.num[1]);
15834
+ return schema.int ? Math.round(c) : Number(c.toFixed(3));
15835
+ }
15836
+ function splitBody(rawBody) {
15837
+ const parts = String(rawBody ?? "").split(";");
15838
+ const main = parts[0].trim();
15839
+ const options = {};
15840
+ for (const part of parts.slice(1)) {
15841
+ const eq = part.indexOf("=");
15842
+ if (eq < 0) continue;
15843
+ const key = part.slice(0, eq).trim().toLowerCase();
15844
+ if (key) options[key] = part.slice(eq + 1).trim();
15845
+ }
15846
+ return { main, options };
15847
+ }
15848
+ function parseReadoutArgs(body) {
15849
+ const words2 = String(body || "").toLowerCase().split(/[\s,]+/).filter(Boolean);
15850
+ const format = words2.find((w) => TIMECODE_FORMATS.includes(w)) || null;
15851
+ const clip = words2.includes("clip");
15852
+ if (words2.some((w) => w !== "clip" && w !== "project" && !TIMECODE_FORMATS.includes(w))) return null;
15853
+ return { clip, format };
15854
+ }
15855
+ function parseUnit(type, rawBody) {
15856
+ const { main, options: rawOptions } = splitBody(rawBody);
15857
+ let unit = null;
15858
+ switch (type) {
15859
+ case "COUNTER": {
15860
+ const spec = parseCounterRange(main);
15861
+ unit = spec ? { type: "number", spec } : null;
15862
+ break;
15863
+ }
15864
+ case "TIMER": {
15865
+ const spec = parseTimerRange(main);
15866
+ unit = spec ? { type: "number", spec } : null;
15867
+ break;
15868
+ }
15869
+ case "CYCLE": {
15870
+ const words2 = main.split("|").map((w) => w.trim()).filter(Boolean);
15871
+ unit = words2.length ? { type: "cycle", words: words2 } : null;
15872
+ break;
15873
+ }
15874
+ case "SCRAMBLE":
15875
+ unit = main ? { type: "scramble", text: main } : null;
15876
+ break;
15877
+ case "TIMECODE": {
15878
+ const args = parseReadoutArgs(main);
15879
+ unit = args ? { type: "readout", mode: "timecode", clip: args.clip, format: args.format || "hh:mm:ss:ff" } : null;
15880
+ break;
15881
+ }
15882
+ case "FRAME": {
15883
+ const args = parseReadoutArgs(main);
15884
+ unit = args && !args.format ? { type: "readout", mode: "frame", clip: args.clip } : null;
15885
+ break;
15886
+ }
15887
+ default:
15888
+ return null;
15889
+ }
15890
+ if (!unit) return null;
15891
+ const schema = PLACEHOLDER_OPTIONS[unit.type];
15892
+ const options = {};
15893
+ for (const [k, v] of Object.entries(rawOptions)) {
15894
+ const clean = cleanOption(schema[k], v);
15895
+ if (clean !== null) options[k] = clean;
15896
+ }
15897
+ return { ...unit, tokenType: type, main, options };
15898
+ }
15899
+ var hasToken = (text) => typeof text === "string" && text.indexOf("{{{") >= 0;
14613
15900
  var PLACEHOLDER_LABELS = Object.freeze({
14614
15901
  COUNTER: "Counter",
14615
15902
  TIMER: "Timer",
@@ -14618,6 +15905,55 @@ var PLACEHOLDER_LABELS = Object.freeze({
14618
15905
  TIMECODE: "Timecode",
14619
15906
  FRAME: "Frame number"
14620
15907
  });
15908
+ function findVariable(variables, name) {
15909
+ if (!Array.isArray(variables)) return null;
15910
+ const key = name.toLowerCase();
15911
+ return variables.find((v) => String(v?.name || "").toLowerCase() === key) || null;
15912
+ }
15913
+ function formatTimecode(sec, fps, format = "hh:mm:ss:ff") {
15914
+ const f2 = Math.max(1, Math.round(Number(fps) || 30));
15915
+ const frames = Math.max(0, Math.floor(sec * f2 + 1e-6));
15916
+ const ff = frames % f2;
15917
+ const total = Math.floor(frames / f2);
15918
+ const ss = total % 60;
15919
+ const mm = Math.floor(total / 60) % 60;
15920
+ const hh = Math.floor(total / 3600);
15921
+ const p2 = (n2) => String(n2).padStart(2, "0");
15922
+ const pf = (n2) => String(n2).padStart(String(f2 - 1).length, "0");
15923
+ switch (format) {
15924
+ case "hh:mm:ss":
15925
+ return `${p2(hh)}:${p2(mm)}:${p2(ss)}`;
15926
+ case "mm:ss:ff":
15927
+ return `${p2(Math.floor(total / 60))}:${p2(ss)}:${pf(ff)}`;
15928
+ case "mm:ss":
15929
+ return `${p2(Math.floor(total / 60))}:${p2(ss)}`;
15930
+ case "ss:ff":
15931
+ return `${p2(total)}:${pf(ff)}`;
15932
+ case "ss":
15933
+ return p2(total);
15934
+ default:
15935
+ return `${p2(hh)}:${p2(mm)}:${p2(ss)}:${pf(ff)}`;
15936
+ }
15937
+ }
15938
+ function resolvePlaceholderText(text, env = {}) {
15939
+ if (!hasToken(text)) return text;
15940
+ const expanded = text.replace(TOKEN_RE, (whole, rawType, body) => {
15941
+ const type = rawType.toUpperCase();
15942
+ if (type === "VAR") {
15943
+ const name = String(body || "").trim();
15944
+ const v = name ? findVariable(env.variables, name) : null;
15945
+ return v && typeof v.value === "string" ? v.value : name || whole;
15946
+ }
15947
+ if (type === "MARKER") return String(body || "").trim() || "Marker";
15948
+ const unit = parseUnit(type, body);
15949
+ if (!unit) return whole;
15950
+ if (unit.type === "number") return formatCounterValue(unit.spec.to, unit.spec);
15951
+ if (unit.type === "cycle") return unit.words[0];
15952
+ if (unit.type === "scramble") return unit.text;
15953
+ return unit.mode === "frame" ? "0" : formatTimecode(0, env.fps, unit.format);
15954
+ });
15955
+ return expanded;
15956
+ }
14621
15957
 
14622
15958
  // src/components/videoEditor/keyframeFx.js
14623
15959
  var KEYFRAME_FX_TYPES = [
@@ -14735,11 +16071,308 @@ function sanitizeKeyframeFxMap(raw, propertyKeyframes = null) {
14735
16071
  }
14736
16072
  return out;
14737
16073
  }
16074
+ function hasAnyKeyframeFx(map) {
16075
+ return !!map && typeof map === "object" && Object.keys(map).length > 0;
16076
+ }
16077
+ function numOr(v, fallback) {
16078
+ const n2 = Number.parseFloat(v);
16079
+ return Number.isFinite(n2) ? n2 : fallback;
16080
+ }
16081
+ function elasticProfile(u, wobbles, decay) {
16082
+ if (u <= 0) return 0;
16083
+ if (decay * u > 6.9) return 1;
16084
+ return 1 - Math.exp(-decay * u) * Math.cos(2 * Math.PI * wobbles * u);
16085
+ }
16086
+ function bounceProfile(u, e2, nMax) {
16087
+ if (u <= 0) return 0;
16088
+ const hopSum = 2 * e2 * (1 - Math.pow(e2, nMax)) / (1 - e2);
16089
+ const T0 = 1 / (1 + hopSum);
16090
+ if (u >= 1) return 1;
16091
+ if (u < T0) {
16092
+ const r = u / T0;
16093
+ return r * r;
16094
+ }
16095
+ const g = 2 / (T0 * T0);
16096
+ const v = g * T0;
16097
+ let vk = v * e2;
16098
+ let hop = 2 * vk / g;
16099
+ let tb = u - T0;
16100
+ let nb = 1;
16101
+ while (tb > hop && nb <= nMax) {
16102
+ tb -= hop;
16103
+ vk *= e2;
16104
+ hop *= e2;
16105
+ nb += 1;
16106
+ }
16107
+ if (nb > nMax) return 1;
16108
+ return 1 - (vk * tb - g * tb * tb / 2);
16109
+ }
16110
+ function profiledValue(track, pct, base, durationSec, strength, profile) {
16111
+ if (track.length < 2 || !(strength > 0)) return base;
16112
+ const t = pct / 100 * durationSec;
16113
+ let value = numOr(track[0].value, NaN);
16114
+ if (!Number.isFinite(value)) return base;
16115
+ for (let i = 1; i < track.length; i += 1) {
16116
+ const prev = numOr(track[i - 1].value, NaN);
16117
+ const cur = numOr(track[i].value, NaN);
16118
+ if (!Number.isFinite(prev) || !Number.isFinite(cur)) return base;
16119
+ const step = cur - prev;
16120
+ if (step === 0) continue;
16121
+ const tPrev = Number(track[i - 1].position) / 100 * durationSec;
16122
+ const segSec = Number(track[i].position) / 100 * durationSec - tPrev;
16123
+ if (!(segSec > 0)) continue;
16124
+ value += step * profile((t - tPrev) / segSec);
16125
+ }
16126
+ return base + strength * (value - base);
16127
+ }
16128
+ function elasticValue(cfg, track, pct, base, durationSec) {
16129
+ const { amp, freq, decay } = cfg.params;
16130
+ return profiledValue(track, pct, base, durationSec, amp / 100, (u) => elasticProfile(u, freq, decay));
16131
+ }
16132
+ function bounceValue(cfg, track, pct, base, durationSec) {
16133
+ const { amp, elasticity, bounces } = cfg.params;
16134
+ const e2 = Math.min(0.95, Math.max(0.01, elasticity / 100));
16135
+ const nMax = Math.max(1, Math.round(bounces));
16136
+ return profiledValue(track, pct, base, durationSec, amp / 100, (u) => bounceProfile(u, e2, nMax));
16137
+ }
16138
+ function wiggleOffset(cfg, tSec, seed) {
16139
+ const amp = cfg.params.amp;
16140
+ if (!(amp > 0)) return 0;
16141
+ return amp * smoothNoise(tSec * cfg.params.freq, seed);
16142
+ }
16143
+ var scene = null;
16144
+ var layerNameCache = /* @__PURE__ */ new WeakMap();
16145
+ function layersByName() {
16146
+ if (!scene?.index) return null;
16147
+ const hit = layerNameCache.get(scene.index);
16148
+ if (hit) return hit;
16149
+ const map = /* @__PURE__ */ new Map();
16150
+ for (const entry of scene.index.values()) {
16151
+ const name = String(scene.nameOf?.(entry) || "").trim().toLowerCase();
16152
+ if (name && !map.has(name)) map.set(name, entry);
16153
+ }
16154
+ layerNameCache.set(scene.index, map);
16155
+ return map;
16156
+ }
16157
+ function staticPropValue(item, prop) {
16158
+ switch (prop) {
16159
+ case "opacity":
16160
+ return (Number(item.opacity) || 0) / 100;
16161
+ case "scale":
16162
+ return Number(item.zoom) || 1;
16163
+ case "scaleX":
16164
+ return Number(item.scaleX) || 1;
16165
+ case "scaleY":
16166
+ return Number(item.scaleY) || 1;
16167
+ case "rotate":
16168
+ case "rotateZ":
16169
+ return Number(item.rotateZ ?? item.rotation) || 0;
16170
+ case "rotateX":
16171
+ return Number(item.rotateX) || 0;
16172
+ case "rotateY":
16173
+ return Number(item.rotateY) || 0;
16174
+ case "xPercent":
16175
+ return Number(item.panX) || 0;
16176
+ case "yPercent":
16177
+ return Number(item.panY) || 0;
16178
+ case "translateZ":
16179
+ return Number(item.panZ) || 0;
16180
+ case "motionPathT":
16181
+ return Number(item.motionPathT) || 0;
16182
+ default:
16183
+ return 0;
16184
+ }
16185
+ }
16186
+ var evalDepth = 0;
16187
+ var MAX_LAYER_DEPTH = 2;
14738
16188
  var KNOWN_PROP_NAMES = new Set(KF_PROP_LABELS);
16189
+ var innerIndexCache = /* @__PURE__ */ new WeakMap();
16190
+ function innerIndexOf(clip) {
16191
+ const channels = clip?.sequence?.channels;
16192
+ if (!Array.isArray(channels)) return null;
16193
+ const hit = innerIndexCache.get(channels);
16194
+ if (hit) return hit;
16195
+ const clipsMap = new Map((clip.sequence.clips || []).map((c) => [c.id, c]));
16196
+ const byName = /* @__PURE__ */ new Map();
16197
+ channels.forEach((channel) => {
16198
+ const layout = getTimelineLayout(channel.items || [], { unsnapped: channel.unsnapped });
16199
+ (channel.items || []).forEach((item, i) => {
16200
+ const span = layout[i];
16201
+ if (!span) return;
16202
+ const inner = clipsMap.get(item.clipId);
16203
+ const name = String(
16204
+ item.customName || (inner?.mediaType === "text" ? resolvePlaceholderText(item.textContent) || inner?.name : inner?.name) || ""
16205
+ ).trim().toLowerCase();
16206
+ if (!name || byName.has(name)) return;
16207
+ byName.set(name, {
16208
+ item,
16209
+ clip: inner,
16210
+ startTime: span.startTime,
16211
+ duration: Math.max(0, span.endTime - span.startTime)
16212
+ });
16213
+ });
16214
+ });
16215
+ innerIndexCache.set(channels, byName);
16216
+ return byName;
16217
+ }
16218
+ function containerInnerTime(item, outerLocal) {
16219
+ const trimStart = Number(item.trimStart) || 0;
16220
+ const sourceDuration = (Number(item.trimEnd) || 0) - trimStart;
16221
+ if (!(sourceDuration > 0)) return Math.max(0, outerLocal);
16222
+ const timelineDuration = getTimelineDuration(sourceDuration, item.speedPoints);
16223
+ const normalized = timelineDuration > 0 ? Math.max(0, Math.min(1, outerLocal / timelineDuration)) : 0;
16224
+ return trimStart + mapTimelineToSource(item.speedPoints, normalized) * sourceDuration;
16225
+ }
16226
+ function layerReader(entry, nowOf, label) {
16227
+ if (!entry) return null;
16228
+ const localTime = () => Math.max(0, (Number(nowOf()) || 0) - (Number(entry.startTime) || 0));
16229
+ return {
16230
+ get(prop) {
16231
+ const item = entry.item;
16232
+ if (!item) return 0;
16233
+ const dur = Number(entry.duration) || 0;
16234
+ const progress = dur > 0 ? Math.max(0, Math.min(1, localTime() / dur)) : 0;
16235
+ if (Array.isArray(item.propertyKeyframes?.[prop]) && typeof scene?.evaluate === "function") {
16236
+ evalDepth += 1;
16237
+ try {
16238
+ const v = scene.evaluate(item, progress)?.[prop];
16239
+ const n2 = Number.parseFloat(v);
16240
+ if (Number.isFinite(n2)) return n2;
16241
+ } finally {
16242
+ evalDepth -= 1;
16243
+ }
16244
+ }
16245
+ if (!KNOWN_PROP_NAMES.has(prop)) {
16246
+ throw new Error(`"${label}" has no property "${prop}"`);
16247
+ }
16248
+ return staticPropValue(item, prop);
16249
+ },
16250
+ // Drill into a container layer's own timeline.
16251
+ child(name) {
16252
+ const clip = entry.clip || scene?.clipOf?.(entry.item);
16253
+ if (!isContainerClip(clip)) throw new Error(`"${label}" is not a sequence, so it has no layers inside it`);
16254
+ if (evalDepth >= MAX_LAYER_DEPTH) return null;
16255
+ const index = innerIndexOf(clip);
16256
+ const found = index?.get(String(name).trim().toLowerCase());
16257
+ if (!found) throw new Error(`"${label}" has no layer named ${JSON.stringify(String(name))} inside it`);
16258
+ const innerNow = () => containerInnerTime(entry.item, localTime());
16259
+ return layerReader(found, innerNow, `${label} \u203A ${name}`);
16260
+ }
16261
+ };
16262
+ }
16263
+ function readerFor(entry) {
16264
+ return layerReader(entry, () => Number(scene?.time) || 0, scene?.nameOf?.(entry) || "that layer");
16265
+ }
14739
16266
  var trackSampler = null;
14740
16267
  function setKeyframeTrackSampler(fn) {
14741
16268
  if (typeof fn === "function") trackSampler = fn;
14742
16269
  }
16270
+ function linearSampleTrack(track, pct) {
16271
+ const keys = [...track].sort((a, b2) => Number(a.position) - Number(b2.position));
16272
+ const first = keys[0];
16273
+ const last = keys[keys.length - 1];
16274
+ if (pct <= Number(first.position)) return numOr(first.value, 0);
16275
+ if (pct >= Number(last.position)) return numOr(last.value, 0);
16276
+ for (let i = 0; i < keys.length - 1; i += 1) {
16277
+ const a = keys[i];
16278
+ const b2 = keys[i + 1];
16279
+ const pa = Number(a.position);
16280
+ const pb = Number(b2.position);
16281
+ if (pct >= pa && pct <= pb) {
16282
+ const u = pb > pa ? (pct - pa) / (pb - pa) : 0;
16283
+ return numOr(a.value, 0) + (numOr(b2.value, 0) - numOr(a.value, 0)) * u;
16284
+ }
16285
+ }
16286
+ return numOr(last.value, 0);
16287
+ }
16288
+ function sampleTrackAt(track, pct) {
16289
+ const v = trackSampler ? trackSampler(track, pct) : linearSampleTrack(track, pct);
16290
+ return numOr(v, 0);
16291
+ }
16292
+ function glideTrackValue(cfg, track, pct, base, durationSec) {
16293
+ const { fast, slow, weight, delay } = cfg.params;
16294
+ const ctx = {
16295
+ value: base,
16296
+ time: pct / 100 * durationSec,
16297
+ valueAt: (tSec) => sampleTrackAt(track, tSec / durationSec * 100)
16298
+ };
16299
+ return glideValue(ctx, fast, slow, weight / 100, delay);
16300
+ }
16301
+ function expressionContext(item, track, pct, base, durationSec, seedKey) {
16302
+ const localTime = pct / 100 * durationSec;
16303
+ const fps = Number(scene?.fps) > 0 ? Number(scene.fps) : 30;
16304
+ const selfEntry = scene?.index?.get(item?.id) || null;
16305
+ const keys = Array.isArray(track) ? track : [];
16306
+ return {
16307
+ value: base,
16308
+ time: localTime,
16309
+ frame: Math.floor(localTime * fps + 1e-6),
16310
+ fps,
16311
+ duration: durationSec,
16312
+ progress: pct / 100,
16313
+ index: selfEntry?.channelIndex ?? 0,
16314
+ seed: stringSeed(seedKey),
16315
+ // This property's own keyframes, in clip-local seconds, plus a sampler —
16316
+ // what loopOut/loopIn/valueAtTime read.
16317
+ keyTimes: keys.map((k) => Number(k.position) / 100 * durationSec).filter((t) => Number.isFinite(t)).sort((a, b2) => a - b2),
16318
+ valueAt: (tSec) => durationSec > 0 ? sampleTrackAt(keys, tSec / durationSec * 100) : base,
16319
+ resolveLayer: (name) => {
16320
+ if (evalDepth >= MAX_LAYER_DEPTH) return null;
16321
+ if (typeof name === "number") {
16322
+ for (const entry of scene?.index?.values() || []) {
16323
+ if (entry.channelIndex === Math.trunc(name)) return readerFor(entry);
16324
+ }
16325
+ return null;
16326
+ }
16327
+ const found = layersByName()?.get(String(name).trim().toLowerCase());
16328
+ return found ? readerFor(found) : null;
16329
+ },
16330
+ resolveSelf: () => selfEntry ? readerFor(selfEntry) : null
16331
+ };
16332
+ }
16333
+ function evaluateKeyframeFx(cfg, track, pct, base, durationSec, seedKey = "", item = null) {
16334
+ if (!cfg || !Array.isArray(track) || track.length === 0) return base;
16335
+ if (!Number.isFinite(base) || !(durationSec > 0)) return base;
16336
+ switch (cfg.type) {
16337
+ case "elastic":
16338
+ return elasticValue(cfg, track, pct, base, durationSec);
16339
+ case "bounce":
16340
+ return bounceValue(cfg, track, pct, base, durationSec);
16341
+ case "wiggle":
16342
+ return base + wiggleOffset(cfg, pct / 100 * durationSec, stringSeed(seedKey));
16343
+ case "glide":
16344
+ return glideTrackValue(cfg, track, pct, base, durationSec);
16345
+ case "custom": {
16346
+ const ctx = expressionContext(item || {}, track, pct, base, durationSec, seedKey);
16347
+ return runExpression(cfg.params.code, ctx).value;
16348
+ }
16349
+ default:
16350
+ return base;
16351
+ }
16352
+ }
16353
+ function applyKeyframeFxToValues(item, values, progress, durationSec) {
16354
+ const fxMap = item?.propertyKeyframeFx;
16355
+ if (!values || !hasAnyKeyframeFx(fxMap)) return values;
16356
+ if (evalDepth >= MAX_LAYER_DEPTH) return values;
16357
+ let out = values;
16358
+ let cloned = false;
16359
+ const pct = progress * 100;
16360
+ for (const [prop, cfg] of Object.entries(fxMap)) {
16361
+ const track = item.propertyKeyframes?.[prop];
16362
+ if (!Array.isArray(track) || track.length === 0) continue;
16363
+ const base = Number.parseFloat(out[prop]);
16364
+ if (!Number.isFinite(base)) continue;
16365
+ const v = evaluateKeyframeFx(cfg, track, pct, base, durationSec, `${item.id || ""}|${prop}`, item);
16366
+ if (v !== base) {
16367
+ if (!cloned) {
16368
+ out = { ...values };
16369
+ cloned = true;
16370
+ }
16371
+ out[prop] = v;
16372
+ }
16373
+ }
16374
+ return out;
16375
+ }
14743
16376
 
14744
16377
  // src/components/videoEditor/appEases.js
14745
16378
  init_define_import_meta_env();
@@ -14762,6 +16395,17 @@ function playedEase(easingType) {
14762
16395
  return parseEase(getGsapEase(easingType));
14763
16396
  }
14764
16397
 
16398
+ // src/components/videoEditor/posterizeTime.js
16399
+ init_define_import_meta_env();
16400
+ var integrateRate = null;
16401
+ function setPosterizeRateIntegrator(fn) {
16402
+ integrateRate = typeof fn === "function" ? fn : null;
16403
+ }
16404
+ var PT_NUMBER_FIELDS = [
16405
+ { key: "ptFps", label: "Frame Rate", min: 1, max: 60, step: 1, suffix: "fps", kf: true }
16406
+ ];
16407
+ var PT_KF_KEYS = PT_NUMBER_FIELDS.filter((f2) => f2.kf).map((f2) => f2.key);
16408
+
14765
16409
  // src/components/videoEditor/gsapTransitions.js
14766
16410
  var HOLD_EASE = () => 0;
14767
16411
  var customEaseCache = /* @__PURE__ */ new Map();
@@ -14915,7 +16559,92 @@ function evaluatePropertyTrack(track, pct, isColor = false) {
14915
16559
  const easeFn = resolveKeyEase(low);
14916
16560
  return lerpValue(low.value, high.value, easeFn(linearT), isColor);
14917
16561
  }
16562
+ function evaluatePathTrack(track, pct, prop = null) {
16563
+ if (!Array.isArray(track) || track.length === 0) return void 0;
16564
+ const sorted = [...track].sort((a2, b3) => a2.position - b3.position);
16565
+ const read = (key) => decodePathTrackValue(prop, key?.value) || void 0;
16566
+ if (sorted.length === 1 || pct <= sorted[0].position) return read(sorted[0]);
16567
+ if (pct >= sorted[sorted.length - 1].position) return read(sorted[sorted.length - 1]);
16568
+ let low = sorted[0];
16569
+ let high = sorted[1];
16570
+ for (let i = 0; i < sorted.length - 1; i += 1) {
16571
+ if (pct >= sorted[i].position && pct <= sorted[i + 1].position) {
16572
+ low = sorted[i];
16573
+ high = sorted[i + 1];
16574
+ break;
16575
+ }
16576
+ }
16577
+ const a = read(low);
16578
+ const b2 = read(high);
16579
+ if (!a) return b2;
16580
+ if (!b2) return a;
16581
+ const range = high.position - low.position;
16582
+ const linearT = range > 0 ? (pct - low.position) / range : 0;
16583
+ const easeFn = resolveKeyEase(low);
16584
+ return interpolatePathTrackValues(prop, a, b2, easeFn(linearT));
16585
+ }
16586
+ function interpolatePropertyKeyframes(propertyKeyframes, progress, transformOrigin = "center") {
16587
+ if (!propertyKeyframes || typeof propertyKeyframes !== "object") return null;
16588
+ const pct = progress * 100;
16589
+ const values = {};
16590
+ let any = false;
16591
+ for (const [prop, track] of Object.entries(propertyKeyframes)) {
16592
+ const v = isPathTrackKey(prop) ? evaluatePathTrack(track, pct, prop) : evaluatePropertyTrack(track, pct, isColorTrackKey(prop));
16593
+ if (v !== void 0) {
16594
+ values[prop] = v;
16595
+ any = true;
16596
+ }
16597
+ }
16598
+ if (!any) return null;
16599
+ values.transformOrigin = typeof transformOrigin === "string" && transformOrigin.trim() ? transformOrigin.trim() : "center";
16600
+ return values;
16601
+ }
14918
16602
  setKeyframeTrackSampler((track, pct) => evaluatePropertyTrack(track, pct, false));
16603
+ function getItemKeyframeValues(item, progress) {
16604
+ if (!item) return null;
16605
+ const values = interpolatePropertyKeyframes(item.propertyKeyframes, progress, item.keyframeTransformOrigin);
16606
+ if (!values || !hasAnyKeyframeFx(item.propertyKeyframeFx)) return values;
16607
+ const durationSec = getTimelineDuration(
16608
+ Math.max(0, (Number(item.trimEnd) || 0) - (Number(item.trimStart) || 0)),
16609
+ item.speedPoints
16610
+ );
16611
+ return applyKeyframeFxToValues(item, values, progress, durationSec);
16612
+ }
16613
+ var pathRateIntegralCache = /* @__PURE__ */ new Map();
16614
+ function integratePathRate(item, rateKey, staticValue, localTime, durationSec) {
16615
+ const track = item?.propertyKeyframes?.[rateKey];
16616
+ if (!Array.isArray(track) || track.length === 0 || !(durationSec > 0)) {
16617
+ return localTime * staticValue;
16618
+ }
16619
+ const cacheId = `${item.id}|${rateKey}`;
16620
+ const key = `${JSON.stringify(track)}:${Math.round(durationSec * 1e3)}`;
16621
+ let entry = pathRateIntegralCache.get(cacheId);
16622
+ if (!entry || entry.key !== key) {
16623
+ const steps = Math.max(8, Math.min(3600, Math.ceil(durationSec * 30)));
16624
+ const cum = new Float64Array(steps + 1);
16625
+ const dt = durationSec / steps;
16626
+ const valueAt = (progress) => {
16627
+ const v = getItemKeyframeValues(item, progress)[rateKey];
16628
+ return Number.isFinite(v) ? v : staticValue;
16629
+ };
16630
+ let prev = valueAt(0);
16631
+ for (let i2 = 1; i2 <= steps; i2 += 1) {
16632
+ const v = valueAt(i2 / steps);
16633
+ cum[i2] = cum[i2 - 1] + (prev + v) / 2 * dt;
16634
+ prev = v;
16635
+ }
16636
+ entry = { key, cum, steps };
16637
+ pathRateIntegralCache.set(cacheId, entry);
16638
+ if (pathRateIntegralCache.size > 64) {
16639
+ const first = pathRateIntegralCache.keys().next().value;
16640
+ if (first !== cacheId) pathRateIntegralCache.delete(first);
16641
+ }
16642
+ }
16643
+ const f2 = Math.max(0, Math.min(1, localTime / durationSec)) * entry.steps;
16644
+ const i = Math.min(Math.floor(f2), entry.steps - 1);
16645
+ return entry.cum[i] + (entry.cum[i + 1] - entry.cum[i]) * (f2 - i);
16646
+ }
16647
+ setPosterizeRateIntegrator(integratePathRate);
14919
16648
  function commonToPropertyKeyframes(keyframes, allowedProps = KF_PROP_LABELS) {
14920
16649
  const propertyKeyframes = {};
14921
16650
  const list = Array.isArray(keyframes) ? [...keyframes].sort((a, b2) => a.position - b2.position) : [];
@@ -15012,7 +16741,7 @@ function parseColorWithAlpha(value) {
15012
16741
  }
15013
16742
  }
15014
16743
  try {
15015
- const color = new THREE.Color(str);
16744
+ const color = new f(str);
15016
16745
  return { r: color.r, g: color.g, b: color.b, a: 1 };
15017
16746
  } catch {
15018
16747
  return null;
@@ -15050,7 +16779,7 @@ function directionsOf(preset) {
15050
16779
  return preset.directions === "diagonal" ? DIAGONAL : CARDINAL;
15051
16780
  }
15052
16781
  var transpose = ([[a, b2], [c, d]]) => [[a, c], [b2, d]];
15053
- var mul2 = ([[a, b2], [c, d]], [[e2, f], [g, h]]) => [[a * e2 + b2 * g, a * f + b2 * h], [c * e2 + d * g, c * f + d * h]];
16782
+ var mul2 = ([[a, b2], [c, d]], [[e2, f2], [g, h]]) => [[a * e2 + b2 * g, a * f2 + b2 * h], [c * e2 + d * g, c * f2 + d * h]];
15054
16783
  var det = ([[a, b2], [c, d]]) => a * d - b2 * c;
15055
16784
  function directionMap(baseDir, dir) {
15056
16785
  const base = FROM_CANONICAL[baseDir];
@@ -15058,7 +16787,7 @@ function directionMap(baseDir, dir) {
15058
16787
  if (!base || !target) return [[1, 0], [0, 1]];
15059
16788
  return mul2(target, transpose(base));
15060
16789
  }
15061
- var num2 = (v) => Number.parseFloat(v);
16790
+ var num5 = (v) => Number.parseFloat(v);
15062
16791
  var fmt = (v) => {
15063
16792
  const r = Math.round(v * 1e4) / 1e4;
15064
16793
  return String(Object.is(r, -0) ? 0 : r);
@@ -15071,13 +16800,13 @@ function mapProperties(props, M) {
15071
16800
  if (!hasX && !hasY) continue;
15072
16801
  delete out[px];
15073
16802
  delete out[py];
15074
- const x = hasX ? num2(props[px]) : 0;
15075
- const y = hasY ? num2(props[py]) : 0;
16803
+ const x = hasX ? num5(props[px]) : 0;
16804
+ const y = hasY ? num5(props[py]) : 0;
15076
16805
  const [[a, b2], [c, d]] = M;
15077
16806
  if (a !== 0 && hasX || b2 !== 0 && hasY) out[px] = fmt(a * x + b2 * y);
15078
16807
  if (c !== 0 && hasX || d !== 0 && hasY) out[py] = fmt(c * x + d * y);
15079
16808
  }
15080
- if (det(M) < 0 && props.rotate != null && props.rotate !== "") out.rotate = fmt(-num2(props.rotate));
16809
+ if (det(M) < 0 && props.rotate != null && props.rotate !== "") out.rotate = fmt(-num5(props.rotate));
15081
16810
  return out;
15082
16811
  }
15083
16812
  function mapFx(fx, M) {
@@ -15950,6 +17679,966 @@ function applyKeyframePreset(item, preset, { startPct = 0, endPct = 100 } = {})
15950
17679
  return out;
15951
17680
  }
15952
17681
 
17682
+ // src/components/videoEditor/cameraMotionPresets.js
17683
+ init_define_import_meta_env();
17684
+ var CAMERA_MOTION_NEUTRAL = {
17685
+ camZoom: 1,
17686
+ camX: 0,
17687
+ camY: 0,
17688
+ camRoll: 0,
17689
+ camDolly: 0,
17690
+ camTiltX: 0,
17691
+ camTiltY: 0
17692
+ };
17693
+ var DOF_DEFAULTS = {
17694
+ camDofEnabled: 0,
17695
+ camFocusDistance: 0,
17696
+ camFocusX: 50,
17697
+ camFocusY: 50,
17698
+ camAperture: 24,
17699
+ camBlurLevel: 100,
17700
+ camHighlightGain: 0,
17701
+ camFocusWidth: 0
17702
+ };
17703
+ var E = TransitionEasing;
17704
+ var RAW = [
17705
+ // ── Cinematic ──
17706
+ {
17707
+ id: "cm-quiet-push",
17708
+ category: "Cinematic",
17709
+ name: "Quiet Push",
17710
+ easeLabel: "AE Smooth",
17711
+ duration: 6,
17712
+ description: "A restrained forward creep \u2014 attention without announcing itself",
17713
+ k: [
17714
+ { p: 0, e: E.AE_SMOOTH, v: { camZoom: 1.02, camY: 1 } },
17715
+ { p: 100, v: { camZoom: 1.16, camDolly: 80, camY: 0 } }
17716
+ ]
17717
+ },
17718
+ {
17719
+ id: "cm-swoop-dive",
17720
+ category: "Cinematic",
17721
+ name: "Swoop Dive",
17722
+ easeLabel: "Expo Out",
17723
+ duration: 4.5,
17724
+ description: "A drone dive from high and behind that levels off at the subject",
17725
+ k: [
17726
+ { p: 0, e: E.EXPO_OUT, v: { camY: 9, camTiltX: -14, camZoom: 1.35, camDolly: -120, camRoll: -3 } },
17727
+ { p: 100, v: { camY: -3, camTiltX: 3, camZoom: 1.1, camDolly: 160, camRoll: 0 } }
17728
+ ]
17729
+ },
17730
+ {
17731
+ id: "cm-corkscrew",
17732
+ category: "Cinematic",
17733
+ name: "Corkscrew",
17734
+ easeLabel: "AE Smooth",
17735
+ duration: 5,
17736
+ description: "Screws in from a canted wide \u2014 roll, yaw and zoom resolve together",
17737
+ k: [
17738
+ { p: 0, e: E.AE_SMOOTH, v: { camRoll: -22, camTiltY: -10, camZoom: 1.6, camX: -5, camY: -3 } },
17739
+ { p: 100, v: { camRoll: 0, camTiltY: 4, camZoom: 1.08, camX: 1, camY: 0 } }
17740
+ ]
17741
+ },
17742
+ {
17743
+ id: "cm-fast-orbit",
17744
+ category: "Cinematic",
17745
+ name: "Fast Orbit",
17746
+ easeLabel: "Linear",
17747
+ duration: 3,
17748
+ description: "A brisk constant-speed yaw sweep with real angular strength",
17749
+ k: [
17750
+ { p: 0, e: E.LINEAR, v: { camTiltY: -20, camX: -8, camZoom: 1.12 } },
17751
+ { p: 100, v: { camTiltY: 20, camX: 8, camZoom: 1.12 } }
17752
+ ]
17753
+ },
17754
+ // Ported from the retired popover catalog (CAMERA_PRESETS 'lateral-glide').
17755
+ {
17756
+ id: "cm-lateral-glide",
17757
+ category: "Cinematic",
17758
+ name: "Lateral Glide",
17759
+ easeLabel: "Smooth",
17760
+ duration: 6,
17761
+ description: "A polished slider move with just enough yaw to reveal depth",
17762
+ k: [
17763
+ { p: 0, e: E.EASE_IN_OUT, v: { camX: -7, camY: 0.8, camTiltY: -1.8, camZoom: 1.08 } },
17764
+ { p: 50, e: E.EASE_IN_OUT, v: { camX: 0, camY: 0, camTiltY: 0, camZoom: 1.1 } },
17765
+ { p: 100, v: { camX: 7, camY: -0.8, camTiltY: 1.8, camZoom: 1.12 } }
17766
+ ]
17767
+ },
17768
+ // Ported from the retired popover catalog (CAMERA_PRESETS 'parallax-arc').
17769
+ {
17770
+ id: "cm-parallax-arc",
17771
+ category: "Cinematic",
17772
+ name: "Parallax Arc",
17773
+ easeLabel: "Smooth",
17774
+ duration: 7,
17775
+ description: "An arcing move that lets foreground and background separate",
17776
+ k: [
17777
+ { p: 0, e: E.EASE_IN_OUT, v: { camX: -8, camTiltY: -4.5, camDolly: -80, camZoom: 1.08 } },
17778
+ { p: 50, e: E.EASE_IN_OUT, v: { camX: 0, camTiltY: 0, camDolly: 170, camZoom: 1.13 } },
17779
+ { p: 100, v: { camX: 8, camTiltY: 4.5, camDolly: -80, camZoom: 1.08 } }
17780
+ ]
17781
+ },
17782
+ // Ported from the Mockup builder's Arc Close-Up (mockupGenerator/PresetsPanel.jsx).
17783
+ {
17784
+ id: "cm-arc-closeup",
17785
+ category: "Cinematic",
17786
+ name: "Arc Close-Up",
17787
+ easeLabel: "Cubic Out",
17788
+ duration: 6,
17789
+ description: "Sweeps in on an arc and lands in a crisp close detail",
17790
+ k: [
17791
+ { p: 0, e: E.CUBIC_OUT, v: { camX: -11, camY: 2, camTiltY: -9, camDolly: -140, camZoom: 0.96 } },
17792
+ { p: 30, e: E.EASE_IN_OUT, v: { camX: -4, camY: 1, camTiltY: -4, camDolly: 40, camZoom: 1.06 } },
17793
+ { p: 65, e: E.EASE_IN_OUT, v: { camX: 5, camY: -1, camTiltY: 2, camDolly: 230, camZoom: 1.26 } },
17794
+ { p: 100, v: { camX: 1.5, camY: 0, camTiltY: 3, camDolly: 160, camZoom: 1.16 } }
17795
+ ]
17796
+ },
17797
+ // ── Editorial ──
17798
+ {
17799
+ id: "cm-slingshot",
17800
+ category: "Editorial",
17801
+ name: "Slingshot",
17802
+ easeLabel: "Impulse",
17803
+ duration: 1.8,
17804
+ description: "Pulls back to load, then launches forward \u2014 one Impulse segment",
17805
+ k: [
17806
+ { p: 0, e: E.IMPULSE, v: { camZoom: 1.05 } },
17807
+ { p: 100, v: { camZoom: 1.75, camDolly: 260, camY: -1 } }
17808
+ ]
17809
+ },
17810
+ {
17811
+ id: "cm-step-zoom",
17812
+ category: "Editorial",
17813
+ name: "Step Zoom",
17814
+ easeLabel: "Hold (cuts)",
17815
+ duration: 2.4,
17816
+ description: "Three hard cuts closer \u2014 hold keys, no interpolation at all",
17817
+ k: [
17818
+ { p: 0, e: E.HOLD, v: {} },
17819
+ { p: 25, e: E.HOLD, v: { camZoom: 1.16, camX: 2 } },
17820
+ { p: 50, e: E.HOLD, v: { camZoom: 1.32, camX: -1.5 } },
17821
+ { p: 75, e: E.HOLD, v: { camZoom: 1.5, camX: 0 } },
17822
+ { p: 100, v: {} }
17823
+ ]
17824
+ },
17825
+ {
17826
+ id: "cm-handheld-breath",
17827
+ category: "Editorial",
17828
+ name: "Handheld Breath",
17829
+ easeLabel: "Sine",
17830
+ duration: 8,
17831
+ description: "Small, irregular operator drift without the synthetic wobble",
17832
+ k: [
17833
+ { p: 0, e: E.SINE_IN_OUT, v: { camX: -0.4, camY: 0.4, camRoll: -0.15, camZoom: 1.05 } },
17834
+ { p: 19, e: E.SINE_IN_OUT, v: { camX: 1.1, camY: -0.6, camRoll: 0.3, camZoom: 1.062 } },
17835
+ { p: 43, e: E.SINE_IN_OUT, v: { camX: -0.8, camY: 0.9, camRoll: -0.25, camZoom: 1.054 } },
17836
+ { p: 68, e: E.SINE_IN_OUT, v: { camX: 0.6, camY: 0.2, camRoll: 0.2, camZoom: 1.068 } },
17837
+ { p: 87, e: E.SINE_IN_OUT, v: { camX: -0.9, camY: -0.5, camRoll: -0.22, camZoom: 1.058 } },
17838
+ { p: 100, v: { camX: -0.4, camY: 0.4, camRoll: -0.15, camZoom: 1.05 } }
17839
+ ]
17840
+ },
17841
+ // Ported from the Mockup builder's Soft Land.
17842
+ {
17843
+ id: "cm-soft-land",
17844
+ category: "Editorial",
17845
+ name: "Soft Land",
17846
+ easeLabel: "Expo Out",
17847
+ duration: 4.2,
17848
+ description: "Drops in from above and settles onto the framing without a bump",
17849
+ k: [
17850
+ { p: 0, e: E.EXPO_OUT, v: { camY: 9, camTiltX: -8, camDolly: -130, camZoom: 0.94, camRoll: -2 } },
17851
+ { p: 60, e: E.EASE_IN_OUT, v: { camY: -0.6, camTiltX: 1.2, camDolly: 30, camZoom: 1.04, camRoll: 0.4 } },
17852
+ { p: 100, v: { camY: 0, camTiltX: 0.6, camDolly: 0, camZoom: 1.01, camRoll: 0 } }
17853
+ ]
17854
+ },
17855
+ // Ported from the retired popover catalog (CAMERA_PRESETS 'documentary-follow').
17856
+ {
17857
+ id: "cm-documentary-follow",
17858
+ category: "Editorial",
17859
+ name: "Documentary Follow",
17860
+ easeLabel: "Smooth",
17861
+ duration: 5,
17862
+ description: "A gently imperfect follow move that keeps finding the subject",
17863
+ k: [
17864
+ { p: 0, e: E.EASE_IN_OUT, v: { camX: -3, camY: 1.5, camRoll: -0.15, camZoom: 1.06 } },
17865
+ { p: 38, e: E.EASE_IN_OUT, v: { camX: 1, camY: -0.5, camRoll: 0.25, camZoom: 1.1 } },
17866
+ { p: 72, e: E.EASE_IN_OUT, v: { camX: -0.5, camY: 0.8, camRoll: -0.1, camZoom: 1.08 } },
17867
+ { p: 100, v: { camX: 2.5, camY: 0, camRoll: 0.1, camZoom: 1.12 } }
17868
+ ]
17869
+ },
17870
+ // ── Energy ──
17871
+ {
17872
+ id: "cm-weave-run",
17873
+ category: "Energy",
17874
+ name: "Weave Run",
17875
+ easeLabel: "Sine",
17876
+ duration: 5.5,
17877
+ description: "A forward run that slaloms through the frame, banking into each turn",
17878
+ k: [
17879
+ { p: 0, e: E.SINE_IN_OUT, v: { camX: -8, camRoll: -3, camDolly: -150, camZoom: 1.1, camTiltY: -4 } },
17880
+ { p: 30, e: E.SINE_IN_OUT, v: { camX: 6, camRoll: 3, camDolly: 0, camTiltY: 3 } },
17881
+ { p: 60, e: E.SINE_IN_OUT, v: { camX: -5, camRoll: -2.5, camDolly: 140, camTiltY: -3 } },
17882
+ { p: 100, v: { camX: 7, camRoll: 2, camDolly: 300, camTiltY: 4 } }
17883
+ ]
17884
+ },
17885
+ // Ported from the Mockup builder's S-Curve Drift.
17886
+ {
17887
+ id: "cm-s-curve-drift",
17888
+ category: "Energy",
17889
+ name: "S-Curve Drift",
17890
+ easeLabel: "Smooth",
17891
+ duration: 6.5,
17892
+ description: "An elegant S-shaped path that keeps changing direction mid-drift",
17893
+ k: [
17894
+ { p: 0, e: E.EASE_IN_OUT, v: { camX: -10, camY: 3, camTiltY: -6, camDolly: -90, camZoom: 1.05, camRoll: -1 } },
17895
+ { p: 33, e: E.EASE_IN_OUT, v: { camX: 4, camY: -1.5, camTiltY: 1, camDolly: 60, camZoom: 1.12, camRoll: 0.5 } },
17896
+ { p: 66, e: E.EASE_IN_OUT, v: { camX: -3, camY: 1, camTiltY: -2, camDolly: 160, camZoom: 1.18, camRoll: -0.5 } },
17897
+ { p: 100, v: { camX: 10, camY: -2, camTiltY: 5, camDolly: 90, camZoom: 1.1, camRoll: 1 } }
17898
+ ]
17899
+ },
17900
+ // Ported from the Mockup builder's Macro Sweep.
17901
+ {
17902
+ id: "cm-macro-sweep",
17903
+ category: "Energy",
17904
+ name: "Macro Sweep",
17905
+ easeLabel: "Smooth",
17906
+ duration: 5,
17907
+ description: "Stays tight on the subject while sweeping across it side to side",
17908
+ k: [
17909
+ { p: 0, e: E.EASE_IN_OUT, v: { camZoom: 1.5, camX: -8, camTiltY: -5, camY: 1 } },
17910
+ { p: 50, e: E.EASE_IN_OUT, v: { camZoom: 1.62, camX: 2, camTiltY: 1, camY: -0.5 } },
17911
+ { p: 100, v: { camZoom: 1.46, camX: 9, camTiltY: 5, camY: 1 } }
17912
+ ]
17913
+ },
17914
+ // Authored by hand in the graph editor (the "8.8 Preview" project) and lifted from
17915
+ // it verbatim. Pure DOLLY, no zoom: the viewpoint really travels, so the parallax
17916
+ // opens up instead of the lens just magnifying. Two very different segments —
17917
+ // 0–71 % is a LINEAR creep of ~137 px that reads as almost nothing, then one
17918
+ // hand-shaped segment holds and detonates (dolly ~490 at its midpoint, ~1420 at
17919
+ // 90 %, 2600 at the end). 2600 is past the comp plane of a 1080-tall project
17920
+ // (perspectiveDistance ≈ 2166), so the last instant flies THROUGH the frame —
17921
+ // that blow-out is the point of the move, not a bug. Park layers on negative panZ
17922
+ // if you want the camera to stop short of them instead. `sparse` keeps it to the
17923
+ // ONE dolly track — no constant zoom/pan/roll/tilt tracks to wade through in the
17924
+ // keyframe builder, and it layers onto a pose you have already framed.
17925
+ {
17926
+ id: "cm-punch-through",
17927
+ category: "Energy",
17928
+ name: "Punch Through",
17929
+ easeLabel: "Custom",
17930
+ duration: 8.7,
17931
+ sparse: true,
17932
+ description: "A long, almost invisible creep forward that detonates into a flight straight through the frame",
17933
+ k: [
17934
+ { p: 0, e: E.LINEAR, v: { camDolly: 98 } },
17935
+ { p: 71, e: E.CUSTOM, b: [0.8187, 0.1079, 0.9089, 0.302], v: { camDolly: 234.7692 } },
17936
+ { p: 100, e: E.AE_IN, v: { camDolly: 2600 } }
17937
+ ]
17938
+ },
17939
+ // ── Loops (first and last snapshots match, so trimming loops cleanly) ──
17940
+ {
17941
+ id: "cm-orbit-pulse",
17942
+ category: "Loops",
17943
+ name: "Orbit Pulse",
17944
+ easeLabel: "Sine",
17945
+ duration: 7,
17946
+ description: "A seamless orbit that breathes closer on every pass",
17947
+ k: [
17948
+ { p: 0, e: E.SINE_IN_OUT, v: { camTiltY: -13, camX: -5, camY: 1, camZoom: 1.06 } },
17949
+ { p: 50, e: E.SINE_IN_OUT, v: { camTiltY: 13, camX: 5, camY: -1, camZoom: 1.2 } },
17950
+ { p: 100, v: { camTiltY: -13, camX: -5, camY: 1, camZoom: 1.06 } }
17951
+ ]
17952
+ },
17953
+ // Ported from the Mockup builder's Float & Rotate.
17954
+ {
17955
+ id: "cm-float",
17956
+ category: "Loops",
17957
+ name: "Float",
17958
+ easeLabel: "Sine",
17959
+ duration: 5.5,
17960
+ description: "A weightless vertical bob with a slow roll \u2014 ambient and seamless",
17961
+ k: [
17962
+ { p: 0, e: E.SINE_IN_OUT, v: { camY: 2.2, camRoll: -1.6, camZoom: 1.1 } },
17963
+ { p: 50, e: E.SINE_IN_OUT, v: { camY: -2.2, camRoll: 1.6, camZoom: 1.17 } },
17964
+ { p: 100, v: { camY: 2.2, camRoll: -1.6, camZoom: 1.1 } }
17965
+ ]
17966
+ },
17967
+ {
17968
+ id: "cm-figure-eight",
17969
+ category: "Loops",
17970
+ name: "Figure Eight",
17971
+ easeLabel: "Sine",
17972
+ duration: 6,
17973
+ description: "Traces a loop through the frame and returns exactly to its start",
17974
+ k: [
17975
+ { p: 0, e: E.SINE_IN_OUT, v: { camX: -11, camY: 0, camRoll: 0, camZoom: 1.15 } },
17976
+ { p: 25, e: E.SINE_IN_OUT, v: { camX: 0, camY: 7, camRoll: 2.5 } },
17977
+ { p: 50, e: E.SINE_IN_OUT, v: { camX: 11, camY: 0, camRoll: 0 } },
17978
+ { p: 75, e: E.SINE_IN_OUT, v: { camX: 0, camY: -7, camRoll: -2.5 } },
17979
+ { p: 100, v: { camX: -11, camY: 0, camRoll: 0 } }
17980
+ ]
17981
+ },
17982
+ // ── Focus (ported from the retired popover catalog; DOF tracks, so they only
17983
+ // read with the camera's Depth of Field enabled — they enable it themselves) ──
17984
+ {
17985
+ id: "cm-rack-to-near",
17986
+ category: "Focus",
17987
+ name: "Rack to Near",
17988
+ easeLabel: "Smooth",
17989
+ duration: 3.5,
17990
+ description: "Holds the background, then pulls focus onto a near subject",
17991
+ k: [
17992
+ { p: 0, e: E.EASE_IN_OUT, v: { camDofEnabled: 1, camFocusDistance: 2800, camFocusX: 68, camFocusY: 48, camAperture: 36, camBlurLevel: 110, camHighlightGain: 18 } },
17993
+ { p: 22, e: E.EASE_IN_OUT, v: {} },
17994
+ { p: 78, e: E.EASE_IN_OUT, v: { camFocusDistance: 1350, camFocusX: 35, camFocusY: 55 } },
17995
+ { p: 100, v: {} }
17996
+ ]
17997
+ },
17998
+ {
17999
+ id: "cm-rack-to-far",
18000
+ category: "Focus",
18001
+ name: "Rack to Far",
18002
+ easeLabel: "Smooth",
18003
+ duration: 3.5,
18004
+ description: "Transfers attention from a close detail to the scene behind it",
18005
+ k: [
18006
+ { p: 0, e: E.EASE_IN_OUT, v: { camDofEnabled: 1, camFocusDistance: 1350, camFocusX: 35, camFocusY: 55, camAperture: 36, camBlurLevel: 110, camHighlightGain: 18 } },
18007
+ { p: 22, e: E.EASE_IN_OUT, v: {} },
18008
+ { p: 78, e: E.EASE_IN_OUT, v: { camFocusDistance: 2800, camFocusX: 68, camFocusY: 48 } },
18009
+ { p: 100, v: {} }
18010
+ ]
18011
+ },
18012
+ {
18013
+ id: "cm-focus-chase",
18014
+ category: "Focus",
18015
+ name: "Focus Chase",
18016
+ easeLabel: "Smooth",
18017
+ duration: 6,
18018
+ description: "Moves attention across three depth beats for layered layouts",
18019
+ k: [
18020
+ { p: 0, e: E.EASE_IN_OUT, v: { camDofEnabled: 1, camFocusDistance: 1450, camFocusX: 28, camFocusY: 55, camAperture: 30, camBlurLevel: 95, camHighlightGain: 12 } },
18021
+ { p: 45, e: E.EASE_IN_OUT, v: { camFocusDistance: 2600, camFocusX: 72, camFocusY: 42 } },
18022
+ { p: 100, v: { camFocusDistance: 1750, camFocusX: 48, camFocusY: 50 } }
18023
+ ]
18024
+ },
18025
+ {
18026
+ id: "cm-macro-bloom",
18027
+ category: "Focus",
18028
+ name: "Macro Bloom",
18029
+ easeLabel: "Smooth",
18030
+ duration: 4.5,
18031
+ description: "Pushes into a detail while focus and highlight bloom tighten",
18032
+ k: [
18033
+ { p: 0, e: E.EASE_IN_OUT, v: { camZoom: 1.05, camDolly: 0, camDofEnabled: 1, camFocusDistance: 2500, camFocusX: 50, camFocusY: 50, camAperture: 28, camBlurLevel: 100, camHighlightGain: 15 } },
18034
+ { p: 52, e: E.EASE_IN_OUT, v: { camZoom: 1.16, camDolly: 100, camFocusDistance: 1650, camAperture: 46, camBlurLevel: 125, camHighlightGain: 32 } },
18035
+ { p: 100, v: { camZoom: 1.23, camDolly: 180, camFocusDistance: 1150, camAperture: 38, camBlurLevel: 110, camHighlightGain: 22 } }
18036
+ ]
18037
+ },
18038
+ // Deliberately LAST in the wall (user's call) rather than up with the other
18039
+ // Cinematic moves — array order is card order, in "All" and inside a category chip
18040
+ // alike. The reveal itself is an ENDING beat: hold the close-up, swing open in the
18041
+ // last stretch.
18042
+ {
18043
+ id: "cm-grand-reveal",
18044
+ category: "Cinematic",
18045
+ name: "Grand Reveal",
18046
+ easeLabel: "Smooth",
18047
+ duration: 3.5,
18048
+ description: "Holds an extreme tilted close-up, then swings open right at the end",
18049
+ k: [
18050
+ { p: 0, e: E.LINEAR, v: { camZoom: 2.3, camRoll: -5, camY: -4, camDolly: 220 } },
18051
+ { p: 55, e: E.EASE_IN_OUT, v: {} },
18052
+ { p: 100, v: { camZoom: 1, camRoll: 0, camY: 0, camDolly: 0 } }
18053
+ ]
18054
+ }
18055
+ ];
18056
+ var CAMERA_MOTION_PRESETS = RAW.map(({ k, sparse, ...meta }) => {
18057
+ const used = new Set(k.flatMap((key) => Object.keys(key.v)));
18058
+ const usesMotion = !sparse && Object.keys(CAMERA_MOTION_NEUTRAL).some((prop) => used.has(prop));
18059
+ let prev = usesMotion ? { ...CAMERA_MOTION_NEUTRAL } : {};
18060
+ for (const prop of Object.keys(DOF_DEFAULTS)) {
18061
+ if (used.has(prop)) prev[prop] = DOF_DEFAULTS[prop];
18062
+ }
18063
+ const keyframes = k.map((key) => {
18064
+ const pose = { ...prev, ...key.v };
18065
+ prev = pose;
18066
+ const properties = {};
18067
+ for (const [prop, value] of Object.entries(pose)) properties[prop] = String(value);
18068
+ return {
18069
+ position: key.p,
18070
+ easing: key.e || TransitionEasing.EASE_IN_OUT,
18071
+ ...key.b ? { bezier: [...key.b] } : {},
18072
+ properties
18073
+ };
18074
+ });
18075
+ return { ...meta, keyframes };
18076
+ });
18077
+
18078
+ // src/components/videoEditor/cameraRig.js
18079
+ init_define_import_meta_env();
18080
+ var D2R = Math.PI / 180;
18081
+ var CAMERA_RIG_TYPES = ["orbit", "dolly", "crane", "pushIn", "truck", "spiral", "handheld", "follow"];
18082
+ var CAMERA_RIG_PARAM_META = {
18083
+ radius: { label: "Radius", min: 100, max: 2e4, step: 10, suffix: "px" },
18084
+ height: { label: "Height", min: -8e3, max: 8e3, step: 10, suffix: "px" },
18085
+ startAngle: { label: "Start", min: -360, max: 360, step: 1, suffix: "\xB0" },
18086
+ sweep: { label: "Sweep", min: -720, max: 720, step: 1, suffix: "\xB0" },
18087
+ rise: { label: "Rise", min: -8e3, max: 8e3, step: 10, suffix: "px" },
18088
+ distance: { label: "Travel", min: -2e4, max: 2e4, step: 10, suffix: "px" },
18089
+ turns: { label: "Turns", min: 0.25, max: 8, step: 0.25, suffix: "\xD7" },
18090
+ tilt: { label: "Tilt", min: -80, max: 80, step: 1, suffix: "\xB0" },
18091
+ roll: { label: "Roll", min: -180, max: 180, step: 1, suffix: "\xB0" },
18092
+ amplitude: { label: "Amount", min: 0, max: 2e3, step: 5, suffix: "px" },
18093
+ frequency: { label: "Rate", min: 0, max: 40, step: 0.5, suffix: "\xD7" }
18094
+ };
18095
+ var CAMERA_RIG_AIMS = ["center", "path"];
18096
+ var CAMERA_RIG_DEFAULTS = Object.freeze({
18097
+ type: "orbit",
18098
+ aim: "center",
18099
+ radius: 1800,
18100
+ height: 400,
18101
+ startAngle: -40,
18102
+ sweep: 90,
18103
+ rise: 0,
18104
+ tilt: -8,
18105
+ roll: 0,
18106
+ distance: 1200,
18107
+ turns: 1,
18108
+ amplitude: 0,
18109
+ frequency: 0,
18110
+ seed: 7
18111
+ });
18112
+ function num6(value, fallback, min, max) {
18113
+ const n2 = typeof value === "number" ? value : Number(value);
18114
+ if (!Number.isFinite(n2)) return fallback;
18115
+ if (Number.isFinite(min) && n2 < min) return min;
18116
+ if (Number.isFinite(max) && n2 > max) return max;
18117
+ return n2;
18118
+ }
18119
+ function normalizeCameraRig(raw) {
18120
+ const source = raw && typeof raw === "object" ? raw : {};
18121
+ const D = CAMERA_RIG_DEFAULTS;
18122
+ const out = { type: CAMERA_RIG_TYPES.includes(source.type) ? source.type : D.type };
18123
+ out.aim = CAMERA_RIG_AIMS.includes(source.aim) ? source.aim : D.aim;
18124
+ for (const [key, meta] of Object.entries(CAMERA_RIG_PARAM_META)) {
18125
+ out[key] = num6(source[key], D[key], meta.min, meta.max);
18126
+ }
18127
+ out.seed = Math.round(num6(source.seed, D.seed, 0, 1e5));
18128
+ out.targets = Array.isArray(source.targets) ? source.targets.filter((id) => typeof id === "string" && id).slice(0, 50) : [];
18129
+ return out;
18130
+ }
18131
+ function noiseCell(seed, cell) {
18132
+ const s = Math.sin(cell * 127.1 + seed * 311.7) * 43758.5453123;
18133
+ return (s - Math.floor(s)) * 2 - 1;
18134
+ }
18135
+ function smoothNoise2(seed, x) {
18136
+ const cell = Math.floor(x);
18137
+ const f2 = x - cell;
18138
+ const eased = f2 * f2 * f2 * (f2 * (f2 * 6 - 15) + 10);
18139
+ return noiseCell(seed, cell) * (1 - eased) + noiseCell(seed, cell + 1) * eased;
18140
+ }
18141
+ var drift = (seed, x) => smoothNoise2(seed, x) * 0.65 + smoothNoise2(seed + 19, x * 2.13) * 0.35;
18142
+ function evalCameraRig(rig, u, ctx = null) {
18143
+ const r = normalizeCameraRig(rig);
18144
+ const t = Math.max(0, Math.min(1, num6(u, 0)));
18145
+ const c = ctx?.center ? { x: num6(ctx.center.x, 0), y: num6(ctx.center.y, 0), z: num6(ctx.center.z, 0) } : { x: 0, y: 0, z: 0 };
18146
+ const pos = rigPosition(r, t, c);
18147
+ const lift = -r.height * (r.tilt / 45);
18148
+ if (r.aim === "path") {
18149
+ const dir = rigTravelDirection(r, t, c);
18150
+ if (dir) {
18151
+ const reach = r.radius;
18152
+ return {
18153
+ pos,
18154
+ aim: { x: pos.x + dir.x * reach, y: pos.y + dir.y * reach + lift, z: pos.z + dir.z * reach },
18155
+ roll: r.roll
18156
+ };
18157
+ }
18158
+ }
18159
+ return { pos, aim: { x: c.x, y: c.y + lift, z: c.z }, roll: r.roll };
18160
+ }
18161
+ function rigTravelDirection(r, t, c) {
18162
+ for (const h of [1e-3, 0.01, 0.05]) {
18163
+ const a = rigPosition(r, t - h, c);
18164
+ const b2 = rigPosition(r, t + h, c);
18165
+ const dx = b2.x - a.x;
18166
+ const dy = b2.y - a.y;
18167
+ const dz = b2.z - a.z;
18168
+ const len = Math.hypot(dx, dy, dz);
18169
+ if (len > 1e-3) return { x: dx / len, y: dy / len, z: dz / len };
18170
+ }
18171
+ return null;
18172
+ }
18173
+ function rigPosition(r, t, c) {
18174
+ const ang = (a) => a * D2R;
18175
+ let pos;
18176
+ switch (r.type) {
18177
+ case "follow": {
18178
+ const a = ang(r.startAngle + r.sweep * t);
18179
+ pos = { x: c.x + Math.sin(a) * r.radius, y: c.y - r.height, z: c.z + Math.cos(a) * r.radius };
18180
+ break;
18181
+ }
18182
+ case "dolly": {
18183
+ const a = ang(r.startAngle);
18184
+ const d = r.radius + r.distance * (1 - t);
18185
+ pos = { x: Math.sin(a) * d, y: -r.height, z: Math.cos(a) * d };
18186
+ break;
18187
+ }
18188
+ case "pushIn": {
18189
+ const a = ang(r.startAngle);
18190
+ const e2 = 1 - (1 - t) ** 3;
18191
+ const d = r.radius + r.distance * (1 - e2);
18192
+ pos = { x: Math.sin(a) * d, y: -r.height, z: Math.cos(a) * d };
18193
+ break;
18194
+ }
18195
+ case "truck": {
18196
+ const a = ang(r.startAngle);
18197
+ const off = r.distance * (t - 0.5);
18198
+ pos = {
18199
+ x: Math.sin(a) * r.radius + Math.cos(a) * off,
18200
+ y: -r.height,
18201
+ z: Math.cos(a) * r.radius - Math.sin(a) * off
18202
+ };
18203
+ break;
18204
+ }
18205
+ case "crane": {
18206
+ const a = ang(r.startAngle + r.sweep * t);
18207
+ pos = { x: Math.sin(a) * r.radius, y: -(r.height + r.rise * t), z: Math.cos(a) * r.radius };
18208
+ break;
18209
+ }
18210
+ case "spiral": {
18211
+ const a = ang(r.startAngle + r.sweep * r.turns * t);
18212
+ const rad = r.radius * (1 - 0.35 * t);
18213
+ pos = { x: Math.sin(a) * rad, y: -(r.height + r.rise * t), z: Math.cos(a) * rad };
18214
+ break;
18215
+ }
18216
+ case "handheld": {
18217
+ const a = ang(r.startAngle);
18218
+ const p = t * Math.max(0, r.frequency);
18219
+ pos = {
18220
+ x: Math.sin(a) * r.radius + drift(r.seed, p) * r.amplitude,
18221
+ y: -r.height + drift(r.seed + 51, p) * r.amplitude * 0.7,
18222
+ z: Math.cos(a) * r.radius + drift(r.seed + 97, p) * r.amplitude * 0.5
18223
+ };
18224
+ break;
18225
+ }
18226
+ case "orbit":
18227
+ default: {
18228
+ const a = ang(r.startAngle + r.sweep * t);
18229
+ pos = { x: Math.sin(a) * r.radius, y: -r.height, z: Math.cos(a) * r.radius };
18230
+ break;
18231
+ }
18232
+ }
18233
+ return pos;
18234
+ }
18235
+ var CAMERA_RIG_PRESETS = [
18236
+ { id: "heroOrbit", name: "Hero Orbit", rig: { type: "orbit", radius: 1800, height: 400, startAngle: -40, sweep: 90, tilt: -8 } },
18237
+ { id: "slowReveal", name: "Slow Reveal", rig: { type: "orbit", radius: 2400, height: 220, startAngle: -120, sweep: 150, tilt: -4 } },
18238
+ { id: "turntable", name: "Turntable", rig: { type: "orbit", radius: 1500, height: 300, startAngle: 0, sweep: 360, tilt: -6 } },
18239
+ { id: "craneDown", name: "Crane Down", rig: { type: "crane", radius: 1700, height: 1400, startAngle: -30, sweep: 55, rise: -1100, tilt: 14 } },
18240
+ { id: "riseReveal", name: "Rise & Reveal", rig: { type: "crane", radius: 1900, height: 150, startAngle: -60, sweep: 80, rise: 1200, tilt: -16 } },
18241
+ { id: "pushIn", name: "Push In", rig: { type: "pushIn", radius: 900, height: 260, startAngle: 0, distance: 1800, tilt: -3 } },
18242
+ { id: "pullBack", name: "Pull Back", rig: { type: "dolly", radius: 2600, height: 380, startAngle: 12, distance: -1700, tilt: -6 } },
18243
+ { id: "truckLeft", name: "Truck Left", rig: { type: "truck", radius: 1600, height: 300, startAngle: 0, distance: 2200, tilt: -4 } },
18244
+ // Retuned: the spiral pulls IN as it climbs (`radius * (1 - 0.35 * t)`), so the
18245
+ // start radius has to leave room for that. At 2200 the move ended 1430 out and
18246
+ // 1140 up and a normal product left the top of frame. 2800 lands the tightest
18247
+ // moment at ~0.79 of frame height with the climb intact. Framing is a joint
18248
+ // property of radius + rise + tilt here — retuning one alone just moves the clip.
18249
+ { id: "productSpiral", name: "Product Spiral", rig: { type: "spiral", radius: 2800, height: 240, startAngle: -20, sweep: 300, turns: 1, rise: 800, tilt: -16 } },
18250
+ { id: "handheldDrift", name: "Handheld Drift", rig: { type: "handheld", radius: 1500, height: 320, startAngle: -8, amplitude: 90, frequency: 1.5, tilt: -5 } },
18251
+ // Follow presets carry NO targets — which layer to ride is project wiring, not
18252
+ // part of a shot. The panel preserves the current list when a preset is applied.
18253
+ { id: "droneFollow", name: "Drone Follow", rig: { type: "follow", radius: 1600, height: 420, startAngle: -35, sweep: 110, tilt: -7 } },
18254
+ // A rear three-quarter chase: low, tight, and barely swinging — reads as a
18255
+ // camera operator running behind the subject rather than a crane circling it.
18256
+ { id: "chaseCam", name: "Chase Cam", rig: { type: "follow", radius: 950, height: 170, startAngle: 160, sweep: 35, tilt: -4 } }
18257
+ ];
18258
+ function seedCameraRigProgress(propertyKeyframes) {
18259
+ const existing = propertyKeyframes?.camPathT;
18260
+ if (Array.isArray(existing) && existing.length > 0) return null;
18261
+ return {
18262
+ ...propertyKeyframes || {},
18263
+ // `position`, NOT `time`, and `'Linear'`, NOT `'linear'`.
18264
+ //
18265
+ // A keyframe in this project is `{ position, value, easing }` with position in
18266
+ // 0-100 percent of the clip. Written as `time` both keys parsed to position 0,
18267
+ // the de-duplicator then collapsed them into one, and the result was a rig with
18268
+ // a single 0 % key — visibly "the rig only made one keyframe", and a camera that
18269
+ // never moved because progress never advanced. The easing string has to be a
18270
+ // TransitionEasing VALUE ('Linear'), not its key name, or the sanitizer silently
18271
+ // substitutes the default. Linear like every Path Progress track
18272
+ // (objectMotionPath.js PATH_PROGRESS_EASING — not imported, this is a leaf).
18273
+ camPathT: [
18274
+ { position: 0, value: 0, easing: "Linear" },
18275
+ { position: 100, value: 100, easing: "Linear" }
18276
+ ]
18277
+ };
18278
+ }
18279
+
18280
+ // src/components/videoEditor/projectCamera.js
18281
+ init_define_import_meta_env();
18282
+
18283
+ // src/components/videoEditor/keyframeParenting.js
18284
+ init_define_import_meta_env();
18285
+ var PARENTING_ADDITIVE = "additive";
18286
+ var PARENTING_FULL_COPY = "full-copy";
18287
+ var PARENTING_TYPE_LABELS = {
18288
+ [PARENTING_ADDITIVE]: "Additive",
18289
+ [PARENTING_FULL_COPY]: "Full Copy"
18290
+ };
18291
+ var PRS_KF_PROPS = [
18292
+ "translateX",
18293
+ "translateY",
18294
+ "translateZ",
18295
+ "xPercent",
18296
+ "yPercent",
18297
+ "rotate",
18298
+ "rotateX",
18299
+ "rotateY",
18300
+ "scale",
18301
+ "scaleX",
18302
+ "scaleY",
18303
+ // Path Progress belongs here too: a live motion path is the final authority on
18304
+ // local position (see localPrs), so animating it on a layer whose PRS is owned
18305
+ // by something else does nothing at all. Its offsets are the same story — they
18306
+ // only displace the position the path just claimed.
18307
+ "motionPathT",
18308
+ "motionPathOffsetX",
18309
+ "motionPathOffsetY",
18310
+ "motionPathOffsetZ",
18311
+ // The animated path itself (objectMotionPath.js MOTION_PATH_KF_KEY), same reason.
18312
+ "motionPathPoints"
18313
+ ];
18314
+ var PRS_KF_PROP_SET = new Set(PRS_KF_PROPS);
18315
+ var DEG2RAD2 = Math.PI / 180;
18316
+ var RAD2DEG = 180 / Math.PI;
18317
+
18318
+ // src/components/videoEditor/smartZoomCamera.js
18319
+ init_define_import_meta_env();
18320
+
18321
+ // src/components/videoEditor/smartZoom.js
18322
+ init_define_import_meta_env();
18323
+ var PRESETS2 = [
18324
+ {
18325
+ id: "studio",
18326
+ hint: "Smooth spring zoom toward your clicks, like Screen Studio.",
18327
+ short: "Like Screen Studio",
18328
+ name: "Studio",
18329
+ note: "Matches Screen Studio as measured: spring zoom, aims part-way to the clicks, the cursor trails your hand a little.",
18330
+ camera: { preset: "studio", stiffness: 170, damping: 50, mass: 3, zoom: 1.45, motion: "independent", zoomSpace: "linear", follow: "clicks", boxRatio: 0.7, aim: 0.6, retargetLeadMs: 1e3, holdMinMs: 1500, rampMs: 40, edgeSnap: 0, preRollMs: 0 },
18331
+ cursor: { smoothing: "spring", preset: "studio", strength: 1, splineTolPx: 2.5, landMs: 120, shakePx: 3, size: 1.5, clickEffect: "press", tilt: 0, hideIdleMs: 0 }
18332
+ },
18333
+ {
18334
+ id: "calm",
18335
+ hint: "A slower, gentler zoom that keeps the clicked spot in place.",
18336
+ short: "Slow, gentle zoom",
18337
+ name: "Calm",
18338
+ note: "Slow, gentle and less zoom. The clicked spot stays put while the camera zooms. The cursor glides with no lag and fades when idle.",
18339
+ camera: { preset: "calm", stiffness: 110, damping: 46, mass: 3, zoom: 1.35, motion: "aboutPoint", zoomSpace: "linear", follow: "clicks", boxRatio: 0.7, aim: 0.5, retargetLeadMs: 1200, holdMinMs: 1800, rampMs: 80, edgeSnap: 0, preRollMs: 0 },
18340
+ cursor: { smoothing: "zerolag", preset: "soft", strength: 0.8, splineTolPx: 2.5, landMs: 160, shakePx: 3, size: 1.4, clickEffect: "press", tilt: 0, hideIdleMs: 2500 }
18341
+ },
18342
+ {
18343
+ id: "precise",
18344
+ hint: "A closer zoom that centres what you click.",
18345
+ short: "Closer, clicks centred",
18346
+ name: "Precise",
18347
+ note: "Closer zoom that puts the clicks near the middle. The cursor follows your real path exactly, minus the shake. A ring shows each click.",
18348
+ camera: { preset: "smooth", stiffness: 200, damping: 49, mass: 3, zoom: 1.6, motion: "aboutPoint", zoomSpace: "linear", follow: "clicks", boxRatio: 0.7, aim: 0.85, retargetLeadMs: 800, holdMinMs: 1500, rampMs: 40, edgeSnap: 0, preRollMs: 0 },
18349
+ cursor: { smoothing: "spline", preset: "studio", strength: 1, splineTolPx: 2, landMs: 80, shakePx: 3, size: 1.3, clickEffect: "ring", tilt: 0, hideIdleMs: 0 }
18350
+ },
18351
+ {
18352
+ id: "lively",
18353
+ hint: "A fast, punchy zoom that goes in further.",
18354
+ short: "Quick, punchy zoom",
18355
+ name: "Lively",
18356
+ note: "Quick, punchy zooms that go in further. The cursor is tight and tilts as it moves, for short social clips.",
18357
+ camera: { preset: "snappy", stiffness: 340, damping: 66, mass: 3, zoom: 1.7, motion: "independent", zoomSpace: "log", follow: "clicks", boxRatio: 0.7, aim: 0.75, retargetLeadMs: 600, holdMinMs: 1200, rampMs: 20, edgeSnap: 0, preRollMs: 0 },
18358
+ cursor: { smoothing: "spring", preset: "tight", strength: 1, splineTolPx: 2.5, landMs: 100, shakePx: 3, size: 1.6, clickEffect: "press", tilt: 10, hideIdleMs: 0 }
18359
+ },
18360
+ {
18361
+ id: "follow",
18362
+ hint: "While zoomed in, the camera follows your cursor.",
18363
+ short: "Follows the cursor",
18364
+ name: "Follow",
18365
+ note: "While zoomed in, the camera follows the cursor, moving only when it leaves a box around the view, not just on clicks. The cursor has no lag.",
18366
+ camera: { preset: "smooth", stiffness: 200, damping: 49, mass: 3, zoom: 1.5, motion: "independent", zoomSpace: "linear", follow: "cursor", boxRatio: 0.6, aim: 0.8, retargetLeadMs: 300, holdMinMs: 1500, rampMs: 60, edgeSnap: 0, preRollMs: 0 },
18367
+ cursor: { smoothing: "zerolag", preset: "studio", strength: 1, splineTolPx: 2.5, landMs: 120, shakePx: 3, size: 1.5, clickEffect: "press", tilt: 0, hideIdleMs: 0 }
18368
+ }
18369
+ ];
18370
+
18371
+ // src/components/videoEditor/smartZoomCamera.js
18372
+ var ZOOM_RANGE_MIN_MS = 400;
18373
+ var ZOOM_MIN = 1.05;
18374
+ var ZOOM_MAX = 4;
18375
+ var clamp4 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
18376
+ var r4 = (v) => Math.round(v * 1e4) / 1e4;
18377
+ function sanitizeZoomRanges(list) {
18378
+ if (!Array.isArray(list)) return void 0;
18379
+ const out = [];
18380
+ const ids = /* @__PURE__ */ new Set();
18381
+ for (const r of list.slice(0, 200)) {
18382
+ if (!r || typeof r !== "object") continue;
18383
+ const s = Number(r.startMs);
18384
+ const e2 = Number(r.endMs);
18385
+ if (!Number.isFinite(s) || !Number.isFinite(e2) || e2 - s < 100) continue;
18386
+ let id = typeof r.id === "string" && r.id ? r.id.slice(0, 40) : `z${out.length + 1}`;
18387
+ while (ids.has(id)) id = `${id}x`;
18388
+ ids.add(id);
18389
+ const z = Number(r.zoom);
18390
+ const p = r.point && Number.isFinite(Number(r.point.x)) && Number.isFinite(Number(r.point.y)) ? { x: r4(clamp4(Number(r.point.x), 0, 1)), y: r4(clamp4(Number(r.point.y), 0, 1)) } : null;
18391
+ out.push({
18392
+ id,
18393
+ startMs: Math.max(0, Math.round(s)),
18394
+ endMs: Math.round(e2),
18395
+ ...Number.isFinite(z) && z >= 1 ? { zoom: Math.round(clamp4(z, 1, ZOOM_MAX) * 1e3) / 1e3 } : {},
18396
+ mode: r.mode === "fixed" ? "fixed" : "follow",
18397
+ ...p ? { point: p } : {},
18398
+ ...r.disabled ? { disabled: true } : {}
18399
+ });
18400
+ }
18401
+ return out.sort((a, b2) => a.startMs - b2.startMs);
18402
+ }
18403
+
18404
+ // src/components/videoEditor/projectCamera.js
18405
+ var CAMERA_FOV = 28;
18406
+ var D2R2 = Math.PI / 180;
18407
+ function perspectiveDistance(height, fovDeg = CAMERA_FOV) {
18408
+ return Math.max(1, height) * 0.5 / Math.tan(fovDeg * Math.PI / 180 * 0.5);
18409
+ }
18410
+ var CAMERA_KF_PROPS = [
18411
+ // Zoom is deliberately uncapped upward — extreme push-ins are a valid look;
18412
+ // only the positive floor is structural (fovEff needs zoom > 0).
18413
+ { label: "camZoom", displayLabel: "Zoom", group: "Motion", placeholder: "1", unit: "\xD7", type: "number", min: 0.1, step: 0.01 },
18414
+ { label: "camX", displayLabel: "Pan X", group: "Motion", placeholder: "0", unit: "%", type: "number", min: -200, max: 200, step: 0.5 },
18415
+ { label: "camY", displayLabel: "Pan Y", group: "Motion", placeholder: "0", unit: "%", type: "number", min: -200, max: 200, step: 0.5 },
18416
+ { label: "camRoll", displayLabel: "Roll", group: "Motion", placeholder: "0", unit: "deg", type: "number", min: -180, max: 180, step: 0.5 },
18417
+ { label: "camDolly", displayLabel: "Dolly", group: "Motion", placeholder: "0", unit: "px", type: "number", min: -3e3, max: 3e3, step: 10 },
18418
+ { label: "camTiltX", displayLabel: "Tilt X", group: "Motion", placeholder: "0", unit: "deg", type: "number", min: -60, max: 60, step: 0.5 },
18419
+ { label: "camTiltY", displayLabel: "Tilt Y", group: "Motion", placeholder: "0", unit: "deg", type: "number", min: -60, max: 60, step: 0.5 },
18420
+ { label: "camDofEnabled", displayLabel: "Depth of Field", group: "Depth of Field", placeholder: "0", unit: "", type: "number", min: 0, max: 1, step: 1 },
18421
+ { label: "camFocusDistance", displayLabel: "Focus Distance", group: "Depth of Field", placeholder: "0", unit: "px", type: "number", min: 0, max: 2e4, step: 10 },
18422
+ { label: "camAperture", displayLabel: "Aperture", group: "Depth of Field", placeholder: "24", unit: "px", type: "number", min: 0, max: 300, step: 1 },
18423
+ { label: "camBlurLevel", displayLabel: "Blur Level", group: "Depth of Field", placeholder: "100", unit: "%", type: "number", min: 0, max: 300, step: 1 },
18424
+ { label: "camHighlightGain", displayLabel: "Highlight Gain", group: "Depth of Field", placeholder: "0", unit: "%", type: "number", min: 0, max: 300, step: 1 },
18425
+ { label: "camFocusX", displayLabel: "Focus Point X", group: "Depth of Field", placeholder: "50", unit: "%", type: "number", min: 0, max: 100, step: 1 },
18426
+ { label: "camFocusY", displayLabel: "Focus Point Y", group: "Depth of Field", placeholder: "50", unit: "%", type: "number", min: 0, max: 100, step: 1 },
18427
+ // 0 = the focus point is only a depth probe (pure per-layer DOF). Above 0 it also
18428
+ // becomes a SHARP REGION of the frame: blur is held off inside the area and ramps
18429
+ // to the layer's full circle of confusion by its edge, so a subject can stay crisp
18430
+ // while the rest of its own layer falls away.
18431
+ { label: "camFocusWidth", displayLabel: "Focus Area Width", group: "Depth of Field", placeholder: "0", unit: "%", type: "number", min: 0, max: 200, step: 1 },
18432
+ // How far along the camera path the camera has travelled. Only does anything
18433
+ // while the clip has a path with `follow` on — see resolveCameraPath below. It
18434
+ // lives in the registry so the keyframe builder and the timeline diamonds pick
18435
+ // it up like any other track.
18436
+ { label: "camPathT", displayLabel: "Path Progress", group: "Motion", placeholder: "0", unit: "%", type: "number", min: 0, max: 100, step: 0.5 },
18437
+ // A constant displacement from the path, in WORLD axes (+X right, +Y down,
18438
+ // +Z toward the viewer — the panel's own signs). The aim is computed AFTER it,
18439
+ // so a camera pushed off a shared path still looks at its target.
18440
+ { label: "camPathOffsetX", displayLabel: "Path Offset X", group: "Motion", placeholder: "0", unit: "px", type: "number", min: -1e4, max: 1e4, step: 5 },
18441
+ { label: "camPathOffsetY", displayLabel: "Path Offset Y", group: "Motion", placeholder: "0", unit: "px", type: "number", min: -1e4, max: 1e4, step: 5 },
18442
+ { label: "camPathOffsetZ", displayLabel: "Path Offset Z", group: "Motion", placeholder: "0", unit: "px", type: "number", min: -1e4, max: 1e4, step: 5 },
18443
+ // Which entry of the Follow Object rig's target list the camera looks at,
18444
+ // 1-based. Fractional values BLEND the two neighbouring targets' positions, so
18445
+ // an eased key from 1 to 2 pans the look-at across smoothly — that blend IS the
18446
+ // hand-off mechanism, not an interpolation artefact to be rounded away.
18447
+ { label: "camFollowTarget", displayLabel: "Follow Object", group: "Motion", placeholder: "1", unit: "", type: "number", min: 1, step: 1 }
18448
+ ];
18449
+ var CAMERA_KF_LABELS = CAMERA_KF_PROPS.map((p) => p.label);
18450
+ function cameraMoveHash(propertyKeyframes) {
18451
+ const easings = new Set(Object.values(TransitionEasing));
18452
+ const parts = [];
18453
+ for (const prop of CAMERA_KF_LABELS) {
18454
+ const track = propertyKeyframes?.[prop];
18455
+ if (!Array.isArray(track) || track.length === 0) continue;
18456
+ const seen = /* @__PURE__ */ new Set();
18457
+ const keys = track.filter((k) => k && Number.isFinite(Number.parseFloat(k.position))).map((k) => {
18458
+ const easing = easings.has(k.easing) ? k.easing : TransitionEasing.EASE_IN_OUT;
18459
+ const bezier = easing === TransitionEasing.CUSTOM && Array.isArray(k.bezier) ? k.bezier.map((n2) => Math.round(Number(n2) * 1e4) / 1e4).join(",") : "";
18460
+ return [Math.round(Number.parseFloat(k.position) * 1e3) / 1e3, String(k.value ?? ""), easing, bezier];
18461
+ }).sort((a, b2) => a[0] - b2[0]).filter((k) => seen.has(k[0]) ? false : (seen.add(k[0]), true));
18462
+ parts.push(`${prop}=${keys.map((k) => k.join("|")).join(";")}`);
18463
+ }
18464
+ const str = parts.join("&");
18465
+ let h = 2166136261;
18466
+ for (let i = 0; i < str.length; i += 1) {
18467
+ h ^= str.charCodeAt(i);
18468
+ h = Math.imul(h, 16777619);
18469
+ }
18470
+ return (h >>> 0).toString(36);
18471
+ }
18472
+ var _pfMat = new f();
18473
+ var _pfVec = new f();
18474
+ var _pfPos = new f();
18475
+ var _pfQuat = new f();
18476
+ var _pfScale = new f(1, 1, 1);
18477
+ var CAMERA_PRESETS = [
18478
+ {
18479
+ id: "quiet-push",
18480
+ category: "Cinematic",
18481
+ name: "Quiet Push",
18482
+ description: "A restrained forward creep that adds attention without announcing itself",
18483
+ duration: 6,
18484
+ keyframes: [
18485
+ { position: 0, easing: TransitionEasing.EASE_IN_OUT, properties: { camZoom: "1.02", camDolly: "0", camY: "1" } },
18486
+ { position: 100, easing: TransitionEasing.EASE_IN_OUT, properties: { camZoom: "1.16", camDolly: "80", camY: "0" } }
18487
+ ]
18488
+ },
18489
+ {
18490
+ id: "wide-reveal",
18491
+ category: "Cinematic",
18492
+ name: "Wide Reveal",
18493
+ description: "Begins close and slightly low, then opens cleanly onto the full composition",
18494
+ duration: 5.5,
18495
+ keyframes: [
18496
+ { position: 0, easing: TransitionEasing.EASE_IN_OUT, properties: { camZoom: "1.36", camY: "-5", camTiltX: "2.5", camDolly: "120" } },
18497
+ { position: 100, easing: TransitionEasing.EASE_IN_OUT, properties: { camZoom: "1", camY: "0", camTiltX: "0", camDolly: "0" } }
18498
+ ]
18499
+ },
18500
+ {
18501
+ id: "lateral-glide",
18502
+ category: "Cinematic",
18503
+ name: "Lateral Glide",
18504
+ description: "A polished slider move with just enough yaw to reveal depth between layers",
18505
+ duration: 6,
18506
+ keyframes: [
18507
+ { position: 0, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "-7", camY: "0.8", camTiltY: "-1.8", camZoom: "1.08" } },
18508
+ { position: 50, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "0", camY: "0", camTiltY: "0", camZoom: "1.1" } },
18509
+ { position: 100, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "7", camY: "-0.8", camTiltY: "1.8", camZoom: "1.12" } }
18510
+ ]
18511
+ },
18512
+ {
18513
+ id: "parallax-arc",
18514
+ category: "Cinematic",
18515
+ name: "Parallax Arc",
18516
+ description: "An arcing move that lets foreground and background layers separate naturally",
18517
+ duration: 7,
18518
+ keyframes: [
18519
+ { position: 0, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "-8", camTiltY: "-4.5", camDolly: "-80", camZoom: "1.08" } },
18520
+ { position: 50, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "0", camTiltY: "0", camDolly: "170", camZoom: "1.13" } },
18521
+ { position: 100, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "8", camTiltY: "4.5", camDolly: "-80", camZoom: "1.08" } }
18522
+ ]
18523
+ },
18524
+ {
18525
+ id: "crane-rise",
18526
+ category: "Cinematic",
18527
+ name: "Crane Rise",
18528
+ description: "Rises through the composition while easing into a wider, level view",
18529
+ duration: 6,
18530
+ keyframes: [
18531
+ { position: 0, easing: TransitionEasing.EASE_IN_OUT, properties: { camY: "-7", camTiltX: "4", camZoom: "1.13", camDolly: "0" } },
18532
+ { position: 100, easing: TransitionEasing.EASE_IN_OUT, properties: { camY: "5", camTiltX: "-2", camZoom: "1.06", camDolly: "120" } }
18533
+ ]
18534
+ },
18535
+ {
18536
+ id: "soft-punch",
18537
+ category: "Editorial",
18538
+ name: "Soft Punch",
18539
+ description: "A quick emphasis beat with a controlled overshoot and a clean settle",
18540
+ duration: 1.6,
18541
+ keyframes: [
18542
+ { position: 0, easing: TransitionEasing.EASE_IN_OUT, properties: { camZoom: "1", camX: "0", camY: "0" } },
18543
+ { position: 35, easing: TransitionEasing.EASE_IN_OUT, properties: { camZoom: "1.2", camX: "-1", camY: "0.5" } },
18544
+ { position: 62, easing: TransitionEasing.EASE_IN_OUT, properties: { camZoom: "1.145", camX: "0", camY: "0" } },
18545
+ { position: 100, easing: TransitionEasing.EASE_IN_OUT, properties: { camZoom: "1.16", camX: "0", camY: "0" } }
18546
+ ]
18547
+ },
18548
+ {
18549
+ id: "whip-and-land",
18550
+ category: "Editorial",
18551
+ name: "Whip & Land",
18552
+ description: "A real reframing whip that accelerates late and settles on a new composition",
18553
+ duration: 1.15,
18554
+ keyframes: [
18555
+ { position: 0, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "0", camRoll: "0", camZoom: "1.08" } },
18556
+ { position: 24, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "0", camRoll: "0", camZoom: "1.08" } },
18557
+ { position: 63, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "49", camRoll: "2", camZoom: "1.13" } },
18558
+ { position: 80, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "42", camRoll: "0", camZoom: "1.1" } },
18559
+ { position: 100, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "42", camRoll: "0", camZoom: "1.1" } }
18560
+ ]
18561
+ },
18562
+ {
18563
+ id: "handheld-breath",
18564
+ category: "Editorial",
18565
+ name: "Handheld Breath",
18566
+ description: "Small, irregular operator drift without the synthetic wobble",
18567
+ duration: 8,
18568
+ keyframes: [
18569
+ { position: 0, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "-0.2", camY: "0.2", camRoll: "-0.08", camZoom: "1.04" } },
18570
+ { position: 19, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "0.65", camY: "-0.35", camRoll: "0.16", camZoom: "1.048" } },
18571
+ { position: 43, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "-0.45", camY: "0.5", camRoll: "-0.13", camZoom: "1.042" } },
18572
+ { position: 68, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "0.35", camY: "0.12", camRoll: "0.1", camZoom: "1.052" } },
18573
+ { position: 87, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "-0.55", camY: "-0.28", camRoll: "-0.12", camZoom: "1.046" } },
18574
+ { position: 100, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "-0.2", camY: "0.2", camRoll: "-0.08", camZoom: "1.04" } }
18575
+ ]
18576
+ },
18577
+ {
18578
+ id: "documentary-follow",
18579
+ category: "Editorial",
18580
+ name: "Documentary Follow",
18581
+ description: "A gently imperfect follow move that keeps finding the subject",
18582
+ duration: 5,
18583
+ keyframes: [
18584
+ { position: 0, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "-3", camY: "1.5", camRoll: "-0.15", camZoom: "1.06" } },
18585
+ { position: 38, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "1", camY: "-0.5", camRoll: "0.25", camZoom: "1.1" } },
18586
+ { position: 72, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "-0.5", camY: "0.8", camRoll: "-0.1", camZoom: "1.08" } },
18587
+ { position: 100, easing: TransitionEasing.EASE_IN_OUT, properties: { camX: "2.5", camY: "0", camRoll: "0.1", camZoom: "1.12" } }
18588
+ ]
18589
+ },
18590
+ {
18591
+ id: "rack-to-near",
18592
+ category: "Focus",
18593
+ name: "Rack to Near",
18594
+ description: "Holds the background, then pulls focus decisively onto a near subject",
18595
+ duration: 3.5,
18596
+ keyframes: [
18597
+ { position: 0, easing: TransitionEasing.EASE_IN_OUT, properties: { camDofEnabled: "1", camFocusDistance: "2800", camFocusX: "68", camFocusY: "48", camAperture: "36", camBlurLevel: "110", camHighlightGain: "18" } },
18598
+ { position: 22, easing: TransitionEasing.EASE_IN_OUT, properties: { camDofEnabled: "1", camFocusDistance: "2800", camFocusX: "68", camFocusY: "48", camAperture: "36", camBlurLevel: "110", camHighlightGain: "18" } },
18599
+ { position: 78, easing: TransitionEasing.EASE_IN_OUT, properties: { camDofEnabled: "1", camFocusDistance: "1350", camFocusX: "35", camFocusY: "55", camAperture: "36", camBlurLevel: "110", camHighlightGain: "18" } },
18600
+ { position: 100, easing: TransitionEasing.EASE_IN_OUT, properties: { camDofEnabled: "1", camFocusDistance: "1350", camFocusX: "35", camFocusY: "55", camAperture: "36", camBlurLevel: "110", camHighlightGain: "18" } }
18601
+ ]
18602
+ },
18603
+ {
18604
+ id: "rack-to-far",
18605
+ category: "Focus",
18606
+ name: "Rack to Far",
18607
+ description: "Transfers attention from a close detail to the wider scene behind it",
18608
+ duration: 3.5,
18609
+ keyframes: [
18610
+ { position: 0, easing: TransitionEasing.EASE_IN_OUT, properties: { camDofEnabled: "1", camFocusDistance: "1350", camFocusX: "35", camFocusY: "55", camAperture: "36", camBlurLevel: "110", camHighlightGain: "18" } },
18611
+ { position: 22, easing: TransitionEasing.EASE_IN_OUT, properties: { camDofEnabled: "1", camFocusDistance: "1350", camFocusX: "35", camFocusY: "55", camAperture: "36", camBlurLevel: "110", camHighlightGain: "18" } },
18612
+ { position: 78, easing: TransitionEasing.EASE_IN_OUT, properties: { camDofEnabled: "1", camFocusDistance: "2800", camFocusX: "68", camFocusY: "48", camAperture: "36", camBlurLevel: "110", camHighlightGain: "18" } },
18613
+ { position: 100, easing: TransitionEasing.EASE_IN_OUT, properties: { camDofEnabled: "1", camFocusDistance: "2800", camFocusX: "68", camFocusY: "48", camAperture: "36", camBlurLevel: "110", camHighlightGain: "18" } }
18614
+ ]
18615
+ },
18616
+ {
18617
+ id: "focus-chase",
18618
+ category: "Focus",
18619
+ name: "Focus Chase",
18620
+ description: "Moves attention across three depth beats for layered product or title layouts",
18621
+ duration: 6,
18622
+ keyframes: [
18623
+ { position: 0, easing: TransitionEasing.EASE_IN_OUT, properties: { camDofEnabled: "1", camFocusDistance: "1450", camFocusX: "28", camFocusY: "55", camAperture: "30", camBlurLevel: "95", camHighlightGain: "12" } },
18624
+ { position: 45, easing: TransitionEasing.EASE_IN_OUT, properties: { camDofEnabled: "1", camFocusDistance: "2600", camFocusX: "72", camFocusY: "42", camAperture: "30", camBlurLevel: "95", camHighlightGain: "12" } },
18625
+ { position: 100, easing: TransitionEasing.EASE_IN_OUT, properties: { camDofEnabled: "1", camFocusDistance: "1750", camFocusX: "48", camFocusY: "50", camAperture: "30", camBlurLevel: "95", camHighlightGain: "12" } }
18626
+ ]
18627
+ },
18628
+ {
18629
+ id: "macro-bloom",
18630
+ category: "Focus",
18631
+ name: "Macro Bloom",
18632
+ description: "Pushes into a detail while focus and highlight bloom tighten around it",
18633
+ duration: 4.5,
18634
+ keyframes: [
18635
+ { position: 0, easing: TransitionEasing.EASE_IN_OUT, properties: { camZoom: "1.05", camDolly: "0", camDofEnabled: "1", camFocusDistance: "2500", camFocusX: "50", camFocusY: "50", camAperture: "28", camBlurLevel: "100", camHighlightGain: "15" } },
18636
+ { position: 52, easing: TransitionEasing.EASE_IN_OUT, properties: { camZoom: "1.16", camDolly: "100", camDofEnabled: "1", camFocusDistance: "1650", camFocusX: "50", camFocusY: "50", camAperture: "46", camBlurLevel: "125", camHighlightGain: "32" } },
18637
+ { position: 100, easing: TransitionEasing.EASE_IN_OUT, properties: { camZoom: "1.23", camDolly: "180", camDofEnabled: "1", camFocusDistance: "1150", camFocusX: "50", camFocusY: "50", camAperture: "38", camBlurLevel: "110", camHighlightGain: "22" } }
18638
+ ]
18639
+ }
18640
+ ];
18641
+
15953
18642
  // src/motionEngine/mediaProbe.js
15954
18643
  init_define_import_meta_env();
15955
18644
  var LIMITS = { image: 20 * 1024 * 1024, video: 60 * 1024 * 1024, audio: 30 * 1024 * 1024 };
@@ -16161,16 +18850,54 @@ var SHADERS = [
16161
18850
  "windowhaze"
16162
18851
  ];
16163
18852
  var CAMERAS = ["none", "push-in", "pull-out", "drift-left", "drift-right", "drift-up"];
18853
+ var MOVES = new Map(CAMERA_MOTION_PRESETS.map((p) => [p.id, p]));
18854
+ var RIGS = new Map(CAMERA_RIG_PRESETS.map((p) => [p.id, p]));
18855
+ var FEELS = PRESETS2.map((p) => p.id);
18856
+ var CAM_TRACKS = { zoom: "camZoom", x: "camX", y: "camY", roll: "camRoll", dolly: "camDolly", tiltX: "camTiltX", tiltY: "camTiltY", focusZ: "camFocusDistance" };
18857
+ var CAM_DRIVEN = { rig: ["x", "y", "roll", "dolly", "tiltX", "tiltY"], smartZoom: ["zoom", "x", "y"] };
18858
+ var CAM_FIELDS = ["move", "rig", "zooms", "feel", "keyframes", "dof", "start", "end"];
18859
+ var RIG_FIELDS = ["preset", "type", "aim", "targets", "seed", ...Object.keys(CAMERA_RIG_PARAM_META)];
18860
+ function cameraCatalog() {
18861
+ return {
18862
+ moves: CAMERA_MOTION_PRESETS.map((p) => `\`${p.id}\` ${p.name}${p.category ? ` (${p.category})` : ""}`),
18863
+ rigs: CAMERA_RIG_PRESETS.map((p) => `\`${p.id}\` ${p.name} (${p.rig.type})`),
18864
+ feels: PRESETS2.map((p) => `\`${p.id}\` ${p.name}`),
18865
+ rigFields: Object.entries(CAMERA_RIG_PARAM_META).map(([k, m]) => `${k} ${m.min}\u2013${m.max}`),
18866
+ rigTypes: CAMERA_RIG_TYPES,
18867
+ rigAims: CAMERA_RIG_AIMS
18868
+ };
18869
+ }
16164
18870
  var FONT = "'Helvetica Neue', Helvetica, Arial, sans-serif";
16165
18871
  var TEXT = new Map([...PRESETS, ...CLASSIC_PRESETS].map((p) => [p.id, p]));
16166
18872
  var TEXT_RENAMED = { "glide-soft": "glide-continuous" };
18873
+ var rgbOf = (c) => {
18874
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(c || "").trim());
18875
+ if (!m) return null;
18876
+ const h = m[1].length === 3 ? m[1].split("").map((x) => x + x).join("") : m[1];
18877
+ return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
18878
+ };
18879
+ var vivid = (c) => {
18880
+ const v = rgbOf(c);
18881
+ return !!v && Math.max(...v) - Math.min(...v) >= 40;
18882
+ };
18883
+ var luminance = (c) => {
18884
+ const v = rgbOf(c);
18885
+ if (!v) return null;
18886
+ const [r, g, b2] = v.map((x) => x / 255 <= 0.03928 ? x / 255 / 12.92 : ((x / 255 + 0.055) / 1.055) ** 2.4);
18887
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b2;
18888
+ };
18889
+ var contrast = (a, b2) => {
18890
+ const la = luminance(a), lb = luminance(b2);
18891
+ return la == null || lb == null ? 21 : (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
18892
+ };
18893
+ var EDGE_FILLED = /* @__PURE__ */ new Set(["Advanced Zoom", "Advanced Movement"]);
16167
18894
  var KF_IN = new Map(kfpVariants(KEYFRAME_PRESETS, "in").map((p) => [p.id, p]));
16168
18895
  var KF_OUT = new Map(kfpVariants(KEYFRAME_PRESETS, "out").map((p) => [p.id, p]));
16169
18896
  var ROLES = { headline: [0.11, 800], subhead: [0.05, 500], body: [0.036, 400], label: [0.028, 600] };
16170
- var clamp2 = (v, a, b2) => Math.max(a, Math.min(b2, v));
16171
- var num3 = (v, d) => Number.isFinite(Number(v)) ? Number(v) : d;
18897
+ var clamp5 = (v, a, b2) => Math.max(a, Math.min(b2, v));
18898
+ var num7 = (v, d) => Number.isFinite(Number(v)) ? Number(v) : d;
16172
18899
  var r3 = (v) => +(+v).toFixed(3);
16173
- var KF_TRACKS = { x: "translateX", y: "translateY", scale: "scale", rotate: "rotate", opacity: "opacity" };
18900
+ var KF_TRACKS = { x: "translateX", y: "translateY", z: "translateZ", scale: "scale", rotate: "rotate", opacity: "opacity" };
16174
18901
  function scriptSources(script) {
16175
18902
  const out = /* @__PURE__ */ new Set();
16176
18903
  for (const sc of Array.isArray(script?.scenes) ? script.scenes : []) {
@@ -16220,16 +18947,16 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16220
18947
  };
16221
18948
  const easings = new Set(caps?.easings?.keyframe?.values || []);
16222
18949
  const fileFor = (src, where, want) => {
16223
- const f = files.get(String(src));
16224
- if (!f) {
18950
+ const f2 = files.get(String(src));
18951
+ if (!f2) {
16225
18952
  problems.push(`${where}: no file "${String(src).slice(0, 80)}" \u2014 a src is an https link, or asset:<file name> for the user's own file (they add it on the page); for art you draw use "svg"`);
16226
18953
  return null;
16227
18954
  }
16228
- if (want && !want.includes(f.kind)) {
16229
- problems.push(`${where}: "${f.name || src}" is ${f.kind === "audio" ? "a sound" : `a ${f.kind}`}, not ${want.join(" or ")}`);
18955
+ if (want && !want.includes(f2.kind)) {
18956
+ problems.push(`${where}: "${f2.name || src}" is ${f2.kind === "audio" ? "a sound" : `a ${f2.kind}`}, not ${want.join(" or ")}`);
16230
18957
  return null;
16231
18958
  }
16232
- return f;
18959
+ return f2;
16233
18960
  };
16234
18961
  const background = (b2, where) => {
16235
18962
  if (b2 == null) return null;
@@ -16243,7 +18970,7 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16243
18970
  problems.push(`${where}: unknown shader "${b2.shader}" (one of ${SHADERS.join(", ")})`);
16244
18971
  return null;
16245
18972
  }
16246
- return { type: "shader", shaderId: b2.shader, ...b2.look ? { shaderLook: String(b2.look) } : {}, shaderSpeed: clamp2(num3(b2.speed, 1), 0, 4), solidColor: b2.color || "#000000" };
18973
+ return { type: "shader", shaderId: b2.shader, ...b2.look ? { shaderLook: String(b2.look) } : {}, shaderSpeed: clamp5(num7(b2.speed, 1), 0, 4), solidColor: b2.color || "#000000" };
16247
18974
  }
16248
18975
  if (b2 && typeof b2 === "object" && b2.gradient) return { type: "gradient", gradientCss: String(b2.gradient), gradientAnimated: !!b2.animated, solidColor: "#000000" };
16249
18976
  problems.push(`${where}: background must be a colour, a CSS gradient, or { "shader": id, "look"? }`);
@@ -16251,7 +18978,7 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16251
18978
  };
16252
18979
  const projectBg = background(style.background ?? "#0f1112", "style") || { type: "solid", solidColor: "#0f1112" };
16253
18980
  const at = (pos) => {
16254
- if (Array.isArray(pos)) return [num3(pos[0], 0), num3(pos[1], 0)];
18981
+ if (Array.isArray(pos)) return [num7(pos[0], 0), num7(pos[1], 0)];
16255
18982
  const m = {
16256
18983
  center: [0, 0],
16257
18984
  top: [0, -0.3],
@@ -16265,6 +18992,176 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16265
18992
  }[pos || "center"];
16266
18993
  return m ? [m[0] * W, m[1] * H] : [0, 0];
16267
18994
  };
18995
+ const P = perspectiveDistance(H);
18996
+ const rigSamples = (rig, n2 = 12) => Array.from({ length: n2 + 1 }, (_, i) => ({ u: i / n2, ...evalCameraRig(rig, i / n2) }));
18997
+ const focusFollow = (fz, rig, dolly) => {
18998
+ if (rig && rig.type !== "follow") {
18999
+ return rigSamples(rig).map(({ u, pos, aim }) => {
19000
+ const dx = aim.x - pos.x;
19001
+ const dy = aim.y - pos.y;
19002
+ const dz = aim.z - pos.z;
19003
+ const L = Math.hypot(dx, dy, dz) || 1;
19004
+ const depth = (-pos.x * dx - pos.y * dy + (fz - pos.z) * dz) / L;
19005
+ return { position: r3(u * 100), value: String(Math.round(Math.max(1, depth))), easing: "Linear" };
19006
+ });
19007
+ }
19008
+ if (!rig && Array.isArray(dolly) && dolly.some((k) => Math.abs(Number(k.value) || 0) > 0.5)) {
19009
+ return dolly.map((k) => ({ position: k.position, value: String(Math.round(Math.max(1, P - (Number(k.value) || 0) - fz))), easing: k.easing, ...k.bezier ? { bezier: [...k.bezier] } : {} }));
19010
+ }
19011
+ return null;
19012
+ };
19013
+ const sceneCamera = (c, cw, D, seq, sceneId, elIds, zOf) => {
19014
+ for (const k of Object.keys(c)) if (!CAM_FIELDS.includes(k)) problems.push(`${cw}: unknown field "${k}" (${CAM_FIELDS.join(", ")})`);
19015
+ const kinds = ["move", "rig", "zooms"].filter((k) => c[k] != null);
19016
+ if (kinds.length > 1) problems.push(`${cw}: one of move, rig or zooms \u2014 not ${kinds.join(" and ")}`);
19017
+ if (!kinds.length && c.keyframes == null && c.dof == null) problems.push(`${cw}: a camera needs move, rig, zooms, keyframes or dof`);
19018
+ const start = clamp5(num7(c.start, 0), 0, D - 0.2);
19019
+ const end = clamp5(num7(c.end, D), start + 0.2, D);
19020
+ const len = end - start;
19021
+ const id = uid(`${sceneId}-camera`);
19022
+ const item = { id, clipId: `c-${id}`, freeStart: r3(start), trimStart: 0, trimEnd: r3(len) };
19023
+ let pk = {};
19024
+ let mode = "free";
19025
+ if (c.move != null) {
19026
+ const p = MOVES.get(c.move);
19027
+ if (!p) problems.push(`${cw}: unknown move "${c.move}"${near(c.move, [...MOVES.keys()])}`);
19028
+ else {
19029
+ pk = commonToPropertyKeyframes(p.keyframes, CAMERA_KF_LABELS).propertyKeyframes;
19030
+ item.cameraPreset = { id: p.id, name: p.name, hash: cameraMoveHash(pk) };
19031
+ }
19032
+ }
19033
+ if (c.rig != null) {
19034
+ const spec = typeof c.rig === "string" ? { preset: c.rig } : c.rig && typeof c.rig === "object" && !Array.isArray(c.rig) ? c.rig : null;
19035
+ if (!spec) problems.push(`${cw}: rig is a rig id or { "preset"?, "type"?, \u2026its fields }`);
19036
+ else {
19037
+ const { preset, targets, ...own } = spec;
19038
+ const base = preset != null ? RIGS.get(preset) : null;
19039
+ if (preset != null && !base) problems.push(`${cw}: unknown rig "${preset}"${near(preset, [...RIGS.keys()])}`);
19040
+ for (const k of Object.keys(own)) if (!RIG_FIELDS.includes(k)) problems.push(`${cw}: rig field "${k}" \u2014 one of ${RIG_FIELDS.join(", ")}`);
19041
+ if (own.type != null && !CAMERA_RIG_TYPES.includes(own.type)) problems.push(`${cw}: rig type must be one of ${CAMERA_RIG_TYPES.join(", ")}`);
19042
+ if (own.aim != null && !CAMERA_RIG_AIMS.includes(own.aim)) problems.push(`${cw}: rig aim must be ${CAMERA_RIG_AIMS.join(" or ")}`);
19043
+ const ids = [];
19044
+ for (const t of Array.isArray(targets) ? targets : targets != null ? [targets] : []) {
19045
+ if (elIds.has(String(t))) ids.push(elIds.get(String(t)));
19046
+ else problems.push(`${cw}: rig target "${t}" is not the id of an element in this scene`);
19047
+ }
19048
+ const rig = normalizeCameraRig({ ...base?.rig || {}, ...own, targets: ids });
19049
+ if (rig.type === "follow" && !ids.length) problems.push(`${cw}: a follow rig rides elements: "targets": [element ids]`);
19050
+ item.cameraRig = rig;
19051
+ mode = "rig";
19052
+ pk = seedCameraRigProgress(pk) || pk;
19053
+ const side = Math.max(...rigSamples(rig).map(({ pos, aim }) => Math.acos(clamp5((pos.z - aim.z) / (Math.hypot(pos.x - aim.x, pos.y - aim.y, pos.z - aim.z) || 1), -1, 1)) * (180 / Math.PI)));
19054
+ if (side > 75) warnings.push(`${cw}: the rig swings ${Math.round(side)}\xB0 off the front, so the scene's flat layers turn ${side > 90 ? "round and read mirrored" : "edge-on"} \u2014 keep it within about \xB170\xB0 (e.g. startAngle -35, sweep 70)`);
19055
+ }
19056
+ }
19057
+ if (c.zooms != null) {
19058
+ if (!Array.isArray(c.zooms) || !c.zooms.length) problems.push(`${cw}: zooms is a list of { "start", "end", "at"?, "zoom"? }`);
19059
+ else {
19060
+ const feel = c.feel ?? "studio";
19061
+ if (!FEELS.includes(feel)) problems.push(`${cw}: feel must be one of ${FEELS.join(", ")}`);
19062
+ const ranges = [];
19063
+ c.zooms.forEach((z, i) => {
19064
+ const zw = `${cw}, zoom ${i + 1}`;
19065
+ const s = num7(z?.start, NaN);
19066
+ const e2 = num7(z?.end, NaN);
19067
+ if (!(s >= -1e-6 && e2 <= len + 1e-6 && e2 - s >= ZOOM_RANGE_MIN_MS / 1e3 - 1e-6)) {
19068
+ problems.push(`${zw}: start and end are seconds into the camera (0\u2013${r3(len)}), at least ${ZOOM_RANGE_MIN_MS / 1e3} s apart`);
19069
+ return;
19070
+ }
19071
+ if (z?.zoom != null && !(num7(z.zoom, 0) >= ZOOM_MIN && num7(z.zoom, 0) <= ZOOM_MAX)) {
19072
+ problems.push(`${zw}: zoom must be ${ZOOM_MIN}\u2013${ZOOM_MAX}`);
19073
+ return;
19074
+ }
19075
+ if (ranges.length && s < ranges.at(-1).endMs / 1e3 - 1e-6) {
19076
+ problems.push(`${zw}: overlaps the zoom before it`);
19077
+ return;
19078
+ }
19079
+ const [x, y] = at(z?.at);
19080
+ ranges.push({
19081
+ id: `z${i + 1}`,
19082
+ startMs: Math.round(s * 1e3),
19083
+ endMs: Math.round(e2 * 1e3),
19084
+ mode: "fixed",
19085
+ point: { x: clamp5(0.5 + x / W, 0, 1), y: clamp5(0.5 + y / H, 0, 1) },
19086
+ ...z?.zoom != null ? { zoom: num7(z.zoom, 0) } : {}
19087
+ });
19088
+ });
19089
+ item.smartZoom = { clipId: null, preset: FEELS.includes(feel) ? feel : "studio", ranges: sanitizeZoomRanges(ranges) };
19090
+ mode = "smartZoom";
19091
+ }
19092
+ }
19093
+ if (c.keyframes != null) {
19094
+ if (typeof c.keyframes !== "object" || Array.isArray(c.keyframes)) problems.push(`${cw}: keyframes is { ${Object.keys(CAM_TRACKS).join(" | ")}: [[seconds, value, easing?], \u2026] }`);
19095
+ else for (const [name, keys] of Object.entries(c.keyframes)) {
19096
+ const track = CAM_TRACKS[name];
19097
+ if (!track) {
19098
+ problems.push(`${cw}: keyframes "${name}" \u2014 one of ${Object.keys(CAM_TRACKS).join(", ")}`);
19099
+ continue;
19100
+ }
19101
+ if (CAM_DRIVEN[mode]?.includes(name)) {
19102
+ problems.push(`${cw}: keyframes.${name} \u2014 ${mode === "rig" ? "a rig" : "zooms"} drive${mode === "rig" ? "s" : ""} ${CAM_DRIVEN[mode].join(", ")} themselves; key ${Object.keys(CAM_TRACKS).filter((t) => !CAM_DRIVEN[mode].includes(t)).join(", ")}`);
19103
+ continue;
19104
+ }
19105
+ if (!Array.isArray(keys) || keys.length < 2) {
19106
+ problems.push(`${cw}: keyframes.${name} needs two or more [seconds, value] keys`);
19107
+ continue;
19108
+ }
19109
+ const list = [];
19110
+ for (const k of keys) {
19111
+ const [t, v, easing = "AE Smooth"] = Array.isArray(k) ? k : [k?.t, k?.value, k?.easing];
19112
+ if (!Number.isFinite(Number(t)) || !Number.isFinite(Number(v))) {
19113
+ problems.push(`${cw}: keyframes.${name}: each key is [seconds, value, easing?]`);
19114
+ break;
19115
+ }
19116
+ if (easings.size && !easings.has(easing)) {
19117
+ problems.push(`${cw}: unknown easing "${easing}"${near(easing, [...easings])}`);
19118
+ break;
19119
+ }
19120
+ if (name === "zoom" && !(Number(v) > 0)) {
19121
+ problems.push(`${cw}: keyframes.zoom values are above 0 (1 = no zoom)`);
19122
+ break;
19123
+ }
19124
+ if (name === "focusZ" && !(Number(v) < P - 50)) {
19125
+ problems.push(`${cw}: keyframes.focusZ must stay in front of the camera (below ${Math.round(P - 50)})`);
19126
+ break;
19127
+ }
19128
+ const value = name === "x" ? Number(v) / W * 100 : name === "y" ? Number(v) / H * 100 : name === "focusZ" ? P - Number(v) : Number(v);
19129
+ list.push({ position: r3(clamp5(Number(t) / len * 100, 0, 100)), value: String(r3(value)), easing });
19130
+ }
19131
+ if (list.length === keys.length) {
19132
+ if (pk[track] && track !== "camPathT") warnings.push(`${cw}: keyframes.${name} replace the ${track} keys of move "${c.move}"`);
19133
+ pk[track] = list.sort((a, b2) => a.position - b2.position);
19134
+ if (name === "focusZ") item.camDofEnabled = 1;
19135
+ }
19136
+ }
19137
+ }
19138
+ if (c.dof != null) {
19139
+ const d = c.dof === true ? {} : c.dof && typeof c.dof === "object" && !Array.isArray(c.dof) ? c.dof : null;
19140
+ if (!d) problems.push(`${cw}: dof is true or { "aperture"?, "blur"?, "focus"? (an element id) | "focusZ"? }`);
19141
+ else {
19142
+ for (const k of Object.keys(d)) if (!["aperture", "blur", "focus", "focusZ"].includes(k)) problems.push(`${cw}: dof field "${k}" \u2014 aperture, blur, focus or focusZ`);
19143
+ let fz = num7(d.focusZ, 0);
19144
+ if (d.focus != null) {
19145
+ if (elIds.has(String(d.focus))) fz = zOf.get(elIds.get(String(d.focus))) || 0;
19146
+ else problems.push(`${cw}: dof focus "${d.focus}" is not the id of an element in this scene`);
19147
+ }
19148
+ if (!(fz < P - 50)) problems.push(`${cw}: dof focusZ must be in front of the camera (below ${Math.round(P - 50)})`);
19149
+ item.camDofEnabled = 1;
19150
+ item.camAperture = r3(clamp5(num7(d.aperture, 40), 0, 300));
19151
+ if (d.blur != null) item.camBlurLevel = r3(clamp5(num7(d.blur, 100), 0, 300));
19152
+ if (!pk.camFocusDistance) {
19153
+ const follow = focusFollow(fz, mode === "rig" ? item.cameraRig : null, pk.camDolly);
19154
+ if (follow) pk.camFocusDistance = follow;
19155
+ else if (fz) item.camFocusDistance = Math.round(P - fz);
19156
+ }
19157
+ }
19158
+ }
19159
+ if (mode !== "free") item.cameraMoveMode = mode;
19160
+ if (Object.keys(pk).length) item.propertyKeyframes = pk;
19161
+ seq.clips.push({ id: `c-${id}`, source: "LOCAL", mediaType: "camera", url: "", proxyUrl: null, name: "Camera", duration: r3(len) });
19162
+ seq.channels.push({ id: `ch-${id}`, name: "Camera", kind: "camera", visible: true, unsnapped: true, items: [item] });
19163
+ seq.channels[0].cameraLocked = true;
19164
+ };
16268
19165
  const project = {
16269
19166
  name: String(S.title || "GreenLight Motion video").slice(0, 120),
16270
19167
  videoSize: { w: W, h: H },
@@ -16289,9 +19186,9 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16289
19186
  let total = 0;
16290
19187
  scenes.forEach((sc, si) => {
16291
19188
  const where = `scene ${si + 1}`;
16292
- const dur = num3(sc?.duration, 3);
19189
+ const dur = num7(sc?.duration, 3);
16293
19190
  if (!(dur >= 0.8 && dur <= 30)) problems.push(`${where}: duration must be 0.8\u201330 s`);
16294
- const D = clamp2(dur, 0.8, 30);
19191
+ const D = clamp5(dur, 0.8, 30);
16295
19192
  const els = Array.isArray(sc?.elements) ? sc.elements : [];
16296
19193
  if (els.length > 40) problems.push(`${where}: at most 40 elements`);
16297
19194
  const seq = { clips: [], channels: [] };
@@ -16316,64 +19213,65 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16316
19213
  if (bg.type === "solid" && !sceneBg) fields.push({ bg: true, itemId: bgItem.id, color: bg.solidColor });
16317
19214
  const media = (el, ew, id, start, end, pos) => {
16318
19215
  const type = el.type;
16319
- let f;
19216
+ let f2;
16320
19217
  if (el.svg && type === "image") {
16321
19218
  try {
16322
- f = { ...svgDataUrl(el.svg), kind: "image", type: "svg", name: el.label || "art.svg" };
19219
+ f2 = { ...svgDataUrl(el.svg), kind: "image", type: "svg", name: el.label || "art.svg" };
16323
19220
  } catch (e2) {
16324
19221
  problems.push(`${ew}: ${e2.message}`);
16325
19222
  return null;
16326
19223
  }
16327
- } else f = fileFor(el.src, ew, [type]);
16328
- if (!f) return null;
16329
- const iw = f.width || W;
16330
- const ih = f.height || H;
16331
- if (!f.width) warnings.push(`${ew}: the size of "${f.name}" is unknown, so it is fitted to the frame`);
16332
- const s0 = type === "video" ? Math.min(W / iw, H / ih) : f.type === "svg" ? 1 : Math.min(1, W / iw, H / ih);
19224
+ } else f2 = fileFor(el.src, ew, [type]);
19225
+ if (!f2) return null;
19226
+ const iw = f2.width || W;
19227
+ const ih = f2.height || H;
19228
+ if (!f2.width) warnings.push(`${ew}: the size of "${f2.name}" is unknown, so it is fitted to the frame`);
19229
+ const s0 = type === "video" ? Math.min(W / iw, H / ih) : f2.type === "svg" ? 1 : Math.min(1, W / iw, H / ih);
16333
19230
  const bw = iw * s0;
16334
19231
  const bh = ih * s0;
16335
19232
  const fit = el.fit || (el.w || el.h ? null : type === "video" ? "cover" : "contain");
16336
- const zoom = el.w ? num3(el.w, bw) / bw : el.h ? num3(el.h, bh) / bh : fit === "cover" ? Math.max(W / bw, H / bh) : fit === "contain" ? Math.min(W / bw, H / bh) * (el.fit ? 1 : 0.7) : 1;
19233
+ const zoom = el.w ? num7(el.w, bw) / bw : el.h ? num7(el.h, bh) / bh : fit === "cover" ? Math.max(W / bw, H / bh) : fit === "contain" ? Math.min(W / bw, H / bh) * (el.fit ? 1 : 0.7) : 1;
16337
19234
  const len = Math.max(0.05, end - start);
16338
- const from = Math.max(0, num3(el.startAt, 0));
16339
- const srcLen = type === "video" ? f.duration || from + len : len;
16340
- if (type === "video" && f.duration && from + len > f.duration + 0.05) warnings.push(`${ew}: "${f.name}" is ${f.duration} s; from ${from} s it ends before the element (it holds its last frame)`);
19235
+ const from = Math.max(0, num7(el.startAt, 0));
19236
+ const srcLen = type === "video" ? f2.duration || from + len : len;
19237
+ if (type === "video" && f2.duration && from + len > f2.duration + 0.05) warnings.push(`${ew}: "${f2.name}" is ${f2.duration} s; from ${from} s it ends before the element (it holds its last frame)`);
16341
19238
  seq.clips.push({
16342
19239
  id: `c-${id}`,
16343
19240
  source: "LOCAL",
16344
19241
  mediaType: type,
16345
- url: f.url,
19242
+ url: f2.url,
16346
19243
  proxyUrl: null,
16347
- name: f.name || id,
19244
+ name: f2.name || id,
16348
19245
  duration: r3(srcLen),
16349
- ...f.width ? { width: f.width, height: f.height } : {}
19246
+ ...f2.width ? { width: f2.width, height: f2.height } : {}
16350
19247
  });
16351
- const vol = clamp2(num3(el.volume, 0), 0, 100);
19248
+ const vol = clamp5(num7(el.volume, 0), 0, 100);
16352
19249
  const item = {
16353
19250
  id,
16354
19251
  clipId: `c-${id}`,
16355
19252
  freeStart: r3(start),
16356
19253
  trimStart: r3(from),
16357
19254
  trimEnd: r3(from + len),
16358
- zoom: r3(clamp2(zoom, 0.02, 20)),
19255
+ zoom: r3(clamp5(zoom, 0.02, 20)),
16359
19256
  ...pos,
16360
- ...el.rotate ? { rotation: num3(el.rotate, 0) } : {},
16361
- ...el.opacity != null ? { opacity: r3(clamp2(num3(el.opacity, 1), 0, 1) * 100) } : {},
16362
- // the item's opacity is 0–100 (its keyframe track 0–1)
19257
+ ...el.rotate ? { rotation: num7(el.rotate, 0) } : {},
16363
19258
  ...type === "video" ? vol > 0 ? { volume: vol } : { volume: 0, muted: true } : {}
16364
19259
  };
16365
- seq.channels.push({ id: `ch-${id}`, name: f.name || id, visible: true, unsnapped: true, items: [item] });
19260
+ seq.channels.push({ id: `ch-${id}`, name: f2.name || id, visible: true, unsnapped: true, items: [item] });
16366
19261
  return item;
16367
19262
  };
16368
19263
  if (sceneBg?.type === "media") media({ type: sceneBg.kind, src: sceneBg.src, fit: "cover" }, `${where}, background`, uid(`${sceneId}-bg-${sceneBg.kind}`), 0, D, {});
16369
19264
  let firstHeadline = null;
19265
+ const elIds = /* @__PURE__ */ new Map();
19266
+ const zOf = /* @__PURE__ */ new Map();
16370
19267
  els.forEach((el, ei) => {
16371
19268
  const ew = `${where}, element ${ei + 1}`;
16372
19269
  const type = el?.type || "text";
16373
- const start = clamp2(num3(el?.start, 0.1 + ei * 0.12), 0, D - 0.2);
16374
- const end = clamp2(num3(el?.end, D), start + 0.2, D);
19270
+ const start = clamp5(num7(el?.start, 0.1 + ei * 0.12), 0, D - 0.2);
19271
+ const end = clamp5(num7(el?.end, D), start + 0.2, D);
16375
19272
  const [x, y] = at(el?.at);
16376
- const pos = { ...x ? { panX: r3(x / W * 100) } : {}, ...y ? { panY: r3(y / H * 100) } : {} };
19273
+ const z = clamp5(num7(el?.z, 0), -6e3, 1500);
19274
+ const pos = { ...x ? { panX: r3(x / W * 100) } : {}, ...y ? { panY: r3(y / H * 100) } : {}, ...z ? { panZ: r3(z) } : {} };
16377
19275
  const id = uid(el?.id || `${sceneId}-${type}-${ei + 1}`);
16378
19276
  let item;
16379
19277
  if (type === "text") {
@@ -16383,9 +19281,9 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16383
19281
  return;
16384
19282
  }
16385
19283
  const role = ROLES[el?.role] ? el.role : ei === 0 ? "headline" : "subhead";
16386
- const size = clamp2(num3(el?.size, Math.round(ROLES[role][0] * Math.min(W, H) * (W > H ? 1 : 1.25))), 8, 600);
16387
- const weight = clamp2(num3(el?.weight, ROLES[role][1]), 100, 900);
16388
- const width = clamp2(num3(el?.width, Math.min(W * 0.86, Math.max(...t.split("\n").map((l) => l.length)) * size * 0.62 + size)), 40, W);
19284
+ const size = clamp5(num7(el?.size, Math.round(ROLES[role][0] * Math.min(W, H) * (W > H ? 1 : 1.25))), 8, 600);
19285
+ const weight = clamp5(num7(el?.weight, ROLES[role][1]), 100, 900);
19286
+ const width = clamp5(num7(el?.width, Math.min(W * 0.86, Math.max(...t.split("\n").map((l) => l.length)) * size * 0.62 + size)), 40, W);
16389
19287
  const color = paint(el?.color || (role === "headline" ? "text" : role === "label" ? "accent" : "muted"), colors.text);
16390
19288
  item = add("text", id, start, end, {
16391
19289
  textContent: t,
@@ -16396,13 +19294,13 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16396
19294
  textAlign: el?.align || "center",
16397
19295
  textWidthUnit: "px",
16398
19296
  textWidthPx: Math.round(width),
16399
- textLineHeight: num3(el?.lineHeight, 1.15),
16400
- ...el?.letterSpacing ? { textLetterSpacing: num3(el.letterSpacing, 0) } : {},
19297
+ textLineHeight: num7(el?.lineHeight, 1.15),
19298
+ ...el?.letterSpacing ? { textLetterSpacing: num7(el.letterSpacing, 0) } : {},
16401
19299
  ...el?.uppercase ? { textLetterCase: "uppercase" } : {},
16402
19300
  ...pos
16403
19301
  });
16404
19302
  if (el?.color === "accent" || !el?.color && role === "label") accentTargets.push({ itemId: id, path: "textColor" });
16405
- if (fields.filter((f) => f.text).length < 10) fields.push({ text: true, key: id, label: el?.label || t.split(/\s+/).slice(0, 4).join(" "), itemId: id, value: t });
19303
+ if (fields.filter((f2) => f2.text).length < 10) fields.push({ text: true, key: id, label: el?.label || t.split(/\s+/).slice(0, 4).join(" "), itemId: id, value: t });
16406
19304
  const preset = el?.textAnimation ?? (role === "headline" ? "glide-text" : null);
16407
19305
  if (preset) {
16408
19306
  const p = TEXT.get(TEXT_RENAMED[preset] || preset);
@@ -16410,12 +19308,33 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16410
19308
  else {
16411
19309
  const patch = presetPatch(item, p, { clipSec: end - start, fontPx: size, mode: "replace" });
16412
19310
  item.textAnimators = el?.textExit ? patch.textAnimators : patch.textAnimators.filter((a) => !(a.range?.drive === "time" && a.range?.animate === "out"));
19311
+ const boxed = [];
16413
19312
  item.textAnimators.forEach((a, ai) => (a.effects || []).forEach((fx, fi) => {
16414
- if (fx.type === "fill" && typeof fx.color === "string") {
19313
+ const at2 = `textAnimators.${ai}.effects.${fi}`;
19314
+ if (Array.isArray(fx.colors)) {
19315
+ fx.colors.forEach((c, ci) => {
19316
+ if (vivid(c)) {
19317
+ fx.colors[ci] = colors.accent;
19318
+ accentTargets.push({ itemId: id, path: `${at2}.colors.${ci}` });
19319
+ }
19320
+ });
19321
+ } else if (typeof fx.color === "string" && vivid(fx.color)) {
16415
19322
  fx.color = colors.accent;
16416
- accentTargets.push({ itemId: id, path: `textAnimators.${ai}.effects.${fi}.color` });
19323
+ accentTargets.push({ itemId: id, path: `${at2}.color` });
19324
+ if (fx.type === "box" && !boxed.includes(a)) boxed.push(a);
16417
19325
  }
16418
19326
  }));
19327
+ if (boxed.length) {
19328
+ const ink = contrast("#0b0d0c", colors.accent) >= contrast("#ffffff", colors.accent) ? "#0b0d0c" : "#ffffff";
19329
+ const fills = item.textAnimators.flatMap((a) => (a.effects || []).filter((fx) => fx.type === "fill"));
19330
+ if (fills.length) fills.forEach((fx) => {
19331
+ if (contrast(fx.color, colors.accent) < 4.5) fx.color = ink;
19332
+ });
19333
+ else if (contrast(color, colors.accent) < 4.5) {
19334
+ let pidN = Math.max(0, ...item.textAnimators.flatMap((a) => (a.effects || []).map((fx) => Number(String(fx.pid).replace(/^\D+/, "")) || 0)));
19335
+ for (const a of boxed) a.effects = [...a.effects || [], { pid: `p${++pidN}`, type: "fill", color: ink }];
19336
+ }
19337
+ }
16419
19338
  item.propertyKeyframes = { ...item.propertyKeyframes || {}, ...patch.propertyKeyframes };
16420
19339
  if (patch.textAnimatorPivotX != null) {
16421
19340
  item.textAnimatorPivotX = patch.textAnimatorPivotX;
@@ -16425,8 +19344,8 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16425
19344
  }
16426
19345
  if (role === "headline" && !firstHeadline) firstHeadline = { id, start };
16427
19346
  } else if (["rect", "pill", "circle"].includes(type)) {
16428
- const w = clamp2(num3(el?.w, type === "circle" ? 200 : 400), 1, W * 4);
16429
- const h = clamp2(num3(el?.h, type === "circle" ? w : 120), 1, H * 4);
19347
+ const w = clamp5(num7(el?.w, type === "circle" ? 200 : 400), 1, W * 4);
19348
+ const h = clamp5(num7(el?.h, type === "circle" ? w : 120), 1, H * 4);
16430
19349
  const fillBg = typeof el?.color === "object" || /gradient\(/i.test(String(el?.color || "")) ? background(el.color, ew) : null;
16431
19350
  const fill = fillBg ? fillBg.solidColor : paint(el?.color || "accent", colors.accent);
16432
19351
  item = add("shape", id, start, end, {
@@ -16436,14 +19355,14 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16436
19355
  shapeHeightPx: h,
16437
19356
  shapeFillColor: fill,
16438
19357
  ...fillBg && fillBg.type !== "solid" ? { shapeBackground: fillBg } : {},
16439
- ...type === "pill" ? { borderRadius: h / 2 } : el?.radius ? { borderRadius: num3(el.radius, 0) } : {},
16440
- ...el?.border ? { borderWidth: num3(el.border.width, 2), borderColor: paint(el.border.color, colors.text), borderStyle: "solid" } : {},
19358
+ ...type === "pill" ? { borderRadius: h / 2 } : el?.radius ? { borderRadius: num7(el.radius, 0) } : {},
19359
+ ...el?.border ? { borderWidth: num7(el.border.width, 2), borderColor: paint(el.border.color, colors.text), borderStyle: "solid" } : {},
16441
19360
  ...el?.shadow ? { shadows: [{ x: 0, y: 12, blur: 40, spread: 0, color: "rgba(0,0,0,0.35)", inset: false }] } : {},
16442
19361
  ...pos
16443
19362
  });
16444
19363
  if (!el?.color || el.color === "accent") accentTargets.push({ itemId: id, path: "shapeFillColor" });
16445
19364
  } else if (type === "line") {
16446
- const [x2, y2] = Array.isArray(el?.to) ? [num3(el.to[0], 0), num3(el.to[1], 0)] : [x + 300, y];
19365
+ const [x2, y2] = Array.isArray(el?.to) ? [num7(el.to[0], 0), num7(el.to[1], 0)] : [x + 300, y];
16447
19366
  const cx = (x + x2) / 2;
16448
19367
  const cy = (y + y2) / 2;
16449
19368
  const L = Math.max(4, Math.abs(x2 - x), Math.abs(y2 - y));
@@ -16455,10 +19374,11 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16455
19374
  shapeHeightPx: L,
16456
19375
  shapeFillColor: paint(el?.color || "accent", colors.accent),
16457
19376
  shapePath: { points: pts, closed: false },
16458
- lineWidth: num3(el?.width, 6),
19377
+ lineWidth: num7(el?.width, 6),
16459
19378
  lineCap: "round",
16460
19379
  ...cx ? { panX: r3(cx / W * 100) } : {},
16461
- ...cy ? { panY: r3(cy / H * 100) } : {}
19380
+ ...cy ? { panY: r3(cy / H * 100) } : {},
19381
+ ...z ? { panZ: r3(z) } : {}
16462
19382
  });
16463
19383
  if (el?.draw !== false) item.propertyKeyframes = { lineTrimEnd: [{ position: 0, value: "0", easing: "AE Out" }, { position: r3(Math.min(100, 0.7 / (end - start) * 100)), value: "100", easing: "Linear" }] };
16464
19384
  if (!el?.color || el.color === "accent") accentTargets.push({ itemId: id, path: "shapeFillColor" });
@@ -16469,6 +19389,13 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16469
19389
  problems.push(`${ew}: type must be text, rect, pill, circle, line, image or video`);
16470
19390
  return;
16471
19391
  }
19392
+ if (el?.opacity != null) item.opacity = r3(clamp5(num7(el.opacity, 1), 0, 1) * 100);
19393
+ if (el?.id != null) elIds.set(String(el.id), id);
19394
+ zOf.set(id, z);
19395
+ if (el?.locked) {
19396
+ const ch = seq.channels.find((c) => c.items[0] === item);
19397
+ if (ch) ch.cameraLocked = true;
19398
+ }
16472
19399
  const len = Math.max(1e-3, end - start);
16473
19400
  const span = Math.min(100, KFP_DEFAULT_LENGTH / len * 100);
16474
19401
  const enter = el?.enter ?? (["text", "image", "video"].includes(type) ? null : "pop-overshoot");
@@ -16494,8 +19421,8 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16494
19421
  continue;
16495
19422
  }
16496
19423
  const list = [];
16497
- for (const k2 of keys) {
16498
- const [t, v, easing = "AE Smooth"] = Array.isArray(k2) ? k2 : [k2?.t, k2?.value, k2?.easing];
19424
+ for (const k of keys) {
19425
+ const [t, v, easing = "AE Smooth"] = Array.isArray(k) ? k : [k?.t, k?.value, k?.easing];
16499
19426
  if (!Number.isFinite(Number(t)) || !Number.isFinite(Number(v))) {
16500
19427
  problems.push(`${ew}: keyframes.${name}: each key is [seconds, value, easing?]`);
16501
19428
  break;
@@ -16504,7 +19431,7 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16504
19431
  problems.push(`${ew}: unknown easing "${easing}"${near(easing, [...easings])}`);
16505
19432
  break;
16506
19433
  }
16507
- list.push({ position: r3(clamp2(Number(t) / len * 100, 0, 100)), value: String(name === "opacity" ? clamp2(Number(v), 0, 1) : Number(v)), easing });
19434
+ list.push({ position: r3(clamp5(Number(t) / len * 100, 0, 100)), value: String(name === "opacity" ? clamp5(Number(v), 0, 1) : Number(v)), easing });
16508
19435
  }
16509
19436
  if (list.length === keys.length) {
16510
19437
  if (item.propertyKeyframes?.[track]) warnings.push(`${ew}: keyframes.${name} replace the ${track} keys of its entrance / exit`);
@@ -16530,25 +19457,30 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16530
19457
  const tr = sc?.transition ?? "Cross Fade";
16531
19458
  if (!CROSS_TRANSITIONS.includes(tr)) problems.push(`${where}: unknown transition "${tr}" (one of ${CROSS_TRANSITIONS.join(", ")})`);
16532
19459
  else if (tr !== "None") {
16533
- const prev = num3(scenes[si - 1]?.duration, 3);
19460
+ const prev = num7(scenes[si - 1]?.duration, 3);
16534
19461
  sceneItem.transition = tr;
16535
- sceneItem.transitionDuration = r3(clamp2(num3(sc?.transitionDuration, 0.6), 0.2, Math.min(prev, D) / 2));
19462
+ if (EDGE_FILLED.has(tr)) sceneItem.transitionOptions = { edgeMode: "none" };
19463
+ sceneItem.transitionDuration = r3(clamp5(num7(sc?.transitionDuration, 0.6), 0.2, Math.min(prev, D) / 2));
16536
19464
  total -= sceneItem.transitionDuration;
16537
19465
  if ((S.sound ?? "auto") === "auto") project.sounds.push({ sfx: soundOk("soft-swoosh", where) ? "soft-swoosh" : "standard-swoosh", on: project.channels[0].items.at(-1).id, at: "end" });
16538
19466
  }
16539
19467
  }
16540
19468
  const cam = sc?.camera ?? "push-in";
16541
- if (!CAMERAS.includes(cam)) problems.push(`${where}: camera must be one of ${CAMERAS.join(", ")}`);
16542
- const k = (track, a, b2) => [{ position: 0, value: String(a), easing: "AE Smooth" }, { position: 100, value: String(b2), easing: "Linear" }];
16543
- const camKeys = {
16544
- "push-in": { scale: k("scale", 1, 1.06) },
16545
- "pull-out": { scale: k("scale", 1.06, 1) },
16546
- "drift-left": { translateX: k("x", 0, -36) },
16547
- "drift-right": { translateX: k("x", 0, 36) },
16548
- "drift-up": { translateY: k("y", 0, -28) }
16549
- }[cam];
16550
- if (camKeys) sceneItem.propertyKeyframes = camKeys;
16551
- for (const s of Array.isArray(sc?.sounds) ? sc.sounds : []) if (s?.sfx && soundOk(s.sfx, where)) project.sounds.push({ sfx: s.sfx, on: sceneId, at: r3(num3(s.at, 0)), ...s.gain ? { gain: num3(s.gain, 0) } : {} });
19469
+ const real = cam && typeof cam === "object" && !Array.isArray(cam) ? cam : typeof cam === "string" && MOVES.has(cam) ? { move: cam } : typeof cam === "string" && RIGS.has(cam) ? { rig: cam } : null;
19470
+ if (real) sceneCamera(real, `${where}, camera`, D, seq, sceneId, elIds, zOf);
19471
+ else if (!CAMERAS.includes(cam)) problems.push(`${where}: camera must be one of ${CAMERAS.join(", ")}, a camera move or rig id, or { "move" | "rig" | "zooms" | "keyframes", "dof"? }${typeof cam === "string" ? near(cam, [...CAMERAS, ...MOVES.keys(), ...RIGS.keys()]) : ""}`);
19472
+ else {
19473
+ const k = (track, a, b2) => [{ position: 0, value: String(a), easing: "AE Smooth" }, { position: 100, value: String(b2), easing: "Linear" }];
19474
+ const camKeys = {
19475
+ "push-in": { scale: k("scale", 1, 1.06) },
19476
+ "pull-out": { scale: k("scale", 1.06, 1) },
19477
+ "drift-left": { translateX: k("x", 0, -36) },
19478
+ "drift-right": { translateX: k("x", 0, 36) },
19479
+ "drift-up": { translateY: k("y", 0, -28) }
19480
+ }[cam];
19481
+ if (camKeys) sceneItem.propertyKeyframes = camKeys;
19482
+ }
19483
+ for (const s of Array.isArray(sc?.sounds) ? sc.sounds : []) if (s?.sfx && soundOk(s.sfx, where)) project.sounds.push({ sfx: s.sfx, on: sceneId, at: r3(num7(s.at, 0)), ...s.gain ? { gain: num7(s.gain, 0) } : {} });
16552
19484
  if ((S.sound ?? "auto") === "auto" && si === 0 && firstHeadline && soundOk("whoosh-3", where)) project.sounds.push({ sfx: "whoosh-3", on: sceneId, at: r3(firstHeadline.start) });
16553
19485
  project.channels[0].items.push(sceneItem);
16554
19486
  total += D;
@@ -16556,30 +19488,30 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16556
19488
  });
16557
19489
  const music = typeof S.music === "string" ? { src: S.music } : S.music;
16558
19490
  if (music?.src) {
16559
- const f = fileFor(music.src, "music", ["audio", "video"]);
16560
- if (f) {
19491
+ const f2 = fileFor(music.src, "music", ["audio", "video"]);
19492
+ if (f2) {
16561
19493
  const len = Math.max(0.5, total);
16562
- const from = Math.max(0, num3(music.startAt, 0));
16563
- if (f.duration && f.duration - from < len - 0.05) warnings.push(`music: "${f.name}" is ${f.duration} s, the video ${r3(len)} s \u2014 it ends early`);
16564
- project.clips.push({ id: "c-music", source: "LOCAL", mediaType: "audio", url: f.url, proxyUrl: null, name: f.name || "music", duration: r3(f.duration || from + len) });
19494
+ const from = Math.max(0, num7(music.startAt, 0));
19495
+ if (f2.duration && f2.duration - from < len - 0.05) warnings.push(`music: "${f2.name}" is ${f2.duration} s, the video ${r3(len)} s \u2014 it ends early`);
19496
+ project.clips.push({ id: "c-music", source: "LOCAL", mediaType: "audio", url: f2.url, proxyUrl: null, name: f2.name || "music", duration: r3(f2.duration || from + len) });
16565
19497
  project.channels.push({ id: "ch-music", name: "Music", items: [{
16566
19498
  id: "music",
16567
19499
  clipId: "c-music",
16568
19500
  trimStart: r3(from),
16569
- trimEnd: r3(from + Math.min(len, f.duration ? f.duration - from : len)),
16570
- volume: clamp2(num3(music.volume, 80), 0, 100),
16571
- audioFadeIn: r3(clamp2(num3(music.fadeIn, 0.3), 0, 10)),
16572
- audioFadeOut: r3(clamp2(num3(music.fadeOut, 1.2), 0, 10))
19501
+ trimEnd: r3(from + Math.min(len, f2.duration ? f2.duration - from : len)),
19502
+ volume: clamp5(num7(music.volume, 80), 0, 100),
19503
+ audioFadeIn: r3(clamp5(num7(music.fadeIn, 0.3), 0, 10)),
19504
+ audioFadeOut: r3(clamp5(num7(music.fadeOut, 1.2), 0, 10))
16573
19505
  }] });
16574
19506
  }
16575
19507
  }
16576
- const tFields = fields.filter((f) => f.text).map((f) => ({ key: f.key, label: f.label, type: "text", targets: [{ itemId: f.itemId, path: "textContent" }] }));
16577
- const values = Object.fromEntries(fields.filter((f) => f.text).map((f) => [f.key, f.value]));
19508
+ const tFields = fields.filter((f2) => f2.text).map((f2) => ({ key: f2.key, label: f2.label, type: "text", targets: [{ itemId: f2.itemId, path: "textContent" }] }));
19509
+ const values = Object.fromEntries(fields.filter((f2) => f2.text).map((f2) => [f2.key, f2.value]));
16578
19510
  if (accentTargets.length) {
16579
19511
  tFields.push({ key: "accent", label: "Accent colour", type: "color", targets: accentTargets });
16580
19512
  values.accent = colors.accent;
16581
19513
  }
16582
- const bgs = fields.filter((f) => f.bg);
19514
+ const bgs = fields.filter((f2) => f2.bg);
16583
19515
  if (bgs.length) {
16584
19516
  tFields.push({ key: "backdrop", label: "Background", type: "color", targets: bgs.map((b2) => ({ itemId: b2.itemId, path: "shapeFillColor" })) });
16585
19517
  values.backdrop = bgs[0].color;
@@ -16593,7 +19525,7 @@ function compileScript(script, caps, files = /* @__PURE__ */ new Map()) {
16593
19525
  init_define_import_meta_env();
16594
19526
 
16595
19527
  // ../skills/greenlight-dash/references/video-design.md
16596
- var video_design_default = "# Motion design that looks made, not generated\n\nWhat makes a video read as designed rather than generated. The rules hold however the video is built \u2014 a project\nthrough the app's API, `project.json` with the engine's CLI, or a scene script \u2014 so read them before the first version\nand check every version against them.\n\n## Story and words\n\n- **One idea per scene**: 3\u20136 scenes of 2\u20133 s for a short video. A scene that says two things says neither.\n- **At most ~8 words on screen at once, held at least 1 s.** People read about 3 words a second; text that leaves\n before it is read is noise.\n- **9:16**: bigger text, stacked vertically, everything in the middle 80 %.\n\n## Hierarchy\n\n- One headline per scene; supporting text much smaller. Hierarchy comes from size and weight, not colour alone.\n- One accent colour and strong contrast.\n- **No text shadows unless the user asks for one.** A shadow under type reads as a stock default and softens the\n letter edges. When text has to read over footage or a busy picture, get the contrast from the design instead: a\n calmer or darker part of the frame, a caption bar (`textBgEnabled`), or a shape behind the text. So no\n `textShadowX` / `textShadowY` / `textShadowBlur` / `textShadowSpread` on a text layer, no `shadow` property in a\n Text Animator and no `longShadow` effect on text. A shadow already in the user's project stays.\n- Keep text inside the middle 80 % of the frame (in a project: |panX| and |panY| \u2264 40): players and feeds crop the\n edges.\n\n## Timing\n\n- Stagger related elements by 0.06\u20130.12 s.\n- **One focal move at a time.** If everything moves, nothing reads: supporting elements move less and later.\n- Shapes and pictures enter in 0.4\u20130.8 s; exits are faster than entrances.\n- A text animation takes at least 0.8\u20131 s over the whole line, first unit in to last unit settled, unless the video\n needs snap (a beat hit, a glitch flick): faster text flashes past before it is read. The arithmetic per kind of\n animation: video-projects.md, Text animation pace.\n\n## Easing by intent\n\n- Entrances: `AE Out` or `Expo Out` (fast start, soft landing). Camera and big moves: `AE Smooth`.\n- A layer that passes through (in, glide, out): `Smooth Glide` with the `glide` expression \u2014 the Glide keyframe\n presets build it (video-keyframes.md).\n- Playful arrivals: `Back Out` or `Overshoot`. Exits: `Ease In`.\n- `Linear` only for constant motion: tickers, rotation, progress along a path.\n\n## Text animation\n\n- Animate words or letters, never a whole line at once \u2014 the text presets do this.\n- One family per video. Calm presets for lines and captions: Glide Text (`glide-text`), Classic Fade Up\n (`classic-fade-up`), Classic Rise (Mask) (`classic-rise-mask`); Focus Type (`focus-type`) for a title.\n- Punchy presets that tilt or overshoot \u2014 Swing Pop (`swing-pop`), Whip (`whip`), Letter Vortex (`letter-vortex`) \u2014\n only on 1\u20133 words when the video wants energy. On a longer caption they read cheap.\n- Glide Continuous (`glide-continuous`) and Glide Soft Stop (`glide-soft-stop`) never let the text rest: use them\n only when the user asks for drifting text.\n- On a coloured card, pick a preset without a colour band (Classic Rise (Mask), Classic Fade Up): a band's colour can\n clash with the card.\n\n## Transitions and camera\n\n- Vary two or three kinds of transition across a video: one kind everywhere looks like a template, a new one at\n every cut looks random.\n- Camera moves are slow and small unless they are the point. Fast motion looks best at 60 fps with motion blur.\n\n## Sound\n\n- A soft whoosh on each transition and on the first headline is a good base. `data-1` or `selection-data-click` for\n UI-like pops, `cinematic-transition-13` for one big reveal.\n\n## What to check in every version\n\nLook at real frames across the video (a contact sheet) before the user sees it \u2014 the JSON doesn't show these:\nblank or empty frames, overlapping or cut-off text, text too small or gone too fast, nothing moving or everything\nmoving at once, weak hierarchy, clashing colours. Fix them and look again; two rounds are usually enough.\n";
19528
+ var video_design_default = "# Motion design that looks made, not generated\n\nWhat makes a video read as designed rather than generated. The rules hold however the video is built \u2014 a project\nthrough the app's API, `project.json` with the engine's CLI, or a scene script \u2014 so read them before the first version\nand check every version against them.\n\n## Story and words\n\n- **One idea per scene**: 3\u20136 scenes of 2\u20133 s for a short video. A scene that says two things says neither.\n- **At most ~8 words on screen at once, held at least 1 s.** People read about 3 words a second; text that leaves\n before it is read is noise.\n- **9:16**: bigger text, stacked vertically, everything in the middle 80 %.\n\n## Hierarchy\n\n- One headline per scene; supporting text much smaller. Hierarchy comes from size and weight, not colour alone.\n- One accent colour and strong contrast.\n- **No text shadows unless the user asks for one.** A shadow under type reads as a stock default and softens the\n letter edges. When text has to read over footage or a busy picture, get the contrast from the design instead: a\n calmer or darker part of the frame, a caption bar (`textBgEnabled`), or a shape behind the text. So no\n `textShadowX` / `textShadowY` / `textShadowBlur` / `textShadowSpread` on a text layer, no `shadow` property in a\n Text Animator and no `longShadow` effect on text. A shadow already in the user's project stays.\n- Keep text inside the middle 80 % of the frame (in a project: |panX| and |panY| \u2264 40): players and feeds crop the\n edges.\n\n## Timing\n\n- Stagger related elements by 0.06\u20130.12 s.\n- **One focal move at a time.** If everything moves, nothing reads: supporting elements move less and later.\n- Shapes and pictures enter in 0.4\u20130.8 s; exits are faster than entrances.\n- A text animation takes at least 0.8\u20131 s over the whole line, first unit in to last unit settled, unless the video\n needs snap (a beat hit, a glitch flick): faster text flashes past before it is read. The arithmetic per kind of\n animation: video-projects.md, Text animation pace.\n\n## Easing by intent\n\n- Entrances: `AE Out` or `Expo Out` (fast start, soft landing). Camera and big moves: `AE Smooth`.\n- A layer that passes through (in, glide, out): `Smooth Glide` with the `glide` expression \u2014 the Glide keyframe\n presets build it (video-keyframes.md).\n- Playful arrivals: `Back Out` or `Overshoot`. Exits: `Ease In`.\n- `Linear` only for constant motion: tickers, rotation, progress along a path.\n\n## Text animation\n\n- Animate words or letters, never a whole line at once \u2014 the text presets do this.\n- One family per video. Calm presets for lines and captions: Glide Text (`glide-text`), Classic Fade Up\n (`classic-fade-up`), Classic Rise (Mask) (`classic-rise-mask`); Focus Type (`focus-type`) for a title.\n- Punchy presets that tilt or overshoot \u2014 Swing Pop (`swing-pop`), Whip (`whip`), Letter Vortex (`letter-vortex`) \u2014\n only on 1\u20133 words when the video wants energy. On a longer caption they read cheap.\n- Glide Continuous (`glide-continuous`) and Glide Soft Stop (`glide-soft-stop`) never let the text rest: use them\n only when the user asks for drifting text.\n- On a coloured card, pick a preset without a colour band (Classic Rise (Mask), Classic Fade Up): a band's colour can\n clash with the card.\n\n## Transitions and camera\n\n- Vary two or three kinds of transition across a video: one kind everywhere looks like a template, a new one at\n every cut looks random.\n- Camera moves are slow and small unless they are the point. Fast motion looks best at 60 fps with motion blur.\n- A real 3D camera earns its place on one or two moments, not every scene: a zoom onto the one thing a scene is about\n (a number, the button a screen recording clicks; 1.3\u20132\xD7, then back out), or a slow orbit or push that shows depth.\n- Depth needs layers at different distances: a soft shape or picture far behind, the words at the front. Depth of\n field reads only then; keep the words sharp.\n- Keep an orbit in front of flat layers (within about \xB170\xB0): text turned further goes edge-on, then mirrored.\n\n## Sound\n\n- A soft whoosh on each transition and on the first headline is a good base. `data-1` or `selection-data-click` for\n UI-like pops, `cinematic-transition-13` for one big reveal.\n\n## What to check in every version\n\nLook at real frames across the video (a contact sheet) before the user sees it \u2014 the JSON doesn't show these:\nblank or empty frames, overlapping or cut-off text, text too small or gone too fast, nothing moving or everything\nmoving at once, weak hierarchy, clashing colours. Fix them and look again; two rounds are usually enough.\n";
16597
19529
 
16598
19530
  // src/motionEngine/sceneScriptGuide.js
16599
19531
  var PREFERRED = {
@@ -16606,6 +19538,19 @@ var refOf = (host, topic) => host === "chat" ? `get_motion_reference "${topic}"`
16606
19538
  function scriptFormatGuide(host = "chat") {
16607
19539
  return `## The scene script
16608
19540
 
19541
+ **Before the script, a direction.** Presets make a video move; they don't give it an idea. Decide in a few lines first,
19542
+ and keep them next to the script:
19543
+ - **The idea**: ONE picture from the subject's own world (its object, place or action: a coffee shop's steam and cup rim,
19544
+ a budgeting app's receipt printing line by line, a running app's track the scenes run along), carried through every scene.
19545
+ - **The palette**: background, text, one accent, plus only the colours that carry meaning (good / bad, before / after).
19546
+ Light or dark on purpose.
19547
+ - **The type**: a big headline against a small support line; aligned left or centred on purpose, not by default.
19548
+ - **Each scene**: what it SHOWS besides its words, built from shapes, lines, \`svg\` you write, keyframes and the camera.
19549
+ The same line centred on a gradient in every scene reads as a template. Judge the stills against the direction, not
19550
+ only for mistakes.${host === "cli" ? `
19551
+ What a script can't say (a custom GLSL shader on a shape or the background, words dipping under a surface with a track
19552
+ matte, masks, particles, 3D) goes on from \`project.json\`: SKILL.md, "Beyond the script".` : ""}
19553
+
16609
19554
  \`\`\`json
16610
19555
  {
16611
19556
  "title": "Bean There \u2014 opening day",
@@ -16622,8 +19567,10 @@ function scriptFormatGuide(host = "chat") {
16622
19567
  { "type": "pill", "w": 560, "h": 116, "color": "accent", "start": 0.15 },
16623
19568
  { "type": "text", "text": "Opening day: 2 for 1", "role": "body", "color": "#1b120d", "start": 0.3, "enter": "pop-overshoot", "sound": "data-1" } ] },
16624
19569
  { "name": "Close", "duration": 2.4, "transition": "Gradient Glow Stack", "background": { "shader": "silk", "look": "sunset" },
19570
+ "camera": { "move": "cm-quiet-push", "dof": { "focus": "street", "aperture": 50 } },
16625
19571
  "elements": [
16626
- { "type": "text", "text": "12 Elm Street", "role": "headline", "textAnimation": "word-swing" },
19572
+ { "type": "circle", "w": 420, "color": "#fff3e0", "opacity": 0.3, "at": [330, -60], "z": -700 },
19573
+ { "id": "street", "type": "text", "text": "12 Elm Street", "role": "headline", "textAnimation": "word-swing" },
16627
19574
  { "type": "text", "text": "OPEN 7 \u2013 7", "role": "label", "at": [0, 130], "start": 0.8, "letterSpacing": 6 } ] }
16628
19575
  ]
16629
19576
  }
@@ -16637,15 +19584,36 @@ function scriptFormatGuide(host = "chat") {
16637
19584
  - **line**: from \`at\` to \`to\`, \`width\`, drawn on (\`draw\`: false to skip).
16638
19585
  - **image \xB7 video**: \`src\` (${host === "chat" ? "a link or the user's file \u2014 below" : "a file in the folder's assets/ as `asset:<name>`, or an https link"}) or, for an image, \`svg\` (SVG markup you write); \`w\` or \`h\`
16639
19586
  (px; the other follows the file's shape) or \`fit\` cover | contain
16640
- (default: a video covers the frame, a picture sits at 70 % of contain), \`rotate\` (deg), \`opacity\` (0\u20131). A video also takes
19587
+ (default: a video covers the frame, a picture sits at 70 % of contain), \`rotate\` (deg). A video also takes
16641
19588
  \`startAt\` (s into the file) and \`volume\` (0\u2013100, default muted). No entrance unless you set \`enter\`.
16642
19589
  - **Keyframes on any element** \u2014 your own motion: \`"keyframes": { "x": [[0, -400], [0.8, 0, "Smooth Glide"]], "scale": [[0, 0.9], [2, 1.05]] }\`
16643
19590
  \u2014 tracks x, y (px offsets from \`at\`), scale, rotate (deg), opacity (0\u20131); each key [seconds into the element, value, easing of the
16644
19591
  segment after it \u2014 Smooth Glide, AE Smooth, Expo Out, Back Out, Elastic, Linear \u2026]. They replace an entrance's keys on the same track.
16645
19592
  - **Every element**: \`start\` / \`end\` (s within its scene; default: staggered 0.12 s, to the scene's end), \`enter\` / \`exit\` (keyframe presets, \`direction\` up | down | left | right
16646
- for directional ones), \`effects\`: [{ "id", "params"? }], \`sound\` (played at its start), \`id\`, \`label\` (its name in the page's options).
16647
- - **Scene**: \`duration\` (s), \`transition\` into it (default Cross Fade, 0.6 s; \`transitionDuration\`), \`camera\` ${CAMERAS.join(" | ")} (default push-in),
19593
+ for directional ones), \`effects\`: [{ "id", "params"? }], \`sound\` (played at its start), \`id\`, \`label\` (its name in the page's options), \`opacity\` (0\u20131),
19594
+ \`z\` (px of depth, + toward the viewer, default 0: a real camera's dolly, orbit and depth of field separate layers by it), \`locked\`: true
19595
+ (the scene's camera doesn't move it: a logo, a caption). Keyframes also take a \`z\` track.
19596
+ - **Scene**: \`duration\` (s), \`transition\` into it (default Cross Fade, 0.6 s; \`transitionDuration\`), \`camera\` (below; default push-in),
16648
19597
  \`background\` (overrides style), \`sounds\`: [{ "sfx", "at" }]. Script-level \`sound\`: auto (default: a whoosh on each transition and on the first headline) | none.
19598
+ **A transition overlaps the scenes on both sides**, so the video lasts the scenes' durations added up MINUS every
19599
+ transition: five 3 s scenes with the default 0.6 s transitions run 15 \u2212 4 \xD7 0.6 = 12.6 s. To hit a length, add the
19600
+ transitions back to the scenes (${host === "chat" ? "build_video" : "`script`"} prints the total).
19601
+ - **camera** \u2014 a quick move of the whole scene: ${CAMERAS.join(" | ")}. Or a REAL camera that films the scene's elements in 3D (the
19602
+ Video Editor's own camera layer; the scene's backdrop stays put, so its edge never shows). One of:
19603
+ - \`{ "move": id }\` \u2014 a camera move from the catalogue (or just \`"camera": "cm-quiet-push"\`), spread over the scene;
19604
+ - \`{ "rig": id }\` or \`{ "rig": { "preset"?: id, "type"?, \u2026fields } }\` \u2014 an orbit, crane, push-in, truck, spiral, handheld or follow move
19605
+ (\`"camera": "heroOrbit"\` works too). A \`follow\` rig rides elements: \`"targets": [element ids]\`. Layers are flat: keep an orbit within about
19606
+ \xB170\xB0 of the front (heroOrbit, slowReveal) or they turn edge-on and read mirrored;
19607
+ - \`{ "zooms": [{ "start", "end", "at"?, "zoom"? }], "feel"? }\` \u2014 zoom in on spots and back out, like a screen recording's Smart Zoom: \`at\` as for
19608
+ elements (the spot to frame, clamped so the view stays inside the frame), \`zoom\` 1.05\u20134 (default the feel's, about 1.45), each zoom at least
19609
+ 0.4 s and none overlapping; \`feel\` studio | calm | precise | lively | follow (the spring). Put a screen recording in as a video element (or the
19610
+ scene's video background) and zoom on what it shows;
19611
+ - \`{ "keyframes": { zoom | x | y | roll | dolly | tiltX | tiltY | focusZ: [[seconds, value, easing?], \u2026] } }\` \u2014 your own move: zoom (\xD7, 1 = none),
19612
+ x / y (px the camera pans right / down), roll / tilt (deg), dolly (px toward the scene: it changes perspective, so layers at different \`z\` part).
19613
+ With a \`move\` they replace its track; a rig drives x, y, roll, dolly and tilt itself and zooms drive zoom, x and y, so key only the others there.
19614
+ - plus \`"dof": { "focus": element id | "focusZ": px, "aperture"?: 40, "blur"?: 100 }\` (or \`true\`) \u2014 depth of field: what sits at another \`z\` blurs;
19615
+ the focus stays on its subject while the camera dollies or rides a rig. \`focusZ\` keys are a rack focus (measured from where the camera starts).
19616
+ - \`start\` / \`end\` (s in the scene; default the whole scene; outside them the camera rests). Keys and zooms count seconds into the camera.
16649
19617
  - **background**: a colour, a CSS gradient, { "shader": id, "look"? } \u2014 shaders: ${SHADERS.join(", ")} (looks: ${refOf(host, "video-backgrounds")}) \u2014 or { "image": src } / { "video": src } (covers the frame; b-roll behind the scene).
16650
19618
  - **music** (script level): \`"music": { "src": file, "volume": 80, "fadeIn": 0.3, "fadeOut": 1.2, "startAt": 0 }\` \u2014 one track under the video.
16651
19619
 
@@ -16658,6 +19626,7 @@ function designGuide() {
16658
19626
  }
16659
19627
  function catalogueGuide(caps, host = "chat") {
16660
19628
  const cat = presetCatalog();
19629
+ const cam = cameraCatalog();
16661
19630
  const star = (line, ids) => ids.some((id) => line.startsWith(`\`${id}\``)) ? `\u2605 ${line}` : line;
16662
19631
  const sounds = {};
16663
19632
  for (const [id, sd] of Object.entries(caps?.sounds?.library || {})) if (sd.category !== "Voice Clips & Memes") (sounds[sd.category] ||= []).push(`${id} (${sd.duration}s)`);
@@ -16672,6 +19641,12 @@ function catalogueGuide(caps, host = "chat") {
16672
19641
 
16673
19642
  **Transitions** (\u2605 = preferred): ${CROSS_TRANSITIONS.map((t) => PREFERRED.transitions.includes(t) ? `\u2605 ${t}` : t).join(" \xB7 ")}
16674
19643
 
19644
+ **Camera moves** (\`camera.move\`; spread over the camera's length): ${cam.moves.join(" \xB7 ")}
19645
+
19646
+ **Camera rigs** (\`camera.rig\`; the fields you can override: type ${cam.rigTypes.join(" | ")}, aim ${cam.rigAims.join(" | ")}, ${cam.rigFields.join(", ")}): ${cam.rigs.join(" \xB7 ")}
19647
+
19648
+ **Zoom feels** (\`camera.feel\`): ${cam.feels.join(" \xB7 ")}. Full camera layers (paths, Smart Zoom on a recording's clicks, the drawn cursor): ${refOf(host, "video-camera")}.
19649
+
16675
19650
  **Overlay effects** (\`effects\`): ${fx(caps?.effects?.overlay)}
16676
19651
 
16677
19652
  **Layer effects** (\`effects\`): ${fx(caps?.effects?.layer)}
@@ -16735,6 +19710,7 @@ export {
16735
19710
  MIME,
16736
19711
  PREFERRED,
16737
19712
  SHADERS,
19713
+ cameraCatalog,
16738
19714
  catalogueGuide,
16739
19715
  checkTemplate,
16740
19716
  compileScript,