reframe-video 0.6.34 → 0.6.39

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
@@ -255,11 +255,68 @@ function compileScene(ir) {
255
255
  const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
256
256
  const natural = durationOf(grouping, 0);
257
257
  const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, natural) : 1);
258
- const beatStart = tl.at ?? start + (tl.gap ?? 0);
258
+ const at = typeof tl.at === "number" ? tl.at : void 0;
259
+ const beatStart = at ?? start + (tl.gap ?? 0);
259
260
  return beatStart + k * natural;
260
261
  }
261
262
  }
262
263
  };
264
+ let labelClock;
265
+ const anyAnchor = (tl) => tl.kind === "beat" && typeof tl.at === "string" || "children" in tl && tl.children.some(anyAnchor);
266
+ if (ir.timeline && anyAnchor(ir.timeline)) {
267
+ const clock = /* @__PURE__ */ new Map();
268
+ const clockWalk = (tl, start) => {
269
+ let end = start;
270
+ switch (tl.kind) {
271
+ case "seq": {
272
+ let t = start;
273
+ for (const c of orderBeats(tl.children)) t = clockWalk(c, t);
274
+ end = t;
275
+ break;
276
+ }
277
+ case "par": {
278
+ for (const c of tl.children) end = Math.max(end, clockWalk(c, start));
279
+ break;
280
+ }
281
+ case "stagger": {
282
+ tl.children.forEach((c, i) => {
283
+ end = Math.max(end, clockWalk(c, start + i * tl.interval));
284
+ });
285
+ break;
286
+ }
287
+ case "wait":
288
+ end = start + tl.duration;
289
+ break;
290
+ case "tween":
291
+ end = start + (tl.duration ?? DEFAULT_TWEEN_DURATION);
292
+ break;
293
+ case "motionPath":
294
+ end = start + (tl.duration ?? DEFAULT_MOTIONPATH_DURATION);
295
+ break;
296
+ case "to": {
297
+ const override = ir.states?.[tl.state] ?? {};
298
+ const si = tl.stagger ?? 0;
299
+ const targets = nodeOrder.filter((id) => id in override && (tl.filter === void 0 || tl.filter.includes(id)));
300
+ end = start + (tl.duration ?? DEFAULT_TO_DURATION) + Math.max(0, targets.length - 1) * si;
301
+ break;
302
+ }
303
+ case "beat": {
304
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
305
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
306
+ const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
307
+ const at = typeof tl.at === "number" ? tl.at : void 0;
308
+ const beatStart = at ?? start + (tl.gap ?? 0);
309
+ end = clockWalk(inner, beatStart);
310
+ clock.set(tl.name, { t0: beatStart, t1: end });
311
+ break;
312
+ }
313
+ }
314
+ if ("label" in tl && tl.label !== void 0) clock.set(tl.label, { t0: start, t1: end });
315
+ return end;
316
+ };
317
+ clockWalk(ir.timeline, 0);
318
+ labelClock = clock;
319
+ }
263
320
  const walk = (tl, start) => {
264
321
  const end = walkInner(tl, start);
265
322
  if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
@@ -276,7 +333,8 @@ function compileScene(ir) {
276
333
  const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
277
334
  const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
278
335
  const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
279
- const beatStart = tl.at ?? start + (tl.gap ?? 0);
336
+ const anchored = typeof tl.at === "string" ? labelClock?.get(tl.at)?.t0 : tl.at;
337
+ const beatStart = anchored !== void 0 ? anchored + (typeof tl.at === "string" ? tl.gap ?? 0 : 0) : start + (tl.gap ?? 0);
280
338
  const end = walk(inner, beatStart);
281
339
  beatTimes.set(tl.name, { t0: beatStart, t1: end });
282
340
  labelTimes.set(tl.name, { t0: beatStart, t1: end });
@@ -596,6 +654,7 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
596
654
  function validateScene(ir) {
597
655
  const problems = [];
598
656
  const nodeById = /* @__PURE__ */ new Map();
657
+ const startAnchors = [];
599
658
  const checkPaint = (where, value) => {
600
659
  if (typeof value !== "object" || value === null) return;
601
660
  const g = value;
@@ -628,6 +687,7 @@ function validateScene(ir) {
628
687
  if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
629
688
  if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
630
689
  if (typeof props.fit === "string" && !IMAGE_FITS.has(props.fit)) problems.push(`node "${node.id}": unknown fit "${props.fit}" \u2014 use ${[...IMAGE_FITS].join(", ")}`);
690
+ if (node.type === "video" && typeof node.props.start === "string") startAnchors.push({ id: node.id, at: node.props.start });
631
691
  if (node.type === "group") {
632
692
  const clip = node.props.clip;
633
693
  if (clip) {
@@ -688,6 +748,7 @@ function validateScene(ir) {
688
748
  );
689
749
  }
690
750
  const labels = /* @__PURE__ */ new Set();
751
+ const beatAnchors = [];
691
752
  const checkEase = (path2, ease) => {
692
753
  if (ease === void 0) return;
693
754
  if (typeof ease === "string") {
@@ -779,6 +840,7 @@ function validateScene(ir) {
779
840
  );
780
841
  }
781
842
  labels.add(tl.name);
843
+ if (typeof tl.at === "string") beatAnchors.push({ name: tl.name, at: tl.at, path: path2 });
782
844
  if (tl.duration !== void 0 && tl.duration <= 0) {
783
845
  problems.push(`${path2}: beat "${tl.name}" duration must be > 0`);
784
846
  }
@@ -797,6 +859,22 @@ function validateScene(ir) {
797
859
  }
798
860
  };
799
861
  if (ir.timeline) checkTimeline(ir.timeline, "timeline");
862
+ for (const a of beatAnchors) {
863
+ if (a.at === a.name) {
864
+ problems.push(`${a.path}: beat "${a.name}" at: "${a.at}" cannot anchor to itself`);
865
+ } else if (!labels.has(a.at)) {
866
+ problems.push(
867
+ `${a.path}: beat "${a.name}" at: "${a.at}" \u2014 unknown timeline label \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
868
+ );
869
+ }
870
+ }
871
+ for (const a of startAnchors) {
872
+ if (!labels.has(a.at)) {
873
+ problems.push(
874
+ `video "${a.id}" start: "${a.at}" \u2014 unknown timeline label \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
875
+ );
876
+ }
877
+ }
800
878
  for (const [i, b] of (ir.behaviors ?? []).entries()) {
801
879
  checkProps(`behaviors[${i}]`, b.target, { [b.prop]: 0 });
802
880
  }
@@ -1000,6 +1078,13 @@ function compileComposition(comp) {
1000
1078
 
1001
1079
  // ../core/src/compose.ts
1002
1080
  var SCENE_PATCHABLE = ["background", "duration", "fps"];
1081
+ var TIMELINE_PATCHABLE = {
1082
+ to: ["duration", "ease", "stagger"],
1083
+ tween: ["duration", "ease"],
1084
+ wait: ["duration"],
1085
+ motionPath: ["points", "duration", "ease", "curviness", "autoRotate"],
1086
+ beat: ["at", "gap", "scale", "duration", "order"]
1087
+ };
1003
1088
  function composeScene(base, ...overlays) {
1004
1089
  const ir = structuredClone(base);
1005
1090
  const report = { applied: [], orphans: [], warnings: [] };
@@ -1134,13 +1219,6 @@ function applyOverlay(ir, overlay, layer, report, baseNodeIds) {
1134
1219
  if ("children" in tl) tl.children.forEach(walkTimeline);
1135
1220
  };
1136
1221
  if (ir.timeline) walkTimeline(ir.timeline);
1137
- const PATCHABLE = {
1138
- to: ["duration", "ease", "stagger"],
1139
- tween: ["duration", "ease"],
1140
- wait: ["duration"],
1141
- motionPath: ["points", "duration", "ease", "curviness", "autoRotate"],
1142
- beat: ["at", "gap", "scale", "duration", "order"]
1143
- };
1144
1222
  let timingPatched = false;
1145
1223
  for (const [label, patch] of Object.entries(overlay.timeline)) {
1146
1224
  const step = byLabel.get(label);
@@ -1151,7 +1229,7 @@ function applyOverlay(ir, overlay, layer, report, baseNodeIds) {
1151
1229
  );
1152
1230
  continue;
1153
1231
  }
1154
- const allowed = PATCHABLE[step.kind] ?? [];
1232
+ const allowed = TIMELINE_PATCHABLE[step.kind] ?? [];
1155
1233
  for (const [key2, value] of Object.entries(patch)) {
1156
1234
  if (value === void 0) continue;
1157
1235
  if (!allowed.includes(key2)) {
@@ -1231,6 +1309,134 @@ function formatComposeReport(report) {
1231
1309
  return lines.join("\n");
1232
1310
  }
1233
1311
 
1312
+ // ../core/src/manifest.ts
1313
+ function animatedPropsOf(compiled, id) {
1314
+ const props = /* @__PURE__ */ new Set();
1315
+ const prefix = `${id}.`;
1316
+ for (const key2 of compiled.segments.keys()) {
1317
+ if (key2.startsWith(prefix)) props.add(key2.slice(prefix.length));
1318
+ }
1319
+ const drivers = compiled.motionPaths.get(id);
1320
+ if (drivers && drivers.length > 0) {
1321
+ props.add("x");
1322
+ props.add("y");
1323
+ if (drivers.some((d) => d.autoRotate)) props.add("rotation");
1324
+ }
1325
+ return [...props].sort();
1326
+ }
1327
+ function indexTimeline(tl) {
1328
+ const byLabel = /* @__PURE__ */ new Map();
1329
+ const walk = (t) => {
1330
+ if ("label" in t && t.label !== void 0) byLabel.set(t.label, t);
1331
+ if (t.kind === "beat") byLabel.set(t.name, t);
1332
+ if ("children" in t) t.children.forEach(walk);
1333
+ };
1334
+ if (tl) walk(tl);
1335
+ return byLabel;
1336
+ }
1337
+ function walkMotion(tl, visit) {
1338
+ const walk = (t) => {
1339
+ if (t.kind === "tween" || t.kind === "motionPath") {
1340
+ visit(t.kind, "label" in t && t.label !== void 0, t.target);
1341
+ } else if (t.kind === "to") {
1342
+ visit("to", t.label !== void 0, t.state);
1343
+ }
1344
+ if ("children" in t) t.children.forEach(walk);
1345
+ };
1346
+ if (tl) walk(tl);
1347
+ }
1348
+ function sceneManifest(compiled) {
1349
+ const ir = compiled.ir;
1350
+ const statesByNode = /* @__PURE__ */ new Map();
1351
+ for (const [name, override] of Object.entries(ir.states ?? {})) {
1352
+ for (const id of Object.keys(override)) {
1353
+ const list = statesByNode.get(id) ?? [];
1354
+ list.push(name);
1355
+ statesByNode.set(id, list);
1356
+ }
1357
+ }
1358
+ const nodes = [];
1359
+ const walkNodes = (list, parent) => {
1360
+ for (const node of list) {
1361
+ nodes.push({
1362
+ id: node.id,
1363
+ type: node.type,
1364
+ ...parent !== void 0 ? { parent } : {},
1365
+ address: `nodes.${node.id}`,
1366
+ editableProps: [...PROPS_BY_TYPE[node.type]],
1367
+ animatedProps: animatedPropsOf(compiled, node.id),
1368
+ inStates: statesByNode.get(node.id) ?? []
1369
+ });
1370
+ if (node.type === "group") walkNodes(node.children, node.id);
1371
+ }
1372
+ };
1373
+ walkNodes(ir.nodes, void 0);
1374
+ const states = Object.entries(ir.states ?? {}).map(([name, override]) => ({
1375
+ name,
1376
+ address: `states.${name}`,
1377
+ touches: Object.entries(override).map(([id, props]) => ({ id, props: Object.keys(props) }))
1378
+ }));
1379
+ const byLabel = indexTimeline(ir.timeline);
1380
+ const timeline = [];
1381
+ const beats = [];
1382
+ for (const [label, span] of compiled.labelTimes) {
1383
+ const step = byLabel.get(label);
1384
+ const kind = step?.kind ?? "seq";
1385
+ if (compiled.beatTimes.has(label) && step?.kind === "beat") {
1386
+ beats.push({ name: label, t0: span.t0, t1: span.t1, ownsNodes: step.nodes ?? [], address: `timeline.${label}` });
1387
+ } else {
1388
+ timeline.push({ label, kind, t0: span.t0, t1: span.t1, patchable: TIMELINE_PATCHABLE[kind] ?? [], address: `timeline.${label}` });
1389
+ }
1390
+ }
1391
+ timeline.sort((a, b) => a.t0 - b.t0 || a.label.localeCompare(b.label));
1392
+ beats.sort((a, b) => a.t0 - b.t0 || a.name.localeCompare(b.name));
1393
+ const behaviors = (ir.behaviors ?? []).map((b) => ({
1394
+ target: b.target,
1395
+ prop: b.prop,
1396
+ kind: b.behavior.name,
1397
+ address: `behaviors.${b.target}.${b.prop}`
1398
+ }));
1399
+ let motionTotal = 0;
1400
+ let motionLabeled = 0;
1401
+ walkMotion(ir.timeline, (_kind, labeled) => {
1402
+ motionTotal++;
1403
+ if (labeled) motionLabeled++;
1404
+ });
1405
+ return {
1406
+ scene: {
1407
+ id: ir.id,
1408
+ duration: compiled.duration,
1409
+ fps: ir.fps ?? 30,
1410
+ size: ir.size,
1411
+ ...ir.background !== void 0 ? { background: ir.background } : {}
1412
+ },
1413
+ nodes,
1414
+ states,
1415
+ timeline,
1416
+ beats,
1417
+ behaviors,
1418
+ summary: {
1419
+ nodeCount: nodes.length,
1420
+ labeledSteps: timeline.length + beats.length,
1421
+ unlabeledMotionSteps: motionTotal - motionLabeled,
1422
+ motionAddressableRatio: motionTotal === 0 ? 1 : motionLabeled / motionTotal
1423
+ }
1424
+ };
1425
+ }
1426
+ function lintScene(compiled) {
1427
+ const findings = [];
1428
+ walkMotion(compiled.ir.timeline, (kind, labeled, target) => {
1429
+ if (labeled) return;
1430
+ const what = kind === "to" ? `to("${target}")` : `${kind} on "${target}"`;
1431
+ findings.push({
1432
+ rule: "unlabeled-motion",
1433
+ severity: "warn",
1434
+ message: `${what} has no label \u2014 its timing can't be retimed or redirected by an overlay, and a base regeneration can silently drop it. Add a stable label.`
1435
+ });
1436
+ });
1437
+ return findings;
1438
+ }
1439
+
1234
1440
  // ../core/src/camera.ts
1235
1441
  var CAMERA_ID = "camera";
1236
1442
  var CAMERA_PROPS2 = ["x", "y", "zoom", "rotation", "perspective"];
@@ -1654,7 +1860,8 @@ function evaluate(compiled, t) {
1654
1860
  const height = num2(id, "height", node.props.height);
1655
1861
  const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
1656
1862
  const fps = compiled.ir.fps ?? 30;
1657
- const start = node.props.start ?? 0;
1863
+ const startRaw = node.props.start;
1864
+ const start = typeof startRaw === "string" ? compiled.labelTimes.get(startRaw)?.t0 ?? 0 : startRaw ?? 0;
1658
1865
  const rate = node.props.rate ?? 1;
1659
1866
  const clipStart = node.props.clipStart ?? 0;
1660
1867
  const srcT = clipStart + Math.max(0, t - start) * rate;
@@ -1923,13 +2130,10 @@ function photoMontage(images, opts = {}) {
1923
2130
  const cy = H / 2;
1924
2131
  const nodes = [];
1925
2132
  const shots = [];
1926
- let clock = 0;
1927
2133
  slides.forEach((slide, i) => {
1928
2134
  const nid = `${id}-${i}`;
1929
2135
  const slideHold = Math.max(0.5, slide.hold ?? hold);
1930
2136
  const transition = Math.min(opts.transition ?? 0.6, slideHold * 0.9);
1931
- const shotStart = clock;
1932
- clock += slideHold;
1933
2137
  const kind = slide.ken ?? ["in", "out", "pan"][Math.floor(rand2() * 3)] ?? "in";
1934
2138
  const angle = rand2() * Math.PI * 2;
1935
2139
  const panFrac = 0.4 + rand2() * 0.35;
@@ -1955,7 +2159,7 @@ function photoMontage(images, opts = {}) {
1955
2159
  }
1956
2160
  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
2161
  nodes.push(
1958
- isVideoSrc(slide.src) ? video({ ...box, start: shotStart, volume: slide.volume ?? 0 }) : image(box)
2162
+ isVideoSrc(slide.src) ? video({ ...box, start: `shot-${i}`, volume: slide.volume ?? 0 }) : image(box)
1959
2163
  );
1960
2164
  const ken = tween(
1961
2165
  nid,
@@ -2009,903 +2213,6 @@ function photoMontage(images, opts = {}) {
2009
2213
  }
2010
2214
  var videoMontage = photoMontage;
2011
2215
 
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) {
2022
- let a = seed >>> 0 || 2654435769;
2023
- return () => {
2024
- a = a + 1831565813 | 0;
2025
- let t = Math.imul(a ^ a >>> 15, 1 | a);
2026
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
2027
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
2028
- };
2029
- }
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
- return {
2035
- e: clamp01(o.energy ?? 0.5),
2036
- sp: Math.max(0.25, o.speed ?? 1),
2037
- it: clamp01(o.intensity ?? 0.5),
2038
- from: o.from,
2039
- rand: rand2,
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
2047
- };
2048
- }
2049
- var dur = (base, sp) => base / sp;
2050
- function settleEase(e) {
2051
- return e < 0.34 ? "easeOutCubic" : e < 0.67 ? "easeOutBack" : "easeOutElastic";
2052
- }
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];
2063
- }
2064
- }
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" } })
2070
- )
2071
- );
2072
- }
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
- );
2080
- }
2081
- function motionPreset(name, opts) {
2082
- const c = ctx(opts);
2083
- switch (name) {
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
- ]);
2188
- }
2189
- }
2190
- }
2191
-
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 }
2207
- };
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 };
2225
- }
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 };
2229
- }
2230
- function deviceScreenCenter(name, opts = {}) {
2231
- const d = screenDims(name, opts);
2232
- return { x: d.cx, y: d.cy };
2233
- }
2234
- function deviceBounds(name, opts = {}) {
2235
- const b = BOUNDS[name];
2236
- return isLandscape(name, opts) ? { width: b.height, height: b.width } : { ...b };
2237
- }
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])];
2242
- }
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
- );
2283
- }
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
- ];
2394
- }
2395
- }
2396
- }
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" })));
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);
2581
- }
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
- };
2596
- }
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
- };
2612
- }
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];
2619
- }
2620
- const u = (p - 0.5) / 0.5;
2621
- return [-stride + 2 * stride * u, 138 - Math.sin(Math.PI * u) * lift];
2622
- }
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
- };
2639
- }
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" }));
2659
- }
2660
- return beat(run ? "run" : "walk", {}, [par(...children)]);
2661
- }
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
- ));
2694
- }
2695
- keys.push(tween(c.g, { y: y0 }, { duration: d }));
2696
- return beat("dance", {}, [seq(...keys)]);
2697
- }
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)]);
2707
- }
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 })));
2716
- }
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}"`);
2744
- }
2745
- }
2746
- return c.label && tl.kind === "beat" ? { ...tl, name: c.label } : tl;
2747
- }
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`;
2755
- }
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"
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;
2798
- return {
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
- ]
2824
- };
2825
- }
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;
2829
- return {
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
- ]
2861
- };
2862
- }
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
- };
2894
- }
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);
2907
- }
2908
-
2909
2216
  // ../core/src/textMetrics.ts
2910
2217
  var INTER_ADVANCE = {
2911
2218
  "400": {
@@ -3200,212 +2507,1320 @@ var INTER_ADVANCE = {
3200
2507
  "~": 68.55
3201
2508
  }
3202
2509
  };
3203
- var INTER_FALLBACK = {
3204
- "400": 56.16,
3205
- "700": 58.74,
3206
- "800": 59.79
2510
+ var INTER_FALLBACK = {
2511
+ "400": 56.16,
2512
+ "700": 58.74,
2513
+ "800": 59.79
2514
+ };
2515
+
2516
+ // ../core/src/textFx.ts
2517
+ var clamp01 = (v) => Math.max(0, Math.min(1, v));
2518
+ var fract = (v) => v - Math.floor(v);
2519
+ var rand = (i, salt) => fract(Math.sin(i * 127.1 + salt * 311.7) * 43758.5453);
2520
+ var dur = (base, sp) => base / sp;
2521
+ var SCRAMBLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&@";
2522
+ var advance = (ch, weight, fontSize) => (INTER_ADVANCE[weight]?.[ch] ?? INTER_FALLBACK[weight]) * (fontSize / 100);
2523
+ function splitText(textStr, opts) {
2524
+ const { id, x, y, fontSize } = opts;
2525
+ const weight = opts.fontWeight ?? 800;
2526
+ const fill = opts.fill ?? "#FFFFFF";
2527
+ const ls = opts.letterSpacing ?? 0;
2528
+ const align = opts.align ?? "center";
2529
+ const unit = opts.unit ?? "glyph";
2530
+ const opacity = opts.opacity ?? 0;
2531
+ const chars = [...textStr];
2532
+ let total = 0;
2533
+ chars.forEach((ch, i) => {
2534
+ total += advance(ch, weight, fontSize) + (i < chars.length - 1 ? ls : 0);
2535
+ });
2536
+ let cursor2 = align === "center" ? x - total / 2 : x;
2537
+ const glyphs = [];
2538
+ const nodes = [];
2539
+ const mk = (ch, cx, adv, lsProp) => {
2540
+ const g = { id: `${id}-${glyphs.length}`, ch, x: cx, y, advance: adv, i: glyphs.length };
2541
+ glyphs.push(g);
2542
+ nodes.push(
2543
+ text({
2544
+ id: g.id,
2545
+ x: cx,
2546
+ y,
2547
+ content: ch,
2548
+ fontFamily: "Inter",
2549
+ fontSize,
2550
+ fontWeight: weight,
2551
+ fill,
2552
+ anchor: "center",
2553
+ opacity,
2554
+ ...lsProp ? { letterSpacing: lsProp } : {}
2555
+ })
2556
+ );
2557
+ };
2558
+ if (unit === "word") {
2559
+ let i = 0;
2560
+ while (i < chars.length) {
2561
+ if (chars[i] === " ") {
2562
+ cursor2 += advance(" ", weight, fontSize) + ls;
2563
+ i++;
2564
+ continue;
2565
+ }
2566
+ let word = "";
2567
+ let w = 0;
2568
+ const startCursor = cursor2;
2569
+ while (i < chars.length && chars[i] !== " ") {
2570
+ const a = advance(chars[i], weight, fontSize);
2571
+ word += chars[i];
2572
+ w += a + (chars[i + 1] && chars[i + 1] !== " " ? ls : 0);
2573
+ i++;
2574
+ }
2575
+ mk(word, startCursor + w / 2, w, ls);
2576
+ cursor2 = startCursor + w + ls;
2577
+ }
2578
+ } else {
2579
+ chars.forEach((ch) => {
2580
+ const a = advance(ch, weight, fontSize);
2581
+ if (ch !== " ") mk(ch, cursor2 + a / 2, a);
2582
+ cursor2 += a + ls;
2583
+ });
2584
+ }
2585
+ return { nodes, glyphs, ids: glyphs.map((g) => g.id), width: total, x, y, fontSize };
2586
+ }
2587
+ var ctx = (o) => ({
2588
+ sp: Math.max(0.25, o.speed ?? 1),
2589
+ e: clamp01(o.energy ?? 0.5),
2590
+ seed: o.seed ?? 0,
2591
+ fs: 0,
2592
+ stag: o.stagger
2593
+ });
2594
+ var IN_STAGGER = { typewriter: 0.065, cascade: 0.04, rise: 0.03, bounce: 0.045, assemble: 0.05, decode: 0.05 };
2595
+ function glyphIn(name, g, c) {
2596
+ const set = (props) => tween(g.id, props, { duration: 1e-3 });
2597
+ const rs = (salt) => rand(g.i, salt + c.seed);
2598
+ switch (name) {
2599
+ case "typewriter":
2600
+ return tween(g.id, { opacity: 1 }, { duration: dur(0.04, c.sp), ease: "linear" });
2601
+ case "cascade":
2602
+ return seq(
2603
+ set({ y: g.y + 56, opacity: 0 }),
2604
+ par(
2605
+ tween(g.id, { opacity: 1 }, { duration: dur(0.22, c.sp), ease: "easeOutQuad" }),
2606
+ tween(g.id, { y: g.y }, { duration: dur(0.34, c.sp), ease: "easeOutCubic" })
2607
+ )
2608
+ );
2609
+ case "rise":
2610
+ return seq(
2611
+ set({ y: g.y + 36, opacity: 0 }),
2612
+ par(
2613
+ tween(g.id, { opacity: 1 }, { duration: dur(0.3, c.sp), ease: "easeOutQuad" }),
2614
+ tween(g.id, { y: g.y }, { duration: dur(0.4, c.sp), ease: "easeOutQuad" })
2615
+ )
2616
+ );
2617
+ case "bounce":
2618
+ return seq(
2619
+ set({ y: g.y - 80 * (0.6 + c.e), opacity: 0, scale: 0.7 }),
2620
+ par(
2621
+ tween(g.id, { opacity: 1 }, { duration: dur(0.2, c.sp), ease: "easeOutQuad" }),
2622
+ tween(g.id, { y: g.y, scale: 1 }, { duration: dur(0.7, c.sp), ease: "easeOutBounce" })
2623
+ )
2624
+ );
2625
+ case "assemble":
2626
+ return seq(
2627
+ 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 }),
2628
+ par(
2629
+ tween(g.id, { opacity: 1 }, { duration: dur(0.4, c.sp), ease: "easeOutQuad" }),
2630
+ tween(g.id, { x: g.x, y: g.y, rotation: 0, scale: 1 }, { duration: dur(0.8, c.sp), ease: "easeOutExpo" })
2631
+ )
2632
+ );
2633
+ case "decode": {
2634
+ const steps = 4 + Math.floor(rs(7) * 3);
2635
+ const flicker = [set({ opacity: 1 })];
2636
+ for (let k = 0; k < steps; k++) {
2637
+ flicker.push(tween(g.id, { content: SCRAMBLE[Math.floor(rand(g.i, 20 + k + c.seed) * SCRAMBLE.length)] }, { duration: dur(0.05, c.sp), ease: "linear" }));
2638
+ }
2639
+ flicker.push(tween(g.id, { content: g.ch }, { duration: dur(0.05, c.sp), ease: "linear" }));
2640
+ return seq(...flicker);
2641
+ }
2642
+ }
2643
+ }
2644
+ function textIn(name, block, opts = {}) {
2645
+ const c = { ...ctx(opts), fs: block.fontSize };
2646
+ const interval = (c.stag ?? IN_STAGGER[name]) / c.sp;
2647
+ return beat(opts.label ?? `text-in-${name}`, {}, [stagger(interval, ...block.glyphs.map((g) => glyphIn(name, g, c)))]);
2648
+ }
2649
+ function textLoop(name, block, opts = {}) {
2650
+ const win = { ...opts.from !== void 0 && { from: opts.from }, ...opts.until !== void 0 && { until: opts.until }, ...opts.ramp !== void 0 && { ramp: opts.ramp } };
2651
+ const f = opts.frequency ?? (name === "wave" ? 0.9 : name === "shimmer" ? 1.4 : 0.7);
2652
+ const ps = opts.phaseStep ?? 0.55;
2653
+ return block.glyphs.map((g, i) => {
2654
+ switch (name) {
2655
+ case "wave":
2656
+ return oscillate(g.id, "y", { amplitude: opts.amplitude ?? 9, frequency: f, phase: i * ps }, win);
2657
+ case "shimmer":
2658
+ return oscillate(g.id, "opacity", { amplitude: opts.amplitude ?? 0.25, frequency: f, phase: i * ps }, win);
2659
+ case "wobble":
2660
+ return oscillate(g.id, "rotation", { amplitude: opts.amplitude ?? 6, frequency: f, phase: i * ps }, win);
2661
+ case "float":
2662
+ return oscillate(g.id, "y", { amplitude: opts.amplitude ?? 5, frequency: f, phase: i * ps }, win);
2663
+ }
2664
+ });
2665
+ }
2666
+ var OUT_STAGGER = { shatter: 0.02, fly: 0.012, dissolve: 0, fall: 0.02, collapse: 0.02 };
2667
+ function glyphOut(name, g, c, block, dir) {
2668
+ const rs = (salt) => rand(g.i, salt + c.seed);
2669
+ switch (name) {
2670
+ case "shatter":
2671
+ return par(
2672
+ tween(g.id, { x: g.x + (rs(21) - 0.5) * 1100 * (0.6 + c.e), y: g.y + (rs(22) - 0.5) * 760 }, { duration: dur(0.7, c.sp), ease: "easeInCubic" }),
2673
+ tween(g.id, { rotation: (rs(23) - 0.5) * 300, opacity: 0 }, { duration: dur(0.7, c.sp), ease: "easeInQuad" })
2674
+ );
2675
+ case "fly":
2676
+ return par(
2677
+ tween(g.id, { x: g.x + dir[0] * 1200, y: g.y + dir[1] * 1200 }, { duration: dur(0.6, c.sp), ease: "easeInCubic" }),
2678
+ tween(g.id, { opacity: 0 }, { duration: dur(0.5, c.sp), ease: "easeInQuad" })
2679
+ );
2680
+ case "dissolve":
2681
+ return seq(wait(rs(31) * 0.5), par(
2682
+ tween(g.id, { opacity: 0 }, { duration: dur(0.4, c.sp), ease: "easeInQuad" }),
2683
+ tween(g.id, { scale: 1.4 }, { duration: dur(0.4, c.sp), ease: "easeOutQuad" })
2684
+ ));
2685
+ case "fall":
2686
+ return par(
2687
+ tween(g.id, { y: g.y + 700 + rs(41) * 200 }, { duration: dur(0.8, c.sp), ease: "easeInQuad" }),
2688
+ tween(g.id, { rotation: (rs(42) - 0.5) * 120, opacity: 0 }, { duration: dur(0.8, c.sp), ease: "easeInQuad" })
2689
+ );
2690
+ case "collapse":
2691
+ return par(
2692
+ tween(g.id, { x: block.x, y: block.y, scale: 0.2 }, { duration: dur(0.5, c.sp), ease: "easeInBack" }),
2693
+ tween(g.id, { opacity: 0 }, { duration: dur(0.5, c.sp), ease: "easeInQuad" })
2694
+ );
2695
+ }
2696
+ }
2697
+ function textOut(name, block, opts = {}) {
2698
+ const c = { ...ctx(opts), fs: block.fontSize };
2699
+ const dir = opts.dir ?? [0, -1];
2700
+ const steps = block.glyphs.map((g) => glyphOut(name, g, c, block, dir));
2701
+ const interval = (c.stag ?? OUT_STAGGER[name]) / c.sp;
2702
+ const body = interval > 0 ? stagger(interval, ...steps) : par(...steps);
2703
+ return beat(opts.label ?? `text-out-${name}`, {}, [body]);
2704
+ }
2705
+ function textTypeCues(block, opts) {
2706
+ const interval = opts.interval ?? 0.065;
2707
+ const gain = opts.gain ?? 0.4;
2708
+ const off = opts.offset ?? 0;
2709
+ const KEYS = ["001", "004", "007", "010", "014"];
2710
+ return block.glyphs.map((g, i) => ({
2711
+ at: opts.at,
2712
+ offset: off + i * interval,
2713
+ file: `keypress-${KEYS[i % KEYS.length]}.wav`,
2714
+ gain: gain + 0.2 * rand(i, 31)
2715
+ }));
2716
+ }
2717
+
2718
+ // ../core/src/titles.ts
2719
+ function title(opts) {
2720
+ const id = opts.id ?? "title";
2721
+ const block = splitText(opts.text, {
2722
+ id,
2723
+ x: opts.x ?? 960,
2724
+ y: opts.y ?? 540,
2725
+ fontSize: opts.fontSize ?? 96,
2726
+ ...opts.fontWeight !== void 0 && { fontWeight: opts.fontWeight },
2727
+ ...opts.fill !== void 0 && { fill: opts.fill },
2728
+ ...opts.letterSpacing !== void 0 && { letterSpacing: opts.letterSpacing }
2729
+ });
2730
+ const fx = {
2731
+ ...opts.speed !== void 0 && { speed: opts.speed },
2732
+ ...opts.seed !== void 0 && { seed: opts.seed }
2733
+ };
2734
+ const entrance = textIn(opts.entrance ?? "cascade", block, { ...fx, label: `${id}-in` });
2735
+ if (!opts.exit) return { nodes: block.nodes, timeline: entrance, block };
2736
+ const timeline = seq(
2737
+ entrance,
2738
+ wait(Math.max(0, opts.hold ?? 2)),
2739
+ textOut(opts.exit, block, { ...fx, label: `${id}-out` })
2740
+ );
2741
+ return { nodes: block.nodes, timeline, block };
2742
+ }
2743
+ function lowerThird(opts) {
2744
+ const id = opts.id ?? "lt";
2745
+ const x = opts.x ?? 120;
2746
+ const y = opts.y ?? 860;
2747
+ const fs = opts.fontSize ?? 48;
2748
+ const roleFs = Math.round(fs * 0.58);
2749
+ const barH = opts.role ? fs + roleFs + 24 : fs + 16;
2750
+ const restX = 28;
2751
+ const startX = restX - 8;
2752
+ const children = [
2753
+ rect({ id: `${id}-bar`, x: 0, y: 0, width: 6, height: barH, anchor: "top-left", fill: opts.accent ?? "#FF4D00", scaleY: 0 }),
2754
+ text({ id: `${id}-name`, x: startX, y: 6, anchor: "top-left", content: opts.name, fontFamily: "Inter", fontSize: fs, fontWeight: 700, fill: opts.fill ?? "#FFFFFF", opacity: 0 })
2755
+ ];
2756
+ if (opts.role !== void 0) {
2757
+ children.push(
2758
+ text({ id: `${id}-role`, x: startX, y: 6 + fs + 8, anchor: "top-left", content: opts.role, fontFamily: "Inter", fontSize: roleFs, fill: opts.subFill ?? "#C9C9C9", opacity: 0 })
2759
+ );
2760
+ }
2761
+ const nodes = [group({ id, x, y }, children)];
2762
+ const entrance = beat(`${id}-in`, {}, [
2763
+ par(
2764
+ tween(`${id}-bar`, { scaleY: 1 }, { duration: 0.5, ease: "easeOutCubic" }),
2765
+ seq(wait(0.08), tween(`${id}-name`, { opacity: 1, x: restX }, { duration: 0.45, ease: "easeOutCubic" })),
2766
+ ...opts.role !== void 0 ? [seq(wait(0.16), tween(`${id}-role`, { opacity: 1, x: restX }, { duration: 0.45, ease: "easeOutCubic" }))] : []
2767
+ )
2768
+ ]);
2769
+ const exit = beat(`${id}-out`, {}, [
2770
+ par(
2771
+ tween(`${id}-bar`, { scaleY: 0 }, { duration: 0.35, ease: "easeInCubic" }),
2772
+ tween(`${id}-name`, { opacity: 0 }, { duration: 0.3, ease: "easeInCubic" }),
2773
+ ...opts.role !== void 0 ? [tween(`${id}-role`, { opacity: 0 }, { duration: 0.3, ease: "easeInCubic" })] : []
2774
+ )
2775
+ ]);
2776
+ return { nodes, timeline: seq(entrance, wait(Math.max(0, opts.hold ?? 3)), exit) };
2777
+ }
2778
+
2779
+ // ../core/src/presets.ts
2780
+ var PRESET_NAMES = [
2781
+ "draw-bloom",
2782
+ "punch-in",
2783
+ "rise-settle",
2784
+ "slide-bank",
2785
+ "reveal-orbit",
2786
+ "spin-forge"
2787
+ ];
2788
+ function makeRng2(seed) {
2789
+ let a = seed >>> 0 || 2654435769;
2790
+ return () => {
2791
+ a = a + 1831565813 | 0;
2792
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
2793
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
2794
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
2795
+ };
2796
+ }
2797
+ var clamp012 = (x) => Math.max(0, Math.min(1, x));
2798
+ var SET = 1 / 120;
2799
+ function ctx2(o) {
2800
+ const rand2 = makeRng2((o.seed ?? 0) + 1);
2801
+ return {
2802
+ e: clamp012(o.energy ?? 0.5),
2803
+ sp: Math.max(0.25, o.speed ?? 1),
2804
+ it: clamp012(o.intensity ?? 0.5),
2805
+ from: o.from,
2806
+ rand: rand2,
2807
+ jit: (amp) => (rand2() - 0.5) * 2 * amp,
2808
+ g: o.target.group,
2809
+ cx: o.target.center[0],
2810
+ cy: o.target.center[1],
2811
+ s: o.target.baseScale,
2812
+ fills: o.target.fills,
2813
+ inks: o.target.inks
2814
+ };
2815
+ }
2816
+ var dur2 = (base, sp) => base / sp;
2817
+ function settleEase(e) {
2818
+ return e < 0.34 ? "easeOutCubic" : e < 0.67 ? "easeOutBack" : "easeOutElastic";
2819
+ }
2820
+ function fromVec(from, dist) {
2821
+ switch (from) {
2822
+ case "left":
2823
+ return [-dist, 0];
2824
+ case "right":
2825
+ return [dist, 0];
2826
+ case "top":
2827
+ return [0, -dist];
2828
+ default:
2829
+ return [0, dist];
2830
+ }
2831
+ }
2832
+ function fadeFills(c, base = 0.4, gap = 0.06) {
2833
+ return stagger(
2834
+ gap / c.sp,
2835
+ ...c.fills.map(
2836
+ (id, i) => tween(id, { opacity: 1 }, { duration: dur2(base, c.sp), ease: "easeOutQuad", ...i === 0 && { label: "reveal" } })
2837
+ )
2838
+ );
2839
+ }
2840
+ function drawInks(c) {
2841
+ return stagger(
2842
+ 0.15 / c.sp,
2843
+ ...c.inks.map(
2844
+ (id, i) => tween(id, { progress: 1 }, { duration: dur2(1.3 + c.jit(0.2), c.sp), ease: "easeInOutQuad", ...i === 0 && { label: "draw" } })
2845
+ )
2846
+ );
2847
+ }
2848
+ function motionPreset(name, opts) {
2849
+ const c = ctx2(opts);
2850
+ switch (name) {
2851
+ case "draw-bloom":
2852
+ return beat("draw-bloom", {}, [
2853
+ drawInks(c),
2854
+ fadeFills(c, 0.45),
2855
+ tween(c.g, { scale: c.s * (1.02 + 0.05 * c.e) }, { duration: dur2(2.4, c.sp), ease: "easeInOutQuad", label: "settle" })
2856
+ ]);
2857
+ case "punch-in": {
2858
+ const peak = c.s * (1 + 0.06 + 0.24 * c.e + c.jit(0.02));
2859
+ return beat("punch-in", {}, [
2860
+ par(
2861
+ fadeFills(c, 0.25),
2862
+ seq(
2863
+ tween(c.g, { scale: peak }, { duration: dur2(0.45 + c.jit(0.05), c.sp), ease: "easeOutCubic", label: "punch" }),
2864
+ tween(c.g, { scale: c.s }, { duration: dur2(0.5, c.sp), ease: settleEase(c.e) })
2865
+ )
2866
+ )
2867
+ ]);
2868
+ }
2869
+ case "rise-settle": {
2870
+ const es = 0.65 + c.rand() * 0.7;
2871
+ const dist = (220 + 260 * c.it) * es;
2872
+ const [dx, dy] = fromVec(c.from ?? "bottom", dist);
2873
+ const jx = c.jit(110);
2874
+ return beat("rise-settle", {}, [
2875
+ par(
2876
+ motionPath(
2877
+ c.g,
2878
+ [
2879
+ [c.cx + dx + jx, c.cy + dy],
2880
+ [c.cx + dx * 0.4 - jx * 0.6, c.cy + dy * 0.4],
2881
+ [c.cx, c.cy]
2882
+ ],
2883
+ { duration: dur2(1.1, c.sp), ease: settleEase(c.e), label: "rise" }
2884
+ ),
2885
+ fadeFills(c, 0.4)
2886
+ )
2887
+ ]);
2888
+ }
2889
+ case "slide-bank": {
2890
+ const es = 0.65 + c.rand() * 0.7;
2891
+ const dist = (420 + 240 * c.it) * es;
2892
+ const [dx, dy] = fromVec(c.from ?? "left", dist);
2893
+ const arc = c.jit(140);
2894
+ const midx = c.jit(120);
2895
+ const move = dur2(1.2, c.sp);
2896
+ return beat("slide-bank", {}, [
2897
+ par(
2898
+ motionPath(
2899
+ c.g,
2900
+ [
2901
+ [c.cx + dx, c.cy + dy],
2902
+ [c.cx + dx * 0.4 + midx, c.cy + dy * 0.4 - 70 - arc],
2903
+ [c.cx, c.cy]
2904
+ ],
2905
+ { duration: move, ease: settleEase(c.e), autoRotate: true, label: "slide" }
2906
+ ),
2907
+ // level the bank out once it lands (authored after the path → wins for rotation)
2908
+ seq(wait(move), tween(c.g, { rotation: 0 }, { duration: dur2(0.5, c.sp), ease: "easeOutCubic" })),
2909
+ fadeFills(c, 0.4)
2910
+ )
2911
+ ]);
2912
+ }
2913
+ case "reveal-orbit": {
2914
+ const es = 0.65 + c.rand() * 0.7;
2915
+ const orbit = (180 + 160 * c.it) * es;
2916
+ const jx = c.jit(0.4);
2917
+ const jy = c.jit(0.4);
2918
+ return beat("reveal-orbit", {}, [
2919
+ drawInks(c),
2920
+ fadeFills(c, 0.45),
2921
+ par(
2922
+ motionPath(
2923
+ c.g,
2924
+ [
2925
+ [c.cx, c.cy],
2926
+ [c.cx - orbit * (1 + jx), c.cy - orbit * 0.8],
2927
+ [c.cx + orbit * (1 + jy), c.cy - orbit],
2928
+ [c.cx, c.cy]
2929
+ ],
2930
+ { duration: dur2(1.7, c.sp), ease: "easeInOutCubic", label: "orbit" }
2931
+ ),
2932
+ seq(
2933
+ tween(c.g, { scale: c.s * (1.12 + 0.1 * c.e) }, { duration: dur2(0.85, c.sp), ease: "easeOutBack" }),
2934
+ tween(c.g, { scale: c.s }, { duration: dur2(0.85, c.sp), ease: "easeInOutQuad" })
2935
+ )
2936
+ )
2937
+ ]);
2938
+ }
2939
+ case "spin-forge": {
2940
+ const turns = 1 + Math.round(c.it);
2941
+ const dir = c.rand() < 0.5 ? -1 : 1;
2942
+ const startRot = dir * 360 * turns;
2943
+ const peak = c.s * (1 + 0.05 + 0.2 * c.e);
2944
+ return beat("spin-forge", {}, [
2945
+ par(
2946
+ seq(
2947
+ tween(c.g, { scale: c.s * 0.2, rotation: startRot }, { duration: SET }),
2948
+ // establish (invisible)
2949
+ tween(c.g, { scale: peak, rotation: 0 }, { duration: dur2(0.9, c.sp), ease: "easeOutBack", label: "spin" }),
2950
+ tween(c.g, { scale: c.s }, { duration: dur2(0.3, c.sp), ease: "easeInOutQuad" })
2951
+ ),
2952
+ seq(wait(SET), fadeFills(c, 0.3))
2953
+ )
2954
+ ]);
2955
+ }
2956
+ }
2957
+ }
2958
+
2959
+ // ../core/src/devicePreset.ts
2960
+ var DEVICE_PRESET_NAMES = ["phone", "tablet", "laptop", "browser", "watch", "monitor", "tv", "foldable", "terminal", "car"];
2961
+ var DARK = {
2962
+ body: "#15161C",
2963
+ bodyStroke: "#2A2D38",
2964
+ bodyGrad: ["#272A35", "#0E0F15"],
2965
+ screen: "#0E0F15",
2966
+ detail: "#3A3D48",
2967
+ chrome: "#1B1D24",
2968
+ chromeGrad: ["#23262F", "#141620"],
2969
+ chromeText: "#9AA0AD",
2970
+ ambient: "#9FB4FF"
2971
+ };
2972
+ var LIGHT = {
2973
+ body: "#E7E9EE",
2974
+ bodyStroke: "#C3C7D1",
2975
+ bodyGrad: ["#FAFBFD", "#D4D8E1"],
2976
+ screen: "#FFFFFF",
2977
+ detail: "#AEB3C0",
2978
+ chrome: "#F2F3F6",
2979
+ chromeGrad: ["#FCFCFE", "#E2E5EC"],
2980
+ chromeText: "#5B606C",
2981
+ ambient: "#7C9BFF"
2982
+ };
2983
+ var NEON_ACCENTS = ["#00E5FF", "#FF3DCB", "#7C5CFF", "#3DFF88", "#FF7A00"];
2984
+ var SCREENS = {
2985
+ phone: { width: 352, height: 736, radius: 38 },
2986
+ tablet: { width: 544, height: 764, radius: 18 },
2987
+ laptop: { width: 840, height: 520, radius: 8, cy: -150 },
2988
+ browser: { width: 984, height: 568, radius: 6, cy: 24 },
2989
+ watch: { width: 184, height: 224, radius: 44 },
2990
+ monitor: { width: 1056, height: 600, radius: 6 },
2991
+ tv: { width: 1280, height: 720, radius: 8, cy: -24 },
2992
+ foldable: { width: 760, height: 560, radius: 20 },
2993
+ terminal: { width: 900, height: 560, radius: 6, cy: 18 },
2994
+ car: { width: 1e3, height: 520, radius: 24 }
2995
+ };
2996
+ var BOUNDS = {
2997
+ phone: { width: 392, height: 812 },
2998
+ tablet: { width: 600, height: 820 },
2999
+ laptop: { width: 1100, height: 650 },
3000
+ browser: { width: 1e3, height: 660 },
3001
+ watch: { width: 220, height: 300 },
3002
+ monitor: { width: 1120, height: 860 },
3003
+ tv: { width: 1340, height: 920 },
3004
+ foldable: { width: 800, height: 600 },
3005
+ terminal: { width: 916, height: 636 },
3006
+ car: { width: 1060, height: 600 }
3007
+ };
3008
+ var isLandscape = (name, o) => (name === "phone" || name === "tablet") && o.orientation === "landscape";
3009
+ function screenDims(name, o) {
3010
+ const d = SCREENS[name];
3011
+ const base = { cx: d.cx ?? 0, cy: d.cy ?? 0 };
3012
+ return isLandscape(name, o) ? { width: d.height, height: d.width, radius: d.radius, ...base } : { width: d.width, height: d.height, radius: d.radius, ...base };
3013
+ }
3014
+ function deviceScreen(name, opts = {}) {
3015
+ const d = screenDims(name, opts);
3016
+ return { x: 0, y: 0, width: d.width, height: d.height, radius: d.radius };
3017
+ }
3018
+ function deviceScreenCenter(name, opts = {}) {
3019
+ const d = screenDims(name, opts);
3020
+ return { x: d.cx, y: d.cy };
3021
+ }
3022
+ function deviceBounds(name, opts = {}) {
3023
+ const b = BOUNDS[name];
3024
+ return isLandscape(name, opts) ? { width: b.height, height: b.width } : { ...b };
3025
+ }
3026
+ function deviceScreenPoint(name, opts, local) {
3027
+ const c = deviceScreenCenter(name, opts);
3028
+ const s = opts.scale ?? 1;
3029
+ return [(opts.x ?? 0) + s * (c.x + local[0]), (opts.y ?? 0) + s * (c.y + local[1])];
3030
+ }
3031
+ function makeRng3(seed) {
3032
+ let a = seed >>> 0 || 2654435769;
3033
+ return () => {
3034
+ a = a + 1831565813 | 0;
3035
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
3036
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
3037
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
3038
+ };
3039
+ }
3040
+ function hashId(id) {
3041
+ let h = 2166136261;
3042
+ for (let i = 0; i < id.length; i++) {
3043
+ h ^= id.charCodeAt(i);
3044
+ h = Math.imul(h, 16777619);
3045
+ }
3046
+ return h >>> 0;
3047
+ }
3048
+ function makeCtx(opts) {
3049
+ const id = opts.id ?? "device";
3050
+ const palette = opts.color === "light" ? LIGHT : DARK;
3051
+ const material = opts.material ?? "premium";
3052
+ const style = opts.style ?? "glass";
3053
+ const seed = (opts.seed ?? hashId(id)) >>> 0;
3054
+ const rng = makeRng3(seed + 1);
3055
+ const accent = NEON_ACCENTS[Math.floor(rng() * NEON_ACCENTS.length)] ?? NEON_ACCENTS[0];
3056
+ return {
3057
+ id,
3058
+ palette,
3059
+ material,
3060
+ style,
3061
+ accent,
3062
+ s: opts.scale ?? 1,
3063
+ rng,
3064
+ jit: (amp) => (rng() - 0.5) * 2 * amp,
3065
+ o: opts
3066
+ };
3067
+ }
3068
+ var isPremium = (c) => c.material === "premium";
3069
+ var isNeon = (c) => c.material === "premium" && c.style === "neon";
3070
+ function premiumFill(c, solid, grad) {
3071
+ if (!isPremium(c) || c.style === "neon") return solid;
3072
+ return linearGradient(grad, { angle: 125 + c.jit(12) });
3073
+ }
3074
+ var bodyStroke = (c, fallback) => isNeon(c) ? c.accent : fallback;
3075
+ function bodyFx(c) {
3076
+ if (!isPremium(c)) return {};
3077
+ return isNeon(c) ? glow(c.accent, (24 + c.jit(5)) * c.s) : dropShadow("rgba(0,0,0,0.5)", 46 * c.s, 0, 24 * c.s);
3078
+ }
3079
+ function slab(c, suffix, x, y, w, h, r, grad, solid, strokeC) {
3080
+ return rect({
3081
+ id: `${c.id}-${suffix}`,
3082
+ x,
3083
+ y,
3084
+ anchor: "center",
3085
+ width: w,
3086
+ height: h,
3087
+ radius: r,
3088
+ fill: premiumFill(c, solid, grad),
3089
+ stroke: bodyStroke(c, strokeC),
3090
+ strokeWidth: isNeon(c) ? 2.5 : 2,
3091
+ ...bodyFx(c)
3092
+ });
3093
+ }
3094
+ function screenStack(c, cx, cy, dims, content) {
3095
+ const children = [
3096
+ rect({ id: `${c.id}-screenbg`, x: 0, y: 0, anchor: "center", width: dims.width, height: dims.height, fill: c.o.screen ?? c.palette.screen })
3097
+ ];
3098
+ if (isPremium(c)) {
3099
+ const tint = isNeon(c) ? c.accent : c.palette.ambient;
3100
+ children.push(
3101
+ rect({
3102
+ id: `${c.id}-ambient`,
3103
+ x: 0,
3104
+ y: 0,
3105
+ anchor: "center",
3106
+ width: dims.width,
3107
+ height: dims.height,
3108
+ fill: radialGradient([`${tint}33`, `${tint}00`], { cy: 0.32, r: 0.72 }),
3109
+ opacity: isNeon(c) ? 0.55 : 0.32,
3110
+ blend: "screen"
3111
+ })
3112
+ );
3113
+ }
3114
+ children.push(group({ id: `${c.id}-content`, x: 0, y: 0 }, content));
3115
+ if (isPremium(c) && c.style === "glass") {
3116
+ const ang = 22 + c.jit(9);
3117
+ children.push(
3118
+ rect({
3119
+ id: `${c.id}-glare`,
3120
+ x: -dims.width * 0.18,
3121
+ y: 0,
3122
+ anchor: "center",
3123
+ width: dims.width * 0.46,
3124
+ height: dims.height * 1.8,
3125
+ fill: "#FFFFFF",
3126
+ opacity: 0.05 + Math.abs(c.jit(0.025)),
3127
+ rotation: ang,
3128
+ blend: "screen"
3129
+ })
3130
+ );
3131
+ }
3132
+ return group({ id: `${c.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 } }, children);
3133
+ }
3134
+ function phoneNotch(c, style, sh) {
3135
+ const yTop = -sh / 2 + 16;
3136
+ switch (style) {
3137
+ case "none":
3138
+ return null;
3139
+ case "punch":
3140
+ return rect({ id: `${c.id}-notch`, x: 0, y: yTop + 2, anchor: "center", width: 18, height: 18, fill: "#000000", radius: 9 });
3141
+ case "notch":
3142
+ return rect({ id: `${c.id}-notch`, x: 0, y: -sh / 2 + 11, anchor: "center", width: 150, height: 26, fill: "#000000", radius: 13 });
3143
+ case "island":
3144
+ default:
3145
+ return rect({ id: `${c.id}-notch`, x: 0, y: yTop, anchor: "center", width: 96, height: 30, fill: "#000000", radius: 15 });
3146
+ }
3147
+ }
3148
+ var urlText = (url) => {
3149
+ const u = url ?? "reframe.video";
3150
+ return u.length > 70 ? `${u.slice(0, 67)}\u2026` : u;
3151
+ };
3152
+ var CHASSIS = {
3153
+ phone: (c, dims, content) => phoneOrTablet("phone", c, dims, content),
3154
+ tablet: (c, dims, content) => phoneOrTablet("tablet", c, dims, content),
3155
+ laptop: (c, dims, content) => {
3156
+ const p = c.palette;
3157
+ const sw = dims.width, sh = dims.height;
3158
+ const lidTop = dims.cy - (sh + 40) / 2;
3159
+ const keyRows = [0, 1, 2, 3].map(
3160
+ (r) => rect({ id: `${c.id}-keys${r}`, x: 0, y: 150 + r * 11, anchor: "center", width: 640 + r * 50, height: 6, fill: p.chrome, radius: 3 })
3161
+ );
3162
+ return [
3163
+ path({ id: `${c.id}-base`, x: 0, y: 0, d: "M -450 140 L 450 140 L 520 196 L -520 196 Z", fill: premiumFill(c, p.body, p.bodyGrad), stroke: bodyStroke(c, p.bodyStroke), strokeWidth: isNeon(c) ? 2.5 : 2, ...bodyFx(c) }),
3164
+ rect({ id: `${c.id}-foot-l`, x: -360, y: 198, anchor: "center", width: 70, height: 5, fill: p.detail, radius: 3 }),
3165
+ rect({ id: `${c.id}-foot-r`, x: 360, y: 198, anchor: "center", width: 70, height: 5, fill: p.detail, radius: 3 }),
3166
+ ...keyRows,
3167
+ rect({ id: `${c.id}-trackpad`, x: 0, y: 184, anchor: "center", width: 150, height: 8, fill: p.detail, radius: 4 }),
3168
+ rect({ id: `${c.id}-hinge`, x: 0, y: 134, anchor: "center", width: 900, height: 10, fill: p.detail, radius: 5 }),
3169
+ screenStack(c, dims.cx, dims.cy, dims, content),
3170
+ ellipse({ id: `${c.id}-webcam`, x: 0, y: lidTop + 14, anchor: "center", width: 6, height: 6, fill: p.detail }),
3171
+ rect({ id: `${c.id}-lid`, x: 0, y: dims.cy, anchor: "center", width: sw + 40, height: sh + 40, stroke: bodyStroke(c, p.bodyStroke), strokeWidth: 2, radius: 18 })
3172
+ ];
3173
+ },
3174
+ browser: (c, dims, content) => {
3175
+ const p = c.palette;
3176
+ const sw = dims.width, sh = dims.height;
3177
+ const winW = sw + 16;
3178
+ const winH = sh + 92;
3179
+ const barY = -winH / 2 + 24;
3180
+ return [
3181
+ rect({ id: `${c.id}-win`, x: 0, y: 0, anchor: "center", width: winW, height: winH, fill: premiumFill(c, p.chrome, p.chromeGrad), stroke: bodyStroke(c, p.bodyStroke), strokeWidth: 1.5, radius: 14, ...bodyFx(c) }),
3182
+ ellipse({ id: `${c.id}-dot1`, x: -winW / 2 + 30, y: barY, anchor: "center", width: 13, height: 13, fill: "#FF5F57" }),
3183
+ ellipse({ id: `${c.id}-dot2`, x: -winW / 2 + 54, y: barY, anchor: "center", width: 13, height: 13, fill: "#FEBC2E" }),
3184
+ ellipse({ id: `${c.id}-dot3`, x: -winW / 2 + 78, y: barY, anchor: "center", width: 13, height: 13, fill: "#28C840" }),
3185
+ // an active tab tucked under the lights
3186
+ rect({ id: `${c.id}-tab`, x: -winW / 2 + 230, y: barY, anchor: "center", width: 190, height: 30, fill: c.o.screen ?? p.screen, radius: 8 }),
3187
+ text({ id: `${c.id}-tabtext`, x: -winW / 2 + 156, y: barY, anchor: "center-left", content: "Overview", fontFamily: "Inter", fontSize: 13, fill: p.chromeText }),
3188
+ rect({ id: `${c.id}-urlpill`, x: 96, y: barY, anchor: "center", width: 700, height: 26, fill: c.o.screen ?? p.screen, stroke: p.bodyStroke, strokeWidth: 1, radius: 13 }),
3189
+ rect({ id: `${c.id}-lock`, x: 96 - 330, y: barY, anchor: "center", width: 8, height: 10, fill: p.chromeText, radius: 2 }),
3190
+ text({ id: `${c.id}-urltext`, x: 96 - 312, y: barY, anchor: "center-left", content: urlText(c.o.url), fontFamily: "Inter", fontSize: 14, fill: p.chromeText }),
3191
+ screenStack(c, dims.cx, dims.cy, dims, content)
3192
+ ];
3193
+ },
3194
+ watch: (c, dims, content) => {
3195
+ const p = c.palette;
3196
+ const sw = dims.width, sh = dims.height;
3197
+ const bw = sw + 36;
3198
+ const bh = sh + 36;
3199
+ return [
3200
+ // straps (drawn behind the body) flaring out top & bottom
3201
+ path({ id: `${c.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 }),
3202
+ path({ id: `${c.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 }),
3203
+ slab(c, "body", 0, 0, bw, bh, 60, p.bodyGrad, p.body, p.bodyStroke),
3204
+ screenStack(c, dims.cx, dims.cy, dims, content),
3205
+ rect({ id: `${c.id}-crown`, x: bw / 2, y: -20, anchor: "center", width: 14, height: 40, fill: p.detail, radius: 6 }),
3206
+ rect({ id: `${c.id}-button`, x: bw / 2 - 2, y: 40, anchor: "center", width: 8, height: 34, fill: p.detail, radius: 4 })
3207
+ ];
3208
+ },
3209
+ monitor: (c, dims, content) => {
3210
+ const p = c.palette;
3211
+ const sw = dims.width, sh = dims.height;
3212
+ const panelW = sw + 44;
3213
+ const panelH = sh + 60;
3214
+ return [
3215
+ slab(c, "panel", 0, 0, panelW, panelH, 16, p.bodyGrad, p.body, p.bodyStroke),
3216
+ screenStack(c, dims.cx, dims.cy, dims, content),
3217
+ ellipse({ id: `${c.id}-led`, x: panelW / 2 - 26, y: panelH / 2 - 16, anchor: "center", width: 6, height: 6, fill: "#28C840" }),
3218
+ rect({ id: `${c.id}-neck`, x: 0, y: panelH / 2 + 60, anchor: "center", width: 60, height: 120, fill: p.body }),
3219
+ path({ id: `${c.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 })
3220
+ ];
3221
+ },
3222
+ tv: (c, dims, content) => {
3223
+ const p = c.palette;
3224
+ const sw = dims.width, sh = dims.height;
3225
+ const panelW = sw + 44;
3226
+ const panelH = sh + 48;
3227
+ const panelBottom = dims.cy + panelH / 2;
3228
+ return [
3229
+ slab(c, "panel", 0, dims.cy, panelW, panelH, 12, p.bodyGrad, p.body, p.bodyStroke),
3230
+ screenStack(c, dims.cx, dims.cy, dims, content),
3231
+ ellipse({ id: `${c.id}-brand`, x: 0, y: panelBottom - 12, anchor: "center", width: 6, height: 6, fill: p.detail }),
3232
+ rect({ id: `${c.id}-neck`, x: 0, y: panelBottom + 48, anchor: "center", width: 64, height: 96, fill: p.body }),
3233
+ path({ id: `${c.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 })
3234
+ ];
3235
+ },
3236
+ foldable: (c, dims, content) => {
3237
+ const p = c.palette;
3238
+ const sw = dims.width, sh = dims.height;
3239
+ const bodyW = sw + 40;
3240
+ const bodyH = sh + 40;
3241
+ return [
3242
+ rect({ id: `${c.id}-hinge-l`, x: -bodyW / 2, y: 0, anchor: "center", width: 8, height: bodyH * 0.5, fill: p.detail, radius: 4 }),
3243
+ rect({ id: `${c.id}-hinge-r`, x: bodyW / 2, y: 0, anchor: "center", width: 8, height: bodyH * 0.5, fill: p.detail, radius: 4 }),
3244
+ slab(c, "body", 0, 0, bodyW, bodyH, 28, p.bodyGrad, p.body, p.bodyStroke),
3245
+ screenStack(c, dims.cx, dims.cy, dims, content),
3246
+ rect({ id: `${c.id}-crease`, x: 0, y: 0, anchor: "center", width: 4, height: sh, fill: p.bodyStroke, radius: 2, opacity: 0.5 }),
3247
+ ellipse({ id: `${c.id}-cam1`, x: -10, y: -sh / 2 + 18, anchor: "center", width: 8, height: 8, fill: p.detail }),
3248
+ ellipse({ id: `${c.id}-cam2`, x: 10, y: -sh / 2 + 18, anchor: "center", width: 8, height: 8, fill: p.detail })
3249
+ ];
3250
+ },
3251
+ terminal: (c, dims, content) => {
3252
+ const p = c.palette;
3253
+ const sw = dims.width, sh = dims.height;
3254
+ const winW = sw + 16;
3255
+ const winH = sh + 76;
3256
+ return [
3257
+ rect({ id: `${c.id}-win`, x: 0, y: 0, anchor: "center", width: winW, height: winH, fill: premiumFill(c, p.chrome, p.chromeGrad), stroke: bodyStroke(c, p.bodyStroke), strokeWidth: 1.5, radius: 12, ...bodyFx(c) }),
3258
+ ellipse({ id: `${c.id}-dot1`, x: -winW / 2 + 28, y: -winH / 2 + 22, anchor: "center", width: 12, height: 12, fill: "#FF5F57" }),
3259
+ ellipse({ id: `${c.id}-dot2`, x: -winW / 2 + 50, y: -winH / 2 + 22, anchor: "center", width: 12, height: 12, fill: "#FEBC2E" }),
3260
+ ellipse({ id: `${c.id}-dot3`, x: -winW / 2 + 72, y: -winH / 2 + 22, anchor: "center", width: 12, height: 12, fill: "#28C840" }),
3261
+ rect({ id: `${c.id}-tab`, x: -winW / 2 + 170, y: -winH / 2 + 22, anchor: "center", width: 130, height: 24, fill: c.o.screen ?? p.screen, radius: 6 }),
3262
+ text({ id: `${c.id}-title`, x: -winW / 2 + 170, y: -winH / 2 + 22, anchor: "center", content: urlText(c.o.url ?? "zsh"), fontFamily: "Inter", fontSize: 13, fill: p.chromeText }),
3263
+ screenStack(c, dims.cx, dims.cy, dims, content)
3264
+ ];
3265
+ },
3266
+ car: (c, dims, content) => {
3267
+ const p = c.palette;
3268
+ const sw = dims.width, sh = dims.height;
3269
+ const bodyW = sw + 60;
3270
+ const bodyH = sh + 60;
3271
+ return [
3272
+ slab(c, "body", 0, 0, bodyW, bodyH, 40, p.bodyGrad, p.body, p.bodyStroke),
3273
+ ellipse({ id: `${c.id}-knob`, x: -bodyW / 2 + 18, y: 0, anchor: "center", width: 22, height: 22, fill: p.body, stroke: p.detail, strokeWidth: 3 }),
3274
+ screenStack(c, dims.cx, dims.cy, dims, content),
3275
+ ellipse({ id: `${c.id}-btn1`, x: -44, y: sh / 2 + 16, anchor: "center", width: 12, height: 12, fill: p.detail }),
3276
+ ellipse({ id: `${c.id}-btn2`, x: 0, y: sh / 2 + 16, anchor: "center", width: 12, height: 12, fill: p.detail }),
3277
+ ellipse({ id: `${c.id}-btn3`, x: 44, y: sh / 2 + 16, anchor: "center", width: 12, height: 12, fill: p.detail })
3278
+ ];
3279
+ }
3207
3280
  };
3281
+ function phoneOrTablet(name, c, dims, content) {
3282
+ const p = c.palette;
3283
+ const sw = dims.width, sh = dims.height;
3284
+ const bezel = name === "phone" ? 20 : 28;
3285
+ const bodyW = sw + bezel * 2;
3286
+ const bodyH = sh + bezel * 2;
3287
+ const bodyR = name === "phone" ? 54 : 34;
3288
+ const land = isLandscape(name, c.o);
3289
+ const nodes = [
3290
+ slab(c, "body", 0, 0, bodyW, bodyH, bodyR, p.bodyGrad, p.body, p.bodyStroke),
3291
+ screenStack(c, dims.cx, dims.cy, dims, content)
3292
+ ];
3293
+ if (name === "phone") {
3294
+ if (land) {
3295
+ nodes.push(
3296
+ rect({ id: `${c.id}-notch`, x: -sw / 2 + 16, y: 0, anchor: "center", width: 30, height: 96, fill: "#000000", radius: 15 }),
3297
+ rect({ id: `${c.id}-home`, x: sw / 2 - 4, y: 0, anchor: "center", width: 5, height: 120, fill: p.detail, radius: 3 })
3298
+ );
3299
+ } else {
3300
+ const notch = phoneNotch(c, c.o.notch ?? "island", sh);
3301
+ if (notch) nodes.push(notch);
3302
+ nodes.push(
3303
+ rect({ id: `${c.id}-home`, x: 0, y: sh / 2 - 18, anchor: "center", width: 120, height: 5, fill: p.detail, radius: 3 }),
3304
+ // side hardware: power on the right, volume rocker on the left
3305
+ rect({ id: `${c.id}-pwr`, x: bodyW / 2, y: -bodyH * 0.1, anchor: "center", width: 4, height: 78, fill: p.detail, radius: 2 }),
3306
+ rect({ id: `${c.id}-volup`, x: -bodyW / 2, y: -bodyH * 0.16, anchor: "center", width: 4, height: 48, fill: p.detail, radius: 2 }),
3307
+ rect({ id: `${c.id}-voldn`, x: -bodyW / 2, y: -bodyH * 0.16 + 60, anchor: "center", width: 4, height: 48, fill: p.detail, radius: 2 })
3308
+ );
3309
+ }
3310
+ } else {
3311
+ nodes.push(
3312
+ rect({ id: `${c.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 }),
3313
+ rect({ id: `${c.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 })
3314
+ );
3315
+ }
3316
+ return nodes;
3317
+ }
3318
+ function devicePreset(name, opts = {}) {
3319
+ const c = makeCtx(opts);
3320
+ const dims = screenDims(name, opts);
3321
+ const children = CHASSIS[name](c, dims, opts.content ?? []);
3322
+ return group(
3323
+ {
3324
+ id: c.id,
3325
+ x: opts.x ?? 0,
3326
+ y: opts.y ?? 0,
3327
+ ...opts.scale !== void 0 && opts.scale !== 1 && { scale: opts.scale },
3328
+ ...opts.opacity !== void 0 && opts.opacity !== 1 && { opacity: opts.opacity }
3329
+ },
3330
+ children
3331
+ );
3332
+ }
3333
+
3334
+ // ../core/src/cursor.ts
3335
+ var ARROW_D = "M0 0 L0 30 L8 23 L12.6 33 L17 31 L12.4 21.4 L21 21.4 Z";
3336
+ function cursor(opts = {}) {
3337
+ const id = opts.id ?? "cursor";
3338
+ const style = opts.style ?? "arrow";
3339
+ const fill = opts.fill ?? "#FFFFFF";
3340
+ const accent = opts.accent ?? "#FF5A1F";
3341
+ 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" })];
3342
+ return group(
3343
+ { id, x: opts.x ?? 0, y: opts.y ?? 0, scale: opts.scale ?? 1, opacity: opts.opacity ?? 1 },
3344
+ [
3345
+ // ripple ring (behind the pointer), emanates from the hotspot on click
3346
+ ellipse({ id: `${id}-ripple`, x: 0, y: 0, width: 30, height: 30, fill: "none", stroke: accent, strokeWidth: 3, opacity: 0, scale: 0, anchor: "center" }),
3347
+ // the pointer art lives in its own group so a click "tap" can scale it
3348
+ // independently of the cursor's resting scale
3349
+ group({ id: `${id}-art`, x: 0, y: 0 }, art)
3350
+ ]
3351
+ );
3352
+ }
3353
+ var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
3354
+ function cursorTo(id, from, to2, opts = {}) {
3355
+ const dx = to2[0] - from[0], dy = to2[1] - from[1];
3356
+ const dist = Math.hypot(dx, dy) || 1;
3357
+ const arc = opts.arc ?? 0.12;
3358
+ const mid = [(from[0] + to2[0]) / 2 + -dy / dist * arc * dist, (from[1] + to2[1]) / 2 + dx / dist * arc * dist];
3359
+ const duration = opts.duration ?? clamp(dist / 1400, 0.4, 0.9);
3360
+ return motionPath(id, [from, mid, to2], { duration, ease: opts.ease ?? "easeInOutCubic", curviness: 1, ...opts.label && { label: opts.label } });
3361
+ }
3362
+ function cursorPath(id, points, opts = {}) {
3363
+ return motionPath(id, points, {
3364
+ duration: opts.duration ?? clamp(points.length * 0.5, 0.5, 4),
3365
+ ease: opts.ease ?? "easeInOutCubic",
3366
+ curviness: opts.curviness ?? 1,
3367
+ ...opts.label && { label: opts.label }
3368
+ });
3369
+ }
3370
+ function clickBody(id, o) {
3371
+ const sp = Math.max(0.25, o.speed ?? 1);
3372
+ const d = (b) => b / sp;
3373
+ const out = [
3374
+ // the pointer taps
3375
+ seq(tween(`${id}-art`, { scale: 0.82 }, { duration: d(0.08), ease: "easeOutQuad" }), tween(`${id}-art`, { scale: 1 }, { duration: d(0.1), ease: "easeOutBack" }))
3376
+ ];
3377
+ if (o.ripple !== false) {
3378
+ out.push(seq(
3379
+ tween(`${id}-ripple`, { scale: 0.2, opacity: 0.55 }, { duration: 1e-3 }),
3380
+ par(
3381
+ tween(`${id}-ripple`, { scale: 5 }, { duration: d(0.5), ease: "easeOutCubic" }),
3382
+ tween(`${id}-ripple`, { opacity: 0 }, { duration: d(0.5), ease: "easeOutQuad" })
3383
+ )
3384
+ ));
3385
+ }
3386
+ if (o.press) {
3387
+ 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" })));
3388
+ }
3389
+ return out;
3390
+ }
3391
+ function cursorClick(id, opts = {}) {
3392
+ return beat(opts.label ?? "cursor-click", {}, [par(...clickBody(id, opts))]);
3393
+ }
3394
+ function cursorDouble(id, opts = {}) {
3395
+ const sp = Math.max(0.25, opts.speed ?? 1);
3396
+ return beat(opts.label ?? "cursor-double", {}, [
3397
+ seq(par(...clickBody(id, { ...opts, ripple: false })), wait(0.12 / sp), par(...clickBody(id, opts)))
3398
+ ]);
3399
+ }
3400
+
3401
+ // ../core/src/rig.ts
3402
+ var DEFAULT_LINE = "#FFE3D2";
3403
+ var DEFAULT_FILL = "#0E1424";
3404
+ var LINE_W = 5;
3405
+ var GLOW_W = 16;
3406
+ var K = 0.5523;
3407
+ var n = (v) => Number(v.toFixed(2));
3408
+ function capsulePath(hw, len) {
3409
+ const yT = hw;
3410
+ const yB = Math.max(hw, len - hw);
3411
+ const k = hw * K;
3412
+ 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`;
3413
+ }
3414
+ function ovalPath(a, b, cx = 0, cy = 0) {
3415
+ const ka = n(a * K), kb = n(b * K);
3416
+ const t = n(cy - b), bo = n(cy + b), c = n(cy), A = n(a), L = n(-a), X = n(cx);
3417
+ 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`;
3418
+ }
3419
+ function boneShape(jointId, bone, o) {
3420
+ if (bone.shape) return bone.shape;
3421
+ const len = bone.length ?? 0;
3422
+ if (len <= 0) return [];
3423
+ const d = capsulePath((bone.width ?? 20) / 2, len);
3424
+ const nodes = [];
3425
+ 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 }));
3426
+ nodes.push(path({ id: `${jointId}-shape`, d, x: 0, y: 0, fill: o.fill, stroke: o.color, strokeWidth: LINE_W }));
3427
+ return nodes;
3428
+ }
3429
+ function buildBone(bone, id, o) {
3430
+ const jointId = `${id}-${bone.name}`;
3431
+ return group(
3432
+ { id: jointId, x: bone.at[0], y: bone.at[1], rotation: bone.rotation ?? 0 },
3433
+ [...boneShape(jointId, bone, o), ...(bone.children ?? []).map((c) => buildBone(c, id, o))]
3434
+ );
3435
+ }
3436
+ function rig(root, opts = {}) {
3437
+ const id = opts.id ?? "rig";
3438
+ const o = { color: opts.color ?? DEFAULT_LINE, fill: opts.fill ?? DEFAULT_FILL, glow: opts.glow };
3439
+ return group(
3440
+ { id, x: opts.x ?? 0, y: opts.y ?? 0, scale: opts.scale ?? 1, opacity: opts.opacity ?? 1 },
3441
+ [buildBone(root, id, o)]
3442
+ );
3443
+ }
3444
+ function rigPose(id, pose) {
3445
+ const out = {};
3446
+ for (const [name, deg] of Object.entries(pose)) out[`${id}-${name}`] = { rotation: deg };
3447
+ return out;
3448
+ }
3449
+ function poseTo(id, pose, opts = {}) {
3450
+ const tweens = Object.entries(pose).map(
3451
+ ([name, deg]) => tween(`${id}-${name}`, { rotation: deg }, { duration: opts.duration ?? 0.5, ease: opts.ease ?? "easeInOutCubic" })
3452
+ );
3453
+ return opts.stagger ? stagger(opts.stagger, ...tweens) : par(...tweens);
3454
+ }
3455
+ function ikReach(upper, lower, dx, dy, flip = false) {
3456
+ const D = Math.hypot(dx, dy);
3457
+ const cos2 = Math.max(-1, Math.min(1, (D * D - upper * upper - lower * lower) / (2 * upper * lower)));
3458
+ const theta2 = (flip ? -1 : 1) * Math.acos(cos2);
3459
+ const vx = -lower * Math.sin(theta2);
3460
+ const vy = upper + lower * Math.cos(theta2);
3461
+ const theta1 = Math.atan2(dy, dx) - Math.atan2(vy, vx);
3462
+ const deg = (r) => r * 180 / Math.PI;
3463
+ return [deg(theta1), deg(theta2)];
3464
+ }
3465
+ function humanoid(opts = {}) {
3466
+ const line2 = opts.color ?? DEFAULT_LINE;
3467
+ const fill = opts.fill ?? DEFAULT_FILL;
3468
+ const glow2 = opts.glow;
3469
+ const blob = (jid, a, b, cy) => {
3470
+ const d = ovalPath(a, b, 0, cy);
3471
+ const nodes = [];
3472
+ if (glow2) nodes.push(path({ id: `${jid}-glow`, d, x: 0, y: 0, fill: "none", stroke: glow2, strokeWidth: GLOW_W, opacity: 0.18 }));
3473
+ nodes.push(path({ id: `${jid}-shape`, d, x: 0, y: 0, fill, stroke: line2, strokeWidth: LINE_W }));
3474
+ return nodes;
3475
+ };
3476
+ const id = opts.id ?? "rig";
3477
+ const root = {
3478
+ name: "chest",
3479
+ at: [0, 0],
3480
+ shape: blob(`${id}-chest`, 44, 62, 22),
3481
+ children: [
3482
+ { name: "head", at: [0, -42], rotation: 0, shape: blob(`${id}-head`, 40, 42, -34) },
3483
+ { name: "armUpperL", at: [-42, -20], length: 60, width: 20, rotation: 10, children: [
3484
+ { name: "armLowerL", at: [0, 60], length: 56, width: 16, rotation: 8 }
3485
+ ] },
3486
+ { name: "armUpperR", at: [42, -20], length: 60, width: 20, rotation: -10, children: [
3487
+ { name: "armLowerR", at: [0, 60], length: 56, width: 16, rotation: -8 }
3488
+ ] },
3489
+ { name: "legUpperL", at: [-20, 76], length: 76, width: 26, rotation: 3, children: [
3490
+ { name: "legLowerL", at: [0, 76], length: 72, width: 22, rotation: -2 }
3491
+ ] },
3492
+ { name: "legUpperR", at: [20, 76], length: 76, width: 26, rotation: -3, children: [
3493
+ { name: "legLowerR", at: [0, 76], length: 72, width: 22, rotation: 2 }
3494
+ ] }
3495
+ ]
3496
+ };
3497
+ return rig(root, opts);
3498
+ }
3208
3499
 
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);
3500
+ // ../core/src/characterPreset.ts
3501
+ var CHARACTER_PRESET_NAMES = ["walk", "run", "jump", "dance", "wave", "cheer"];
3502
+ var THIGH = 76;
3503
+ var SHIN = 72;
3504
+ var clamp013 = (x) => Math.max(0, Math.min(1, x));
3505
+ function makeRng4(seed) {
3506
+ let a = seed >>> 0 || 2654435769;
3507
+ return () => {
3508
+ a = a + 1831565813 | 0;
3509
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
3510
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
3511
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
3512
+ };
3513
+ }
3213
3514
  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
