reframe-video 0.2.0 → 0.3.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.
@@ -10,6 +10,7 @@ Verified against each asset page's license field on 2026-06-11.
10
10
  | whoosh.wav (wind body), rise.wav (reversed slice) | [Air whoosh](https://opengameart.org/content/air-whoosh) | qubodup | CC0 |
11
11
  | whoosh.wav (transient layer: swish-9) | [Swishes Sound Pack](https://opengameart.org/content/swishes-sound-pack) | artisticdude | CC0 |
12
12
  | thud.wav (trimmed) | [Muffled Distant Explosion](https://opengameart.org/content/muffled-distant-explosion) | NenadSimic | CC0 |
13
+ | footstep_001/002/003.ogg (footstep00/03/06) | [RPG Audio](https://kenney.nl/assets/rpg-audio) | Kenney (kenney.nl) | CC0 |
13
14
  | bgm-song21.mp3 | [Mysterious Ambience (song21)](https://opengameart.org/content/mysterious-ambience-song21) | cynicmusic (pixelsphere.org) | multi-licensed; used under its CC0 option |
14
15
 
15
16
  CC0 requires no attribution; this file records provenance anyway.
Binary file
Binary file
Binary file
package/dist/bin.js CHANGED
@@ -1044,6 +1044,32 @@ var init_devicePreset = __esm({
1044
1044
  }
1045
1045
  });
1046
1046
 
1047
+ // ../core/src/rig.ts
1048
+ var init_rig = __esm({
1049
+ "../core/src/rig.ts"() {
1050
+ "use strict";
1051
+ init_dsl();
1052
+ }
1053
+ });
1054
+
1055
+ // ../core/src/characterPreset.ts
1056
+ var init_characterPreset = __esm({
1057
+ "../core/src/characterPreset.ts"() {
1058
+ "use strict";
1059
+ init_dsl();
1060
+ init_rig();
1061
+ }
1062
+ });
1063
+
1064
+ // ../core/src/figure.ts
1065
+ var init_figure = __esm({
1066
+ "../core/src/figure.ts"() {
1067
+ "use strict";
1068
+ init_dsl();
1069
+ init_rig();
1070
+ }
1071
+ });
1072
+
1047
1073
  // ../core/src/motionOps.ts
1048
1074
  var init_motionOps = __esm({
1049
1075
  "../core/src/motionOps.ts"() {
@@ -1262,6 +1288,9 @@ var init_src = __esm({
1262
1288
  init_path();
1263
1289
  init_presets();
1264
1290
  init_devicePreset();
1291
+ init_rig();
1292
+ init_characterPreset();
1293
+ init_figure();
1265
1294
  init_motionOps();
1266
1295
  init_audio();
1267
1296
  init_evaluate();
@@ -1361,7 +1390,7 @@ function buildLogoSting(d) {
1361
1390
  const inks = d.paths.map(
1362
1391
  (p, i) => path({ id: `ink-${i}`, d: p.d, originX: vcx, originY: vcy, x: 0, y: 0, stroke: p.fill, strokeWidth: sw, progress: 0 })
1363
1392
  );
1364
- const rig = {
1393
+ const rig2 = {
1365
1394
  group: "logo",
1366
1395
  center: [CX, CY],
1367
1396
  baseScale: fit,
@@ -1381,7 +1410,7 @@ function buildLogoSting(d) {
1381
1410
  ],
1382
1411
  timeline: seq(
1383
1412
  motionPreset(d.motion ?? "reveal-orbit", {
1384
- target: rig,
1413
+ target: rig2,
1385
1414
  ...d.energy !== void 0 && { energy: d.energy },
1386
1415
  ...d.speed !== void 0 && { speed: d.speed },
1387
1416
  ...d.intensity !== void 0 && { intensity: d.intensity },
@@ -458,8 +458,57 @@
458
458
  a[3] + (b[3] - a[3]) * u
459
459
  ]);
460
460
  }
461
+ if (looksLikePath(from) && looksLikePath(to)) {
462
+ const a = tokenizePath(from);
463
+ const b = tokenizePath(to);
464
+ if (a && b && morphCompatible(a, b)) return morphPath(a, b, u);
465
+ return u < 0.5 ? from : to;
466
+ }
461
467
  return to;
462
468
  }
469
+ var PATH_BODY = /^[\sMmLlHhVvCcSsQqTtAaZz0-9.,eE+-]+$/;
470
+ function looksLikePath(v) {
471
+ return typeof v === "string" && /^\s*[Mm]/.test(v) && PATH_BODY.test(v);
472
+ }
473
+ var PATH_TOKEN = /([MmLlHhVvCcSsQqTtAaZz])|(-?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)/g;
474
+ function tokenizePath(d) {
475
+ const out = [];
476
+ let cur = null;
477
+ let m;
478
+ PATH_TOKEN.lastIndex = 0;
479
+ while (m = PATH_TOKEN.exec(d)) {
480
+ if (m[1]) out.push(cur = { cmd: m[1], nums: [] });
481
+ else if (m[2]) {
482
+ if (!cur) return null;
483
+ cur.nums.push(parseFloat(m[2]));
484
+ }
485
+ }
486
+ return out.length ? out : null;
487
+ }
488
+ function morphCompatible(a, b) {
489
+ if (a.length !== b.length) return false;
490
+ for (let i = 0; i < a.length; i++) {
491
+ const ca = a[i];
492
+ const cb = b[i];
493
+ if (ca.cmd !== cb.cmd || ca.nums.length !== cb.nums.length) return false;
494
+ if (ca.cmd === "A" || ca.cmd === "a") return false;
495
+ }
496
+ return true;
497
+ }
498
+ var fmtNum = (v) => {
499
+ const r = Number(v.toFixed(3));
500
+ return Object.is(r, -0) ? "0" : String(r);
501
+ };
502
+ function morphPath(a, b, u) {
503
+ let s = "";
504
+ for (let i = 0; i < a.length; i++) {
505
+ const an = a[i].nums;
506
+ const bn = b[i].nums;
507
+ s += (i ? " " : "") + a[i].cmd;
508
+ for (let j = 0; j < an.length; j++) s += " " + fmtNum(an[j] + (bn[j] - an[j]) * u);
509
+ }
510
+ return s;
511
+ }
463
512
 
464
513
  // ../core/src/evaluate.ts
465
514
  var IDENTITY = [1, 0, 0, 1, 0, 0];
package/dist/index.js CHANGED
@@ -15,22 +15,22 @@ function locate(segCount, u) {
15
15
  return { i, t: scaled - i };
16
16
  }
17
17
  function controls(points, closed, i) {
18
- const n = points.length;
18
+ const n3 = points.length;
19
19
  const at = (k) => {
20
- if (closed) return points[(k % n + n) % n];
21
- return points[Math.max(0, Math.min(n - 1, k))];
20
+ if (closed) return points[(k % n3 + n3) % n3];
21
+ return points[Math.max(0, Math.min(n3 - 1, k))];
22
22
  };
23
23
  return [at(i - 1), at(i), at(i + 1), at(i + 2)];
24
24
  }
25
25
  function segCountOf(points, closed) {
26
- const n = points.length;
27
- if (n < 2) return 0;
28
- return closed ? n : n - 1;
26
+ const n3 = points.length;
27
+ if (n3 < 2) return 0;
28
+ return closed ? n3 : n3 - 1;
29
29
  }
30
30
  function pathPoint(points, closed, u, curviness = 1) {
31
- const n = points.length;
32
- if (n === 0) return [0, 0];
33
- if (n === 1) return [points[0][0], points[0][1]];
31
+ const n3 = points.length;
32
+ if (n3 === 0) return [0, 0];
33
+ if (n3 === 1) return [points[0][0], points[0][1]];
34
34
  const segs = segCountOf(points, closed);
35
35
  const { i, t } = locate(segs, u);
36
36
  const [p0, p1, p2, p3] = controls(points, closed, i);
@@ -49,8 +49,8 @@ function pathPoint(points, closed, u, curviness = 1) {
49
49
  return [H(p0[0], p1[0], p2[0], p3[0]), H(p0[1], p1[1], p2[1], p3[1])];
50
50
  }
51
51
  function pathTangentAngle(points, closed, u, curviness = 1) {
52
- const n = points.length;
53
- if (n < 2) return 0;
52
+ const n3 = points.length;
53
+ if (n3 < 2) return 0;
54
54
  const segs = segCountOf(points, closed);
55
55
  const { i, t } = locate(segs, u);
56
56
  const [p0, p1, p2, p3] = controls(points, closed, i);
@@ -816,7 +816,7 @@ function applyOverlay(ir, overlay, layer, report, baseNodeIds) {
816
816
  );
817
817
  continue;
818
818
  }
819
- const index = ir.nodes.findIndex((n) => n.id === id);
819
+ const index = ir.nodes.findIndex((n3) => n3.id === id);
820
820
  if (index < 0) {
821
821
  orphan(
822
822
  `removeNodes.${id}`,
@@ -1263,9 +1263,434 @@ function devicePreset(name, opts = {}) {
1263
1263
  );
1264
1264
  }
1265
1265
 
1266
+ // ../core/src/rig.ts
1267
+ var DEFAULT_LINE = "#FFE3D2";
1268
+ var DEFAULT_FILL = "#0E1424";
1269
+ var LINE_W = 5;
1270
+ var GLOW_W = 16;
1271
+ var K = 0.5523;
1272
+ var n = (v) => Number(v.toFixed(2));
1273
+ function capsulePath(hw, len) {
1274
+ const yT = hw;
1275
+ const yB = Math.max(hw, len - hw);
1276
+ const k = hw * K;
1277
+ return `M ${n(-hw)} ${n(yT)} C ${n(-hw)} ${n(yT - k)} ${n(-k)} ${n(yT - hw)} 0 ${n(yT - hw)} C ${n(k)} ${n(yT - hw)} ${n(hw)} ${n(yT - k)} ${n(hw)} ${n(yT)} L ${n(hw)} ${n(yB)} C ${n(hw)} ${n(yB + k)} ${n(k)} ${n(yB + hw)} 0 ${n(yB + hw)} C ${n(-k)} ${n(yB + hw)} ${n(-hw)} ${n(yB + k)} ${n(-hw)} ${n(yB)} Z`;
1278
+ }
1279
+ function ovalPath(a, b, cx = 0, cy = 0) {
1280
+ const ka = n(a * K), kb = n(b * K);
1281
+ const t = n(cy - b), bo = n(cy + b), c = n(cy), A = n(a), L = n(-a), X = n(cx);
1282
+ return `M ${X} ${t} C ${n(cx + ka)} ${t} ${n(cx + A)} ${n(cy - kb)} ${n(cx + A)} ${c} C ${n(cx + A)} ${n(cy + kb)} ${n(cx + ka)} ${bo} ${X} ${bo} C ${n(cx - ka)} ${bo} ${n(cx + L)} ${n(cy + kb)} ${n(cx + L)} ${c} C ${n(cx + L)} ${n(cy - kb)} ${n(cx - ka)} ${t} ${X} ${t} Z`;
1283
+ }
1284
+ function boneShape(jointId, bone, o) {
1285
+ if (bone.shape) return bone.shape;
1286
+ const len = bone.length ?? 0;
1287
+ if (len <= 0) return [];
1288
+ const d = capsulePath((bone.width ?? 20) / 2, len);
1289
+ const nodes = [];
1290
+ if (o.glow) nodes.push(path({ id: `${jointId}-glow`, d, x: 0, y: 0, fill: "none", stroke: o.glow, strokeWidth: GLOW_W, opacity: 0.18 }));
1291
+ nodes.push(path({ id: `${jointId}-shape`, d, x: 0, y: 0, fill: o.fill, stroke: o.color, strokeWidth: LINE_W }));
1292
+ return nodes;
1293
+ }
1294
+ function buildBone(bone, id, o) {
1295
+ const jointId = `${id}-${bone.name}`;
1296
+ return group(
1297
+ { id: jointId, x: bone.at[0], y: bone.at[1], rotation: bone.rotation ?? 0 },
1298
+ [...boneShape(jointId, bone, o), ...(bone.children ?? []).map((c) => buildBone(c, id, o))]
1299
+ );
1300
+ }
1301
+ function rig(root, opts = {}) {
1302
+ const id = opts.id ?? "rig";
1303
+ const o = { color: opts.color ?? DEFAULT_LINE, fill: opts.fill ?? DEFAULT_FILL, glow: opts.glow };
1304
+ return group(
1305
+ { id, x: opts.x ?? 0, y: opts.y ?? 0, scale: opts.scale ?? 1, opacity: opts.opacity ?? 1 },
1306
+ [buildBone(root, id, o)]
1307
+ );
1308
+ }
1309
+ function rigPose(id, pose) {
1310
+ const out = {};
1311
+ for (const [name, deg] of Object.entries(pose)) out[`${id}-${name}`] = { rotation: deg };
1312
+ return out;
1313
+ }
1314
+ function poseTo(id, pose, opts = {}) {
1315
+ const tweens = Object.entries(pose).map(
1316
+ ([name, deg]) => tween(`${id}-${name}`, { rotation: deg }, { duration: opts.duration ?? 0.5, ease: opts.ease ?? "easeInOutCubic" })
1317
+ );
1318
+ return opts.stagger ? stagger(opts.stagger, ...tweens) : par(...tweens);
1319
+ }
1320
+ function ikReach(upper, lower, dx, dy, flip = false) {
1321
+ const D = Math.hypot(dx, dy);
1322
+ const cos2 = Math.max(-1, Math.min(1, (D * D - upper * upper - lower * lower) / (2 * upper * lower)));
1323
+ const theta2 = (flip ? -1 : 1) * Math.acos(cos2);
1324
+ const vx = -lower * Math.sin(theta2);
1325
+ const vy = upper + lower * Math.cos(theta2);
1326
+ const theta1 = Math.atan2(dy, dx) - Math.atan2(vy, vx);
1327
+ const deg = (r) => r * 180 / Math.PI;
1328
+ return [deg(theta1), deg(theta2)];
1329
+ }
1330
+ function humanoid(opts = {}) {
1331
+ const line2 = opts.color ?? DEFAULT_LINE;
1332
+ const fill = opts.fill ?? DEFAULT_FILL;
1333
+ const glow = opts.glow;
1334
+ const blob = (jid, a, b, cy) => {
1335
+ const d = ovalPath(a, b, 0, cy);
1336
+ const nodes = [];
1337
+ if (glow) nodes.push(path({ id: `${jid}-glow`, d, x: 0, y: 0, fill: "none", stroke: glow, strokeWidth: GLOW_W, opacity: 0.18 }));
1338
+ nodes.push(path({ id: `${jid}-shape`, d, x: 0, y: 0, fill, stroke: line2, strokeWidth: LINE_W }));
1339
+ return nodes;
1340
+ };
1341
+ const id = opts.id ?? "rig";
1342
+ const root = {
1343
+ name: "chest",
1344
+ at: [0, 0],
1345
+ shape: blob(`${id}-chest`, 44, 62, 22),
1346
+ children: [
1347
+ { name: "head", at: [0, -42], rotation: 0, shape: blob(`${id}-head`, 40, 42, -34) },
1348
+ { name: "armUpperL", at: [-42, -20], length: 60, width: 20, rotation: 10, children: [
1349
+ { name: "armLowerL", at: [0, 60], length: 56, width: 16, rotation: 8 }
1350
+ ] },
1351
+ { name: "armUpperR", at: [42, -20], length: 60, width: 20, rotation: -10, children: [
1352
+ { name: "armLowerR", at: [0, 60], length: 56, width: 16, rotation: -8 }
1353
+ ] },
1354
+ { name: "legUpperL", at: [-20, 76], length: 76, width: 26, rotation: 3, children: [
1355
+ { name: "legLowerL", at: [0, 76], length: 72, width: 22, rotation: -2 }
1356
+ ] },
1357
+ { name: "legUpperR", at: [20, 76], length: 76, width: 26, rotation: -3, children: [
1358
+ { name: "legLowerR", at: [0, 76], length: 72, width: 22, rotation: 2 }
1359
+ ] }
1360
+ ]
1361
+ };
1362
+ return rig(root, opts);
1363
+ }
1364
+
1365
+ // ../core/src/characterPreset.ts
1366
+ var CHARACTER_PRESET_NAMES = ["walk", "run", "jump", "dance", "wave", "cheer"];
1367
+ var THIGH = 76;
1368
+ var SHIN = 72;
1369
+ var clamp012 = (x) => Math.max(0, Math.min(1, x));
1370
+ function makeRng2(seed) {
1371
+ let a = seed >>> 0 || 2654435769;
1372
+ return () => {
1373
+ a = a + 1831565813 | 0;
1374
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
1375
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
1376
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
1377
+ };
1378
+ }
1379
+ var dur2 = (base, sp) => base / sp;
1380
+ function ctx2(o) {
1381
+ const rand = makeRng2((o.seed ?? 0) + 1);
1382
+ return {
1383
+ g: o.target,
1384
+ label: o.label,
1385
+ e: clamp012(o.energy ?? 0.5),
1386
+ sp: Math.max(0.25, o.speed ?? 1),
1387
+ cycles: Math.max(1, Math.round(o.cycles ?? 4)),
1388
+ facing: o.facing ?? 1,
1389
+ at: o.at ?? [0, 0],
1390
+ travel: o.travel,
1391
+ rand,
1392
+ jit: (amp) => (rand() - 0.5) * 2 * amp
1393
+ };
1394
+ }
1395
+ var round = (v) => Math.round(v * 1e3) / 1e3;
1396
+ function footPos(p, stride, lift) {
1397
+ p = (p % 1 + 1) % 1;
1398
+ if (p < 0.5) {
1399
+ const u2 = p / 0.5;
1400
+ return [stride * (1 - 2 * u2), 138];
1401
+ }
1402
+ const u = (p - 0.5) / 0.5;
1403
+ return [-stride + 2 * stride * u, 138 - Math.sin(Math.PI * u) * lift];
1404
+ }
1405
+ function gaitPose(ph, stride, lift, armSwing, facing) {
1406
+ const fl = footPos(ph, stride, lift);
1407
+ const fr = footPos(ph + 0.5, stride, lift);
1408
+ const [hipL, kneeL] = ikReach(THIGH, SHIN, facing * fl[0], fl[1], facing < 0);
1409
+ const [hipR, kneeR] = ikReach(THIGH, SHIN, facing * fr[0], fr[1], facing < 0);
1410
+ const swing = Math.cos(2 * Math.PI * ph);
1411
+ return {
1412
+ legUpperL: round(hipL),
1413
+ legLowerL: round(kneeL),
1414
+ legUpperR: round(hipR),
1415
+ legLowerR: round(kneeR),
1416
+ armUpperR: round(-10 - armSwing * swing),
1417
+ armLowerR: -16,
1418
+ armUpperL: round(10 + armSwing * swing),
1419
+ armLowerL: 16
1420
+ };
1421
+ }
1422
+ function gait(c, run) {
1423
+ const stride = (run ? 34 : 24) + (run ? 40 : 30) * c.e + c.jit(3);
1424
+ const lift = run ? 40 : 26;
1425
+ const armSwing = (run ? 26 : 16) + 20 * c.e;
1426
+ const halfDur = (run ? 0.26 : 0.42) + c.jit(0.02);
1427
+ const lean = run ? c.facing * -6 : 0;
1428
+ const steps = c.cycles * 2;
1429
+ const d = dur2(halfDur, c.sp);
1430
+ const intro = dur2(0.16, c.sp);
1431
+ const keys = [];
1432
+ for (let k = 0; k <= steps; k++) {
1433
+ const pose = { ...gaitPose(k / 2, stride, lift, armSwing, c.facing), chest: lean };
1434
+ keys.push(poseTo(c.g, pose, { duration: k === 0 ? intro : d, ease: k === 0 ? "easeOutQuad" : "linear" }));
1435
+ }
1436
+ const total = intro + steps * d;
1437
+ const travel = c.travel ?? stride * 2;
1438
+ const children = [seq(...keys)];
1439
+ if (travel !== 0) {
1440
+ children.push(tween(c.g, { x: c.at[0] + c.facing * travel * c.cycles }, { duration: total, ease: "linear", label: "travel" }));
1441
+ }
1442
+ return beat(run ? "run" : "walk", {}, [par(...children)]);
1443
+ }
1444
+ function jumpBeat(c) {
1445
+ const h = 120 + 150 * c.e;
1446
+ const [y0] = [c.at[1]];
1447
+ const CROUCH = { legUpperL: 18, legLowerL: 54, legUpperR: -18, legLowerR: 54, armUpperL: 28, armUpperR: -28 };
1448
+ const LAUNCH = { legUpperL: 0, legLowerL: 0, legUpperR: 0, legLowerR: 0, armUpperL: 150, armUpperR: -150 };
1449
+ const TUCK = { legUpperL: -28, legLowerL: 66, legUpperR: -28, legLowerR: 66, armUpperL: 124, armUpperR: -124 };
1450
+ const REST = { legUpperL: 3, legLowerL: -2, legUpperR: -3, legLowerR: 2, armUpperL: 10, armLowerL: 8, armUpperR: -10, armLowerR: -8 };
1451
+ const j = c.jit(0.03);
1452
+ return beat("jump", {}, [
1453
+ seq(
1454
+ par(poseTo(c.g, CROUCH, { duration: dur2(0.24, c.sp), ease: "easeOutQuad" }), tween(c.g, { y: y0 + 26 }, { duration: dur2(0.24, c.sp), ease: "easeOutQuad" })),
1455
+ par(poseTo(c.g, LAUNCH, { duration: dur2(0.22 + j, c.sp), ease: "easeOutCubic" }), tween(c.g, { y: y0 - h }, { duration: dur2(0.36, c.sp), ease: "easeOutCubic", label: "launch" })),
1456
+ poseTo(c.g, TUCK, { duration: dur2(0.22, c.sp) }),
1457
+ par(poseTo(c.g, CROUCH, { duration: dur2(0.28, c.sp), ease: "easeInQuad" }), tween(c.g, { y: y0 + 18 }, { duration: dur2(0.3, c.sp), ease: "easeInCubic", label: "land" })),
1458
+ par(poseTo(c.g, REST, { duration: dur2(0.45, c.sp), ease: "easeOutBack" }), tween(c.g, { y: y0 }, { duration: dur2(0.45, c.sp), ease: "easeOutBack" }))
1459
+ )
1460
+ ]);
1461
+ }
1462
+ function danceBeat(c) {
1463
+ const y0 = c.at[1];
1464
+ const sway = 8 + 6 * c.e;
1465
+ const armUp = 130 + 30 * c.e;
1466
+ const A = { chest: sway, head: -sway * 0.5, armUpperR: -armUp, armLowerR: -20, armUpperL: 40, armLowerL: 30, legUpperL: 8, legUpperR: -2 };
1467
+ const B = { chest: -sway, head: sway * 0.5, armUpperL: armUp, armLowerL: 20, armUpperR: -40, armLowerR: -30, legUpperL: 2, legUpperR: -8 };
1468
+ const d = dur2(0.34, c.sp);
1469
+ const keys = [];
1470
+ for (let k = 0; k < c.cycles * 2; k++) {
1471
+ const pose = k % 2 === 0 ? A : B;
1472
+ keys.push(par(
1473
+ poseTo(c.g, pose, { duration: d, ease: "easeInOutQuad" }),
1474
+ tween(c.g, { y: y0 - (k % 2 === 0 ? 14 : 0) }, { duration: d, ease: "easeInOutQuad" })
1475
+ ));
1476
+ }
1477
+ keys.push(tween(c.g, { y: y0 }, { duration: d }));
1478
+ return beat("dance", {}, [seq(...keys)]);
1479
+ }
1480
+ function waveBeat(c) {
1481
+ const n3 = 3 + Math.round(c.rand() * 2);
1482
+ const amp = 16 + 10 * c.e;
1483
+ const steps = [poseTo(c.g, { armUpperR: -150, armLowerR: -24 }, { duration: dur2(0.4, c.sp), ease: "easeOutBack" })];
1484
+ for (let k = 0; k < n3; k++) {
1485
+ steps.push(poseTo(c.g, { armLowerR: -24 + (k % 2 === 0 ? amp : -amp) }, { duration: dur2(0.22, c.sp), ease: "easeInOutQuad" }));
1486
+ }
1487
+ steps.push(poseTo(c.g, { armUpperR: -10, armLowerR: -8 }, { duration: dur2(0.4, c.sp), ease: "easeInOutCubic" }));
1488
+ return beat("wave", {}, [seq(...steps)]);
1489
+ }
1490
+ function cheerBeat(c) {
1491
+ const y0 = c.at[1];
1492
+ const UP = { armUpperL: 152, armLowerL: 8, armUpperR: -152, armLowerR: -8 };
1493
+ const d = dur2(0.3, c.sp);
1494
+ const keys = [poseTo(c.g, UP, { duration: dur2(0.35, c.sp), ease: "easeOutBack" })];
1495
+ for (let k = 0; k < c.cycles; k++) {
1496
+ keys.push(par(tween(c.g, { y: y0 - 28 }, { duration: d, ease: "easeOutQuad" }), poseTo(c.g, { armUpperL: 160, armUpperR: -160 }, { duration: d })));
1497
+ keys.push(par(tween(c.g, { y: y0 }, { duration: d, ease: "easeInQuad" }), poseTo(c.g, { armUpperL: 145, armUpperR: -145 }, { duration: d })));
1498
+ }
1499
+ return beat("cheer", {}, [seq(...keys)]);
1500
+ }
1501
+ function characterPreset(name, opts) {
1502
+ const c = ctx2(opts);
1503
+ let tl;
1504
+ switch (name) {
1505
+ case "walk":
1506
+ tl = gait(c, false);
1507
+ break;
1508
+ case "run":
1509
+ tl = gait(c, true);
1510
+ break;
1511
+ case "jump":
1512
+ tl = jumpBeat(c);
1513
+ break;
1514
+ case "dance":
1515
+ tl = danceBeat(c);
1516
+ break;
1517
+ case "wave":
1518
+ tl = waveBeat(c);
1519
+ break;
1520
+ case "cheer":
1521
+ tl = cheerBeat(c);
1522
+ break;
1523
+ default: {
1524
+ const _exhaustive = name;
1525
+ throw new Error(`unknown characterPreset "${_exhaustive}"`);
1526
+ }
1527
+ }
1528
+ return c.label && tl.kind === "beat" ? { ...tl, name: c.label } : tl;
1529
+ }
1530
+
1531
+ // ../core/src/figure.ts
1532
+ var K2 = 0.5523;
1533
+ var n2 = (v) => Number(v.toFixed(2));
1534
+ function limb(a, b, y0, y1) {
1535
+ const ka = n2(a * K2), kb = n2(b * K2);
1536
+ return `M ${-a} ${y0} C ${-a} ${n2(y0 - ka)} ${-ka} ${n2(y0 - a)} 0 ${n2(y0 - a)} C ${ka} ${n2(y0 - a)} ${a} ${n2(y0 - ka)} ${a} ${y0} L ${b} ${y1} C ${b} ${n2(y1 + kb)} ${kb} ${n2(y1 + b)} 0 ${n2(y1 + b)} C ${-kb} ${n2(y1 + b)} ${-b} ${n2(y1 + kb)} ${-b} ${y1} Z`;
1537
+ }
1538
+ function rrect(a, b, y0, y1, r) {
1539
+ return `M ${n2(-a + r)} ${y0} L ${n2(a - r)} ${y0} Q ${a} ${y0} ${a} ${n2(y0 + r)} L ${b} ${n2(y1 - r)} Q ${b} ${y1} ${n2(b - r)} ${y1} L ${n2(-b + r)} ${y1} Q ${-b} ${y1} ${-b} ${n2(y1 - r)} L ${-a} ${n2(y0 + r)} Q ${-a} ${y0} ${n2(-a + r)} ${y0} Z`;
1540
+ }
1541
+ function darken(hex, f) {
1542
+ const h = hex.replace("#", "");
1543
+ const v = parseInt(h.length === 3 ? [...h].map((c) => c + c).join("") : h, 16);
1544
+ const ch = (s) => Math.max(0, Math.min(255, Math.round((v >> s & 255) * (1 - f))));
1545
+ const hx = (x) => x.toString(16).padStart(2, "0");
1546
+ return `#${hx(ch(16))}${hx(ch(8))}${hx(ch(0))}`;
1547
+ }
1548
+ var DEF = {
1549
+ clean: { skin: "#E9B58E", hair: "#2B313F", top: "#E86C4A", pants: "#39425C", shoe: "#20242F", accent: "#E86C4A" },
1550
+ cute: { skin: "#FFD2A6", hair: "#5B4636", top: "#FF7E5F", pants: "#3E6F8E", shoe: "#272B38", accent: "#FF7E5F" }
1551
+ };
1552
+ function resolvePal(style, p = {}) {
1553
+ const d = DEF[style];
1554
+ const accent = p.accent ?? d.accent;
1555
+ const top = p.top ?? (style === "clean" ? accent : d.top);
1556
+ const skin = p.skin ?? d.skin;
1557
+ const hair = p.hair ?? d.hair;
1558
+ const pants = p.pants ?? d.pants;
1559
+ const shoe = p.shoe ?? d.shoe;
1560
+ return {
1561
+ skin,
1562
+ skinSh: darken(skin, 0.12),
1563
+ hair,
1564
+ hairSh: darken(hair, 0.14),
1565
+ top,
1566
+ topSh: darken(top, 0.12),
1567
+ pants,
1568
+ pantsSh: darken(pants, 0.14),
1569
+ shoe,
1570
+ shoeSh: darken(shoe, 0.22),
1571
+ eye: "#2B313F",
1572
+ cheek: "#FF9E7E",
1573
+ white: "#FFFFFF",
1574
+ mouth: "#8A4233"
1575
+ };
1576
+ }
1577
+ var fp = (id, d, fill, stroke, sw = 0, opacity = 1) => path({ id, d, x: 0, y: 0, fill, opacity, ...stroke && sw > 0 ? { stroke, strokeWidth: sw } : {} });
1578
+ function cleanParts(p, face) {
1579
+ const HC = -42;
1580
+ return {
1581
+ upperArm: (j) => [fp(`${j}-sleeve`, limb(12, 10, 2, 58), p.top)],
1582
+ forearm: (j) => [
1583
+ fp(`${j}-elbow`, ovalPath(10, 10, 0, 3), p.skin),
1584
+ fp(`${j}-fore`, limb(10, 8, 2, 48), p.skin),
1585
+ fp(`${j}-hand`, ovalPath(11, 12, 0, 50), p.skin)
1586
+ ],
1587
+ thigh: (j) => [fp(`${j}-thigh`, limb(15, 13, 2, 72), p.pants)],
1588
+ shin: (j) => [
1589
+ fp(`${j}-knee`, ovalPath(13, 13, 0, 2), p.pants),
1590
+ fp(`${j}-shin`, limb(13, 11, 2, 62), p.pants),
1591
+ fp(`${j}-shoe`, ovalPath(15, 9, 4, 67), p.shoe)
1592
+ ],
1593
+ torso: (j) => [
1594
+ fp(`${j}-shadow`, rrect(38, 26, -28, 52, 20), p.topSh),
1595
+ fp(`${j}-top`, rrect(40, 27, -30, 52, 22), p.top),
1596
+ fp(`${j}-pelvis`, rrect(29, 24, 46, 104, 14), p.pants)
1597
+ ],
1598
+ head: (j) => [
1599
+ fp(`${j}-neck`, rrect(9, 9, 2, 22, 5), p.skin),
1600
+ fp(`${j}-skin`, ovalPath(42, 46, 0, HC), p.skin),
1601
+ fp(`${j}-hair`, ovalPath(44, 27, 0, HC - 31), p.hair),
1602
+ fp(`${j}-hairL`, ovalPath(8, 14, -39, HC - 18), p.hair),
1603
+ fp(`${j}-hairR`, ovalPath(8, 14, 39, HC - 18), p.hair),
1604
+ ...face ? [fp(`${j}-eyeL`, ovalPath(5, 7, -14, HC + 2), p.eye), fp(`${j}-eyeR`, ovalPath(5, 7, 14, HC + 2), p.eye)] : []
1605
+ ]
1606
+ };
1607
+ }
1608
+ var CUTE_HAIR = "M -64 -54 C -78 -96 -50 -126 0 -126 C 50 -126 78 -96 64 -54 C 60 -34 48 -28 41 -33 C 35 -54 23 -60 9 -60 C 3 -60 -3 -60 -9 -60 C -23 -60 -35 -54 -41 -33 C -48 -28 -60 -34 -64 -54 Z";
1609
+ function cuteParts(p, face) {
1610
+ const HC = -50;
1611
+ return {
1612
+ upperArm: (j) => [fp(`${j}-sleeve`, limb(15, 13, 2, 58), p.top, p.topSh, 2.5)],
1613
+ forearm: (j) => [
1614
+ fp(`${j}-elbow`, ovalPath(12, 12, 0, 3), p.skin, p.skinSh, 2.5),
1615
+ fp(`${j}-fore`, limb(12, 10, 2, 46), p.skin, p.skinSh, 2.5),
1616
+ fp(`${j}-hand`, ovalPath(13, 14, 0, 50), p.skin, p.skinSh, 2.5)
1617
+ ],
1618
+ thigh: (j) => [fp(`${j}-thigh`, limb(19, 16, 2, 72), p.pants, p.pantsSh, 2.5)],
1619
+ shin: (j) => [
1620
+ fp(`${j}-knee`, ovalPath(16, 16, 0, 2), p.pants, p.pantsSh, 2.5),
1621
+ fp(`${j}-shin`, limb(15, 12, 2, 60), p.pants, p.pantsSh, 2.5),
1622
+ fp(`${j}-shoe`, ovalPath(18, 11, 5, 66), p.shoe, darken(p.shoe, 0.25), 2.5)
1623
+ ],
1624
+ torso: (j) => [
1625
+ fp(`${j}-shadow`, rrect(40, 34, -18, 52, 16), p.topSh),
1626
+ fp(`${j}-top`, rrect(42, 35, -20, 52, 18), p.top, p.topSh, 2.5),
1627
+ fp(`${j}-pelvis`, rrect(36, 30, 46, 104, 16), p.pants, p.pantsSh, 2.5)
1628
+ ],
1629
+ head: (j) => [
1630
+ fp(`${j}-neck`, ovalPath(15, 12, 0, 12), p.skinSh),
1631
+ fp(`${j}-skin`, ovalPath(42, 46, 0, HC + 4), p.skin, p.skinSh, 2.5),
1632
+ fp(`${j}-cheekL`, ovalPath(11, 8, -40, HC + 22), p.cheek, void 0, 0, 0.5),
1633
+ fp(`${j}-cheekR`, ovalPath(11, 8, 40, HC + 22), p.cheek, void 0, 0, 0.5),
1634
+ fp(`${j}-hair`, CUTE_HAIR, p.hair, p.hairSh, 2),
1635
+ ...face ? [
1636
+ fp(`${j}-eyeL`, ovalPath(10, 13, -25, HC + 8), p.eye),
1637
+ fp(`${j}-eyeR`, ovalPath(10, 13, 25, HC + 8), p.eye),
1638
+ fp(`${j}-glL`, ovalPath(3.4, 3.4, -28, HC + 3), p.white),
1639
+ fp(`${j}-glR`, ovalPath(3.4, 3.4, 22, HC + 3), p.white),
1640
+ path({ id: `${j}-mouth`, d: "M -15 0 Q 0 15 15 0", x: 0, y: HC + 28, fill: "none", stroke: p.mouth, strokeWidth: 5 })
1641
+ ] : []
1642
+ ]
1643
+ };
1644
+ }
1645
+ function buildSkeleton(id, S) {
1646
+ const arm = (side, x, r1, r2) => ({
1647
+ name: `armUpper${side}`,
1648
+ at: [x, -14],
1649
+ length: 60,
1650
+ width: 0,
1651
+ rotation: r1,
1652
+ shape: S.upperArm(`${id}-armUpper${side}`),
1653
+ children: [{ name: `armLower${side}`, at: [0, 60], length: 56, width: 0, rotation: r2, shape: S.forearm(`${id}-armLower${side}`) }]
1654
+ });
1655
+ const leg = (side, x, r1, r2) => ({
1656
+ name: `legUpper${side}`,
1657
+ at: [x, 76],
1658
+ length: 76,
1659
+ width: 0,
1660
+ rotation: r1,
1661
+ shape: S.thigh(`${id}-legUpper${side}`),
1662
+ children: [{ name: `legLower${side}`, at: [0, 76], length: 72, width: 0, rotation: r2, shape: S.shin(`${id}-legLower${side}`) }]
1663
+ });
1664
+ return {
1665
+ name: "chest",
1666
+ at: [0, 0],
1667
+ shape: S.torso(`${id}-chest`),
1668
+ children: [
1669
+ { name: "head", at: [0, -42], rotation: 0, shape: S.head(`${id}-head`) },
1670
+ arm("L", -40, 8, 6),
1671
+ arm("R", 40, -8, -6),
1672
+ leg("L", -20, 3, -2),
1673
+ leg("R", 20, -3, 2)
1674
+ ]
1675
+ };
1676
+ }
1677
+ function figure(opts = {}) {
1678
+ const style = opts.style ?? "clean";
1679
+ const pal = resolvePal(style, opts.palette);
1680
+ const face = opts.face ?? true;
1681
+ const id = opts.id ?? "figure";
1682
+ const parts = style === "clean" ? cleanParts(pal, face) : cuteParts(pal, face);
1683
+ const rigOpts = { id };
1684
+ if (opts.x !== void 0) rigOpts.x = opts.x;
1685
+ if (opts.y !== void 0) rigOpts.y = opts.y;
1686
+ if (opts.scale !== void 0) rigOpts.scale = opts.scale;
1687
+ if (opts.opacity !== void 0) rigOpts.opacity = opts.opacity;
1688
+ return rig(buildSkeleton(id, parts), rigOpts);
1689
+ }
1690
+
1266
1691
  // ../core/src/motionOps.ts
1267
1692
  var MOTION_OPS = ["rotate", "zoom", "ken-burns", "slide-in", "fade", "draw-on", "pulse"];
1268
- var clamp012 = (n) => Math.max(0, Math.min(1, n));
1693
+ var clamp013 = (n3) => Math.max(0, Math.min(1, n3));
1269
1694
  function settleEase2(e) {
1270
1695
  return e < 0.34 ? "easeOutCubic" : e < 0.67 ? "easeOutBack" : "easeOutElastic";
1271
1696
  }
@@ -1283,7 +1708,7 @@ function fromVec2(from, dist) {
1283
1708
  }
1284
1709
  var motionOpLabel = (name, target) => `op-${name}-${target}`;
1285
1710
  function motionOp(name, target, opts = {}) {
1286
- const e = clamp012(opts.energy ?? 0.5);
1711
+ const e = clamp013(opts.energy ?? 0.5);
1287
1712
  const sp = Math.max(0.25, opts.speed ?? 1);
1288
1713
  const amt = opts.amount ?? 1;
1289
1714
  const b = { scale: 1, x: 0, y: 0, rotation: 0, ...opts.base };
@@ -1493,8 +1918,8 @@ function valueNoise(x, seed) {
1493
1918
  const b = hash01(i + 1, seed) * 2 - 1;
1494
1919
  return a + (b - a) * u;
1495
1920
  }
1496
- function hash01(n, seed) {
1497
- let h = n * 374761393 + seed * 668265263 | 0;
1921
+ function hash01(n3, seed) {
1922
+ let h = n3 * 374761393 + seed * 668265263 | 0;
1498
1923
  h = h ^ h >>> 13 | 0;
1499
1924
  h = Math.imul(h, 1274126177);
1500
1925
  h = (h ^ h >>> 16) >>> 0;
@@ -1587,8 +2012,8 @@ function isColor(v) {
1587
2012
  function parseColor(hex) {
1588
2013
  let h = hex.slice(1);
1589
2014
  if (h.length <= 4) h = [...h].map((c) => c + c).join("");
1590
- const n = parseInt(h.padEnd(8, "f"), 16);
1591
- return [n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255];
2015
+ const n3 = parseInt(h.padEnd(8, "f"), 16);
2016
+ return [n3 >>> 24 & 255, n3 >>> 16 & 255, n3 >>> 8 & 255, n3 & 255];
1592
2017
  }
1593
2018
  function formatColor([r, g, b, a]) {
1594
2019
  const hex = (v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0");
@@ -1608,19 +2033,68 @@ function lerpValue(from, to2, u) {
1608
2033
  a[3] + (b[3] - a[3]) * u
1609
2034
  ]);
1610
2035
  }
2036
+ if (looksLikePath(from) && looksLikePath(to2)) {
2037
+ const a = tokenizePath(from);
2038
+ const b = tokenizePath(to2);
2039
+ if (a && b && morphCompatible(a, b)) return morphPath(a, b, u);
2040
+ return u < 0.5 ? from : to2;
2041
+ }
1611
2042
  return to2;
1612
2043
  }
2044
+ var PATH_BODY = /^[\sMmLlHhVvCcSsQqTtAaZz0-9.,eE+-]+$/;
2045
+ function looksLikePath(v) {
2046
+ return typeof v === "string" && /^\s*[Mm]/.test(v) && PATH_BODY.test(v);
2047
+ }
2048
+ var PATH_TOKEN = /([MmLlHhVvCcSsQqTtAaZz])|(-?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)/g;
2049
+ function tokenizePath(d) {
2050
+ const out = [];
2051
+ let cur = null;
2052
+ let m;
2053
+ PATH_TOKEN.lastIndex = 0;
2054
+ while (m = PATH_TOKEN.exec(d)) {
2055
+ if (m[1]) out.push(cur = { cmd: m[1], nums: [] });
2056
+ else if (m[2]) {
2057
+ if (!cur) return null;
2058
+ cur.nums.push(parseFloat(m[2]));
2059
+ }
2060
+ }
2061
+ return out.length ? out : null;
2062
+ }
2063
+ function morphCompatible(a, b) {
2064
+ if (a.length !== b.length) return false;
2065
+ for (let i = 0; i < a.length; i++) {
2066
+ const ca = a[i];
2067
+ const cb = b[i];
2068
+ if (ca.cmd !== cb.cmd || ca.nums.length !== cb.nums.length) return false;
2069
+ if (ca.cmd === "A" || ca.cmd === "a") return false;
2070
+ }
2071
+ return true;
2072
+ }
2073
+ var fmtNum = (v) => {
2074
+ const r = Number(v.toFixed(3));
2075
+ return Object.is(r, -0) ? "0" : String(r);
2076
+ };
2077
+ function morphPath(a, b, u) {
2078
+ let s = "";
2079
+ for (let i = 0; i < a.length; i++) {
2080
+ const an = a[i].nums;
2081
+ const bn = b[i].nums;
2082
+ s += (i ? " " : "") + a[i].cmd;
2083
+ for (let j = 0; j < an.length; j++) s += " " + fmtNum(an[j] + (bn[j] - an[j]) * u);
2084
+ }
2085
+ return s;
2086
+ }
1613
2087
 
1614
2088
  // ../core/src/evaluate.ts
1615
2089
  var IDENTITY = [1, 0, 0, 1, 0, 0];
1616
- function multiply(m, n) {
2090
+ function multiply(m, n3) {
1617
2091
  return [
1618
- m[0] * n[0] + m[2] * n[1],
1619
- m[1] * n[0] + m[3] * n[1],
1620
- m[0] * n[2] + m[2] * n[3],
1621
- m[1] * n[2] + m[3] * n[3],
1622
- m[0] * n[4] + m[2] * n[5] + m[4],
1623
- m[1] * n[4] + m[3] * n[5] + m[5]
2092
+ m[0] * n3[0] + m[2] * n3[1],
2093
+ m[1] * n3[0] + m[3] * n3[1],
2094
+ m[0] * n3[2] + m[2] * n3[3],
2095
+ m[1] * n3[2] + m[3] * n3[3],
2096
+ m[0] * n3[4] + m[2] * n3[5] + m[4],
2097
+ m[1] * n3[4] + m[3] * n3[5] + m[5]
1624
2098
  ];
1625
2099
  }
1626
2100
  function localMatrix(x, y, rotationDeg, scale, scaleX = 1, scaleY = 1, skewXDeg = 0, skewYDeg = 0) {
@@ -1635,9 +2109,9 @@ function localMatrix(x, y, rotationDeg, scale, scaleX = 1, scaleY = 1, skewXDeg
1635
2109
  const tx = Math.tan(skewXDeg * Math.PI / 180);
1636
2110
  const ty = Math.tan(skewYDeg * Math.PI / 180);
1637
2111
  const R = [c, s, -s, c, 0, 0];
1638
- const K = [1, ty, tx, 1, 0, 0];
2112
+ const K3 = [1, ty, tx, 1, 0, 0];
1639
2113
  const S = [scale * scaleX, 0, 0, scale * scaleY, 0, 0];
1640
- const m = multiply(R, multiply(K, S));
2114
+ const m = multiply(R, multiply(K3, S));
1641
2115
  return [m[0], m[1], m[2], m[3], x, y];
1642
2116
  }
1643
2117
  var ANCHOR_FACTORS = {
@@ -1937,29 +2411,29 @@ function sketchToTimeline(sketch, nodeIds) {
1937
2411
  const steps = [];
1938
2412
  events.forEach((ev, i) => {
1939
2413
  const node = nodeIds[i % nodeIds.length];
1940
- const dur2 = Math.max(0.05, ev.t1 - ev.t0);
2414
+ const dur3 = Math.max(0.05, ev.t1 - ev.t0);
1941
2415
  const ease = easeFor(ev.easing);
1942
2416
  let motion;
1943
2417
  switch (ev.kind) {
1944
2418
  case "enter":
1945
- motion = tween(node, { opacity: 1 }, { duration: dur2, ease });
2419
+ motion = tween(node, { opacity: 1 }, { duration: dur3, ease });
1946
2420
  break;
1947
2421
  case "exit":
1948
- motion = tween(node, { opacity: 0 }, { duration: dur2, ease });
2422
+ motion = tween(node, { opacity: 0 }, { duration: dur3, ease });
1949
2423
  break;
1950
2424
  case "emphasis": {
1951
2425
  const peak = 1 + Math.max(0.08, Math.min(0.5, ev.magnitude));
1952
2426
  motion = seq(
1953
- tween(node, { scale: peak }, { duration: dur2 / 2, ease: "easeOutCubic" }),
1954
- tween(node, { scale: 1 }, { duration: dur2 / 2, ease: "easeInOutQuad" })
2427
+ tween(node, { scale: peak }, { duration: dur3 / 2, ease: "easeOutCubic" }),
2428
+ tween(node, { scale: 1 }, { duration: dur3 / 2, ease: "easeInOutQuad" })
1955
2429
  );
1956
2430
  break;
1957
2431
  }
1958
2432
  case "scale":
1959
- motion = tween(node, { scale: 1 + Math.max(-0.5, Math.min(0.5, ev.magnitude)) }, { duration: dur2, ease });
2433
+ motion = tween(node, { scale: 1 + Math.max(-0.5, Math.min(0.5, ev.magnitude)) }, { duration: dur3, ease });
1960
2434
  break;
1961
2435
  case "move":
1962
- motion = tween(node, { opacity: 1 }, { duration: dur2, ease });
2436
+ motion = tween(node, { opacity: 1 }, { duration: dur3, ease });
1963
2437
  break;
1964
2438
  }
1965
2439
  steps.push(ev.t0 > 0 ? seq(wait(ev.t0), motion) : motion);
@@ -1967,6 +2441,7 @@ function sketchToTimeline(sketch, nodeIds) {
1967
2441
  return par(...steps);
1968
2442
  }
1969
2443
  export {
2444
+ CHARACTER_PRESET_NAMES,
1970
2445
  DEFAULT_CROSSFADE,
1971
2446
  DEFAULT_FPS,
1972
2447
  DEFAULT_MOTIONPATH_DURATION,
@@ -1980,6 +2455,7 @@ export {
1980
2455
  SFX_DURATION,
1981
2456
  SceneValidationError,
1982
2457
  beat,
2458
+ characterPreset,
1983
2459
  collectImageSrcs,
1984
2460
  compileComposition,
1985
2461
  compileScene,
@@ -1991,8 +2467,11 @@ export {
1991
2467
  deviceScreenCenter,
1992
2468
  ellipse,
1993
2469
  evaluate,
2470
+ figure,
1994
2471
  formatComposeReport,
1995
2472
  group,
2473
+ humanoid,
2474
+ ikReach,
1996
2475
  image,
1997
2476
  isColor,
1998
2477
  lerpValue,
@@ -2003,14 +2482,18 @@ export {
2003
2482
  motionPreset,
2004
2483
  nodeParentMatrix,
2005
2484
  oscillate,
2485
+ ovalPath,
2006
2486
  par,
2007
2487
  path,
2008
2488
  pathPoint,
2009
2489
  pathTangentAngle,
2490
+ poseTo,
2010
2491
  rect,
2011
2492
  resolveAudioPlan,
2012
2493
  resolveCompositionAudioPlan,
2013
2494
  resolveEase,
2495
+ rig,
2496
+ rigPose,
2014
2497
  sampleBehavior,
2015
2498
  sampleProp,
2016
2499
  scene,
@@ -0,0 +1,39 @@
1
+ /**
2
+ * characterPreset — a SEEDED motion generator for the humanoid rig. The
3
+ * character analog of `motionPreset`: `characterPreset(name, opts)` returns a
4
+ * `beat` (a TimelineIR) that drives a `humanoid()` rig's joints through a named
5
+ * performance (walk/run/jump/dance/wave/cheer). Same `(name, knobs, seed)` →
6
+ * identical IR; a different `seed` varies it within the family.
7
+ *
8
+ * seq(characterPreset("walk", { target: "hero", at: [CX, BASE_Y], cycles: 4 }))
9
+ *
10
+ * Pure keyframe timeline (a beat can't hold behaviors): secondary motion is
11
+ * baked into poses; continuous idle stays the author's `oscillate`. Legs use the
12
+ * 2-bone `ikReach` solver (foot targets relative to the hip → natural knee bend);
13
+ * arms swing via FK. Assumes the `humanoid()` joint names.
14
+ */
15
+ import type { TimelineIR } from "./ir.js";
16
+ export declare const CHARACTER_PRESET_NAMES: readonly ["walk", "run", "jump", "dance", "wave", "cheer"];
17
+ export type CharacterPresetName = (typeof CHARACTER_PRESET_NAMES)[number];
18
+ export interface CharacterPresetOpts {
19
+ /** humanoid rig id — joints are `${target}-${name}`, outer group = `${target}`. */
20
+ target: string;
21
+ /** 0..1 — stride / swing / bounce / jump-height amplitude (default 0.5). */
22
+ energy?: number;
23
+ /** >0 — tempo; durations divide by it (default 1, min 0.25). */
24
+ speed?: number;
25
+ /** Deterministic within-family variation (default 0). */
26
+ seed?: number;
27
+ /** Repeats for cyclic motions walk/run/dance (default 4). */
28
+ cycles?: number;
29
+ /** 1 = faces/moves right (default 1). */
30
+ facing?: 1 | -1;
31
+ /** The rig's scene position — needed to translate the body (walk travel, jump lift). Default [0,0]. */
32
+ at?: [number, number];
33
+ /** px travelled per cycle for walk/run (default ~stride·2; 0 = walk in place). */
34
+ travel?: number;
35
+ /** Override the beat name (overlay address) — set this when the same preset is
36
+ * used more than once in a scene so the beat labels stay unique. */
37
+ label?: string;
38
+ }
39
+ export declare function characterPreset(name: CharacterPresetName, opts: CharacterPresetOpts): TimelineIR;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * figure() — a parametric DRESSED character, the sibling of `humanoid()`. Same
3
+ * skeleton geometry (so `characterPreset` / `ikReach` / overlays apply unchanged),
4
+ * but each bone carries a coloured, designed flat shape instead of a neon capsule.
5
+ * Two styles — `clean` (corporate-flat, undraw register: one accent + neutrals,
6
+ * minimal face, slim adult proportions) and `cute` (mascot: big head, full face).
7
+ * Palette knobs re-skin it in one line; shadows are derived so a single `accent`
8
+ * recolours the whole figure.
9
+ *
10
+ * figure({ id: "fig", style: "clean", palette: { accent: "#3B82F6" } })
11
+ * characterPreset("walk", { target: "fig", at: [x, y] }) // drives it
12
+ */
13
+ import type { NodeIR } from "./ir.js";
14
+ import { type RigOpts } from "./rig.js";
15
+ export type FigureStyle = "clean" | "cute";
16
+ export interface FigurePalette {
17
+ skin?: string;
18
+ hair?: string;
19
+ top?: string;
20
+ pants?: string;
21
+ shoe?: string;
22
+ accent?: string;
23
+ }
24
+ export interface FigureOpts extends RigOpts {
25
+ /** "clean" (corporate-flat, default) | "cute" (mascot). */
26
+ style?: FigureStyle;
27
+ /** Colour overrides merged onto the per-style defaults. */
28
+ palette?: FigurePalette;
29
+ /** Draw minimal facial features (default true). `false` = faceless (pure undraw). */
30
+ face?: boolean;
31
+ }
32
+ export declare function figure(opts?: FigureOpts): NodeIR;
@@ -7,6 +7,9 @@ export { compileScene, type CompiledScene, type PropertySegment, type LabelSpan,
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
9
  export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, DEVICE_PRESET_NAMES, type DevicePresetName, type DevicePresetOpts } from "./devicePreset.js";
10
+ export { rig, rigPose, poseTo, ikReach, humanoid, ovalPath, type Bone, type RigOpts, type Pose, type HumanoidOpts } from "./rig.js";
11
+ export { characterPreset, CHARACTER_PRESET_NAMES, type CharacterPresetName, type CharacterPresetOpts } from "./characterPreset.js";
12
+ export { figure, type FigureStyle, type FigureOpts, type FigurePalette } from "./figure.js";
10
13
  export { motionOp, motionOpLabel, MOTION_OPS, type MotionOpName, type MotionOpOpts, type MotionOpResult } from "./motionOps.js";
11
14
  export { resolveAudioPlan, resolveCompositionAudioPlan, SFX_DURATION, type AudioPlan, type ResolvedCue, } from "./audio.js";
12
15
  export { evaluate, sampleProp, nodeParentMatrix, type DisplayList, type DisplayOp, type Mat2D, type ClipRegion, type TextAlign, type TextBaseline, } from "./evaluate.js";
@@ -5,8 +5,9 @@ export declare function resolveEase(ease: Ease | undefined): EaseFn;
5
5
  export declare function isColor(v: PropValue): v is string;
6
6
  /**
7
7
  * Interpolate two prop values at progress u (already eased).
8
- * number↔number lerps, color↔color lerps in RGB, anything else switches
9
- * discretely at the start of the segment.
8
+ * number↔number lerps, color↔color lerps in RGB, two *compatible* SVG path
9
+ * `d` strings morph vertex-by-vertex (the Lottie-style shape tween), anything
10
+ * else switches discretely.
10
11
  */
11
12
  export declare function lerpValue(from: PropValue, to: PropValue, u: number): PropValue;
12
13
  export {};
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Character rig: a first-class, declarative skeleton that COMPILES to plain IR.
3
+ * The character analog of `devicePreset` (generates a NodeIR subtree) and
4
+ * `motionPreset` (motion vocabulary). A `Bone` tree → nested `group` joints with
5
+ * stable ids, each holding the bone's vector art; posing is forward-kinematics
6
+ * (tween a joint group's `rotation`). Because it lowers to groups + paths, it
7
+ * inherits the renderer, evaluate, overlay editing, preview, validation and the
8
+ * determinism/golden contract for free — purely additive.
9
+ *
10
+ * const body = humanoid({ id: "hero" }); // one-call skeleton
11
+ * poseTo("hero", { armUpperR: -150 }, { duration: 0.4 }); // wave
12
+ *
13
+ * Bone convention: the joint sits at the group origin (0,0); the bone extends
14
+ * along +Y at rotation 0. A child joint's `at` pivot is in the PARENT bone's
15
+ * local space (e.g. an elbow at [0, upperLength]). Joint names are the STABLE
16
+ * regen addresses — id `${id}-${name}`; never rename them across a regen. Each
17
+ * rig instance needs a distinct `id` (duplicate joints collide via the scene's
18
+ * duplicate-id validation, exactly like `devicePreset`).
19
+ */
20
+ import type { Ease, NodeIR, TimelineIR } from "./ir.js";
21
+ export interface Bone {
22
+ /** Stable joint name → group id `${id}-${name}`. */
23
+ name: string;
24
+ /** Joint pivot, in the PARENT bone's local space (root: usually [0,0]). */
25
+ at: [number, number];
26
+ /** Bone length along +Y — drives the default capsule shape and IK. */
27
+ length?: number;
28
+ /** Default-capsule width (default 20). */
29
+ width?: number;
30
+ /** Rest-pose angle (deg); 0 = points down (+Y). */
31
+ rotation?: number;
32
+ /** Custom bone art (joint at origin, extends +Y) — overrides the default capsule. */
33
+ shape?: NodeIR[];
34
+ children?: Bone[];
35
+ }
36
+ export interface RigOpts {
37
+ /** Id PREFIX for the outer group and every joint (default "rig"); unique per instance. */
38
+ id?: string;
39
+ /** Root placement (default 0,0). */
40
+ x?: number;
41
+ y?: number;
42
+ /** Uniform scale (default 1). */
43
+ scale?: number;
44
+ /** Outer-group opacity (default 1) — start hidden for an entrance. */
45
+ opacity?: number;
46
+ /** Bone line colour (default warm). */
47
+ color?: string;
48
+ /** Bone fill (default near-bg, so overlapping joints occlude cleanly). */
49
+ fill?: string;
50
+ /** Glow accent for the double-path look on default bones (default off). */
51
+ glow?: string | false;
52
+ }
53
+ /** jointName → angle (deg). */
54
+ export type Pose = Record<string, number>;
55
+ /** A 4-cubic oval (morphable) centred at (cx,cy). Handy for heads/torsos/hands. */
56
+ export declare function ovalPath(a: number, b: number, cx?: number, cy?: number): string;
57
+ /** Compile a skeleton to a NodeIR group tree. Outer group id = `${id}`; each
58
+ * joint = `${id}-${name}` (the stable pose/overlay address). */
59
+ export declare function rig(root: Bone, opts?: RigOpts): NodeIR;
60
+ /** A pose as a `states` fragment: `{ "${id}-${joint}": { rotation } }`. Merge
61
+ * into scene `states` and transition with the existing `to(state, …)`. */
62
+ export declare function rigPose(id: string, pose: Pose): Record<string, {
63
+ rotation: number;
64
+ }>;
65
+ /** Pose-to-pose on the timeline: a `par` (or `stagger`) of rotation tweens. */
66
+ export declare function poseTo(id: string, pose: Pose, opts?: {
67
+ duration?: number;
68
+ ease?: Ease;
69
+ stagger?: number;
70
+ }): TimelineIR;
71
+ /**
72
+ * 2-bone inverse kinematics. Returns `[shoulderDeg, elbowDeg]` (the +Y-down bone
73
+ * convention) that place the chain's tip at `(dx,dy)` relative to the root joint.
74
+ * Exact for in-reach targets; clamps gracefully (no NaN) when out of reach.
75
+ * `flip` chooses the elbow-up vs elbow-down solution.
76
+ *
77
+ * Derivation: with R(θ) the canvas rotation, the tip is
78
+ * R(θ1)·[ (0,upper) + R(θ2)·(0,lower) ]. The bracket has length D=hypot(dx,dy),
79
+ * giving cosθ2 = (D²−u²−l²)/(2ul); then θ1 rotates that bracket onto the target.
80
+ */
81
+ export declare function ikReach(upper: number, lower: number, dx: number, dy: number, flip?: boolean): [number, number];
82
+ export interface HumanoidOpts extends Omit<RigOpts, never> {
83
+ }
84
+ /** A ready upright humanoid skeleton — the one-call body. Joints:
85
+ * chest, head, armUpper/LowerL, armUpper/LowerR, legUpper/LowerL, legUpper/LowerR.
86
+ * Rooted at the chest so every limb extends naturally along +Y. */
87
+ export declare function humanoid(opts?: HumanoidOpts): NodeIR;
@@ -41,6 +41,12 @@ Factories return plain data. Every node needs a unique `id`.
41
41
  the art's centre (e.g. the viewBox centre) so `scale`/`rotation` happen about the
42
42
  middle. `d` is drawn in its own coords; `x`/`y` place that pivot. Classic logo
43
43
  reveal: a stroke path drawing on, then a fill path fading in over it.
44
+ **`d` is animatable (shape morph):** `tween(id, { d: otherShape }, …)` morphs
45
+ the path vertex-by-vertex (the Lottie-style shape tween) when both `d` strings
46
+ share the same command sequence and arg counts — author the two poses with the
47
+ same structure (e.g. both 4-cubic ovals). Arcs (`A`) can't morph (their 0/1
48
+ flags aren't interpolable) and incompatible shapes snap at the midpoint; build
49
+ morph targets from `M/L/C/Q/Z` only.
44
50
  - `image({ id, src, x, y, width, height, opacity?, rotation?, scale?, anchor? })` —
45
51
  `src` is a file path, absolute or relative to the scene file; drawn stretched
46
52
  to `width`×`height` (png/jpg/webp). `src` switches discretely (no crossfade) —
@@ -115,6 +121,52 @@ bound — e.g. a pulse only during the hold:
115
121
  `oscillate("title", "scale", { amplitude: 0.04, frequency: 1.2 }, { from: 1.5, until: 3.5 })`.
116
122
  Omit the window to run for the whole scene.
117
123
 
124
+ ## Character rig (skeleton, poses, IK)
125
+
126
+ A first-class, declarative character rig that **compiles to plain IR** (nested
127
+ `group` joints + bone paths) — the character analog of `devicePreset`. It needs
128
+ no new renderer concept, so overlays/preview/determinism all apply.
129
+
130
+ - `humanoid({ id, x, y, scale, opacity?, color?, fill?, glow? })` → a NodeIR: a
131
+ ready upright body. Joints (stable ids `${id}-${name}`): `chest`, `head`,
132
+ `armUpperL/armLowerL`, `armUpperR/armLowerR`, `legUpperL/legLowerL`,
133
+ `legUpperR/legLowerR`. Drop it in `nodes`.
134
+ - `rig(boneTree, opts)` → build your own skeleton. A `Bone` is
135
+ `{ name, at:[x,y], length?, width?, rotation?, shape?, children? }`. The joint
136
+ sits at the group origin; the bone extends **+Y at rotation 0**; a child's `at`
137
+ pivot is in the PARENT bone's local space (e.g. an elbow at `[0, upperLength]`).
138
+ Nested groups give forward kinematics — a child's rotation composes on its
139
+ parent's. Default bone = a bezier capsule (morphable); pass `shape` for custom art.
140
+ - A **pose** is `{ jointName: angleDeg }` (0 = bone points down). Animate it:
141
+ - `poseTo(id, pose, { duration, ease, stagger? })` → a timeline step (a `par`
142
+ of rotation tweens). Sequence poses for wave/jump/run.
143
+ - `rigPose(id, pose)` → a `states` fragment, to transition with `to(state, …)`.
144
+ - `ikReach(upper, lower, dx, dy, flip?)` → `[shoulderDeg, elbowDeg]` that place a
145
+ 2-bone limb's tip at `(dx,dy)` relative to its shoulder joint (law of cosines;
146
+ clamps when out of reach). Feed the two angles into a pose.
147
+ - Joint names are the **stable regen addresses** — never rename them across a
148
+ regen; each rig instance needs a distinct `id` (duplicates collide via scene
149
+ validation). Squash/stretch and expressions are per-bone `d` morphs (above),
150
+ composed on top of FK posing. Idle sway/breathing = `oscillate` on a joint.
151
+ - `figure(opts)` — a **dressed** character (the styled sibling of `humanoid`):
152
+ same skeleton, but coloured flat-design shapes. `style: "clean"` (corporate-flat
153
+ / undraw register, the default) or `"cute"` (mascot); `palette` knobs
154
+ (`skin`/`hair`/`top`/`pants`/`shoe`/`accent`) re-skin it — for `clean` the top
155
+ follows `accent`, so `figure({ palette: { accent: "#3B82F6" } })` recolours the
156
+ whole figure; `face: false` makes it faceless. It exposes the humanoid joint
157
+ ids, so `characterPreset` / `ikReach` drive it unchanged. Use it as the
158
+ supporting actor in a product promo (gesturing at a `devicePreset`), not the hero.
159
+ - `characterPreset(name, opts)` — a **seeded motion generator** for a `humanoid`
160
+ or `figure` rig (the character analog of `motionPreset`). Returns a composable `beat`;
161
+ drop it in the timeline: `seq(characterPreset("walk", { target: "hero", at:
162
+ [cx, cy], cycles: 4 }))`. Names: `walk`, `run`, `jump`, `dance`, `wave`,
163
+ `cheer`. Knobs: `target` (rig id), `energy` 0..1, `speed` (>0, divides
164
+ durations), `seed` (varies within the family), `cycles` (walk/run/dance),
165
+ `facing` (±1), `at: [x,y]` (the rig's scene position — needed for walk travel
166
+ & jump lift), `travel` (px/cycle, 0 = in place), `label` (unique beat name —
167
+ set it when the same preset is used more than once in a scene). Legs use
168
+ `ikReach`, arms FK; pure keyframes, so add continuous idle yourself with `oscillate`.
169
+
118
170
  ## Audio (optional)
119
171
 
120
172
  Label-anchored sound design — cues follow retiming and regeneration:
@@ -16,3 +16,14 @@ source):
16
16
  When the contract is broken anyway, `composeScene` skips the affected edits
17
17
  and reports them as orphans with the known-ids list — loud, diagnosable,
18
18
  never a silent drop and never a render failure.
19
+
20
+ ## Generated subtrees (devicePreset, rig/humanoid)
21
+
22
+ Generators emit nodes with deterministic ids under an instance prefix, and those
23
+ ids are stable addresses too. For `devicePreset(name,{id})` the screen/content
24
+ parts are `${id}-screen` / `${id}-content`. For `rig(...)` / `humanoid({id})`
25
+ each joint is `${id}-${jointName}` (e.g. `hero-armUpperR`) and its bone art is
26
+ `${id}-${jointName}-shape`. Across a regen, **keep the instance `id` and the
27
+ joint `name`s** for any character/device that survives the redesign — overlay
28
+ edits (a retimed wave, a nudged limb angle) reference those exact ids. Renaming a
29
+ joint orphans the edit, exactly like renaming a hand-authored node id.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reframe-video",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",