reframe-video 0.3.0 → 0.5.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
@@ -1044,6 +1044,14 @@ var init_devicePreset = __esm({
1044
1044
  }
1045
1045
  });
1046
1046
 
1047
+ // ../core/src/cursor.ts
1048
+ var init_cursor = __esm({
1049
+ "../core/src/cursor.ts"() {
1050
+ "use strict";
1051
+ init_dsl();
1052
+ }
1053
+ });
1054
+
1047
1055
  // ../core/src/rig.ts
1048
1056
  var init_rig = __esm({
1049
1057
  "../core/src/rig.ts"() {
@@ -1070,6 +1078,22 @@ var init_figure = __esm({
1070
1078
  }
1071
1079
  });
1072
1080
 
1081
+ // ../core/src/textMetrics.ts
1082
+ var init_textMetrics = __esm({
1083
+ "../core/src/textMetrics.ts"() {
1084
+ "use strict";
1085
+ }
1086
+ });
1087
+
1088
+ // ../core/src/textFx.ts
1089
+ var init_textFx = __esm({
1090
+ "../core/src/textFx.ts"() {
1091
+ "use strict";
1092
+ init_dsl();
1093
+ init_textMetrics();
1094
+ }
1095
+ });
1096
+
1073
1097
  // ../core/src/motionOps.ts
1074
1098
  var init_motionOps = __esm({
1075
1099
  "../core/src/motionOps.ts"() {
@@ -1288,9 +1312,11 @@ var init_src = __esm({
1288
1312
  init_path();
1289
1313
  init_presets();
1290
1314
  init_devicePreset();
1315
+ init_cursor();
1291
1316
  init_rig();
1292
1317
  init_characterPreset();
1293
1318
  init_figure();
1319
+ init_textFx();
1294
1320
  init_motionOps();
1295
1321
  init_audio();
1296
1322
  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],
@@ -1089,6 +1089,11 @@ function deviceBounds(name, opts = {}) {
1089
1089
  const b = BOUNDS[name];
1090
1090
  return isLandscape(name, opts) ? { width: b.height, height: b.width } : { ...b };
1091
1091
  }
1092
+ function deviceScreenPoint(name, opts, local) {
1093
+ const c = deviceScreenCenter(name, opts);
1094
+ const s = opts.scale ?? 1;
1095
+ return [(opts.x ?? 0) + s * (c.x + local[0]), (opts.y ?? 0) + s * (c.y + local[1])];
1096
+ }
1092
1097
  function screenGroup(id, p, o, cx, cy, dims, content) {
1093
1098
  return group({ id: `${id}-screen`, x: cx, y: cy, clip: { kind: "rect", x: -dims.width / 2, y: -dims.height / 2, width: dims.width, height: dims.height, radius: dims.radius } }, [
1094
1099
  rect({ id: `${id}-screenbg`, x: 0, y: 0, anchor: "center", width: dims.width, height: dims.height, fill: o.screen ?? p.screen }),
@@ -1263,6 +1268,73 @@ function devicePreset(name, opts = {}) {
1263
1268
  );
1264
1269
  }
1265
1270
 
1271
+ // ../core/src/cursor.ts
1272
+ var ARROW_D = "M0 0 L0 30 L8 23 L12.6 33 L17 31 L12.4 21.4 L21 21.4 Z";
1273
+ function cursor(opts = {}) {
1274
+ const id = opts.id ?? "cursor";
1275
+ const style = opts.style ?? "arrow";
1276
+ const fill = opts.fill ?? "#FFFFFF";
1277
+ const accent = opts.accent ?? "#FF5A1F";
1278
+ const art = style === "arrow" ? [path({ id: `${id}-arrow`, d: ARROW_D, x: 0, y: 0, fill, stroke: "#15171E", strokeWidth: 2 })] : style === "dot" ? [ellipse({ id: `${id}-dot`, x: 0, y: 0, width: 18, height: 18, fill: accent, anchor: "center" })] : [ellipse({ id: `${id}-ring`, x: 0, y: 0, width: 22, height: 22, fill: "none", stroke: accent, strokeWidth: 3, anchor: "center" })];
1279
+ return group(
1280
+ { id, x: opts.x ?? 0, y: opts.y ?? 0, scale: opts.scale ?? 1, opacity: opts.opacity ?? 1 },
1281
+ [
1282
+ // ripple ring (behind the pointer), emanates from the hotspot on click
1283
+ ellipse({ id: `${id}-ripple`, x: 0, y: 0, width: 30, height: 30, fill: "none", stroke: accent, strokeWidth: 3, opacity: 0, scale: 0, anchor: "center" }),
1284
+ // the pointer art lives in its own group so a click "tap" can scale it
1285
+ // independently of the cursor's resting scale
1286
+ group({ id: `${id}-art`, x: 0, y: 0 }, art)
1287
+ ]
1288
+ );
1289
+ }
1290
+ var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
1291
+ function cursorTo(id, from, to2, opts = {}) {
1292
+ const dx = to2[0] - from[0], dy = to2[1] - from[1];
1293
+ const dist = Math.hypot(dx, dy) || 1;
1294
+ const arc = opts.arc ?? 0.12;
1295
+ const mid = [(from[0] + to2[0]) / 2 + -dy / dist * arc * dist, (from[1] + to2[1]) / 2 + dx / dist * arc * dist];
1296
+ const duration = opts.duration ?? clamp(dist / 1400, 0.4, 0.9);
1297
+ return motionPath(id, [from, mid, to2], { duration, ease: opts.ease ?? "easeInOutCubic", curviness: 1, ...opts.label && { label: opts.label } });
1298
+ }
1299
+ function cursorPath(id, points, opts = {}) {
1300
+ return motionPath(id, points, {
1301
+ duration: opts.duration ?? clamp(points.length * 0.5, 0.5, 4),
1302
+ ease: opts.ease ?? "easeInOutCubic",
1303
+ curviness: opts.curviness ?? 1,
1304
+ ...opts.label && { label: opts.label }
1305
+ });
1306
+ }
1307
+ function clickBody(id, o) {
1308
+ const sp = Math.max(0.25, o.speed ?? 1);
1309
+ const d = (b) => b / sp;
1310
+ const out = [
1311
+ // the pointer taps
1312
+ seq(tween(`${id}-art`, { scale: 0.82 }, { duration: d(0.08), ease: "easeOutQuad" }), tween(`${id}-art`, { scale: 1 }, { duration: d(0.1), ease: "easeOutBack" }))
1313
+ ];
1314
+ if (o.ripple !== false) {
1315
+ out.push(seq(
1316
+ tween(`${id}-ripple`, { scale: 0.2, opacity: 0.55 }, { duration: 1e-3 }),
1317
+ par(
1318
+ tween(`${id}-ripple`, { scale: 5 }, { duration: d(0.5), ease: "easeOutCubic" }),
1319
+ tween(`${id}-ripple`, { opacity: 0 }, { duration: d(0.5), ease: "easeOutQuad" })
1320
+ )
1321
+ ));
1322
+ }
1323
+ if (o.press) {
1324
+ out.push(seq(tween(o.press, { scale: 0.94 }, { duration: d(0.08), ease: "easeOutQuad" }), tween(o.press, { scale: 1 }, { duration: d(0.14), ease: "easeOutBack" })));
1325
+ }
1326
+ return out;
1327
+ }
1328
+ function cursorClick(id, opts = {}) {
1329
+ return beat(opts.label ?? "cursor-click", {}, [par(...clickBody(id, opts))]);
1330
+ }
1331
+ function cursorDouble(id, opts = {}) {
1332
+ const sp = Math.max(0.25, opts.speed ?? 1);
1333
+ return beat(opts.label ?? "cursor-double", {}, [
1334
+ seq(par(...clickBody(id, { ...opts, ripple: false })), wait(0.12 / sp), par(...clickBody(id, opts)))
1335
+ ]);
1336
+ }
1337
+
1266
1338
  // ../core/src/rig.ts
1267
1339
  var DEFAULT_LINE = "#FFE3D2";
1268
1340
  var DEFAULT_FILL = "#0E1424";
@@ -1378,7 +1450,7 @@ function makeRng2(seed) {
1378
1450
  }
1379
1451
  var dur2 = (base, sp) => base / sp;
1380
1452
  function ctx2(o) {
1381
- const rand = makeRng2((o.seed ?? 0) + 1);
1453
+ const rand2 = makeRng2((o.seed ?? 0) + 1);
1382
1454
  return {
1383
1455
  g: o.target,
1384
1456
  label: o.label,
@@ -1388,8 +1460,8 @@ function ctx2(o) {
1388
1460
  facing: o.facing ?? 1,
1389
1461
  at: o.at ?? [0, 0],
1390
1462
  travel: o.travel,
1391
- rand,
1392
- jit: (amp) => (rand() - 0.5) * 2 * amp
1463
+ rand: rand2,
1464
+ jit: (amp) => (rand2() - 0.5) * 2 * amp
1393
1465
  };
1394
1466
  }
1395
1467
  var round = (v) => Math.round(v * 1e3) / 1e3;
@@ -1688,9 +1760,511 @@ function figure(opts = {}) {
1688
1760
  return rig(buildSkeleton(id, parts), rigOpts);
1689
1761
  }
1690
1762
 
1763
+ // ../core/src/textMetrics.ts
1764
+ var INTER_ADVANCE = {
1765
+ "400": {
1766
+ "0": 63.09,
1767
+ "1": 40.67,
1768
+ "2": 60.99,
1769
+ "3": 61.77,
1770
+ "4": 64.6,
1771
+ "5": 59.33,
1772
+ "6": 62.01,
1773
+ "7": 56.59,
1774
+ "8": 61.87,
1775
+ "9": 62.01,
1776
+ " ": 28.13,
1777
+ "!": 28.76,
1778
+ '"': 46.58,
1779
+ "#": 63.33,
1780
+ "$": 64.16,
1781
+ "%": 98.19,
1782
+ "&": 64.4,
1783
+ "'": 29.98,
1784
+ "(": 36.47,
1785
+ ")": 36.47,
1786
+ "*": 50.1,
1787
+ "+": 66.16,
1788
+ ",": 28.81,
1789
+ "-": 46,
1790
+ ".": 28.81,
1791
+ "/": 36.04,
1792
+ ":": 28.81,
1793
+ ";": 30.18,
1794
+ "<": 66.16,
1795
+ "=": 66.16,
1796
+ ">": 66.16,
1797
+ "?": 51.12,
1798
+ "@": 96.58,
1799
+ "A": 68.99,
1800
+ "B": 65.43,
1801
+ "C": 73.05,
1802
+ "D": 72.17,
1803
+ "E": 60.11,
1804
+ "F": 59.03,
1805
+ "G": 74.61,
1806
+ "H": 74.32,
1807
+ "I": 26.86,
1808
+ "J": 57.08,
1809
+ "K": 67.19,
1810
+ "L": 56.54,
1811
+ "M": 90.33,
1812
+ "N": 75.34,
1813
+ "O": 76.46,
1814
+ "P": 63.87,
1815
+ "Q": 76.46,
1816
+ "R": 64.36,
1817
+ "S": 64.16,
1818
+ "T": 64.55,
1819
+ "U": 74.41,
1820
+ "V": 68.99,
1821
+ "W": 98.54,
1822
+ "X": 68.21,
1823
+ "Y": 67.87,
1824
+ "Z": 62.89,
1825
+ "[": 36.47,
1826
+ "\\": 36.04,
1827
+ "]": 36.47,
1828
+ "^": 47.12,
1829
+ "_": 45.61,
1830
+ "`": 32.28,
1831
+ "a": 56.15,
1832
+ "b": 61.23,
1833
+ "c": 57.13,
1834
+ "d": 61.23,
1835
+ "e": 58.3,
1836
+ "f": 37.01,
1837
+ "g": 61.33,
1838
+ "h": 59.13,
1839
+ "i": 24.22,
1840
+ "j": 24.22,
1841
+ "k": 54.88,
1842
+ "l": 24.22,
1843
+ "m": 87.6,
1844
+ "n": 59.08,
1845
+ "o": 59.96,
1846
+ "p": 61.23,
1847
+ "q": 61.23,
1848
+ "r": 37.65,
1849
+ "s": 52.78,
1850
+ "t": 32.71,
1851
+ "u": 59.13,
1852
+ "v": 56.2,
1853
+ "w": 81.84,
1854
+ "x": 54.59,
1855
+ "y": 56.2,
1856
+ "z": 55.22,
1857
+ "{": 42.63,
1858
+ "|": 33.25,
1859
+ "}": 42.63,
1860
+ "~": 66.16
1861
+ },
1862
+ "700": {
1863
+ "0": 67.43,
1864
+ "1": 43.12,
1865
+ "2": 62.94,
1866
+ "3": 64.55,
1867
+ "4": 67.63,
1868
+ "5": 62.21,
1869
+ "6": 64.94,
1870
+ "7": 58.15,
1871
+ "8": 65.09,
1872
+ "9": 64.94,
1873
+ " ": 23.68,
1874
+ "!": 33.79,
1875
+ '"': 55.13,
1876
+ "#": 64.89,
1877
+ "$": 65.48,
1878
+ "%": 101.56,
1879
+ "&": 67.19,
1880
+ "'": 33.89,
1881
+ "(": 37.7,
1882
+ ")": 37.7,
1883
+ "*": 55.91,
1884
+ "+": 67.87,
1885
+ ",": 33.4,
1886
+ "-": 46.78,
1887
+ ".": 33.4,
1888
+ "/": 38.82,
1889
+ ":": 33.4,
1890
+ ";": 34.28,
1891
+ "<": 67.87,
1892
+ "=": 67.87,
1893
+ ">": 67.87,
1894
+ "?": 55.96,
1895
+ "@": 101.61,
1896
+ "A": 74.66,
1897
+ "B": 66.16,
1898
+ "C": 73.97,
1899
+ "D": 72.22,
1900
+ "E": 60.74,
1901
+ "F": 58.69,
1902
+ "G": 75.05,
1903
+ "H": 74.71,
1904
+ "I": 28.08,
1905
+ "J": 58.45,
1906
+ "K": 71.92,
1907
+ "L": 56.54,
1908
+ "M": 93.16,
1909
+ "N": 76.22,
1910
+ "O": 77.05,
1911
+ "P": 64.79,
1912
+ "Q": 77.69,
1913
+ "R": 65.67,
1914
+ "S": 65.48,
1915
+ "T": 66.75,
1916
+ "U": 73.19,
1917
+ "V": 74.66,
1918
+ "W": 103.76,
1919
+ "X": 73.83,
1920
+ "Y": 73.1,
1921
+ "Z": 66.41,
1922
+ "[": 37.7,
1923
+ "\\": 38.82,
1924
+ "]": 37.7,
1925
+ "^": 48.68,
1926
+ "_": 47.61,
1927
+ "`": 36.52,
1928
+ "a": 58.06,
1929
+ "b": 63.04,
1930
+ "c": 58.84,
1931
+ "d": 63.04,
1932
+ "e": 59.57,
1933
+ "f": 39.79,
1934
+ "g": 63.18,
1935
+ "h": 62.26,
1936
+ "i": 27.1,
1937
+ "j": 27.1,
1938
+ "k": 58.01,
1939
+ "l": 27.1,
1940
+ "m": 91.26,
1941
+ "n": 62.26,
1942
+ "o": 61.33,
1943
+ "p": 63.04,
1944
+ "q": 63.04,
1945
+ "r": 40.72,
1946
+ "s": 56.01,
1947
+ "t": 36.62,
1948
+ "u": 62.26,
1949
+ "v": 59.96,
1950
+ "w": 85.01,
1951
+ "x": 58.01,
1952
+ "y": 60.21,
1953
+ "z": 57.28,
1954
+ "{": 46.88,
1955
+ "|": 37.16,
1956
+ "}": 46.88,
1957
+ "~": 67.87
1958
+ },
1959
+ "800": {
1960
+ "0": 69.19,
1961
+ "1": 44.14,
1962
+ "2": 63.77,
1963
+ "3": 65.67,
1964
+ "4": 68.85,
1965
+ "5": 63.38,
1966
+ "6": 66.16,
1967
+ "7": 58.79,
1968
+ "8": 66.41,
1969
+ "9": 66.16,
1970
+ " ": 21.88,
1971
+ "!": 35.84,
1972
+ '"': 58.64,
1973
+ "#": 65.53,
1974
+ "$": 66.02,
1975
+ "%": 102.93,
1976
+ "&": 68.31,
1977
+ "'": 35.45,
1978
+ "(": 38.23,
1979
+ ")": 38.23,
1980
+ "*": 58.25,
1981
+ "+": 68.55,
1982
+ ",": 35.25,
1983
+ "-": 47.12,
1984
+ ".": 35.25,
1985
+ "/": 39.99,
1986
+ ":": 35.25,
1987
+ ";": 35.99,
1988
+ "<": 68.55,
1989
+ "=": 68.55,
1990
+ ">": 68.55,
1991
+ "?": 57.91,
1992
+ "@": 103.61,
1993
+ "A": 76.95,
1994
+ "B": 66.46,
1995
+ "C": 74.37,
1996
+ "D": 72.27,
1997
+ "E": 60.99,
1998
+ "F": 58.54,
1999
+ "G": 75.24,
2000
+ "H": 74.85,
2001
+ "I": 28.56,
2002
+ "J": 58.98,
2003
+ "K": 73.83,
2004
+ "L": 56.54,
2005
+ "M": 94.34,
2006
+ "N": 76.56,
2007
+ "O": 77.29,
2008
+ "P": 65.19,
2009
+ "Q": 78.17,
2010
+ "R": 66.21,
2011
+ "S": 66.02,
2012
+ "T": 67.68,
2013
+ "U": 72.71,
2014
+ "V": 76.95,
2015
+ "W": 105.86,
2016
+ "X": 76.12,
2017
+ "Y": 75.2,
2018
+ "Z": 67.87,
2019
+ "[": 38.23,
2020
+ "\\": 39.99,
2021
+ "]": 38.23,
2022
+ "^": 49.32,
2023
+ "_": 48.44,
2024
+ "`": 38.23,
2025
+ "a": 58.84,
2026
+ "b": 63.77,
2027
+ "c": 59.52,
2028
+ "d": 63.77,
2029
+ "e": 60.06,
2030
+ "f": 40.97,
2031
+ "g": 63.92,
2032
+ "h": 63.53,
2033
+ "i": 28.32,
2034
+ "j": 28.32,
2035
+ "k": 59.28,
2036
+ "l": 28.32,
2037
+ "m": 92.72,
2038
+ "n": 63.53,
2039
+ "o": 61.91,
2040
+ "p": 63.77,
2041
+ "q": 63.77,
2042
+ "r": 41.99,
2043
+ "s": 57.32,
2044
+ "t": 38.18,
2045
+ "u": 63.53,
2046
+ "v": 61.52,
2047
+ "w": 86.28,
2048
+ "x": 59.42,
2049
+ "y": 61.82,
2050
+ "z": 58.11,
2051
+ "{": 48.63,
2052
+ "|": 38.77,
2053
+ "}": 48.63,
2054
+ "~": 68.55
2055
+ }
2056
+ };
2057
+ var INTER_FALLBACK = {
2058
+ "400": 56.16,
2059
+ "700": 58.74,
2060
+ "800": 59.79
2061
+ };
2062
+
2063
+ // ../core/src/textFx.ts
2064
+ var clamp013 = (v) => Math.max(0, Math.min(1, v));
2065
+ var fract = (v) => v - Math.floor(v);
2066
+ var rand = (i, salt) => fract(Math.sin(i * 127.1 + salt * 311.7) * 43758.5453);
2067
+ var dur3 = (base, sp) => base / sp;
2068
+ var SCRAMBLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&@";
2069
+ var advance = (ch, weight, fontSize) => (INTER_ADVANCE[weight]?.[ch] ?? INTER_FALLBACK[weight]) * (fontSize / 100);
2070
+ function splitText(textStr, opts) {
2071
+ const { id, x, y, fontSize } = opts;
2072
+ const weight = opts.fontWeight ?? 800;
2073
+ const fill = opts.fill ?? "#FFFFFF";
2074
+ const ls = opts.letterSpacing ?? 0;
2075
+ const align = opts.align ?? "center";
2076
+ const unit = opts.unit ?? "glyph";
2077
+ const opacity = opts.opacity ?? 0;
2078
+ const chars = [...textStr];
2079
+ let total = 0;
2080
+ chars.forEach((ch, i) => {
2081
+ total += advance(ch, weight, fontSize) + (i < chars.length - 1 ? ls : 0);
2082
+ });
2083
+ let cursor2 = align === "center" ? x - total / 2 : x;
2084
+ const glyphs = [];
2085
+ const nodes = [];
2086
+ const mk = (ch, cx, adv, lsProp) => {
2087
+ const g = { id: `${id}-${glyphs.length}`, ch, x: cx, y, advance: adv, i: glyphs.length };
2088
+ glyphs.push(g);
2089
+ nodes.push(
2090
+ text({
2091
+ id: g.id,
2092
+ x: cx,
2093
+ y,
2094
+ content: ch,
2095
+ fontFamily: "Inter",
2096
+ fontSize,
2097
+ fontWeight: weight,
2098
+ fill,
2099
+ anchor: "center",
2100
+ opacity,
2101
+ ...lsProp ? { letterSpacing: lsProp } : {}
2102
+ })
2103
+ );
2104
+ };
2105
+ if (unit === "word") {
2106
+ let i = 0;
2107
+ while (i < chars.length) {
2108
+ if (chars[i] === " ") {
2109
+ cursor2 += advance(" ", weight, fontSize) + ls;
2110
+ i++;
2111
+ continue;
2112
+ }
2113
+ let word = "";
2114
+ let w = 0;
2115
+ const startCursor = cursor2;
2116
+ while (i < chars.length && chars[i] !== " ") {
2117
+ const a = advance(chars[i], weight, fontSize);
2118
+ word += chars[i];
2119
+ w += a + (chars[i + 1] && chars[i + 1] !== " " ? ls : 0);
2120
+ i++;
2121
+ }
2122
+ mk(word, startCursor + w / 2, w, ls);
2123
+ cursor2 = startCursor + w + ls;
2124
+ }
2125
+ } else {
2126
+ chars.forEach((ch) => {
2127
+ const a = advance(ch, weight, fontSize);
2128
+ if (ch !== " ") mk(ch, cursor2 + a / 2, a);
2129
+ cursor2 += a + ls;
2130
+ });
2131
+ }
2132
+ return { nodes, glyphs, ids: glyphs.map((g) => g.id), width: total, x, y, fontSize };
2133
+ }
2134
+ var ctx3 = (o) => ({
2135
+ sp: Math.max(0.25, o.speed ?? 1),
2136
+ e: clamp013(o.energy ?? 0.5),
2137
+ seed: o.seed ?? 0,
2138
+ fs: 0,
2139
+ stag: o.stagger
2140
+ });
2141
+ var IN_STAGGER = { typewriter: 0.065, cascade: 0.04, rise: 0.03, bounce: 0.045, assemble: 0.05, decode: 0.05 };
2142
+ function glyphIn(name, g, c) {
2143
+ const set = (props) => tween(g.id, props, { duration: 1e-3 });
2144
+ const rs = (salt) => rand(g.i, salt + c.seed);
2145
+ switch (name) {
2146
+ case "typewriter":
2147
+ return tween(g.id, { opacity: 1 }, { duration: dur3(0.04, c.sp), ease: "linear" });
2148
+ case "cascade":
2149
+ return seq(
2150
+ set({ y: g.y + 56, opacity: 0 }),
2151
+ par(
2152
+ tween(g.id, { opacity: 1 }, { duration: dur3(0.22, c.sp), ease: "easeOutQuad" }),
2153
+ tween(g.id, { y: g.y }, { duration: dur3(0.34, c.sp), ease: "easeOutCubic" })
2154
+ )
2155
+ );
2156
+ case "rise":
2157
+ return seq(
2158
+ set({ y: g.y + 36, opacity: 0 }),
2159
+ par(
2160
+ tween(g.id, { opacity: 1 }, { duration: dur3(0.3, c.sp), ease: "easeOutQuad" }),
2161
+ tween(g.id, { y: g.y }, { duration: dur3(0.4, c.sp), ease: "easeOutQuad" })
2162
+ )
2163
+ );
2164
+ case "bounce":
2165
+ return seq(
2166
+ set({ y: g.y - 80 * (0.6 + c.e), opacity: 0, scale: 0.7 }),
2167
+ par(
2168
+ tween(g.id, { opacity: 1 }, { duration: dur3(0.2, c.sp), ease: "easeOutQuad" }),
2169
+ tween(g.id, { y: g.y, scale: 1 }, { duration: dur3(0.7, c.sp), ease: "easeOutBounce" })
2170
+ )
2171
+ );
2172
+ case "assemble":
2173
+ return seq(
2174
+ 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 }),
2175
+ par(
2176
+ tween(g.id, { opacity: 1 }, { duration: dur3(0.4, c.sp), ease: "easeOutQuad" }),
2177
+ tween(g.id, { x: g.x, y: g.y, rotation: 0, scale: 1 }, { duration: dur3(0.8, c.sp), ease: "easeOutExpo" })
2178
+ )
2179
+ );
2180
+ case "decode": {
2181
+ const steps = 4 + Math.floor(rs(7) * 3);
2182
+ const flicker = [set({ opacity: 1 })];
2183
+ for (let k = 0; k < steps; k++) {
2184
+ 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" }));
2185
+ }
2186
+ flicker.push(tween(g.id, { content: g.ch }, { duration: dur3(0.05, c.sp), ease: "linear" }));
2187
+ return seq(...flicker);
2188
+ }
2189
+ }
2190
+ }
2191
+ function textIn(name, block, opts = {}) {
2192
+ const c = { ...ctx3(opts), fs: block.fontSize };
2193
+ const interval = (c.stag ?? IN_STAGGER[name]) / c.sp;
2194
+ return beat(opts.label ?? `text-in-${name}`, {}, [stagger(interval, ...block.glyphs.map((g) => glyphIn(name, g, c)))]);
2195
+ }
2196
+ function textLoop(name, block, opts = {}) {
2197
+ const win = { ...opts.from !== void 0 && { from: opts.from }, ...opts.until !== void 0 && { until: opts.until }, ...opts.ramp !== void 0 && { ramp: opts.ramp } };
2198
+ const f = opts.frequency ?? (name === "wave" ? 0.9 : name === "shimmer" ? 1.4 : 0.7);
2199
+ const ps = opts.phaseStep ?? 0.55;
2200
+ return block.glyphs.map((g, i) => {
2201
+ switch (name) {
2202
+ case "wave":
2203
+ return oscillate(g.id, "y", { amplitude: opts.amplitude ?? 9, frequency: f, phase: i * ps }, win);
2204
+ case "shimmer":
2205
+ return oscillate(g.id, "opacity", { amplitude: opts.amplitude ?? 0.25, frequency: f, phase: i * ps }, win);
2206
+ case "wobble":
2207
+ return oscillate(g.id, "rotation", { amplitude: opts.amplitude ?? 6, frequency: f, phase: i * ps }, win);
2208
+ case "float":
2209
+ return oscillate(g.id, "y", { amplitude: opts.amplitude ?? 5, frequency: f, phase: i * ps }, win);
2210
+ }
2211
+ });
2212
+ }
2213
+ var OUT_STAGGER = { shatter: 0.02, fly: 0.012, dissolve: 0, fall: 0.02, collapse: 0.02 };
2214
+ function glyphOut(name, g, c, block, dir) {
2215
+ const rs = (salt) => rand(g.i, salt + c.seed);
2216
+ switch (name) {
2217
+ case "shatter":
2218
+ return par(
2219
+ 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" }),
2220
+ tween(g.id, { rotation: (rs(23) - 0.5) * 300, opacity: 0 }, { duration: dur3(0.7, c.sp), ease: "easeInQuad" })
2221
+ );
2222
+ case "fly":
2223
+ return par(
2224
+ tween(g.id, { x: g.x + dir[0] * 1200, y: g.y + dir[1] * 1200 }, { duration: dur3(0.6, c.sp), ease: "easeInCubic" }),
2225
+ tween(g.id, { opacity: 0 }, { duration: dur3(0.5, c.sp), ease: "easeInQuad" })
2226
+ );
2227
+ case "dissolve":
2228
+ return seq(wait(rs(31) * 0.5), par(
2229
+ tween(g.id, { opacity: 0 }, { duration: dur3(0.4, c.sp), ease: "easeInQuad" }),
2230
+ tween(g.id, { scale: 1.4 }, { duration: dur3(0.4, c.sp), ease: "easeOutQuad" })
2231
+ ));
2232
+ case "fall":
2233
+ return par(
2234
+ tween(g.id, { y: g.y + 700 + rs(41) * 200 }, { duration: dur3(0.8, c.sp), ease: "easeInQuad" }),
2235
+ tween(g.id, { rotation: (rs(42) - 0.5) * 120, opacity: 0 }, { duration: dur3(0.8, c.sp), ease: "easeInQuad" })
2236
+ );
2237
+ case "collapse":
2238
+ return par(
2239
+ tween(g.id, { x: block.x, y: block.y, scale: 0.2 }, { duration: dur3(0.5, c.sp), ease: "easeInBack" }),
2240
+ tween(g.id, { opacity: 0 }, { duration: dur3(0.5, c.sp), ease: "easeInQuad" })
2241
+ );
2242
+ }
2243
+ }
2244
+ function textOut(name, block, opts = {}) {
2245
+ const c = { ...ctx3(opts), fs: block.fontSize };
2246
+ const dir = opts.dir ?? [0, -1];
2247
+ const steps = block.glyphs.map((g) => glyphOut(name, g, c, block, dir));
2248
+ const interval = (c.stag ?? OUT_STAGGER[name]) / c.sp;
2249
+ const body = interval > 0 ? stagger(interval, ...steps) : par(...steps);
2250
+ return beat(opts.label ?? `text-out-${name}`, {}, [body]);
2251
+ }
2252
+ function textTypeCues(block, opts) {
2253
+ const interval = opts.interval ?? 0.065;
2254
+ const gain = opts.gain ?? 0.4;
2255
+ const off = opts.offset ?? 0;
2256
+ const KEYS = ["001", "004", "007", "010", "014"];
2257
+ return block.glyphs.map((g, i) => ({
2258
+ at: opts.at,
2259
+ offset: off + i * interval,
2260
+ file: `keypress-${KEYS[i % KEYS.length]}.wav`,
2261
+ gain: gain + 0.2 * rand(i, 31)
2262
+ }));
2263
+ }
2264
+
1691
2265
  // ../core/src/motionOps.ts
1692
2266
  var MOTION_OPS = ["rotate", "zoom", "ken-burns", "slide-in", "fade", "draw-on", "pulse"];
1693
- var clamp013 = (n3) => Math.max(0, Math.min(1, n3));
2267
+ var clamp014 = (n3) => Math.max(0, Math.min(1, n3));
1694
2268
  function settleEase2(e) {
1695
2269
  return e < 0.34 ? "easeOutCubic" : e < 0.67 ? "easeOutBack" : "easeOutElastic";
1696
2270
  }
@@ -1708,7 +2282,7 @@ function fromVec2(from, dist) {
1708
2282
  }
1709
2283
  var motionOpLabel = (name, target) => `op-${name}-${target}`;
1710
2284
  function motionOp(name, target, opts = {}) {
1711
- const e = clamp013(opts.energy ?? 0.5);
2285
+ const e = clamp014(opts.energy ?? 0.5);
1712
2286
  const sp = Math.max(0.25, opts.speed ?? 1);
1713
2287
  const amt = opts.amount ?? 1;
1714
2288
  const b = { scale: 1, x: 0, y: 0, rotation: 0, ...opts.base };
@@ -2411,29 +2985,29 @@ function sketchToTimeline(sketch, nodeIds) {
2411
2985
  const steps = [];
2412
2986
  events.forEach((ev, i) => {
2413
2987
  const node = nodeIds[i % nodeIds.length];
2414
- const dur3 = Math.max(0.05, ev.t1 - ev.t0);
2988
+ const dur4 = Math.max(0.05, ev.t1 - ev.t0);
2415
2989
  const ease = easeFor(ev.easing);
2416
2990
  let motion;
2417
2991
  switch (ev.kind) {
2418
2992
  case "enter":
2419
- motion = tween(node, { opacity: 1 }, { duration: dur3, ease });
2993
+ motion = tween(node, { opacity: 1 }, { duration: dur4, ease });
2420
2994
  break;
2421
2995
  case "exit":
2422
- motion = tween(node, { opacity: 0 }, { duration: dur3, ease });
2996
+ motion = tween(node, { opacity: 0 }, { duration: dur4, ease });
2423
2997
  break;
2424
2998
  case "emphasis": {
2425
2999
  const peak = 1 + Math.max(0.08, Math.min(0.5, ev.magnitude));
2426
3000
  motion = seq(
2427
- tween(node, { scale: peak }, { duration: dur3 / 2, ease: "easeOutCubic" }),
2428
- tween(node, { scale: 1 }, { duration: dur3 / 2, ease: "easeInOutQuad" })
3001
+ tween(node, { scale: peak }, { duration: dur4 / 2, ease: "easeOutCubic" }),
3002
+ tween(node, { scale: 1 }, { duration: dur4 / 2, ease: "easeInOutQuad" })
2429
3003
  );
2430
3004
  break;
2431
3005
  }
2432
3006
  case "scale":
2433
- motion = tween(node, { scale: 1 + Math.max(-0.5, Math.min(0.5, ev.magnitude)) }, { duration: dur3, ease });
3007
+ motion = tween(node, { scale: 1 + Math.max(-0.5, Math.min(0.5, ev.magnitude)) }, { duration: dur4, ease });
2434
3008
  break;
2435
3009
  case "move":
2436
- motion = tween(node, { opacity: 1 }, { duration: dur3, ease });
3010
+ motion = tween(node, { opacity: 1 }, { duration: dur4, ease });
2437
3011
  break;
2438
3012
  }
2439
3013
  steps.push(ev.t0 > 0 ? seq(wait(ev.t0), motion) : motion);
@@ -2461,10 +3035,16 @@ export {
2461
3035
  compileScene,
2462
3036
  composeScene,
2463
3037
  composition,
3038
+ cursor,
3039
+ cursorClick,
3040
+ cursorDouble,
3041
+ cursorPath,
3042
+ cursorTo,
2464
3043
  deviceBounds,
2465
3044
  devicePreset,
2466
3045
  deviceScreen,
2467
3046
  deviceScreenCenter,
3047
+ deviceScreenPoint,
2468
3048
  ellipse,
2469
3049
  evaluate,
2470
3050
  figure,
@@ -2499,8 +3079,13 @@ export {
2499
3079
  scene,
2500
3080
  seq,
2501
3081
  sketchToTimeline,
3082
+ splitText,
2502
3083
  stagger,
2503
3084
  text,
3085
+ textIn,
3086
+ textLoop,
3087
+ textOut,
3088
+ textTypeCues,
2504
3089
  to,
2505
3090
  tween,
2506
3091
  validateComposition,
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Cursor / pointer motion — a vector mouse pointer that glides across the scene
3
+ * and clicks things (the UI-demo staple). `cursor()` returns a NodeIR (like
4
+ * `devicePreset`); `cursorTo` / `cursorPath` / `cursorClick` return TimelineIR
5
+ * (like `characterPreset`). The pointer's HOTSPOT is the group origin (0,0), so a
6
+ * move lands the tip exactly on a target. Pairs with `deviceScreenPoint` to click
7
+ * UI inside a `devicePreset` screen.
8
+ *
9
+ * nodes: [devicePreset("browser", { id: "d", x, y, scale, content }), cursor({ id: "cur" })]
10
+ * timeline: seq(cursorTo("cur", [start], deviceScreenPoint("browser", dOpts, [lx, ly])),
11
+ * cursorClick("cur", { press: "d-ui-cta" }))
12
+ */
13
+ import type { Ease, NodeIR, TimelineIR } from "./ir.js";
14
+ export type CursorStyle = "arrow" | "dot" | "ring";
15
+ export interface CursorOpts {
16
+ id?: string;
17
+ x?: number;
18
+ y?: number;
19
+ scale?: number;
20
+ opacity?: number;
21
+ style?: CursorStyle;
22
+ /** Pointer body colour (default white for arrow). */
23
+ fill?: string;
24
+ /** Accent for dot/ring body and the click ripple. */
25
+ accent?: string;
26
+ }
27
+ export declare function cursor(opts?: CursorOpts): NodeIR;
28
+ export interface CursorToOpts {
29
+ duration?: number;
30
+ ease?: Ease;
31
+ /** perpendicular bow as a fraction of distance (default 0.12; 0 = straight). */
32
+ arc?: number;
33
+ label?: string;
34
+ }
35
+ /** Glide the cursor from `from` to `to` along a gentle human arc. */
36
+ export declare function cursorTo(id: string, from: [number, number], to: [number, number], opts?: CursorToOpts): TimelineIR;
37
+ export interface CursorPathOpts {
38
+ duration?: number;
39
+ ease?: Ease;
40
+ curviness?: number;
41
+ label?: string;
42
+ }
43
+ /** Move the cursor through a tour of waypoints (one smooth path). */
44
+ export declare function cursorPath(id: string, points: [number, number][], opts?: CursorPathOpts): TimelineIR;
45
+ export interface CursorClickOpts {
46
+ /** overall click duration scale (default 1). */
47
+ speed?: number;
48
+ /** node id to "press" (a quick scale dip) when the cursor clicks it. */
49
+ press?: string;
50
+ /** show the expanding ripple ring (default true). */
51
+ ripple?: boolean;
52
+ label?: string;
53
+ }
54
+ /** A click: the pointer taps, a ripple ring expands, and an optional target presses. */
55
+ export declare function cursorClick(id: string, opts?: CursorClickOpts): TimelineIR;
56
+ /** Two quick clicks. */
57
+ export declare function cursorDouble(id: string, opts?: CursorClickOpts): TimelineIR;
@@ -61,5 +61,9 @@ export declare function deviceBounds(name: DevicePresetName, opts?: DevicePreset
61
61
  width: number;
62
62
  height: number;
63
63
  };
64
+ /** Map a SCREEN-LOCAL point (origin = screen centre, the coords `content` is
65
+ * authored in) to absolute SCENE coords, given the same `opts` passed to
66
+ * `devicePreset`. For aiming a `cursor` at on-screen UI. */
67
+ export declare function deviceScreenPoint(name: DevicePresetName, opts: DevicePresetOpts, local: [number, number]): [number, number];
64
68
  /** Build a device-mockup frame (a group) with a clipped screen content slot. */
65
69
  export declare function devicePreset(name: DevicePresetName, opts?: DevicePresetOpts): NodeIR;
@@ -6,10 +6,12 @@ export { composeScene, formatComposeReport, type OverlayDoc, type ComposeReport,
6
6
  export { compileScene, type CompiledScene, type PropertySegment, type LabelSpan, type MotionDriver } from "./compile.js";
7
7
  export { pathPoint, pathTangentAngle, type Pt } from "./path.js";
8
8
  export { motionPreset, PRESET_NAMES, type PresetName, type PresetRig, type PresetOpts } from "./presets.js";
9
- export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, DEVICE_PRESET_NAMES, type DevicePresetName, type DevicePresetOpts } from "./devicePreset.js";
9
+ export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, deviceScreenPoint, DEVICE_PRESET_NAMES, type DevicePresetName, type DevicePresetOpts } from "./devicePreset.js";
10
+ export { cursor, cursorTo, cursorPath, cursorClick, cursorDouble, type CursorStyle, type CursorOpts, type CursorToOpts, type CursorPathOpts, type CursorClickOpts } from "./cursor.js";
10
11
  export { rig, rigPose, poseTo, ikReach, humanoid, ovalPath, type Bone, type RigOpts, type Pose, type HumanoidOpts } from "./rig.js";
11
12
  export { characterPreset, CHARACTER_PRESET_NAMES, type CharacterPresetName, type CharacterPresetOpts } from "./characterPreset.js";
12
13
  export { figure, type FigureStyle, type FigureOpts, type FigurePalette } from "./figure.js";
14
+ 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
15
  export { motionOp, motionOpLabel, MOTION_OPS, type MotionOpName, type MotionOpOpts, type MotionOpResult } from "./motionOps.js";
14
16
  export { resolveAudioPlan, resolveCompositionAudioPlan, SFX_DURATION, type AudioPlan, type ResolvedCue, } from "./audio.js";
15
17
  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,65 @@ 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
+
202
+ ## Cursor (UI demos)
203
+
204
+ A vector mouse pointer that glides across the scene and clicks things — for app
205
+ walkthroughs. `cursor()` returns a node; the moves/clicks return timeline steps.
206
+ The pointer's **hotspot is the group origin**, so a move lands the tip on a target.
207
+
208
+ - `cursor({ id, x, y, scale?, opacity?, style?, accent? }) → NodeIR` — styles
209
+ `arrow` (default), `dot`, `ring`. Draw it LAST so it sits on top. Carries a
210
+ hidden `${id}-ripple` ring for clicks.
211
+ - `cursorTo(id, from, to, { duration?, ease?, arc? }) → TimelineIR` — glide along
212
+ a gentle human arc (`arc` is the bow, default 0.12). Thread the position: start
213
+ = the node's `x/y`, each `to` becomes the next `from`.
214
+ - `cursorPath(id, points, opts)` — a multi-stop tour through waypoints.
215
+ - `cursorClick(id, { press?, ripple?, label? })` / `cursorDouble(...)` — the
216
+ pointer taps, a ripple ring expands, and the `press` node (a button) dips. Pass
217
+ a unique `label` when you click more than once in a scene.
218
+ - `deviceScreenPoint(name, deviceOpts, [lx, ly]) → [x, y]` — map a UI element's
219
+ screen-local coords (the coords `devicePreset` `content` is authored in) to
220
+ scene coords, so the cursor clicks on-screen UI precisely (account for the
221
+ device's `scale` at click time and any `slot` offset).
222
+
223
+ ```ts
224
+ // nodes: devicePreset("browser", { id:"d", x, y, scale:0.88, content }), cursor({ id:"cur" })
225
+ const cta = deviceScreenPoint("browser", { x, y, scale: 0.88 }, [lx, ly]);
226
+ seq(cursorTo("cur", [sx, sy], cta), cursorClick("cur", { press: "browser-ui-cta" }))
227
+ ```
228
+
170
229
  ## Audio (optional)
171
230
 
172
231
  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.5.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",