- );
3515
+ function ctx3(o) {
3516
+ const rand2 = makeRng4((o.seed ?? 0) + 1);
3517
+ return {
3518
+ g: o.target,
3519
+ label: o.label,
3520
+ e: clamp013(o.energy ?? 0.5),
3521
+ sp: Math.max(0.25, o.speed ?? 1),
3522
+ cycles: Math.max(1, Math.round(o.cycles ?? 4)),
3523
+ facing: o.facing ?? 1,
3524
+ at: o.at ?? [0, 0],
3525
+ travel: o.travel,
3526
+ rand: rand2,
3527
+ jit: (amp) => (rand2() - 0.5) * 2 * amp
3250
3528
  };
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;
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;
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
- });
3529
+ }
3530
+ var round = (v) => Math.round(v * 1e3) / 1e3;
3531
+ function footPos(p, stride, lift) {
3532
+ p = (p % 1 + 1) % 1;
3533
+ if (p < 0.5) {
3534
+ const u2 = p / 0.5;
3535
+ return [stride * (1 - 2 * u2), 138];
3277
3536
  }
3278
- return { nodes, glyphs, ids: glyphs.map((g) => g.id), width: total, x, y, fontSize };
3537
+ const u = (p - 0.5) / 0.5;
3538
+ return [-stride + 2 * stride * u, 138 - Math.sin(Math.PI * u) * lift];
3279
3539
  }
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" }));
3331
- }
3332
- flicker.push(tween(g.id, { content: g.ch }, { duration: dur3(0.05, c.sp), ease: "linear" }));
3333
- return seq(...flicker);
3334
- }
3540
+ function gaitPose(ph, stride, lift, armSwing, facing) {
3541
+ const fl = footPos(ph, stride, lift);
3542
+ const fr = footPos(ph + 0.5, stride, lift);
3543
+ const [hipL, kneeL] = ikReach(THIGH, SHIN, facing * fl[0], fl[1], facing < 0);
3544
+ const [hipR, kneeR] = ikReach(THIGH, SHIN, facing * fr[0], fr[1], facing < 0);
3545
+ const swing = Math.cos(2 * Math.PI * ph);
3546
+ return {
3547
+ legUpperL: round(hipL),
3548
+ legLowerL: round(kneeL),
3549
+ legUpperR: round(hipR),
3550
+ legLowerR: round(kneeR),
3551
+ armUpperR: round(-10 - armSwing * swing),
3552
+ armLowerR: -16,
3553
+ armUpperL: round(10 + armSwing * swing),
3554
+ armLowerL: 16
3555
+ };
3556
+ }
3557
+ function gait(c, run) {
3558
+ const stride = (run ? 34 : 24) + (run ? 40 : 30) * c.e + c.jit(3);
3559
+ const lift = run ? 40 : 26;
3560
+ const armSwing = (run ? 26 : 16) + 20 * c.e;
3561
+ const halfDur = (run ? 0.26 : 0.42) + c.jit(0.02);
3562
+ const lean = run ? c.facing * -6 : 0;
3563
+ const steps = c.cycles * 2;
3564
+ const d = dur3(halfDur, c.sp);
3565
+ const intro = dur3(0.16, c.sp);
3566
+ const keys = [];
3567
+ for (let k = 0; k <= steps; k++) {
3568
+ const pose = { ...gaitPose(k / 2, stride, lift, armSwing, c.facing), chest: lean };
3569
+ keys.push(poseTo(c.g, pose, { duration: k === 0 ? intro : d, ease: k === 0 ? "easeOutQuad" : "linear" }));
3570
+ }
3571
+ const total = intro + steps * d;
3572
+ const travel = c.travel ?? stride * 2;
3573
+ const children = [seq(...keys)];
3574
+ if (travel !== 0) {
3575
+ children.push(tween(c.g, { x: c.at[0] + c.facing * travel * c.cycles }, { duration: total, ease: "linear", label: "travel" }));
3576
+ }
3577
+ return beat(run ? "run" : "walk", {}, [par(...children)]);
3578
+ }
3579
+ function jumpBeat(c) {
3580
+ const h = 120 + 150 * c.e;
3581
+ const [y0] = [c.at[1]];
3582
+ const CROUCH = { legUpperL: 18, legLowerL: 54, legUpperR: -18, legLowerR: 54, armUpperL: 28, armUpperR: -28 };
3583
+ const LAUNCH = { legUpperL: 0, legLowerL: 0, legUpperR: 0, legLowerR: 0, armUpperL: 150, armUpperR: -150 };
3584
+ const TUCK = { legUpperL: -28, legLowerL: 66, legUpperR: -28, legLowerR: 66, armUpperL: 124, armUpperR: -124 };
3585
+ const REST = { legUpperL: 3, legLowerL: -2, legUpperR: -3, legLowerR: 2, armUpperL: 10, armLowerL: 8, armUpperR: -10, armLowerR: -8 };
3586
+ const j = c.jit(0.03);
3587
+ return beat("jump", {}, [
3588
+ seq(
3589
+ par(poseTo(c.g, CROUCH, { duration: dur3(0.24, c.sp), ease: "easeOutQuad" }), tween(c.g, { y: y0 + 26 }, { duration: dur3(0.24, c.sp), ease: "easeOutQuad" })),
3590
+ par(poseTo(c.g, LAUNCH, { duration: dur3(0.22 + j, c.sp), ease: "easeOutCubic" }), tween(c.g, { y: y0 - h }, { duration: dur3(0.36, c.sp), ease: "easeOutCubic", label: "launch" })),
3591
+ poseTo(c.g, TUCK, { duration: dur3(0.22, c.sp) }),
3592
+ par(poseTo(c.g, CROUCH, { duration: dur3(0.28, c.sp), ease: "easeInQuad" }), tween(c.g, { y: y0 + 18 }, { duration: dur3(0.3, c.sp), ease: "easeInCubic", label: "land" })),
3593
+ par(poseTo(c.g, REST, { duration: dur3(0.45, c.sp), ease: "easeOutBack" }), tween(c.g, { y: y0 }, { duration: dur3(0.45, c.sp), ease: "easeOutBack" }))
3594
+ )
3595
+ ]);
3596
+ }
3597
+ function danceBeat(c) {
3598
+ const y0 = c.at[1];
3599
+ const sway = 8 + 6 * c.e;
3600
+ const armUp = 130 + 30 * c.e;
3601
+ const A = { chest: sway, head: -sway * 0.5, armUpperR: -armUp, armLowerR: -20, armUpperL: 40, armLowerL: 30, legUpperL: 8, legUpperR: -2 };
3602
+ const B = { chest: -sway, head: sway * 0.5, armUpperL: armUp, armLowerL: 20, armUpperR: -40, armLowerR: -30, legUpperL: 2, legUpperR: -8 };
3603
+ const d = dur3(0.34, c.sp);
3604
+ const keys = [];
3605
+ for (let k = 0; k < c.cycles * 2; k++) {
3606
+ const pose = k % 2 === 0 ? A : B;
3607
+ keys.push(par(
3608
+ poseTo(c.g, pose, { duration: d, ease: "easeInOutQuad" }),
3609
+ tween(c.g, { y: y0 - (k % 2 === 0 ? 14 : 0) }, { duration: d, ease: "easeInOutQuad" })
3610
+ ));
3335
3611
  }
3612
+ keys.push(tween(c.g, { y: y0 }, { duration: d }));
3613
+ return beat("dance", {}, [seq(...keys)]);
3336
3614
  }
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)))]);
3615
+ function waveBeat(c) {
3616
+ const n3 = 3 + Math.round(c.rand() * 2);
3617
+ const amp = 16 + 10 * c.e;
3618
+ const steps = [poseTo(c.g, { armUpperR: -150, armLowerR: -24 }, { duration: dur3(0.4, c.sp), ease: "easeOutBack" })];
3619
+ for (let k = 0; k < n3; k++) {
3620
+ steps.push(poseTo(c.g, { armLowerR: -24 + (k % 2 === 0 ? amp : -amp) }, { duration: dur3(0.22, c.sp), ease: "easeInOutQuad" }));
3621
+ }
3622
+ steps.push(poseTo(c.g, { armUpperR: -10, armLowerR: -8 }, { duration: dur3(0.4, c.sp), ease: "easeInOutCubic" }));
3623
+ return beat("wave", {}, [seq(...steps)]);
3341
3624
  }
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);
3356
- }
3357
- });
3625
+ function cheerBeat(c) {
3626
+ const y0 = c.at[1];
3627
+ const UP = { armUpperL: 152, armLowerL: 8, armUpperR: -152, armLowerR: -8 };
3628
+ const d = dur3(0.3, c.sp);
3629
+ const keys = [poseTo(c.g, UP, { duration: dur3(0.35, c.sp), ease: "easeOutBack" })];
3630
+ for (let k = 0; k < c.cycles; k++) {
3631
+ keys.push(par(tween(c.g, { y: y0 - 28 }, { duration: d, ease: "easeOutQuad" }), poseTo(c.g, { armUpperL: 160, armUpperR: -160 }, { duration: d })));
3632
+ keys.push(par(tween(c.g, { y: y0 }, { duration: d, ease: "easeInQuad" }), poseTo(c.g, { armUpperL: 145, armUpperR: -145 }, { duration: d })));
3633
+ }
3634
+ return beat("cheer", {}, [seq(...keys)]);
3358
3635
  }
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);
3636
+ function characterPreset(name, opts) {
3637
+ const c = ctx3(opts);
3638
+ let tl;
3362
3639
  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
- );
3640
+ case "walk":
3641
+ tl = gait(c, false);
3642
+ break;
3643
+ case "run":
3644
+ tl = gait(c, true);
3645
+ break;
3646
+ case "jump":
3647
+ tl = jumpBeat(c);
3648
+ break;
3649
+ case "dance":
3650
+ tl = danceBeat(c);
3651
+ break;
3652
+ case "wave":
3653
+ tl = waveBeat(c);
3654
+ break;
3655
+ case "cheer":
3656
+ tl = cheerBeat(c);
3657
+ break;
3658
+ default: {
3659
+ const _exhaustive = name;
3660
+ throw new Error(`unknown characterPreset "${_exhaustive}"`);
3661
+ }
3388
3662
  }
