volito 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +200 -9
  2. package/dist/browser.d.ts +50 -1
  3. package/dist/browser.js +352 -43
  4. package/dist/chunks/{chunk-CFDFWZAC.js → chunk-CUQY6YI3.js} +1032 -404
  5. package/dist/chunks/chunk-PM3PLI3X.js +71 -0
  6. package/dist/chunks/chunk-QA4SEJCZ.js +601 -0
  7. package/dist/chunks/chunk-ZFG3X3DU.js +67 -0
  8. package/dist/chunks/chunk-ZHMWEZ4C.js +186 -0
  9. package/dist/chunks/init-JX2BYRKT.js +189 -0
  10. package/dist/chunks/review-RBH5EUZN.js +18 -0
  11. package/dist/cli.js +232 -32
  12. package/dist/index.d.ts +314 -2
  13. package/dist/index.js +75 -21
  14. package/dist/mcp.js +129 -22
  15. package/dist/player.d.ts +643 -0
  16. package/dist/player.js +6320 -0
  17. package/dist/registry.json +1 -1
  18. package/examples/player.html +93 -0
  19. package/examples/states.html +73 -0
  20. package/examples/volito-intro/DESIGN.md +23 -0
  21. package/examples/volito-intro/frame.js +187 -0
  22. package/examples/volito-intro/item.json +43 -0
  23. package/package.json +15 -7
  24. package/packs/3d/pack.json +1 -1
  25. package/packs/annotate/emphasis/frame.js +8 -0
  26. package/packs/annotate/pack.json +1 -1
  27. package/packs/annotate/reactions/frame.js +12 -0
  28. package/packs/backdrops/gradient/frame.js +5 -0
  29. package/packs/backdrops/pack.json +1 -1
  30. package/packs/broadcast/pack.json +1 -1
  31. package/packs/data/pack.json +1 -1
  32. package/packs/effects/chromatic/frame.js +17 -0
  33. package/packs/effects/chromatic/item.json +15 -0
  34. package/packs/effects/glass/frame.js +44 -0
  35. package/packs/effects/glass/item.json +16 -0
  36. package/packs/effects/grain/frame.js +20 -0
  37. package/packs/effects/grain/item.json +16 -0
  38. package/packs/effects/light-leak/frame.js +24 -0
  39. package/packs/effects/light-leak/item.json +15 -0
  40. package/packs/effects/neon/frame.js +28 -0
  41. package/packs/effects/neon/item.json +15 -0
  42. package/packs/effects/pack.json +13 -0
  43. package/packs/effects/shared.js +23 -0
  44. package/packs/effects/warp/frame.js +13 -0
  45. package/packs/effects/warp/item.json +15 -0
  46. package/packs/essentials/pack.json +1 -1
  47. package/packs/essentials/wipe/frame.js +13 -0
  48. package/packs/promo/pack.json +1 -1
  49. package/packs/social/pack.json +1 -1
  50. package/packs/transitions/glitch/frame.js +14 -0
  51. package/packs/transitions/pack.json +1 -1
  52. package/packs/transitions/shared.js +12 -0
  53. package/packs/type/pack.json +1 -1
  54. package/packs/type/word-by-word/frame.js +10 -0
  55. package/packs/ui/bell/frame.js +24 -0
  56. package/packs/ui/bell/item.json +21 -0
  57. package/packs/ui/button/frame.js +55 -0
  58. package/packs/ui/button/item.json +26 -0
  59. package/packs/ui/like/frame.js +34 -0
  60. package/packs/ui/like/item.json +21 -0
  61. package/packs/ui/loader/frame.js +34 -0
  62. package/packs/ui/loader/item.json +23 -0
  63. package/packs/ui/pack.json +13 -0
  64. package/packs/ui/shared.js +54 -0
  65. package/packs/ui/toggle/frame.js +30 -0
  66. package/packs/ui/toggle/item.json +22 -0
  67. package/{skill → skills/volito}/SKILL.md +43 -3
  68. package/skills/volito/references/art-direction.md +58 -0
  69. package/skills/volito/references/brand.md +59 -0
  70. package/skills/volito/references/delivery.md +40 -0
  71. package/skills/volito/references/matching-a-reference.md +39 -0
  72. package/skills/volito/references/motion-craft.md +56 -0
  73. package/skills/volito/references/self-review.md +44 -0
  74. package/skills/volito/references/type-and-layout.md +44 -0
  75. package/skills/volito/references/voice-and-timing.md +37 -0
  76. package/skills/volito/references/writing-items.md +44 -0
  77. /package/{skill/template → templates/item}/frame.js +0 -0
  78. /package/{skill/template → templates/item}/item.json +0 -0
@@ -1,3 +1,9 @@
1
+ import {
2
+ FfmpegMissing,
3
+ fontsDir,
4
+ probeFfmpeg
5
+ } from "./chunk-ZFG3X3DU.js";
6
+
1
7
  // src/engine/graphics-runtime.ts
2
8
  var MG_RUNTIME = String.raw`
3
9
  var MG = (function () {
@@ -3660,6 +3666,36 @@ function motion($, t) {
3660
3666
  return [x, y];
3661
3667
  }
3662
3668
 
3669
+ /** Moves that read as a spring (a pop in the sound design) rather than travel across the frame. */
3670
+ var MM_SPRINGY = { pop: 1, zoom: 1, push: 1, grow: 1, spin: 1, swing: 1, elastic: 1, bounce: 1, squash: 1, stretch: 1, flip: 1, turn: 1, unfold: 1, "grow-x": 1, "grow-y": 1 };
3671
+ /**
3672
+ * Tells the sound trace what a part does, through the runtime's timing helpers (they leave the marks a
3673
+ * sound design reads): a slam shakes, a spring pops, a move across the frame enters from a distance, a
3674
+ * fade fades, a typewriter types, a glitch flashes, and every emphasis hit bumps. One cheap call a frame;
3675
+ * nothing is drawn.
3676
+ */
3677
+ function note(s, tm, c, text) {
3678
+ if (tm.mv.always || typeof $.enter !== "function") return;
3679
+ var mv = s["in"];
3680
+ if (mv === "slam") $.shake(t, tm.at + tm.dur * 0.55, 24);
3681
+ else if (mv === "typewriter") $.typed(t, tm.at, text || "", speed / (s.stagger > 0 ? Number(s.stagger) : 0.055));
3682
+ else if (mv === "scramble") $.typed(t, tm.at, text || "", (text ? Array.from(text).length : 8) / Math.max(0.1, tm.dur));
3683
+ else if (mv === "glitch" || mv === "flicker") $.flash(t, tm.at, { a: 0 });
3684
+ else if (mv === "fade" || mv === "blur" || mv === "appear" || mv === "float") $.fadeIn(t, tm.at);
3685
+ else if (MM_SPRINGY[mv]) $.pop(t, tm.at);
3686
+ else {
3687
+ var p0 = tm.mv.pose(0, 0, c) || {};
3688
+ var dx = p0.x || 0, dy = p0.y || 0;
3689
+ if (!dx && !dy && tm.mv.box && c.box) dx = c.box.w * 0.5;
3690
+ if (dx || dy) $.enter(t, tm.at, { dx: dx, dy: dy }); else $.pop(t, tm.at);
3691
+ }
3692
+ hits(s).forEach(function (h) {
3693
+ var at = hitTime(h.at, tm.at);
3694
+ if (!isFinite(at)) return;
3695
+ if (h.fx === "shake") $.shake(t, at, 16); else if (h.fx === "flash") $.flash(t, at, { a: 0 }); else $.bump(t, at);
3696
+ });
3697
+ }
3698
+
3663
3699
  var api = {};
3664
3700
  api.mood = mood;
3665
3701
  api.seed = seed;
@@ -3674,6 +3710,7 @@ function motion($, t) {
3674
3710
  var tm = timing(s);
3675
3711
  var pv = pivot(o.box, s.origin, o, tm.mv.origin);
3676
3712
  var c = context(s, tm, o.box, pv[0], pv[1], salt(name) % 97);
3713
+ note(s, tm, c);
3677
3714
  return render(kids, poseAt(s, tm, c, t), c);
3678
3715
  };
3679
3716
 
@@ -3740,6 +3777,7 @@ function motion($, t) {
3740
3777
  if (!lay.units.length || lay.mode === "none") {
3741
3778
  var pv = pivot(lay.box, s.origin, o, tm.mv.origin);
3742
3779
  var c = context(s, tm, lay.box, pv[0], pv[1], salt(name) % 97);
3780
+ note(s, tm, c, lay.text);
3743
3781
  return render($.text(lay.text, font), poseAt(s, tm, c, t), c);
3744
3782
  }
3745
3783
  var n = lay.units.length;
@@ -3764,6 +3802,9 @@ function motion($, t) {
3764
3802
  shown = pool.charAt(Math.floor(mmHash(i * 7.7 + Math.floor(t * 18) * 1.9 + vseed) * pool.length));
3765
3803
  }
3766
3804
  if (t >= ut.at) lastShown = u;
3805
+ // The sound trace: typing and scrambles once for the whole text, else the first unit with its move and every other one a tick.
3806
+ if (behave === "type" || behave === "scramble") { if (i === 0) note(s, tm, c2, lay.text); }
3807
+ else if (rk === 0) note(s, ut, c2, u.text); else if (typeof $.bump === "function") $.bump(t, ut.at);
3767
3808
  out += render(draw(shown, u.x, u), poseAt(s, ut, c2, t), c2);
3768
3809
  });
3769
3810
  var typed = tm.at + n * (s.stagger > 0 ? Number(s.stagger) : 0.055) / speed;
@@ -4194,8 +4235,8 @@ function setGlyphSource(fn) {
4194
4235
  }
4195
4236
  function glyphsFor(spec2) {
4196
4237
  const { key, file } = glyphFaceFile(spec2);
4197
- const hit = cache.get(key);
4198
- if (hit) return hit;
4238
+ const hit2 = cache.get(key);
4239
+ if (hit2) return hit2;
4199
4240
  const t = source(key, file);
4200
4241
  if (t) cache.set(key, t);
4201
4242
  return t;
@@ -4206,6 +4247,72 @@ function glyphLibrary(tables) {
4206
4247
  return `var MM_GLYPHS = ${JSON.stringify(map)};`;
4207
4248
  }
4208
4249
 
4250
+ // src/player/states.ts
4251
+ var STATE_PLAYS = ["once", "loop", "hold"];
4252
+ var NAME = /^[A-Za-z][\w-]{0,40}$/;
4253
+ function parseStates(value, where = "states") {
4254
+ if (value == null || value === "") return void 0;
4255
+ let v = value;
4256
+ if (typeof v === "string") {
4257
+ try {
4258
+ v = JSON.parse(v);
4259
+ } catch {
4260
+ throw new Error(`${where} must be a JSON object, like {"idle":{"from":0,"to":1,"play":"loop"}}`);
4261
+ }
4262
+ }
4263
+ if (!v || typeof v !== "object" || Array.isArray(v)) throw new Error(`${where} must be a JSON object of states`);
4264
+ const out = {};
4265
+ for (const [name, raw] of Object.entries(v)) {
4266
+ if (!NAME.test(name)) throw new Error(`${where}: "${name}" is not a state name (letters, digits, - and _)`);
4267
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`${where}.${name} must be an object like {"from":"hover","to":"press","play":"once"}`);
4268
+ const d = raw;
4269
+ const play = d.play ?? "once";
4270
+ if (!STATE_PLAYS.includes(play)) throw new Error(`${where}.${name}.play is one of ${STATE_PLAYS.join(", ")}`);
4271
+ for (const k of ["from", "to"]) if (d[k] !== void 0 && typeof d[k] !== "number" && typeof d[k] !== "string") throw new Error(`${where}.${name}.${k} is seconds or a beat name`);
4272
+ out[name] = { ...d.from !== void 0 ? { from: d.from } : {}, ...d.to !== void 0 ? { to: d.to } : {}, play, ...typeof d.next === "string" ? { next: d.next } : {} };
4273
+ }
4274
+ for (const [name, d] of Object.entries(out)) if (d.next && !(d.next in out)) throw new Error(`${where}.${name}.next is "${d.next}", which is not a state (${Object.keys(out).join(", ")})`);
4275
+ return Object.keys(out).length ? out : void 0;
4276
+ }
4277
+ function parseTriggers(value, states) {
4278
+ if (value == null || value === "") return [];
4279
+ if (typeof value !== "string") throw new Error('on takes "event: state" pairs, like "pointerenter: hover; click: press"');
4280
+ const out = [];
4281
+ for (const raw of value.split(/[;\n]+/)) {
4282
+ const s = raw.trim();
4283
+ if (!s) continue;
4284
+ const m = /^([a-z][a-z-]*)\s*(?:\(([^)]*)\))?\s*:\s*([A-Za-z][\w-]*)$/i.exec(s);
4285
+ if (!m) throw new Error(`on: "${s}" is not "event: state" (or "event(fromState): state")`);
4286
+ const from = (m[2] ?? "").split(/[\s|,]+/).filter(Boolean);
4287
+ const t = { event: m[1].toLowerCase(), from, to: m[3] };
4288
+ if (states) {
4289
+ for (const n of [t.to, ...t.from]) if (!(n in states)) throw new Error(`on: "${n}" is not a state (${Object.keys(states).join(", ")})`);
4290
+ }
4291
+ out.push(t);
4292
+ }
4293
+ return out;
4294
+ }
4295
+ function resolveTime(v, fallback, duration, beats) {
4296
+ let t;
4297
+ if (v === void 0) t = fallback;
4298
+ else if (typeof v === "number") t = v;
4299
+ else if (v === "start") t = 0;
4300
+ else if (v === "end") t = duration;
4301
+ else if (v in beats) t = beats[v];
4302
+ else if (Number.isFinite(Number(v))) t = Number(v);
4303
+ else throw new Error(`"${v}" is not a beat (${Object.keys(beats).join(", ") || "none"}), seconds, start or end`);
4304
+ if (t < 0) t = duration + t;
4305
+ return Math.max(0, Math.min(duration, t));
4306
+ }
4307
+ function resolveState(name, states, duration, beats = {}) {
4308
+ const d = states[name];
4309
+ if (!d) throw new Error(`no state "${name}" (states: ${Object.keys(states).join(", ")})`);
4310
+ const start = resolveTime(d.from, 0, duration, beats);
4311
+ const end = resolveTime(d.to, duration, duration, beats);
4312
+ if (end < start) throw new Error(`state "${name}" ends (${end.toFixed(2)} s) before it starts (${start.toFixed(2)} s)`);
4313
+ return { name, start, end, play: d.play ?? "once", ...d.next ? { next: d.next } : {} };
4314
+ }
4315
+
4209
4316
  // src/three.ts
4210
4317
  var MOTION_3D = String.raw`
