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.
- package/README.md +200 -9
- package/dist/browser.d.ts +50 -1
- package/dist/browser.js +352 -43
- package/dist/chunks/{chunk-CFDFWZAC.js → chunk-CUQY6YI3.js} +1032 -404
- package/dist/chunks/chunk-PM3PLI3X.js +71 -0
- package/dist/chunks/chunk-QA4SEJCZ.js +601 -0
- package/dist/chunks/chunk-ZFG3X3DU.js +67 -0
- package/dist/chunks/chunk-ZHMWEZ4C.js +186 -0
- package/dist/chunks/init-JX2BYRKT.js +189 -0
- package/dist/chunks/review-RBH5EUZN.js +18 -0
- package/dist/cli.js +232 -32
- package/dist/index.d.ts +314 -2
- package/dist/index.js +75 -21
- package/dist/mcp.js +129 -22
- package/dist/player.d.ts +643 -0
- package/dist/player.js +6320 -0
- package/dist/registry.json +1 -1
- package/examples/player.html +93 -0
- package/examples/states.html +73 -0
- package/examples/volito-intro/DESIGN.md +23 -0
- package/examples/volito-intro/frame.js +187 -0
- package/examples/volito-intro/item.json +43 -0
- package/package.json +15 -7
- package/packs/3d/pack.json +1 -1
- package/packs/annotate/emphasis/frame.js +8 -0
- package/packs/annotate/pack.json +1 -1
- package/packs/annotate/reactions/frame.js +12 -0
- package/packs/backdrops/gradient/frame.js +5 -0
- package/packs/backdrops/pack.json +1 -1
- package/packs/broadcast/pack.json +1 -1
- package/packs/data/pack.json +1 -1
- package/packs/effects/chromatic/frame.js +17 -0
- package/packs/effects/chromatic/item.json +15 -0
- package/packs/effects/glass/frame.js +44 -0
- package/packs/effects/glass/item.json +16 -0
- package/packs/effects/grain/frame.js +20 -0
- package/packs/effects/grain/item.json +16 -0
- package/packs/effects/light-leak/frame.js +24 -0
- package/packs/effects/light-leak/item.json +15 -0
- package/packs/effects/neon/frame.js +28 -0
- package/packs/effects/neon/item.json +15 -0
- package/packs/effects/pack.json +13 -0
- package/packs/effects/shared.js +23 -0
- package/packs/effects/warp/frame.js +13 -0
- package/packs/effects/warp/item.json +15 -0
- package/packs/essentials/pack.json +1 -1
- package/packs/essentials/wipe/frame.js +13 -0
- package/packs/promo/pack.json +1 -1
- package/packs/social/pack.json +1 -1
- package/packs/transitions/glitch/frame.js +14 -0
- package/packs/transitions/pack.json +1 -1
- package/packs/transitions/shared.js +12 -0
- package/packs/type/pack.json +1 -1
- package/packs/type/word-by-word/frame.js +10 -0
- package/packs/ui/bell/frame.js +24 -0
- package/packs/ui/bell/item.json +21 -0
- package/packs/ui/button/frame.js +55 -0
- package/packs/ui/button/item.json +26 -0
- package/packs/ui/like/frame.js +34 -0
- package/packs/ui/like/item.json +21 -0
- package/packs/ui/loader/frame.js +34 -0
- package/packs/ui/loader/item.json +23 -0
- package/packs/ui/pack.json +13 -0
- package/packs/ui/shared.js +54 -0
- package/packs/ui/toggle/frame.js +30 -0
- package/packs/ui/toggle/item.json +22 -0
- package/{skill → skills/volito}/SKILL.md +43 -3
- package/skills/volito/references/art-direction.md +58 -0
- package/skills/volito/references/brand.md +59 -0
- package/skills/volito/references/delivery.md +40 -0
- package/skills/volito/references/matching-a-reference.md +39 -0
- package/skills/volito/references/motion-craft.md +56 -0
- package/skills/volito/references/self-review.md +44 -0
- package/skills/volito/references/type-and-layout.md +44 -0
- package/skills/volito/references/voice-and-timing.md +37 -0
- package/skills/volito/references/writing-items.md +44 -0
- /package/{skill/template → templates/item}/frame.js +0 -0
- /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
|
|
4198
|
-
if (
|
|
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
|
|
5031
|
-
import { join
|
|
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 =
|
|
5037
|
-
if (!
|
|
5038
|
-
faces = JSON.parse(
|
|
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
|
|
5072
|
-
if (
|
|
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) =>
|
|
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) =>
|
|
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(
|
|
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
|
|
5135
|
-
if (
|
|
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,${
|
|
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
|
|
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,
|
|
5291
|
-
import { availableParallelism } from "node:os";
|
|
5292
|
-
import { dirname
|
|
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
|
|
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 =
|
|
5459
|
-
y =
|
|
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(
|
|
6188
|
+
seg(num() + ox, num() + oy);
|
|
5466
6189
|
break;
|
|
5467
6190
|
case "H":
|
|
5468
|
-
seg(
|
|
6191
|
+
seg(num() + ox, y);
|
|
5469
6192
|
break;
|
|
5470
6193
|
case "V":
|
|
5471
|
-
seg(x,
|
|
6194
|
+
seg(x, num() + oy);
|
|
5472
6195
|
break;
|
|
5473
6196
|
case "C": {
|
|
5474
|
-
const a =
|
|
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 =
|
|
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 =
|
|
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,
|
|
6222
|
+
quad(a, b, num() + ox, num() + oy);
|
|
5500
6223
|
break;
|
|
5501
6224
|
}
|
|
5502
6225
|
case "A": {
|
|
5503
|
-
const rx =
|
|
5504
|
-
arc(rx, ry, phi, large ? 1 : 0, sweep ? 1 : 0,
|
|
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 =
|
|
5647
|
-
mkdirSync(
|
|
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 =
|
|
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(
|
|
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(
|
|
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 :
|
|
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
|
};
|