3663
+ return c.label && tl.kind === "beat" ? { ...tl, name: c.label } : tl;
3389
3664
  }
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]);
3665
+
3666
+ // ../core/src/figure.ts
3667
+ var K2 = 0.5523;
3668
+ var n2 = (v) => Number(v.toFixed(2));
3669
+ function limb(a, b, y0, y1) {
3670
+ const ka = n2(a * K2), kb = n2(b * K2);
3671
+ 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`;
3397
3672
  }
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
- }));
3673
+ function rrect(a, b, y0, y1, r) {
3674
+ 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`;
3675
+ }
3676
+ function darken(hex, f) {
3677
+ const h = hex.replace("#", "");
3678
+ const v = parseInt(h.length === 3 ? [...h].map((c) => c + c).join("") : h, 16);
3679
+ const ch = (s) => Math.max(0, Math.min(255, Math.round((v >> s & 255) * (1 - f))));
3680
+ const hx = (x) => x.toString(16).padStart(2, "0");
3681
+ return `#${hx(ch(16))}${hx(ch(8))}${hx(ch(0))}`;
3682
+ }
3683
+ var DEF = {
3684
+ clean: { skin: "#E9B58E", hair: "#2B313F", top: "#E86C4A", pants: "#39425C", shoe: "#20242F", accent: "#E86C4A" },
3685
+ cute: { skin: "#FFD2A6", hair: "#5B4636", top: "#FF7E5F", pants: "#3E6F8E", shoe: "#272B38", accent: "#FF7E5F" }
3686
+ };
3687
+ function resolvePal(style, p = {}) {
3688
+ const d = DEF[style];
3689
+ const accent = p.accent ?? d.accent;
3690
+ const top = p.top ?? (style === "clean" ? accent : d.top);
3691
+ const skin = p.skin ?? d.skin;
3692
+ const hair = p.hair ?? d.hair;
3693
+ const pants = p.pants ?? d.pants;
3694
+ const shoe = p.shoe ?? d.shoe;
3695
+ return {
3696
+ skin,
3697
+ skinSh: darken(skin, 0.12),
3698
+ hair,
3699
+ hairSh: darken(hair, 0.14),
3700
+ top,
3701
+ topSh: darken(top, 0.12),
3702
+ pants,
3703
+ pantsSh: darken(pants, 0.14),
3704
+ shoe,
3705
+ shoeSh: darken(shoe, 0.22),
3706
+ eye: "#2B313F",
3707
+ cheek: "#FF9E7E",
3708
+ white: "#FFFFFF",
3709
+ mouth: "#8A4233"
3710
+ };
3711
+ }
3712
+ 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 } : {} });
3713
+ function cleanParts(p, face) {
3714
+ const HC = -42;
3715
+ return {
3716
+ upperArm: (j) => [fp(`${j}-sleeve`, limb(12, 10, 2, 58), p.top)],
3717
+ forearm: (j) => [
3718
+ fp(`${j}-elbow`, ovalPath(10, 10, 0, 3), p.skin),
3719
+ fp(`${j}-fore`, limb(10, 8, 2, 48), p.skin),
3720
+ fp(`${j}-hand`, ovalPath(11, 12, 0, 50), p.skin)
3721
+ ],
3722
+ thigh: (j) => [fp(`${j}-thigh`, limb(15, 13, 2, 72), p.pants)],
3723
+ shin: (j) => [
3724
+ fp(`${j}-knee`, ovalPath(13, 13, 0, 2), p.pants),
3725
+ fp(`${j}-shin`, limb(13, 11, 2, 62), p.pants),
3726
+ fp(`${j}-shoe`, ovalPath(15, 9, 4, 67), p.shoe)
3727
+ ],
3728
+ torso: (j) => [
3729
+ fp(`${j}-shadow`, rrect(38, 26, -28, 52, 20), p.topSh),
3730
+ fp(`${j}-top`, rrect(40, 27, -30, 52, 22), p.top),
3731
+ fp(`${j}-pelvis`, rrect(29, 24, 46, 104, 14), p.pants)
3732
+ ],
3733
+ head: (j) => [
3734
+ fp(`${j}-neck`, rrect(9, 9, 2, 22, 5), p.skin),
3735
+ fp(`${j}-skin`, ovalPath(42, 46, 0, HC), p.skin),
3736
+ fp(`${j}-hair`, ovalPath(44, 27, 0, HC - 31), p.hair),
3737
+ fp(`${j}-hairL`, ovalPath(8, 14, -39, HC - 18), p.hair),
3738
+ fp(`${j}-hairR`, ovalPath(8, 14, 39, HC - 18), p.hair),
3739
+ ...face ? [fp(`${j}-eyeL`, ovalPath(5, 7, -14, HC + 2), p.eye), fp(`${j}-eyeR`, ovalPath(5, 7, 14, HC + 2), p.eye)] : []
3740
+ ]
3741
+ };
3742
+ }
3743
+ 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";
3744
+ function cuteParts(p, face) {
3745
+ const HC = -50;
3746
+ return {
3747
+ upperArm: (j) => [fp(`${j}-sleeve`, limb(15, 13, 2, 58), p.top, p.topSh, 2.5)],
3748
+ forearm: (j) => [
3749
+ fp(`${j}-elbow`, ovalPath(12, 12, 0, 3), p.skin, p.skinSh, 2.5),
3750
+ fp(`${j}-fore`, limb(12, 10, 2, 46), p.skin, p.skinSh, 2.5),
3751
+ fp(`${j}-hand`, ovalPath(13, 14, 0, 50), p.skin, p.skinSh, 2.5)
3752
+ ],
3753
+ thigh: (j) => [fp(`${j}-thigh`, limb(19, 16, 2, 72), p.pants, p.pantsSh, 2.5)],
3754
+ shin: (j) => [
3755
+ fp(`${j}-knee`, ovalPath(16, 16, 0, 2), p.pants, p.pantsSh, 2.5),
3756
+ fp(`${j}-shin`, limb(15, 12, 2, 60), p.pants, p.pantsSh, 2.5),
3757
+ fp(`${j}-shoe`, ovalPath(18, 11, 5, 66), p.shoe, darken(p.shoe, 0.25), 2.5)
3758
+ ],
3759
+ torso: (j) => [
3760
+ fp(`${j}-shadow`, rrect(40, 34, -18, 52, 16), p.topSh),
3761
+ fp(`${j}-top`, rrect(42, 35, -20, 52, 18), p.top, p.topSh, 2.5),
3762
+ fp(`${j}-pelvis`, rrect(36, 30, 46, 104, 16), p.pants, p.pantsSh, 2.5)
3763
+ ],
3764
+ head: (j) => [
3765
+ fp(`${j}-neck`, ovalPath(15, 12, 0, 12), p.skinSh),
3766
+ fp(`${j}-skin`, ovalPath(42, 46, 0, HC + 4), p.skin, p.skinSh, 2.5),
3767
+ fp(`${j}-cheekL`, ovalPath(11, 8, -40, HC + 22), p.cheek, void 0, 0, 0.5),
3768
+ fp(`${j}-cheekR`, ovalPath(11, 8, 40, HC + 22), p.cheek, void 0, 0, 0.5),
3769
+ fp(`${j}-hair`, CUTE_HAIR, p.hair, p.hairSh, 2),
3770
+ ...face ? [
3771
+ fp(`${j}-eyeL`, ovalPath(10, 13, -25, HC + 8), p.eye),
3772
+ fp(`${j}-eyeR`, ovalPath(10, 13, 25, HC + 8), p.eye),
3773
+ fp(`${j}-glL`, ovalPath(3.4, 3.4, -28, HC + 3), p.white),
3774
+ fp(`${j}-glR`, ovalPath(3.4, 3.4, 22, HC + 3), p.white),
3775
+ 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 })
3776
+ ] : []
3777
+ ]
3778
+ };
3779
+ }
3780
+ function buildSkeleton(id, S) {
3781
+ const arm = (side, x, r1, r2) => ({
3782
+ name: `armUpper${side}`,
3783
+ at: [x, -14],
3784
+ length: 60,
3785
+ width: 0,
3786
+ rotation: r1,
3787
+ shape: S.upperArm(`${id}-armUpper${side}`),
3788
+ children: [{ name: `armLower${side}`, at: [0, 60], length: 56, width: 0, rotation: r2, shape: S.forearm(`${id}-armLower${side}`) }]
3789
+ });
3790
+ const leg = (side, x, r1, r2) => ({
3791
+ name: `legUpper${side}`,
3792
+ at: [x, 76],
3793
+ length: 76,
3794
+ width: 0,
3795
+ rotation: r1,
3796
+ shape: S.thigh(`${id}-legUpper${side}`),
3797
+ children: [{ name: `legLower${side}`, at: [0, 76], length: 72, width: 0, rotation: r2, shape: S.shin(`${id}-legLower${side}`) }]
3798
+ });
3799
+ return {
3800
+ name: "chest",
3801
+ at: [0, 0],
3802
+ shape: S.torso(`${id}-chest`),
3803
+ children: [
3804
+ { name: "head", at: [0, -42], rotation: 0, shape: S.head(`${id}-head`) },
3805
+ arm("L", -40, 8, 6),
3806
+ arm("R", 40, -8, -6),
3807
+ leg("L", -20, 3, -2),
3808
+ leg("R", 20, -3, 2)
3809
+ ]
3810
+ };
3811
+ }
3812
+ function figure(opts = {}) {
3813
+ const style = opts.style ?? "clean";
3814
+ const pal = resolvePal(style, opts.palette);
3815
+ const face = opts.face ?? true;
3816
+ const id = opts.id ?? "figure";
3817
+ const parts = style === "clean" ? cleanParts(pal, face) : cuteParts(pal, face);
3818
+ const rigOpts = { id };
3819
+ if (opts.x !== void 0) rigOpts.x = opts.x;
3820
+ if (opts.y !== void 0) rigOpts.y = opts.y;
3821
+ if (opts.scale !== void 0) rigOpts.scale = opts.scale;
3822
+ if (opts.opacity !== void 0) rigOpts.opacity = opts.opacity;
3823
+ return rig(buildSkeleton(id, parts), rigOpts);
3409
3824
  }
