reframe-video 0.6.31 → 0.6.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1259,6 +1259,35 @@ function cameraTo(props, opts = {}) {
1259
1259
  return tween(CAMERA_ID, props, opts);
1260
1260
  }
1261
1261
 
1262
+ // ../core/src/behaviors.ts
1263
+ function sampleBehavior(b, t) {
1264
+ switch (b.name) {
1265
+ case "oscillate": {
1266
+ const { amplitude, frequency, phase = 0 } = b.params;
1267
+ return amplitude * Math.sin(2 * Math.PI * frequency * t + phase);
1268
+ }
1269
+ case "wiggle": {
1270
+ const { amplitude, frequency, seed } = b.params;
1271
+ return amplitude * valueNoise(t * frequency, seed);
1272
+ }
1273
+ }
1274
+ }
1275
+ function valueNoise(x, seed) {
1276
+ const i = Math.floor(x);
1277
+ const f = x - i;
1278
+ const u = f * f * (3 - 2 * f);
1279
+ const a = hash01(i, seed) * 2 - 1;
1280
+ const b = hash01(i + 1, seed) * 2 - 1;
1281
+ return a + (b - a) * u;
1282
+ }
1283
+ function hash01(n3, seed) {
1284
+ let h = n3 * 374761393 + seed * 668265263 | 0;
1285
+ h = h ^ h >>> 13 | 0;
1286
+ h = Math.imul(h, 1274126177);
1287
+ h = (h ^ h >>> 16) >>> 0;
1288
+ return h / 4294967295;
1289
+ }
1290
+
1262
1291
  // ../core/src/gradient.ts
1263
1292
  function isGradient(p) {
1264
1293
  return typeof p === "object" && p !== null && (p.kind === "linear" || p.kind === "radial" || p.kind === "conic");
@@ -1291,735 +1320,700 @@ function conicGradient(stops, opts = {}) {
1291
1320
  };
1292
1321
  }
1293
1322
 