4211
4318
  // ———————— Volito 3D ————————
@@ -4557,6 +4664,19 @@ function manifestProblems(m) {
4557
4664
  if (p?.type === "choice" && !p.options?.length) out.push(`prop "${name}" is a choice with no options`);
4558
4665
  if (p?.when && !(p.when.prop in (m.props ?? {}))) out.push(`prop "${name}" depends on an unknown prop "${p.when.prop}"`);
4559
4666
  }
4667
+ if (m.states !== void 0 || m.on !== void 0) {
4668
+ try {
4669
+ const states = parseStates(m.states, "states");
4670
+ if (!states) out.push("on needs states");
4671
+ else {
4672
+ parseTriggers(m.on, states);
4673
+ const dur = m.duration?.default ?? 0;
4674
+ for (const name of Object.keys(states)) resolveState(name, states, dur, m.beats ?? {});
4675
+ }
4676
+ } catch (e) {
4677
+ out.push(e.message);
4678
+ }
4679
+ }
4560
4680
  return out;
4561
4681
  }
4562
4682
  function makeItem(pack, manifest, frameCode, shared = "") {
@@ -4867,175 +4987,19 @@ ${parts.join("\n")}` : ""
4867
4987
  ].filter(Boolean).join("\n");
4868
4988
  }
4869
4989
 
4870
- // src/node/paths.ts
4871
- import { existsSync, readFileSync } from "node:fs";
4872
- import { dirname, join } from "node:path";
4873
- import { fileURLToPath } from "node:url";
4874
- var PACKAGE_NAME = "volito";
4875
- var root = null;
4876
- function packageRoot() {
4877
- if (root) return root;
4878
- let dir = dirname(fileURLToPath(import.meta.url));
4879
- for (let i = 0; i < 6; i++) {
4880
- const pj = join(dir, "package.json");
4881
- if (existsSync(pj)) {
4882
- try {
4883
- if (JSON.parse(readFileSync(pj, "utf8")).name === PACKAGE_NAME) return root = dir;
4884
- } catch {
4885
- }
4886
- }
4887
- dir = dirname(dir);
4888
- }
4889
- throw new Error(`${PACKAGE_NAME}: cannot find its package folder from ${fileURLToPath(import.meta.url)}`);
4890
- }
4891
- var fontsDir = () => join(packageRoot(), "fonts");
4892
- var packsDir = () => join(packageRoot(), "packs");
4893
- var libsDir = () => join(packageRoot(), "libs");
4894
- function packageVersion() {
4895
- return JSON.parse(readFileSync(join(packageRoot(), "package.json"), "utf8")).version;
4896
- }
4897
-
4898
- // src/node/packs.ts
4899
- import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync, statSync } from "node:fs";
4900
- import { basename, join as join3, resolve } from "node:path";
4901
-
4902
- // src/node/setup.ts
4903
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
4904
- import { join as join2 } from "node:path";
4905
- setGlyphSource((key, file) => {
4906
- const path = join2(fontsDir(), file);
4907
- return existsSync2(path) ? glyphTable(new TrueTypeFace(new Uint8Array(readFileSync2(path))), key) : void 0;
4908
- });
4909
- for (const name of LIBRARY_NAMES) {
4910
- const file = join2(libsDir(), `${name}.js`);
4911
- if (existsSync2(file)) registerLibrary(name, () => readFileSync2(file, "utf8"));
4912
- }
4913
-
4914
- // src/node/packs.ts
4915
- var readJson = (file) => {
4916
- try {
4917
- return JSON.parse(readFileSync3(file, "utf8"));
4918
- } catch (err) {
4919
- throw new MotionError(`${file}: ${err.message}`);
4920
- }
4921
- };
4922
- function loadPack(dir) {
4923
- const manifest = readJson(join3(dir, "pack.json"));
4924
- if (manifest.format !== MOTION_FORMAT) throw new MotionError(`${dir}: pack format ${manifest.format} \u2014 this version reads format ${MOTION_FORMAT}`);
4925
- const shared = manifest.shared ? readFileSync3(join3(dir, manifest.shared), "utf8") : "";
4926
- const entries = manifest.items.map((id) => {
4927
- const itemDir = join3(dir, id);
4928
- const m = readJson(join3(itemDir, "item.json"));
4929
- if (m.id !== id) throw new MotionError(`${itemDir}: item.json id "${m.id}" does not match its folder`);
4930
- return makeItem(manifest.id, m, readFileSync3(join3(itemDir, "frame.js"), "utf8"), shared);
4931
- });
4932
- return { ...manifest, entries };
4933
- }
4934
- var builtin = null;
4935
- function builtinPacks() {
4936
- if (builtin) return builtin;
4937
- const dir = packsDir();
4938
- const order = ["essentials", "broadcast", "social", "data", "3d", "transitions", "annotate", "type", "backdrops", "promo"];
4939
- const rank = (d) => order.includes(d) ? order.indexOf(d) : order.length;
4940
- builtin = existsSync3(dir) ? readdirSync(dir).filter((d) => existsSync3(join3(dir, d, "pack.json"))).sort((a, b) => rank(a) - rank(b) || a.localeCompare(b)).map((d) => loadPack(join3(dir, d))) : [];
4941
- return builtin;
4942
- }
4943
- function packsIn(dirs) {
4944
- const out = [];
4945
- for (const raw of dirs) {
4946
- const dir = resolve(raw);
4947
- if (existsSync3(join3(dir, "pack.json"))) out.push(loadPack(dir));
4948
- else if (existsSync3(dir) && statSync(dir).isDirectory()) {
4949
- for (const d of readdirSync(dir).sort()) if (existsSync3(join3(dir, d, "pack.json"))) out.push(loadPack(join3(dir, d)));
4950
- }
4951
- }
4952
- return out;
4953
- }
4954
- function library(extraDirs = []) {
4955
- const env = (process.env.VOLITO_PACKS ?? process.env.MONARC_MOTION_PACKS ?? "").split(":").filter(Boolean);
4956
- const packs = [...builtinPacks(), ...packsIn([...env, ...extraDirs])];
4957
- const seen = /* @__PURE__ */ new Set();
4958
- for (const p of packs) {
4959
- if (seen.has(p.id)) throw new MotionError(`two packs are named "${p.id}"`);
4960
- seen.add(p.id);
4961
- }
4962
- return { packs, items: packs.flatMap((p) => p.entries) };
4963
- }
4964
- function findItem(name, lib = library()) {
4965
- const n = name.trim();
4966
- const byRef = lib.items.find((i) => i.ref === n);
4967
- if (byRef) return byRef;
4968
- const path = resolve(n);
4969
- if (existsSync3(path)) {
4970
- if (statSync(path).isDirectory() && existsSync3(join3(path, "item.json"))) {
4971
- const m = readJson(join3(path, "item.json"));
4972
- const shared = existsSync3(join3(path, "shared.js")) ? readFileSync3(join3(path, "shared.js"), "utf8") : "";
4973
- return makeItem("local", m, readFileSync3(join3(path, "frame.js"), "utf8"), shared);
4974
- }
4975
- if (/\.(m?js|txt)$/.test(path)) return customItem(readFileSync3(path, "utf8"), { id: basename(path).replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") });
4976
- }
4977
- const byId = lib.items.filter((i) => i.id === n);
4978
- if (byId.length === 1) return byId[0];
4979
- if (byId.length > 1) throw new MotionError(`"${n}" is in more than one pack: ${byId.map((i) => i.ref).join(", ")}`);
4980
- const q2 = n.toLowerCase();
4981
- const score = (i) => i.id.startsWith(q2) ? 0 : i.id.includes(q2) || q2.includes(i.id) ? 1 : i.name.toLowerCase().includes(q2) ? 2 : (i.tags ?? []).some((t) => t.includes(q2)) ? 3 : 9;
4982
- const near = lib.items.map((i) => ({ ref: i.ref, s: score(i) })).filter((r) => r.s < 9).sort((a, b) => a.s - b.s).map((r) => r.ref);
4983
- throw new MotionError(`no item "${n}"${near.length ? ` \u2014 did you mean ${near.slice(0, 4).join(", ")}?` : ""} (list them with: volito list)`);
4984
- }
4985
- function searchItems(q2 = {}, lib = library()) {
4986
- const words = (q2.search ?? "").toLowerCase().split(/\s+/).filter(Boolean);
4987
- return lib.items.filter((i) => {
4988
- if (q2.category && i.category !== q2.category) return false;
4989
- if (q2.pack && i.pack !== q2.pack) return false;
4990
- if (q2.placement && i.placement !== q2.placement) return false;
4991
- if (!words.length) return true;
4992
- const hay = [i.id, i.name, i.description, i.category, ...i.tags ?? []].join(" ").toLowerCase();
4993
- return words.every((w) => hay.includes(w));
4994
- });
4995
- }
4996
-
4997
- // src/node/ffmpeg.ts
4998
- import { execFileSync } from "node:child_process";
4999
- var probed;
5000
- function ffmpegPath() {
5001
- return process.env.VOLITO_FFMPEG || process.env.MONARC_MOTION_FFMPEG || "ffmpeg";
5002
- }
5003
- function probeFfmpeg() {
5004
- if (probed !== void 0) return probed;
5005
- const path = ffmpegPath();
5006
- try {
5007
- const out = execFileSync(path, ["-hide_banner", "-encoders"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 });
5008
- const encoders = /* @__PURE__ */ new Set();
5009
- for (const line of out.split("\n")) {
5010
- const m = /^\s*[VAS][A-Z.]{5}\s+(\S+)/.exec(line);
5011
- if (m) encoders.add(m[1]);
5012
- }
5013
- probed = { path, encoders };
5014
- } catch {
5015
- probed = null;
5016
- }
5017
- return probed;
5018
- }
5019
- var FfmpegMissing = class extends Error {
5020
- constructor(what) {
5021
- super(`${what} needs ffmpeg \u2014 install it (macOS: brew install ffmpeg \xB7 Debian/Ubuntu: apt install ffmpeg \xB7 Windows: winget install ffmpeg) or set VOLITO_FFMPEG to its path. PNG sequences and stills need no ffmpeg.`);
5022
- this.name = "FfmpegMissing";
5023
- }
5024
- };
5025
-
5026
4990
  // src/node/sandbox.ts
5027
4991
  import vm from "node:vm";
5028
4992
 
5029
4993
  // src/node/fonts.ts
5030
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
5031
- import { join as join4 } from "node:path";
4994
+ import { existsSync, readFileSync } from "node:fs";
4995
+ import { join } from "node:path";
5032
4996
  import * as fontkit from "fontkit";
5033
4997
  var faces = null;
5034
4998
  function allFaces() {
5035
4999
  if (faces) return faces;
5036
- const index = join4(fontsDir(), "faces.json");
5037
- if (!existsSync4(index)) throw new Error(`Volito: no fonts at ${fontsDir()} \u2014 run "pnpm run fonts" (or reinstall the package)`);
5038
- faces = JSON.parse(readFileSync4(index, "utf8")).faces;
5000
+ const index = join(fontsDir(), "faces.json");
5001
+ if (!existsSync(index)) throw new Error(`Volito: no fonts at ${fontsDir()} \u2014 run "pnpm run fonts" (or reinstall the package)`);
5002
+ faces = JSON.parse(readFileSync(index, "utf8")).faces;
5039
5003
  return faces;
5040
5004
  }
5041
5005
  var familiesOf = () => [...new Set(allFaces().map((f) => f.family))];
@@ -5068,8 +5032,8 @@ function subsetsOf(family) {
5068
5032
  var resolved = /* @__PURE__ */ new Map();
5069
5033
  function faceFor(family, cp) {
5070
5034
  const key = `${family}|${cp}`;
5071
- const hit = resolved.get(key);
5072
- if (hit !== void 0) return hit;
5035
+ const hit2 = resolved.get(key);
5036
+ if (hit2 !== void 0) return hit2;
5073
5037
  let face = null;
5074
5038
  const own = subsetsOf(family);
5075
5039
  const latin = own.find((s) => s.subset === "latin");
@@ -5100,18 +5064,18 @@ function fontFilesForDocument(doc) {
5100
5064
  }
5101
5065
  }
5102
5066
  const dir = fontsDir();
5103
- return allFaces().filter((f) => names.has(f.face)).map((f) => join4(dir, f.file));
5067
+ return allFaces().filter((f) => names.has(f.face)).map((f) => join(dir, f.file));
5104
5068
  }
5105
5069
  function fontFilesFor(families) {
5106
5070
  const want = /* @__PURE__ */ new Set(["Inter", ...families]);
5107
5071
  const dir = fontsDir();
5108
- return allFaces().filter((f) => want.has(f.family) && f.subset === "latin").map((f) => join4(dir, f.file));
5072
+ return allFaces().filter((f) => want.has(f.family) && f.subset === "latin").map((f) => join(dir, f.file));
5109
5073
  }
5110
5074
  var fontCache = /* @__PURE__ */ new Map();
5111
5075
  var loadFont = (file) => {
5112
5076
  let f = fontCache.get(file);
5113
5077
  if (!f) {
5114
- f = fontkit.create(readFileSync4(join4(fontsDir(), file)));
5078
+ f = fontkit.create(readFileSync(join(fontsDir(), file)));
5115
5079
  fontCache.set(file, f);
5116
5080
  }
5117
5081
  return f;
@@ -5131,8 +5095,8 @@ function fontForChar(family, weight, italic, cp) {
5131
5095
  var measured = /* @__PURE__ */ new Map();
5132
5096
  function measureText(font, text) {
5133
5097
  const key = `${font}|${text}`;
5134
- const hit = measured.get(key);
5135
- if (hit !== void 0) return hit;
5098
+ const hit2 = measured.get(key);
5099
+ if (hit2 !== void 0) return hit2;
5136
5100
  const m = /^(italic\s+)?(\d+)\s+([\d.]+)px\s+'([^']+)'/.exec(font);
5137
5101
  if (!m) return Number.NaN;
5138
5102
  const family = allFaces().some((f) => f.family === m[4]) ? m[4] : "Inter";
@@ -5169,7 +5133,7 @@ function fontFaceCss(doc) {
5169
5133
  for (const f of allFaces()) if (f.subset !== "latin" && rangeCovers(f.range, cp)) subsets.add(f.subset);
5170
5134
  }
5171
5135
  const dir = fontsDir();
5172
- return allFaces().filter((f) => fams.has(f.family) && subsets.has(f.subset)).map((f) => `@font-face{font-family:'${f.family}';font-weight:${f.weight};font-style:${f.italic ? "italic" : "normal"};${f.range ? `unicode-range:${f.range};` : ""}src:url(data:font/ttf;base64,${readFileSync4(join4(dir, f.file)).toString("base64")}) format('truetype');}`).join("");
5136
+ return allFaces().filter((f) => fams.has(f.family) && subsets.has(f.subset)).map((f) => `@font-face{font-family:'${f.family}';font-weight:${f.weight};font-style:${f.italic ? "italic" : "normal"};${f.range ? `unicode-range:${f.range};` : ""}src:url(data:font/ttf;base64,${readFileSync(join(dir, f.file)).toString("base64")}) format('truetype');}`).join("");
5173
5137
  }
5174
5138
 
5175
5139
  // src/node/sandbox.ts
@@ -5198,6 +5162,11 @@ self.postMessage = function (m) { self.__out = m; };
5198
5162
  globalThis.OffscreenCanvas = OffscreenCanvas;
5199
5163
  })();
5200
5164
  `;
5165
+ var RETURN_LINE = "return { frame: typeof frame === 'function' ? frame : null, setup: typeof setup === 'function' ? setup : null };";
5166
+ var RETURN_WITH_SOUND = "return { frame: typeof frame === 'function' ? frame : null, setup: typeof setup === 'function' ? setup : null, sound: typeof sound === 'function' ? sound : null };";
5167
+ function soundHookAvailable() {
5168
+ return programWorkerSource2("function frame() { return ''; }").source.includes(RETURN_LINE);
5169
+ }
5201
5170
  var ProgramRunner = class _ProgramRunner {
5202
5171
  ctx;
5203
5172
  codeLine;
@@ -5211,7 +5180,9 @@ var ProgramRunner = class _ProgramRunner {
5211
5180
  * start. `lineOffset` is how many lines of the code come before the author's own (the kit, a pack's helpers).
5212
5181
  */
5213
5182
  static async create(program, env, timeoutMs = 5e3, lineOffset = 0) {
5214
- const { source: source2, codeLine } = programWorkerSource2(program.code);
5183
+ const built = programWorkerSource2(program.code);
5184
+ const codeLine = built.codeLine;
5185
+ const source2 = built.source.includes(RETURN_LINE) ? built.source.replace(RETURN_LINE, RETURN_WITH_SOUND) : built.source;
5215
5186
  const measure = (font, text) => {
5216
5187
  try {
5217
5188
  const w = measureText(String(font), String(text));
@@ -5278,6 +5249,27 @@ var ProgramRunner = class _ProgramRunner {
5278
5249
  if (out.error) throw new ProgramError(out.error, this.mapLine(out.line));
5279
5250
  return { svg: out.svg ?? "", defs: out.defs ?? "", warnings: out.warnings ?? [], ms: performance.now() - started };
5280
5251
  }
5252
+ /**
5253
+ * The program's own sound design: what `function sound($)` returns, read once with `$` at t = 0 ($.dur,
5254
+ * $.beat, $.at and $.data are there). `spec` is null when the program defines none; `error` when it threw.
5255
+ */
5256
+ sound(timeoutMs = 5e3) {
5257
+ if (this.dead) throw this.dead;
5258
+ let raw;
5259
+ try {
5260
+ raw = vm.runInContext(
5261
+ "(function () { if (!__program.sound) return null; var $ = MG.make(__env, 0); $.state = __state; try { return JSON.stringify({ spec: __program.sound($) }); } catch (err) { return JSON.stringify({ error: String(err && err.message || err), line: __line(err) }); } })()",
5262
+ this.ctx,
5263
+ { timeout: timeoutMs }
5264
+ );
5265
+ } catch (err) {
5266
+ return { spec: null, error: `sound: ${this.message(err)}` };
5267
+ }
5268
+ if (!raw) return { spec: null };
5269
+ const out = JSON.parse(raw);
5270
+ if (out.error) return { spec: null, error: new ProgramError(`sound: ${out.error}`, this.mapLine(out.line)).message };
5271
+ return { spec: out.spec ?? null };
5272
+ }
5281
5273
  /** Every frame run once with the runtime's trace on: when each element pops, slides, stamps or types in. */
5282
5274
  trace(fps, timeoutMs = 2e4) {
5283
5275
  const out = this.sendSync({ type: "trace", id: 1, fps }, timeoutMs);
@@ -5285,11 +5277,742 @@ var ProgramRunner = class _ProgramRunner {
5285
5277
  }
5286
5278
  };
5287
5279
 
5280
+ // src/sound.ts
5281
+ var SOUND_KINDS = ["whoosh", "hit", "stamp", "pop", "tick", "click", "riser", "fall", "sub", "shimmer", "ding", "swell", "zap", "type", "scratch", "tone", "chord"];
5282
+ var SOUND_NOTES = {
5283
+ whoosh: "air sweeping past \u2014 a move across the frame (dur 0.45)",
5284
+ hit: "an impact: a sub thump with a click \u2014 something lands",
5285
+ stamp: "a heavy slam \u2014 bigger than hit",
5286
+ pop: "a small bubble pop \u2014 something springs up",
5287
+ tick: "a tiny click \u2014 one letter, one word, one step",
5288
+ click: "a UI click \u2014 a button pressed",
5289
+ riser: "noise and a tone climbing \u2014 tension into a moment (dur 1)",
5290
+ fall: "the reverse \u2014 a downlifter after a moment (dur 0.8)",
5291
+ sub: "a low drop under a big reveal",
5292
+ shimmer: "high sparkle \u2014 a light sweep, particles (dur 1.2)",
5293
+ ding: "a bell \u2014 a notification, a check, a logo lockup",
5294
+ swell: "soft air rising \u2014 a fade, a gentle entrance (dur 0.8)",
5295
+ zap: "static and a chirp \u2014 a glitch (dur 0.28)",
5296
+ type: "typing: keys ticking for dur seconds (dur 0.8)",
5297
+ scratch: "pencil or marker on paper for dur seconds (dur 0.8)",
5298
+ tone: 'a note (note: "A4", or pitch in semitones from A4) for dur seconds',
5299
+ chord: "a chord in the bed's key for dur seconds"
5300
+ };
5301
+ var SR = 48e3;
5302
+ var TAU = Math.PI * 2;
5303
+ var clamp = (v, a, b) => v < a ? a : v > b ? b : v;
5304
+ var semis = (n) => Math.pow(2, n / 12);
5305
+ function rng(seed) {
5306
+ let a = (Math.floor(seed * 1000003) ^ 2654435769) >>> 0;
5307
+ return () => {
5308
+ a = a + 1831565813 >>> 0;
5309
+ let t = a;
5310
+ t = Math.imul(t ^ t >>> 15, t | 1);
5311
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
5312
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
5313
+ };
5314
+ }
5315
+ var Svf = class {
5316
+ ic1 = 0;
5317
+ ic2 = 0;
5318
+ a1 = 0;
5319
+ a2 = 0;
5320
+ a3 = 0;
5321
+ k = 1;
5322
+ lp = 0;
5323
+ bp = 0;
5324
+ hp = 0;
5325
+ set(cutoff, q2) {
5326
+ const g = Math.tan(Math.PI * clamp(cutoff, 10, SR * 0.45) / SR);
5327
+ this.k = 1 / Math.max(0.1, q2);
5328
+ this.a1 = 1 / (1 + g * (g + this.k));
5329
+ this.a2 = g * this.a1;
5330
+ this.a3 = g * this.a2;
5331
+ }
5332
+ run(x) {
5333
+ const v3 = x - this.ic2;
5334
+ const v1 = this.a1 * this.ic1 + this.a2 * v3;
5335
+ const v2 = this.ic2 + this.a2 * this.ic1 + this.a3 * v3;
5336
+ this.ic1 = 2 * v1 - this.ic1;
5337
+ this.ic2 = 2 * v2 - this.ic2;
5338
+ this.lp = v2;
5339
+ this.bp = v1;
5340
+ this.hp = x - this.k * v1 - v2;
5341
+ }
5342
+ };
5343
+ var NOTE_INDEX = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
5344
+ function noteHz(note) {
5345
+ const m = /^\s*([A-Ga-g])\s*([#b♯♭]?)\s*(-?\d)?\s*$/.exec(note);
5346
+ if (!m) return null;
5347
+ let n = NOTE_INDEX[m[1].toUpperCase()];
5348
+ if (m[2] === "#" || m[2] === "\u266F") n += 1;
5349
+ if (m[2] === "b" || m[2] === "\u266D") n -= 1;
5350
+ const oct = m[3] === void 0 ? 4 : Number(m[3]);
5351
+ return 440 * semis(n + (oct - 4) * 12 - 9);
5352
+ }
5353
+ function parseKey(key) {
5354
+ const m = /^\s*([A-Ga-g])\s*([#b♯♭]?)\s*(m\b|min|minor|maj|major)?\s*$/i.exec(key ?? "");
5355
+ if (!m) return { root: 0, mode: "minor" };
5356
+ let n = NOTE_INDEX[m[1].toUpperCase()] - 9;
5357
+ if (m[2] === "#" || m[2] === "\u266F") n += 1;
5358
+ if (m[2] === "b" || m[2] === "\u266D") n -= 1;
5359
+ const q2 = (m[3] ?? "").toLowerCase();
5360
+ return { root: (n % 12 + 12) % 12, mode: q2.startsWith("maj") ? "major" : q2 ? "minor" : m[1] === m[1].toUpperCase() && !q2 ? "major" : "minor" };
5361
+ }
5362
+ function chordTones(root, mode, degree, centre) {
5363
+ const scale = mode === "major" ? [0, 2, 4, 5, 7, 9, 11] : [0, 2, 3, 5, 7, 8, 10];
5364
+ const d = degree - 1;
5365
+ const notes = [scale[d], scale[(d + 2) % 7] + (d + 2 >= 7 ? 12 : 0), scale[(d + 4) % 7] + (d + 4 >= 7 ? 12 : 0)];
5366
+ return notes.map((n) => {
5367
+ let s = root + n + centre;
5368
+ while (s > centre + 7) s -= 12;
5369
+ while (s < centre - 7) s += 12;
5370
+ return s;
5371
+ });
5372
+ }
5373
+ var BASE_LEVEL = {
5374
+ whoosh: 0.5,
5375
+ hit: 0.95,
5376
+ stamp: 1.05,
5377
+ pop: 0.5,
5378
+ tick: 0.34,
5379
+ click: 0.3,
5380
+ riser: 0.5,
5381
+ fall: 0.45,
5382
+ sub: 0.95,
5383
+ shimmer: 0.32,
5384
+ ding: 0.5,
5385
+ swell: 0.32,
5386
+ zap: 0.5,
5387
+ type: 0.4,
5388
+ scratch: 0.32,
5389
+ tone: 0.42,
5390
+ chord: 0.45
5391
+ };
5392
+ var DEFAULT_DUR = { whoosh: 0.45, riser: 1, fall: 0.8, shimmer: 1.2, swell: 0.8, zap: 0.28, type: 0.8, scratch: 0.8, tone: 0.6, chord: 1.2 };
5393
+ var alloc = (sec) => new Float32Array(Math.max(1, Math.round(sec * SR)));
5394
+ var envAD = (t, a, tau) => t < a ? t / a : Math.exp(-(t - a) / tau);
5395
+ function whoosh(dur, pitch, r) {
5396
+ const buf = alloc(dur);
5397
+ const f = new Svf();
5398
+ const n = buf.length;
5399
+ const base = 260 * semis(pitch) * (0.9 + 0.2 * r());
5400
+ for (let i = 0; i < n; i++) {
5401
+ const p = i / n;
5402
+ const bell = Math.sin(Math.PI * p);
5403
+ f.set(base * Math.pow(2, 3.4 * Math.pow(bell, 0.85)), 1.4);
5404
+ f.run(r() * 2 - 1);
5405
+ buf[i] = (f.bp * 1.7 + f.lp * 0.25) * Math.pow(bell, 1.6);
5406
+ }
5407
+ return { buf, send: 0.22, duck: 0 };
5408
+ }
5409
+ function hit(pitch, r, heavy) {
5410
+ const dur = heavy ? 0.75 : 0.5;
5411
+ const buf = alloc(dur);
5412
+ const n = buf.length;
5413
+ const body = new Svf();
5414
+ body.set(heavy ? 900 : 1800, 0.9);
5415
+ const click2 = new Svf();
5416
+ click2.set(3200, 0.8);
5417
+ const f0 = (heavy ? 120 : 170) * semis(pitch);
5418
+ const f1 = (heavy ? 38 : 48) * semis(pitch);
5419
+ let ph = 0;
5420
+ for (let i = 0; i < n; i++) {
5421
+ const t = i / SR;
5422
+ const freq = f1 + (f0 - f1) * Math.exp(-t / (heavy ? 0.09 : 0.06));
5423
+ ph += TAU * freq / SR;
5424
+ const sub2 = Math.sin(ph) * envAD(t, 2e-3, heavy ? 0.22 : 0.14);
5425
+ const w = r() * 2 - 1;
5426
+ body.run(w);
5427
+ click2.run(w);
5428
+ const slap = body.lp * envAD(t, 1e-3, heavy ? 0.05 : 0.03) * (heavy ? 1 : 0.8);
5429
+ const tk = click2.hp * (t < 4e-3 ? 1 - t / 4e-3 : 0) * 0.5;
5430
+ const rattle = heavy ? Math.sin(TAU * 70 * t) * Math.sin(TAU * 11 * t) * Math.exp(-t / 0.12) * 0.25 : 0;
5431
+ const x = sub2 + slap + tk + rattle;
5432
+ buf[i] = Math.tanh(x * 1.5);
5433
+ }
5434
+ return { buf, send: heavy ? 0.4 : 0.3, duck: 1 };
5435
+ }
5436
+ function pop(pitch, r) {
5437
+ const buf = alloc(0.2);
5438
+ const n = buf.length;
5439
+ const f1 = 330 * semis(pitch) * (0.95 + 0.1 * r());
5440
+ const f0 = f1 * 3.4;
5441
+ let ph = 0;
5442
+ for (let i = 0; i < n; i++) {
5443
+ const t = i / SR;
5444
+ ph += TAU * (f1 + (f0 - f1) * Math.exp(-t / 0.018)) / SR;
5445
+ buf[i] = Math.sin(ph) * envAD(t, 1e-3, 0.045) + (r() * 2 - 1) * (t < 15e-4 ? 0.5 : 0);
5446
+ }
5447
+ return { buf, send: 0.15, duck: 0.35 };
5448
+ }
5449
+ function tick(pitch, r, soft = false) {
5450
+ const buf = alloc(0.06);
5451
+ const n = buf.length;
5452
+ const f = new Svf();
5453
+ f.set((soft ? 2e3 : 2700) * semis(pitch), 7);
5454
+ const fb = 1700 * semis(pitch);
5455
+ for (let i = 0; i < n; i++) {
5456
+ const t = i / SR;
5457
+ f.run(r() * 2 - 1);
5458
+ buf[i] = f.bp * 2.2 * envAD(t, 5e-4, 9e-3) + Math.sin(TAU * fb * t) * envAD(t, 5e-4, 6e-3) * 0.5;
5459
+ }
5460
+ return { buf, send: 0.1, duck: 0 };
5461
+ }
5462
+ function click(pitch, r) {
5463
+ const buf = alloc(0.05);
5464
+ const n = buf.length;
5465
+ const f = new Svf();
5466
+ f.set(4200 * semis(pitch), 1);
5467
+ for (let i = 0; i < n; i++) {
5468
+ const t = i / SR;
5469
+ f.run(r() * 2 - 1);
5470
+ buf[i] = f.hp * envAD(t, 3e-4, 3e-3) * 1.4 + Math.sin(TAU * 1400 * semis(pitch) * t) * envAD(t, 5e-4, 0.012) * 0.6;
5471
+ }
5472
+ return { buf, send: 0.08, duck: 0 };
5473
+ }
5474
+ function riser(dur, pitch, r, down) {
5475
+ const buf = alloc(dur);
5476
+ const n = buf.length;
5477
+ const f = new Svf();
5478
+ let ph = 0;
5479
+ const lo = 110 * semis(pitch);
5480
+ for (let i = 0; i < n; i++) {
5481
+ const t = i / SR;
5482
+ let p = i / n;
5483
+ if (down) p = 1 - p;
5484
+ f.set(300 * Math.pow(2, 4.3 * p), 1.1);
5485
+ f.run(r() * 2 - 1);
5486
+ const trem = 0.7 + 0.3 * Math.sin(TAU * (3 + 22 * p * p) * t);
5487
+ const amp = Math.pow(p, down ? 1.4 : 1.8);
5488
+ ph += TAU * lo * Math.pow(2, 2 * p) / SR;
5489
+ const tone2 = Math.sin(ph) * Math.pow(p, 2) * 0.4;
5490
+ const edge = down ? Math.min(1, t / 0.01) : Math.min(1, (dur - t) / 0.012);
5491
+ buf[i] = (f.hp * 1.1 * trem * amp + f.bp * 0.4 * amp + tone2) * edge;
5492
+ }
5493
+ return { buf, send: 0.3, duck: 0 };
5494
+ }
5495
+ function sub(pitch) {
5496
+ const buf = alloc(0.75);
5497
+ const n = buf.length;
5498
+ const f0 = 72 * semis(pitch), f1 = 34 * semis(pitch);
5499
+ let ph = 0;
5500
+ for (let i = 0; i < n; i++) {
5501
+ const t = i / SR;
5502
+ ph += TAU * (f1 + (f0 - f1) * Math.exp(-t / 0.12)) / SR;
5503
+ buf[i] = (Math.sin(ph) + Math.sin(2 * ph) * 0.2) * envAD(t, 5e-3, 0.26);
5504
+ }
5505
+ return { buf, send: 0.15, duck: 1 };
5506
+ }
5507
+ function shimmer(dur, pitch, r) {
5508
+ const buf = alloc(dur);
5509
+ const n = buf.length;
5510
+ const parts = [];
5511
+ for (let k = 0; k < 7; k++) {
5512
+ const f = (1400 + 3800 * r()) * semis(pitch);
5513
+ parts.push({ f, on: r() * dur * 0.35, tau: dur * (0.25 + 0.2 * r()), a: Math.sqrt(1400 / f), vib: 5 + 2 * r(), ph: r() * TAU });
5514
+ }
5515
+ const air = new Svf();
5516
+ air.set(6e3, 0.7);
5517
+ for (let i = 0; i < n; i++) {
5518
+ const t = i / SR;
5519
+ let x = 0;
5520
+ for (const p of parts) {
5521
+ if (t < p.on) continue;
5522
+ const dt = t - p.on;
5523
+ x += Math.sin(p.ph + TAU * p.f * dt * (1 + 3e-3 * Math.sin(TAU * p.vib * dt))) * p.a * envAD(dt, 0.03, p.tau);
5524
+ }
5525
+ air.run(r() * 2 - 1);
5526
+ buf[i] = x * 0.3 + air.hp * 0.05 * Math.sin(Math.PI * (i / n));
5527
+ }
5528
+ return { buf, send: 0.6, duck: 0 };
5529
+ }
5530
+ function ding(hz) {
5531
+ const buf = alloc(1.8);
5532
+ const n = buf.length;
5533
+ const ratios = [1, 2, 3.01, 4.16, 5.43];
5534
+ const amps = [1, 0.5, 0.3, 0.18, 0.1];
5535
+ const scale = Math.sqrt(880 / hz);
5536
+ const taus = [1.5, 0.8, 0.55, 0.38, 0.22].map((x) => x * scale);
5537
+ for (let i = 0; i < n; i++) {
5538
+ const t = i / SR;
5539
+ const bend = 1 + 0.01 * Math.exp(-t / 0.02);
5540
+ let x = 0;
5541
+ for (let k = 0; k < ratios.length; k++) x += Math.sin(TAU * hz * ratios[k] * bend * t) * amps[k] * envAD(t, 2e-3, taus[k]);
5542
+ buf[i] = x * 0.5;
5543
+ }
5544
+ return { buf, send: 0.5, duck: 0.3 };
5545
+ }
5546
+ function swell(dur, pitch, r) {
5547
+ const buf = alloc(dur);
5548
+ const n = buf.length;
5549
+ const f = new Svf();
5550
+ const base = 110 * semis(pitch);
5551
+ for (let i = 0; i < n; i++) {
5552
+ const t = i / SR;
5553
+ const p = i / n;
5554
+ const bell = Math.pow(Math.sin(Math.PI * p), 2);
5555
+ f.set(300 + 900 * Math.sin(Math.PI * Math.min(1, p * 1.3)), 0.8);
5556
+ f.run(r() * 2 - 1);
5557
+ buf[i] = f.lp * 0.7 * bell + Math.sin(TAU * base * t * (1 + 0.02 * Math.sin(TAU * 0.7 * t))) * 0.45 * bell;
5558
+ }
5559
+ return { buf, send: 0.45, duck: 0 };
5560
+ }
5561
+ function zap(dur, pitch, r) {
5562
+ const buf = alloc(dur);
5563
+ const n = buf.length;
5564
+ const f = new Svf();
5565
+ f.set(900, 0.8);
5566
+ let gate = 1, next = 0, hold = 0, holdN = 0, ph = 0;
5567
+ for (let i = 0; i < n; i++) {
5568
+ const t = i / SR;
5569
+ if (i >= next) {
5570
+ gate = r() < 0.55 ? 1 : 0;
5571
+ next = i + Math.floor(SR / (45 + 60 * r()));
5572
+ }
5573
+ const dec = Math.max(1, Math.floor(1 + 14 * (i / n)));
5574
+ if (holdN-- <= 0) {
5575
+ hold = r() * 2 - 1;
5576
+ holdN = dec;
5577
+ }
5578
+ f.run(hold);
5579
+ ph += TAU * (180 + 2e3 * Math.exp(-t / 0.05)) * semis(pitch) / SR;
5580
+ const chirp = (Math.sin(ph) > 0 ? 1 : -1) * 0.35 * Math.exp(-t / 0.06);
5581
+ buf[i] = (f.hp * 1.2 * gate + chirp) * envAD(t, 1e-3, 0.12);
5582
+ }
5583
+ return { buf, send: 0.12, duck: 0.2 };
5584
+ }
5585
+ function typing(dur, pitch, r) {
5586
+ const buf = alloc(dur + 0.08);
5587
+ let t0 = 0;
5588
+ while (t0 < dur) {
5589
+ const k = tick(pitch + (r() - 0.5) * 5, r);
5590
+ const start = Math.floor(t0 * SR);
5591
+ const vel = 0.7 + 0.3 * r();
5592
+ for (let i = 0; i < k.buf.length && start + i < buf.length; i++) buf[start + i] += k.buf[i] * vel;
5593
+ for (let i = 0; i < 400 && start + i < buf.length; i++) buf[start + i] += Math.sin(TAU * 190 * (i / SR)) * envAD(i / SR, 5e-4, 4e-3) * 0.35 * vel;
5594
+ t0 += 0.065 + 0.05 * r() + (r() < 0.12 ? 0.08 : 0);
5595
+ }
5596
+ return { buf, send: 0.1, duck: 0 };
5597
+ }
5598
+ function scratch(dur, pitch, r) {
5599
+ const buf = alloc(dur);
5600
+ const n = buf.length;
5601
+ const f = new Svf();
5602
+ let wob = 0.5;
5603
+ for (let i = 0; i < n; i++) {
5604
+ const t = i / SR;
5605
+ const p = i / n;
5606
+ if (i % 240 === 0) wob += (r() - 0.5) * 0.5, wob = clamp(wob, 0.1, 1);
5607
+ f.set(2200 * semis(pitch) * (0.8 + 0.4 * wob), 2.2);
5608
+ f.run(r() * 2 - 1);
5609
+ const grit = 0.75 + 0.25 * Math.sin(TAU * (28 + 30 * wob) * t);
5610
+ buf[i] = f.bp * 1.6 * grit * wob * Math.pow(Math.sin(Math.PI * p), 0.5);
5611
+ }
5612
+ return { buf, send: 0.15, duck: 0 };
5613
+ }
5614
+ function tone(dur, hz, r) {
5615
+ const buf = alloc(dur);
5616
+ const n = buf.length;
5617
+ const rel = dur * 0.3;
5618
+ const ph0 = r() * TAU;
5619
+ for (let i = 0; i < n; i++) {
5620
+ const t = i / SR;
5621
+ const vib = 1 + 4e-3 * Math.sin(TAU * 5.5 * t);
5622
+ const env = Math.min(1, t / 0.015) * (t > dur - rel ? (dur - t) / rel : 1);
5623
+ buf[i] = (Math.sin(ph0 + TAU * hz * vib * t) + Math.sin(3 * (ph0 + TAU * hz * vib * t)) / 9) * env * 0.8;
5624
+ }
5625
+ return { buf, send: 0.35, duck: 0 };
5626
+ }
5627
+ function pad(dur, hzs, level, bright, r, attack, release) {
5628
+ const buf = alloc(dur);
5629
+ const n = buf.length;
5630
+ const f = new Svf();
5631
+ const voices = hzs.flatMap((hz) => [-1, 1].map((d) => ({ hz: hz * semis(d * (5 + 3 * r()) / 100), ph: r() })));
5632
+ const subHz = Math.min(...hzs) / 2;
5633
+ const lfo = r() * TAU;
5634
+ for (let i = 0; i < n; i++) {
5635
+ const t = i / SR;
5636
+ let x = 0;
5637
+ for (const v of voices) {
5638
+ v.ph += v.hz / SR;
5639
+ if (v.ph >= 1) v.ph -= 1;
5640
+ x += v.ph * 2 - 1;
5641
+ }
5642
+ x /= voices.length;
5643
+ x += Math.sin(TAU * subHz * t) * 0.35;
5644
+ f.set(380 * Math.pow(2, 1.6 * bright) * (1 + 0.2 * Math.sin(lfo + TAU * 0.09 * t)), 0.7);
5645
+ f.run(x);
5646
+ const env = Math.min(1, t / attack) * Math.min(1, (dur - t) / release);
5647
+ buf[i] = f.lp * level * env;
5648
+ }
5649
+ return buf;
5650
+ }
5651
+ function reverb(inL, inR, wet) {
5652
+ const n = inL.length;
5653
+ const mk = (delays, aps) => {
5654
+ const combs = delays.map((d) => ({ buf: new Float32Array(d), i: 0, lp: 0 }));
5655
+ const all = aps.map((d) => ({ buf: new Float32Array(d), i: 0 }));
5656
+ return (x) => {
5657
+ let y = 0;
5658
+ for (const c of combs) {
5659
+ const out = c.buf[c.i];
5660
+ c.lp = out * 0.8 + c.lp * 0.2;
5661
+ c.buf[c.i] = x + c.lp * 0.78;
5662
+ c.i = (c.i + 1) % c.buf.length;
5663
+ y += out;
5664
+ }
5665
+ y /= combs.length;
5666
+ for (const a of all) {
5667
+ const b = a.buf[a.i];
5668
+ const v = y + b * 0.5;
5669
+ a.buf[a.i] = v;
5670
+ a.i = (a.i + 1) % a.buf.length;
5671
+ y = b - 0.5 * v;
5672
+ }
5673
+ return y;
5674
+ };
5675
+ };
5676
+ const L = mk([1215, 1293, 1390, 1476], [605, 480]);
5677
+ const R = mk([1238, 1316, 1413, 1499], [628, 503]);
5678
+ const oL = new Float32Array(n), oR = new Float32Array(n);
5679
+ for (let i = 0; i < n; i++) {
5680
+ const m = (inL[i] + inR[i]) * 0.5;
5681
+ oL[i] = L(m) * wet;
5682
+ oR[i] = R(m) * wet;
5683
+ }
5684
+ return [oL, oR];
5685
+ }
5686
+ function normalizeSoundSpec(raw, duration) {
5687
+ const warnings = [];
5688
+ const spec2 = {};
5689
+ if (raw == null || raw === false) return { spec: spec2, warnings };
5690
+ const obj = Array.isArray(raw) ? { cues: raw } : typeof raw === "object" ? raw : {};
5691
+ const cues = [];
5692
+ for (const c of Array.isArray(obj.cues) ? obj.cues : []) {
5693
+ if (!c || typeof c !== "object") continue;
5694
+ const q2 = c;
5695
+ const kind = String(q2.kind ?? q2.sound ?? "");
5696
+ if (!SOUND_KINDS.includes(kind)) {
5697
+ warnings.push(`sound: "${kind}" is not a sound (${SOUND_KINDS.join(", ")})`);
5698
+ continue;
5699
+ }
5700
+ const at = Number(q2.at);
5701
+ if (!Number.isFinite(at)) {
5702
+ warnings.push(`sound: a ${kind} cue has no time (at)`);
5703
+ continue;
5704
+ }
5705
+ const cue = { at: clamp(at < 0 ? duration + at : at, 0, duration), kind };
5706
+ if (Number.isFinite(Number(q2.dur)) && Number(q2.dur) > 0) cue.dur = clamp(Number(q2.dur), 0.02, 60);
5707
+ if (Number.isFinite(Number(q2.level))) cue.level = clamp(Number(q2.level), 0, 2);
5708
+ if (Number.isFinite(Number(q2.pitch))) cue.pitch = clamp(Number(q2.pitch), -36, 36);
5709
+ if (Number.isFinite(Number(q2.pan))) cue.pan = clamp(Number(q2.pan), -1, 1);
5710
+ if (Number.isFinite(Number(q2.seed))) cue.seed = Number(q2.seed);
5711
+ if (typeof q2.note === "string" && noteHz(q2.note) !== null) cue.note = q2.note;
5712
+ cues.push(cue);
5713
+ }
5714
+ if (cues.length) spec2.cues = cues;
5715
+ if (obj.bed !== void 0) {
5716
+ const b = obj.bed;
5717
+ if (b === true || typeof b === "string") spec2.bed = b;
5718
+ else if (b && typeof b === "object") {
5719
+ const o = b;
5720
+ const bed = {};
5721
+ if (typeof o.key === "string") bed.key = o.key;
5722
+ if (o.mode === "major" || o.mode === "minor") bed.mode = o.mode;
5723
+ if (Number.isFinite(Number(o.from))) bed.from = Math.max(0, Number(o.from));
5724
+ if (Number.isFinite(Number(o.to))) bed.to = Number(o.to) < 0 ? duration + Number(o.to) : Number(o.to);
5725
+ if (Number.isFinite(Number(o.level))) bed.level = clamp(Number(o.level), 0, 1);
5726
+ if (Number.isFinite(Number(o.tone))) bed.tone = clamp(Number(o.tone), 0, 1);
5727
+ spec2.bed = bed;
5728
+ } else spec2.bed = false;
5729
+ }
5730
+ if (obj.auto !== void 0) spec2.auto = Boolean(obj.auto);
5731
+ if (Number.isFinite(Number(obj.level))) spec2.level = clamp(Number(obj.level), 0, 2);
5732
+ if (Number.isFinite(Number(obj.room))) spec2.room = clamp(Number(obj.room), 0, 1);
5733
+ return { spec: spec2, warnings };
5734
+ }
5735
+ function bedOf(b, duration) {
5736
+ if (!b) return null;
5737
+ const base = b === true ? {} : typeof b === "string" ? { ...parseKeyToBed(b) } : { ...b };
5738
+ const k = parseKey(base.key);
5739
+ return { key: base.key ?? "A", mode: base.mode ?? k.mode, from: clamp(base.from ?? 0, 0, duration), to: clamp(base.to ?? duration, 0, duration), level: base.level ?? 0.5, tone: base.tone ?? 0.4 };
5740
+ }
5741
+ function parseKeyToBed(key) {
5742
+ const k = parseKey(key);
5743
+ const name = /^\s*([A-Ga-g][#b♯♭]?)/.exec(key)?.[1] ?? "A";
5744
+ return { key: name, mode: k.mode };
5745
+ }
5746
+ function cuesFromMarks(marks, duration) {
5747
+ const list = marks.filter((m) => Number.isFinite(m[1]) && m[1] >= -0.01 && m[1] <= duration + 0.01).map((m) => ({ kind: m[0], at: Math.max(0, Math.round(m[1] * 1e3) / 1e3, 0), a: m[2], b: m[3] })).sort((x, y) => x.at - y.at);
5748
+ const RANK = { stamp: 9, shake: 8, burst: 7, flash: 7, enter: 6, reveal: 6, typed: 6, write: 6, ink: 6, press: 6, underline: 5, strike: 5, count: 5, bar: 5, pop: 4, bump: 3, letter: 2, word: 2, fade: 1, exit: 1, beat: 0 };
5749
+ const moments = [];
5750
+ for (const m of list) {
5751
+ if (m.kind === "beat") continue;
5752
+ const last = moments[moments.length - 1];
5753
+ if (last && Math.abs(last.at - m.at) < 0.012) {
5754
+ last.n++;
5755
+ if ((RANK[m.kind] ?? 0) > (RANK[last.kind] ?? 0)) Object.assign(last, { kind: m.kind, a: m.a, b: m.b });
5756
+ continue;
5757
+ }
5758
+ moments.push({ ...m, n: 1 });
5759
+ }
5760
+ const cues = [];
5761
+ const unit = /* @__PURE__ */ new Set(["letter", "word", "bump", "pop", "bar", "enter", "fade"]);
5762
+ let i = 0;
5763
+ while (i < moments.length) {
5764
+ const m = moments[i];
5765
+ let j = i;
5766
+ if (unit.has(m.kind)) while (j + 1 < moments.length && unit.has(moments[j + 1].kind) && moments[j + 1].at - moments[j].at < 0.26 && moments[j + 1].at - m.at < 0.9) j++;
5767
+ if (j - i + 1 >= 4) {
5768
+ const n = j - i + 1;
5769
+ cues.push(...cueFor(m, duration));
5770
+ for (let k = i + 1; k <= j; k++) cues.push({ at: moments[k].at, kind: "tick", level: 0.75 - 0.35 * (k - i) / n, pitch: Math.min(10, (k - i) * (9 / n)), pan: ((k - i) / n - 0.5) * 0.5 });
5771
+ i = j + 1;
5772
+ continue;
5773
+ }
5774
+ cues.push(...cueFor(m, duration));
5775
+ i++;
5776
+ }
5777
+ const strong = cues.filter((c) => c.kind !== "swell" && c.kind !== "tick");
5778
+ const out = cues.filter((c) => c.kind !== "swell" ? true : !strong.some((s) => Math.abs(s.at - c.at) < 0.15));
5779
+ if (out.length > 60) {
5780
+ const ticks = out.filter((c) => c.kind === "tick");
5781
+ const keepEvery = Math.ceil(ticks.length / Math.max(1, 60 - (out.length - ticks.length)));
5782
+ let k = 0;
5783
+ return out.filter((c) => c.kind !== "tick" || k++ % keepEvery === 0);
5784
+ }
5785
+ return out;
5786
+ }
5787
+ function cueFor(m, duration) {
5788
+ const at = m.at;
5789
+ switch (m.kind) {
5790
+ case "stamp":
5791
+ return [{ at, kind: "stamp" }];
5792
+ case "shake":
5793
+ return [{ at, kind: "hit", level: clamp(0.6 + (m.a ?? 18) / 60, 0.6, 1.1) }];
5794
+ case "burst":
5795
+ return [{ at, kind: "pop", level: 0.9 }, { at, kind: "shimmer", level: 0.8, dur: 1 }];
5796
+ case "flash":
5797
+ return [{ at, kind: "zap", level: 0.7 }];
5798
+ case "enter": {
5799
+ const dist = Math.hypot(m.a ?? 0, m.b ?? 40);
5800
+ return dist < 12 ? [{ at, kind: "pop", level: 0.7 }] : [{ at, kind: "whoosh", level: clamp(0.45 + dist / 360, 0.45, 1), dur: clamp(0.3 + dist / 900, 0.3, 0.6), pan: (m.a ?? 0) > 0 ? 0.25 : (m.a ?? 0) < 0 ? -0.25 : 0 }];
5801
+ }
5802
+ case "reveal":
5803
+ return [{ at, kind: "whoosh", pitch: 4, dur: 0.45, level: 0.8 }];
5804
+ case "typed":
5805
+ return [{ at, kind: "type", dur: clamp(m.a ?? 0.8, 0.15, 6) }];
5806
+ case "write":
5807
+ case "ink":
5808
+ return [{ at, kind: "scratch", dur: clamp(m.a ?? 0.8, 0.15, 6), pitch: m.b ? 3 : 0 }];
5809
+ case "press":
5810
+ return [{ at, kind: "click" }];
5811
+ case "underline":
5812
+ return [{ at, kind: "whoosh", pitch: 12, dur: 0.3, level: 0.6 }];
5813
+ case "strike":
5814
+ return [{ at, kind: "whoosh", pitch: 2, dur: 0.3, level: 0.7 }, { at: at + 0.12, kind: "hit", level: 0.5 }];
5815
+ case "count":
5816
+ return [{ at, kind: "type", dur: clamp(m.a ?? 0.8, 0.2, 4), level: 0.6, pitch: 4 }];
5817
+ case "bar":
5818
+ return [{ at, kind: "pop", level: 0.7, pitch: 3 }];
5819
+ case "pop":
5820
+ return [{ at, kind: "pop", level: m.n > 1 ? 0.9 : 0.8 }];
5821
+ case "bump":
5822
+ return [{ at, kind: "tick", level: 0.8 }];
5823
+ case "letter":
5824
+ return [{ at, kind: "tick", level: 0.6 }];
5825
+ case "word":
5826
+ return [{ at, kind: "tick", level: 0.7, pitch: -4 }];
5827
+ case "fade":
5828
+ return [{ at, kind: "swell", level: 0.7 }];
5829
+ case "exit":
5830
+ return at > duration - 0.05 ? [] : [{ at, kind: "whoosh", level: 0.45, pitch: -5, dur: 0.35 }];
5831
+ default:
5832
+ return [];
5833
+ }
5834
+ }
5835
+ function voiceFor(cue, bed, index) {
5836
+ const seed = cue.seed ?? index * 7.13 + cue.at * 3.7 + cue.kind.length;
5837
+ const r = rng(seed);
5838
+ const pitch = cue.pitch ?? 0;
5839
+ const dur = cue.dur ?? DEFAULT_DUR[cue.kind] ?? 0.5;
5840
+ switch (cue.kind) {
5841
+ case "whoosh":
5842
+ return whoosh(dur, pitch, r);
5843
+ case "hit":
5844
+ return hit(pitch, r, false);
5845
+ case "stamp":
5846
+ return hit(pitch, r, true);
5847
+ case "pop":
5848
+ return pop(pitch, r);
5849
+ case "tick":
5850
+ return tick(pitch, r);
5851
+ case "click":
5852
+ return click(pitch, r);
5853
+ case "riser":
5854
+ return riser(dur, pitch, r, false);
5855
+ case "fall":
5856
+ return riser(dur, pitch, r, true);
5857
+ case "sub":
5858
+ return sub(pitch);
5859
+ case "shimmer":
5860
+ return shimmer(dur, pitch, r);
5861
+ case "ding":
5862
+ return ding((cue.note ? noteHz(cue.note) : null) ?? 880 * semis(pitch));
5863
+ case "swell":
5864
+ return swell(dur, pitch, r);
5865
+ case "zap":
5866
+ return zap(dur, pitch, r);
5867
+ case "type":
5868
+ return typing(dur, pitch, r);
5869
+ case "scratch":
5870
+ return scratch(dur, pitch, r);
5871
+ case "tone":
5872
+ return tone(dur, (cue.note ? noteHz(cue.note) : null) ?? 440 * semis(pitch), r);
5873
+ case "chord": {
5874
+ const k = parseKey(bed?.key ? `${bed.key}${bed.mode === "major" ? "" : "m"}` : "Am");
5875
+ const centre = -12 + pitch;
5876
+ const hz = cue.note ? [noteHz(cue.note), noteHz(cue.note) * semis(bed?.mode === "major" ? 4 : 3), noteHz(cue.note) * semis(7)] : chordTones(k.root, k.mode, 1, centre).map((s) => 440 * semis(s));
5877
+ return { buf: pad(dur, hz, 1.1, bed?.tone ?? 0.5, r, Math.min(0.06, dur * 0.2), Math.max(0.05, dur * 0.3)), send: 0.4, duck: 0 };
5878
+ }
5879
+ }
5880
+ }
5881
+ function renderSound(spec2, duration, opts = {}) {
5882
+ const sr = SR;
5883
+ void opts;
5884
+ const warnings = [];
5885
+ const n = Math.max(1, Math.round(duration * sr));
5886
+ const L = new Float32Array(n), R = new Float32Array(n), sL = new Float32Array(n), sR = new Float32Array(n);
5887
+ const cues = (spec2.cues ?? []).filter((c) => c.at >= 0 && c.at <= duration).sort((a, b) => a.at - b.at);
5888
+ const ducks = [];
5889
+ cues.forEach((cue, idx) => {
5890
+ const v = voiceFor(cue, bedOf(spec2.bed, duration), idx);
5891
+ const level = BASE_LEVEL[cue.kind] * (cue.level ?? 1);
5892
+ const pan = clamp(cue.pan ?? 0, -1, 1);
5893
+ const gl = Math.cos((pan + 1) * Math.PI / 4) * Math.SQRT2 * 0.78;
5894
+ const gr = Math.sin((pan + 1) * Math.PI / 4) * Math.SQRT2 * 0.78;
5895
+ const start = Math.round(cue.at * sr);
5896
+ for (let i = 0; i < v.buf.length; i++) {
5897
+ const j = start + i;
5898
+ if (j >= n) break;
5899
+ const x = v.buf[i] * level;
5900
+ L[j] += x * gl;
5901
+ R[j] += x * gr;
5902
+ sL[j] += x * gl * v.send;
5903
+ sR[j] += x * gr * v.send;
5904
+ }
5905
+ if (v.duck > 0) ducks.push({ at: cue.at, depth: v.duck * clamp(cue.level ?? 1, 0, 1.2) });
5906
+ });
5907
+ const bed = bedOf(spec2.bed, duration);
5908
+ if (bed && bed.to > bed.from + 0.2) {
5909
+ const r = rng(11.7);
5910
+ const k = parseKey(`${bed.key}${bed.mode === "major" ? "" : "m"}`);
5911
+ const len = bed.to - bed.from;
5912
+ const two = len > 5;
5913
+ const split = two ? bed.from + len * 0.55 : bed.to;
5914
+ const degreeB = bed.mode === "major" ? 4 : 6;
5915
+ const lvl = 0.26 * bed.level;
5916
+ const chords = [{ from: bed.from, to: two ? split + 0.6 : bed.to, tones: chordTones(k.root, k.mode, 1, -15) }];
5917
+ if (two) chords.push({ from: split - 0.2, to: bed.to, tones: chordTones(k.root, k.mode, degreeB, -15) });
5918
+ chords.forEach((ch, ci) => {
5919
+ const hz = ch.tones.map((s) => 440 * semis(s));
5920
+ hz.push(440 * semis(ch.tones[0] + 12));
5921
+ const d = ch.to - ch.from;
5922
+ const attack = ci === 0 ? Math.min(1.4, d * 0.35) : 0.7;
5923
+ const release = Math.min(1.6, d * 0.4);
5924
+ const mL = pad(d, hz, lvl, bed.tone, rng(3.3 + ci), attack, release);
5925
+ const mR = pad(d, hz, lvl, bed.tone, rng(9.1 + ci), attack, release);
5926
+ const start = Math.round(ch.from * sr);
5927
+ for (let i = 0; i < mL.length; i++) {
5928
+ const j = start + i;
5929
+ if (j >= n) break;
5930
+ const t = j / sr;
5931
+ let g = 1;
5932
+ for (const dk of ducks) {
5933
+ const dt = t - dk.at;
5934
+ if (dt >= 0 && dt < 1.2) g -= 0.4 * dk.depth * (dt < 6e-3 ? dt / 6e-3 : Math.exp(-(dt - 6e-3) / 0.33));
5935
+ }
5936
+ g = Math.max(0.35, g);
5937
+ L[j] += mL[i] * g;
5938
+ R[j] += mR[i] * g;
5939
+ sL[j] += mL[i] * g * 0.3;
5940
+ sR[j] += mR[i] * g * 0.3;
5941
+ }
5942
+ });
5943
+ void r;
5944
+ }
5945
+ const room = spec2.room ?? 0.25;
5946
+ if (room > 0) {
5947
+ const [wL, wR] = reverb(sL, sR, 1.4 * room);
5948
+ for (let i = 0; i < n; i++) {
5949
+ L[i] += wL[i];
5950
+ R[i] += wR[i];
5951
+ }
5952
+ }
5953
+ const master = 0.85 * (spec2.level ?? 1);
5954
+ const rel = Math.exp(-1 / (0.12 * sr));
5955
+ const ceiling = 0.95;
5956
+ let env = 0, peak = 0;
5957
+ const fadeIn = Math.round(0.02 * sr), fadeOut = Math.round(0.06 * sr);
5958
+ for (let i = 0; i < n; i++) {
5959
+ let l = L[i] * master, r22 = R[i] * master;
5960
+ const edge = (i < fadeIn ? i / fadeIn : 1) * (i > n - fadeOut ? (n - i) / fadeOut : 1);
5961
+ l *= edge;
5962
+ r22 *= edge;
5963
+ const a = Math.max(Math.abs(l), Math.abs(r22));
5964
+ env = Math.max(a, env * rel);
5965
+ const g = env > ceiling ? ceiling / env : 1;
5966
+ l = knee(l * g);
5967
+ r22 = knee(r22 * g);
5968
+ L[i] = l;
5969
+ R[i] = r22;
5970
+ peak = Math.max(peak, Math.abs(l), Math.abs(r22));
5971
+ }
5972
+ return { left: L, right: R, sampleRate: sr, cues, peak, bed, warnings };
5973
+ }
5974
+ function knee(x) {
5975
+ const a = Math.abs(x);
5976
+ if (a <= 0.8) return x;
5977
+ return Math.sign(x) * (0.8 + 0.2 * Math.tanh((a - 0.8) / 0.2));
5978
+ }
5979
+ function wavEncode(left, right, sampleRate) {
5980
+ const n = Math.min(left.length, right.length);
5981
+ const data = new Uint8Array(44 + n * 4);
5982
+ const dv = new DataView(data.buffer);
5983
+ const str2 = (o2, s) => {
5984
+ for (let i = 0; i < s.length; i++) data[o2 + i] = s.charCodeAt(i);
5985
+ };
5986
+ str2(0, "RIFF");
5987
+ dv.setUint32(4, 36 + n * 4, true);
5988
+ str2(8, "WAVE");
5989
+ str2(12, "fmt ");
5990
+ dv.setUint32(16, 16, true);
5991
+ dv.setUint16(20, 1, true);
5992
+ dv.setUint16(22, 2, true);
5993
+ dv.setUint32(24, sampleRate, true);
5994
+ dv.setUint32(28, sampleRate * 4, true);
5995
+ dv.setUint16(32, 4, true);
5996
+ dv.setUint16(34, 16, true);
5997
+ str2(36, "data");
5998
+ dv.setUint32(40, n * 4, true);
5999
+ let o = 44;
6000
+ for (let i = 0; i < n; i++) {
6001
+ dv.setInt16(o, Math.round(clamp(left[i], -1, 1) * 32767), true);
6002
+ dv.setInt16(o + 2, Math.round(clamp(right[i], -1, 1) * 32767), true);
6003
+ o += 4;
6004
+ }
6005
+ return data;
6006
+ }
6007
+ function describeCues(cues) {
6008
+ return cues.map((c) => ` ${c.at.toFixed(2).padStart(6)} s ${c.kind.padEnd(8)}${c.dur ? ` ${c.dur.toFixed(2)} s` : ""}${c.level !== void 0 && c.level !== 1 ? ` \xD7${c.level.toFixed(2)}` : ""}${c.pitch ? ` ${c.pitch > 0 ? "+" : ""}${c.pitch} st` : ""}${c.note ? ` ${c.note}` : ""}${c.pan ? ` pan ${c.pan > 0 ? "R" : "L"}${Math.abs(c.pan).toFixed(1)}` : ""}`).join("\n");
6009
+ }
6010
+
5288
6011
  // src/node/render.ts
5289
6012
  import { spawn } from "node:child_process";
5290
- import { mkdirSync, statSync as statSync2, writeFileSync } from "node:fs";
5291
- import { availableParallelism } from "node:os";
5292
- import { dirname as dirname2, join as join5, resolve as resolve2 } from "node:path";
6013
+ import { mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
6014
+ import { availableParallelism, tmpdir } from "node:os";
6015
+ import { dirname, join as join2, resolve } from "node:path";
5293
6016
  import { Resvg, renderAsync } from "@resvg/resvg-js";
5294
6017
 
5295
6018
  // src/node/outline.ts
@@ -5395,7 +6118,7 @@ function pathDataLength(d) {
5395
6118
  const tokens = d.match(/[MmLlHhVvCcSsQqTtAaZz]|-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/g) ?? [];
5396
6119
  let i = 0, cmd = "", x = 0, y = 0, sx = 0, sy = 0, len = 0;
5397
6120
  let cx = 0, cy = 0, qx = 0, qy = 0, prev = "";
5398
- const num2 = () => Number(tokens[i++]);
6121
+ const num = () => Number(tokens[i++]);
5399
6122
  const seg = (nx, ny) => {
5400
6123
  len += Math.hypot(nx - x, ny - y);
5401
6124
  x = nx;
@@ -5455,23 +6178,23 @@ function pathDataLength(d) {
5455
6178
  const ox = rel ? x : 0, oy = rel ? y : 0;
5456
6179
  switch (cmd.toUpperCase()) {
5457
6180
  case "M":
5458
- x = num2() + ox;
5459
- y = num2() + oy;
6181
+ x = num() + ox;
6182
+ y = num() + oy;
5460
6183
  sx = x;
5461
6184
  sy = y;
5462
6185
  cmd = rel ? "l" : "L";
5463
6186
  break;
5464
6187
  case "L":
5465
- seg(num2() + ox, num2() + oy);
6188
+ seg(num() + ox, num() + oy);
5466
6189
  break;
5467
6190
  case "H":
5468
- seg(num2() + ox, y);
6191
+ seg(num() + ox, y);
5469
6192
  break;
5470
6193
  case "V":
5471
- seg(x, num2() + oy);
6194
+ seg(x, num() + oy);
5472
6195
  break;
5473
6196
  case "C": {
5474
- const a = num2() + ox, b = num2() + oy, c = num2() + ox, e = num2() + oy, f = num2() + ox, g = num2() + oy;
6197
+ const a = num() + ox, b = num() + oy, c = num() + ox, e = num() + oy, f = num() + ox, g = num() + oy;
5475
6198
  cx = c;
5476
6199
  cy = e;
5477
6200
  cubic(a, b, c, e, f, g);
@@ -5479,14 +6202,14 @@ function pathDataLength(d) {
5479
6202
  }
5480
6203
  case "S": {
5481
6204
  const r1x = /[CS]/i.test(prev) ? 2 * x - cx : x, r1y = /[CS]/i.test(prev) ? 2 * y - cy : y;
5482
- const c = num2() + ox, e = num2() + oy, f = num2() + ox, g = num2() + oy;
6205
+ const c = num() + ox, e = num() + oy, f = num() + ox, g = num() + oy;
5483
6206
  cx = c;
5484
6207
  cy = e;
5485
6208
  cubic(r1x, r1y, c, e, f, g);
5486
6209
  break;
5487
6210
  }
5488
6211
  case "Q": {
5489
- const a = num2() + ox, b = num2() + oy, f = num2() + ox, g = num2() + oy;
6212
+ const a = num() + ox, b = num() + oy, f = num() + ox, g = num() + oy;
5490
6213
  qx = a;
5491
6214
  qy = b;
5492
6215
  quad(a, b, f, g);
@@ -5496,12 +6219,12 @@ function pathDataLength(d) {
5496
6219
  const a = /[QT]/i.test(prev) ? 2 * x - qx : x, b = /[QT]/i.test(prev) ? 2 * y - qy : y;
5497
6220
  qx = a;
5498
6221
  qy = b;
5499
- quad(a, b, num2() + ox, num2() + oy);
6222
+ quad(a, b, num() + ox, num() + oy);
5500
6223
  break;
5501
6224
  }
5502
6225
  case "A": {
5503
- const rx = num2(), ry = num2(), phi = num2(), large = num2(), sweep = num2();
5504
- arc(rx, ry, phi, large ? 1 : 0, sweep ? 1 : 0, num2() + ox, num2() + oy);
6226
+ const rx = num(), ry = num(), phi = num(), large = num(), sweep = num();
6227
+ arc(rx, ry, phi, large ? 1 : 0, sweep ? 1 : 0, num() + ox, num() + oy);
5505
6228
  break;
5506
6229
  }
5507
6230
  case "Z":
@@ -5621,6 +6344,15 @@ function frameAt(s, t, background, timeoutMs) {
5621
6344
  const doc = frameDocument(s.built.program, frame, s.width, s.height, { background });
5622
6345
  return { doc, fonts: fontFilesForDocument(doc), frame };
5623
6346
  }
6347
+ function framePixels(s, t) {
6348
+ const f = frameAt(s, t, null);
6349
+ try {
6350
+ const img = new Resvg(f.doc, resvgOptions(f.fonts, s.width)).render();
6351
+ return { data: img.pixels, width: img.width, height: img.height };
6352
+ } catch (err) {
6353
+ throw parseError(err);
6354
+ }
6355
+ }
5624
6356
  function parseError(err) {
5625
6357
  return new MotionError(`the frame's SVG does not parse (${String(err?.message ?? err).slice(0, 200)}) \u2014 escape text with $.esc() and close every tag`);
5626
6358
  }
@@ -5643,8 +6375,8 @@ async function renderStill(item, opts = {}) {
5643
6375
  }
5644
6376
  const result = { format, data, width: s.width, height: s.height, t, warnings: s.warnings };
5645
6377
  if (opts.out) {
5646
- const file = resolve2(opts.out);
5647
- mkdirSync(dirname2(file), { recursive: true });
6378
+ const file = resolve(opts.out);
6379
+ mkdirSync(dirname(file), { recursive: true });
5648
6380
  writeFileSync(file, data);
5649
6381
  result.file = file;
5650
6382
  }
@@ -5688,6 +6420,54 @@ async function renderSheet(item, opts = {}) {
5688
6420
  return { png, times, warnings: s.warnings, width: W, height: H };
5689
6421
  }
5690
6422
  var EXT = { mov: ".mov", webm: ".webm", mp4: ".mp4", hevc: ".mov", gif: ".gif", png: "" };
6423
+ function designSound(s, opts, duration = s.built.duration) {
6424
+ const warnings = [];
6425
+ const own = s.runner.sound();
6426
+ if (own.error) warnings.push(own.error);
6427
+ const { spec: spec2, warnings: w } = normalizeSoundSpec(own.spec, duration);
6428
+ warnings.push(...w);
6429
+ if (typeof opts.sound === "object" && opts.sound) {
6430
+ const o = normalizeSoundSpec(opts.sound, duration);
6431
+ warnings.push(...o.warnings);
6432
+ if (o.spec.cues) spec2.cues = [...spec2.cues ?? [], ...o.spec.cues];
6433
+ if (o.spec.bed !== void 0) spec2.bed = o.spec.bed;
6434
+ if (o.spec.auto !== void 0) spec2.auto = o.spec.auto;
6435
+ if (o.spec.level !== void 0) spec2.level = o.spec.level;
6436
+ if (o.spec.room !== void 0) spec2.room = o.spec.room;
6437
+ }
6438
+ if (opts.bed !== void 0) spec2.bed = opts.bed;
6439
+ if (spec2.auto ?? !spec2.cues?.length) {
6440
+ const tr = s.runner.trace(s.fps);
6441
+ if (tr.error) warnings.push(tr.error);
6442
+ spec2.cues = [...spec2.cues ?? [], ...cuesFromMarks(tr.marks, duration)].sort((a, b) => a.at - b.at);
6443
+ }
6444
+ if (!spec2.cues?.length && !spec2.bed) warnings.push("sound: nothing in this graphic moves in a way the sound design reads, so its track is silent \u2014 add a bed (--bed Dm) or a sound($) to the program");
6445
+ return { spec: spec2, warnings };
6446
+ }
6447
+ async function renderSoundFile(item, opts = {}) {
6448
+ const s = await openSession(item, opts);
6449
+ const total = Math.max(1, Math.round(s.built.duration * s.fps));
6450
+ const duration = total / s.fps;
6451
+ const { spec: spec2, warnings } = designSound(s, { sound: opts.sound ?? true, bed: opts.bed }, duration);
6452
+ const r = renderSound(spec2, duration);
6453
+ const file = resolve(opts.out ?? join2(opts.outDir ?? ".", `${item.id}.wav`));
6454
+ mkdirSync(dirname(file), { recursive: true });
6455
+ writeFileSync(file, wavEncode(r.left, r.right, r.sampleRate));
6456
+ return { cues: r.cues, bed: r.bed, peak: r.peak, duration, file, warnings: [...s.warnings, ...warnings, ...r.warnings] };
6457
+ }
6458
+ function audioArgs(format, encoders) {
6459
+ switch (format) {
6460
+ case "mov":
6461
+ return ["-c:a", "pcm_s16le"];
6462
+ case "mp4":
6463
+ case "hevc":
6464
+ return ["-c:a", "aac", "-b:a", "192k"];
6465
+ case "webm":
6466
+ return encoders.has("libopus") ? ["-c:a", "libopus", "-b:a", "128k"] : encoders.has("libvorbis") ? ["-c:a", "libvorbis"] : [];
6467
+ default:
6468
+ return [];
6469
+ }
6470
+ }
5691
6471
  function formatFromPath(path) {
5692
6472
  const ext = /\.([a-z0-9]+)$/i.exec(path)?.[1]?.toLowerCase();
5693
6473
  if (!ext) return "png";
@@ -5696,7 +6476,14 @@ function formatFromPath(path) {
5696
6476
  function defaultOutput(item, format, s) {
5697
6477
  return `${item.id}-${s.width}x${s.height}${format === "hevc" ? "-hevc" : ""}${EXT[format]}`;
5698
6478
  }
5699
- function encoderArgs(format, s, alpha, encoders) {
6479
+ function encoderArgs(format, s, alpha, encoders, audio = null) {
6480
+ const video = videoArgs(format, s, alpha, encoders);
6481
+ const codec = audio ? audioArgs(format, encoders) : [];
6482
+ if (!audio || !codec.length) return video;
6483
+ const i = video.indexOf("-i") + 2;
6484
+ return [...video.slice(0, i), "-i", audio, "-map", "0:v:0", "-map", "1:a:0", ...video.slice(i), ...codec];
6485
+ }
6486
+ function videoArgs(format, s, alpha, encoders) {
5700
6487
  const input = ["-hide_banner", "-loglevel", "error", "-y", "-f", "rawvideo", "-pix_fmt", "rgba", "-s", `${s.width}x${s.height}`, "-r", String(s.fps), "-i", "-"];
5701
6488
  switch (format) {
5702
6489
  case "mov":
@@ -5730,17 +6517,44 @@ async function renderVideo(item, opts = {}) {
5730
6517
  if (item.placement === "overlay" && format === "mp4") s.warnings.push("MP4 has no alpha: the overlay is drawn on black \u2014 pass --bg, or use mov/webm/hevc/png to keep it transparent");
5731
6518
  }
5732
6519
  const alpha = !bg;
5733
- const file = resolve2(opts.out ?? join5(opts.outDir ?? ".", defaultOutput(item, format, s)));
6520
+ const file = resolve(opts.out ?? join2(opts.outDir ?? ".", defaultOutput(item, format, s)));
5734
6521
  if (format === "png") mkdirSync(file, { recursive: true });
5735
- else mkdirSync(dirname2(file), { recursive: true });
6522
+ else mkdirSync(dirname(file), { recursive: true });
5736
6523
  const total = Math.max(1, Math.round(s.built.duration * s.fps));
5737
6524
  const concurrency = Math.max(1, Math.min(16, opts.concurrency ?? availableParallelism()));
5738
6525
  const holdAfter = s.built.program.holdAfter;
6526
+ let audio = null;
6527
+ let soundTmp = null;
6528
+ let sound;
6529
+ if (opts.sound || opts.bed !== void 0 || opts.soundOut) {
6530
+ const duration = total / s.fps;
6531
+ const { spec: spec2, warnings } = designSound(s, { sound: opts.sound ?? true, bed: opts.bed }, duration);
6532
+ s.warnings.push(...warnings);
6533
+ const r = renderSound(spec2, duration);
6534
+ const wav = wavEncode(r.left, r.right, r.sampleRate);
6535
+ sound = { cues: r.cues, bed: r.bed, peak: r.peak, duration, warnings: r.warnings };
6536
+ if (opts.soundOut) {
6537
+ sound.file = resolve(opts.soundOut);
6538
+ mkdirSync(dirname(sound.file), { recursive: true });
6539
+ writeFileSync(sound.file, wav);
6540
+ }
6541
+ if (!ff || format === "png" || format === "gif") s.warnings.push(`${format} carries no sound${sound.file ? "" : " \u2014 pass --sound-out file.wav to keep the track"}`);
6542
+ else if (!audioArgs(format, ff.encoders).length) s.warnings.push(`this ffmpeg has no audio encoder for ${format} (libopus or libvorbis) \u2014 the video has no sound${sound.file ? "" : "; pass --sound-out file.wav to keep the track"}`);
6543
+ else {
6544
+ soundTmp = mkdtempSync(join2(tmpdir(), "volito-sound-"));
6545
+ audio = join2(soundTmp, "sound.wav");
6546
+ writeFileSync(audio, wav);
6547
+ }
6548
+ }
6549
+ const cleanSound = () => {
6550
+ if (soundTmp) rmSync(soundTmp, { recursive: true, force: true });
6551
+ soundTmp = null;
6552
+ };
5739
6553
  let enc = null;
5740
6554
  let encError = "";
5741
6555
  let encDone = null;
5742
6556
  if (ff) {
5743
- enc = spawn(ff.path, [...encoderArgs(format, s, alpha, ff.encoders), file], { stdio: ["pipe", "ignore", "pipe"] });
6557
+ enc = spawn(ff.path, [...encoderArgs(format, s, alpha, ff.encoders, audio), file], { stdio: ["pipe", "ignore", "pipe"] });
5744
6558
  enc.stderr.on("data", (d) => {
5745
6559
  encError = (encError + d.toString()).slice(-4e3);
5746
6560
  });
@@ -5771,7 +6585,7 @@ async function renderVideo(item, opts = {}) {
5771
6585
  const flush = async (keep) => {
5772
6586
  while (queue.length > keep) {
5773
6587
  const buf = await queue.shift();
5774
- if (format === "png") writeFileSync(join5(file, `frame_${String(done + 1).padStart(5, "0")}.png`), buf);
6588
+ if (format === "png") writeFileSync(join2(file, `frame_${String(done + 1).padStart(5, "0")}.png`), buf);
5775
6589
  else await write(buf);
5776
6590
  done++;
5777
6591
  opts.onProgress?.(done, total);
@@ -5801,19 +6615,21 @@ async function renderVideo(item, opts = {}) {
5801
6615
  } catch (err) {
5802
6616
  enc?.stdin?.destroy();
5803
6617
  enc?.kill("SIGKILL");
6618
+ cleanSound();
5804
6619
  throw err;
5805
6620
  }
5806
6621
  if (enc) {
5807
6622
  enc.stdin.end();
5808
6623
  const code = await encDone;
6624
+ cleanSound();
5809
6625
  if (code !== 0) throw new MotionError(`ffmpeg failed (${code}): ${encError.trim().slice(-600)}`);
5810
6626
  }
5811
6627
  let bytes = 0;
5812
6628
  try {
5813
- bytes = format === "png" ? 0 : statSync2(file).size;
6629
+ bytes = format === "png" ? 0 : statSync(file).size;
5814
6630
  } catch {
5815
6631
  }
5816
- return { file, format, width: s.width, height: s.height, fps: s.fps, frames: total, duration: s.built.duration, drawn, bytes, ms: Math.round(performance.now() - started), warnings: s.warnings };
6632
+ return { file, format, width: s.width, height: s.height, fps: s.fps, frames: total, duration: s.built.duration, drawn, bytes, ms: Math.round(performance.now() - started), warnings: s.warnings, ...sound ? { sound } : {} };
5817
6633
  }
5818
6634
 
5819
6635
  // src/node/check.ts
@@ -5865,194 +6681,6 @@ function describeReport(rep) {
5865
6681
  return lines.join("\n");
5866
6682
  }
5867
6683
 
5868
- // src/node/inputs.ts
5869
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
5870
- import { extname, resolve as resolve3 } from "node:path";
5871
- var num = (v) => typeof v === "number" ? v : Number.parseFloat(String(v));
5872
- function normalizeWords(input, offset = 0) {
5873
- const rows = Array.isArray(input) ? input : input && typeof input === "object" ? Array.isArray(input.words) ? input.words : Array.isArray(input.segments) ? input.segments.flatMap((s) => s.words ?? []) : [] : [];
5874
- const out = [];
5875
- for (const r of rows) {
5876
- const o = r ?? {};
5877
- const w = String(o.w ?? o.word ?? o.text ?? "").trim();
5878
- const s = num(o.s ?? o.start) - offset;
5879
- const e = num(o.e ?? o.end) - offset;
5880
- if (!w || !Number.isFinite(s)) continue;
5881
- if (s < -0.01) continue;
5882
- out.push({ w, s: Math.max(0, s), e: Number.isFinite(e) ? Math.max(s, e) : s + 0.3 });
5883
- }
5884
- return out.slice(0, 600);
5885
- }
5886
- function readWords(file, offset = 0) {
5887
- const path = resolve3(file);
5888
- if (!existsSync5(path)) throw new MotionError(`no transcript at ${path}`);
5889
- const words = normalizeWords(JSON.parse(readFileSync5(path, "utf8")), offset);
5890
- if (!words.length) throw new MotionError(`${path} has no timed words (expected [{w, s, e}] or Whisper JSON)`);
5891
- return words;
5892
- }
5893
- var MIME = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".gif": "image/gif", ".svg": "image/svg+xml" };
5894
- async function loadImage(src) {
5895
- if (src.startsWith("data:image/")) return src;
5896
- if (/^https?:\/\//i.test(src)) {
5897
- const res = await fetch(src);
5898
- if (!res.ok) throw new MotionError(`${src}: HTTP ${res.status}`);
5899
- const type2 = res.headers.get("content-type")?.split(";")[0] ?? MIME[extname(new URL(src).pathname).toLowerCase()] ?? "image/png";
5900
- if (!type2.startsWith("image/")) throw new MotionError(`${src} is not an image (${type2})`);
5901
- return `data:${type2};base64,${Buffer.from(await res.arrayBuffer()).toString("base64")}`;
5902
- }
5903
- const path = resolve3(src);
5904
- if (!existsSync5(path)) throw new MotionError(`no picture at ${path}`);
5905
- const type = MIME[extname(path).toLowerCase()];
5906
- if (!type) throw new MotionError(`${path}: pictures are PNG, JPEG, WebP, GIF or SVG`);
5907
- return `data:${type};base64,${readFileSync5(path).toString("base64")}`;
5908
- }
5909
- async function loadMedia(item, props = {}) {
5910
- const out = {};
5911
- for (const [name, spec2] of Object.entries(item.props)) {
5912
- const v = props[name] ?? spec2.default;
5913
- if (spec2.type !== "image" || typeof v !== "string" || !v.trim()) continue;
5914
- out[name] = await loadImage(v.trim());
5915
- }
5916
- return out;
5917
- }
5918
- function readStyleArg(arg) {
5919
- if (!arg) return void 0;
5920
- const path = resolve3(arg);
5921
- if (/\.(md|markdown|txt)$/i.test(arg) || existsSync5(path) && !/^[a-z0-9-]+$/i.test(arg)) {
5922
- if (!existsSync5(path)) throw new MotionError(`no style file at ${path}`);
5923
- return readFileSync5(path, "utf8");
5924
- }
5925
- return arg;
5926
- }
5927
-
5928
- // src/guide.ts
5929
- var MOTION_GUIDE = `Motion: any part, any move
5930
-
5931
- Every graphic is made of named parts (volito info <item> lists them). Each part enters, may idle
5932
- while it holds, may take emphasis hits, and leaves \u2014 and every one of those is yours to change:
5933
-
5934
- in / out any move (volito moves): rise, slam, wipe, iris, glitch, scramble, typewriter,
5935
- spin, swing, stretch, smear, blur, flip, bounce \u2026 An exit plays the move backwards.
5936
- ease brand, out, inOut, expo, back, elastic, bounce, snappy, punchy, bouncy, calm, wobbly,
5937
- spring(freq, damping), steps(n), cubic(x1,y1,x2,y2)
5938
- from the side a move comes from (left, right, up, down, up-left \u2026); outFrom for the exit
5939
- dur, delay seconds; at / outAt replace the item's own times (outAt negative = from the end)
5940
- distance how far it travels (px at 1080) \xB7 intensity 0\u20132 scales distance, size and rotation
5941
- blur true: motion blur on the way in and out \xB7 origin: the pivot (center, top-left, bottom \u2026)
5942
- idle a loop while it holds: float, bob, pulse, breathe, wiggle, sway, drift, jitter, spin, heartbeat, shimmer
5943
- emphasis hits: "pulse@1.2, shake@now, jump@beatName" (a time, a beat, or a spoken word)
5944
- split text by lines, words or letters \xB7 order forward | reverse | center | edges | random \xB7 stagger s
5945
- keys your own keyframes: [{"t":0,"x":-300,"op":0},{"t":0.8,"x":0,"op":1,"ease":"bouncy"}]
5946
- (t in clip seconds, x/y px at 1080, s scale, r degrees, skx, op, blur) \u2014 any path you like
5947
-
5948
- Set them per part, or for every part with "*":
5949
- CLI --motion name.in=typewriter --motion "*.ease=bouncy" --motion plate.out=wipe --motion role.emphasis=pulse@2
5950
- API { motion: { "*": { ease: "bouncy" }, name: { in: "typewriter" } } }
5951
-
5952
- Or let a mood choreograph it: --mood energetic | elegant | playful | cinematic | glitchy | minimal | retro | kinetic,
5953
- and --seed <n> for a new take every number (a seed alone picks a mood too). Your own settings still win.
5954
- --speed 2 plays every move twice as fast.
5955
-
5956
- In your own code the same kit is there: const M = motion($, t);
5957
- M.part(name, { at, box: {x,y,w,h}, in, out, \u2026 }, ...markup) a part, posed by its motion
5958
- M.text(name, { ...$.text options, at, in, split, \u2026 }, text) text, whole or by unit
5959
- M.layout(text, textOptions, split) \u2192 { units: [{text, x, y, w, h, line, index, box}], box }
5960
- M.progress(name, { at, in, dur, ease }) \u2192 { in, out, at, end, outAt } for things you draw yourself
5961
- Give every part a box when you can: wipes, iris, shutter and blinds need it, and moods pick from more moves.`;
5962
- var LIBRARY_GUIDE = `Libraries: layout, patterns, 3D and maps
5963
-
5964
- Layout (always there)
5965
- safeArea($) \u2192 {x, y, w, h, tall, square, wide}: the title-safe box (on 9:16 it clears the platforms' buttons)
5966
- fitFont($, str, role, size, maxW, extra) \u2192 a font in the brand's role, shrunk until str fits maxW
5967
- fitLines($, str, font, maxW, maxSize, minSize, maxLines) \u2192 {size, lines, font}: the biggest size that sets in \u2264 maxLines
5968
- landOn($, str, fallback, after) \u2192 when the first spoken word of str starts (0.08 s early), else fallback
5969
- widest($, lines, font) \xB7 avatar($, imageProp, cx, cy, r, {name, fill, ring}) \xB7 appIcon($, icon, x, y, size, {fill}) \xB7 initials(name)
5970
-
5971
- Patterns (always there) \u2014 sixteen animated grounds in the brand's colours, pure functions of t:
5972
- $.pattern(t, kind, {box, color, color2, a (strength), scale, speed, angle, at: [x, y], count, seed, r}) \u2192 markup
5973
- kinds: dots \xB7 plus \xB7 graph \xB7 hatch \xB7 stripes \xB7 checker \xB7 halftone \xB7 dither \xB7 waves \xB7 rings \xB7 mesh \xB7 confetti \xB7 bauhaus \xB7 tiles \xB7 pills \xB7 sunburst
5974
- A style whose texture names one draws it on every $.panel / $.texture ground. confetti, bauhaus and pills keep to the edges.
5975
-
5976
- Faces: Inter, Space Grotesk, Space Mono, Poppins, Playfair Display, Montserrat, Anton, Outfit, Gabarito, Caveat, Press Start 2P,
5977
- Permanent Marker, Patrick Hand, Fredoka, IM Fell English, Bebas Neue, DM Serif Display, Archivo Black, Syne, Unbounded,
5978
- Righteous, Orbitron, Pacifico, Fraunces, Bricolage Grotesque \u2014 name them "Family:weight" (e.g. "Fraunces:900", "Syne:800").
5979
-
5980
- 3D \u2014 real depth, light and a camera, drawn as SVG paths (no GPU). Call it and the library comes with your program:
5981
- const S = scene3d($, { camera: { fov: 35, distance, yaw, pitch, roll, target: [x, y, z], x, y }, light: { dir: [x, y, z], ambient, spec, shine } });
5982
- Coordinates: x right, y down, z away from you, px at the target's depth (z = 0 draws at 1 px per unit by default).
5983
- Meshes: S.text(str, {face: $.font("display"), size, depth, color, side, back, anchor, ls, each: (i, n, ch) => ({rotate, offset, scale, hide})})
5984
- S.box(w, h, d, {color, side, back, radius}) \xB7 S.card(w, h, {depth, radius, color, back}) \xB7 S.disc(r, d, {...})
5985
- S.shape(loops | "M\u2026 path data", depth, {scale, color, side}) \xB7 S.sphere(r, {color}) (smooth) or {colorAt(lat, lon)}
5986
- Place: S.add(mesh, {position, rotate: [x\xB0, y\xB0, z\xB0], scale, parent: {position, rotate, parent}, opacity, flat, decal, twoSided})
5987
- \u2014 parent moves text with the card it is printed on; decal draws it over that card's face.
5988
- Also: S.line(points3d, {stroke, width}) \xB7 S.at(point3d, (x, y, scale) => markup) flat labels at 3D points \xB7 S.project(p) \u2192 {x, y, z, s}
5989
- S.measure(str, {face, size}) \xB7 S.clip(poly, normal, point)
5990
- Finish with: return S.render(); 3D text uses the brand's display face (an item lists others in "faces3d").
5991
-
5992
- Maps (Natural Earth, 177 countries). Call one and the library comes with your program:
5993
- worldFind("India" | "UK" | "USA" | 356) \u2192 {name, id, c: [lon, lat], rings}
5994
- const m = worldMap($, { x, y, w, h, center: [lon, lat], zoom }); m.path(country | name) \u2192 path data \xB7 m.project(lon, lat) \u2192 [x, y]
5995
- m.countries \xB7 m.outline (the map's edge) \xB7 m.clipId() (clip to it) \xB7 m.ring(points)
5996
- worldGlobe(S, { r, spin, tilt, colorOf: (country) => colour }) countries on a 3D globe \xB7 worldOnSphere(lon, lat, r, spin, tilt) \u2192 a 3D point`;
5997
- var AUTHORING_GUIDE = `Writing Volito graphics
5998
-
5999
- A graphic is a program: plain JavaScript that defines function frame(t, $) and returns the SVG markup of
6000
- one frame (no <svg> root). It is a pure function of t \u2014 any frame can be drawn alone, in any order \u2014 so it
6001
- renders the same in MONARC PRO, in a browser and from this CLI.
6002
-
6003
- Props and moments
6004
- An item's props arrive as $.data (const p = $.data; p.name, p.role \u2026). Never hard-code text a creator
6005
- would change: make it a prop with a default and a maxLength the design can hold.
6006
- Named moments come from $.beat(name, fallback). Pack items read "in" (when the entrance starts) and
6007
- "out" (when the exit starts) so a creator can move them: --beat in=0.4 --beat out=-0.6.
6008
- When words are given (a transcript), land each element 0.04\u20130.12 s before the word that says it:
6009
- $.words.length ? $.atOr(firstWord, fallback) : fallback. Without words, use the beats.
6010
-
6011
- Parts
6012
- Draw each element through the motion kit (const M = motion($, t)) as a named part, with the item's own
6013
- motion as defaults: M.part("plate", { at, in: "wipe", box }, markup), M.text("name", { ...font, at, in: "rise" }, text).
6014
- List the parts in item.json ("parts": { "plate": "the card" }) so creators know what they can move.
6015
- Backgrounds (scrims, panels) pass vary: false so a mood leaves them still.
6016
-
6017
- Shape of a good item
6018
- - Enter in the first ~0.8 s, hold, leave in the last ~0.4 s (or hold to the end when out \u2265 duration).
6019
- - Lay out from $.W, $.H and scale every size by $.k (= min(W, H) / 1080): one design fits 16:9, 9:16, 1:1, 4:5.
6020
- - Keep text inside the title-safe area: ~5% of the width at the sides on 16:9; on 9:16 leave ~13% at the
6021
- top and ~22% at the bottom for platform UI (captions, buttons).
6022
- - Size text to fit: measure it ($.measure) and shrink the size until it fits the box it owns; wrap long
6023
- text with maxW. The check reports any line that runs off the frame.
6024
- - Colour comes from the brand: $.c.bg / $.c.fg for ground and text, $.c.accent for the one thing that
6025
- matters, $.c.accent2 for markers, $.c.surface for cards. Faces from $.font("display" | "text" | "mono").
6026
- $.case(str) applies the brand's case. Never invent colours \u2014 a creator's DESIGN.md re-skins the item.
6027
- - overlay items draw nothing behind themselves (footage shows through); put a $.scrim under text that sits
6028
- on footage. full items draw their own ground with $.panel(t, { box: { x: 0, y: 0, w: $.W, h: $.H } }).
6029
- - Escape any text you put in raw markup with $.esc(); $.text, $.chip and friends escape for you.
6030
-
6031
- An item on disk
6032
- my-item/item.json { "id": "my-item", "name": "\u2026", "description": "\u2026", "category": "title",
6033
- "placement": "overlay" | "full", "duration": { "default": 5, "min": 2, "max": 20 },
6034
- "aspects": ["16:9", "9:16", "1:1", "4:5"],
6035
- "props": { "title": { "type": "text", "default": "Hello", "maxLength": 28 } },
6036
- "beats": { "in": 0.15, "out": -0.45 } }
6037
- my-item/frame.js function frame(t, $) { const p = $.data; \u2026 return svg; }
6038
- Prop types: text, longtext (line breaks kept), number (min, max, step), boolean, choice (options),
6039
- color (#hex, "" = the brand's), list (rows of text, or of {field: "text" | "number"}; maxItems),
6040
- image (a path or URL the caller loads; draw it with $.image(propName, x, y, w, h)), icon (an icon name).
6041
-
6042
- A pack
6043
- my-pack/pack.json { "format": 1, "id": "my-pack", "name": "\u2026", "version": "1.0.0", "description": "\u2026",
6044
- "author": "\u2026", "license": "\u2026", "style": "ember", "shared": "shared.js",
6045
- "items": ["my-item", \u2026] }
6046
- my-pack/shared.js helpers prepended to every item's code (function declarations only).
6047
- Load extra packs with --packs <dir> or VOLITO_PACKS=<dir>.
6048
-
6049
- The loop
6050
- volito new my-item a template to start from
6051
- volito check ./my-item errors and warnings at every aspect
6052
- volito sheet ./my-item frames across time in one PNG \u2014 look at it
6053
- volito render ./my-item --format mov
6054
- Agents: motion_check, then motion_preview, fix what you see, then motion_render.`;
6055
-
6056
6684
  export {
6057
6685
  MG_SPRITE_NAMES,
6058
6686
  MG_ICON_NAMES,
@@ -6063,6 +6691,9 @@ export {
6063
6691
  BUILTIN_STYLES2 as BUILTIN_STYLES,
6064
6692
  composeDesignMd2 as composeDesignMd,
6065
6693
  EDITOR_STYLES,
6694
+ TrueTypeFace,
6695
+ glyphTable,
6696
+ setGlyphSource,
6066
6697
  MOVES,
6067
6698
  IDLES,
6068
6699
  HITS,
@@ -6082,6 +6713,8 @@ export {
6082
6713
  MotionError,
6083
6714
  manifestProblems,
6084
6715
  makeItem,
6716
+ LIBRARY_NAMES,
6717
+ registerLibrary,
6085
6718
  resolveProps,
6086
6719
  resolveStyle,
6087
6720
  canvasFor,
@@ -6089,37 +6722,32 @@ export {
6089
6722
  buildProgram,
6090
6723
  customItem,
6091
6724
  describeItem,
6092
- fontsDir,
6093
- packsDir,
6094
- packageVersion,
6095
- loadPack,
6096
- builtinPacks,
6097
- packsIn,
6098
- library,
6099
- findItem,
6100
- searchItems,
6101
- probeFfmpeg,
6102
- FfmpegMissing,
6103
6725
  ProgramError,
6726
+ soundHookAvailable,
6104
6727
  ProgramRunner,
6728
+ SOUND_KINDS,
6729
+ SOUND_NOTES,
6730
+ noteHz,
6731
+ parseKey,
6732
+ normalizeSoundSpec,
6733
+ bedOf,
6734
+ cuesFromMarks,
6735
+ renderSound,
6736
+ wavEncode,
6737
+ describeCues,
6105
6738
  VIDEO_FORMATS,
6106
6739
  FORMAT_NOTES,
6107
6740
  openSession,
6108
6741
  frameDocument,
6742
+ framePixels,
6109
6743
  renderStill,
6110
6744
  sheetTimes,
6111
6745
  renderSheet,
6746
+ designSound,
6747
+ renderSoundFile,
6112
6748
  formatFromPath,
6113
6749
  defaultOutput,
6114
6750
  renderVideo,
6115
6751
  checkItem,
6116
- describeReport,
6117
- normalizeWords,
6118
- readWords,
6119
- loadImage,
6120
- loadMedia,
6121
- readStyleArg,
6122
- MOTION_GUIDE,
6123
- LIBRARY_GUIDE,
6124
- AUTHORING_GUIDE
6752
+ describeReport
6125
6753
  };