reframe-video 0.6.32 → 0.6.34

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