1294
- // ../core/src/effects.ts
1295
- function glow(color, blur = 24) {
1296
- return { shadowColor: color, shadowBlur: blur, shadowX: 0, shadowY: 0 };
1323
+ // ../core/src/evaluate.ts
1324
+ var IDENTITY = [1, 0, 0, 1, 0, 0];
1325
+ function multiply(m, n3) {
1326
+ return [
1327
+ m[0] * n3[0] + m[2] * n3[1],
1328
+ m[1] * n3[0] + m[3] * n3[1],
1329
+ m[0] * n3[2] + m[2] * n3[3],
1330
+ m[1] * n3[2] + m[3] * n3[3],
1331
+ m[0] * n3[4] + m[2] * n3[5] + m[4],
1332
+ m[1] * n3[4] + m[3] * n3[5] + m[5]
1333
+ ];
1297
1334
  }
1298
- function dropShadow(color, blur = 24, x = 0, y = 12) {
1299
- return { shadowColor: color, shadowBlur: blur, shadowX: x, shadowY: y };
1335
+ var DEG = Math.PI / 180;
1336
+ var z0 = (x) => x === 0 ? 0 : x;
1337
+ function projectDepth(m, z, vx, vy, d) {
1338
+ if (z === 0) return m;
1339
+ const p = d + z > 0 ? d / (d + z) : 1e-6;
1340
+ return [
1341
+ z0(m[0] * p),
1342
+ z0(m[1] * p),
1343
+ z0(m[2] * p),
1344
+ z0(m[3] * p),
1345
+ z0(vx + (m[4] - vx) * p),
1346
+ z0(vy + (m[5] - vy) * p)
1347
+ ];
1300
1348
  }
1301
-
1302
- // ../core/src/layout.ts
1303
- function row(count, opts = {}) {
1304
- if (count <= 0) return [];
1305
- const center = opts.center ?? 0;
1306
- if (count === 1) return [center];
1307
- if (opts.span !== void 0) {
1308
- const start2 = center - opts.span / 2;
1309
- const pitch2 = opts.span / (count - 1);
1310
- return Array.from({ length: count }, (_, i) => start2 + i * pitch2);
1349
+ function tiltSkew(m, rotXdeg, rotYdeg, hw, hh, d) {
1350
+ const ky = Math.sin(rotYdeg * DEG) * hw / d;
1351
+ const kx = Math.sin(rotXdeg * DEG) * hh / d;
1352
+ if (ky === 0 && kx === 0) return m;
1353
+ return multiply(m, [1, kx, ky, 1, 0, 0]);
1354
+ }
1355
+ function localMatrix(x, y, rotationDeg, scale, scaleX = 1, scaleY = 1, skewXDeg = 0, skewYDeg = 0) {
1356
+ const r = rotationDeg * Math.PI / 180;
1357
+ if (scaleX === 1 && scaleY === 1 && skewXDeg === 0 && skewYDeg === 0) {
1358
+ const cos = Math.cos(r) * scale;
1359
+ const sin = Math.sin(r) * scale;
1360
+ return [cos, sin, -sin, cos, x, y];
1311
1361
  }
1312
- const iw = opts.itemWidth ?? 0;
1313
- const gap = opts.gap ?? 0;
1314
- const pitch = iw + gap;
1315
- const total = count * iw + (count - 1) * gap;
1316
- const start = center - total / 2 + iw / 2;
1317
- return Array.from({ length: count }, (_, i) => start + i * pitch);
1362
+ const c = Math.cos(r);
1363
+ const s = Math.sin(r);
1364
+ const tx = Math.tan(skewXDeg * Math.PI / 180);
1365
+ const ty = Math.tan(skewYDeg * Math.PI / 180);
1366
+ const R = [c, s, -s, c, 0, 0];
1367
+ const K3 = [1, ty, tx, 1, 0, 0];
1368
+ const S = [scale * scaleX, 0, 0, scale * scaleY, 0, 0];
1369
+ const m = multiply(R, multiply(K3, S));
1370
+ return [m[0], m[1], m[2], m[3], x, y];
1318
1371
  }
1319
- var column = row;
1320
- function grid(rows, cols, opts = {}) {
1321
- const axis = (center, gap, item, span) => ({
1322
- center,
1323
- ...gap !== void 0 ? { gap } : {},
1324
- ...item !== void 0 ? { itemWidth: item } : {},
1325
- ...span !== void 0 ? { span } : {}
1326
- });
1327
- const xs = row(cols, axis(opts.center?.x ?? 0, opts.gapX, opts.cellW, opts.spanX));
1328
- const ys = row(rows, axis(opts.center?.y ?? 0, opts.gapY, opts.cellH, opts.spanY));
1329
- const out = [];
1330
- for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) out.push({ x: xs[c], y: ys[r] });
1331
- return out;
1372
+ var ANCHOR_FACTORS = {
1373
+ "top-left": [0, 0],
1374
+ "top-center": [0.5, 0],
1375
+ "top-right": [1, 0],
1376
+ "center-left": [0, 0.5],
1377
+ center: [0.5, 0.5],
1378
+ "center-right": [1, 0.5],
1379
+ "bottom-left": [0, 1],
1380
+ "bottom-center": [0.5, 1],
1381
+ "bottom-right": [1, 1]
1382
+ };
1383
+ var TEXT_ALIGN = { 0: "left", 0.5: "center", 1: "right" };
1384
+ var TEXT_BASELINE = { 0: "top", 0.5: "middle", 1: "bottom" };
1385
+ function formatNumber(value, decimals, thousands) {
1386
+ const fixed = value.toFixed(decimals);
1387
+ if (!thousands) return fixed;
1388
+ const neg = fixed.startsWith("-");
1389
+ const body = neg ? fixed.slice(1) : fixed;
1390
+ const dot = body.indexOf(".");
1391
+ const intPart = dot === -1 ? body : body.slice(0, dot);
1392
+ const frac = dot === -1 ? "" : body.slice(dot);
1393
+ const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1394
+ return (neg ? "-" : "") + grouped + frac;
1332
1395
  }
1333
-
1334
- // ../core/src/montage.ts
1335
- var VIDEO_EXT = /\.(mp4|mov|webm|m4v|mkv)$/i;
1336
- var isVideoSrc = (src) => VIDEO_EXT.test(src);
1337
- function makeRng(seed) {
1338
- let a = seed >>> 0 || 2654435769;
1339
- return () => {
1340
- a = a + 1831565813 | 0;
1341
- let t = Math.imul(a ^ a >>> 15, 1 | a);
1342
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
1343
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
1344
- };
1396
+ function behaviorEnvelope(b, t) {
1397
+ const from = b.from ?? Number.NEGATIVE_INFINITY;
1398
+ const until = b.until ?? Number.POSITIVE_INFINITY;
1399
+ if (t < from || t > until) return 0;
1400
+ const ramp = b.ramp ?? 0.2;
1401
+ let envelope = 1;
1402
+ if (Number.isFinite(from) && ramp > 0) envelope = Math.min(envelope, (t - from) / ramp);
1403
+ if (Number.isFinite(until) && ramp > 0) envelope = Math.min(envelope, (until - t) / ramp);
1404
+ return Math.max(0, Math.min(1, envelope));
1345
1405
  }
1346
- var norm = (img) => typeof img === "string" ? { src: img } : img;
1347
- function photoMontage(images, opts = {}) {
1348
- const id = opts.id ?? "shot";
1349
- const W = opts.size?.width ?? 1920;
1350
- const H = opts.size?.height ?? 1080;
1351
- const hold = Math.max(0.5, opts.hold ?? 3.2);
1352
- const zoom = Math.max(1.001, opts.zoom ?? 1.18);
1353
- const grade = opts.grade !== false;
1354
- const rand2 = makeRng((opts.seed ?? 0) + 1);
1355
- const slides = images.map(norm);
1356
- const cx = W / 2;
1357
- const cy = H / 2;
1358
- const nodes = [];
1359
- const shots = [];
1360
- let clock = 0;
1361
- slides.forEach((slide, i) => {
1362
- const nid = `${id}-${i}`;
1363
- const slideHold = Math.max(0.5, slide.hold ?? hold);
1364
- const transition = Math.min(opts.transition ?? 0.6, slideHold * 0.9);
1365
- const shotStart = clock;
1366
- clock += slideHold;
1367
- const kind = slide.ken ?? ["in", "out", "pan"][Math.floor(rand2() * 3)] ?? "in";
1368
- const angle = rand2() * Math.PI * 2;
1369
- const panFrac = 0.4 + rand2() * 0.35;
1370
- const dx = Math.cos(angle);
1371
- const dy = Math.sin(angle);
1372
- let kA, kB;
1373
- let xA, xB, yA, yB;
1374
- if (kind === "pan") {
1375
- kA = kB = zoom;
1376
- const sx = dx * (zoom - 1) * (W / 2) * panFrac;
1377
- const sy = dy * (zoom - 1) * (H / 2) * panFrac;
1378
- xA = cx - sx;
1379
- xB = cx + sx;
1380
- yA = cy - sy;
1381
- yB = cy + sy;
1382
- } else {
1383
- kA = kind === "in" ? 1 : zoom;
1384
- kB = kind === "in" ? zoom : 1;
1385
- xA = cx + dx * (kA - 1) * (W / 2) * panFrac;
1386
- xB = cx + dx * (kB - 1) * (W / 2) * panFrac;
1387
- yA = cy + dy * (kA - 1) * (H / 2) * panFrac;
1388
- yB = cy + dy * (kB - 1) * (H / 2) * panFrac;
1406
+ function sampleProp(compiled, t, target, prop, fallback) {
1407
+ let value = compiled.initialValues.get(`${target}.${prop}`) ?? fallback;
1408
+ let segStart = Number.NEGATIVE_INFINITY;
1409
+ const segs = compiled.segments.get(`${target}.${prop}`);
1410
+ if (segs) {
1411
+ let active;
1412
+ for (const seg of segs) {
1413
+ if (seg.t0 <= t) active = seg;
1414
+ else break;
1389
1415
  }
1390
- const box = { id: nid, src: slide.src, x: xA, y: yA, width: W, height: H, anchor: "center", fit: "cover", scale: kA, opacity: i === 0 ? 1 : 0 };
1391
- nodes.push(
1392
- isVideoSrc(slide.src) ? video({ ...box, start: shotStart, volume: slide.volume ?? 0 }) : image(box)
1393
- );
1394
- const ken = tween(
1395
- nid,
1396
- { scale: kB, x: xB, y: yB },
1397
- { duration: slideHold, ease: "easeInOutQuad", label: `shot-${i}` }
1398
- );
1399
- const shot = i === 0 ? par(ken) : par(
1400
- ken,
1401
- tween(`${id}-${i - 1}`, { opacity: 0 }, { duration: transition, ease: "linear", label: `cross-${i}` }),
1402
- tween(nid, { opacity: 1 }, { duration: transition, ease: "linear" })
1403
- );
1404
- shots.push(shot);
1405
- });
1406
- if (grade) {
1407
- nodes.push(
1408
- rect({
1409
- id: `${id}-vignette`,
1410
- x: 0,
1411
- y: 0,
1412
- width: W,
1413
- height: H,
1414
- fill: radialGradient(
1415
- [
1416
- { offset: 0.55, color: "#FFFFFF" },
1417
- { offset: 1, color: "#6E6E6E" }
1418
- ],
1419
- { cx: 0.5, cy: 0.5, r: 0.72 }
1420
- ),
1421
- blend: "multiply"
1422
- })
1423
- );
1424
- nodes.push(
1425
- rect({
1426
- id: `${id}-scrim`,
1427
- x: 0,
1428
- y: 0,
1429
- width: W,
1430
- height: H,
1431
- fill: linearGradient(
1432
- [
1433
- { offset: 0, color: "#00000000" },
1434
- { offset: 0.62, color: "#00000000" },
1435
- { offset: 1, color: "#000000B0" }
1436
- ],
1437
- { angle: 90 }
1438
- )
1439
- })
1440
- );
1441
- }
1442
- return { nodes, timeline: beat("montage", { nodes: nodes.map((n3) => n3.id) }, [seq(...shots)]) };
1443
- }
1444
- var videoMontage = photoMontage;
1445
-
1446
- // ../core/src/presets.ts
1447
- var PRESET_NAMES = [
1448
- "draw-bloom",
1449
- "punch-in",
1450
- "rise-settle",
1451
- "slide-bank",
1452
- "reveal-orbit",
1453
- "spin-forge"
1454
- ];
1455
- function makeRng2(seed) {
1456
- let a = seed >>> 0 || 2654435769;
1457
- return () => {
1458
- a = a + 1831565813 | 0;
1459
- let t = Math.imul(a ^ a >>> 15, 1 | a);
1460
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
1461
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
1462
- };
1463
- }
1464
- var clamp01 = (x) => Math.max(0, Math.min(1, x));
1465
- var SET = 1 / 120;
1466
- function ctx(o) {
1467
- const rand2 = makeRng2((o.seed ?? 0) + 1);
1468
- return {
1469
- e: clamp01(o.energy ?? 0.5),
1470
- sp: Math.max(0.25, o.speed ?? 1),
1471
- it: clamp01(o.intensity ?? 0.5),
1472
- from: o.from,
1473
- rand: rand2,
1474
- jit: (amp) => (rand2() - 0.5) * 2 * amp,
1475
- g: o.target.group,
1476
- cx: o.target.center[0],
1477
- cy: o.target.center[1],
1478
- s: o.target.baseScale,
1479
- fills: o.target.fills,
1480
- inks: o.target.inks
1481
- };
1482
- }
1483
- var dur = (base, sp) => base / sp;
1484
- function settleEase(e) {
1485
- return e < 0.34 ? "easeOutCubic" : e < 0.67 ? "easeOutBack" : "easeOutElastic";
1486
- }
1487
- function fromVec(from, dist) {
1488
- switch (from) {
1489
- case "left":
1490
- return [-dist, 0];
1491
- case "right":
1492
- return [dist, 0];
1493
- case "top":
1494
- return [0, -dist];
1495
- default:
1496
- return [0, dist];
1497
- }
1498
- }
1499
- function fadeFills(c, base = 0.4, gap = 0.06) {
1500
- return stagger(
1501
- gap / c.sp,
1502
- ...c.fills.map(
1503
- (id, i) => tween(id, { opacity: 1 }, { duration: dur(base, c.sp), ease: "easeOutQuad", ...i === 0 && { label: "reveal" } })
1504
- )
1505
- );
1506
- }
1507
- function drawInks(c) {
1508
- return stagger(
1509
- 0.15 / c.sp,
1510
- ...c.inks.map(
1511
- (id, i) => tween(id, { progress: 1 }, { duration: dur(1.3 + c.jit(0.2), c.sp), ease: "easeInOutQuad", ...i === 0 && { label: "draw" } })
1512
- )
1513
- );
1514
- }
1515
- function motionPreset(name, opts) {
1516
- const c = ctx(opts);
1517
- switch (name) {
1518
- case "draw-bloom":
1519
- return beat("draw-bloom", {}, [
1520
- drawInks(c),
1521
- fadeFills(c, 0.45),
1522
- tween(c.g, { scale: c.s * (1.02 + 0.05 * c.e) }, { duration: dur(2.4, c.sp), ease: "easeInOutQuad", label: "settle" })
1523
- ]);
1524
- case "punch-in": {
1525
- const peak = c.s * (1 + 0.06 + 0.24 * c.e + c.jit(0.02));
1526
- return beat("punch-in", {}, [
1527
- par(
1528
- fadeFills(c, 0.25),
1529
- seq(
1530
- tween(c.g, { scale: peak }, { duration: dur(0.45 + c.jit(0.05), c.sp), ease: "easeOutCubic", label: "punch" }),
1531
- tween(c.g, { scale: c.s }, { duration: dur(0.5, c.sp), ease: settleEase(c.e) })
1532
- )
1533
- )
1534
- ]);
1416
+ if (active) {
1417
+ segStart = active.t0;
1418
+ if (t >= active.t1) {
1419
+ value = active.to;
1420
+ } else {
1421
+ const u = resolveEase(active.ease)((t - active.t0) / (active.t1 - active.t0));
1422
+ value = lerpValue(active.from, active.to, u);
1423
+ }
1535
1424
  }
1536
- case "rise-settle": {
1537
- const es = 0.65 + c.rand() * 0.7;
1538
- const dist = (220 + 260 * c.it) * es;
1539
- const [dx, dy] = fromVec(c.from ?? "bottom", dist);
1540
- const jx = c.jit(110);
1541
- return beat("rise-settle", {}, [
1542
- par(
1543
- motionPath(
1544
- c.g,
1545
- [
1546
- [c.cx + dx + jx, c.cy + dy],
1547
- [c.cx + dx * 0.4 - jx * 0.6, c.cy + dy * 0.4],
1548
- [c.cx, c.cy]
1549
- ],
1550
- { duration: dur(1.1, c.sp), ease: settleEase(c.e), label: "rise" }
1551
- ),
1552
- fadeFills(c, 0.4)
1553
- )
1554
- ]);
1425
+ }
1426
+ if (prop === "x" || prop === "y" || prop === "rotation") {
1427
+ const drivers = compiled.motionPaths.get(target);
1428
+ if (drivers) {
1429
+ let active;
1430
+ for (const d of drivers) {
1431
+ if (d.t0 <= t) active = d;
1432
+ else break;
1433
+ }
1434
+ if (active && active.t0 >= segStart && (prop !== "rotation" || active.autoRotate) && active.points.length > 0) {
1435
+ const span = active.t1 - active.t0;
1436
+ const u = span <= 0 ? 1 : resolveEase(active.ease)(Math.max(0, Math.min(1, (t - active.t0) / span)));
1437
+ if (prop === "x") value = pathPoint(active.points, active.closed, u, active.curviness)[0];
1438
+ else if (prop === "y") value = pathPoint(active.points, active.closed, u, active.curviness)[1];
1439
+ else value = pathTangentAngle(active.points, active.closed, u, active.curviness) + active.rotateOffset;
1440
+ }
1555
1441
  }
1556
- case "slide-bank": {
1557
- const es = 0.65 + c.rand() * 0.7;
1558
- const dist = (420 + 240 * c.it) * es;
1559
- const [dx, dy] = fromVec(c.from ?? "left", dist);
1560
- const arc = c.jit(140);
1561
- const midx = c.jit(120);
1562
- const move = dur(1.2, c.sp);
1563
- return beat("slide-bank", {}, [
1564
- par(
1565
- motionPath(
1566
- c.g,
1567
- [
1568
- [c.cx + dx, c.cy + dy],
1569
- [c.cx + dx * 0.4 + midx, c.cy + dy * 0.4 - 70 - arc],
1570
- [c.cx, c.cy]
1571
- ],
1572
- { duration: move, ease: settleEase(c.e), autoRotate: true, label: "slide" }
1573
- ),
1574
- // level the bank out once it lands (authored after the path → wins for rotation)
1575
- seq(wait(move), tween(c.g, { rotation: 0 }, { duration: dur(0.5, c.sp), ease: "easeOutCubic" })),
1576
- fadeFills(c, 0.4)
1577
- )
1578
- ]);
1442
+ }
1443
+ for (const b of compiled.ir.behaviors ?? []) {
1444
+ if (b.target === target && b.prop === prop && typeof value === "number") {
1445
+ const envelope = behaviorEnvelope(b, t);
1446
+ if (envelope > 0) value = value + envelope * sampleBehavior(b.behavior, t);
1579
1447
  }
1580
- case "reveal-orbit": {
1581
- const es = 0.65 + c.rand() * 0.7;
1582
- const orbit = (180 + 160 * c.it) * es;
1583
- const jx = c.jit(0.4);
1584
- const jy = c.jit(0.4);
1585
- return beat("reveal-orbit", {}, [
1586
- drawInks(c),
1587
- fadeFills(c, 0.45),
1588
- par(
1589
- motionPath(
1590
- c.g,
1591
- [
1592
- [c.cx, c.cy],
1593
- [c.cx - orbit * (1 + jx), c.cy - orbit * 0.8],
1594
- [c.cx + orbit * (1 + jy), c.cy - orbit],
1595
- [c.cx, c.cy]
1596
- ],
1597
- { duration: dur(1.7, c.sp), ease: "easeInOutCubic", label: "orbit" }
1598
- ),
1599
- seq(
1600
- tween(c.g, { scale: c.s * (1.12 + 0.1 * c.e) }, { duration: dur(0.85, c.sp), ease: "easeOutBack" }),
1601
- tween(c.g, { scale: c.s }, { duration: dur(0.85, c.sp), ease: "easeInOutQuad" })
1602
- )
1603
- )
1604
- ]);
1448
+ }
1449
+ return value;
1450
+ }
1451
+ function nodeParentMatrix(compiled, id, t) {
1452
+ const num2 = (target, prop, fallback) => {
1453
+ const v = sampleProp(compiled, t, target, prop, fallback);
1454
+ return typeof v === "number" ? v : fallback;
1455
+ };
1456
+ let result = null;
1457
+ const walk = (node, parent) => {
1458
+ if (node.id === id) {
1459
+ result = parent;
1460
+ return true;
1605
1461
  }
1606
- case "spin-forge": {
1607
- const turns = 1 + Math.round(c.it);
1608
- const dir = c.rand() < 0.5 ? -1 : 1;
1609
- const startRot = dir * 360 * turns;
1610
- const peak = c.s * (1 + 0.05 + 0.2 * c.e);
1611
- return beat("spin-forge", {}, [
1612
- par(
1613
- seq(
1614
- tween(c.g, { scale: c.s * 0.2, rotation: startRot }, { duration: SET }),
1615
- // establish (invisible)
1616
- tween(c.g, { scale: peak, rotation: 0 }, { duration: dur(0.9, c.sp), ease: "easeOutBack", label: "spin" }),
1617
- tween(c.g, { scale: c.s }, { duration: dur(0.3, c.sp), ease: "easeInOutQuad" })
1618
- ),
1619
- seq(wait(SET), fadeFills(c, 0.3))
1462
+ if (node.type === "group") {
1463
+ const m = multiply(
1464
+ parent,
1465
+ localMatrix(
1466
+ num2(node.id, "x", node.props.x),
1467
+ num2(node.id, "y", node.props.y),
1468
+ num2(node.id, "rotation", node.props.rotation ?? 0),
1469
+ num2(node.id, "scale", node.props.scale ?? 1),
1470
+ num2(node.id, "scaleX", node.props.scaleX ?? 1),
1471
+ num2(node.id, "scaleY", node.props.scaleY ?? 1),
1472
+ num2(node.id, "skewX", node.props.skewX ?? 0),
1473
+ num2(node.id, "skewY", node.props.skewY ?? 0)
1620
1474
  )
1621
- ]);
1475
+ );
1476
+ for (const child of node.children) if (walk(child, m)) return true;
1622
1477
  }
1623
- }
1478
+ return false;
1479
+ };
1480
+ for (const node of compiled.ir.nodes) if (walk(node, IDENTITY)) break;
1481
+ return result;
1624
1482
  }
1625
-
1626
- // ../core/src/devicePreset.ts
1627
- var DEVICE_PRESET_NAMES = ["phone", "tablet", "laptop", "browser", "watch", "monitor", "tv", "foldable", "terminal", "car"];
1628
- var DARK = { body: "#15161C", bodyStroke: "#2A2D38", screen: "#0E0F15", detail: "#3A3D48", chrome: "#1B1D24", chromeText: "#9AA0AD" };
1629
- var LIGHT = { body: "#E7E9EE", bodyStroke: "#C3C7D1", screen: "#FFFFFF", detail: "#AEB3C0", chrome: "#F2F3F6", chromeText: "#5B606C" };
1630
- var SCREENS = {
1631
- phone: { width: 352, height: 736, radius: 38 },
1632
- tablet: { width: 544, height: 764, radius: 18 },
1633
- laptop: { width: 840, height: 520, radius: 8, cy: -150 },
1634
- browser: { width: 984, height: 568, radius: 6, cy: 24 },
1635
- watch: { width: 184, height: 224, radius: 44 },
1636
- monitor: { width: 1056, height: 600, radius: 6 },
1637
- tv: { width: 1280, height: 720, radius: 8, cy: -24 },
1638
- foldable: { width: 760, height: 560, radius: 20 },
1639
- terminal: { width: 900, height: 560, radius: 6, cy: 18 },
1640
- car: { width: 1e3, height: 520, radius: 24 }
1641
- };
1642
- var BOUNDS = {
1643
- phone: { width: 392, height: 812 },
1644
- tablet: { width: 600, height: 820 },
1645
- laptop: { width: 1100, height: 650 },
1646
- browser: { width: 1e3, height: 660 },
1647
- watch: { width: 220, height: 300 },
1648
- monitor: { width: 1120, height: 860 },
1649
- tv: { width: 1340, height: 920 },
1650
- foldable: { width: 800, height: 600 },
1651
- terminal: { width: 916, height: 636 },
1652
- car: { width: 1060, height: 600 }
1653
- };
1654
- var isLandscape = (name, o) => (name === "phone" || name === "tablet") && o.orientation === "landscape";
1655
- function screenDims(name, o) {
1656
- const d = SCREENS[name];
1657
- const base = { cx: d.cx ?? 0, cy: d.cy ?? 0 };
1658
- return isLandscape(name, o) ? { width: d.height, height: d.width, radius: d.radius, ...base } : { width: d.width, height: d.height, radius: d.radius, ...base };
1659
- }
1660
- function deviceScreen(name, opts = {}) {
1661
- const d = screenDims(name, opts);
1662
- return { x: 0, y: 0, width: d.width, height: d.height, radius: d.radius };
1663
- }
1664
- function deviceScreenCenter(name, opts = {}) {
1665
- const d = screenDims(name, opts);
1666
- return { x: d.cx, y: d.cy };
1667
- }
1668
- function deviceBounds(name, opts = {}) {
1669
- const b = BOUNDS[name];
1670
- return isLandscape(name, opts) ? { width: b.height, height: b.width } : { ...b };
1671
- }
1672
- function deviceScreenPoint(name, opts, local) {
1673
- const c = deviceScreenCenter(name, opts);
1674
- const s = opts.scale ?? 1;
1675
- return [(opts.x ?? 0) + s * (c.x + local[0]), (opts.y ?? 0) + s * (c.y + local[1])];
1676
- }
1677
- function screenGroup(id, p, o, cx, cy, dims, content) {
1678
- return group({ id: `${id}-screen`, x: cx, y: cy, clip: { kind: "rect", x: -dims.width / 2, y: -dims.height / 2, width: dims.width, height: dims.height, radius: dims.radius } }, [
1679
- rect({ id: `${id}-screenbg`, x: 0, y: 0, anchor: "center", width: dims.width, height: dims.height, fill: o.screen ?? p.screen }),
1680
- group({ id: `${id}-content`, x: 0, y: 0 }, content)
1681
- ]);
1682
- }
1683
- function buildDevice(name, id, p, o, content) {
1684
- const dims = screenDims(name, o);
1685
- const sw = dims.width;
1686
- const sh = dims.height;
1687
- const screen = () => screenGroup(id, p, o, dims.cx, dims.cy, dims, content);
1688
- switch (name) {
1689
- case "phone":
1690
- case "tablet": {
1691
- const bezel = name === "phone" ? 20 : 28;
1692
- const bodyW = sw + bezel * 2;
1693
- const bodyH = sh + bezel * 2;
1694
- const bodyR = name === "phone" ? 54 : 34;
1695
- const land = isLandscape(name, o);
1696
- const nodes = [
1697
- rect({ id: `${id}-body`, x: 0, y: 0, anchor: "center", width: bodyW, height: bodyH, fill: p.body, stroke: p.bodyStroke, strokeWidth: 2, radius: bodyR }),
1698
- screen()
1699
- ];
1700
- if (name === "phone") {
1701
- nodes.push(
1702
- land ? rect({ id: `${id}-notch`, x: -sw / 2 + 16, y: 0, anchor: "center", width: 30, height: 96, fill: "#000000", radius: 15 }) : rect({ id: `${id}-notch`, x: 0, y: -sh / 2 + 16, anchor: "center", width: 96, height: 30, fill: "#000000", radius: 15 }),
1703
- land ? rect({ id: `${id}-home`, x: sw / 2 - 4, y: 0, anchor: "center", width: 5, height: 120, fill: p.detail, radius: 3 }) : rect({ id: `${id}-home`, x: 0, y: sh / 2 - 18, anchor: "center", width: 120, height: 5, fill: p.detail, radius: 3 })
1704
- );
1705
- if (!land) {
1706
- nodes.push(
1707
- rect({ id: `${id}-pwr`, x: bodyW / 2, y: -bodyH * 0.1, anchor: "center", width: 4, height: 78, fill: p.detail, radius: 2 }),
1708
- rect({ id: `${id}-volup`, x: -bodyW / 2, y: -bodyH * 0.16, anchor: "center", width: 4, height: 48, fill: p.detail, radius: 2 }),
1709
- rect({ id: `${id}-voldn`, x: -bodyW / 2, y: -bodyH * 0.16 + 60, anchor: "center", width: 4, height: 48, fill: p.detail, radius: 2 })
1710
- );
1711
- }
1712
- } else {
1713
- nodes.push(
1714
- rect({ id: `${id}-camera`, x: land ? -sw / 2 - 14 : 0, y: land ? 0 : -sh / 2 - 14, anchor: "center", width: 8, height: 8, fill: p.detail, radius: 4 }),
1715
- rect({ id: `${id}-pwr`, x: land ? -bodyW * 0.18 : bodyW * 0.18, y: land ? -bodyH / 2 : -bodyH / 2, anchor: "center", width: 60, height: 4, fill: p.detail, radius: 2 })
1716
- );
1717
- }
1718
- return nodes;
1719
- }
1720
- case "laptop": {
1721
- const lidTop = dims.cy - (sh + 40) / 2;
1722
- const keyRows = [0, 1, 2, 3].map(
1723
- (r) => rect({ id: `${id}-keys${r}`, x: 0, y: 150 + r * 11, anchor: "center", width: 640 + r * 50, height: 6, fill: p.chrome, radius: 3 })
1724
- );
1725
- return [
1726
- path({ id: `${id}-base`, x: 0, y: 0, d: "M -450 140 L 450 140 L 520 196 L -520 196 Z", fill: p.body, stroke: p.bodyStroke, strokeWidth: 2 }),
1727
- rect({ id: `${id}-foot-l`, x: -360, y: 198, anchor: "center", width: 70, height: 5, fill: p.detail, radius: 3 }),
1728
- rect({ id: `${id}-foot-r`, x: 360, y: 198, anchor: "center", width: 70, height: 5, fill: p.detail, radius: 3 }),
1729
- ...keyRows,
1730
- rect({ id: `${id}-trackpad`, x: 0, y: 184, anchor: "center", width: 150, height: 8, fill: p.detail, radius: 4 }),
1731
- rect({ id: `${id}-hinge`, x: 0, y: 134, anchor: "center", width: 900, height: 10, fill: p.detail, radius: 5 }),
1732
- screen(),
1733
- ellipse({ id: `${id}-webcam`, x: 0, y: lidTop + 14, anchor: "center", width: 6, height: 6, fill: p.detail }),
1734
- rect({ id: `${id}-lid`, x: 0, y: dims.cy, anchor: "center", width: sw + 40, height: sh + 40, stroke: p.bodyStroke, strokeWidth: 2, radius: 18 })
1735
- ];
1736
- }
1737
- case "browser": {
1738
- const winW = sw + 16;
1739
- const winH = sh + 92;
1740
- const barY = -winH / 2 + 24;
1741
- return [
1742
- rect({ id: `${id}-win`, x: 0, y: 0, anchor: "center", width: winW, height: winH, fill: p.chrome, stroke: p.bodyStroke, strokeWidth: 1.5, radius: 14 }),
1743
- ellipse({ id: `${id}-dot1`, x: -winW / 2 + 30, y: barY, anchor: "center", width: 13, height: 13, fill: "#FF5F57" }),
1744
- ellipse({ id: `${id}-dot2`, x: -winW / 2 + 54, y: barY, anchor: "center", width: 13, height: 13, fill: "#FEBC2E" }),
1745
- ellipse({ id: `${id}-dot3`, x: -winW / 2 + 78, y: barY, anchor: "center", width: 13, height: 13, fill: "#28C840" }),
1746
- // an active tab tucked under the lights
1747
- rect({ id: `${id}-tab`, x: -winW / 2 + 230, y: barY, anchor: "center", width: 190, height: 30, fill: o.screen ?? p.screen, radius: 8 }),
1748
- text({ id: `${id}-tabtext`, x: -winW / 2 + 156, y: barY, anchor: "center-left", content: "Overview", fontFamily: "Inter", fontSize: 13, fill: p.chromeText }),
1749
- rect({ id: `${id}-urlpill`, x: 96, y: barY, anchor: "center", width: 700, height: 26, fill: o.screen ?? p.screen, stroke: p.bodyStroke, strokeWidth: 1, radius: 13 }),
1750
- rect({ id: `${id}-lock`, x: 96 - 330, y: barY, anchor: "center", width: 8, height: 10, fill: p.chromeText, radius: 2 }),
1751
- text({ id: `${id}-urltext`, x: 96 - 312, y: barY, anchor: "center-left", content: urlText(o.url), fontFamily: "Inter", fontSize: 14, fill: p.chromeText }),
1752
- screen()
1753
- ];
1754
- }
1755
- case "watch": {
1756
- const bw = sw + 36;
1757
- const bh = sh + 36;
1758
- return [
1759
- // straps (drawn behind the body) flaring out top & bottom
1760
- path({ id: `${id}-bandtop`, x: 0, y: -bh / 2 + 4, d: "M -78 0 L 78 0 L 64 -86 L -64 -86 Z", fill: p.body, stroke: p.bodyStroke, strokeWidth: 2 }),
1761
- path({ id: `${id}-bandbot`, x: 0, y: bh / 2 - 4, d: "M -78 0 L 78 0 L 64 86 L -64 86 Z", fill: p.body, stroke: p.bodyStroke, strokeWidth: 2 }),
1762
- rect({ id: `${id}-body`, x: 0, y: 0, anchor: "center", width: bw, height: bh, fill: p.body, stroke: p.bodyStroke, strokeWidth: 3, radius: 60 }),
1763
- screen(),
1764
- rect({ id: `${id}-crown`, x: bw / 2, y: -20, anchor: "center", width: 14, height: 40, fill: p.detail, radius: 6 }),
1765
- rect({ id: `${id}-button`, x: bw / 2 - 2, y: 40, anchor: "center", width: 8, height: 34, fill: p.detail, radius: 4 })
1766
- ];
1767
- }
1768
- case "monitor": {
1769
- const panelW = sw + 44;
1770
- const panelH = sh + 60;
1771
- return [
1772
- rect({ id: `${id}-panel`, x: 0, y: 0, anchor: "center", width: panelW, height: panelH, fill: p.body, stroke: p.bodyStroke, strokeWidth: 2, radius: 16 }),
1773
- screen(),
1774
- ellipse({ id: `${id}-led`, x: panelW / 2 - 26, y: panelH / 2 - 16, anchor: "center", width: 6, height: 6, fill: "#28C840" }),
1775
- rect({ id: `${id}-neck`, x: 0, y: panelH / 2 + 60, anchor: "center", width: 60, height: 120, fill: p.body }),
1776
- path({ id: `${id}-stand`, x: 0, y: panelH / 2 + 60, d: "M -160 50 L 160 50 L 220 80 L -220 80 Z", fill: p.body, stroke: p.bodyStroke, strokeWidth: 2 })
1777
- ];
1778
- }
1779
- case "tv": {
1780
- const panelW = sw + 44;
1781
- const panelH = sh + 48;
1782
- const panelBottom = dims.cy + panelH / 2;
1783
- return [
1784
- rect({ id: `${id}-panel`, x: 0, y: dims.cy, anchor: "center", width: panelW, height: panelH, fill: p.body, stroke: p.bodyStroke, strokeWidth: 2, radius: 12 }),
1785
- screen(),
1786
- ellipse({ id: `${id}-brand`, x: 0, y: panelBottom - 12, anchor: "center", width: 6, height: 6, fill: p.detail }),
1787
- rect({ id: `${id}-neck`, x: 0, y: panelBottom + 48, anchor: "center", width: 64, height: 96, fill: p.body }),
1788
- path({ id: `${id}-stand`, x: 0, y: panelBottom + 96, d: "M -210 0 L 210 0 L 270 34 L -270 34 Z", fill: p.body, stroke: p.bodyStroke, strokeWidth: 2 })
1789
- ];
1790
- }
1791
- case "foldable": {
1792
- const bodyW = sw + 40;
1793
- const bodyH = sh + 40;
1794
- return [
1795
- rect({ id: `${id}-hinge-l`, x: -bodyW / 2, y: 0, anchor: "center", width: 8, height: bodyH * 0.5, fill: p.detail, radius: 4 }),
1796
- rect({ id: `${id}-hinge-r`, x: bodyW / 2, y: 0, anchor: "center", width: 8, height: bodyH * 0.5, fill: p.detail, radius: 4 }),
1797
- rect({ id: `${id}-body`, x: 0, y: 0, anchor: "center", width: bodyW, height: bodyH, fill: p.body, stroke: p.bodyStroke, strokeWidth: 2, radius: 28 }),
1798
- screen(),
1799
- rect({ id: `${id}-crease`, x: 0, y: 0, anchor: "center", width: 4, height: sh, fill: p.bodyStroke, radius: 2, opacity: 0.5 }),
1800
- ellipse({ id: `${id}-cam1`, x: -10, y: -sh / 2 + 18, anchor: "center", width: 8, height: 8, fill: p.detail }),
1801
- ellipse({ id: `${id}-cam2`, x: 10, y: -sh / 2 + 18, anchor: "center", width: 8, height: 8, fill: p.detail })
1802
- ];
1483
+ function evaluate(compiled, t) {
1484
+ const ops = [];
1485
+ const valueAt = (target, prop, fallback) => sampleProp(compiled, t, target, prop, fallback);
1486
+ const num2 = (target, prop, fallback) => {
1487
+ const v = valueAt(target, prop, fallback);
1488
+ return typeof v === "number" ? v : fallback;
1489
+ };
1490
+ const str = (target, prop, fallback) => {
1491
+ const v = valueAt(target, prop, fallback);
1492
+ return typeof v === "string" ? v : String(v);
1493
+ };
1494
+ const opt = (target, prop, base) => {
1495
+ const v = valueAt(target, prop, base ?? "");
1496
+ return v === "" && base === void 0 ? void 0 : String(v);
1497
+ };
1498
+ const effectFx = (id, p) => {
1499
+ const fx = {};
1500
+ if (p.blur !== void 0) fx.blur = num2(id, "blur", p.blur);
1501
+ if (p.shadowColor !== void 0) {
1502
+ fx.shadowColor = str(id, "shadowColor", p.shadowColor);
1503
+ fx.shadowBlur = num2(id, "shadowBlur", p.shadowBlur ?? 0);
1504
+ fx.shadowX = num2(id, "shadowX", p.shadowX ?? 0);
1505
+ fx.shadowY = num2(id, "shadowY", p.shadowY ?? 0);
1803
1506
  }
1804
- case "terminal": {
1805
- const winW = sw + 16;
1806
- const winH = sh + 76;
1807
- return [
1808
- rect({ id: `${id}-win`, x: 0, y: 0, anchor: "center", width: winW, height: winH, fill: p.chrome, stroke: p.bodyStroke, strokeWidth: 1.5, radius: 12 }),
1809
- ellipse({ id: `${id}-dot1`, x: -winW / 2 + 28, y: -winH / 2 + 22, anchor: "center", width: 12, height: 12, fill: "#FF5F57" }),
1810
- ellipse({ id: `${id}-dot2`, x: -winW / 2 + 50, y: -winH / 2 + 22, anchor: "center", width: 12, height: 12, fill: "#FEBC2E" }),
1811
- ellipse({ id: `${id}-dot3`, x: -winW / 2 + 72, y: -winH / 2 + 22, anchor: "center", width: 12, height: 12, fill: "#28C840" }),
1812
- rect({ id: `${id}-tab`, x: -winW / 2 + 170, y: -winH / 2 + 22, anchor: "center", width: 130, height: 24, fill: o.screen ?? p.screen, radius: 6 }),
1813
- text({ id: `${id}-title`, x: -winW / 2 + 170, y: -winH / 2 + 22, anchor: "center", content: urlText(o.url ?? "zsh"), fontFamily: "Inter", fontSize: 13, fill: p.chromeText }),
1814
- screen()
1815
- ];
1507
+ if (p.blend !== void 0 && p.blend !== "normal") fx.blend = p.blend;
1508
+ return fx;
1509
+ };
1510
+ const persp = compiled.hasPerspective;
1511
+ const dPersp = persp ? num2("camera", "perspective", 0) : 0;
1512
+ const vx = persp ? compiled.ir.size.width / 2 : 0;
1513
+ const vy = persp ? compiled.ir.size.height / 2 : 0;
1514
+ const aperture = persp ? num2("camera", "aperture", 0) : 0;
1515
+ const focus = persp ? num2("camera", "focus", 0) : 0;
1516
+ const dofFx = (fx, depth, project) => {
1517
+ if (!project || aperture <= 0) return fx;
1518
+ const extra = aperture * Math.abs(depth - focus);
1519
+ if (extra <= 0) return fx;
1520
+ return { ...fx, blur: z0((fx.blur ?? 0) + extra) };
1521
+ };
1522
+ const zSort = compiled.zSort;
1523
+ const depthOf = (node, zAcc) => zAcc + num2(node.id, "z", node.props.z ?? 0);
1524
+ const depthOrder = (children, zAcc) => [...children].sort((a, b) => depthOf(b, zAcc) - depthOf(a, zAcc));
1525
+ const walk = (node, parent, parentOpacity, clips, zAcc, project) => {
1526
+ const id = node.id;
1527
+ const clipSpread = clips.length > 0 ? { clips } : void 0;
1528
+ const fx = effectFx(id, node.props);
1529
+ if (node.type === "line") {
1530
+ const opacity2 = parentOpacity * num2(id, "opacity", node.props.opacity ?? 1);
1531
+ if (opacity2 <= 0) return;
1532
+ const progress = Math.max(0, Math.min(1, num2(id, "progress", node.props.progress ?? 1)));
1533
+ const x1 = num2(id, "x1", node.props.x1);
1534
+ const y1 = num2(id, "y1", node.props.y1);
1535
+ ops.push({
1536
+ type: "line",
1537
+ id,
1538
+ // a line carries no z/rotate of its own — it just inherits the subtree's depth
1539
+ transform: project ? projectDepth(parent, zAcc, vx, vy, dPersp) : parent,
1540
+ opacity: opacity2,
1541
+ x1,
1542
+ y1,
1543
+ x2: x1 + (num2(id, "x2", node.props.x2) - x1) * progress,
1544
+ y2: y1 + (num2(id, "y2", node.props.y2) - y1) * progress,
1545
+ stroke: str(id, "stroke", node.props.stroke),
1546
+ strokeWidth: num2(id, "strokeWidth", node.props.strokeWidth ?? 1),
1547
+ // a line carries no z of its own — DOF uses the inherited subtree depth
1548
+ ...dofFx(fx, zAcc, project),
1549
+ ...clipSpread
1550
+ });
1551
+ return;
1816
1552
  }
1817
- case "car": {
1818
- const bodyW = sw + 60;
1819
- const bodyH = sh + 60;
1820
- return [
1821
- rect({ id: `${id}-body`, x: 0, y: 0, anchor: "center", width: bodyW, height: bodyH, fill: p.body, stroke: p.bodyStroke, strokeWidth: 2, radius: 40 }),
1822
- ellipse({ id: `${id}-knob`, x: -bodyW / 2 + 18, y: 0, anchor: "center", width: 22, height: 22, fill: p.body, stroke: p.detail, strokeWidth: 3 }),
1823
- screen(),
1824
- ellipse({ id: `${id}-btn1`, x: -44, y: sh / 2 + 16, anchor: "center", width: 12, height: 12, fill: p.detail }),
1825
- ellipse({ id: `${id}-btn2`, x: 0, y: sh / 2 + 16, anchor: "center", width: 12, height: 12, fill: p.detail }),
1826
- ellipse({ id: `${id}-btn3`, x: 44, y: sh / 2 + 16, anchor: "center", width: 12, height: 12, fill: p.detail })
1827
- ];
1553
+ const opacity = parentOpacity * num2(id, "opacity", node.props.opacity ?? 1);
1554
+ if (opacity <= 0) return;
1555
+ let effScaleX = num2(id, "scaleX", node.props.scaleX ?? 1);
1556
+ let effScaleY = num2(id, "scaleY", node.props.scaleY ?? 1);
1557
+ let depth = zAcc;
1558
+ let rotX = 0;
1559
+ let rotY = 0;
1560
+ if (project) {
1561
+ rotX = num2(id, "rotateX", node.props.rotateX ?? 0);
1562
+ rotY = num2(id, "rotateY", node.props.rotateY ?? 0);
1563
+ depth = zAcc + num2(id, "z", node.props.z ?? 0);
1564
+ if (rotY !== 0) effScaleX *= Math.abs(Math.cos(rotY * DEG));
1565
+ if (rotX !== 0) effScaleY *= Math.abs(Math.cos(rotX * DEG));
1828
1566
  }
1829
- }
1830
- }
1831
- var urlText = (url) => {
1832
- const u = url ?? "reframe.video";
1833
- return u.length > 70 ? `${u.slice(0, 67)}\u2026` : u;
1834
- };
1835
- function devicePreset(name, opts = {}) {
1836
- const id = opts.id ?? "device";
1837
- const p = opts.color === "light" ? LIGHT : DARK;
1838
- const children = buildDevice(name, id, p, opts, opts.content ?? []);
1839
- return group(
1840
- {
1841
- id,
1842
- x: opts.x ?? 0,
1843
- y: opts.y ?? 0,
1844
- ...opts.scale !== void 0 && opts.scale !== 1 && { scale: opts.scale },
1845
- ...opts.opacity !== void 0 && opts.opacity !== 1 && { opacity: opts.opacity }
1846
- },
1847
- children
1848
- );
1849
- }
1850
-
1851
- // ../core/src/cursor.ts
1852
- var ARROW_D = "M0 0 L0 30 L8 23 L12.6 33 L17 31 L12.4 21.4 L21 21.4 Z";
1853
- function cursor(opts = {}) {
1854
- const id = opts.id ?? "cursor";
1855
- const style = opts.style ?? "arrow";
1856
- const fill = opts.fill ?? "#FFFFFF";
1857
- const accent = opts.accent ?? "#FF5A1F";
1858
- const art = style === "arrow" ? [path({ id: `${id}-arrow`, d: ARROW_D, x: 0, y: 0, fill, stroke: "#15171E", strokeWidth: 2 })] : style === "dot" ? [ellipse({ id: `${id}-dot`, x: 0, y: 0, width: 18, height: 18, fill: accent, anchor: "center" })] : [ellipse({ id: `${id}-ring`, x: 0, y: 0, width: 22, height: 22, fill: "none", stroke: accent, strokeWidth: 3, anchor: "center" })];
1859
- return group(
1860
- { id, x: opts.x ?? 0, y: opts.y ?? 0, scale: opts.scale ?? 1, opacity: opts.opacity ?? 1 },
1861
- [
1862
- // ripple ring (behind the pointer), emanates from the hotspot on click
1863
- ellipse({ id: `${id}-ripple`, x: 0, y: 0, width: 30, height: 30, fill: "none", stroke: accent, strokeWidth: 3, opacity: 0, scale: 0, anchor: "center" }),
1864
- // the pointer art lives in its own group so a click "tap" can scale it
1865
- // independently of the cursor's resting scale
1866
- group({ id: `${id}-art`, x: 0, y: 0 }, art)
1867
- ]
1868
- );
1869
- }
1870
- var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
1871
- function cursorTo(id, from, to2, opts = {}) {
1872
- const dx = to2[0] - from[0], dy = to2[1] - from[1];
1873
- const dist = Math.hypot(dx, dy) || 1;
1874
- const arc = opts.arc ?? 0.12;
1875
- const mid = [(from[0] + to2[0]) / 2 + -dy / dist * arc * dist, (from[1] + to2[1]) / 2 + dx / dist * arc * dist];
1876
- const duration = opts.duration ?? clamp(dist / 1400, 0.4, 0.9);
1877
- return motionPath(id, [from, mid, to2], { duration, ease: opts.ease ?? "easeInOutCubic", curviness: 1, ...opts.label && { label: opts.label } });
1878
- }
1879
- function cursorPath(id, points, opts = {}) {
1880
- return motionPath(id, points, {
1881
- duration: opts.duration ?? clamp(points.length * 0.5, 0.5, 4),
1882
- ease: opts.ease ?? "easeInOutCubic",
1883
- curviness: opts.curviness ?? 1,
1884
- ...opts.label && { label: opts.label }
1885
- });
1886
- }
1887
- function clickBody(id, o) {
1888
- const sp = Math.max(0.25, o.speed ?? 1);
1889
- const d = (b) => b / sp;
1890
- const out = [
1891
- // the pointer taps
1892
- seq(tween(`${id}-art`, { scale: 0.82 }, { duration: d(0.08), ease: "easeOutQuad" }), tween(`${id}-art`, { scale: 1 }, { duration: d(0.1), ease: "easeOutBack" }))
1893
- ];
1894
- if (o.ripple !== false) {
1895
- out.push(seq(
1896
- tween(`${id}-ripple`, { scale: 0.2, opacity: 0.55 }, { duration: 1e-3 }),
1897
- par(
1898
- tween(`${id}-ripple`, { scale: 5 }, { duration: d(0.5), ease: "easeOutCubic" }),
1899
- tween(`${id}-ripple`, { opacity: 0 }, { duration: d(0.5), ease: "easeOutQuad" })
1567
+ const matrix = multiply(
1568
+ parent,
1569
+ localMatrix(
1570
+ num2(id, "x", node.props.x),
1571
+ num2(id, "y", node.props.y),
1572
+ num2(id, "rotation", node.props.rotation ?? 0),
1573
+ num2(id, "scale", node.props.scale ?? 1),
1574
+ effScaleX,
1575
+ effScaleY,
1576
+ num2(id, "skewX", node.props.skewX ?? 0),
1577
+ num2(id, "skewY", node.props.skewY ?? 0)
1900
1578
  )
1901
- ));
1579
+ );
1580
+ const projDraw = (m, hw, hh) => {
1581
+ if (!project) return m;
1582
+ const tilted = rotX !== 0 || rotY !== 0 ? tiltSkew(m, rotX, rotY, hw, hh, dPersp) : m;
1583
+ return projectDepth(tilted, depth, vx, vy, dPersp);
1584
+ };
1585
+ const leafFx = dofFx(fx, depth, project);
1586
+ switch (node.type) {
1587
+ case "group": {
1588
+ const clipTf = projDraw(matrix, 0, 0);
1589
+ const childClips = node.props.clip ? [...clips, { transform: clipTf, shape: node.props.clip }] : clips;
1590
+ const hasFx = fx.blur !== void 0 || fx.shadowColor !== void 0 || fx.blend !== void 0;
1591
+ if (hasFx) ops.push({ type: "group-fx-push", id, transform: matrix, opacity, ...fx, ...clipSpread });
1592
+ if (node.props.matte && node.children.length >= 2) {
1593
+ ops.push({ type: "matte-push", id, transform: matrix, opacity, mode: node.props.matte, ...clipSpread });
1594
+ walk(node.children[0], matrix, opacity, childClips, depth, project);
1595
+ ops.push({ type: "matte-sep", id, transform: matrix, opacity });
1596
+ for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips, depth, project);
1597
+ ops.push({ type: "matte-pop", id, transform: matrix, opacity });
1598
+ } else {
1599
+ const kids = zSort && project ? depthOrder(node.children, depth) : node.children;
1600
+ for (const child of kids) walk(child, matrix, opacity, childClips, depth, project);
1601
+ }
1602
+ if (hasFx) ops.push({ type: "group-fx-pop", id, transform: matrix, opacity });
1603
+ return;
1604
+ }
1605
+ case "rect":
1606
+ case "ellipse": {
1607
+ const width = num2(id, "width", node.props.width);
1608
+ const height = num2(id, "height", node.props.height);
1609
+ const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
1610
+ const strokeWidth = num2(id, "strokeWidth", node.props.strokeWidth ?? 1);
1611
+ const fillP = node.props.fill;
1612
+ const strokeP = node.props.stroke;
1613
+ const fill = isGradient(fillP) ? fillP : opt(id, "fill", fillP);
1614
+ const stroke = isGradient(strokeP) ? strokeP : opt(id, "stroke", strokeP);
1615
+ ops.push({
1616
+ type: node.type,
1617
+ id,
1618
+ transform: projDraw(matrix, width / 2, height / 2),
1619
+ opacity,
1620
+ width,
1621
+ height,
1622
+ offsetX: -width * ax,
1623
+ offsetY: -height * ay,
1624
+ ...fill !== void 0 && { fill },
1625
+ ...stroke !== void 0 && { stroke, strokeWidth },
1626
+ ...node.type === "rect" && { radius: num2(id, "radius", node.props.radius ?? 0) },
1627
+ ...leafFx,
1628
+ ...clipSpread
1629
+ });
1630
+ return;
1631
+ }
1632
+ case "image": {
1633
+ const width = num2(id, "width", node.props.width);
1634
+ const height = num2(id, "height", node.props.height);
1635
+ const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
1636
+ ops.push({
1637
+ type: "image",
1638
+ id,
1639
+ transform: projDraw(matrix, width / 2, height / 2),
1640
+ opacity,
1641
+ src: str(id, "src", node.props.src),
1642
+ width,
1643
+ height,
1644
+ offsetX: -width * ax,
1645
+ offsetY: -height * ay,
1646
+ ...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
1647
+ ...leafFx,
1648
+ ...clipSpread
1649
+ });
1650
+ return;
1651
+ }
1652
+ case "video": {
1653
+ const width = num2(id, "width", node.props.width);
1654
+ const height = num2(id, "height", node.props.height);
1655
+ const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
1656
+ const fps = compiled.ir.fps ?? 30;
1657
+ const start = node.props.start ?? 0;
1658
+ const rate = node.props.rate ?? 1;
1659
+ const clipStart = node.props.clipStart ?? 0;
1660
+ const srcT = clipStart + Math.max(0, t - start) * rate;
1661
+ const frame = Math.max(0, Math.round(srcT * fps));
1662
+ ops.push({
1663
+ type: "video",
1664
+ id,
1665
+ transform: projDraw(matrix, width / 2, height / 2),
1666
+ opacity,
1667
+ src: str(id, "src", node.props.src),
1668
+ width,
1669
+ height,
1670
+ offsetX: -width * ax,
1671
+ offsetY: -height * ay,
1672
+ frame,
1673
+ ...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
1674
+ ...leafFx,
1675
+ ...clipSpread
1676
+ });
1677
+ return;
1678
+ }
1679
+ case "path": {
1680
+ const ox = num2(id, "originX", node.props.originX ?? 0);
1681
+ const oy = num2(id, "originY", node.props.originY ?? 0);
1682
+ const fillP = node.props.fill;
1683
+ const strokeP = node.props.stroke;
1684
+ const fill = isGradient(fillP) ? fillP : opt(id, "fill", fillP);
1685
+ const stroke = isGradient(strokeP) ? strokeP : opt(id, "stroke", strokeP);
1686
+ const dStr = str(id, "d", node.props.d);
1687
+ const needsBox = isGradient(fill) || isGradient(stroke);
1688
+ ops.push({
1689
+ type: "path",
1690
+ id,
1691
+ // origin-shift in local space, then project (no per-op extent → cos + VP only)
1692
+ transform: projDraw(ox === 0 && oy === 0 ? matrix : multiply(matrix, [1, 0, 0, 1, -ox, -oy]), 0, 0),
1693
+ opacity,
1694
+ d: dStr,
1695
+ progress: Math.max(0, Math.min(1, num2(id, "progress", node.props.progress ?? 1))),
1696
+ ...fill !== void 0 && { fill },
1697
+ ...stroke !== void 0 && { stroke, strokeWidth: num2(id, "strokeWidth", node.props.strokeWidth ?? 1) },
1698
+ ...needsBox && { bbox: pathBBox(dStr) },
1699
+ ...leafFx,
1700
+ ...clipSpread
1701
+ });
1702
+ return;
1703
+ }
1704
+ case "text": {
1705
+ const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
1706
+ const raw = valueAt(id, "content", node.props.content);
1707
+ const decimals = Math.max(
1708
+ 0,
1709
+ Math.round(num2(id, "contentDecimals", node.props.contentDecimals ?? 0))
1710
+ );
1711
+ const body = typeof raw === "number" ? formatNumber(raw, decimals, node.props.contentThousands === true) : raw;
1712
+ ops.push({
1713
+ type: "text",
1714
+ id,
1715
+ transform: projDraw(matrix, 0, 0),
1716
+ opacity,
1717
+ // static affixes wrap the (possibly counting-up) body; absent ⇒ body unchanged
1718
+ content: (node.props.prefix ?? "") + body + (node.props.suffix ?? ""),
1719
+ fontFamily: str(id, "fontFamily", node.props.fontFamily),
1720
+ fontSize: num2(id, "fontSize", node.props.fontSize),
1721
+ fontWeight: num2(id, "fontWeight", node.props.fontWeight ?? 400),
1722
+ fill: str(id, "fill", node.props.fill ?? "#ffffff"),
1723
+ letterSpacing: num2(id, "letterSpacing", node.props.letterSpacing ?? 0),
1724
+ align: TEXT_ALIGN[ax] ?? "left",
1725
+ baseline: TEXT_BASELINE[ay] ?? "top",
1726
+ ...leafFx,
1727
+ ...clipSpread
1728
+ });
1729
+ return;
1730
+ }
1731
+ }
1732
+ };
1733
+ const cameraRoot = compiled.hasCamera ? cameraMatrix(
1734
+ {
1735
+ x: num2("camera", "x", compiled.ir.size.width / 2),
1736
+ y: num2("camera", "y", compiled.ir.size.height / 2),
1737
+ zoom: num2("camera", "zoom", 1),
1738
+ rotation: num2("camera", "rotation", 0)
1739
+ },
1740
+ compiled.ir.size
1741
+ ) : IDENTITY;
1742
+ let roots = compiled.ir.nodes;
1743
+ if (zSort) {
1744
+ const isHud = (n3) => !!(n3.props.fixed && compiled.hasCamera);
1745
+ roots = [...depthOrder(compiled.ir.nodes.filter((n3) => !isHud(n3)), 0), ...compiled.ir.nodes.filter(isHud)];
1902
1746
  }
1903
- if (o.press) {
1904
- out.push(seq(tween(o.press, { scale: 0.94 }, { duration: d(0.08), ease: "easeOutQuad" }), tween(o.press, { scale: 1 }, { duration: d(0.14), ease: "easeOutBack" })));
1747
+ for (const node of roots) {
1748
+ const root = compiled.hasCamera && node.props.fixed ? IDENTITY : cameraRoot;
1749
+ const project = persp && !(node.props.fixed && compiled.hasCamera);
1750
+ walk(node, root, 1, [], 0, project);
1905
1751
  }
1906
- return out;
1907
- }
1908
- function cursorClick(id, opts = {}) {
1909
- return beat(opts.label ?? "cursor-click", {}, [par(...clickBody(id, opts))]);
1752
+ return ops;
1910
1753
  }
1911
- function cursorDouble(id, opts = {}) {
1912
- const sp = Math.max(0.25, opts.speed ?? 1);
1913
- return beat(opts.label ?? "cursor-double", {}, [
1914
- seq(par(...clickBody(id, { ...opts, ripple: false })), wait(0.12 / sp), par(...clickBody(id, opts)))
1915
- ]);
1754
+
1755
+ // ../core/src/autoFoley.ts
1756
+ var V_MIN = 360;
1757
+ var V_STOP = 60;
1758
+ var V_DECEL = 520;
1759
+ var V_MAX = 2600;
1760
+ var MIN_DUR = 0.1;
1761
+ var num = (c, t, id, prop, fb) => {
1762
+ const v = sampleProp(c, t, id, prop, fb);
1763
+ return typeof v === "number" ? v : fb;
1764
+ };
1765
+ function nodeSize(node) {
1766
+ const p = node.props;
1767
+ return Math.max(p.width ?? 0, p.height ?? 0, (p.radius ?? 0) * 2, p.fontSize ?? 0, 0);
1768
+ }
1769
+ var FAMILY = { whoosh: "move", swish: "move", thud: "hit", knock: "hit", pop: "pop" };
1770
+ var DEDUP_DT = 0.09;
1771
+ function autoFoley(compiled, opts = {}) {
1772
+ const master = opts.gain ?? 0.5;
1773
+ const sens = opts.sensitivity ?? 1;
1774
+ const wantWhoosh = opts.whoosh !== false;
1775
+ const wantImpact = opts.impact !== false;
1776
+ const wantPop = opts.pop !== false;
1777
+ const wantPan = opts.pan !== false;
1778
+ const vMin = V_MIN / sens, vStop = V_STOP, vDecel = V_DECEL / sens;
1779
+ const fps = compiled.ir.fps ?? 30;
1780
+ const N = Math.max(1, Math.ceil(compiled.duration * fps));
1781
+ const W = compiled.ir.size.width || 1920;
1782
+ const ids = opts.nodes ?? [...compiled.nodeById.keys()];
1783
+ const cands = [];
1784
+ const panOf = (x) => wantPan ? Math.max(-1, Math.min(1, x / W * 2 - 1)) : 0;
1785
+ const loud = (v) => Math.max(0.2, Math.min(1, (v - vMin) / (V_MAX - vMin)));
1786
+ for (const id of ids) {
1787
+ const node = compiled.nodeById.get(id);
1788
+ if (!node || node.type === "line") continue;
1789
+ const size = nodeSize(node);
1790
+ const xs = [], ys = [], ss = [], os = [];
1791
+ for (let i2 = 0; i2 <= N; i2++) {
1792
+ const t = i2 / fps;
1793
+ xs.push(num(compiled, t, id, "x", node.props.x ?? 0));
1794
+ ys.push(num(compiled, t, id, "y", node.props.y ?? 0));
1795
+ ss.push(num(compiled, t, id, "scale", node.props.scale ?? 1));
1796
+ os.push(num(compiled, t, id, "opacity", node.props.opacity ?? 1));
1797
+ }
1798
+ const speed = (i2) => i2 <= 0 ? 0 : Math.hypot(xs[i2] - xs[i2 - 1], ys[i2] - ys[i2 - 1]) * fps;
1799
+ let i = 1;
1800
+ while (i <= N) {
1801
+ if (speed(i) <= vMin) {
1802
+ i++;
1803
+ continue;
1804
+ }
1805
+ const a = i;
1806
+ let peak = i, peakV = speed(i);
1807
+ while (i <= N && speed(i) > vStop) {
1808
+ const s = speed(i);
1809
+ if (s > peakV) {
1810
+ peakV = s;
1811
+ peak = i;
1812
+ }
1813
+ i++;
1814
+ }
1815
+ const b = i - 1;
1816
+ const durS = (b - a + 1) / fps;
1817
+ const visible = os[peak] > 0.1;
1818
+ if (peakV > vMin && durS >= MIN_DUR && visible) {
1819
+ if (wantWhoosh) {
1820
+ const quickFlick = durS < 0.25;
1821
+ cands.push({ t: peak / fps, sfx: quickFlick ? "swish" : "whoosh", gain: master * loud(peakV), pan: panOf(xs[peak]), rank: peakV });
1822
+ }
1823
+ const stopped = b >= N || speed(b + 1) < vStop && speed(Math.min(N, b + 2)) < vStop;
1824
+ const landsOnScreen = xs[b] >= 0 && xs[b] <= W && os[b] > 0.1;
1825
+ if (wantImpact && peakV > vDecel && stopped && landsOnScreen && b < N) {
1826
+ cands.push({ t: (b + 1) / fps, sfx: size > 220 ? "thud" : "knock", gain: master * loud(peakV), pan: panOf(xs[b]), rank: peakV * 1.1 });
1827
+ }
1828
+ }
1829
+ }
1830
+ if (wantPop && ss[0] < 0.25) {
1831
+ for (let k = 1; k <= N; k++) {
1832
+ if (ss[k - 1] < 0.5 && ss[k] >= 0.5 && os[k] > 0.05) {
1833
+ cands.push({ t: k / fps, sfx: "pop", gain: master * 0.7, pan: panOf(xs[k]), rank: 600 });
1834
+ break;
1835
+ }
1836
+ }
1837
+ }
1838
+ }
1839
+ cands.sort((p, q) => q.rank - p.rank);
1840
+ const kept = [];
1841
+ for (const c of cands) {
1842
+ const fam = FAMILY[c.sfx] ?? c.sfx;
1843
+ if (kept.some((k) => (FAMILY[k.sfx] ?? k.sfx) === fam && Math.abs(k.t - c.t) < DEDUP_DT)) continue;
1844
+ kept.push(c);
1845
+ }
1846
+ const max = opts.maxCues ?? 32;
1847
+ return kept.slice(0, max).map((c) => ({
1848
+ at: Number(c.t.toFixed(3)),
1849
+ sfx: c.sfx,
1850
+ gain: Number(c.gain.toFixed(3)),
1851
+ ...c.pan !== 0 ? { pan: Number(c.pan.toFixed(3)) } : {}
1852
+ }));
1916
1853
  }
1917
1854
 
1918
- // ../core/src/rig.ts
1919
- var DEFAULT_LINE = "#FFE3D2";
1920
- var DEFAULT_FILL = "#0E1424";
1921
- var LINE_W = 5;
1922
- var GLOW_W = 16;
1923
- var K = 0.5523;
1924
- var n = (v) => Number(v.toFixed(2));
1925
- function capsulePath(hw, len) {
1926
- const yT = hw;
1927
- const yB = Math.max(hw, len - hw);
1928
- const k = hw * K;
1929
- 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`;
1855
+ // ../core/src/effects.ts
1856
+ function glow(color, blur = 24) {
1857
+ return { shadowColor: color, shadowBlur: blur, shadowX: 0, shadowY: 0 };
1930
1858
  }
1931
- function ovalPath(a, b, cx = 0, cy = 0) {
1932
- const ka = n(a * K), kb = n(b * K);
1933
- const t = n(cy - b), bo = n(cy + b), c = n(cy), A = n(a), L = n(-a), X = n(cx);
1934
- 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`;
1859
+ function dropShadow(color, blur = 24, x = 0, y = 12) {
1860
+ return { shadowColor: color, shadowBlur: blur, shadowX: x, shadowY: y };
1935
1861
  }
1936
- function boneShape(jointId, bone, o) {
1937
- if (bone.shape) return bone.shape;
1938
- const len = bone.length ?? 0;
1939
- if (len <= 0) return [];
1940
- const d = capsulePath((bone.width ?? 20) / 2, len);
1941
- const nodes = [];
1942
- 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 }));
1943
- nodes.push(path({ id: `${jointId}-shape`, d, x: 0, y: 0, fill: o.fill, stroke: o.color, strokeWidth: LINE_W }));
1944
- return nodes;
1945
- }
1946
- function buildBone(bone, id, o) {
1947
- const jointId = `${id}-${bone.name}`;
1948
- return group(
1949
- { id: jointId, x: bone.at[0], y: bone.at[1], rotation: bone.rotation ?? 0 },
1950
- [...boneShape(jointId, bone, o), ...(bone.children ?? []).map((c) => buildBone(c, id, o))]
1951
- );
1952
- }
1953
- function rig(root, opts = {}) {
1954
- const id = opts.id ?? "rig";
1955
- const o = { color: opts.color ?? DEFAULT_LINE, fill: opts.fill ?? DEFAULT_FILL, glow: opts.glow };
1956
- return group(
1957
- { id, x: opts.x ?? 0, y: opts.y ?? 0, scale: opts.scale ?? 1, opacity: opts.opacity ?? 1 },
1958
- [buildBone(root, id, o)]
1959
- );
1862
+
1863
+ // ../core/src/layout.ts
1864
+ function row(count, opts = {}) {
1865
+ if (count <= 0) return [];
1866
+ const center = opts.center ?? 0;
1867
+ if (count === 1) return [center];
1868
+ if (opts.span !== void 0) {
1869
+ const start2 = center - opts.span / 2;
1870
+ const pitch2 = opts.span / (count - 1);
1871
+ return Array.from({ length: count }, (_, i) => start2 + i * pitch2);
1872
+ }
1873
+ const iw = opts.itemWidth ?? 0;
1874
+ const gap = opts.gap ?? 0;
1875
+ const pitch = iw + gap;
1876
+ const total = count * iw + (count - 1) * gap;
1877
+ const start = center - total / 2 + iw / 2;
1878
+ return Array.from({ length: count }, (_, i) => start + i * pitch);
1960
1879
  }
1961
- function rigPose(id, pose) {
1962
- const out = {};
1963
- for (const [name, deg] of Object.entries(pose)) out[`${id}-${name}`] = { rotation: deg };
1880
+ var column = row;
1881
+ function grid(rows, cols, opts = {}) {
1882
+ const axis = (center, gap, item, span) => ({
1883
+ center,
1884
+ ...gap !== void 0 ? { gap } : {},
1885
+ ...item !== void 0 ? { itemWidth: item } : {},
1886
+ ...span !== void 0 ? { span } : {}
1887
+ });
1888
+ const xs = row(cols, axis(opts.center?.x ?? 0, opts.gapX, opts.cellW, opts.spanX));
1889
+ const ys = row(rows, axis(opts.center?.y ?? 0, opts.gapY, opts.cellH, opts.spanY));
1890
+ const out = [];
1891
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) out.push({ x: xs[c], y: ys[r] });
1964
1892
  return out;
1965
1893
  }
1966
- function poseTo(id, pose, opts = {}) {
1967
- const tweens = Object.entries(pose).map(
1968
- ([name, deg]) => tween(`${id}-${name}`, { rotation: deg }, { duration: opts.duration ?? 0.5, ease: opts.ease ?? "easeInOutCubic" })
1969
- );
1970
- return opts.stagger ? stagger(opts.stagger, ...tweens) : par(...tweens);
1971
- }
1972
- function ikReach(upper, lower, dx, dy, flip = false) {
1973
- const D = Math.hypot(dx, dy);
1974
- const cos2 = Math.max(-1, Math.min(1, (D * D - upper * upper - lower * lower) / (2 * upper * lower)));
1975
- const theta2 = (flip ? -1 : 1) * Math.acos(cos2);
1976
- const vx = -lower * Math.sin(theta2);
1977
- const vy = upper + lower * Math.cos(theta2);
1978
- const theta1 = Math.atan2(dy, dx) - Math.atan2(vy, vx);
1979
- const deg = (r) => r * 180 / Math.PI;
1980
- return [deg(theta1), deg(theta2)];
1981
- }
1982
- function humanoid(opts = {}) {
1983
- const line2 = opts.color ?? DEFAULT_LINE;
1984
- const fill = opts.fill ?? DEFAULT_FILL;
1985
- const glow2 = opts.glow;
1986
- const blob = (jid, a, b, cy) => {
1987
- const d = ovalPath(a, b, 0, cy);
1988
- const nodes = [];
1989
- if (glow2) nodes.push(path({ id: `${jid}-glow`, d, x: 0, y: 0, fill: "none", stroke: glow2, strokeWidth: GLOW_W, opacity: 0.18 }));
1990
- nodes.push(path({ id: `${jid}-shape`, d, x: 0, y: 0, fill, stroke: line2, strokeWidth: LINE_W }));
1991
- return nodes;
1992
- };
1993
- const id = opts.id ?? "rig";
1994
- const root = {
1995
- name: "chest",
1996
- at: [0, 0],
1997
- shape: blob(`${id}-chest`, 44, 62, 22),
1998
- children: [
1999
- { name: "head", at: [0, -42], rotation: 0, shape: blob(`${id}-head`, 40, 42, -34) },
2000
- { name: "armUpperL", at: [-42, -20], length: 60, width: 20, rotation: 10, children: [
2001
- { name: "armLowerL", at: [0, 60], length: 56, width: 16, rotation: 8 }
2002
- ] },
2003
- { name: "armUpperR", at: [42, -20], length: 60, width: 20, rotation: -10, children: [
2004
- { name: "armLowerR", at: [0, 60], length: 56, width: 16, rotation: -8 }
2005
- ] },
2006
- { name: "legUpperL", at: [-20, 76], length: 76, width: 26, rotation: 3, children: [
2007
- { name: "legLowerL", at: [0, 76], length: 72, width: 22, rotation: -2 }
2008
- ] },
2009
- { name: "legUpperR", at: [20, 76], length: 76, width: 26, rotation: -3, children: [
2010
- { name: "legLowerR", at: [0, 76], length: 72, width: 22, rotation: 2 }
2011
- ] }
2012
- ]
1894
+
1895
+ // ../core/src/montage.ts
1896
+ var VIDEO_EXT = /\.(mp4|mov|webm|m4v|mkv)$/i;
1897
+ var isVideoSrc = (src) => VIDEO_EXT.test(src);
1898
+ function makeRng(seed) {
1899
+ let a = seed >>> 0 || 2654435769;
1900
+ return () => {
1901
+ a = a + 1831565813 | 0;
1902
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
1903
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
1904
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
2013
1905
  };
2014
- return rig(root, opts);
2015
1906
  }
1907
+ var norm = (img) => typeof img === "string" ? { src: img } : img;
1908
+ function photoMontage(images, opts = {}) {
1909
+ const id = opts.id ?? "shot";
1910
+ const W = opts.size?.width ?? 1920;
1911
+ const H = opts.size?.height ?? 1080;
1912
+ const hold = Math.max(0.5, opts.hold ?? 3.2);
1913
+ const zoom = Math.max(1.001, opts.zoom ?? 1.18);
1914
+ const grade = opts.grade !== false;
1915
+ const rand2 = makeRng((opts.seed ?? 0) + 1);
1916
+ const slides = images.map(norm);
1917
+ const cx = W / 2;
1918
+ const cy = H / 2;
1919
+ const nodes = [];
1920
+ const shots = [];
1921
+ let clock = 0;
1922
+ slides.forEach((slide, i) => {
1923
+ const nid = `${id}-${i}`;
1924
+ const slideHold = Math.max(0.5, slide.hold ?? hold);
1925
+ const transition = Math.min(opts.transition ?? 0.6, slideHold * 0.9);
1926
+ const shotStart = clock;
1927
+ clock += slideHold;
1928
+ const kind = slide.ken ?? ["in", "out", "pan"][Math.floor(rand2() * 3)] ?? "in";
1929
+ const angle = rand2() * Math.PI * 2;
1930
+ const panFrac = 0.4 + rand2() * 0.35;
1931
+ const dx = Math.cos(angle);
1932
+ const dy = Math.sin(angle);
1933
+ let kA, kB;
1934
+ let xA, xB, yA, yB;
1935
+ if (kind === "pan") {
1936
+ kA = kB = zoom;
1937
+ const sx = dx * (zoom - 1) * (W / 2) * panFrac;
1938
+ const sy = dy * (zoom - 1) * (H / 2) * panFrac;
1939
+ xA = cx - sx;
1940
+ xB = cx + sx;
1941
+ yA = cy - sy;
1942
+ yB = cy + sy;
1943
+ } else {
1944
+ kA = kind === "in" ? 1 : zoom;
1945
+ kB = kind === "in" ? zoom : 1;
1946
+ xA = cx + dx * (kA - 1) * (W / 2) * panFrac;
1947
+ xB = cx + dx * (kB - 1) * (W / 2) * panFrac;
1948
+ yA = cy + dy * (kA - 1) * (H / 2) * panFrac;
1949
+ yB = cy + dy * (kB - 1) * (H / 2) * panFrac;
1950
+ }
1951
+ const box = { id: nid, src: slide.src, x: xA, y: yA, width: W, height: H, anchor: "center", fit: "cover", scale: kA, opacity: i === 0 ? 1 : 0 };
1952
+ nodes.push(
1953
+ isVideoSrc(slide.src) ? video({ ...box, start: shotStart, volume: slide.volume ?? 0 }) : image(box)
1954
+ );
1955
+ const ken = tween(
1956
+ nid,
1957
+ { scale: kB, x: xB, y: yB },
1958
+ { duration: slideHold, ease: "easeInOutQuad", label: `shot-${i}` }
1959
+ );
1960
+ const shot = i === 0 ? par(ken) : par(
1961
+ ken,
1962
+ tween(`${id}-${i - 1}`, { opacity: 0 }, { duration: transition, ease: "linear", label: `cross-${i}` }),
1963
+ tween(nid, { opacity: 1 }, { duration: transition, ease: "linear" })
1964
+ );
1965
+ shots.push(shot);
1966
+ });
1967
+ if (grade) {
1968
+ nodes.push(
1969
+ rect({
1970
+ id: `${id}-vignette`,
1971
+ x: 0,
1972
+ y: 0,
1973
+ width: W,
1974
+ height: H,
1975
+ fill: radialGradient(
1976
+ [
1977
+ { offset: 0.55, color: "#FFFFFF" },
1978
+ { offset: 1, color: "#6E6E6E" }
1979
+ ],
1980
+ { cx: 0.5, cy: 0.5, r: 0.72 }
1981
+ ),
1982
+ blend: "multiply"
1983
+ })
1984
+ );
1985
+ nodes.push(
1986
+ rect({
1987
+ id: `${id}-scrim`,
1988
+ x: 0,
1989
+ y: 0,
1990
+ width: W,
1991
+ height: H,
1992
+ fill: linearGradient(
1993
+ [
1994
+ { offset: 0, color: "#00000000" },
1995
+ { offset: 0.62, color: "#00000000" },
1996
+ { offset: 1, color: "#000000B0" }
1997
+ ],
1998
+ { angle: 90 }
1999
+ )
2000
+ })
2001
+ );
2002
+ }
2003
+ return { nodes, timeline: beat("montage", { nodes: nodes.map((n3) => n3.id) }, [seq(...shots)]) };
2004
+ }
2005
+ var videoMontage = photoMontage;
2016
2006
 
2017
- // ../core/src/characterPreset.ts
2018
- var CHARACTER_PRESET_NAMES = ["walk", "run", "jump", "dance", "wave", "cheer"];
2019
- var THIGH = 76;
2020
- var SHIN = 72;
2021
- var clamp012 = (x) => Math.max(0, Math.min(1, x));
2022
- function makeRng3(seed) {
2007
+ // ../core/src/presets.ts
2008
+ var PRESET_NAMES = [
2009
+ "draw-bloom",
2010
+ "punch-in",
2011
+ "rise-settle",
2012
+ "slide-bank",
2013
+ "reveal-orbit",
2014
+ "spin-forge"
2015
+ ];
2016
+ function makeRng2(seed) {
2023
2017
  let a = seed >>> 0 || 2654435769;
2024
2018
  return () => {
2025
2019
  a = a + 1831565813 | 0;
@@ -2028,1567 +2022,1675 @@ function makeRng3(seed) {
2028
2022
  return ((t ^ t >>> 14) >>> 0) / 4294967296;
2029
2023
  };
2030
2024
  }
2031
- var dur2 = (base, sp) => base / sp;
2032
- function ctx2(o) {
2033
- const rand2 = makeRng3((o.seed ?? 0) + 1);
2025
+ var clamp01 = (x) => Math.max(0, Math.min(1, x));
2026
+ var SET = 1 / 120;
2027
+ function ctx(o) {
2028
+ const rand2 = makeRng2((o.seed ?? 0) + 1);
2034
2029
  return {
2035
- g: o.target,
2036
- label: o.label,
2037
- e: clamp012(o.energy ?? 0.5),
2030
+ e: clamp01(o.energy ?? 0.5),
2038
2031
  sp: Math.max(0.25, o.speed ?? 1),
2039
- cycles: Math.max(1, Math.round(o.cycles ?? 4)),
2040
- facing: o.facing ?? 1,
2041
- at: o.at ?? [0, 0],
2042
- travel: o.travel,
2032
+ it: clamp01(o.intensity ?? 0.5),
2033
+ from: o.from,
2043
2034
  rand: rand2,
2044
- jit: (amp) => (rand2() - 0.5) * 2 * amp
2035
+ jit: (amp) => (rand2() - 0.5) * 2 * amp,
2036
+ g: o.target.group,
2037
+ cx: o.target.center[0],
2038
+ cy: o.target.center[1],
2039
+ s: o.target.baseScale,
2040
+ fills: o.target.fills,
2041
+ inks: o.target.inks
2045
2042
  };
2046
2043
  }
2047
- var round = (v) => Math.round(v * 1e3) / 1e3;
2048
- function footPos(p, stride, lift) {
2049
- p = (p % 1 + 1) % 1;
2050
- if (p < 0.5) {
2051
- const u2 = p / 0.5;
2052
- return [stride * (1 - 2 * u2), 138];
2053
- }
2054
- const u = (p - 0.5) / 0.5;
2055
- return [-stride + 2 * stride * u, 138 - Math.sin(Math.PI * u) * lift];
2056
- }
2057
- function gaitPose(ph, stride, lift, armSwing, facing) {
2058
- const fl = footPos(ph, stride, lift);
2059
- const fr = footPos(ph + 0.5, stride, lift);
2060
- const [hipL, kneeL] = ikReach(THIGH, SHIN, facing * fl[0], fl[1], facing < 0);
2061
- const [hipR, kneeR] = ikReach(THIGH, SHIN, facing * fr[0], fr[1], facing < 0);
2062
- const swing = Math.cos(2 * Math.PI * ph);
2063
- return {
2064
- legUpperL: round(hipL),
2065
- legLowerL: round(kneeL),
2066
- legUpperR: round(hipR),
2067
- legLowerR: round(kneeR),
2068
- armUpperR: round(-10 - armSwing * swing),
2069
- armLowerR: -16,
2070
- armUpperL: round(10 + armSwing * swing),
2071
- armLowerL: 16
2072
- };
2044
+ var dur = (base, sp) => base / sp;
2045
+ function settleEase(e) {
2046
+ return e < 0.34 ? "easeOutCubic" : e < 0.67 ? "easeOutBack" : "easeOutElastic";
2073
2047
  }
2074
- function gait(c, run) {
2075
- const stride = (run ? 34 : 24) + (run ? 40 : 30) * c.e + c.jit(3);
2076
- const lift = run ? 40 : 26;
2077
- const armSwing = (run ? 26 : 16) + 20 * c.e;
2078
- const halfDur = (run ? 0.26 : 0.42) + c.jit(0.02);
2079
- const lean = run ? c.facing * -6 : 0;
2080
- const steps = c.cycles * 2;
2081
- const d = dur2(halfDur, c.sp);
2082
- const intro = dur2(0.16, c.sp);
2083
- const keys = [];
2084
- for (let k = 0; k <= steps; k++) {
2085
- const pose = { ...gaitPose(k / 2, stride, lift, armSwing, c.facing), chest: lean };
2086
- keys.push(poseTo(c.g, pose, { duration: k === 0 ? intro : d, ease: k === 0 ? "easeOutQuad" : "linear" }));
2087
- }
2088
- const total = intro + steps * d;
2089
- const travel = c.travel ?? stride * 2;
2090
- const children = [seq(...keys)];
2091
- if (travel !== 0) {
2092
- children.push(tween(c.g, { x: c.at[0] + c.facing * travel * c.cycles }, { duration: total, ease: "linear", label: "travel" }));
2048
+ function fromVec(from, dist) {
2049
+ switch (from) {
2050
+ case "left":
2051
+ return [-dist, 0];
2052
+ case "right":
2053
+ return [dist, 0];
2054
+ case "top":
2055
+ return [0, -dist];
2056
+ default:
2057
+ return [0, dist];
2093
2058
  }
2094
- return beat(run ? "run" : "walk", {}, [par(...children)]);
2095
2059
  }
2096
- function jumpBeat(c) {
2097
- const h = 120 + 150 * c.e;
2098
- const [y0] = [c.at[1]];
2099
- const CROUCH = { legUpperL: 18, legLowerL: 54, legUpperR: -18, legLowerR: 54, armUpperL: 28, armUpperR: -28 };
2100
- const LAUNCH = { legUpperL: 0, legLowerL: 0, legUpperR: 0, legLowerR: 0, armUpperL: 150, armUpperR: -150 };
2101
- const TUCK = { legUpperL: -28, legLowerL: 66, legUpperR: -28, legLowerR: 66, armUpperL: 124, armUpperR: -124 };
2102
- const REST = { legUpperL: 3, legLowerL: -2, legUpperR: -3, legLowerR: 2, armUpperL: 10, armLowerL: 8, armUpperR: -10, armLowerR: -8 };
2103
- const j = c.jit(0.03);
2104
- return beat("jump", {}, [
2105
- seq(
2106
- 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" })),
2107
- 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" })),
2108
- poseTo(c.g, TUCK, { duration: dur2(0.22, c.sp) }),
2109
- 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" })),
2110
- 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" }))
2060
+ function fadeFills(c, base = 0.4, gap = 0.06) {
2061
+ return stagger(
2062
+ gap / c.sp,
2063
+ ...c.fills.map(
2064
+ (id, i) => tween(id, { opacity: 1 }, { duration: dur(base, c.sp), ease: "easeOutQuad", ...i === 0 && { label: "reveal" } })
2111
2065
  )
2112
- ]);
2113
- }
2114
- function danceBeat(c) {
2115
- const y0 = c.at[1];
2116
- const sway = 8 + 6 * c.e;
2117
- const armUp = 130 + 30 * c.e;
2118
- const A = { chest: sway, head: -sway * 0.5, armUpperR: -armUp, armLowerR: -20, armUpperL: 40, armLowerL: 30, legUpperL: 8, legUpperR: -2 };
2119
- const B = { chest: -sway, head: sway * 0.5, armUpperL: armUp, armLowerL: 20, armUpperR: -40, armLowerR: -30, legUpperL: 2, legUpperR: -8 };
2120
- const d = dur2(0.34, c.sp);
2121
- const keys = [];
2122
- for (let k = 0; k < c.cycles * 2; k++) {
2123
- const pose = k % 2 === 0 ? A : B;
2124
- keys.push(par(
2125
- poseTo(c.g, pose, { duration: d, ease: "easeInOutQuad" }),
2126
- tween(c.g, { y: y0 - (k % 2 === 0 ? 14 : 0) }, { duration: d, ease: "easeInOutQuad" })
2127
- ));
2128
- }
2129
- keys.push(tween(c.g, { y: y0 }, { duration: d }));
2130
- return beat("dance", {}, [seq(...keys)]);
2131
- }
2132
- function waveBeat(c) {
2133
- const n3 = 3 + Math.round(c.rand() * 2);
2134
- const amp = 16 + 10 * c.e;
2135
- const steps = [poseTo(c.g, { armUpperR: -150, armLowerR: -24 }, { duration: dur2(0.4, c.sp), ease: "easeOutBack" })];
2136
- for (let k = 0; k < n3; k++) {
2137
- steps.push(poseTo(c.g, { armLowerR: -24 + (k % 2 === 0 ? amp : -amp) }, { duration: dur2(0.22, c.sp), ease: "easeInOutQuad" }));
2138
- }
2139
- steps.push(poseTo(c.g, { armUpperR: -10, armLowerR: -8 }, { duration: dur2(0.4, c.sp), ease: "easeInOutCubic" }));
2140
- return beat("wave", {}, [seq(...steps)]);
2066
+ );
2141
2067
  }
2142
- function cheerBeat(c) {
2143
- const y0 = c.at[1];
2144
- const UP = { armUpperL: 152, armLowerL: 8, armUpperR: -152, armLowerR: -8 };
2145
- const d = dur2(0.3, c.sp);
2146
- const keys = [poseTo(c.g, UP, { duration: dur2(0.35, c.sp), ease: "easeOutBack" })];
2147
- for (let k = 0; k < c.cycles; k++) {
2148
- keys.push(par(tween(c.g, { y: y0 - 28 }, { duration: d, ease: "easeOutQuad" }), poseTo(c.g, { armUpperL: 160, armUpperR: -160 }, { duration: d })));
2149
- keys.push(par(tween(c.g, { y: y0 }, { duration: d, ease: "easeInQuad" }), poseTo(c.g, { armUpperL: 145, armUpperR: -145 }, { duration: d })));
2150
- }
2151
- return beat("cheer", {}, [seq(...keys)]);
2068
+ function drawInks(c) {
2069
+ return stagger(
2070
+ 0.15 / c.sp,
2071
+ ...c.inks.map(
2072
+ (id, i) => tween(id, { progress: 1 }, { duration: dur(1.3 + c.jit(0.2), c.sp), ease: "easeInOutQuad", ...i === 0 && { label: "draw" } })
2073
+ )
2074
+ );
2152
2075
  }
2153
- function characterPreset(name, opts) {
2154
- const c = ctx2(opts);
2155
- let tl;
2076
+ function motionPreset(name, opts) {
2077
+ const c = ctx(opts);
2156
2078
  switch (name) {
2157
- case "walk":
2158
- tl = gait(c, false);
2159
- break;
2160
- case "run":
2161
- tl = gait(c, true);
2162
- break;
2163
- case "jump":
2164
- tl = jumpBeat(c);
2165
- break;
2166
- case "dance":
2167
- tl = danceBeat(c);
2168
- break;
2169
- case "wave":
2170
- tl = waveBeat(c);
2171
- break;
2172
- case "cheer":
2173
- tl = cheerBeat(c);
2174
- break;
2175
- default: {
2176
- const _exhaustive = name;
2177
- throw new Error(`unknown characterPreset "${_exhaustive}"`);
2079
+ case "draw-bloom":
2080
+ return beat("draw-bloom", {}, [
2081
+ drawInks(c),
2082
+ fadeFills(c, 0.45),
2083
+ tween(c.g, { scale: c.s * (1.02 + 0.05 * c.e) }, { duration: dur(2.4, c.sp), ease: "easeInOutQuad", label: "settle" })
2084
+ ]);
2085
+ case "punch-in": {
2086
+ const peak = c.s * (1 + 0.06 + 0.24 * c.e + c.jit(0.02));
2087
+ return beat("punch-in", {}, [
2088
+ par(
2089
+ fadeFills(c, 0.25),
2090
+ seq(
2091
+ tween(c.g, { scale: peak }, { duration: dur(0.45 + c.jit(0.05), c.sp), ease: "easeOutCubic", label: "punch" }),
2092
+ tween(c.g, { scale: c.s }, { duration: dur(0.5, c.sp), ease: settleEase(c.e) })
2093
+ )
2094
+ )
2095
+ ]);
2096
+ }
2097
+ case "rise-settle": {
2098
+ const es = 0.65 + c.rand() * 0.7;
2099
+ const dist = (220 + 260 * c.it) * es;
2100
+ const [dx, dy] = fromVec(c.from ?? "bottom", dist);
2101
+ const jx = c.jit(110);
2102
+ return beat("rise-settle", {}, [
2103
+ par(
2104
+ motionPath(
2105
+ c.g,
2106
+ [
2107
+ [c.cx + dx + jx, c.cy + dy],
2108
+ [c.cx + dx * 0.4 - jx * 0.6, c.cy + dy * 0.4],
2109
+ [c.cx, c.cy]
2110
+ ],
2111
+ { duration: dur(1.1, c.sp), ease: settleEase(c.e), label: "rise" }
2112
+ ),
2113
+ fadeFills(c, 0.4)
2114
+ )
2115
+ ]);
2116
+ }
2117
+ case "slide-bank": {
2118
+ const es = 0.65 + c.rand() * 0.7;
2119
+ const dist = (420 + 240 * c.it) * es;
2120
+ const [dx, dy] = fromVec(c.from ?? "left", dist);
2121
+ const arc = c.jit(140);
2122
+ const midx = c.jit(120);
2123
+ const move = dur(1.2, c.sp);
2124
+ return beat("slide-bank", {}, [
2125
+ par(
2126
+ motionPath(
2127
+ c.g,
2128
+ [
2129
+ [c.cx + dx, c.cy + dy],
2130
+ [c.cx + dx * 0.4 + midx, c.cy + dy * 0.4 - 70 - arc],
2131
+ [c.cx, c.cy]
2132
+ ],
2133
+ { duration: move, ease: settleEase(c.e), autoRotate: true, label: "slide" }
2134
+ ),
2135
+ // level the bank out once it lands (authored after the path → wins for rotation)
2136
+ seq(wait(move), tween(c.g, { rotation: 0 }, { duration: dur(0.5, c.sp), ease: "easeOutCubic" })),
2137
+ fadeFills(c, 0.4)
2138
+ )
2139
+ ]);
2140
+ }
2141
+ case "reveal-orbit": {
2142
+ const es = 0.65 + c.rand() * 0.7;
2143
+ const orbit = (180 + 160 * c.it) * es;
2144
+ const jx = c.jit(0.4);
2145
+ const jy = c.jit(0.4);
2146
+ return beat("reveal-orbit", {}, [
2147
+ drawInks(c),
2148
+ fadeFills(c, 0.45),
2149
+ par(
2150
+ motionPath(
2151
+ c.g,
2152
+ [
2153
+ [c.cx, c.cy],
2154
+ [c.cx - orbit * (1 + jx), c.cy - orbit * 0.8],
2155
+ [c.cx + orbit * (1 + jy), c.cy - orbit],
2156
+ [c.cx, c.cy]
2157
+ ],
2158
+ { duration: dur(1.7, c.sp), ease: "easeInOutCubic", label: "orbit" }
2159
+ ),
2160
+ seq(
2161
+ tween(c.g, { scale: c.s * (1.12 + 0.1 * c.e) }, { duration: dur(0.85, c.sp), ease: "easeOutBack" }),
2162
+ tween(c.g, { scale: c.s }, { duration: dur(0.85, c.sp), ease: "easeInOutQuad" })
2163
+ )
2164
+ )
2165
+ ]);
2166
+ }
2167
+ case "spin-forge": {
2168
+ const turns = 1 + Math.round(c.it);
2169
+ const dir = c.rand() < 0.5 ? -1 : 1;
2170
+ const startRot = dir * 360 * turns;
2171
+ const peak = c.s * (1 + 0.05 + 0.2 * c.e);
2172
+ return beat("spin-forge", {}, [
2173
+ par(
2174
+ seq(
2175
+ tween(c.g, { scale: c.s * 0.2, rotation: startRot }, { duration: SET }),
2176
+ // establish (invisible)
2177
+ tween(c.g, { scale: peak, rotation: 0 }, { duration: dur(0.9, c.sp), ease: "easeOutBack", label: "spin" }),
2178
+ tween(c.g, { scale: c.s }, { duration: dur(0.3, c.sp), ease: "easeInOutQuad" })
2179
+ ),
2180
+ seq(wait(SET), fadeFills(c, 0.3))
2181
+ )
2182
+ ]);
2178
2183
  }
2179
2184
  }
2180
- return c.label && tl.kind === "beat" ? { ...tl, name: c.label } : tl;
2181
2185
  }
2182
2186
 
2183
- // ../core/src/figure.ts
2184
- var K2 = 0.5523;
2185
- var n2 = (v) => Number(v.toFixed(2));
2186
- function limb(a, b, y0, y1) {
2187
- const ka = n2(a * K2), kb = n2(b * K2);
2188
- 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`;
2189
- }
2190
- function rrect(a, b, y0, y1, r) {
2191
- 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`;
2192
- }
2193
- function darken(hex, f) {
2194
- const h = hex.replace("#", "");
2195
- const v = parseInt(h.length === 3 ? [...h].map((c) => c + c).join("") : h, 16);
2196
- const ch = (s) => Math.max(0, Math.min(255, Math.round((v >> s & 255) * (1 - f))));
2197
- const hx = (x) => x.toString(16).padStart(2, "0");
2198
- return `#${hx(ch(16))}${hx(ch(8))}${hx(ch(0))}`;
2199
- }
2200
- var DEF = {
2201
- clean: { skin: "#E9B58E", hair: "#2B313F", top: "#E86C4A", pants: "#39425C", shoe: "#20242F", accent: "#E86C4A" },
2202
- cute: { skin: "#FFD2A6", hair: "#5B4636", top: "#FF7E5F", pants: "#3E6F8E", shoe: "#272B38", accent: "#FF7E5F" }
2187
+ // ../core/src/devicePreset.ts
2188
+ var DEVICE_PRESET_NAMES = ["phone", "tablet", "laptop", "browser", "watch", "monitor", "tv", "foldable", "terminal", "car"];
2189
+ var DARK = { body: "#15161C", bodyStroke: "#2A2D38", screen: "#0E0F15", detail: "#3A3D48", chrome: "#1B1D24", chromeText: "#9AA0AD" };
2190
+ var LIGHT = { body: "#E7E9EE", bodyStroke: "#C3C7D1", screen: "#FFFFFF", detail: "#AEB3C0", chrome: "#F2F3F6", chromeText: "#5B606C" };
2191
+ var SCREENS = {
2192
+ phone: { width: 352, height: 736, radius: 38 },
2193
+ tablet: { width: 544, height: 764, radius: 18 },
2194
+ laptop: { width: 840, height: 520, radius: 8, cy: -150 },
2195
+ browser: { width: 984, height: 568, radius: 6, cy: 24 },
2196
+ watch: { width: 184, height: 224, radius: 44 },
2197
+ monitor: { width: 1056, height: 600, radius: 6 },
2198
+ tv: { width: 1280, height: 720, radius: 8, cy: -24 },
2199
+ foldable: { width: 760, height: 560, radius: 20 },
2200
+ terminal: { width: 900, height: 560, radius: 6, cy: 18 },
2201
+ car: { width: 1e3, height: 520, radius: 24 }
2203
2202
  };
2204
- function resolvePal(style, p = {}) {
2205
- const d = DEF[style];
2206
- const accent = p.accent ?? d.accent;
2207
- const top = p.top ?? (style === "clean" ? accent : d.top);
2208
- const skin = p.skin ?? d.skin;
2209
- const hair = p.hair ?? d.hair;
2210
- const pants = p.pants ?? d.pants;
2211
- const shoe = p.shoe ?? d.shoe;
2212
- return {
2213
- skin,
2214
- skinSh: darken(skin, 0.12),
2215
- hair,
2216
- hairSh: darken(hair, 0.14),
2217
- top,
2218
- topSh: darken(top, 0.12),
2219
- pants,
2220
- pantsSh: darken(pants, 0.14),
2221
- shoe,
2222
- shoeSh: darken(shoe, 0.22),
2223
- eye: "#2B313F",
2224
- cheek: "#FF9E7E",
2225
- white: "#FFFFFF",
2226
- mouth: "#8A4233"
2227
- };
2203
+ var BOUNDS = {
2204
+ phone: { width: 392, height: 812 },
2205
+ tablet: { width: 600, height: 820 },
2206
+ laptop: { width: 1100, height: 650 },
2207
+ browser: { width: 1e3, height: 660 },
2208
+ watch: { width: 220, height: 300 },
2209
+ monitor: { width: 1120, height: 860 },
2210
+ tv: { width: 1340, height: 920 },
2211
+ foldable: { width: 800, height: 600 },
2212
+ terminal: { width: 916, height: 636 },
2213
+ car: { width: 1060, height: 600 }
2214
+ };
2215
+ var isLandscape = (name, o) => (name === "phone" || name === "tablet") && o.orientation === "landscape";
2216
+ function screenDims(name, o) {
2217
+ const d = SCREENS[name];
2218
+ const base = { cx: d.cx ?? 0, cy: d.cy ?? 0 };
2219
+ return isLandscape(name, o) ? { width: d.height, height: d.width, radius: d.radius, ...base } : { width: d.width, height: d.height, radius: d.radius, ...base };
2228
2220
  }
2229
- 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 } : {} });
2230
- function cleanParts(p, face) {
2231
- const HC = -42;
2232
- return {
2233
- upperArm: (j) => [fp(`${j}-sleeve`, limb(12, 10, 2, 58), p.top)],
2234
- forearm: (j) => [
2235
- fp(`${j}-elbow`, ovalPath(10, 10, 0, 3), p.skin),
2236
- fp(`${j}-fore`, limb(10, 8, 2, 48), p.skin),
2237
- fp(`${j}-hand`, ovalPath(11, 12, 0, 50), p.skin)
2238
- ],
2239
- thigh: (j) => [fp(`${j}-thigh`, limb(15, 13, 2, 72), p.pants)],
2240
- shin: (j) => [
2241
- fp(`${j}-knee`, ovalPath(13, 13, 0, 2), p.pants),
2242
- fp(`${j}-shin`, limb(13, 11, 2, 62), p.pants),
2243
- fp(`${j}-shoe`, ovalPath(15, 9, 4, 67), p.shoe)
2244
- ],
2245
- torso: (j) => [
2246
- fp(`${j}-shadow`, rrect(38, 26, -28, 52, 20), p.topSh),
2247
- fp(`${j}-top`, rrect(40, 27, -30, 52, 22), p.top),
2248
- fp(`${j}-pelvis`, rrect(29, 24, 46, 104, 14), p.pants)
2249
- ],
2250
- head: (j) => [
2251
- fp(`${j}-neck`, rrect(9, 9, 2, 22, 5), p.skin),
2252
- fp(`${j}-skin`, ovalPath(42, 46, 0, HC), p.skin),
2253
- fp(`${j}-hair`, ovalPath(44, 27, 0, HC - 31), p.hair),
2254
- fp(`${j}-hairL`, ovalPath(8, 14, -39, HC - 18), p.hair),
2255
- fp(`${j}-hairR`, ovalPath(8, 14, 39, HC - 18), p.hair),
2256
- ...face ? [fp(`${j}-eyeL`, ovalPath(5, 7, -14, HC + 2), p.eye), fp(`${j}-eyeR`, ovalPath(5, 7, 14, HC + 2), p.eye)] : []
2257
- ]
2258
- };
2221
+ function deviceScreen(name, opts = {}) {
2222
+ const d = screenDims(name, opts);
2223
+ return { x: 0, y: 0, width: d.width, height: d.height, radius: d.radius };
2259
2224
  }
2260
- 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";
2261
- function cuteParts(p, face) {
2262
- const HC = -50;
2263
- return {
2264
- upperArm: (j) => [fp(`${j}-sleeve`, limb(15, 13, 2, 58), p.top, p.topSh, 2.5)],
2265
- forearm: (j) => [
2266
- fp(`${j}-elbow`, ovalPath(12, 12, 0, 3), p.skin, p.skinSh, 2.5),
2267
- fp(`${j}-fore`, limb(12, 10, 2, 46), p.skin, p.skinSh, 2.5),
2268
- fp(`${j}-hand`, ovalPath(13, 14, 0, 50), p.skin, p.skinSh, 2.5)
2269
- ],
2270
- thigh: (j) => [fp(`${j}-thigh`, limb(19, 16, 2, 72), p.pants, p.pantsSh, 2.5)],
2271
- shin: (j) => [
2272
- fp(`${j}-knee`, ovalPath(16, 16, 0, 2), p.pants, p.pantsSh, 2.5),
2273
- fp(`${j}-shin`, limb(15, 12, 2, 60), p.pants, p.pantsSh, 2.5),
2274
- fp(`${j}-shoe`, ovalPath(18, 11, 5, 66), p.shoe, darken(p.shoe, 0.25), 2.5)
2275
- ],
2276
- torso: (j) => [
2277
- fp(`${j}-shadow`, rrect(40, 34, -18, 52, 16), p.topSh),
2278
- fp(`${j}-top`, rrect(42, 35, -20, 52, 18), p.top, p.topSh, 2.5),
2279
- fp(`${j}-pelvis`, rrect(36, 30, 46, 104, 16), p.pants, p.pantsSh, 2.5)
2280
- ],
2281
- head: (j) => [
2282
- fp(`${j}-neck`, ovalPath(15, 12, 0, 12), p.skinSh),
2283
- fp(`${j}-skin`, ovalPath(42, 46, 0, HC + 4), p.skin, p.skinSh, 2.5),
2284
- fp(`${j}-cheekL`, ovalPath(11, 8, -40, HC + 22), p.cheek, void 0, 0, 0.5),
2285
- fp(`${j}-cheekR`, ovalPath(11, 8, 40, HC + 22), p.cheek, void 0, 0, 0.5),
2286
- fp(`${j}-hair`, CUTE_HAIR, p.hair, p.hairSh, 2),
2287
- ...face ? [
2288
- fp(`${j}-eyeL`, ovalPath(10, 13, -25, HC + 8), p.eye),
2289
- fp(`${j}-eyeR`, ovalPath(10, 13, 25, HC + 8), p.eye),
2290
- fp(`${j}-glL`, ovalPath(3.4, 3.4, -28, HC + 3), p.white),
2291
- fp(`${j}-glR`, ovalPath(3.4, 3.4, 22, HC + 3), p.white),
2292
- 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 })
2293
- ] : []
2294
- ]
2295
- };
2225
+ function deviceScreenCenter(name, opts = {}) {
2226
+ const d = screenDims(name, opts);
2227
+ return { x: d.cx, y: d.cy };
2296
2228
  }
2297
- function buildSkeleton(id, S) {
2298
- const arm = (side, x, r1, r2) => ({
2299
- name: `armUpper${side}`,
2300
- at: [x, -14],
2301
- length: 60,
2302
- width: 0,
2303
- rotation: r1,
2304
- shape: S.upperArm(`${id}-armUpper${side}`),
2305
- children: [{ name: `armLower${side}`, at: [0, 60], length: 56, width: 0, rotation: r2, shape: S.forearm(`${id}-armLower${side}`) }]
2306
- });
2307
- const leg = (side, x, r1, r2) => ({
2308
- name: `legUpper${side}`,
2309
- at: [x, 76],
2310
- length: 76,
2311
- width: 0,
2312
- rotation: r1,
2313
- shape: S.thigh(`${id}-legUpper${side}`),
2314
- children: [{ name: `legLower${side}`, at: [0, 76], length: 72, width: 0, rotation: r2, shape: S.shin(`${id}-legLower${side}`) }]
2315
- });
2316
- return {
2317
- name: "chest",
2318
- at: [0, 0],
2319
- shape: S.torso(`${id}-chest`),
2320
- children: [
2321
- { name: "head", at: [0, -42], rotation: 0, shape: S.head(`${id}-head`) },
2322
- arm("L", -40, 8, 6),
2323
- arm("R", 40, -8, -6),
2324
- leg("L", -20, 3, -2),
2325
- leg("R", 20, -3, 2)
2326
- ]
2327
- };
2229
+ function deviceBounds(name, opts = {}) {
2230
+ const b = BOUNDS[name];
2231
+ return isLandscape(name, opts) ? { width: b.height, height: b.width } : { ...b };
2328
2232
  }
2329
- function figure(opts = {}) {
2330
- const style = opts.style ?? "clean";
2331
- const pal = resolvePal(style, opts.palette);
2332
- const face = opts.face ?? true;
2333
- const id = opts.id ?? "figure";
2334
- const parts = style === "clean" ? cleanParts(pal, face) : cuteParts(pal, face);
2335
- const rigOpts = { id };
2336
- if (opts.x !== void 0) rigOpts.x = opts.x;
2337
- if (opts.y !== void 0) rigOpts.y = opts.y;
2338
- if (opts.scale !== void 0) rigOpts.scale = opts.scale;
2339
- if (opts.opacity !== void 0) rigOpts.opacity = opts.opacity;
2340
- return rig(buildSkeleton(id, parts), rigOpts);
2233
+ function deviceScreenPoint(name, opts, local) {
2234
+ const c = deviceScreenCenter(name, opts);
2235
+ const s = opts.scale ?? 1;
2236
+ return [(opts.x ?? 0) + s * (c.x + local[0]), (opts.y ?? 0) + s * (c.y + local[1])];
2341
2237
  }
2342
-
2343
- // ../core/src/textMetrics.ts
2344
- var INTER_ADVANCE = {
2345
- "400": {
2346
- "0": 63.09,
2347
- "1": 40.67,
2348
- "2": 60.99,
2349
- "3": 61.77,
2350
- "4": 64.6,
2351
- "5": 59.33,
2352
- "6": 62.01,
2353
- "7": 56.59,
2354
- "8": 61.87,
2355
- "9": 62.01,
2356
- " ": 28.13,
2357
- "!": 28.76,
2358
- '"': 46.58,
2359
- "#": 63.33,
2360
- "$": 64.16,
2361
- "%": 98.19,
2362
- "&": 64.4,
2363
- "'": 29.98,
2364
- "(": 36.47,
2365
- ")": 36.47,
2366
- "*": 50.1,
2367
- "+": 66.16,
2368
- ",": 28.81,
2369
- "-": 46,
2370
- ".": 28.81,
2371
- "/": 36.04,
2372
- ":": 28.81,
2373
- ";": 30.18,
2374
- "<": 66.16,
2375
- "=": 66.16,
2376
- ">": 66.16,
2377
- "?": 51.12,
2378
- "@": 96.58,
2379
- "A": 68.99,
2380
- "B": 65.43,
2381
- "C": 73.05,
2382
- "D": 72.17,
2383
- "E": 60.11,
2384
- "F": 59.03,
2385
- "G": 74.61,
2386
- "H": 74.32,
2387
- "I": 26.86,
2388
- "J": 57.08,
2389
- "K": 67.19,
2390
- "L": 56.54,
2391
- "M": 90.33,
2392
- "N": 75.34,
2393
- "O": 76.46,
2394
- "P": 63.87,
2395
- "Q": 76.46,
2396
- "R": 64.36,
2397
- "S": 64.16,
2398
- "T": 64.55,
2399
- "U": 74.41,
2400
- "V": 68.99,
2401
- "W": 98.54,
2402
- "X": 68.21,
2403
- "Y": 67.87,
2404
- "Z": 62.89,
2405
- "[": 36.47,
2406
- "\\": 36.04,
2407
- "]": 36.47,
2408
- "^": 47.12,
2409
- "_": 45.61,
2410
- "`": 32.28,
2411
- "a": 56.15,
2412
- "b": 61.23,
2413
- "c": 57.13,
2414
- "d": 61.23,
2415
- "e": 58.3,
2416
- "f": 37.01,
2417
- "g": 61.33,
2418
- "h": 59.13,
2419
- "i": 24.22,
2420
- "j": 24.22,
2421
- "k": 54.88,
2422
- "l": 24.22,
2423
- "m": 87.6,
2424
- "n": 59.08,
2425
- "o": 59.96,
2426
- "p": 61.23,
2427
- "q": 61.23,
2428
- "r": 37.65,
2429
- "s": 52.78,
2430
- "t": 32.71,
2431
- "u": 59.13,
2432
- "v": 56.2,
2433
- "w": 81.84,
2434
- "x": 54.59,
2435
- "y": 56.2,
2436
- "z": 55.22,
2437
- "{": 42.63,
2438
- "|": 33.25,
2439
- "}": 42.63,
2440
- "~": 66.16
2441
- },
2442
- "700": {
2443
- "0": 67.43,
2444
- "1": 43.12,
2445
- "2": 62.94,
2446
- "3": 64.55,
2447
- "4": 67.63,
2448
- "5": 62.21,
2449
- "6": 64.94,
2450
- "7": 58.15,
2451
- "8": 65.09,
2452
- "9": 64.94,
2453
- " ": 23.68,
2454
- "!": 33.79,
2455
- '"': 55.13,
2456
- "#": 64.89,
2457
- "$": 65.48,
2458
- "%": 101.56,
2459
- "&": 67.19,
2460
- "'": 33.89,
2461
- "(": 37.7,
2462
- ")": 37.7,
2463
- "*": 55.91,
2464
- "+": 67.87,
2465
- ",": 33.4,
2466
- "-": 46.78,
2467
- ".": 33.4,
2468
- "/": 38.82,
2469
- ":": 33.4,
2470
- ";": 34.28,
2471
- "<": 67.87,
2472
- "=": 67.87,
2473
- ">": 67.87,
2474
- "?": 55.96,
2475
- "@": 101.61,
2476
- "A": 74.66,
2477
- "B": 66.16,
2478
- "C": 73.97,
2479
- "D": 72.22,
2480
- "E": 60.74,
2481
- "F": 58.69,
2482
- "G": 75.05,
2483
- "H": 74.71,
2484
- "I": 28.08,
2485
- "J": 58.45,
2486
- "K": 71.92,
2487
- "L": 56.54,
2488
- "M": 93.16,
2489
- "N": 76.22,
2490
- "O": 77.05,
2491
- "P": 64.79,
2492
- "Q": 77.69,
2493
- "R": 65.67,
2494
- "S": 65.48,
2495
- "T": 66.75,
2496
- "U": 73.19,
2497
- "V": 74.66,
2498
- "W": 103.76,
2499
- "X": 73.83,
2500
- "Y": 73.1,
2501
- "Z": 66.41,
2502
- "[": 37.7,
2503
- "\\": 38.82,
2504
- "]": 37.7,
2505
- "^": 48.68,
2506
- "_": 47.61,
2507
- "`": 36.52,
2508
- "a": 58.06,
2509
- "b": 63.04,
2510
- "c": 58.84,
2511
- "d": 63.04,
2512
- "e": 59.57,
2513
- "f": 39.79,
2514
- "g": 63.18,
2515
- "h": 62.26,
2516
- "i": 27.1,
2517
- "j": 27.1,
2518
- "k": 58.01,
2519
- "l": 27.1,
2520
- "m": 91.26,
2521
- "n": 62.26,
2522
- "o": 61.33,
2523
- "p": 63.04,
2524
- "q": 63.04,
2525
- "r": 40.72,
2526
- "s": 56.01,
2527
- "t": 36.62,
2528
- "u": 62.26,
2529
- "v": 59.96,
2530
- "w": 85.01,
2531
- "x": 58.01,
2532
- "y": 60.21,
2533
- "z": 57.28,
2534
- "{": 46.88,
2535
- "|": 37.16,
2536
- "}": 46.88,
2537
- "~": 67.87
2538
- },
2539
- "800": {
2540
- "0": 69.19,
2541
- "1": 44.14,
2542
- "2": 63.77,
2543
- "3": 65.67,
2544
- "4": 68.85,
2545
- "5": 63.38,
2546
- "6": 66.16,
2547
- "7": 58.79,
2548
- "8": 66.41,
2549
- "9": 66.16,
2550
- " ": 21.88,
2551
- "!": 35.84,
2552
- '"': 58.64,
2553
- "#": 65.53,
2554
- "$": 66.02,
2555
- "%": 102.93,
2556
- "&": 68.31,
2557
- "'": 35.45,
2558
- "(": 38.23,
2559
- ")": 38.23,
2560
- "*": 58.25,
2561
- "+": 68.55,
2562
- ",": 35.25,
2563
- "-": 47.12,
2564
- ".": 35.25,
2565
- "/": 39.99,
2566
- ":": 35.25,
2567
- ";": 35.99,
2568
- "<": 68.55,
2569
- "=": 68.55,
2570
- ">": 68.55,
2571
- "?": 57.91,
2572
- "@": 103.61,
2573
- "A": 76.95,
2574
- "B": 66.46,
2575
- "C": 74.37,
2576
- "D": 72.27,
2577
- "E": 60.99,
2578
- "F": 58.54,
2579
- "G": 75.24,
2580
- "H": 74.85,
2581
- "I": 28.56,
2582
- "J": 58.98,
2583
- "K": 73.83,
2584
- "L": 56.54,
2585
- "M": 94.34,
2586
- "N": 76.56,
2587
- "O": 77.29,
2588
- "P": 65.19,
2589
- "Q": 78.17,
2590
- "R": 66.21,
2591
- "S": 66.02,
2592
- "T": 67.68,
2593
- "U": 72.71,
2594
- "V": 76.95,
2595
- "W": 105.86,
2596
- "X": 76.12,
2597
- "Y": 75.2,
2598
- "Z": 67.87,
2599
- "[": 38.23,
2600
- "\\": 39.99,
2601
- "]": 38.23,
2602
- "^": 49.32,
2603
- "_": 48.44,
2604
- "`": 38.23,
2605
- "a": 58.84,
2606
- "b": 63.77,
2607
- "c": 59.52,
2608
- "d": 63.77,
2609
- "e": 60.06,
2610
- "f": 40.97,
2611
- "g": 63.92,
2612
- "h": 63.53,
2613
- "i": 28.32,
2614
- "j": 28.32,
2615
- "k": 59.28,
2616
- "l": 28.32,
2617
- "m": 92.72,
2618
- "n": 63.53,
2619
- "o": 61.91,
2620
- "p": 63.77,
2621
- "q": 63.77,
2622
- "r": 41.99,
2623
- "s": 57.32,
2624
- "t": 38.18,
2625
- "u": 63.53,
2626
- "v": 61.52,
2627
- "w": 86.28,
2628
- "x": 59.42,
2629
- "y": 61.82,
2630
- "z": 58.11,
2631
- "{": 48.63,
2632
- "|": 38.77,
2633
- "}": 48.63,
2634
- "~": 68.55
2635
- }
2636
- };
2637
- var INTER_FALLBACK = {
2638
- "400": 56.16,
2639
- "700": 58.74,
2640
- "800": 59.79
2641
- };
2642
-
2643
- // ../core/src/textFx.ts
2644
- var clamp013 = (v) => Math.max(0, Math.min(1, v));
2645
- var fract = (v) => v - Math.floor(v);
2646
- var rand = (i, salt) => fract(Math.sin(i * 127.1 + salt * 311.7) * 43758.5453);
2647
- var dur3 = (base, sp) => base / sp;
2648
- var SCRAMBLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&@";
2649
- var advance = (ch, weight, fontSize) => (INTER_ADVANCE[weight]?.[ch] ?? INTER_FALLBACK[weight]) * (fontSize / 100);
2650
- function splitText(textStr, opts) {
2651
- const { id, x, y, fontSize } = opts;
2652
- const weight = opts.fontWeight ?? 800;
2653
- const fill = opts.fill ?? "#FFFFFF";
2654
- const ls = opts.letterSpacing ?? 0;
2655
- const align = opts.align ?? "center";
2656
- const unit = opts.unit ?? "glyph";
2657
- const opacity = opts.opacity ?? 0;
2658
- const chars = [...textStr];
2659
- let total = 0;
2660
- chars.forEach((ch, i) => {
2661
- total += advance(ch, weight, fontSize) + (i < chars.length - 1 ? ls : 0);
2662
- });
2663
- let cursor2 = align === "center" ? x - total / 2 : x;
2664
- const glyphs = [];
2665
- const nodes = [];
2666
- const mk = (ch, cx, adv, lsProp) => {
2667
- const g = { id: `${id}-${glyphs.length}`, ch, x: cx, y, advance: adv, i: glyphs.length };
2668
- glyphs.push(g);
2669
- nodes.push(
2670
- text({
2671
- id: g.id,
2672
- x: cx,
2673
- y,
2674
- content: ch,
2675
- fontFamily: "Inter",
2676
- fontSize,
2677
- fontWeight: weight,
2678
- fill,
2679
- anchor: "center",
2680
- opacity,
2681
- ...lsProp ? { letterSpacing: lsProp } : {}
2682
- })
2683
- );
2684
- };
2685
- if (unit === "word") {
2686
- let i = 0;
2687
- while (i < chars.length) {
2688
- if (chars[i] === " ") {
2689
- cursor2 += advance(" ", weight, fontSize) + ls;
2690
- i++;
2691
- continue;
2692
- }
2693
- let word = "";
2694
- let w = 0;
2695
- const startCursor = cursor2;
2696
- while (i < chars.length && chars[i] !== " ") {
2697
- const a = advance(chars[i], weight, fontSize);
2698
- word += chars[i];
2699
- w += a + (chars[i + 1] && chars[i + 1] !== " " ? ls : 0);
2700
- i++;
2238
+ function screenGroup(id, p, o, cx, cy, dims, content) {
2239
+ return group({ id: `${id}-screen`, x: cx, y: cy, clip: { kind: "rect", x: -dims.width / 2, y: -dims.height / 2, width: dims.width, height: dims.height, radius: dims.radius } }, [
2240
+ rect({ id: `${id}-screenbg`, x: 0, y: 0, anchor: "center", width: dims.width, height: dims.height, fill: o.screen ?? p.screen }),
2241
+ group({ id: `${id}-content`, x: 0, y: 0 }, content)
2242
+ ]);
2243
+ }
2244
+ function buildDevice(name, id, p, o, content) {
2245
+ const dims = screenDims(name, o);
2246
+ const sw = dims.width;
2247
+ const sh = dims.height;
2248
+ const screen = () => screenGroup(id, p, o, dims.cx, dims.cy, dims, content);
2249
+ switch (name) {
2250
+ case "phone":
2251
+ case "tablet": {
2252
+ const bezel = name === "phone" ? 20 : 28;
2253
+ const bodyW = sw + bezel * 2;
2254
+ const bodyH = sh + bezel * 2;
2255
+ const bodyR = name === "phone" ? 54 : 34;
2256
+ const land = isLandscape(name, o);
2257
+ const nodes = [
2258
+ rect({ id: `${id}-body`, x: 0, y: 0, anchor: "center", width: bodyW, height: bodyH, fill: p.body, stroke: p.bodyStroke, strokeWidth: 2, radius: bodyR }),
2259
+ screen()
2260
+ ];
2261
+ if (name === "phone") {
2262
+ nodes.push(
2263
+ land ? rect({ id: `${id}-notch`, x: -sw / 2 + 16, y: 0, anchor: "center", width: 30, height: 96, fill: "#000000", radius: 15 }) : rect({ id: `${id}-notch`, x: 0, y: -sh / 2 + 16, anchor: "center", width: 96, height: 30, fill: "#000000", radius: 15 }),
2264
+ land ? rect({ id: `${id}-home`, x: sw / 2 - 4, y: 0, anchor: "center", width: 5, height: 120, fill: p.detail, radius: 3 }) : rect({ id: `${id}-home`, x: 0, y: sh / 2 - 18, anchor: "center", width: 120, height: 5, fill: p.detail, radius: 3 })
2265
+ );
2266
+ if (!land) {
2267
+ nodes.push(
2268
+ rect({ id: `${id}-pwr`, x: bodyW / 2, y: -bodyH * 0.1, anchor: "center", width: 4, height: 78, fill: p.detail, radius: 2 }),
2269
+ rect({ id: `${id}-volup`, x: -bodyW / 2, y: -bodyH * 0.16, anchor: "center", width: 4, height: 48, fill: p.detail, radius: 2 }),
2270
+ rect({ id: `${id}-voldn`, x: -bodyW / 2, y: -bodyH * 0.16 + 60, anchor: "center", width: 4, height: 48, fill: p.detail, radius: 2 })
2271
+ );
2272
+ }
2273
+ } else {
2274
+ nodes.push(
2275
+ rect({ id: `${id}-camera`, x: land ? -sw / 2 - 14 : 0, y: land ? 0 : -sh / 2 - 14, anchor: "center", width: 8, height: 8, fill: p.detail, radius: 4 }),
2276
+ rect({ id: `${id}-pwr`, x: land ? -bodyW * 0.18 : bodyW * 0.18, y: land ? -bodyH / 2 : -bodyH / 2, anchor: "center", width: 60, height: 4, fill: p.detail, radius: 2 })
2277
+ );
2701
2278
  }
2702
- mk(word, startCursor + w / 2, w, ls);
2703
- cursor2 = startCursor + w + ls;
2279
+ return nodes;
2280
+ }
2281
+ case "laptop": {
2282
+ const lidTop = dims.cy - (sh + 40) / 2;
2283
+ const keyRows = [0, 1, 2, 3].map(
2284
+ (r) => rect({ id: `${id}-keys${r}`, x: 0, y: 150 + r * 11, anchor: "center", width: 640 + r * 50, height: 6, fill: p.chrome, radius: 3 })
2285
+ );
2286
+ return [
2287
+ path({ id: `${id}-base`, x: 0, y: 0, d: "M -450 140 L 450 140 L 520 196 L -520 196 Z", fill: p.body, stroke: p.bodyStroke, strokeWidth: 2 }),
2288
+ rect({ id: `${id}-foot-l`, x: -360, y: 198, anchor: "center", width: 70, height: 5, fill: p.detail, radius: 3 }),
2289
+ rect({ id: `${id}-foot-r`, x: 360, y: 198, anchor: "center", width: 70, height: 5, fill: p.detail, radius: 3 }),
2290
+ ...keyRows,
2291
+ rect({ id: `${id}-trackpad`, x: 0, y: 184, anchor: "center", width: 150, height: 8, fill: p.detail, radius: 4 }),
2292
+ rect({ id: `${id}-hinge`, x: 0, y: 134, anchor: "center", width: 900, height: 10, fill: p.detail, radius: 5 }),
2293
+ screen(),
2294
+ ellipse({ id: `${id}-webcam`, x: 0, y: lidTop + 14, anchor: "center", width: 6, height: 6, fill: p.detail }),
2295
+ rect({ id: `${id}-lid`, x: 0, y: dims.cy, anchor: "center", width: sw + 40, height: sh + 40, stroke: p.bodyStroke, strokeWidth: 2, radius: 18 })
2296
+ ];
2297
+ }
2298
+ case "browser": {
2299
+ const winW = sw + 16;
2300
+ const winH = sh + 92;
2301
+ const barY = -winH / 2 + 24;
2302
+ return [
2303
+ rect({ id: `${id}-win`, x: 0, y: 0, anchor: "center", width: winW, height: winH, fill: p.chrome, stroke: p.bodyStroke, strokeWidth: 1.5, radius: 14 }),
2304
+ ellipse({ id: `${id}-dot1`, x: -winW / 2 + 30, y: barY, anchor: "center", width: 13, height: 13, fill: "#FF5F57" }),
2305
+ ellipse({ id: `${id}-dot2`, x: -winW / 2 + 54, y: barY, anchor: "center", width: 13, height: 13, fill: "#FEBC2E" }),
2306
+ ellipse({ id: `${id}-dot3`, x: -winW / 2 + 78, y: barY, anchor: "center", width: 13, height: 13, fill: "#28C840" }),
2307
+ // an active tab tucked under the lights
2308
+ rect({ id: `${id}-tab`, x: -winW / 2 + 230, y: barY, anchor: "center", width: 190, height: 30, fill: o.screen ?? p.screen, radius: 8 }),
2309
+ text({ id: `${id}-tabtext`, x: -winW / 2 + 156, y: barY, anchor: "center-left", content: "Overview", fontFamily: "Inter", fontSize: 13, fill: p.chromeText }),
2310
+ rect({ id: `${id}-urlpill`, x: 96, y: barY, anchor: "center", width: 700, height: 26, fill: o.screen ?? p.screen, stroke: p.bodyStroke, strokeWidth: 1, radius: 13 }),
2311
+ rect({ id: `${id}-lock`, x: 96 - 330, y: barY, anchor: "center", width: 8, height: 10, fill: p.chromeText, radius: 2 }),
2312
+ text({ id: `${id}-urltext`, x: 96 - 312, y: barY, anchor: "center-left", content: urlText(o.url), fontFamily: "Inter", fontSize: 14, fill: p.chromeText }),
2313
+ screen()
2314
+ ];
2315
+ }
2316
+ case "watch": {
2317
+ const bw = sw + 36;
2318
+ const bh = sh + 36;
2319
+ return [
2320
+ // straps (drawn behind the body) flaring out top & bottom
2321
+ path({ id: `${id}-bandtop`, x: 0, y: -bh / 2 + 4, d: "M -78 0 L 78 0 L 64 -86 L -64 -86 Z", fill: p.body, stroke: p.bodyStroke, strokeWidth: 2 }),
2322
+ path({ id: `${id}-bandbot`, x: 0, y: bh / 2 - 4, d: "M -78 0 L 78 0 L 64 86 L -64 86 Z", fill: p.body, stroke: p.bodyStroke, strokeWidth: 2 }),
2323
+ rect({ id: `${id}-body`, x: 0, y: 0, anchor: "center", width: bw, height: bh, fill: p.body, stroke: p.bodyStroke, strokeWidth: 3, radius: 60 }),
2324
+ screen(),
2325
+ rect({ id: `${id}-crown`, x: bw / 2, y: -20, anchor: "center", width: 14, height: 40, fill: p.detail, radius: 6 }),
2326
+ rect({ id: `${id}-button`, x: bw / 2 - 2, y: 40, anchor: "center", width: 8, height: 34, fill: p.detail, radius: 4 })
2327
+ ];
2328
+ }
2329
+ case "monitor": {
2330
+ const panelW = sw + 44;
2331
+ const panelH = sh + 60;
2332
+ return [
2333
+ rect({ id: `${id}-panel`, x: 0, y: 0, anchor: "center", width: panelW, height: panelH, fill: p.body, stroke: p.bodyStroke, strokeWidth: 2, radius: 16 }),
2334
+ screen(),
2335
+ ellipse({ id: `${id}-led`, x: panelW / 2 - 26, y: panelH / 2 - 16, anchor: "center", width: 6, height: 6, fill: "#28C840" }),
2336
+ rect({ id: `${id}-neck`, x: 0, y: panelH / 2 + 60, anchor: "center", width: 60, height: 120, fill: p.body }),
2337
+ path({ id: `${id}-stand`, x: 0, y: panelH / 2 + 60, d: "M -160 50 L 160 50 L 220 80 L -220 80 Z", fill: p.body, stroke: p.bodyStroke, strokeWidth: 2 })
2338
+ ];
2339
+ }
2340
+ case "tv": {
2341
+ const panelW = sw + 44;
2342
+ const panelH = sh + 48;
2343
+ const panelBottom = dims.cy + panelH / 2;
2344
+ return [
2345
+ rect({ id: `${id}-panel`, x: 0, y: dims.cy, anchor: "center", width: panelW, height: panelH, fill: p.body, stroke: p.bodyStroke, strokeWidth: 2, radius: 12 }),
2346
+ screen(),
2347
+ ellipse({ id: `${id}-brand`, x: 0, y: panelBottom - 12, anchor: "center", width: 6, height: 6, fill: p.detail }),
2348
+ rect({ id: `${id}-neck`, x: 0, y: panelBottom + 48, anchor: "center", width: 64, height: 96, fill: p.body }),
2349
+ path({ id: `${id}-stand`, x: 0, y: panelBottom + 96, d: "M -210 0 L 210 0 L 270 34 L -270 34 Z", fill: p.body, stroke: p.bodyStroke, strokeWidth: 2 })
2350
+ ];
2351
+ }
2352
+ case "foldable": {
2353
+ const bodyW = sw + 40;
2354
+ const bodyH = sh + 40;
2355
+ return [
2356
+ rect({ id: `${id}-hinge-l`, x: -bodyW / 2, y: 0, anchor: "center", width: 8, height: bodyH * 0.5, fill: p.detail, radius: 4 }),
2357
+ rect({ id: `${id}-hinge-r`, x: bodyW / 2, y: 0, anchor: "center", width: 8, height: bodyH * 0.5, fill: p.detail, radius: 4 }),
2358
+ rect({ id: `${id}-body`, x: 0, y: 0, anchor: "center", width: bodyW, height: bodyH, fill: p.body, stroke: p.bodyStroke, strokeWidth: 2, radius: 28 }),
2359
+ screen(),
2360
+ rect({ id: `${id}-crease`, x: 0, y: 0, anchor: "center", width: 4, height: sh, fill: p.bodyStroke, radius: 2, opacity: 0.5 }),
2361
+ ellipse({ id: `${id}-cam1`, x: -10, y: -sh / 2 + 18, anchor: "center", width: 8, height: 8, fill: p.detail }),
2362
+ ellipse({ id: `${id}-cam2`, x: 10, y: -sh / 2 + 18, anchor: "center", width: 8, height: 8, fill: p.detail })
2363
+ ];
2364
+ }
2365
+ case "terminal": {
2366
+ const winW = sw + 16;
2367
+ const winH = sh + 76;
2368
+ return [
2369
+ rect({ id: `${id}-win`, x: 0, y: 0, anchor: "center", width: winW, height: winH, fill: p.chrome, stroke: p.bodyStroke, strokeWidth: 1.5, radius: 12 }),
2370
+ ellipse({ id: `${id}-dot1`, x: -winW / 2 + 28, y: -winH / 2 + 22, anchor: "center", width: 12, height: 12, fill: "#FF5F57" }),
2371
+ ellipse({ id: `${id}-dot2`, x: -winW / 2 + 50, y: -winH / 2 + 22, anchor: "center", width: 12, height: 12, fill: "#FEBC2E" }),
2372
+ ellipse({ id: `${id}-dot3`, x: -winW / 2 + 72, y: -winH / 2 + 22, anchor: "center", width: 12, height: 12, fill: "#28C840" }),
2373
+ rect({ id: `${id}-tab`, x: -winW / 2 + 170, y: -winH / 2 + 22, anchor: "center", width: 130, height: 24, fill: o.screen ?? p.screen, radius: 6 }),
2374
+ text({ id: `${id}-title`, x: -winW / 2 + 170, y: -winH / 2 + 22, anchor: "center", content: urlText(o.url ?? "zsh"), fontFamily: "Inter", fontSize: 13, fill: p.chromeText }),
2375
+ screen()
2376
+ ];
2377
+ }
2378
+ case "car": {
2379
+ const bodyW = sw + 60;
2380
+ const bodyH = sh + 60;
2381
+ return [
2382
+ rect({ id: `${id}-body`, x: 0, y: 0, anchor: "center", width: bodyW, height: bodyH, fill: p.body, stroke: p.bodyStroke, strokeWidth: 2, radius: 40 }),
2383
+ ellipse({ id: `${id}-knob`, x: -bodyW / 2 + 18, y: 0, anchor: "center", width: 22, height: 22, fill: p.body, stroke: p.detail, strokeWidth: 3 }),
2384
+ screen(),
2385
+ ellipse({ id: `${id}-btn1`, x: -44, y: sh / 2 + 16, anchor: "center", width: 12, height: 12, fill: p.detail }),
2386
+ ellipse({ id: `${id}-btn2`, x: 0, y: sh / 2 + 16, anchor: "center", width: 12, height: 12, fill: p.detail }),
2387
+ ellipse({ id: `${id}-btn3`, x: 44, y: sh / 2 + 16, anchor: "center", width: 12, height: 12, fill: p.detail })
2388
+ ];
2704
2389
  }
2705
- } else {
2706
- chars.forEach((ch) => {
2707
- const a = advance(ch, weight, fontSize);
2708
- if (ch !== " ") mk(ch, cursor2 + a / 2, a);
2709
- cursor2 += a + ls;
2710
- });
2711
2390
  }
2712
- return { nodes, glyphs, ids: glyphs.map((g) => g.id), width: total, x, y, fontSize };
2713
2391
  }
2714
- var ctx3 = (o) => ({
2715
- sp: Math.max(0.25, o.speed ?? 1),
2716
- e: clamp013(o.energy ?? 0.5),
2717
- seed: o.seed ?? 0,
2718
- fs: 0,
2719
- stag: o.stagger
2720
- });
2721
- var IN_STAGGER = { typewriter: 0.065, cascade: 0.04, rise: 0.03, bounce: 0.045, assemble: 0.05, decode: 0.05 };
2722
- function glyphIn(name, g, c) {
2723
- const set = (props) => tween(g.id, props, { duration: 1e-3 });
2724
- const rs = (salt) => rand(g.i, salt + c.seed);
2725
- switch (name) {
2726
- case "typewriter":
2727
- return tween(g.id, { opacity: 1 }, { duration: dur3(0.04, c.sp), ease: "linear" });
2728
- case "cascade":
2729
- return seq(
2730
- set({ y: g.y + 56, opacity: 0 }),
2731
- par(
2732
- tween(g.id, { opacity: 1 }, { duration: dur3(0.22, c.sp), ease: "easeOutQuad" }),
2733
- tween(g.id, { y: g.y }, { duration: dur3(0.34, c.sp), ease: "easeOutCubic" })
2734
- )
2735
- );
2736
- case "rise":
2737
- return seq(
2738
- set({ y: g.y + 36, opacity: 0 }),
2739
- par(
2740
- tween(g.id, { opacity: 1 }, { duration: dur3(0.3, c.sp), ease: "easeOutQuad" }),
2741
- tween(g.id, { y: g.y }, { duration: dur3(0.4, c.sp), ease: "easeOutQuad" })
2742
- )
2743
- );
2744
- case "bounce":
2745
- return seq(
2746
- set({ y: g.y - 80 * (0.6 + c.e), opacity: 0, scale: 0.7 }),
2747
- par(
2748
- tween(g.id, { opacity: 1 }, { duration: dur3(0.2, c.sp), ease: "easeOutQuad" }),
2749
- tween(g.id, { y: g.y, scale: 1 }, { duration: dur3(0.7, c.sp), ease: "easeOutBounce" })
2750
- )
2751
- );
2752
- case "assemble":
2753
- return seq(
2754
- set({ x: g.x + (rs(11) - 0.5) * 1e3 * (0.5 + c.e), y: g.y + (rs(12) - 0.5) * 640, rotation: (rs(13) - 0.5) * 200, scale: 0.4, opacity: 0 }),
2755
- par(
2756
- tween(g.id, { opacity: 1 }, { duration: dur3(0.4, c.sp), ease: "easeOutQuad" }),
2757
- tween(g.id, { x: g.x, y: g.y, rotation: 0, scale: 1 }, { duration: dur3(0.8, c.sp), ease: "easeOutExpo" })
2758
- )
2759
- );
2760
- case "decode": {
2761
- const steps = 4 + Math.floor(rs(7) * 3);
2762
- const flicker = [set({ opacity: 1 })];
2763
- for (let k = 0; k < steps; k++) {
2764
- flicker.push(tween(g.id, { content: SCRAMBLE[Math.floor(rand(g.i, 20 + k + c.seed) * SCRAMBLE.length)] }, { duration: dur3(0.05, c.sp), ease: "linear" }));
2765
- }
2766
- flicker.push(tween(g.id, { content: g.ch }, { duration: dur3(0.05, c.sp), ease: "linear" }));
2767
- return seq(...flicker);
2768
- }
2392
+ var urlText = (url) => {
2393
+ const u = url ?? "reframe.video";
2394
+ return u.length > 70 ? `${u.slice(0, 67)}\u2026` : u;
2395
+ };
2396
+ function devicePreset(name, opts = {}) {
2397
+ const id = opts.id ?? "device";
2398
+ const p = opts.color === "light" ? LIGHT : DARK;
2399
+ const children = buildDevice(name, id, p, opts, opts.content ?? []);
2400
+ return group(
2401
+ {
2402
+ id,
2403
+ x: opts.x ?? 0,
2404
+ y: opts.y ?? 0,
2405
+ ...opts.scale !== void 0 && opts.scale !== 1 && { scale: opts.scale },
2406
+ ...opts.opacity !== void 0 && opts.opacity !== 1 && { opacity: opts.opacity }
2407
+ },
2408
+ children
2409
+ );
2410
+ }
2411
+
2412
+ // ../core/src/cursor.ts
2413
+ var ARROW_D = "M0 0 L0 30 L8 23 L12.6 33 L17 31 L12.4 21.4 L21 21.4 Z";
2414
+ function cursor(opts = {}) {
2415
+ const id = opts.id ?? "cursor";
2416
+ const style = opts.style ?? "arrow";
2417
+ const fill = opts.fill ?? "#FFFFFF";
2418
+ const accent = opts.accent ?? "#FF5A1F";
2419
+ const art = style === "arrow" ? [path({ id: `${id}-arrow`, d: ARROW_D, x: 0, y: 0, fill, stroke: "#15171E", strokeWidth: 2 })] : style === "dot" ? [ellipse({ id: `${id}-dot`, x: 0, y: 0, width: 18, height: 18, fill: accent, anchor: "center" })] : [ellipse({ id: `${id}-ring`, x: 0, y: 0, width: 22, height: 22, fill: "none", stroke: accent, strokeWidth: 3, anchor: "center" })];
2420
+ return group(
2421
+ { id, x: opts.x ?? 0, y: opts.y ?? 0, scale: opts.scale ?? 1, opacity: opts.opacity ?? 1 },
2422
+ [
2423
+ // ripple ring (behind the pointer), emanates from the hotspot on click
2424
+ ellipse({ id: `${id}-ripple`, x: 0, y: 0, width: 30, height: 30, fill: "none", stroke: accent, strokeWidth: 3, opacity: 0, scale: 0, anchor: "center" }),
2425
+ // the pointer art lives in its own group so a click "tap" can scale it
2426
+ // independently of the cursor's resting scale
2427
+ group({ id: `${id}-art`, x: 0, y: 0 }, art)
2428
+ ]
2429
+ );
2430
+ }
2431
+ var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
2432
+ function cursorTo(id, from, to2, opts = {}) {
2433
+ const dx = to2[0] - from[0], dy = to2[1] - from[1];
2434
+ const dist = Math.hypot(dx, dy) || 1;
2435
+ const arc = opts.arc ?? 0.12;
2436
+ const mid = [(from[0] + to2[0]) / 2 + -dy / dist * arc * dist, (from[1] + to2[1]) / 2 + dx / dist * arc * dist];
2437
+ const duration = opts.duration ?? clamp(dist / 1400, 0.4, 0.9);
2438
+ return motionPath(id, [from, mid, to2], { duration, ease: opts.ease ?? "easeInOutCubic", curviness: 1, ...opts.label && { label: opts.label } });
2439
+ }
2440
+ function cursorPath(id, points, opts = {}) {
2441
+ return motionPath(id, points, {
2442
+ duration: opts.duration ?? clamp(points.length * 0.5, 0.5, 4),
2443
+ ease: opts.ease ?? "easeInOutCubic",
2444
+ curviness: opts.curviness ?? 1,
2445
+ ...opts.label && { label: opts.label }
2446
+ });
2447
+ }
2448
+ function clickBody(id, o) {
2449
+ const sp = Math.max(0.25, o.speed ?? 1);
2450
+ const d = (b) => b / sp;
2451
+ const out = [
2452
+ // the pointer taps
2453
+ seq(tween(`${id}-art`, { scale: 0.82 }, { duration: d(0.08), ease: "easeOutQuad" }), tween(`${id}-art`, { scale: 1 }, { duration: d(0.1), ease: "easeOutBack" }))
2454
+ ];
2455
+ if (o.ripple !== false) {
2456
+ out.push(seq(
2457
+ tween(`${id}-ripple`, { scale: 0.2, opacity: 0.55 }, { duration: 1e-3 }),
2458
+ par(
2459
+ tween(`${id}-ripple`, { scale: 5 }, { duration: d(0.5), ease: "easeOutCubic" }),
2460
+ tween(`${id}-ripple`, { opacity: 0 }, { duration: d(0.5), ease: "easeOutQuad" })
2461
+ )
2462
+ ));
2463
+ }
2464
+ if (o.press) {
2465
+ out.push(seq(tween(o.press, { scale: 0.94 }, { duration: d(0.08), ease: "easeOutQuad" }), tween(o.press, { scale: 1 }, { duration: d(0.14), ease: "easeOutBack" })));
2769
2466
  }
2467
+ return out;
2468
+ }
2469
+ function cursorClick(id, opts = {}) {
2470
+ return beat(opts.label ?? "cursor-click", {}, [par(...clickBody(id, opts))]);
2471
+ }
2472
+ function cursorDouble(id, opts = {}) {
2473
+ const sp = Math.max(0.25, opts.speed ?? 1);
2474
+ return beat(opts.label ?? "cursor-double", {}, [
2475
+ seq(par(...clickBody(id, { ...opts, ripple: false })), wait(0.12 / sp), par(...clickBody(id, opts)))
2476
+ ]);
2477
+ }
2478
+
2479
+ // ../core/src/rig.ts
2480
+ var DEFAULT_LINE = "#FFE3D2";
2481
+ var DEFAULT_FILL = "#0E1424";
2482
+ var LINE_W = 5;
2483
+ var GLOW_W = 16;
2484
+ var K = 0.5523;
2485
+ var n = (v) => Number(v.toFixed(2));
2486
+ function capsulePath(hw, len) {
2487
+ const yT = hw;
2488
+ const yB = Math.max(hw, len - hw);
2489
+ const k = hw * K;
2490
+ 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`;
2491
+ }
2492
+ function ovalPath(a, b, cx = 0, cy = 0) {
2493
+ const ka = n(a * K), kb = n(b * K);
2494
+ const t = n(cy - b), bo = n(cy + b), c = n(cy), A = n(a), L = n(-a), X = n(cx);
2495
+ 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`;
2496
+ }
2497
+ function boneShape(jointId, bone, o) {
2498
+ if (bone.shape) return bone.shape;
2499
+ const len = bone.length ?? 0;
2500
+ if (len <= 0) return [];
2501
+ const d = capsulePath((bone.width ?? 20) / 2, len);
2502
+ const nodes = [];
2503
+ 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 }));
2504
+ nodes.push(path({ id: `${jointId}-shape`, d, x: 0, y: 0, fill: o.fill, stroke: o.color, strokeWidth: LINE_W }));
2505
+ return nodes;
2506
+ }
2507
+ function buildBone(bone, id, o) {
2508
+ const jointId = `${id}-${bone.name}`;
2509
+ return group(
2510
+ { id: jointId, x: bone.at[0], y: bone.at[1], rotation: bone.rotation ?? 0 },
2511
+ [...boneShape(jointId, bone, o), ...(bone.children ?? []).map((c) => buildBone(c, id, o))]
2512
+ );
2513
+ }
2514
+ function rig(root, opts = {}) {
2515
+ const id = opts.id ?? "rig";
2516
+ const o = { color: opts.color ?? DEFAULT_LINE, fill: opts.fill ?? DEFAULT_FILL, glow: opts.glow };
2517
+ return group(
2518
+ { id, x: opts.x ?? 0, y: opts.y ?? 0, scale: opts.scale ?? 1, opacity: opts.opacity ?? 1 },
2519
+ [buildBone(root, id, o)]
2520
+ );
2521
+ }
2522
+ function rigPose(id, pose) {
2523
+ const out = {};
2524
+ for (const [name, deg] of Object.entries(pose)) out[`${id}-${name}`] = { rotation: deg };
2525
+ return out;
2526
+ }
2527
+ function poseTo(id, pose, opts = {}) {
2528
+ const tweens = Object.entries(pose).map(
2529
+ ([name, deg]) => tween(`${id}-${name}`, { rotation: deg }, { duration: opts.duration ?? 0.5, ease: opts.ease ?? "easeInOutCubic" })
2530
+ );
2531
+ return opts.stagger ? stagger(opts.stagger, ...tweens) : par(...tweens);
2532
+ }
2533
+ function ikReach(upper, lower, dx, dy, flip = false) {
2534
+ const D = Math.hypot(dx, dy);
2535
+ const cos2 = Math.max(-1, Math.min(1, (D * D - upper * upper - lower * lower) / (2 * upper * lower)));
2536
+ const theta2 = (flip ? -1 : 1) * Math.acos(cos2);
2537
+ const vx = -lower * Math.sin(theta2);
2538
+ const vy = upper + lower * Math.cos(theta2);
2539
+ const theta1 = Math.atan2(dy, dx) - Math.atan2(vy, vx);
2540
+ const deg = (r) => r * 180 / Math.PI;
2541
+ return [deg(theta1), deg(theta2)];
2542
+ }
2543
+ function humanoid(opts = {}) {
2544
+ const line2 = opts.color ?? DEFAULT_LINE;
2545
+ const fill = opts.fill ?? DEFAULT_FILL;
2546
+ const glow2 = opts.glow;
2547
+ const blob = (jid, a, b, cy) => {
2548
+ const d = ovalPath(a, b, 0, cy);
2549
+ const nodes = [];
2550
+ if (glow2) nodes.push(path({ id: `${jid}-glow`, d, x: 0, y: 0, fill: "none", stroke: glow2, strokeWidth: GLOW_W, opacity: 0.18 }));
2551
+ nodes.push(path({ id: `${jid}-shape`, d, x: 0, y: 0, fill, stroke: line2, strokeWidth: LINE_W }));
2552
+ return nodes;
2553
+ };
2554
+ const id = opts.id ?? "rig";
2555
+ const root = {
2556
+ name: "chest",
2557
+ at: [0, 0],
2558
+ shape: blob(`${id}-chest`, 44, 62, 22),
2559
+ children: [
2560
+ { name: "head", at: [0, -42], rotation: 0, shape: blob(`${id}-head`, 40, 42, -34) },
2561
+ { name: "armUpperL", at: [-42, -20], length: 60, width: 20, rotation: 10, children: [
2562
+ { name: "armLowerL", at: [0, 60], length: 56, width: 16, rotation: 8 }
2563
+ ] },
2564
+ { name: "armUpperR", at: [42, -20], length: 60, width: 20, rotation: -10, children: [
2565
+ { name: "armLowerR", at: [0, 60], length: 56, width: 16, rotation: -8 }
2566
+ ] },
2567
+ { name: "legUpperL", at: [-20, 76], length: 76, width: 26, rotation: 3, children: [
2568
+ { name: "legLowerL", at: [0, 76], length: 72, width: 22, rotation: -2 }
2569
+ ] },
2570
+ { name: "legUpperR", at: [20, 76], length: 76, width: 26, rotation: -3, children: [
2571
+ { name: "legLowerR", at: [0, 76], length: 72, width: 22, rotation: 2 }
2572
+ ] }
2573
+ ]
2574
+ };
2575
+ return rig(root, opts);
2770
2576
  }
2771
- function textIn(name, block, opts = {}) {
2772
- const c = { ...ctx3(opts), fs: block.fontSize };
2773
- const interval = (c.stag ?? IN_STAGGER[name]) / c.sp;
2774
- return beat(opts.label ?? `text-in-${name}`, {}, [stagger(interval, ...block.glyphs.map((g) => glyphIn(name, g, c)))]);
2577
+
2578
+ // ../core/src/characterPreset.ts
2579
+ var CHARACTER_PRESET_NAMES = ["walk", "run", "jump", "dance", "wave", "cheer"];
2580
+ var THIGH = 76;
2581
+ var SHIN = 72;
2582
+ var clamp012 = (x) => Math.max(0, Math.min(1, x));
2583
+ function makeRng3(seed) {
2584
+ let a = seed >>> 0 || 2654435769;
2585
+ return () => {
2586
+ a = a + 1831565813 | 0;
2587
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
2588
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
2589
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
2590
+ };
2775
2591
  }
2776
- function textLoop(name, block, opts = {}) {
2777
- const win = { ...opts.from !== void 0 && { from: opts.from }, ...opts.until !== void 0 && { until: opts.until }, ...opts.ramp !== void 0 && { ramp: opts.ramp } };
2778
- const f = opts.frequency ?? (name === "wave" ? 0.9 : name === "shimmer" ? 1.4 : 0.7);
2779
- const ps = opts.phaseStep ?? 0.55;
2780
- return block.glyphs.map((g, i) => {
2781
- switch (name) {
2782
- case "wave":
2783
- return oscillate(g.id, "y", { amplitude: opts.amplitude ?? 9, frequency: f, phase: i * ps }, win);
2784
- case "shimmer":
2785
- return oscillate(g.id, "opacity", { amplitude: opts.amplitude ?? 0.25, frequency: f, phase: i * ps }, win);
2786
- case "wobble":
2787
- return oscillate(g.id, "rotation", { amplitude: opts.amplitude ?? 6, frequency: f, phase: i * ps }, win);
2788
- case "float":
2789
- return oscillate(g.id, "y", { amplitude: opts.amplitude ?? 5, frequency: f, phase: i * ps }, win);
2790
- }
2791
- });
2592
+ var dur2 = (base, sp) => base / sp;
2593
+ function ctx2(o) {
2594
+ const rand2 = makeRng3((o.seed ?? 0) + 1);
2595
+ return {
2596
+ g: o.target,
2597
+ label: o.label,
2598
+ e: clamp012(o.energy ?? 0.5),
2599
+ sp: Math.max(0.25, o.speed ?? 1),
2600
+ cycles: Math.max(1, Math.round(o.cycles ?? 4)),
2601
+ facing: o.facing ?? 1,
2602
+ at: o.at ?? [0, 0],
2603
+ travel: o.travel,
2604
+ rand: rand2,
2605
+ jit: (amp) => (rand2() - 0.5) * 2 * amp
2606
+ };
2792
2607
  }
2793
- var OUT_STAGGER = { shatter: 0.02, fly: 0.012, dissolve: 0, fall: 0.02, collapse: 0.02 };
2794
- function glyphOut(name, g, c, block, dir) {
2795
- const rs = (salt) => rand(g.i, salt + c.seed);
2796
- switch (name) {
2797
- case "shatter":
2798
- return par(
2799
- tween(g.id, { x: g.x + (rs(21) - 0.5) * 1100 * (0.6 + c.e), y: g.y + (rs(22) - 0.5) * 760 }, { duration: dur3(0.7, c.sp), ease: "easeInCubic" }),
2800
- tween(g.id, { rotation: (rs(23) - 0.5) * 300, opacity: 0 }, { duration: dur3(0.7, c.sp), ease: "easeInQuad" })
2801
- );
2802
- case "fly":
2803
- return par(
2804
- tween(g.id, { x: g.x + dir[0] * 1200, y: g.y + dir[1] * 1200 }, { duration: dur3(0.6, c.sp), ease: "easeInCubic" }),
2805
- tween(g.id, { opacity: 0 }, { duration: dur3(0.5, c.sp), ease: "easeInQuad" })
2806
- );
2807
- case "dissolve":
2808
- return seq(wait(rs(31) * 0.5), par(
2809
- tween(g.id, { opacity: 0 }, { duration: dur3(0.4, c.sp), ease: "easeInQuad" }),
2810
- tween(g.id, { scale: 1.4 }, { duration: dur3(0.4, c.sp), ease: "easeOutQuad" })
2811
- ));
2812
- case "fall":
2813
- return par(
2814
- tween(g.id, { y: g.y + 700 + rs(41) * 200 }, { duration: dur3(0.8, c.sp), ease: "easeInQuad" }),
2815
- tween(g.id, { rotation: (rs(42) - 0.5) * 120, opacity: 0 }, { duration: dur3(0.8, c.sp), ease: "easeInQuad" })
2816
- );
2817
- case "collapse":
2818
- return par(
2819
- tween(g.id, { x: block.x, y: block.y, scale: 0.2 }, { duration: dur3(0.5, c.sp), ease: "easeInBack" }),
2820
- tween(g.id, { opacity: 0 }, { duration: dur3(0.5, c.sp), ease: "easeInQuad" })
2821
- );
2608
+ var round = (v) => Math.round(v * 1e3) / 1e3;
2609
+ function footPos(p, stride, lift) {
2610
+ p = (p % 1 + 1) % 1;
2611
+ if (p < 0.5) {
2612
+ const u2 = p / 0.5;
2613
+ return [stride * (1 - 2 * u2), 138];
2822
2614
  }
2615
+ const u = (p - 0.5) / 0.5;
2616
+ return [-stride + 2 * stride * u, 138 - Math.sin(Math.PI * u) * lift];
2823
2617
  }
2824
- function textOut(name, block, opts = {}) {
2825
- const c = { ...ctx3(opts), fs: block.fontSize };
2826
- const dir = opts.dir ?? [0, -1];
2827
- const steps = block.glyphs.map((g) => glyphOut(name, g, c, block, dir));
2828
- const interval = (c.stag ?? OUT_STAGGER[name]) / c.sp;
2829
- const body = interval > 0 ? stagger(interval, ...steps) : par(...steps);
2830
- return beat(opts.label ?? `text-out-${name}`, {}, [body]);
2831
- }
2832
- function textTypeCues(block, opts) {
2833
- const interval = opts.interval ?? 0.065;
2834
- const gain = opts.gain ?? 0.4;
2835
- const off = opts.offset ?? 0;
2836
- const KEYS = ["001", "004", "007", "010", "014"];
2837
- return block.glyphs.map((g, i) => ({
2838
- at: opts.at,
2839
- offset: off + i * interval,
2840
- file: `keypress-${KEYS[i % KEYS.length]}.wav`,
2841
- gain: gain + 0.2 * rand(i, 31)
2842
- }));
2843
- }
2844
-
2845
- // ../core/src/motionOps.ts
2846
- var MOTION_OPS = ["rotate", "zoom", "ken-burns", "slide-in", "fade", "draw-on", "pulse"];
2847
- var clamp014 = (n3) => Math.max(0, Math.min(1, n3));
2848
- function settleEase2(e) {
2849
- return e < 0.34 ? "easeOutCubic" : e < 0.67 ? "easeOutBack" : "easeOutElastic";
2618
+ function gaitPose(ph, stride, lift, armSwing, facing) {
2619
+ const fl = footPos(ph, stride, lift);
2620
+ const fr = footPos(ph + 0.5, stride, lift);
2621
+ const [hipL, kneeL] = ikReach(THIGH, SHIN, facing * fl[0], fl[1], facing < 0);
2622
+ const [hipR, kneeR] = ikReach(THIGH, SHIN, facing * fr[0], fr[1], facing < 0);
2623
+ const swing = Math.cos(2 * Math.PI * ph);
2624
+ return {
2625
+ legUpperL: round(hipL),
2626
+ legLowerL: round(kneeL),
2627
+ legUpperR: round(hipR),
2628
+ legLowerR: round(kneeR),
2629
+ armUpperR: round(-10 - armSwing * swing),
2630
+ armLowerR: -16,
2631
+ armUpperL: round(10 + armSwing * swing),
2632
+ armLowerL: 16
2633
+ };
2850
2634
  }
2851
- function fromVec2(from, dist) {
2852
- switch (from) {
2853
- case "right":
2854
- return [dist, 0];
2855
- case "top":
2856
- return [0, -dist];
2857
- case "bottom":
2858
- return [0, dist];
2859
- default:
2860
- return [-dist, 0];
2635
+ function gait(c, run) {
2636
+ const stride = (run ? 34 : 24) + (run ? 40 : 30) * c.e + c.jit(3);
2637
+ const lift = run ? 40 : 26;
2638
+ const armSwing = (run ? 26 : 16) + 20 * c.e;
2639
+ const halfDur = (run ? 0.26 : 0.42) + c.jit(0.02);
2640
+ const lean = run ? c.facing * -6 : 0;
2641
+ const steps = c.cycles * 2;
2642
+ const d = dur2(halfDur, c.sp);
2643
+ const intro = dur2(0.16, c.sp);
2644
+ const keys = [];
2645
+ for (let k = 0; k <= steps; k++) {
2646
+ const pose = { ...gaitPose(k / 2, stride, lift, armSwing, c.facing), chest: lean };
2647
+ keys.push(poseTo(c.g, pose, { duration: k === 0 ? intro : d, ease: k === 0 ? "easeOutQuad" : "linear" }));
2648
+ }
2649
+ const total = intro + steps * d;
2650
+ const travel = c.travel ?? stride * 2;
2651
+ const children = [seq(...keys)];
2652
+ if (travel !== 0) {
2653
+ children.push(tween(c.g, { x: c.at[0] + c.facing * travel * c.cycles }, { duration: total, ease: "linear", label: "travel" }));
2861
2654
  }
2655
+ return beat(run ? "run" : "walk", {}, [par(...children)]);
2862
2656
  }
2863
- var motionOpLabel = (name, target) => `op-${name}-${target}`;
2864
- function motionOp(name, target, opts = {}) {
2865
- const e = clamp014(opts.energy ?? 0.5);
2866
- const sp = Math.max(0.25, opts.speed ?? 1);
2867
- const amt = opts.amount ?? 1;
2868
- const b = { scale: 1, x: 0, y: 0, rotation: 0, ...opts.base };
2869
- const d = (base) => base / sp;
2870
- const label = motionOpLabel(name, target);
2871
- switch (name) {
2872
- case "rotate":
2873
- return { timeline: beat(label, {}, [tween(target, { rotation: b.rotation + 360 * amt }, { duration: d(1), ease: settleEase2(e) })]) };
2874
- case "zoom": {
2875
- const peak = b.scale * (1 + 0.22 * amt);
2876
- return {
2877
- timeline: beat(label, {}, [
2878
- seq(
2879
- tween(target, { scale: peak }, { duration: d(0.4), ease: "easeOutBack" }),
2880
- tween(target, { scale: b.scale }, { duration: d(0.45), ease: "easeInOutQuad" })
2881
- )
2882
- ])
2883
- };
2884
- }
2885
- case "ken-burns":
2886
- return {
2887
- timeline: beat(label, {}, [
2888
- par(
2889
- tween(target, { scale: b.scale * (1 + 0.1 * amt) }, { duration: d(3), ease: "easeInOutQuad" }),
2890
- tween(target, { x: b.x + 26 * amt, y: b.y - 16 * amt }, { duration: d(3), ease: "easeInOutQuad" })
2891
- )
2892
- ])
2893
- };
2894
- case "slide-in": {
2895
- const [dx, dy] = fromVec2(opts.from ?? "left", 320 * amt);
2896
- return {
2897
- setup: { [target]: { x: b.x + dx, y: b.y + dy, opacity: 0 } },
2898
- timeline: beat(label, {}, [
2899
- par(
2900
- tween(target, { x: b.x, y: b.y }, { duration: d(0.7), ease: settleEase2(e) }),
2901
- tween(target, { opacity: 1 }, { duration: d(0.4), ease: "easeOutQuad" })
2902
- )
2903
- ])
2904
- };
2905
- }
2906
- case "fade":
2907
- return {
2908
- setup: { [target]: { opacity: 0 } },
2909
- timeline: beat(label, {}, [tween(target, { opacity: 1 }, { duration: d(0.6), ease: "easeOutQuad" })])
2910
- };
2911
- case "draw-on":
2912
- return {
2913
- setup: { [target]: { progress: 0 } },
2914
- timeline: beat(label, {}, [tween(target, { progress: 1 }, { duration: d(1.3), ease: "easeInOutQuad" })])
2915
- };
2916
- case "pulse": {
2917
- const hi = b.scale * (1 + 0.12 * amt);
2918
- const pulses = 2 + Math.round(amt);
2919
- const steps = [];
2920
- for (let i = 0; i < pulses; i++) {
2921
- steps.push(tween(target, { scale: hi }, { duration: d(0.22), ease: "easeOutQuad" }));
2922
- steps.push(tween(target, { scale: b.scale }, { duration: d(0.22), ease: "easeInQuad" }));
2923
- }
2924
- return { timeline: beat(label, {}, [seq(...steps)]) };
2925
- }
2657
+ function jumpBeat(c) {
2658
+ const h = 120 + 150 * c.e;
2659
+ const [y0] = [c.at[1]];
2660
+ const CROUCH = { legUpperL: 18, legLowerL: 54, legUpperR: -18, legLowerR: 54, armUpperL: 28, armUpperR: -28 };
2661
+ const LAUNCH = { legUpperL: 0, legLowerL: 0, legUpperR: 0, legLowerR: 0, armUpperL: 150, armUpperR: -150 };
2662
+ const TUCK = { legUpperL: -28, legLowerL: 66, legUpperR: -28, legLowerR: 66, armUpperL: 124, armUpperR: -124 };
2663
+ const REST = { legUpperL: 3, legLowerL: -2, legUpperR: -3, legLowerR: 2, armUpperL: 10, armLowerL: 8, armUpperR: -10, armLowerR: -8 };
2664
+ const j = c.jit(0.03);
2665
+ return beat("jump", {}, [
2666
+ seq(
2667
+ 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" })),
2668
+ 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" })),
2669
+ poseTo(c.g, TUCK, { duration: dur2(0.22, c.sp) }),
2670
+ 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" })),
2671
+ 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" }))
2672
+ )
2673
+ ]);
2674
+ }
2675
+ function danceBeat(c) {
2676
+ const y0 = c.at[1];
2677
+ const sway = 8 + 6 * c.e;
2678
+ const armUp = 130 + 30 * c.e;
2679
+ const A = { chest: sway, head: -sway * 0.5, armUpperR: -armUp, armLowerR: -20, armUpperL: 40, armLowerL: 30, legUpperL: 8, legUpperR: -2 };
2680
+ const B = { chest: -sway, head: sway * 0.5, armUpperL: armUp, armLowerL: 20, armUpperR: -40, armLowerR: -30, legUpperL: 2, legUpperR: -8 };
2681
+ const d = dur2(0.34, c.sp);
2682
+ const keys = [];
2683
+ for (let k = 0; k < c.cycles * 2; k++) {
2684
+ const pose = k % 2 === 0 ? A : B;
2685
+ keys.push(par(
2686
+ poseTo(c.g, pose, { duration: d, ease: "easeInOutQuad" }),
2687
+ tween(c.g, { y: y0 - (k % 2 === 0 ? 14 : 0) }, { duration: d, ease: "easeInOutQuad" })
2688
+ ));
2926
2689
  }
2690
+ keys.push(tween(c.g, { y: y0 }, { duration: d }));
2691
+ return beat("dance", {}, [seq(...keys)]);
2927
2692
  }
2928
-
2929
- // ../core/src/audio.ts
2930
- var SFX_DURATION = {
2931
- // transition
2932
- whoosh: 0.35,
2933
- swish: 0.32,
2934
- swoosh: 0.35,
2935
- rise: 0.5,
2936
- riser: 0.85,
2937
- warp: 0.5,
2938
- // ui
2939
- tick: 0.03,
2940
- click: 0.05,
2941
- blip: 0.1,
2942
- pop: 0.12,
2943
- select: 0.18,
2944
- // impact
2945
- thud: 0.25,
2946
- boom: 0.6,
2947
- knock: 0.14,
2948
- sub: 0.7,
2949
- // positive
2950
- chime: 0.7,
2951
- ding: 0.5,
2952
- coin: 0.3,
2953
- sparkle: 0.6,
2954
- shimmer: 0.9,
2955
- success: 0.6,
2956
- // alert
2957
- zap: 0.22,
2958
- error: 0.4,
2959
- // tech
2960
- glitch: 0.3,
2961
- static: 0.18,
2962
- scan: 0.45,
2963
- powerup: 0.4,
2964
- powerdown: 0.5,
2965
- // rhythm / foley
2966
- snare: 0.18,
2967
- hat: 0.05,
2968
- bubble: 0.16,
2969
- notify: 0.45,
2970
- camera: 0.18
2971
- };
2972
- var FILE_CUE_DURATION = 0.4;
2973
- function collectClipAudio(ir, duration, warnings) {
2974
- const out = [];
2975
- const walk = (nodes) => {
2976
- for (const node of nodes) {
2977
- if (node.type === "video") {
2978
- const gain = node.props.volume ?? 1;
2979
- const start = node.props.start ?? 0;
2980
- if (gain <= 0) continue;
2981
- if (start >= duration) {
2982
- warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
2983
- continue;
2984
- }
2985
- out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain, fadeIn: node.props.fadeIn ?? 0, pan: node.props.pan ?? 0 });
2986
- }
2987
- if (node.type === "group") walk(node.children);
2988
- }
2989
- };
2990
- walk(ir.nodes);
2991
- return out;
2693
+ function waveBeat(c) {
2694
+ const n3 = 3 + Math.round(c.rand() * 2);
2695
+ const amp = 16 + 10 * c.e;
2696
+ const steps = [poseTo(c.g, { armUpperR: -150, armLowerR: -24 }, { duration: dur2(0.4, c.sp), ease: "easeOutBack" })];
2697
+ for (let k = 0; k < n3; k++) {
2698
+ steps.push(poseTo(c.g, { armLowerR: -24 + (k % 2 === 0 ? amp : -amp) }, { duration: dur2(0.22, c.sp), ease: "easeInOutQuad" }));
2699
+ }
2700
+ steps.push(poseTo(c.g, { armUpperR: -10, armLowerR: -8 }, { duration: dur2(0.4, c.sp), ease: "easeInOutCubic" }));
2701
+ return beat("wave", {}, [seq(...steps)]);
2992
2702
  }
2993
- function resolveAudioPlan(compiled) {
2994
- const audio = compiled.ir.audio;
2995
- const warnings = [];
2996
- const duration = compiled.duration;
2997
- const clipAudio = collectClipAudio(compiled.ir, duration, warnings);
2998
- if (!audio || !audio.bgm && (audio.cues ?? []).length === 0) {
2999
- return clipAudio.length === 0 ? null : { duration, bgm: null, cues: [], duckWindows: [], clipAudio, warnings };
2703
+ function cheerBeat(c) {
2704
+ const y0 = c.at[1];
2705
+ const UP = { armUpperL: 152, armLowerL: 8, armUpperR: -152, armLowerR: -8 };
2706
+ const d = dur2(0.3, c.sp);
2707
+ const keys = [poseTo(c.g, UP, { duration: dur2(0.35, c.sp), ease: "easeOutBack" })];
2708
+ for (let k = 0; k < c.cycles; k++) {
2709
+ keys.push(par(tween(c.g, { y: y0 - 28 }, { duration: d, ease: "easeOutQuad" }), poseTo(c.g, { armUpperL: 160, armUpperR: -160 }, { duration: d })));
2710
+ keys.push(par(tween(c.g, { y: y0 }, { duration: d, ease: "easeInQuad" }), poseTo(c.g, { armUpperL: 145, armUpperR: -145 }, { duration: d })));
3000
2711
  }
3001
- const cues = [];
3002
- for (const [index, cue] of (audio.cues ?? []).entries()) {
3003
- let anchor;
3004
- if (typeof cue.at === "number") {
3005
- anchor = cue.at;
3006
- } else {
3007
- const span = compiled.labelTimes.get(cue.at);
3008
- if (!span) {
3009
- warnings.push(`cue[${index}]: unknown label "${cue.at}" \u2014 cue dropped`);
3010
- continue;
3011
- }
3012
- anchor = span.t0;
3013
- }
3014
- const t = Math.max(0, anchor + (cue.offset ?? 0));
3015
- const cueDuration = cue.sfx ? SFX_DURATION[cue.sfx] : FILE_CUE_DURATION;
3016
- if (t >= duration) {
3017
- warnings.push(`cue[${index}] at ${t.toFixed(2)}s starts past the scene end (${duration.toFixed(2)}s) \u2014 dropped`);
3018
- continue;
3019
- }
3020
- if (t + cueDuration > duration) {
3021
- warnings.push(`cue[${index}] at ${t.toFixed(2)}s extends past the scene end \u2014 it will be truncated`);
2712
+ return beat("cheer", {}, [seq(...keys)]);
2713
+ }
2714
+ function characterPreset(name, opts) {
2715
+ const c = ctx2(opts);
2716
+ let tl;
2717
+ switch (name) {
2718
+ case "walk":
2719
+ tl = gait(c, false);
2720
+ break;
2721
+ case "run":
2722
+ tl = gait(c, true);
2723
+ break;
2724
+ case "jump":
2725
+ tl = jumpBeat(c);
2726
+ break;
2727
+ case "dance":
2728
+ tl = danceBeat(c);
2729
+ break;
2730
+ case "wave":
2731
+ tl = waveBeat(c);
2732
+ break;
2733
+ case "cheer":
2734
+ tl = cheerBeat(c);
2735
+ break;
2736
+ default: {
2737
+ const _exhaustive = name;
2738
+ throw new Error(`unknown characterPreset "${_exhaustive}"`);
3022
2739
  }
3023
- cues.push({
3024
- t,
3025
- gain: cue.gain ?? 1,
3026
- duration: cueDuration,
3027
- fadeIn: cue.fadeIn ?? 0,
3028
- fadeOut: cue.fadeOut ?? 0,
3029
- pan: cue.pan ?? 0,
3030
- source: cue.sfx ? (
3031
- // auto-vary: default the seed to the cue's order so repeated sfx differ
3032
- // (pitch/texture); an explicit params.seed always wins.
3033
- { kind: "sfx", name: cue.sfx, params: { seed: index, ...cue.params } }
3034
- ) : { kind: "file", path: cue.file }
3035
- });
3036
2740
  }
3037
- cues.sort((a, b) => a.t - b.t);
3038
- return {
3039
- duration,
3040
- bgm: resolveBgm(audio.bgm),
3041
- cues,
3042
- duckWindows: mergeDuckWindows(cues, duration),
3043
- clipAudio,
3044
- warnings
3045
- };
2741
+ return c.label && tl.kind === "beat" ? { ...tl, name: c.label } : tl;
3046
2742
  }
3047
- function mergeDuckWindows(cues, duration) {
3048
- const duckWindows = [];
3049
- for (const cue of cues) {
3050
- const window = { t0: cue.t, t1: Math.min(duration, cue.t + cue.duration) };
3051
- const last = duckWindows[duckWindows.length - 1];
3052
- if (last && window.t0 <= last.t1 + 0.1) last.t1 = Math.max(last.t1, window.t1);
3053
- else duckWindows.push(window);
3054
- }
3055
- return duckWindows;
2743
+
2744
+ // ../core/src/figure.ts
2745
+ var K2 = 0.5523;
2746
+ var n2 = (v) => Number(v.toFixed(2));
2747
+ function limb(a, b, y0, y1) {
2748
+ const ka = n2(a * K2), kb = n2(b * K2);
2749
+ 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`;
3056
2750
  }
3057
- function resolveBgm(b) {
3058
- if (!b) return null;
3059
- const duck = b.duck === false ? null : {
3060
- depth: b.duck?.depth ?? 0.5,
3061
- attack: b.duck?.attack ?? 0.05,
3062
- release: b.duck?.release ?? 0.25
2751
+ function rrect(a, b, y0, y1, r) {
2752
+ 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`;
2753
+ }
2754
+ function darken(hex, f) {
2755
+ const h = hex.replace("#", "");
2756
+ const v = parseInt(h.length === 3 ? [...h].map((c) => c + c).join("") : h, 16);
2757
+ const ch = (s) => Math.max(0, Math.min(255, Math.round((v >> s & 255) * (1 - f))));
2758
+ const hx = (x) => x.toString(16).padStart(2, "0");
2759
+ return `#${hx(ch(16))}${hx(ch(8))}${hx(ch(0))}`;
2760
+ }
2761
+ var DEF = {
2762
+ clean: { skin: "#E9B58E", hair: "#2B313F", top: "#E86C4A", pants: "#39425C", shoe: "#20242F", accent: "#E86C4A" },
2763
+ cute: { skin: "#FFD2A6", hair: "#5B4636", top: "#FF7E5F", pants: "#3E6F8E", shoe: "#272B38", accent: "#FF7E5F" }
2764
+ };
2765
+ function resolvePal(style, p = {}) {
2766
+ const d = DEF[style];
2767
+ const accent = p.accent ?? d.accent;
2768
+ const top = p.top ?? (style === "clean" ? accent : d.top);
2769
+ const skin = p.skin ?? d.skin;
2770
+ const hair = p.hair ?? d.hair;
2771
+ const pants = p.pants ?? d.pants;
2772
+ const shoe = p.shoe ?? d.shoe;
2773
+ return {
2774
+ skin,
2775
+ skinSh: darken(skin, 0.12),
2776
+ hair,
2777
+ hairSh: darken(hair, 0.14),
2778
+ top,
2779
+ topSh: darken(top, 0.12),
2780
+ pants,
2781
+ pantsSh: darken(pants, 0.14),
2782
+ shoe,
2783
+ shoeSh: darken(shoe, 0.22),
2784
+ eye: "#2B313F",
2785
+ cheek: "#FF9E7E",
2786
+ white: "#FFFFFF",
2787
+ mouth: "#8A4233"
3063
2788
  };
2789
+ }
2790
+ 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 } : {} });
2791
+ function cleanParts(p, face) {
2792
+ const HC = -42;
3064
2793
  return {
3065
- source: b.file ? { kind: "file", path: b.file } : { kind: "synth", name: b.synth ?? "ambient-pad" },
3066
- gain: b.gain ?? 0.5,
3067
- fadeIn: b.fadeIn ?? 0,
3068
- fadeOut: b.fadeOut ?? 0,
3069
- duck
2794
+ upperArm: (j) => [fp(`${j}-sleeve`, limb(12, 10, 2, 58), p.top)],
2795
+ forearm: (j) => [
2796
+ fp(`${j}-elbow`, ovalPath(10, 10, 0, 3), p.skin),
2797
+ fp(`${j}-fore`, limb(10, 8, 2, 48), p.skin),
2798
+ fp(`${j}-hand`, ovalPath(11, 12, 0, 50), p.skin)
2799
+ ],
2800
+ thigh: (j) => [fp(`${j}-thigh`, limb(15, 13, 2, 72), p.pants)],
2801
+ shin: (j) => [
2802
+ fp(`${j}-knee`, ovalPath(13, 13, 0, 2), p.pants),
2803
+ fp(`${j}-shin`, limb(13, 11, 2, 62), p.pants),
2804
+ fp(`${j}-shoe`, ovalPath(15, 9, 4, 67), p.shoe)
2805
+ ],
2806
+ torso: (j) => [
2807
+ fp(`${j}-shadow`, rrect(38, 26, -28, 52, 20), p.topSh),
2808
+ fp(`${j}-top`, rrect(40, 27, -30, 52, 22), p.top),
2809
+ fp(`${j}-pelvis`, rrect(29, 24, 46, 104, 14), p.pants)
2810
+ ],
2811
+ head: (j) => [
2812
+ fp(`${j}-neck`, rrect(9, 9, 2, 22, 5), p.skin),
2813
+ fp(`${j}-skin`, ovalPath(42, 46, 0, HC), p.skin),
2814
+ fp(`${j}-hair`, ovalPath(44, 27, 0, HC - 31), p.hair),
2815
+ fp(`${j}-hairL`, ovalPath(8, 14, -39, HC - 18), p.hair),
2816
+ fp(`${j}-hairR`, ovalPath(8, 14, 39, HC - 18), p.hair),
2817
+ ...face ? [fp(`${j}-eyeL`, ovalPath(5, 7, -14, HC + 2), p.eye), fp(`${j}-eyeR`, ovalPath(5, 7, 14, HC + 2), p.eye)] : []
2818
+ ]
3070
2819
  };
3071
2820
  }
3072
- function resolveCompositionAudioPlan(comp) {
3073
- const audio = comp.ir.audio;
3074
- const duration = comp.duration;
3075
- const warnings = [];
3076
- const cues = [];
3077
- const clipAudio = [];
3078
- for (const placement of comp.scenes) {
3079
- const plan = resolveAudioPlan(placement.compiled);
3080
- if (!plan) continue;
3081
- if (plan.bgm) {
3082
- warnings.push(`scene "${placement.id}": per-scene bgm ignored \u2014 set bgm at the composition level`);
3083
- }
3084
- for (const w of plan.warnings) warnings.push(`scene "${placement.id}": ${w}`);
3085
- for (const cue of plan.cues) {
3086
- const t = cue.t + placement.start;
3087
- if (t >= duration) continue;
3088
- cues.push({ ...cue, t });
3089
- }
3090
- for (const clip of plan.clipAudio) {
3091
- const start = clip.start + placement.start;
3092
- if (start >= duration) continue;
3093
- clipAudio.push({ ...clip, start });
3094
- }
3095
- }
3096
- for (const [index, cue] of (audio?.cues ?? []).entries()) {
3097
- if (typeof cue.at !== "number") {
3098
- warnings.push(`composition cue[${index}]: "at" must be an absolute number (no composition labels) \u2014 dropped`);
3099
- continue;
3100
- }
3101
- const t = Math.max(0, cue.at + (cue.offset ?? 0));
3102
- const cueDuration = cue.sfx ? SFX_DURATION[cue.sfx] : FILE_CUE_DURATION;
3103
- if (t >= duration) {
3104
- warnings.push(`composition cue[${index}] at ${t.toFixed(2)}s past the composition end \u2014 dropped`);
3105
- continue;
3106
- }
3107
- cues.push({
3108
- t,
3109
- gain: cue.gain ?? 1,
3110
- duration: cueDuration,
3111
- fadeIn: cue.fadeIn ?? 0,
3112
- fadeOut: cue.fadeOut ?? 0,
3113
- pan: cue.pan ?? 0,
3114
- source: cue.sfx ? (
3115
- // auto-vary: default the seed to the cue's order so repeated sfx differ
3116
- // (pitch/texture); an explicit params.seed always wins.
3117
- { kind: "sfx", name: cue.sfx, params: { seed: index, ...cue.params } }
3118
- ) : { kind: "file", path: cue.file }
3119
- });
3120
- }
3121
- if (!audio?.bgm && cues.length === 0 && clipAudio.length === 0) return null;
3122
- cues.sort((a, b) => a.t - b.t);
2821
+ 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";
2822
+ function cuteParts(p, face) {
2823
+ const HC = -50;
3123
2824
  return {
3124
- duration,
3125
- bgm: resolveBgm(audio?.bgm),
3126
- cues,
3127
- duckWindows: mergeDuckWindows(cues, duration),
3128
- clipAudio,
3129
- warnings
2825
+ upperArm: (j) => [fp(`${j}-sleeve`, limb(15, 13, 2, 58), p.top, p.topSh, 2.5)],
2826
+ forearm: (j) => [
2827
+ fp(`${j}-elbow`, ovalPath(12, 12, 0, 3), p.skin, p.skinSh, 2.5),
2828
+ fp(`${j}-fore`, limb(12, 10, 2, 46), p.skin, p.skinSh, 2.5),
2829
+ fp(`${j}-hand`, ovalPath(13, 14, 0, 50), p.skin, p.skinSh, 2.5)
2830
+ ],
2831
+ thigh: (j) => [fp(`${j}-thigh`, limb(19, 16, 2, 72), p.pants, p.pantsSh, 2.5)],
2832
+ shin: (j) => [
2833
+ fp(`${j}-knee`, ovalPath(16, 16, 0, 2), p.pants, p.pantsSh, 2.5),
2834
+ fp(`${j}-shin`, limb(15, 12, 2, 60), p.pants, p.pantsSh, 2.5),
2835
+ fp(`${j}-shoe`, ovalPath(18, 11, 5, 66), p.shoe, darken(p.shoe, 0.25), 2.5)
2836
+ ],
2837
+ torso: (j) => [
2838
+ fp(`${j}-shadow`, rrect(40, 34, -18, 52, 16), p.topSh),
2839
+ fp(`${j}-top`, rrect(42, 35, -20, 52, 18), p.top, p.topSh, 2.5),
2840
+ fp(`${j}-pelvis`, rrect(36, 30, 46, 104, 16), p.pants, p.pantsSh, 2.5)
2841
+ ],
2842
+ head: (j) => [
2843
+ fp(`${j}-neck`, ovalPath(15, 12, 0, 12), p.skinSh),
2844
+ fp(`${j}-skin`, ovalPath(42, 46, 0, HC + 4), p.skin, p.skinSh, 2.5),
2845
+ fp(`${j}-cheekL`, ovalPath(11, 8, -40, HC + 22), p.cheek, void 0, 0, 0.5),
2846
+ fp(`${j}-cheekR`, ovalPath(11, 8, 40, HC + 22), p.cheek, void 0, 0, 0.5),
2847
+ fp(`${j}-hair`, CUTE_HAIR, p.hair, p.hairSh, 2),
2848
+ ...face ? [
2849
+ fp(`${j}-eyeL`, ovalPath(10, 13, -25, HC + 8), p.eye),
2850
+ fp(`${j}-eyeR`, ovalPath(10, 13, 25, HC + 8), p.eye),
2851
+ fp(`${j}-glL`, ovalPath(3.4, 3.4, -28, HC + 3), p.white),
2852
+ fp(`${j}-glR`, ovalPath(3.4, 3.4, 22, HC + 3), p.white),
2853
+ 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 })
2854
+ ] : []
2855
+ ]
3130
2856
  };
3131
2857
  }
3132
-
3133
- // ../core/src/behaviors.ts
3134
- function sampleBehavior(b, t) {
3135
- switch (b.name) {
3136
- case "oscillate": {
3137
- const { amplitude, frequency, phase = 0 } = b.params;
3138
- return amplitude * Math.sin(2 * Math.PI * frequency * t + phase);
3139
- }
3140
- case "wiggle": {
3141
- const { amplitude, frequency, seed } = b.params;
3142
- return amplitude * valueNoise(t * frequency, seed);
3143
- }
3144
- }
3145
- }
3146
- function valueNoise(x, seed) {
3147
- const i = Math.floor(x);
3148
- const f = x - i;
3149
- const u = f * f * (3 - 2 * f);
3150
- const a = hash01(i, seed) * 2 - 1;
3151
- const b = hash01(i + 1, seed) * 2 - 1;
3152
- return a + (b - a) * u;
2858
+ function buildSkeleton(id, S) {
2859
+ const arm = (side, x, r1, r2) => ({
2860
+ name: `armUpper${side}`,
2861
+ at: [x, -14],
2862
+ length: 60,
2863
+ width: 0,
2864
+ rotation: r1,
2865
+ shape: S.upperArm(`${id}-armUpper${side}`),
2866
+ children: [{ name: `armLower${side}`, at: [0, 60], length: 56, width: 0, rotation: r2, shape: S.forearm(`${id}-armLower${side}`) }]
2867
+ });
2868
+ const leg = (side, x, r1, r2) => ({
2869
+ name: `legUpper${side}`,
2870
+ at: [x, 76],
2871
+ length: 76,
2872
+ width: 0,
2873
+ rotation: r1,
2874
+ shape: S.thigh(`${id}-legUpper${side}`),
2875
+ children: [{ name: `legLower${side}`, at: [0, 76], length: 72, width: 0, rotation: r2, shape: S.shin(`${id}-legLower${side}`) }]
2876
+ });
2877
+ return {
2878
+ name: "chest",
2879
+ at: [0, 0],
2880
+ shape: S.torso(`${id}-chest`),
2881
+ children: [
2882
+ { name: "head", at: [0, -42], rotation: 0, shape: S.head(`${id}-head`) },
2883
+ arm("L", -40, 8, 6),
2884
+ arm("R", 40, -8, -6),
2885
+ leg("L", -20, 3, -2),
2886
+ leg("R", 20, -3, 2)
2887
+ ]
2888
+ };
3153
2889
  }
3154
- function hash01(n3, seed) {
3155
- let h = n3 * 374761393 + seed * 668265263 | 0;
3156
- h = h ^ h >>> 13 | 0;
3157
- h = Math.imul(h, 1274126177);
3158
- h = (h ^ h >>> 16) >>> 0;
3159
- return h / 4294967295;
2890
+ function figure(opts = {}) {
2891
+ const style = opts.style ?? "clean";
2892
+ const pal = resolvePal(style, opts.palette);
2893
+ const face = opts.face ?? true;
2894
+ const id = opts.id ?? "figure";
2895
+ const parts = style === "clean" ? cleanParts(pal, face) : cuteParts(pal, face);
2896
+ const rigOpts = { id };
2897
+ if (opts.x !== void 0) rigOpts.x = opts.x;
2898
+ if (opts.y !== void 0) rigOpts.y = opts.y;
2899
+ if (opts.scale !== void 0) rigOpts.scale = opts.scale;
2900
+ if (opts.opacity !== void 0) rigOpts.opacity = opts.opacity;
2901
+ return rig(buildSkeleton(id, parts), rigOpts);
3160
2902
  }
3161
2903
 
3162
- // ../core/src/evaluate.ts
3163
- var IDENTITY = [1, 0, 0, 1, 0, 0];
3164
- function multiply(m, n3) {
3165
- return [
3166
- m[0] * n3[0] + m[2] * n3[1],
3167
- m[1] * n3[0] + m[3] * n3[1],
3168
- m[0] * n3[2] + m[2] * n3[3],
3169
- m[1] * n3[2] + m[3] * n3[3],
3170
- m[0] * n3[4] + m[2] * n3[5] + m[4],
3171
- m[1] * n3[4] + m[3] * n3[5] + m[5]
3172
- ];
3173
- }
3174
- var DEG = Math.PI / 180;
3175
- var z0 = (x) => x === 0 ? 0 : x;
3176
- function projectDepth(m, z, vx, vy, d) {
3177
- if (z === 0) return m;
3178
- const p = d + z > 0 ? d / (d + z) : 1e-6;
3179
- return [
3180
- z0(m[0] * p),
3181
- z0(m[1] * p),
3182
- z0(m[2] * p),
3183
- z0(m[3] * p),
3184
- z0(vx + (m[4] - vx) * p),
3185
- z0(vy + (m[5] - vy) * p)
3186
- ];
3187
- }
3188
- function tiltSkew(m, rotXdeg, rotYdeg, hw, hh, d) {
3189
- const ky = Math.sin(rotYdeg * DEG) * hw / d;
3190
- const kx = Math.sin(rotXdeg * DEG) * hh / d;
3191
- if (ky === 0 && kx === 0) return m;
3192
- return multiply(m, [1, kx, ky, 1, 0, 0]);
3193
- }
3194
- function localMatrix(x, y, rotationDeg, scale, scaleX = 1, scaleY = 1, skewXDeg = 0, skewYDeg = 0) {
3195
- const r = rotationDeg * Math.PI / 180;
3196
- if (scaleX === 1 && scaleY === 1 && skewXDeg === 0 && skewYDeg === 0) {
3197
- const cos = Math.cos(r) * scale;
3198
- const sin = Math.sin(r) * scale;
3199
- return [cos, sin, -sin, cos, x, y];
3200
- }
3201
- const c = Math.cos(r);
3202
- const s = Math.sin(r);
3203
- const tx = Math.tan(skewXDeg * Math.PI / 180);
3204
- const ty = Math.tan(skewYDeg * Math.PI / 180);
3205
- const R = [c, s, -s, c, 0, 0];
3206
- const K3 = [1, ty, tx, 1, 0, 0];
3207
- const S = [scale * scaleX, 0, 0, scale * scaleY, 0, 0];
3208
- const m = multiply(R, multiply(K3, S));
3209
- return [m[0], m[1], m[2], m[3], x, y];
3210
- }
3211
- var ANCHOR_FACTORS = {
3212
- "top-left": [0, 0],
3213
- "top-center": [0.5, 0],
3214
- "top-right": [1, 0],
3215
- "center-left": [0, 0.5],
3216
- center: [0.5, 0.5],
3217
- "center-right": [1, 0.5],
3218
- "bottom-left": [0, 1],
3219
- "bottom-center": [0.5, 1],
3220
- "bottom-right": [1, 1]
2904
+ // ../core/src/textMetrics.ts
2905
+ var INTER_ADVANCE = {
2906
+ "400": {
2907
+ "0": 63.09,
2908
+ "1": 40.67,
2909
+ "2": 60.99,
2910
+ "3": 61.77,
2911
+ "4": 64.6,
2912
+ "5": 59.33,
2913
+ "6": 62.01,
2914
+ "7": 56.59,
2915
+ "8": 61.87,
2916
+ "9": 62.01,
2917
+ " ": 28.13,
2918
+ "!": 28.76,
2919
+ '"': 46.58,
2920
+ "#": 63.33,
2921
+ "$": 64.16,
2922
+ "%": 98.19,
2923
+ "&": 64.4,
2924
+ "'": 29.98,
2925
+ "(": 36.47,
2926
+ ")": 36.47,
2927
+ "*": 50.1,
2928
+ "+": 66.16,
2929
+ ",": 28.81,
2930
+ "-": 46,
2931
+ ".": 28.81,
2932
+ "/": 36.04,
2933
+ ":": 28.81,
2934
+ ";": 30.18,
2935
+ "<": 66.16,
2936
+ "=": 66.16,
2937
+ ">": 66.16,
2938
+ "?": 51.12,
2939
+ "@": 96.58,
2940
+ "A": 68.99,
2941
+ "B": 65.43,
2942
+ "C": 73.05,
2943
+ "D": 72.17,
2944
+ "E": 60.11,
2945
+ "F": 59.03,
2946
+ "G": 74.61,
2947
+ "H": 74.32,
2948
+ "I": 26.86,
2949
+ "J": 57.08,
2950
+ "K": 67.19,
2951
+ "L": 56.54,
2952
+ "M": 90.33,
2953
+ "N": 75.34,
2954
+ "O": 76.46,
2955
+ "P": 63.87,
2956
+ "Q": 76.46,
2957
+ "R": 64.36,
2958
+ "S": 64.16,
2959
+ "T": 64.55,
2960
+ "U": 74.41,
2961
+ "V": 68.99,
2962
+ "W": 98.54,
2963
+ "X": 68.21,
2964
+ "Y": 67.87,
2965
+ "Z": 62.89,
2966
+ "[": 36.47,
2967
+ "\\": 36.04,
2968
+ "]": 36.47,
2969
+ "^": 47.12,
2970
+ "_": 45.61,
2971
+ "`": 32.28,
2972
+ "a": 56.15,
2973
+ "b": 61.23,
2974
+ "c": 57.13,
2975
+ "d": 61.23,
2976
+ "e": 58.3,
2977
+ "f": 37.01,
2978
+ "g": 61.33,
2979
+ "h": 59.13,
2980
+ "i": 24.22,
2981
+ "j": 24.22,
2982
+ "k": 54.88,
2983
+ "l": 24.22,
2984
+ "m": 87.6,
2985
+ "n": 59.08,
2986
+ "o": 59.96,
2987
+ "p": 61.23,
2988
+ "q": 61.23,
2989
+ "r": 37.65,
2990
+ "s": 52.78,
2991
+ "t": 32.71,
2992
+ "u": 59.13,
2993
+ "v": 56.2,
2994
+ "w": 81.84,
2995
+ "x": 54.59,
2996
+ "y": 56.2,
2997
+ "z": 55.22,
2998
+ "{": 42.63,
2999
+ "|": 33.25,
3000
+ "}": 42.63,
3001
+ "~": 66.16
3002
+ },
3003
+ "700": {
3004
+ "0": 67.43,
3005
+ "1": 43.12,
3006
+ "2": 62.94,
3007
+ "3": 64.55,
3008
+ "4": 67.63,
3009
+ "5": 62.21,
3010
+ "6": 64.94,
3011
+ "7": 58.15,
3012
+ "8": 65.09,
3013
+ "9": 64.94,
3014
+ " ": 23.68,
3015
+ "!": 33.79,
3016
+ '"': 55.13,
3017
+ "#": 64.89,
3018
+ "$": 65.48,
3019
+ "%": 101.56,
3020
+ "&": 67.19,
3021
+ "'": 33.89,
3022
+ "(": 37.7,
3023
+ ")": 37.7,
3024
+ "*": 55.91,
3025
+ "+": 67.87,
3026
+ ",": 33.4,
3027
+ "-": 46.78,
3028
+ ".": 33.4,
3029
+ "/": 38.82,
3030
+ ":": 33.4,
3031
+ ";": 34.28,
3032
+ "<": 67.87,
3033
+ "=": 67.87,
3034
+ ">": 67.87,
3035
+ "?": 55.96,
3036
+ "@": 101.61,
3037
+ "A": 74.66,
3038
+ "B": 66.16,
3039
+ "C": 73.97,
3040
+ "D": 72.22,
3041
+ "E": 60.74,
3042
+ "F": 58.69,
3043
+ "G": 75.05,
3044
+ "H": 74.71,
3045
+ "I": 28.08,
3046
+ "J": 58.45,
3047
+ "K": 71.92,
3048
+ "L": 56.54,
3049
+ "M": 93.16,
3050
+ "N": 76.22,
3051
+ "O": 77.05,
3052
+ "P": 64.79,
3053
+ "Q": 77.69,
3054
+ "R": 65.67,
3055
+ "S": 65.48,
3056
+ "T": 66.75,
3057
+ "U": 73.19,
3058
+ "V": 74.66,
3059
+ "W": 103.76,
3060
+ "X": 73.83,
3061
+ "Y": 73.1,
3062
+ "Z": 66.41,
3063
+ "[": 37.7,
3064
+ "\\": 38.82,
3065
+ "]": 37.7,
3066
+ "^": 48.68,
3067
+ "_": 47.61,
3068
+ "`": 36.52,
3069
+ "a": 58.06,
3070
+ "b": 63.04,
3071
+ "c": 58.84,
3072
+ "d": 63.04,
3073
+ "e": 59.57,
3074
+ "f": 39.79,
3075
+ "g": 63.18,
3076
+ "h": 62.26,
3077
+ "i": 27.1,
3078
+ "j": 27.1,
3079
+ "k": 58.01,
3080
+ "l": 27.1,
3081
+ "m": 91.26,
3082
+ "n": 62.26,
3083
+ "o": 61.33,
3084
+ "p": 63.04,
3085
+ "q": 63.04,
3086
+ "r": 40.72,
3087
+ "s": 56.01,
3088
+ "t": 36.62,
3089
+ "u": 62.26,
3090
+ "v": 59.96,
3091
+ "w": 85.01,
3092
+ "x": 58.01,
3093
+ "y": 60.21,
3094
+ "z": 57.28,
3095
+ "{": 46.88,
3096
+ "|": 37.16,
3097
+ "}": 46.88,
3098
+ "~": 67.87
3099
+ },
3100
+ "800": {
3101
+ "0": 69.19,
3102
+ "1": 44.14,
3103
+ "2": 63.77,
3104
+ "3": 65.67,
3105
+ "4": 68.85,
3106
+ "5": 63.38,
3107
+ "6": 66.16,
3108
+ "7": 58.79,
3109
+ "8": 66.41,
3110
+ "9": 66.16,
3111
+ " ": 21.88,
3112
+ "!": 35.84,
3113
+ '"': 58.64,
3114
+ "#": 65.53,
3115
+ "$": 66.02,
3116
+ "%": 102.93,
3117
+ "&": 68.31,
3118
+ "'": 35.45,
3119
+ "(": 38.23,
3120
+ ")": 38.23,
3121
+ "*": 58.25,
3122
+ "+": 68.55,
3123
+ ",": 35.25,
3124
+ "-": 47.12,
3125
+ ".": 35.25,
3126
+ "/": 39.99,
3127
+ ":": 35.25,
3128
+ ";": 35.99,
3129
+ "<": 68.55,
3130
+ "=": 68.55,
3131
+ ">": 68.55,
3132
+ "?": 57.91,
3133
+ "@": 103.61,
3134
+ "A": 76.95,
3135
+ "B": 66.46,
3136
+ "C": 74.37,
3137
+ "D": 72.27,
3138
+ "E": 60.99,
3139
+ "F": 58.54,
3140
+ "G": 75.24,
3141
+ "H": 74.85,
3142
+ "I": 28.56,
3143
+ "J": 58.98,
3144
+ "K": 73.83,
3145
+ "L": 56.54,
3146
+ "M": 94.34,
3147
+ "N": 76.56,
3148
+ "O": 77.29,
3149
+ "P": 65.19,
3150
+ "Q": 78.17,
3151
+ "R": 66.21,
3152
+ "S": 66.02,
3153
+ "T": 67.68,
3154
+ "U": 72.71,
3155
+ "V": 76.95,
3156
+ "W": 105.86,
3157
+ "X": 76.12,
3158
+ "Y": 75.2,
3159
+ "Z": 67.87,
3160
+ "[": 38.23,
3161
+ "\\": 39.99,
3162
+ "]": 38.23,
3163
+ "^": 49.32,
3164
+ "_": 48.44,
3165
+ "`": 38.23,
3166
+ "a": 58.84,
3167
+ "b": 63.77,
3168
+ "c": 59.52,
3169
+ "d": 63.77,
3170
+ "e": 60.06,
3171
+ "f": 40.97,
3172
+ "g": 63.92,
3173
+ "h": 63.53,
3174
+ "i": 28.32,
3175
+ "j": 28.32,
3176
+ "k": 59.28,
3177
+ "l": 28.32,
3178
+ "m": 92.72,
3179
+ "n": 63.53,
3180
+ "o": 61.91,
3181
+ "p": 63.77,
3182
+ "q": 63.77,
3183
+ "r": 41.99,
3184
+ "s": 57.32,
3185
+ "t": 38.18,
3186
+ "u": 63.53,
3187
+ "v": 61.52,
3188
+ "w": 86.28,
3189
+ "x": 59.42,
3190
+ "y": 61.82,
3191
+ "z": 58.11,
3192
+ "{": 48.63,
3193
+ "|": 38.77,
3194
+ "}": 48.63,
3195
+ "~": 68.55
3196
+ }
3221
3197
  };
3222
- var TEXT_ALIGN = { 0: "left", 0.5: "center", 1: "right" };
3223
- var TEXT_BASELINE = { 0: "top", 0.5: "middle", 1: "bottom" };
3224
- function formatNumber(value, decimals, thousands) {
3225
- const fixed = value.toFixed(decimals);
3226
- if (!thousands) return fixed;
3227
- const neg = fixed.startsWith("-");
3228
- const body = neg ? fixed.slice(1) : fixed;
3229
- const dot = body.indexOf(".");
3230
- const intPart = dot === -1 ? body : body.slice(0, dot);
3231
- const frac = dot === -1 ? "" : body.slice(dot);
3232
- const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
3233
- return (neg ? "-" : "") + grouped + frac;
3234
- }
3235
- function behaviorEnvelope(b, t) {
3236
- const from = b.from ?? Number.NEGATIVE_INFINITY;
3237
- const until = b.until ?? Number.POSITIVE_INFINITY;
3238
- if (t < from || t > until) return 0;
3239
- const ramp = b.ramp ?? 0.2;
3240
- let envelope = 1;
3241
- if (Number.isFinite(from) && ramp > 0) envelope = Math.min(envelope, (t - from) / ramp);
3242
- if (Number.isFinite(until) && ramp > 0) envelope = Math.min(envelope, (until - t) / ramp);
3243
- return Math.max(0, Math.min(1, envelope));
3244
- }
3245
- function sampleProp(compiled, t, target, prop, fallback) {
3246
- let value = compiled.initialValues.get(`${target}.${prop}`) ?? fallback;
3247
- let segStart = Number.NEGATIVE_INFINITY;
3248
- const segs = compiled.segments.get(`${target}.${prop}`);
3249
- if (segs) {
3250
- let active;
3251
- for (const seg of segs) {
3252
- if (seg.t0 <= t) active = seg;
3253
- else break;
3254
- }
3255
- if (active) {
3256
- segStart = active.t0;
3257
- if (t >= active.t1) {
3258
- value = active.to;
3259
- } else {
3260
- const u = resolveEase(active.ease)((t - active.t0) / (active.t1 - active.t0));
3261
- value = lerpValue(active.from, active.to, u);
3198
+ var INTER_FALLBACK = {
3199
+ "400": 56.16,
3200
+ "700": 58.74,
3201
+ "800": 59.79
3202
+ };
3203
+
3204
+ // ../core/src/textFx.ts
3205
+ var clamp013 = (v) => Math.max(0, Math.min(1, v));
3206
+ var fract = (v) => v - Math.floor(v);
3207
+ var rand = (i, salt) => fract(Math.sin(i * 127.1 + salt * 311.7) * 43758.5453);
3208
+ var dur3 = (base, sp) => base / sp;
3209
+ var SCRAMBLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&@";
3210
+ var advance = (ch, weight, fontSize) => (INTER_ADVANCE[weight]?.[ch] ?? INTER_FALLBACK[weight]) * (fontSize / 100);
3211
+ function splitText(textStr, opts) {
3212
+ const { id, x, y, fontSize } = opts;
3213
+ const weight = opts.fontWeight ?? 800;
3214
+ const fill = opts.fill ?? "#FFFFFF";
3215
+ const ls = opts.letterSpacing ?? 0;
3216
+ const align = opts.align ?? "center";
3217
+ const unit = opts.unit ?? "glyph";
3218
+ const opacity = opts.opacity ?? 0;
3219
+ const chars = [...textStr];
3220
+ let total = 0;
3221
+ chars.forEach((ch, i) => {
3222
+ total += advance(ch, weight, fontSize) + (i < chars.length - 1 ? ls : 0);
3223
+ });
3224
+ let cursor2 = align === "center" ? x - total / 2 : x;
3225
+ const glyphs = [];
3226
+ const nodes = [];
3227
+ const mk = (ch, cx, adv, lsProp) => {
3228
+ const g = { id: `${id}-${glyphs.length}`, ch, x: cx, y, advance: adv, i: glyphs.length };
3229
+ glyphs.push(g);
3230
+ nodes.push(
3231
+ text({
3232
+ id: g.id,
3233
+ x: cx,
3234
+ y,
3235
+ content: ch,
3236
+ fontFamily: "Inter",
3237
+ fontSize,
3238
+ fontWeight: weight,
3239
+ fill,
3240
+ anchor: "center",
3241
+ opacity,
3242
+ ...lsProp ? { letterSpacing: lsProp } : {}
3243
+ })
3244
+ );
3245
+ };
3246
+ if (unit === "word") {
3247
+ let i = 0;
3248
+ while (i < chars.length) {
3249
+ if (chars[i] === " ") {
3250
+ cursor2 += advance(" ", weight, fontSize) + ls;
3251
+ i++;
3252
+ continue;
3262
3253
  }
3254
+ let word = "";
3255
+ let w = 0;
3256
+ const startCursor = cursor2;
3257
+ while (i < chars.length && chars[i] !== " ") {
3258
+ const a = advance(chars[i], weight, fontSize);
3259
+ word += chars[i];
3260
+ w += a + (chars[i + 1] && chars[i + 1] !== " " ? ls : 0);
3261
+ i++;
3262
+ }
3263
+ mk(word, startCursor + w / 2, w, ls);
3264
+ cursor2 = startCursor + w + ls;
3263
3265
  }
3266
+ } else {
3267
+ chars.forEach((ch) => {
3268
+ const a = advance(ch, weight, fontSize);
3269
+ if (ch !== " ") mk(ch, cursor2 + a / 2, a);
3270
+ cursor2 += a + ls;
3271
+ });
3264
3272
  }
3265
- if (prop === "x" || prop === "y" || prop === "rotation") {
3266
- const drivers = compiled.motionPaths.get(target);
3267
- if (drivers) {
3268
- let active;
3269
- for (const d of drivers) {
3270
- if (d.t0 <= t) active = d;
3271
- else break;
3272
- }
3273
- if (active && active.t0 >= segStart && (prop !== "rotation" || active.autoRotate) && active.points.length > 0) {
3274
- const span = active.t1 - active.t0;
3275
- const u = span <= 0 ? 1 : resolveEase(active.ease)(Math.max(0, Math.min(1, (t - active.t0) / span)));
3276
- if (prop === "x") value = pathPoint(active.points, active.closed, u, active.curviness)[0];
3277
- else if (prop === "y") value = pathPoint(active.points, active.closed, u, active.curviness)[1];
3278
- else value = pathTangentAngle(active.points, active.closed, u, active.curviness) + active.rotateOffset;
3273
+ return { nodes, glyphs, ids: glyphs.map((g) => g.id), width: total, x, y, fontSize };
3274
+ }
3275
+ var ctx3 = (o) => ({
3276
+ sp: Math.max(0.25, o.speed ?? 1),
3277
+ e: clamp013(o.energy ?? 0.5),
3278
+ seed: o.seed ?? 0,
3279
+ fs: 0,
3280
+ stag: o.stagger
3281
+ });
3282
+ var IN_STAGGER = { typewriter: 0.065, cascade: 0.04, rise: 0.03, bounce: 0.045, assemble: 0.05, decode: 0.05 };
3283
+ function glyphIn(name, g, c) {
3284
+ const set = (props) => tween(g.id, props, { duration: 1e-3 });
3285
+ const rs = (salt) => rand(g.i, salt + c.seed);
3286
+ switch (name) {
3287
+ case "typewriter":
3288
+ return tween(g.id, { opacity: 1 }, { duration: dur3(0.04, c.sp), ease: "linear" });
3289
+ case "cascade":
3290
+ return seq(
3291
+ set({ y: g.y + 56, opacity: 0 }),
3292
+ par(
3293
+ tween(g.id, { opacity: 1 }, { duration: dur3(0.22, c.sp), ease: "easeOutQuad" }),
3294
+ tween(g.id, { y: g.y }, { duration: dur3(0.34, c.sp), ease: "easeOutCubic" })
3295
+ )
3296
+ );
3297
+ case "rise":
3298
+ return seq(
3299
+ set({ y: g.y + 36, opacity: 0 }),
3300
+ par(
3301
+ tween(g.id, { opacity: 1 }, { duration: dur3(0.3, c.sp), ease: "easeOutQuad" }),
3302
+ tween(g.id, { y: g.y }, { duration: dur3(0.4, c.sp), ease: "easeOutQuad" })
3303
+ )
3304
+ );
3305
+ case "bounce":
3306
+ return seq(
3307
+ set({ y: g.y - 80 * (0.6 + c.e), opacity: 0, scale: 0.7 }),
3308
+ par(
3309
+ tween(g.id, { opacity: 1 }, { duration: dur3(0.2, c.sp), ease: "easeOutQuad" }),
3310
+ tween(g.id, { y: g.y, scale: 1 }, { duration: dur3(0.7, c.sp), ease: "easeOutBounce" })
3311
+ )
3312
+ );
3313
+ case "assemble":
3314
+ return seq(
3315
+ set({ x: g.x + (rs(11) - 0.5) * 1e3 * (0.5 + c.e), y: g.y + (rs(12) - 0.5) * 640, rotation: (rs(13) - 0.5) * 200, scale: 0.4, opacity: 0 }),
3316
+ par(
3317
+ tween(g.id, { opacity: 1 }, { duration: dur3(0.4, c.sp), ease: "easeOutQuad" }),
3318
+ tween(g.id, { x: g.x, y: g.y, rotation: 0, scale: 1 }, { duration: dur3(0.8, c.sp), ease: "easeOutExpo" })
3319
+ )
3320
+ );
3321
+ case "decode": {
3322
+ const steps = 4 + Math.floor(rs(7) * 3);
3323
+ const flicker = [set({ opacity: 1 })];
3324
+ for (let k = 0; k < steps; k++) {
3325
+ flicker.push(tween(g.id, { content: SCRAMBLE[Math.floor(rand(g.i, 20 + k + c.seed) * SCRAMBLE.length)] }, { duration: dur3(0.05, c.sp), ease: "linear" }));
3279
3326
  }
3327
+ flicker.push(tween(g.id, { content: g.ch }, { duration: dur3(0.05, c.sp), ease: "linear" }));
3328
+ return seq(...flicker);
3280
3329
  }
3281
3330
  }
3282
- for (const b of compiled.ir.behaviors ?? []) {
3283
- if (b.target === target && b.prop === prop && typeof value === "number") {
3284
- const envelope = behaviorEnvelope(b, t);
3285
- if (envelope > 0) value = value + envelope * sampleBehavior(b.behavior, t);
3331
+ }
3332
+ function textIn(name, block, opts = {}) {
3333
+ const c = { ...ctx3(opts), fs: block.fontSize };
3334
+ const interval = (c.stag ?? IN_STAGGER[name]) / c.sp;
3335
+ return beat(opts.label ?? `text-in-${name}`, {}, [stagger(interval, ...block.glyphs.map((g) => glyphIn(name, g, c)))]);
3336
+ }
3337
+ function textLoop(name, block, opts = {}) {
3338
+ const win = { ...opts.from !== void 0 && { from: opts.from }, ...opts.until !== void 0 && { until: opts.until }, ...opts.ramp !== void 0 && { ramp: opts.ramp } };
3339
+ const f = opts.frequency ?? (name === "wave" ? 0.9 : name === "shimmer" ? 1.4 : 0.7);
3340
+ const ps = opts.phaseStep ?? 0.55;
3341
+ return block.glyphs.map((g, i) => {
3342
+ switch (name) {
3343
+ case "wave":
3344
+ return oscillate(g.id, "y", { amplitude: opts.amplitude ?? 9, frequency: f, phase: i * ps }, win);
3345
+ case "shimmer":
3346
+ return oscillate(g.id, "opacity", { amplitude: opts.amplitude ?? 0.25, frequency: f, phase: i * ps }, win);
3347
+ case "wobble":
3348
+ return oscillate(g.id, "rotation", { amplitude: opts.amplitude ?? 6, frequency: f, phase: i * ps }, win);
3349
+ case "float":
3350
+ return oscillate(g.id, "y", { amplitude: opts.amplitude ?? 5, frequency: f, phase: i * ps }, win);
3286
3351
  }
3352
+ });
3353
+ }
3354
+ var OUT_STAGGER = { shatter: 0.02, fly: 0.012, dissolve: 0, fall: 0.02, collapse: 0.02 };
3355
+ function glyphOut(name, g, c, block, dir) {
3356
+ const rs = (salt) => rand(g.i, salt + c.seed);
3357
+ switch (name) {
3358
+ case "shatter":
3359
+ return par(
3360
+ tween(g.id, { x: g.x + (rs(21) - 0.5) * 1100 * (0.6 + c.e), y: g.y + (rs(22) - 0.5) * 760 }, { duration: dur3(0.7, c.sp), ease: "easeInCubic" }),
3361
+ tween(g.id, { rotation: (rs(23) - 0.5) * 300, opacity: 0 }, { duration: dur3(0.7, c.sp), ease: "easeInQuad" })
3362
+ );
3363
+ case "fly":
3364
+ return par(
3365
+ tween(g.id, { x: g.x + dir[0] * 1200, y: g.y + dir[1] * 1200 }, { duration: dur3(0.6, c.sp), ease: "easeInCubic" }),
3366
+ tween(g.id, { opacity: 0 }, { duration: dur3(0.5, c.sp), ease: "easeInQuad" })
3367
+ );
3368
+ case "dissolve":
3369
+ return seq(wait(rs(31) * 0.5), par(
3370
+ tween(g.id, { opacity: 0 }, { duration: dur3(0.4, c.sp), ease: "easeInQuad" }),
3371
+ tween(g.id, { scale: 1.4 }, { duration: dur3(0.4, c.sp), ease: "easeOutQuad" })
3372
+ ));
3373
+ case "fall":
3374
+ return par(
3375
+ tween(g.id, { y: g.y + 700 + rs(41) * 200 }, { duration: dur3(0.8, c.sp), ease: "easeInQuad" }),
3376
+ tween(g.id, { rotation: (rs(42) - 0.5) * 120, opacity: 0 }, { duration: dur3(0.8, c.sp), ease: "easeInQuad" })
3377
+ );
3378
+ case "collapse":
3379
+ return par(
3380
+ tween(g.id, { x: block.x, y: block.y, scale: 0.2 }, { duration: dur3(0.5, c.sp), ease: "easeInBack" }),
3381
+ tween(g.id, { opacity: 0 }, { duration: dur3(0.5, c.sp), ease: "easeInQuad" })
3382
+ );
3287
3383
  }
3288
- return value;
3289
3384
  }
3290
- function nodeParentMatrix(compiled, id, t) {
3291
- const num = (target, prop, fallback) => {
3292
- const v = sampleProp(compiled, t, target, prop, fallback);
3293
- return typeof v === "number" ? v : fallback;
3294
- };
3295
- let result = null;
3296
- const walk = (node, parent) => {
3297
- if (node.id === id) {
3298
- result = parent;
3299
- return true;
3300
- }
3301
- if (node.type === "group") {
3302
- const m = multiply(
3303
- parent,
3304
- localMatrix(
3305
- num(node.id, "x", node.props.x),
3306
- num(node.id, "y", node.props.y),
3307
- num(node.id, "rotation", node.props.rotation ?? 0),
3308
- num(node.id, "scale", node.props.scale ?? 1),
3309
- num(node.id, "scaleX", node.props.scaleX ?? 1),
3310
- num(node.id, "scaleY", node.props.scaleY ?? 1),
3311
- num(node.id, "skewX", node.props.skewX ?? 0),
3312
- num(node.id, "skewY", node.props.skewY ?? 0)
3313
- )
3314
- );
3315
- for (const child of node.children) if (walk(child, m)) return true;
3316
- }
3317
- return false;
3318
- };
3319
- for (const node of compiled.ir.nodes) if (walk(node, IDENTITY)) break;
3320
- return result;
3385
+ function textOut(name, block, opts = {}) {
3386
+ const c = { ...ctx3(opts), fs: block.fontSize };
3387
+ const dir = opts.dir ?? [0, -1];
3388
+ const steps = block.glyphs.map((g) => glyphOut(name, g, c, block, dir));
3389
+ const interval = (c.stag ?? OUT_STAGGER[name]) / c.sp;
3390
+ const body = interval > 0 ? stagger(interval, ...steps) : par(...steps);
3391
+ return beat(opts.label ?? `text-out-${name}`, {}, [body]);
3392
+ }
3393
+ function textTypeCues(block, opts) {
3394
+ const interval = opts.interval ?? 0.065;
3395
+ const gain = opts.gain ?? 0.4;
3396
+ const off = opts.offset ?? 0;
3397
+ const KEYS = ["001", "004", "007", "010", "014"];
3398
+ return block.glyphs.map((g, i) => ({
3399
+ at: opts.at,
3400
+ offset: off + i * interval,
3401
+ file: `keypress-${KEYS[i % KEYS.length]}.wav`,
3402
+ gain: gain + 0.2 * rand(i, 31)
3403
+ }));
3404
+ }
3405
+
3406
+ // ../core/src/motionOps.ts
3407
+ var MOTION_OPS = ["rotate", "zoom", "ken-burns", "slide-in", "fade", "draw-on", "pulse"];
3408
+ var clamp014 = (n3) => Math.max(0, Math.min(1, n3));
3409
+ function settleEase2(e) {
3410
+ return e < 0.34 ? "easeOutCubic" : e < 0.67 ? "easeOutBack" : "easeOutElastic";
3321
3411
  }
3322
- function evaluate(compiled, t) {
3323
- const ops = [];
3324
- const valueAt = (target, prop, fallback) => sampleProp(compiled, t, target, prop, fallback);
3325
- const num = (target, prop, fallback) => {
3326
- const v = valueAt(target, prop, fallback);
3327
- return typeof v === "number" ? v : fallback;
3328
- };
3329
- const str = (target, prop, fallback) => {
3330
- const v = valueAt(target, prop, fallback);
3331
- return typeof v === "string" ? v : String(v);
3332
- };
3333
- const opt = (target, prop, base) => {
3334
- const v = valueAt(target, prop, base ?? "");
3335
- return v === "" && base === void 0 ? void 0 : String(v);
3336
- };
3337
- const effectFx = (id, p) => {
3338
- const fx = {};
3339
- if (p.blur !== void 0) fx.blur = num(id, "blur", p.blur);
3340
- if (p.shadowColor !== void 0) {
3341
- fx.shadowColor = str(id, "shadowColor", p.shadowColor);
3342
- fx.shadowBlur = num(id, "shadowBlur", p.shadowBlur ?? 0);
3343
- fx.shadowX = num(id, "shadowX", p.shadowX ?? 0);
3344
- fx.shadowY = num(id, "shadowY", p.shadowY ?? 0);
3345
- }
3346
- if (p.blend !== void 0 && p.blend !== "normal") fx.blend = p.blend;
3347
- return fx;
3348
- };
3349
- const persp = compiled.hasPerspective;
3350
- const dPersp = persp ? num("camera", "perspective", 0) : 0;
3351
- const vx = persp ? compiled.ir.size.width / 2 : 0;
3352
- const vy = persp ? compiled.ir.size.height / 2 : 0;
3353
- const aperture = persp ? num("camera", "aperture", 0) : 0;
3354
- const focus = persp ? num("camera", "focus", 0) : 0;
3355
- const dofFx = (fx, depth, project) => {
3356
- if (!project || aperture <= 0) return fx;
3357
- const extra = aperture * Math.abs(depth - focus);
3358
- if (extra <= 0) return fx;
3359
- return { ...fx, blur: z0((fx.blur ?? 0) + extra) };
3360
- };
3361
- const zSort = compiled.zSort;
3362
- const depthOf = (node, zAcc) => zAcc + num(node.id, "z", node.props.z ?? 0);
3363
- const depthOrder = (children, zAcc) => [...children].sort((a, b) => depthOf(b, zAcc) - depthOf(a, zAcc));
3364
- const walk = (node, parent, parentOpacity, clips, zAcc, project) => {
3365
- const id = node.id;
3366
- const clipSpread = clips.length > 0 ? { clips } : void 0;
3367
- const fx = effectFx(id, node.props);
3368
- if (node.type === "line") {
3369
- const opacity2 = parentOpacity * num(id, "opacity", node.props.opacity ?? 1);
3370
- if (opacity2 <= 0) return;
3371
- const progress = Math.max(0, Math.min(1, num(id, "progress", node.props.progress ?? 1)));
3372
- const x1 = num(id, "x1", node.props.x1);
3373
- const y1 = num(id, "y1", node.props.y1);
3374
- ops.push({
3375
- type: "line",
3376
- id,
3377
- // a line carries no z/rotate of its own — it just inherits the subtree's depth
3378
- transform: project ? projectDepth(parent, zAcc, vx, vy, dPersp) : parent,
3379
- opacity: opacity2,
3380
- x1,
3381
- y1,
3382
- x2: x1 + (num(id, "x2", node.props.x2) - x1) * progress,
3383
- y2: y1 + (num(id, "y2", node.props.y2) - y1) * progress,
3384
- stroke: str(id, "stroke", node.props.stroke),
3385
- strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1),
3386
- // a line carries no z of its own — DOF uses the inherited subtree depth
3387
- ...dofFx(fx, zAcc, project),
3388
- ...clipSpread
3389
- });
3390
- return;
3412
+ function fromVec2(from, dist) {
3413
+ switch (from) {
3414
+ case "right":
3415
+ return [dist, 0];
3416
+ case "top":
3417
+ return [0, -dist];
3418
+ case "bottom":
3419
+ return [0, dist];
3420
+ default:
3421
+ return [-dist, 0];
3422
+ }
3423
+ }
3424
+ var motionOpLabel = (name, target) => `op-${name}-${target}`;
3425
+ function motionOp(name, target, opts = {}) {
3426
+ const e = clamp014(opts.energy ?? 0.5);
3427
+ const sp = Math.max(0.25, opts.speed ?? 1);
3428
+ const amt = opts.amount ?? 1;
3429
+ const b = { scale: 1, x: 0, y: 0, rotation: 0, ...opts.base };
3430
+ const d = (base) => base / sp;
3431
+ const label = motionOpLabel(name, target);
3432
+ switch (name) {
3433
+ case "rotate":
3434
+ return { timeline: beat(label, {}, [tween(target, { rotation: b.rotation + 360 * amt }, { duration: d(1), ease: settleEase2(e) })]) };
3435
+ case "zoom": {
3436
+ const peak = b.scale * (1 + 0.22 * amt);
3437
+ return {
3438
+ timeline: beat(label, {}, [
3439
+ seq(
3440
+ tween(target, { scale: peak }, { duration: d(0.4), ease: "easeOutBack" }),
3441
+ tween(target, { scale: b.scale }, { duration: d(0.45), ease: "easeInOutQuad" })
3442
+ )
3443
+ ])
3444
+ };
3391
3445
  }
3392
- const opacity = parentOpacity * num(id, "opacity", node.props.opacity ?? 1);
3393
- if (opacity <= 0) return;
3394
- let effScaleX = num(id, "scaleX", node.props.scaleX ?? 1);
3395
- let effScaleY = num(id, "scaleY", node.props.scaleY ?? 1);
3396
- let depth = zAcc;
3397
- let rotX = 0;
3398
- let rotY = 0;
3399
- if (project) {
3400
- rotX = num(id, "rotateX", node.props.rotateX ?? 0);
3401
- rotY = num(id, "rotateY", node.props.rotateY ?? 0);
3402
- depth = zAcc + num(id, "z", node.props.z ?? 0);
3403
- if (rotY !== 0) effScaleX *= Math.abs(Math.cos(rotY * DEG));
3404
- if (rotX !== 0) effScaleY *= Math.abs(Math.cos(rotX * DEG));
3446
+ case "ken-burns":
3447
+ return {
3448
+ timeline: beat(label, {}, [
3449
+ par(
3450
+ tween(target, { scale: b.scale * (1 + 0.1 * amt) }, { duration: d(3), ease: "easeInOutQuad" }),
3451
+ tween(target, { x: b.x + 26 * amt, y: b.y - 16 * amt }, { duration: d(3), ease: "easeInOutQuad" })
3452
+ )
3453
+ ])
3454
+ };
3455
+ case "slide-in": {
3456
+ const [dx, dy] = fromVec2(opts.from ?? "left", 320 * amt);
3457
+ return {
3458
+ setup: { [target]: { x: b.x + dx, y: b.y + dy, opacity: 0 } },
3459
+ timeline: beat(label, {}, [
3460
+ par(
3461
+ tween(target, { x: b.x, y: b.y }, { duration: d(0.7), ease: settleEase2(e) }),
3462
+ tween(target, { opacity: 1 }, { duration: d(0.4), ease: "easeOutQuad" })
3463
+ )
3464
+ ])
3465
+ };
3405
3466
  }
3406
- const matrix = multiply(
3407
- parent,
3408
- localMatrix(
3409
- num(id, "x", node.props.x),
3410
- num(id, "y", node.props.y),
3411
- num(id, "rotation", node.props.rotation ?? 0),
3412
- num(id, "scale", node.props.scale ?? 1),
3413
- effScaleX,
3414
- effScaleY,
3415
- num(id, "skewX", node.props.skewX ?? 0),
3416
- num(id, "skewY", node.props.skewY ?? 0)
3417
- )
3418
- );
3419
- const projDraw = (m, hw, hh) => {
3420
- if (!project) return m;
3421
- const tilted = rotX !== 0 || rotY !== 0 ? tiltSkew(m, rotX, rotY, hw, hh, dPersp) : m;
3422
- return projectDepth(tilted, depth, vx, vy, dPersp);
3423
- };
3424
- const leafFx = dofFx(fx, depth, project);
3425
- switch (node.type) {
3426
- case "group": {
3427
- const clipTf = projDraw(matrix, 0, 0);
3428
- const childClips = node.props.clip ? [...clips, { transform: clipTf, shape: node.props.clip }] : clips;
3429
- const hasFx = fx.blur !== void 0 || fx.shadowColor !== void 0 || fx.blend !== void 0;
3430
- if (hasFx) ops.push({ type: "group-fx-push", id, transform: matrix, opacity, ...fx, ...clipSpread });
3431
- if (node.props.matte && node.children.length >= 2) {
3432
- ops.push({ type: "matte-push", id, transform: matrix, opacity, mode: node.props.matte, ...clipSpread });
3433
- walk(node.children[0], matrix, opacity, childClips, depth, project);
3434
- ops.push({ type: "matte-sep", id, transform: matrix, opacity });
3435
- for (let i = 1; i < node.children.length; i++) walk(node.children[i], matrix, opacity, childClips, depth, project);
3436
- ops.push({ type: "matte-pop", id, transform: matrix, opacity });
3437
- } else {
3438
- const kids = zSort && project ? depthOrder(node.children, depth) : node.children;
3439
- for (const child of kids) walk(child, matrix, opacity, childClips, depth, project);
3440
- }
3441
- if (hasFx) ops.push({ type: "group-fx-pop", id, transform: matrix, opacity });
3442
- return;
3443
- }
3444
- case "rect":
3445
- case "ellipse": {
3446
- const width = num(id, "width", node.props.width);
3447
- const height = num(id, "height", node.props.height);
3448
- const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
3449
- const strokeWidth = num(id, "strokeWidth", node.props.strokeWidth ?? 1);
3450
- const fillP = node.props.fill;
3451
- const strokeP = node.props.stroke;
3452
- const fill = isGradient(fillP) ? fillP : opt(id, "fill", fillP);
3453
- const stroke = isGradient(strokeP) ? strokeP : opt(id, "stroke", strokeP);
3454
- ops.push({
3455
- type: node.type,
3456
- id,
3457
- transform: projDraw(matrix, width / 2, height / 2),
3458
- opacity,
3459
- width,
3460
- height,
3461
- offsetX: -width * ax,
3462
- offsetY: -height * ay,
3463
- ...fill !== void 0 && { fill },
3464
- ...stroke !== void 0 && { stroke, strokeWidth },
3465
- ...node.type === "rect" && { radius: num(id, "radius", node.props.radius ?? 0) },
3466
- ...leafFx,
3467
- ...clipSpread
3468
- });
3469
- return;
3470
- }
3471
- case "image": {
3472
- const width = num(id, "width", node.props.width);
3473
- const height = num(id, "height", node.props.height);
3474
- const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
3475
- ops.push({
3476
- type: "image",
3477
- id,
3478
- transform: projDraw(matrix, width / 2, height / 2),
3479
- opacity,
3480
- src: str(id, "src", node.props.src),
3481
- width,
3482
- height,
3483
- offsetX: -width * ax,
3484
- offsetY: -height * ay,
3485
- ...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
3486
- ...leafFx,
3487
- ...clipSpread
3488
- });
3489
- return;
3490
- }
3491
- case "video": {
3492
- const width = num(id, "width", node.props.width);
3493
- const height = num(id, "height", node.props.height);
3494
- const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
3495
- const fps = compiled.ir.fps ?? 30;
3496
- const start = node.props.start ?? 0;
3497
- const rate = node.props.rate ?? 1;
3498
- const clipStart = node.props.clipStart ?? 0;
3499
- const srcT = clipStart + Math.max(0, t - start) * rate;
3500
- const frame = Math.max(0, Math.round(srcT * fps));
3501
- ops.push({
3502
- type: "video",
3503
- id,
3504
- transform: projDraw(matrix, width / 2, height / 2),
3505
- opacity,
3506
- src: str(id, "src", node.props.src),
3507
- width,
3508
- height,
3509
- offsetX: -width * ax,
3510
- offsetY: -height * ay,
3511
- frame,
3512
- ...node.props.fit && node.props.fit !== "fill" ? { fit: node.props.fit } : {},
3513
- ...leafFx,
3514
- ...clipSpread
3515
- });
3516
- return;
3467
+ case "fade":
3468
+ return {
3469
+ setup: { [target]: { opacity: 0 } },
3470
+ timeline: beat(label, {}, [tween(target, { opacity: 1 }, { duration: d(0.6), ease: "easeOutQuad" })])
3471
+ };
3472
+ case "draw-on":
3473
+ return {
3474
+ setup: { [target]: { progress: 0 } },
3475
+ timeline: beat(label, {}, [tween(target, { progress: 1 }, { duration: d(1.3), ease: "easeInOutQuad" })])
3476
+ };
3477
+ case "pulse": {
3478
+ const hi = b.scale * (1 + 0.12 * amt);
3479
+ const pulses = 2 + Math.round(amt);
3480
+ const steps = [];
3481
+ for (let i = 0; i < pulses; i++) {
3482
+ steps.push(tween(target, { scale: hi }, { duration: d(0.22), ease: "easeOutQuad" }));
3483
+ steps.push(tween(target, { scale: b.scale }, { duration: d(0.22), ease: "easeInQuad" }));
3517
3484
  }
3518
- case "path": {
3519
- const ox = num(id, "originX", node.props.originX ?? 0);
3520
- const oy = num(id, "originY", node.props.originY ?? 0);
3521
- const fillP = node.props.fill;
3522
- const strokeP = node.props.stroke;
3523
- const fill = isGradient(fillP) ? fillP : opt(id, "fill", fillP);
3524
- const stroke = isGradient(strokeP) ? strokeP : opt(id, "stroke", strokeP);
3525
- const dStr = str(id, "d", node.props.d);
3526
- const needsBox = isGradient(fill) || isGradient(stroke);
3527
- ops.push({
3528
- type: "path",
3529
- id,
3530
- // origin-shift in local space, then project (no per-op extent → cos + VP only)
3531
- transform: projDraw(ox === 0 && oy === 0 ? matrix : multiply(matrix, [1, 0, 0, 1, -ox, -oy]), 0, 0),
3532
- opacity,
3533
- d: dStr,
3534
- progress: Math.max(0, Math.min(1, num(id, "progress", node.props.progress ?? 1))),
3535
- ...fill !== void 0 && { fill },
3536
- ...stroke !== void 0 && { stroke, strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1) },
3537
- ...needsBox && { bbox: pathBBox(dStr) },
3538
- ...leafFx,
3539
- ...clipSpread
3540
- });
3541
- return;
3485
+ return { timeline: beat(label, {}, [seq(...steps)]) };
3486
+ }
3487
+ }
3488
+ }
3489
+
3490
+ // ../core/src/audio.ts
3491
+ var SFX_DURATION = {
3492
+ // transition
3493
+ whoosh: 0.35,
3494
+ swish: 0.32,
3495
+ swoosh: 0.35,
3496
+ rise: 0.5,
3497
+ riser: 0.85,
3498
+ warp: 0.5,
3499
+ // ui
3500
+ tick: 0.03,
3501
+ click: 0.05,
3502
+ blip: 0.1,
3503
+ pop: 0.12,
3504
+ select: 0.18,
3505
+ // impact
3506
+ thud: 0.25,
3507
+ boom: 0.6,
3508
+ knock: 0.14,
3509
+ sub: 0.7,
3510
+ // positive
3511
+ chime: 0.7,
3512
+ ding: 0.5,
3513
+ coin: 0.3,
3514
+ sparkle: 0.6,
3515
+ shimmer: 0.9,
3516
+ success: 0.6,
3517
+ // alert
3518
+ zap: 0.22,
3519
+ error: 0.4,
3520
+ // tech
3521
+ glitch: 0.3,
3522
+ static: 0.18,
3523
+ scan: 0.45,
3524
+ powerup: 0.4,
3525
+ powerdown: 0.5,
3526
+ // rhythm / foley
3527
+ snare: 0.18,
3528
+ hat: 0.05,
3529
+ bubble: 0.16,
3530
+ notify: 0.45,
3531
+ camera: 0.18
3532
+ };
3533
+ var FILE_CUE_DURATION = 0.4;
3534
+ function collectClipAudio(ir, duration, warnings) {
3535
+ const out = [];
3536
+ const walk = (nodes) => {
3537
+ for (const node of nodes) {
3538
+ if (node.type === "video") {
3539
+ const gain = node.props.volume ?? 1;
3540
+ const start = node.props.start ?? 0;
3541
+ if (gain <= 0) continue;
3542
+ if (start >= duration) {
3543
+ warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
3544
+ continue;
3545
+ }
3546
+ out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain, fadeIn: node.props.fadeIn ?? 0, pan: node.props.pan ?? 0 });
3542
3547
  }
3543
- case "text": {
3544
- const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
3545
- const raw = valueAt(id, "content", node.props.content);
3546
- const decimals = Math.max(
3547
- 0,
3548
- Math.round(num(id, "contentDecimals", node.props.contentDecimals ?? 0))
3549
- );
3550
- const body = typeof raw === "number" ? formatNumber(raw, decimals, node.props.contentThousands === true) : raw;
3551
- ops.push({
3552
- type: "text",
3553
- id,
3554
- transform: projDraw(matrix, 0, 0),
3555
- opacity,
3556
- // static affixes wrap the (possibly counting-up) body; absent ⇒ body unchanged
3557
- content: (node.props.prefix ?? "") + body + (node.props.suffix ?? ""),
3558
- fontFamily: str(id, "fontFamily", node.props.fontFamily),
3559
- fontSize: num(id, "fontSize", node.props.fontSize),
3560
- fontWeight: num(id, "fontWeight", node.props.fontWeight ?? 400),
3561
- fill: str(id, "fill", node.props.fill ?? "#ffffff"),
3562
- letterSpacing: num(id, "letterSpacing", node.props.letterSpacing ?? 0),
3563
- align: TEXT_ALIGN[ax] ?? "left",
3564
- baseline: TEXT_BASELINE[ay] ?? "top",
3565
- ...leafFx,
3566
- ...clipSpread
3567
- });
3568
- return;
3548
+ if (node.type === "group") walk(node.children);
3549
+ }
3550
+ };
3551
+ walk(ir.nodes);
3552
+ return out;
3553
+ }
3554
+ function resolveAudioPlan(compiled) {
3555
+ const audio = compiled.ir.audio;
3556
+ const warnings = [];
3557
+ const duration = compiled.duration;
3558
+ const clipAudio = collectClipAudio(compiled.ir, duration, warnings);
3559
+ const autoCues = audio?.autoFoley ? autoFoley(compiled, audio.autoFoley === true ? {} : audio.autoFoley) : [];
3560
+ const manualCues = [...audio?.cues ?? [], ...autoCues];
3561
+ if (!audio || !audio.bgm && manualCues.length === 0) {
3562
+ return clipAudio.length === 0 ? null : { duration, bgm: null, cues: [], duckWindows: [], clipAudio, warnings };
3563
+ }
3564
+ const cues = [];
3565
+ for (const [index, cue] of manualCues.entries()) {
3566
+ let anchor;
3567
+ if (typeof cue.at === "number") {
3568
+ anchor = cue.at;
3569
+ } else {
3570
+ const span = compiled.labelTimes.get(cue.at);
3571
+ if (!span) {
3572
+ warnings.push(`cue[${index}]: unknown label "${cue.at}" \u2014 cue dropped`);
3573
+ continue;
3569
3574
  }
3575
+ anchor = span.t0;
3570
3576
  }
3577
+ const t = Math.max(0, anchor + (cue.offset ?? 0));
3578
+ const cueDuration = cue.sfx ? SFX_DURATION[cue.sfx] : FILE_CUE_DURATION;
3579
+ if (t >= duration) {
3580
+ warnings.push(`cue[${index}] at ${t.toFixed(2)}s starts past the scene end (${duration.toFixed(2)}s) \u2014 dropped`);
3581
+ continue;
3582
+ }
3583
+ if (t + cueDuration > duration) {
3584
+ warnings.push(`cue[${index}] at ${t.toFixed(2)}s extends past the scene end \u2014 it will be truncated`);
3585
+ }
3586
+ cues.push({
3587
+ t,
3588
+ gain: cue.gain ?? 1,
3589
+ duration: cueDuration,
3590
+ fadeIn: cue.fadeIn ?? 0,
3591
+ fadeOut: cue.fadeOut ?? 0,
3592
+ pan: cue.pan ?? 0,
3593
+ source: cue.sfx ? (
3594
+ // auto-vary: default the seed to the cue's order so repeated sfx differ
3595
+ // (pitch/texture); an explicit params.seed always wins.
3596
+ { kind: "sfx", name: cue.sfx, params: { seed: index, ...cue.params } }
3597
+ ) : { kind: "file", path: cue.file }
3598
+ });
3599
+ }
3600
+ cues.sort((a, b) => a.t - b.t);
3601
+ return {
3602
+ duration,
3603
+ bgm: resolveBgm(audio.bgm),
3604
+ cues,
3605
+ duckWindows: mergeDuckWindows(cues, duration),
3606
+ clipAudio,
3607
+ warnings
3571
3608
  };
3572
- const cameraRoot = compiled.hasCamera ? cameraMatrix(
3573
- {
3574
- x: num("camera", "x", compiled.ir.size.width / 2),
3575
- y: num("camera", "y", compiled.ir.size.height / 2),
3576
- zoom: num("camera", "zoom", 1),
3577
- rotation: num("camera", "rotation", 0)
3578
- },
3579
- compiled.ir.size
3580
- ) : IDENTITY;
3581
- let roots = compiled.ir.nodes;
3582
- if (zSort) {
3583
- const isHud = (n3) => !!(n3.props.fixed && compiled.hasCamera);
3584
- roots = [...depthOrder(compiled.ir.nodes.filter((n3) => !isHud(n3)), 0), ...compiled.ir.nodes.filter(isHud)];
3609
+ }
3610
+ function mergeDuckWindows(cues, duration) {
3611
+ const duckWindows = [];
3612
+ for (const cue of cues) {
3613
+ const window = { t0: cue.t, t1: Math.min(duration, cue.t + cue.duration) };
3614
+ const last = duckWindows[duckWindows.length - 1];
3615
+ if (last && window.t0 <= last.t1 + 0.1) last.t1 = Math.max(last.t1, window.t1);
3616
+ else duckWindows.push(window);
3585
3617
  }
3586
- for (const node of roots) {
3587
- const root = compiled.hasCamera && node.props.fixed ? IDENTITY : cameraRoot;
3588
- const project = persp && !(node.props.fixed && compiled.hasCamera);
3589
- walk(node, root, 1, [], 0, project);
3618
+ return duckWindows;
3619
+ }
3620
+ function resolveBgm(b) {
3621
+ if (!b) return null;
3622
+ const duck = b.duck === false ? null : {
3623
+ depth: b.duck?.depth ?? 0.5,
3624
+ attack: b.duck?.attack ?? 0.05,
3625
+ release: b.duck?.release ?? 0.25
3626
+ };
3627
+ return {
3628
+ source: b.file ? { kind: "file", path: b.file } : { kind: "synth", name: b.synth ?? "ambient-pad" },
3629
+ gain: b.gain ?? 0.5,
3630
+ fadeIn: b.fadeIn ?? 0,
3631
+ fadeOut: b.fadeOut ?? 0,
3632
+ duck
3633
+ };
3634
+ }
3635
+ function resolveCompositionAudioPlan(comp) {
3636
+ const audio = comp.ir.audio;
3637
+ const duration = comp.duration;
3638
+ const warnings = [];
3639
+ const cues = [];
3640
+ const clipAudio = [];
3641
+ for (const placement of comp.scenes) {
3642
+ const plan = resolveAudioPlan(placement.compiled);
3643
+ if (!plan) continue;
3644
+ if (plan.bgm) {
3645
+ warnings.push(`scene "${placement.id}": per-scene bgm ignored \u2014 set bgm at the composition level`);
3646
+ }
3647
+ for (const w of plan.warnings) warnings.push(`scene "${placement.id}": ${w}`);
3648
+ for (const cue of plan.cues) {
3649
+ const t = cue.t + placement.start;
3650
+ if (t >= duration) continue;
3651
+ cues.push({ ...cue, t });
3652
+ }
3653
+ for (const clip of plan.clipAudio) {
3654
+ const start = clip.start + placement.start;
3655
+ if (start >= duration) continue;
3656
+ clipAudio.push({ ...clip, start });
3657
+ }
3590
3658
  }
3591
- return ops;
3659
+ for (const [index, cue] of (audio?.cues ?? []).entries()) {
3660
+ if (typeof cue.at !== "number") {
3661
+ warnings.push(`composition cue[${index}]: "at" must be an absolute number (no composition labels) \u2014 dropped`);
3662
+ continue;
3663
+ }
3664
+ const t = Math.max(0, cue.at + (cue.offset ?? 0));
3665
+ const cueDuration = cue.sfx ? SFX_DURATION[cue.sfx] : FILE_CUE_DURATION;
3666
+ if (t >= duration) {
3667
+ warnings.push(`composition cue[${index}] at ${t.toFixed(2)}s past the composition end \u2014 dropped`);
3668
+ continue;
3669
+ }
3670
+ cues.push({
3671
+ t,
3672
+ gain: cue.gain ?? 1,
3673
+ duration: cueDuration,
3674
+ fadeIn: cue.fadeIn ?? 0,
3675
+ fadeOut: cue.fadeOut ?? 0,
3676
+ pan: cue.pan ?? 0,
3677
+ source: cue.sfx ? (
3678
+ // auto-vary: default the seed to the cue's order so repeated sfx differ
3679
+ // (pitch/texture); an explicit params.seed always wins.
3680
+ { kind: "sfx", name: cue.sfx, params: { seed: index, ...cue.params } }
3681
+ ) : { kind: "file", path: cue.file }
3682
+ });
3683
+ }
3684
+ if (!audio?.bgm && cues.length === 0 && clipAudio.length === 0) return null;
3685
+ cues.sort((a, b) => a.t - b.t);
3686
+ return {
3687
+ duration,
3688
+ bgm: resolveBgm(audio?.bgm),
3689
+ cues,
3690
+ duckWindows: mergeDuckWindows(cues, duration),
3691
+ clipAudio,
3692
+ warnings
3693
+ };
3592
3694
  }
3593
3695
 
3594
3696
  // ../core/src/assets.ts
@@ -3692,6 +3794,7 @@ export {
3692
3794
  SFX_DURATION,
3693
3795
  SFX_NAMES,
3694
3796
  SceneValidationError,
3797
+ autoFoley,
3695
3798
  beat,
3696
3799
  cameraFit,
3697
3800
  cameraMatrix,