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.
- package/assets/sfx/LICENSE.md +1 -0
- package/assets/sfx/footstep_001.ogg +0 -0
- package/assets/sfx/footstep_002.ogg +0 -0
- package/assets/sfx/footstep_003.ogg +0 -0
- package/dist/bin.js +31 -2
- package/dist/browserEntry.js +49 -0
- package/dist/index.js +517 -34
- package/dist/types/characterPreset.d.ts +39 -0
- package/dist/types/figure.d.ts +32 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/interpolate.d.ts +3 -2
- package/dist/types/rig.d.ts +87 -0
- package/guides/edsl-guide.md +52 -0
- package/guides/regen-contract.md +11 -0
- package/package.json +1 -1
package/assets/sfx/LICENSE.md
CHANGED
|
@@ -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
|
|
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:
|
|
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 },
|
package/dist/browserEntry.js
CHANGED
|
@@ -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
|
|
18
|
+
const n3 = points.length;
|
|
19
19
|
const at = (k) => {
|
|
20
|
-
if (closed) return points[(k %
|
|
21
|
-
return points[Math.max(0, Math.min(
|
|
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
|
|
27
|
-
if (
|
|
28
|
-
return closed ?
|
|
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
|
|
32
|
-
if (
|
|
33
|
-
if (
|
|
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
|
|
53
|
-
if (
|
|
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((
|
|
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
|
|
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 =
|
|
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(
|
|
1497
|
-
let h =
|
|
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
|
|
1591
|
-
return [
|
|
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,
|
|
2090
|
+
function multiply(m, n3) {
|
|
1617
2091
|
return [
|
|
1618
|
-
m[0] *
|
|
1619
|
-
m[1] *
|
|
1620
|
-
m[0] *
|
|
1621
|
-
m[1] *
|
|
1622
|
-
m[0] *
|
|
1623
|
-
m[1] *
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
2419
|
+
motion = tween(node, { opacity: 1 }, { duration: dur3, ease });
|
|
1946
2420
|
break;
|
|
1947
2421
|
case "exit":
|
|
1948
|
-
motion = tween(node, { opacity: 0 }, { duration:
|
|
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:
|
|
1954
|
-
tween(node, { scale: 1 }, { duration:
|
|
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:
|
|
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:
|
|
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;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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,
|
|
9
|
-
*
|
|
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;
|
package/guides/edsl-guide.md
CHANGED
|
@@ -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:
|
package/guides/regen-contract.md
CHANGED
|
@@ -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.
|
|
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",
|