reframe-video 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -1070,6 +1070,22 @@ var init_figure = __esm({
1070
1070
  }
1071
1071
  });
1072
1072
 
1073
+ // ../core/src/textMetrics.ts
1074
+ var init_textMetrics = __esm({
1075
+ "../core/src/textMetrics.ts"() {
1076
+ "use strict";
1077
+ }
1078
+ });
1079
+
1080
+ // ../core/src/textFx.ts
1081
+ var init_textFx = __esm({
1082
+ "../core/src/textFx.ts"() {
1083
+ "use strict";
1084
+ init_dsl();
1085
+ init_textMetrics();
1086
+ }
1087
+ });
1088
+
1073
1089
  // ../core/src/motionOps.ts
1074
1090
  var init_motionOps = __esm({
1075
1091
  "../core/src/motionOps.ts"() {
@@ -1291,6 +1307,7 @@ var init_src = __esm({
1291
1307
  init_rig();
1292
1308
  init_characterPreset();
1293
1309
  init_figure();
1310
+ init_textFx();
1294
1311
  init_motionOps();
1295
1312
  init_audio();
1296
1313
  init_evaluate();
package/dist/index.js CHANGED
@@ -884,14 +884,14 @@ function makeRng(seed) {
884
884
  var clamp01 = (x) => Math.max(0, Math.min(1, x));
885
885
  var SET = 1 / 120;
886
886
  function ctx(o) {
887
- const rand = makeRng((o.seed ?? 0) + 1);
887
+ const rand2 = makeRng((o.seed ?? 0) + 1);
888
888
  return {
889
889
  e: clamp01(o.energy ?? 0.5),
890
890
  sp: Math.max(0.25, o.speed ?? 1),
891
891
  it: clamp01(o.intensity ?? 0.5),
892
892
  from: o.from,
893
- rand,
894
- jit: (amp) => (rand() - 0.5) * 2 * amp,
893
+ rand: rand2,
894
+ jit: (amp) => (rand2() - 0.5) * 2 * amp,
895
895
  g: o.target.group,
896
896
  cx: o.target.center[0],
897
897
  cy: o.target.center[1],
@@ -1378,7 +1378,7 @@ function makeRng2(seed) {
1378
1378
  }
1379
1379
  var dur2 = (base, sp) => base / sp;
1380
1380
  function ctx2(o) {
1381
- const rand = makeRng2((o.seed ?? 0) + 1);
1381
+ const rand2 = makeRng2((o.seed ?? 0) + 1);
1382
1382
  return {
1383
1383
  g: o.target,
1384
1384
  label: o.label,
@@ -1388,8 +1388,8 @@ function ctx2(o) {
1388
1388
  facing: o.facing ?? 1,
1389
1389
  at: o.at ?? [0, 0],
1390
1390
  travel: o.travel,
1391
- rand,
1392
- jit: (amp) => (rand() - 0.5) * 2 * amp
1391
+ rand: rand2,
1392
+ jit: (amp) => (rand2() - 0.5) * 2 * amp
1393
1393
  };
1394
1394
  }
1395
1395
  var round = (v) => Math.round(v * 1e3) / 1e3;
@@ -1688,9 +1688,511 @@ function figure(opts = {}) {
1688
1688
  return rig(buildSkeleton(id, parts), rigOpts);
1689
1689
  }
1690
1690
 
1691
+ // ../core/src/textMetrics.ts
1692
+ var INTER_ADVANCE = {
1693
+ "400": {
1694
+ "0": 63.09,
1695
+ "1": 40.67,
1696
+ "2": 60.99,
1697
+ "3": 61.77,
1698
+ "4": 64.6,
1699
+ "5": 59.33,
1700
+ "6": 62.01,
1701
+ "7": 56.59,
1702
+ "8": 61.87,
1703
+ "9": 62.01,
1704
+ " ": 28.13,
1705
+ "!": 28.76,
1706
+ '"': 46.58,
1707
+ "#": 63.33,
1708
+ "$": 64.16,
1709
+ "%": 98.19,
1710
+ "&": 64.4,
1711
+ "'": 29.98,
1712
+ "(": 36.47,
1713
+ ")": 36.47,
1714
+ "*": 50.1,
1715
+ "+": 66.16,
1716
+ ",": 28.81,
1717
+ "-": 46,
1718
+ ".": 28.81,
1719
+ "/": 36.04,
1720
+ ":": 28.81,
1721
+ ";": 30.18,
1722
+ "<": 66.16,
1723
+ "=": 66.16,
1724
+ ">": 66.16,
1725
+ "?": 51.12,
1726
+ "@": 96.58,
1727
+ "A": 68.99,
1728
+ "B": 65.43,
1729
+ "C": 73.05,
1730
+ "D": 72.17,
1731
+ "E": 60.11,
1732
+ "F": 59.03,
1733
+ "G": 74.61,
1734
+ "H": 74.32,
1735
+ "I": 26.86,
1736
+ "J": 57.08,
1737
+ "K": 67.19,
1738
+ "L": 56.54,
1739
+ "M": 90.33,
1740
+ "N": 75.34,
1741
+ "O": 76.46,
1742
+ "P": 63.87,
1743
+ "Q": 76.46,
1744
+ "R": 64.36,
1745
+ "S": 64.16,
1746
+ "T": 64.55,
1747
+ "U": 74.41,
1748
+ "V": 68.99,
1749
+ "W": 98.54,
1750
+ "X": 68.21,
1751
+ "Y": 67.87,
1752
+ "Z": 62.89,
1753
+ "[": 36.47,
1754
+ "\\": 36.04,
1755
+ "]": 36.47,
1756
+ "^": 47.12,
1757
+ "_": 45.61,
1758
+ "`": 32.28,
1759
+ "a": 56.15,
1760
+ "b": 61.23,
1761
+ "c": 57.13,
1762
+ "d": 61.23,
1763
+ "e": 58.3,
1764
+ "f": 37.01,
1765
+ "g": 61.33,
1766
+ "h": 59.13,
1767
+ "i": 24.22,
1768
+ "j": 24.22,
1769
+ "k": 54.88,
1770
+ "l": 24.22,
1771
+ "m": 87.6,
1772
+ "n": 59.08,
1773
+ "o": 59.96,
1774
+ "p": 61.23,
1775
+ "q": 61.23,
1776
+ "r": 37.65,
1777
+ "s": 52.78,
1778
+ "t": 32.71,
1779
+ "u": 59.13,
1780
+ "v": 56.2,
1781
+ "w": 81.84,
1782
+ "x": 54.59,
1783
+ "y": 56.2,
1784
+ "z": 55.22,
1785
+ "{": 42.63,
1786
+ "|": 33.25,
1787
+ "}": 42.63,
1788
+ "~": 66.16
1789
+ },
1790
+ "700": {
1791
+ "0": 67.43,
1792
+ "1": 43.12,
1793
+ "2": 62.94,
1794
+ "3": 64.55,
1795
+ "4": 67.63,
1796
+ "5": 62.21,
1797
+ "6": 64.94,
1798
+ "7": 58.15,
1799
+ "8": 65.09,
1800
+ "9": 64.94,
1801
+ " ": 23.68,
1802
+ "!": 33.79,
1803
+ '"': 55.13,
1804
+ "#": 64.89,
1805
+ "$": 65.48,
1806
+ "%": 101.56,
1807
+ "&": 67.19,
1808
+ "'": 33.89,
1809
+ "(": 37.7,
1810
+ ")": 37.7,
1811
+ "*": 55.91,
1812
+ "+": 67.87,
1813
+ ",": 33.4,
1814
+ "-": 46.78,
1815
+ ".": 33.4,
1816
+ "/": 38.82,
1817
+ ":": 33.4,
1818
+ ";": 34.28,
1819
+ "<": 67.87,
1820
+ "=": 67.87,
1821
+ ">": 67.87,
1822
+ "?": 55.96,
1823
+ "@": 101.61,
1824
+ "A": 74.66,
1825
+ "B": 66.16,
1826
+ "C": 73.97,
1827
+ "D": 72.22,
1828
+ "E": 60.74,
1829
+ "F": 58.69,
1830
+ "G": 75.05,
1831
+ "H": 74.71,
1832
+ "I": 28.08,
1833
+ "J": 58.45,
1834
+ "K": 71.92,
1835
+ "L": 56.54,
1836
+ "M": 93.16,
1837
+ "N": 76.22,
1838
+ "O": 77.05,
1839
+ "P": 64.79,
1840
+ "Q": 77.69,
1841
+ "R": 65.67,
1842
+ "S": 65.48,
1843
+ "T": 66.75,
1844
+ "U": 73.19,
1845
+ "V": 74.66,
1846
+ "W": 103.76,
1847
+ "X": 73.83,
1848
+ "Y": 73.1,
1849
+ "Z": 66.41,
1850
+ "[": 37.7,
1851
+ "\\": 38.82,
1852
+ "]": 37.7,
1853
+ "^": 48.68,
1854
+ "_": 47.61,
1855
+ "`": 36.52,
1856
+ "a": 58.06,
1857
+ "b": 63.04,
1858
+ "c": 58.84,
1859
+ "d": 63.04,
1860
+ "e": 59.57,
1861
+ "f": 39.79,
1862
+ "g": 63.18,
1863
+ "h": 62.26,
1864
+ "i": 27.1,
1865
+ "j": 27.1,
1866
+ "k": 58.01,
1867
+ "l": 27.1,
1868
+ "m": 91.26,
1869
+ "n": 62.26,
1870
+ "o": 61.33,
1871
+ "p": 63.04,
1872
+ "q": 63.04,
1873
+ "r": 40.72,
1874
+ "s": 56.01,
1875
+ "t": 36.62,
1876
+ "u": 62.26,
1877
+ "v": 59.96,
1878
+ "w": 85.01,
1879
+ "x": 58.01,
1880
+ "y": 60.21,
1881
+ "z": 57.28,
1882
+ "{": 46.88,
1883
+ "|": 37.16,
1884
+ "}": 46.88,
1885
+ "~": 67.87
1886
+ },
1887
+ "800": {
1888
+ "0": 69.19,
1889
+ "1": 44.14,
1890
+ "2": 63.77,
1891
+ "3": 65.67,
1892
+ "4": 68.85,
1893
+ "5": 63.38,
1894
+ "6": 66.16,
1895
+ "7": 58.79,
1896
+ "8": 66.41,
1897
+ "9": 66.16,
1898
+ " ": 21.88,
1899
+ "!": 35.84,
1900
+ '"': 58.64,
1901
+ "#": 65.53,
1902
+ "$": 66.02,
1903
+ "%": 102.93,
1904
+ "&": 68.31,
1905
+ "'": 35.45,
1906
+ "(": 38.23,
1907
+ ")": 38.23,
1908
+ "*": 58.25,
1909
+ "+": 68.55,
1910
+ ",": 35.25,
1911
+ "-": 47.12,
1912
+ ".": 35.25,
1913
+ "/": 39.99,
1914
+ ":": 35.25,
1915
+ ";": 35.99,
1916
+ "<": 68.55,
1917
+ "=": 68.55,
1918
+ ">": 68.55,
1919
+ "?": 57.91,
1920
+ "@": 103.61,
1921
+ "A": 76.95,
1922
+ "B": 66.46,
1923
+ "C": 74.37,
1924
+ "D": 72.27,
1925
+ "E": 60.99,
1926
+ "F": 58.54,
1927
+ "G": 75.24,
1928
+ "H": 74.85,
1929
+ "I": 28.56,
1930
+ "J": 58.98,
1931
+ "K": 73.83,
1932
+ "L": 56.54,
1933
+ "M": 94.34,
1934
+ "N": 76.56,
1935
+ "O": 77.29,
1936
+ "P": 65.19,
1937
+ "Q": 78.17,
1938
+ "R": 66.21,
1939
+ "S": 66.02,
1940
+ "T": 67.68,
1941
+ "U": 72.71,
1942
+ "V": 76.95,
1943
+ "W": 105.86,
1944
+ "X": 76.12,
1945
+ "Y": 75.2,
1946
+ "Z": 67.87,
1947
+ "[": 38.23,
1948
+ "\\": 39.99,
1949
+ "]": 38.23,
1950
+ "^": 49.32,
1951
+ "_": 48.44,
1952
+ "`": 38.23,
1953
+ "a": 58.84,
1954
+ "b": 63.77,
1955
+ "c": 59.52,
1956
+ "d": 63.77,
1957
+ "e": 60.06,
1958
+ "f": 40.97,
1959
+ "g": 63.92,
1960
+ "h": 63.53,
1961
+ "i": 28.32,
1962
+ "j": 28.32,
1963
+ "k": 59.28,
1964
+ "l": 28.32,
1965
+ "m": 92.72,
1966
+ "n": 63.53,
1967
+ "o": 61.91,
1968
+ "p": 63.77,
1969
+ "q": 63.77,
1970
+ "r": 41.99,
1971
+ "s": 57.32,
1972
+ "t": 38.18,
1973
+ "u": 63.53,
1974
+ "v": 61.52,
1975
+ "w": 86.28,
1976
+ "x": 59.42,
1977
+ "y": 61.82,
1978
+ "z": 58.11,
1979
+ "{": 48.63,
1980
+ "|": 38.77,
1981
+ "}": 48.63,
1982
+ "~": 68.55
1983
+ }
1984
+ };
1985
+ var INTER_FALLBACK = {
1986
+ "400": 56.16,
1987
+ "700": 58.74,
1988
+ "800": 59.79
1989
+ };
1990
+
1991
+ // ../core/src/textFx.ts
1992
+ var clamp013 = (v) => Math.max(0, Math.min(1, v));
1993
+ var fract = (v) => v - Math.floor(v);
1994
+ var rand = (i, salt) => fract(Math.sin(i * 127.1 + salt * 311.7) * 43758.5453);
1995
+ var dur3 = (base, sp) => base / sp;
1996
+ var SCRAMBLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&@";
1997
+ var advance = (ch, weight, fontSize) => (INTER_ADVANCE[weight]?.[ch] ?? INTER_FALLBACK[weight]) * (fontSize / 100);
1998
+ function splitText(textStr, opts) {
1999
+ const { id, x, y, fontSize } = opts;
2000
+ const weight = opts.fontWeight ?? 800;
2001
+ const fill = opts.fill ?? "#FFFFFF";
2002
+ const ls = opts.letterSpacing ?? 0;
2003
+ const align = opts.align ?? "center";
2004
+ const unit = opts.unit ?? "glyph";
2005
+ const opacity = opts.opacity ?? 0;
2006
+ const chars = [...textStr];
2007
+ let total = 0;
2008
+ chars.forEach((ch, i) => {
2009
+ total += advance(ch, weight, fontSize) + (i < chars.length - 1 ? ls : 0);
2010
+ });
2011
+ let cursor = align === "center" ? x - total / 2 : x;
2012
+ const glyphs = [];
2013
+ const nodes = [];
2014
+ const mk = (ch, cx, adv, lsProp) => {
2015
+ const g = { id: `${id}-${glyphs.length}`, ch, x: cx, y, advance: adv, i: glyphs.length };
2016
+ glyphs.push(g);
2017
+ nodes.push(
2018
+ text({
2019
+ id: g.id,
2020
+ x: cx,
2021
+ y,
2022
+ content: ch,
2023
+ fontFamily: "Inter",
2024
+ fontSize,
2025
+ fontWeight: weight,
2026
+ fill,
2027
+ anchor: "center",
2028
+ opacity,
2029
+ ...lsProp ? { letterSpacing: lsProp } : {}
2030
+ })
2031
+ );
2032
+ };
2033
+ if (unit === "word") {
2034
+ let i = 0;
2035
+ while (i < chars.length) {
2036
+ if (chars[i] === " ") {
2037
+ cursor += advance(" ", weight, fontSize) + ls;
2038
+ i++;
2039
+ continue;
2040
+ }
2041
+ let word = "";
2042
+ let w = 0;
2043
+ const startCursor = cursor;
2044
+ while (i < chars.length && chars[i] !== " ") {
2045
+ const a = advance(chars[i], weight, fontSize);
2046
+ word += chars[i];
2047
+ w += a + (chars[i + 1] && chars[i + 1] !== " " ? ls : 0);
2048
+ i++;
2049
+ }
2050
+ mk(word, startCursor + w / 2, w, ls);
2051
+ cursor = startCursor + w + ls;
2052
+ }
2053
+ } else {
2054
+ chars.forEach((ch) => {
2055
+ const a = advance(ch, weight, fontSize);
2056
+ if (ch !== " ") mk(ch, cursor + a / 2, a);
2057
+ cursor += a + ls;
2058
+ });
2059
+ }
2060
+ return { nodes, glyphs, ids: glyphs.map((g) => g.id), width: total, x, y, fontSize };
2061
+ }
2062
+ var ctx3 = (o) => ({
2063
+ sp: Math.max(0.25, o.speed ?? 1),
2064
+ e: clamp013(o.energy ?? 0.5),
2065
+ seed: o.seed ?? 0,
2066
+ fs: 0,
2067
+ stag: o.stagger
2068
+ });
2069
+ var IN_STAGGER = { typewriter: 0.065, cascade: 0.04, rise: 0.03, bounce: 0.045, assemble: 0.05, decode: 0.05 };
2070
+ function glyphIn(name, g, c) {
2071
+ const set = (props) => tween(g.id, props, { duration: 1e-3 });
2072
+ const rs = (salt) => rand(g.i, salt + c.seed);
2073
+ switch (name) {
2074
+ case "typewriter":
2075
+ return tween(g.id, { opacity: 1 }, { duration: dur3(0.04, c.sp), ease: "linear" });
2076
+ case "cascade":
2077
+ return seq(
2078
+ set({ y: g.y + 56, opacity: 0 }),
2079
+ par(
2080
+ tween(g.id, { opacity: 1 }, { duration: dur3(0.22, c.sp), ease: "easeOutQuad" }),
2081
+ tween(g.id, { y: g.y }, { duration: dur3(0.34, c.sp), ease: "easeOutCubic" })
2082
+ )
2083
+ );
2084
+ case "rise":
2085
+ return seq(
2086
+ set({ y: g.y + 36, opacity: 0 }),
2087
+ par(
2088
+ tween(g.id, { opacity: 1 }, { duration: dur3(0.3, c.sp), ease: "easeOutQuad" }),
2089
+ tween(g.id, { y: g.y }, { duration: dur3(0.4, c.sp), ease: "easeOutQuad" })
2090
+ )
2091
+ );
2092
+ case "bounce":
2093
+ return seq(
2094
+ set({ y: g.y - 80 * (0.6 + c.e), opacity: 0, scale: 0.7 }),
2095
+ par(
2096
+ tween(g.id, { opacity: 1 }, { duration: dur3(0.2, c.sp), ease: "easeOutQuad" }),
2097
+ tween(g.id, { y: g.y, scale: 1 }, { duration: dur3(0.7, c.sp), ease: "easeOutBounce" })
2098
+ )
2099
+ );
2100
+ case "assemble":
2101
+ return seq(
2102
+ set({ x: g.x + (rs(11) - 0.5) * 1e3 * (0.5 + c.e), y: g.y + (rs(12) - 0.5) * 640, rotation: (rs(13) - 0.5) * 200, scale: 0.4, opacity: 0 }),
2103
+ par(
2104
+ tween(g.id, { opacity: 1 }, { duration: dur3(0.4, c.sp), ease: "easeOutQuad" }),
2105
+ tween(g.id, { x: g.x, y: g.y, rotation: 0, scale: 1 }, { duration: dur3(0.8, c.sp), ease: "easeOutExpo" })
2106
+ )
2107
+ );
2108
+ case "decode": {
2109
+ const steps = 4 + Math.floor(rs(7) * 3);
2110
+ const flicker = [set({ opacity: 1 })];
2111
+ for (let k = 0; k < steps; k++) {
2112
+ flicker.push(tween(g.id, { content: SCRAMBLE[Math.floor(rand(g.i, 20 + k + c.seed) * SCRAMBLE.length)] }, { duration: dur3(0.05, c.sp), ease: "linear" }));
2113
+ }
2114
+ flicker.push(tween(g.id, { content: g.ch }, { duration: dur3(0.05, c.sp), ease: "linear" }));
2115
+ return seq(...flicker);
2116
+ }
2117
+ }
2118
+ }
2119
+ function textIn(name, block, opts = {}) {
2120
+ const c = { ...ctx3(opts), fs: block.fontSize };
2121
+ const interval = (c.stag ?? IN_STAGGER[name]) / c.sp;
2122
+ return beat(opts.label ?? `text-in-${name}`, {}, [stagger(interval, ...block.glyphs.map((g) => glyphIn(name, g, c)))]);
2123
+ }
2124
+ function textLoop(name, block, opts = {}) {
2125
+ const win = { ...opts.from !== void 0 && { from: opts.from }, ...opts.until !== void 0 && { until: opts.until }, ...opts.ramp !== void 0 && { ramp: opts.ramp } };
2126
+ const f = opts.frequency ?? (name === "wave" ? 0.9 : name === "shimmer" ? 1.4 : 0.7);
2127
+ const ps = opts.phaseStep ?? 0.55;
2128
+ return block.glyphs.map((g, i) => {
2129
+ switch (name) {
2130
+ case "wave":
2131
+ return oscillate(g.id, "y", { amplitude: opts.amplitude ?? 9, frequency: f, phase: i * ps }, win);
2132
+ case "shimmer":
2133
+ return oscillate(g.id, "opacity", { amplitude: opts.amplitude ?? 0.25, frequency: f, phase: i * ps }, win);
2134
+ case "wobble":
2135
+ return oscillate(g.id, "rotation", { amplitude: opts.amplitude ?? 6, frequency: f, phase: i * ps }, win);
2136
+ case "float":
2137
+ return oscillate(g.id, "y", { amplitude: opts.amplitude ?? 5, frequency: f, phase: i * ps }, win);
2138
+ }
2139
+ });
2140
+ }
2141
+ var OUT_STAGGER = { shatter: 0.02, fly: 0.012, dissolve: 0, fall: 0.02, collapse: 0.02 };
2142
+ function glyphOut(name, g, c, block, dir) {
2143
+ const rs = (salt) => rand(g.i, salt + c.seed);
2144
+ switch (name) {
2145
+ case "shatter":
2146
+ return par(
2147
+ tween(g.id, { x: g.x + (rs(21) - 0.5) * 1100 * (0.6 + c.e), y: g.y + (rs(22) - 0.5) * 760 }, { duration: dur3(0.7, c.sp), ease: "easeInCubic" }),
2148
+ tween(g.id, { rotation: (rs(23) - 0.5) * 300, opacity: 0 }, { duration: dur3(0.7, c.sp), ease: "easeInQuad" })
2149
+ );
2150
+ case "fly":
2151
+ return par(
2152
+ tween(g.id, { x: g.x + dir[0] * 1200, y: g.y + dir[1] * 1200 }, { duration: dur3(0.6, c.sp), ease: "easeInCubic" }),
2153
+ tween(g.id, { opacity: 0 }, { duration: dur3(0.5, c.sp), ease: "easeInQuad" })
2154
+ );
2155
+ case "dissolve":
2156
+ return seq(wait(rs(31) * 0.5), par(
2157
+ tween(g.id, { opacity: 0 }, { duration: dur3(0.4, c.sp), ease: "easeInQuad" }),
2158
+ tween(g.id, { scale: 1.4 }, { duration: dur3(0.4, c.sp), ease: "easeOutQuad" })
2159
+ ));
2160
+ case "fall":
2161
+ return par(
2162
+ tween(g.id, { y: g.y + 700 + rs(41) * 200 }, { duration: dur3(0.8, c.sp), ease: "easeInQuad" }),
2163
+ tween(g.id, { rotation: (rs(42) - 0.5) * 120, opacity: 0 }, { duration: dur3(0.8, c.sp), ease: "easeInQuad" })
2164
+ );
2165
+ case "collapse":
2166
+ return par(
2167
+ tween(g.id, { x: block.x, y: block.y, scale: 0.2 }, { duration: dur3(0.5, c.sp), ease: "easeInBack" }),
2168
+ tween(g.id, { opacity: 0 }, { duration: dur3(0.5, c.sp), ease: "easeInQuad" })
2169
+ );
2170
+ }
2171
+ }
2172
+ function textOut(name, block, opts = {}) {
2173
+ const c = { ...ctx3(opts), fs: block.fontSize };
2174
+ const dir = opts.dir ?? [0, -1];
2175
+ const steps = block.glyphs.map((g) => glyphOut(name, g, c, block, dir));
2176
+ const interval = (c.stag ?? OUT_STAGGER[name]) / c.sp;
2177
+ const body = interval > 0 ? stagger(interval, ...steps) : par(...steps);
2178
+ return beat(opts.label ?? `text-out-${name}`, {}, [body]);
2179
+ }
2180
+ function textTypeCues(block, opts) {
2181
+ const interval = opts.interval ?? 0.065;
2182
+ const gain = opts.gain ?? 0.4;
2183
+ const off = opts.offset ?? 0;
2184
+ const KEYS = ["001", "004", "007", "010", "014"];
2185
+ return block.glyphs.map((g, i) => ({
2186
+ at: opts.at,
2187
+ offset: off + i * interval,
2188
+ file: `keypress-${KEYS[i % KEYS.length]}.wav`,
2189
+ gain: gain + 0.2 * rand(i, 31)
2190
+ }));
2191
+ }
2192
+
1691
2193
  // ../core/src/motionOps.ts
1692
2194
  var MOTION_OPS = ["rotate", "zoom", "ken-burns", "slide-in", "fade", "draw-on", "pulse"];
1693
- var clamp013 = (n3) => Math.max(0, Math.min(1, n3));
2195
+ var clamp014 = (n3) => Math.max(0, Math.min(1, n3));
1694
2196
  function settleEase2(e) {
1695
2197
  return e < 0.34 ? "easeOutCubic" : e < 0.67 ? "easeOutBack" : "easeOutElastic";
1696
2198
  }
@@ -1708,7 +2210,7 @@ function fromVec2(from, dist) {
1708
2210
  }
1709
2211
  var motionOpLabel = (name, target) => `op-${name}-${target}`;
1710
2212
  function motionOp(name, target, opts = {}) {
1711
- const e = clamp013(opts.energy ?? 0.5);
2213
+ const e = clamp014(opts.energy ?? 0.5);
1712
2214
  const sp = Math.max(0.25, opts.speed ?? 1);
1713
2215
  const amt = opts.amount ?? 1;
1714
2216
  const b = { scale: 1, x: 0, y: 0, rotation: 0, ...opts.base };
@@ -2411,29 +2913,29 @@ function sketchToTimeline(sketch, nodeIds) {
2411
2913
  const steps = [];
2412
2914
  events.forEach((ev, i) => {
2413
2915
  const node = nodeIds[i % nodeIds.length];
2414
- const dur3 = Math.max(0.05, ev.t1 - ev.t0);
2916
+ const dur4 = Math.max(0.05, ev.t1 - ev.t0);
2415
2917
  const ease = easeFor(ev.easing);
2416
2918
  let motion;
2417
2919
  switch (ev.kind) {
2418
2920
  case "enter":
2419
- motion = tween(node, { opacity: 1 }, { duration: dur3, ease });
2921
+ motion = tween(node, { opacity: 1 }, { duration: dur4, ease });
2420
2922
  break;
2421
2923
  case "exit":
2422
- motion = tween(node, { opacity: 0 }, { duration: dur3, ease });
2924
+ motion = tween(node, { opacity: 0 }, { duration: dur4, ease });
2423
2925
  break;
2424
2926
  case "emphasis": {
2425
2927
  const peak = 1 + Math.max(0.08, Math.min(0.5, ev.magnitude));
2426
2928
  motion = seq(
2427
- tween(node, { scale: peak }, { duration: dur3 / 2, ease: "easeOutCubic" }),
2428
- tween(node, { scale: 1 }, { duration: dur3 / 2, ease: "easeInOutQuad" })
2929
+ tween(node, { scale: peak }, { duration: dur4 / 2, ease: "easeOutCubic" }),
2930
+ tween(node, { scale: 1 }, { duration: dur4 / 2, ease: "easeInOutQuad" })
2429
2931
  );
2430
2932
  break;
2431
2933
  }
2432
2934
  case "scale":
2433
- motion = tween(node, { scale: 1 + Math.max(-0.5, Math.min(0.5, ev.magnitude)) }, { duration: dur3, ease });
2935
+ motion = tween(node, { scale: 1 + Math.max(-0.5, Math.min(0.5, ev.magnitude)) }, { duration: dur4, ease });
2434
2936
  break;
2435
2937
  case "move":
2436
- motion = tween(node, { opacity: 1 }, { duration: dur3, ease });
2938
+ motion = tween(node, { opacity: 1 }, { duration: dur4, ease });
2437
2939
  break;
2438
2940
  }
2439
2941
  steps.push(ev.t0 > 0 ? seq(wait(ev.t0), motion) : motion);
@@ -2499,8 +3001,13 @@ export {
2499
3001
  scene,
2500
3002
  seq,
2501
3003
  sketchToTimeline,
3004
+ splitText,
2502
3005
  stagger,
2503
3006
  text,
3007
+ textIn,
3008
+ textLoop,
3009
+ textOut,
3010
+ textTypeCues,
2504
3011
  to,
2505
3012
  tween,
2506
3013
  validateComposition,
@@ -10,6 +10,7 @@ export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, DEVICE_PR
10
10
  export { rig, rigPose, poseTo, ikReach, humanoid, ovalPath, type Bone, type RigOpts, type Pose, type HumanoidOpts } from "./rig.js";
11
11
  export { characterPreset, CHARACTER_PRESET_NAMES, type CharacterPresetName, type CharacterPresetOpts } from "./characterPreset.js";
12
12
  export { figure, type FigureStyle, type FigureOpts, type FigurePalette } from "./figure.js";
13
+ export { splitText, textIn, textLoop, textOut, textTypeCues, type SplitOpts, type Glyph, type TextBlock, type FontWeight, type TextInName, type TextLoopName, type TextOutName, type TextLoopOpts, type TextOutOpts, type TypeCueOpts, } from "./textFx.js";
13
14
  export { motionOp, motionOpLabel, MOTION_OPS, type MotionOpName, type MotionOpOpts, type MotionOpResult } from "./motionOps.js";
14
15
  export { resolveAudioPlan, resolveCompositionAudioPlan, SFX_DURATION, type AudioPlan, type ResolvedCue, } from "./audio.js";
15
16
  export { evaluate, sampleProp, nodeParentMatrix, type DisplayList, type DisplayOp, type Mat2D, type ClipRegion, type TextAlign, type TextBaseline, } from "./evaluate.js";
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Kinetic text — a deterministic per-glyph text splitter plus a library of
3
+ * seeded effect generators (entrance / sustained / exit). The text analog of
4
+ * `motionPreset` / `characterPreset`: `splitText()` lays a phrase out as
5
+ * center-anchored `text` nodes (advances measured from the real font, so layout
6
+ * matches the render), then `textIn` / `textLoop` / `textOut` animate the glyphs.
7
+ *
8
+ * const T = splitText("MOTION IS DATA", { id: "t", x: 960, y: 470, fontSize: 130 });
9
+ * // nodes: [...T.nodes]
10
+ * // timeline: seq(textIn("typewriter", T), wait(2), textOut("shatter", T, { seed: 3 }))
11
+ * // behaviors: textLoop("wave", T, { from: 1.6, until: 3.6 })
12
+ */
13
+ import { type BehaviorWindow } from "./dsl.js";
14
+ import type { AudioCueIR, BehaviorIR, NodeIR, TimelineIR } from "./ir.js";
15
+ export type FontWeight = 400 | 700 | 800;
16
+ export interface SplitOpts {
17
+ /** Id PREFIX → glyph ids `${id}-${i}`. */
18
+ id: string;
19
+ /** Anchor point of the line. */
20
+ x: number;
21
+ y: number;
22
+ fontSize: number;
23
+ fontWeight?: FontWeight;
24
+ fill?: string;
25
+ /** Extra px between glyphs (tracking). */
26
+ letterSpacing?: number;
27
+ /** Horizontal alignment about `x` (default "center"). */
28
+ align?: "left" | "center";
29
+ /** Animate per glyph or per word (default "glyph"). */
30
+ unit?: "glyph" | "word";
31
+ /** Starting opacity of the nodes (default 0, for entrances). */
32
+ opacity?: number;
33
+ }
34
+ export interface Glyph {
35
+ id: string;
36
+ /** The character (glyph unit) or word (word unit). */
37
+ ch: string;
38
+ /** Home centre (the laid-out resting position). */
39
+ x: number;
40
+ y: number;
41
+ /** This unit's advance width in px. */
42
+ advance: number;
43
+ /** Index in declaration order. */
44
+ i: number;
45
+ }
46
+ export interface TextBlock {
47
+ nodes: NodeIR[];
48
+ glyphs: Glyph[];
49
+ ids: string[];
50
+ /** Total laid-out width in px. */
51
+ width: number;
52
+ x: number;
53
+ y: number;
54
+ fontSize: number;
55
+ }
56
+ export declare function splitText(textStr: string, opts: SplitOpts): TextBlock;
57
+ interface FxOpts {
58
+ speed?: number;
59
+ energy?: number;
60
+ seed?: number;
61
+ stagger?: number;
62
+ label?: string;
63
+ }
64
+ export type TextInName = "typewriter" | "cascade" | "rise" | "bounce" | "assemble" | "decode";
65
+ export declare function textIn(name: TextInName, block: TextBlock, opts?: FxOpts): TimelineIR;
66
+ export type TextLoopName = "wave" | "shimmer" | "wobble" | "float";
67
+ export interface TextLoopOpts extends BehaviorWindow {
68
+ amplitude?: number;
69
+ frequency?: number;
70
+ /** phase offset per glyph (the travelling-wave speed). */
71
+ phaseStep?: number;
72
+ }
73
+ export declare function textLoop(name: TextLoopName, block: TextBlock, opts?: TextLoopOpts): BehaviorIR[];
74
+ export type TextOutName = "shatter" | "fly" | "dissolve" | "fall" | "collapse";
75
+ export interface TextOutOpts extends FxOpts {
76
+ /** direction for "fly" (default up). */
77
+ dir?: [number, number];
78
+ }
79
+ export declare function textOut(name: TextOutName, block: TextBlock, opts?: TextOutOpts): TimelineIR;
80
+ export interface TypeCueOpts {
81
+ /** the timeline label the typewriter `textIn` starts at. */
82
+ at: string | number;
83
+ /** seconds between keystrokes (match the textIn stagger / speed). */
84
+ interval?: number;
85
+ gain?: number;
86
+ /** offset of the first key from `at`. */
87
+ offset?: number;
88
+ }
89
+ /** Per-glyph CC0 keypress for `textIn("typewriter", …)`. */
90
+ export declare function textTypeCues(block: TextBlock, opts: TypeCueOpts): AudioCueIR[];
91
+ export {};
@@ -0,0 +1,3 @@
1
+ export declare const INTER_ADVANCE: Record<number, Record<string, number>>;
2
+ /** Average advance per weight — the fallback for glyphs outside the table. */
3
+ export declare const INTER_FALLBACK: Record<number, number>;
@@ -167,6 +167,38 @@ no new renderer concept, so overlays/preview/determinism all apply.
167
167
  set it when the same preset is used more than once in a scene). Legs use
168
168
  `ikReach`, arms FK; pure keyframes, so add continuous idle yourself with `oscillate`.
169
169
 
170
+ ## Kinetic text (split + effect presets)
171
+
172
+ reframe's `text` node renders a whole string as one node, so per-glyph effects
173
+ need the string split into per-character nodes. `splitText` does that once;
174
+ seeded effect generators animate the glyphs (the text analog of `motionPreset`).
175
+
176
+ - `splitText(text, { id, x, y, fontSize, fontWeight?, fill?, letterSpacing?,
177
+ align?, unit?, opacity? }) → TextBlock` — lays the phrase out as center-anchored
178
+ `text` nodes using **real Inter advance widths** (so layout matches the render).
179
+ Returns `{ nodes, glyphs, ids, width, ... }`; put `...block.nodes` in `nodes`.
180
+ Glyph ids are `${id}-${i}` (stable regen addresses). `unit: "word"` animates
181
+ whole words instead of letters; `opacity: 0` (default) starts hidden for entrances.
182
+ - `textIn(name, block, { speed?, energy?, seed?, stagger?, label? }) → TimelineIR`
183
+ (a `beat`) — entrance: `typewriter`, `cascade`, `rise`, `bounce`, `assemble`
184
+ (fly in from a seeded scatter), `decode` (scramble through random glyphs then lock).
185
+ - `textLoop(name, block, { from?, until?, ramp?, amplitude?, frequency?, phaseStep? })
186
+ → BehaviorIR[]` — sustained: `wave` (standing sine), `shimmer`, `wobble`, `float`.
187
+ Spread it into `behaviors`.
188
+ - `textOut(name, block, { …, dir? }) → TimelineIR` — exit: `shatter` (random
189
+ direction + spin + fade), `fly` (directional), `dissolve`, `fall`, `collapse`.
190
+ - `textTypeCues(block, { at, interval?, gain? }) → AudioCueIR[]` — per-glyph CC0
191
+ keypress for a typewriter entrance; spread into `audio.cues`.
192
+
193
+ ```ts
194
+ const T = splitText("MOTION IS DATA", { id: "t", x: 960, y: 470, fontSize: 130 });
195
+ // nodes: [...T.nodes]
196
+ // timeline: seq(textIn("cascade", T), wait(2), textOut("shatter", T, { seed: 3 }))
197
+ // behaviors: textLoop("wave", T, { from: 1.6, until: 3.6 })
198
+ ```
199
+ Every effect is seeded (same `seed` → identical) and pure keyframes. To time a
200
+ `textLoop` window, add up the `textIn` beat length (≈ `(n-1)·stagger + glyphDur`).
201
+
170
202
  ## Audio (optional)
171
203
 
172
204
  Label-anchored sound design — cues follow retiming and regeneration:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reframe-video",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Declarative motion graphics that AI can write and humans can tweak — human edits survive AI regeneration. Deterministic mp4 renders from a plain-data scene format.",
5
5
  "keywords": [
6
6
  "motion-graphics",