3410
3825
 
3411
3826
  // ../core/src/motionOps.ts
@@ -3536,13 +3951,14 @@ var SFX_DURATION = {
3536
3951
  camera: 0.18
3537
3952
  };
3538
3953
  var FILE_CUE_DURATION = 0.4;
3539
- function collectClipAudio(ir, duration, warnings) {
3954
+ function collectClipAudio(ir, duration, labelTimes, warnings) {
3540
3955
  const out = [];
3541
3956
  const walk = (nodes) => {
3542
3957
  for (const node of nodes) {
3543
3958
  if (node.type === "video") {
3544
3959
  const gain = node.props.volume ?? 1;
3545
- const start = node.props.start ?? 0;
3960
+ const startRaw = node.props.start;
3961
+ const start = typeof startRaw === "string" ? labelTimes.get(startRaw)?.t0 ?? 0 : startRaw ?? 0;
3546
3962
  if (gain <= 0) continue;
3547
3963
  if (start >= duration) {
3548
3964
  warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
@@ -3560,7 +3976,7 @@ function resolveAudioPlan(compiled) {
3560
3976
  const audio = compiled.ir.audio;
3561
3977
  const warnings = [];
3562
3978
  const duration = compiled.duration;
3563
- const clipAudio = collectClipAudio(compiled.ir, duration, warnings);
3979
+ const clipAudio = collectClipAudio(compiled.ir, duration, compiled.labelTimes, warnings);
3564
3980
  const autoCues = audio?.autoFoley ? autoFoley(compiled, audio.autoFoley === true ? {} : audio.autoFoley) : [];
3565
3981
  const manualCues = [...audio?.cues ?? [], ...autoCues];
3566
3982
  if (!audio || !audio.bgm && manualCues.length === 0) {
@@ -3799,6 +4215,7 @@ export {
3799
4215
  SFX_DURATION,
3800
4216
  SFX_NAMES,
3801
4217
  SceneValidationError,
4218
+ TIMELINE_PATCHABLE,
3802
4219
  autoFoley,
3803
4220
  beat,
3804
4221
  cameraFit,
@@ -3839,6 +4256,8 @@ export {
3839
4256
  lerpValue,
3840
4257
  line,
3841
4258
  linearGradient,
4259
+ lintScene,
4260
+ lowerThird,
3842
4261
  motionOp,
3843
4262
  motionOpLabel,
3844
4263
  motionPath,
@@ -3863,6 +4282,7 @@ export {
3863
4282
  sampleBehavior,
3864
4283
  sampleProp,
3865
4284
  scene,
4285
+ sceneManifest,
3866
4286
  seq,
3867
4287
  sketchToTimeline,
3868
4288
  splitText,
@@ -3872,6 +4292,7 @@ export {
3872
4292
  textLoop,
3873
4293
  textOut,
3874
4294
  textTypeCues,
4295
+ title,
3875
4296
  to,
3876
4297
  tween,
3877
4298
  validateComposition,