volito 0.1.1 → 0.3.1
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 +162 -6
- 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 +147 -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 +12 -4
- package/packs/annotate/emphasis/frame.js +8 -0
- package/packs/annotate/reactions/frame.js +12 -0
- package/packs/backdrops/gradient/frame.js +5 -0
- 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/wipe/frame.js +13 -0
- package/packs/transitions/glitch/frame.js +14 -0
- package/packs/transitions/shared.js +12 -0
- 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
package/dist/browser.js
CHANGED
|
@@ -3894,6 +3894,36 @@ function motion($, t) {
|
|
|
3894
3894
|
return [x, y];
|
|
3895
3895
|
}
|
|
3896
3896
|
|
|
3897
|
+
/** Moves that read as a spring (a pop in the sound design) rather than travel across the frame. */
|
|
3898
|
+
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 };
|
|
3899
|
+
/**
|
|
3900
|
+
* Tells the sound trace what a part does, through the runtime's timing helpers (they leave the marks a
|
|
3901
|
+
* sound design reads): a slam shakes, a spring pops, a move across the frame enters from a distance, a
|
|
3902
|
+
* fade fades, a typewriter types, a glitch flashes, and every emphasis hit bumps. One cheap call a frame;
|
|
3903
|
+
* nothing is drawn.
|
|
3904
|
+
*/
|
|
3905
|
+
function note(s, tm, c, text) {
|
|
3906
|
+
if (tm.mv.always || typeof $.enter !== "function") return;
|
|
3907
|
+
var mv = s["in"];
|
|
3908
|
+
if (mv === "slam") $.shake(t, tm.at + tm.dur * 0.55, 24);
|
|
3909
|
+
else if (mv === "typewriter") $.typed(t, tm.at, text || "", speed / (s.stagger > 0 ? Number(s.stagger) : 0.055));
|
|
3910
|
+
else if (mv === "scramble") $.typed(t, tm.at, text || "", (text ? Array.from(text).length : 8) / Math.max(0.1, tm.dur));
|
|
3911
|
+
else if (mv === "glitch" || mv === "flicker") $.flash(t, tm.at, { a: 0 });
|
|
3912
|
+
else if (mv === "fade" || mv === "blur" || mv === "appear" || mv === "float") $.fadeIn(t, tm.at);
|
|
3913
|
+
else if (MM_SPRINGY[mv]) $.pop(t, tm.at);
|
|
3914
|
+
else {
|
|
3915
|
+
var p0 = tm.mv.pose(0, 0, c) || {};
|
|
3916
|
+
var dx = p0.x || 0, dy = p0.y || 0;
|
|
3917
|
+
if (!dx && !dy && tm.mv.box && c.box) dx = c.box.w * 0.5;
|
|
3918
|
+
if (dx || dy) $.enter(t, tm.at, { dx: dx, dy: dy }); else $.pop(t, tm.at);
|
|
3919
|
+
}
|
|
3920
|
+
hits(s).forEach(function (h) {
|
|
3921
|
+
var at = hitTime(h.at, tm.at);
|
|
3922
|
+
if (!isFinite(at)) return;
|
|
3923
|
+
if (h.fx === "shake") $.shake(t, at, 16); else if (h.fx === "flash") $.flash(t, at, { a: 0 }); else $.bump(t, at);
|
|
3924
|
+
});
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3897
3927
|
var api = {};
|
|
3898
3928
|
api.mood = mood;
|
|
3899
3929
|
api.seed = seed;
|
|
@@ -3908,6 +3938,7 @@ function motion($, t) {
|
|
|
3908
3938
|
var tm = timing(s);
|
|
3909
3939
|
var pv = pivot(o.box, s.origin, o, tm.mv.origin);
|
|
3910
3940
|
var c = context(s, tm, o.box, pv[0], pv[1], salt(name) % 97);
|
|
3941
|
+
note(s, tm, c);
|
|
3911
3942
|
return render(kids, poseAt(s, tm, c, t), c);
|
|
3912
3943
|
};
|
|
3913
3944
|
|
|
@@ -3974,6 +4005,7 @@ function motion($, t) {
|
|
|
3974
4005
|
if (!lay.units.length || lay.mode === "none") {
|
|
3975
4006
|
var pv = pivot(lay.box, s.origin, o, tm.mv.origin);
|
|
3976
4007
|
var c = context(s, tm, lay.box, pv[0], pv[1], salt(name) % 97);
|
|
4008
|
+
note(s, tm, c, lay.text);
|
|
3977
4009
|
return render($.text(lay.text, font), poseAt(s, tm, c, t), c);
|
|
3978
4010
|
}
|
|
3979
4011
|
var n = lay.units.length;
|
|
@@ -3998,6 +4030,9 @@ function motion($, t) {
|
|
|
3998
4030
|
shown = pool.charAt(Math.floor(mmHash(i * 7.7 + Math.floor(t * 18) * 1.9 + vseed) * pool.length));
|
|
3999
4031
|
}
|
|
4000
4032
|
if (t >= ut.at) lastShown = u;
|
|
4033
|
+
// The sound trace: typing and scrambles once for the whole text, else the first unit with its move and every other one a tick.
|
|
4034
|
+
if (behave === "type" || behave === "scramble") { if (i === 0) note(s, tm, c2, lay.text); }
|
|
4035
|
+
else if (rk === 0) note(s, ut, c2, u.text); else if (typeof $.bump === "function") $.bump(t, ut.at);
|
|
4001
4036
|
out += render(draw(shown, u.x, u), poseAt(s, ut, c2, t), c2);
|
|
4002
4037
|
});
|
|
4003
4038
|
var typed = tm.at + n * (s.stagger > 0 ? Number(s.stagger) : 0.055) / speed;
|
|
@@ -4167,6 +4202,72 @@ function parseMotionArgs(args) {
|
|
|
4167
4202
|
return out;
|
|
4168
4203
|
}
|
|
4169
4204
|
|
|
4205
|
+
// src/player/states.ts
|
|
4206
|
+
var STATE_PLAYS = ["once", "loop", "hold"];
|
|
4207
|
+
var NAME = /^[A-Za-z][\w-]{0,40}$/;
|
|
4208
|
+
function parseStates(value, where = "states") {
|
|
4209
|
+
if (value == null || value === "") return void 0;
|
|
4210
|
+
let v = value;
|
|
4211
|
+
if (typeof v === "string") {
|
|
4212
|
+
try {
|
|
4213
|
+
v = JSON.parse(v);
|
|
4214
|
+
} catch {
|
|
4215
|
+
throw new Error(`${where} must be a JSON object, like {"idle":{"from":0,"to":1,"play":"loop"}}`);
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) throw new Error(`${where} must be a JSON object of states`);
|
|
4219
|
+
const out = {};
|
|
4220
|
+
for (const [name, raw] of Object.entries(v)) {
|
|
4221
|
+
if (!NAME.test(name)) throw new Error(`${where}: "${name}" is not a state name (letters, digits, - and _)`);
|
|
4222
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`${where}.${name} must be an object like {"from":"hover","to":"press","play":"once"}`);
|
|
4223
|
+
const d = raw;
|
|
4224
|
+
const play = d.play ?? "once";
|
|
4225
|
+
if (!STATE_PLAYS.includes(play)) throw new Error(`${where}.${name}.play is one of ${STATE_PLAYS.join(", ")}`);
|
|
4226
|
+
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`);
|
|
4227
|
+
out[name] = { ...d.from !== void 0 ? { from: d.from } : {}, ...d.to !== void 0 ? { to: d.to } : {}, play, ...typeof d.next === "string" ? { next: d.next } : {} };
|
|
4228
|
+
}
|
|
4229
|
+
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(", ")})`);
|
|
4230
|
+
return Object.keys(out).length ? out : void 0;
|
|
4231
|
+
}
|
|
4232
|
+
function parseTriggers(value, states) {
|
|
4233
|
+
if (value == null || value === "") return [];
|
|
4234
|
+
if (typeof value !== "string") throw new Error('on takes "event: state" pairs, like "pointerenter: hover; click: press"');
|
|
4235
|
+
const out = [];
|
|
4236
|
+
for (const raw of value.split(/[;\n]+/)) {
|
|
4237
|
+
const s = raw.trim();
|
|
4238
|
+
if (!s) continue;
|
|
4239
|
+
const m = /^([a-z][a-z-]*)\s*(?:\(([^)]*)\))?\s*:\s*([A-Za-z][\w-]*)$/i.exec(s);
|
|
4240
|
+
if (!m) throw new Error(`on: "${s}" is not "event: state" (or "event(fromState): state")`);
|
|
4241
|
+
const from = (m[2] ?? "").split(/[\s|,]+/).filter(Boolean);
|
|
4242
|
+
const t = { event: m[1].toLowerCase(), from, to: m[3] };
|
|
4243
|
+
if (states) {
|
|
4244
|
+
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(", ")})`);
|
|
4245
|
+
}
|
|
4246
|
+
out.push(t);
|
|
4247
|
+
}
|
|
4248
|
+
return out;
|
|
4249
|
+
}
|
|
4250
|
+
function resolveTime(v, fallback, duration, beats) {
|
|
4251
|
+
let t;
|
|
4252
|
+
if (v === void 0) t = fallback;
|
|
4253
|
+
else if (typeof v === "number") t = v;
|
|
4254
|
+
else if (v === "start") t = 0;
|
|
4255
|
+
else if (v === "end") t = duration;
|
|
4256
|
+
else if (v in beats) t = beats[v];
|
|
4257
|
+
else if (Number.isFinite(Number(v))) t = Number(v);
|
|
4258
|
+
else throw new Error(`"${v}" is not a beat (${Object.keys(beats).join(", ") || "none"}), seconds, start or end`);
|
|
4259
|
+
if (t < 0) t = duration + t;
|
|
4260
|
+
return Math.max(0, Math.min(duration, t));
|
|
4261
|
+
}
|
|
4262
|
+
function resolveState(name, states, duration, beats = {}) {
|
|
4263
|
+
const d = states[name];
|
|
4264
|
+
if (!d) throw new Error(`no state "${name}" (states: ${Object.keys(states).join(", ")})`);
|
|
4265
|
+
const start = resolveTime(d.from, 0, duration, beats);
|
|
4266
|
+
const end = resolveTime(d.to, duration, duration, beats);
|
|
4267
|
+
if (end < start) throw new Error(`state "${name}" ends (${end.toFixed(2)} s) before it starts (${start.toFixed(2)} s)`);
|
|
4268
|
+
return { name, start, end, play: d.play ?? "once", ...d.next ? { next: d.next } : {} };
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4170
4271
|
// src/three.ts
|
|
4171
4272
|
var MOTION_3D = String.raw`
|
|
4172
4273
|
// ———————— Volito 3D ————————
|
|
@@ -4517,6 +4618,19 @@ function manifestProblems(m) {
|
|
|
4517
4618
|
if (p?.type === "choice" && !p.options?.length) out.push(`prop "${name}" is a choice with no options`);
|
|
4518
4619
|
if (p?.when && !(p.when.prop in (m.props ?? {}))) out.push(`prop "${name}" depends on an unknown prop "${p.when.prop}"`);
|
|
4519
4620
|
}
|
|
4621
|
+
if (m.states !== void 0 || m.on !== void 0) {
|
|
4622
|
+
try {
|
|
4623
|
+
const states = parseStates(m.states, "states");
|
|
4624
|
+
if (!states) out.push("on needs states");
|
|
4625
|
+
else {
|
|
4626
|
+
parseTriggers(m.on, states);
|
|
4627
|
+
const dur = m.duration?.default ?? 0;
|
|
4628
|
+
for (const name of Object.keys(states)) resolveState(name, states, dur, m.beats ?? {});
|
|
4629
|
+
}
|
|
4630
|
+
} catch (e) {
|
|
4631
|
+
out.push(e.message);
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4520
4634
|
return out;
|
|
4521
4635
|
}
|
|
4522
4636
|
function makeItem(pack, manifest, frameCode, shared = "") {
|
|
@@ -4827,9 +4941,127 @@ ${parts.join("\n")}` : ""
|
|
|
4827
4941
|
].filter(Boolean).join("\n");
|
|
4828
4942
|
}
|
|
4829
4943
|
|
|
4944
|
+
// src/browser/fonts.ts
|
|
4945
|
+
function covers(range, cp) {
|
|
4946
|
+
if (!range) return true;
|
|
4947
|
+
return range.split(",").some((part) => {
|
|
4948
|
+
const m = /U\+([0-9A-F?]+)(?:-([0-9A-F]+))?/i.exec(part.trim());
|
|
4949
|
+
if (!m) return false;
|
|
4950
|
+
const lo = Number.parseInt(m[1].replace(/\?/g, "0"), 16);
|
|
4951
|
+
const hi = m[2] ? Number.parseInt(m[2], 16) : Number.parseInt(m[1].replace(/\?/g, "F"), 16);
|
|
4952
|
+
return cp >= lo && cp <= hi;
|
|
4953
|
+
});
|
|
4954
|
+
}
|
|
4955
|
+
function wideCodepoints(s, into = /* @__PURE__ */ new Set()) {
|
|
4956
|
+
for (let i = 0; i < s.length; i++) {
|
|
4957
|
+
const c = s.charCodeAt(i);
|
|
4958
|
+
if (c >= 256) {
|
|
4959
|
+
const cp = s.codePointAt(i);
|
|
4960
|
+
into.add(cp);
|
|
4961
|
+
if (cp > 65535) i++;
|
|
4962
|
+
} else if (c === 38 && s.charCodeAt(i + 1) === 35) {
|
|
4963
|
+
const m = /^&#(x[0-9a-f]+|\d+);/i.exec(s.slice(i, i + 12));
|
|
4964
|
+
if (m) {
|
|
4965
|
+
const cp = m[1][0] === "x" || m[1][0] === "X" ? Number.parseInt(m[1].slice(1), 16) : Number.parseInt(m[1], 10);
|
|
4966
|
+
if (cp >= 256) into.add(cp);
|
|
4967
|
+
}
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
return into;
|
|
4971
|
+
}
|
|
4972
|
+
function matchWeight(available, want) {
|
|
4973
|
+
const ws = [...new Set(available)].sort((a, b) => a - b);
|
|
4974
|
+
if (!ws.length || ws.includes(want)) return want;
|
|
4975
|
+
const below = ws.filter((w) => w < want).reverse();
|
|
4976
|
+
const above = ws.filter((w) => w > want);
|
|
4977
|
+
if (want >= 400 && want <= 500) {
|
|
4978
|
+
const upTo500 = above.filter((w) => w <= 500);
|
|
4979
|
+
return upTo500[0] ?? below[0] ?? above[0];
|
|
4980
|
+
}
|
|
4981
|
+
return want < 400 ? below[0] ?? above[0] : above[0] ?? below[0];
|
|
4982
|
+
}
|
|
4983
|
+
var WEIGHT_WORDS = { normal: 400, bold: 700, bolder: 700, lighter: 300 };
|
|
4984
|
+
var weightOf = (v) => /^\d{3}$/.test(v) ? Number(v) : WEIGHT_WORDS[v.toLowerCase()];
|
|
4985
|
+
var familiesOf = (v) => v.split(",").map((f) => f.trim().replace(/^['"]|['"]$/g, "").trim()).filter(Boolean);
|
|
4986
|
+
function fontWants(svg) {
|
|
4987
|
+
if (!/font/i.test(svg)) return [];
|
|
4988
|
+
const exact = [];
|
|
4989
|
+
const famLoose = /* @__PURE__ */ new Set();
|
|
4990
|
+
const weightLoose = /* @__PURE__ */ new Set();
|
|
4991
|
+
let italicLoose = false;
|
|
4992
|
+
let loose = false;
|
|
4993
|
+
const read = (decl) => {
|
|
4994
|
+
const fams2 = [];
|
|
4995
|
+
let weight;
|
|
4996
|
+
let italic;
|
|
4997
|
+
for (const m of decl.matchAll(/font-family\s*(?:=\s*"([^"]*)"|=\s*'([^']*)'|:\s*([^;"}]+))/gi)) fams2.push(...familiesOf(m[1] ?? m[2] ?? m[3] ?? ""));
|
|
4998
|
+
for (const m of decl.matchAll(/font-weight\s*(?:=\s*["']?|:\s*)([a-z]+|\d{3})/gi)) weight = weightOf(m[1]) ?? weight;
|
|
4999
|
+
for (const m of decl.matchAll(/font-style\s*(?:=\s*["']?|:\s*)([a-z]+)/gi)) italic = /^(italic|oblique)$/i.test(m[1]);
|
|
5000
|
+
for (const m of decl.matchAll(/(?:^|[;"\s{])font\s*:\s*([^;"}]+)/gi)) {
|
|
5001
|
+
const tokens = m[1].trim();
|
|
5002
|
+
const size = /(?:^|\s)[\d.]+(?:px|em|rem|pt|%)(?:\/\S+)?\s+(.+)$/i.exec(tokens);
|
|
5003
|
+
if (size) fams2.push(...familiesOf(size[1]));
|
|
5004
|
+
const pre = size ? tokens.slice(0, tokens.length - size[0].length) : tokens;
|
|
5005
|
+
for (const t of pre.split(/\s+/)) {
|
|
5006
|
+
if (/^(italic|oblique)$/i.test(t)) italic = true;
|
|
5007
|
+
const w = weightOf(t);
|
|
5008
|
+
if (w) weight = w;
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
return { fams: fams2, weight, italic };
|
|
5012
|
+
};
|
|
5013
|
+
for (const m of svg.matchAll(/<(?!style\b)[a-z][^>]*font[^>]*>/gi)) {
|
|
5014
|
+
const { fams: fams2, weight, italic } = read(m[0]);
|
|
5015
|
+
if (!fams2.length && weight === void 0 && italic === void 0) continue;
|
|
5016
|
+
if (fams2.length && weight !== void 0) for (const family of fams2) exact.push({ family, weight, italic: !!italic });
|
|
5017
|
+
else {
|
|
5018
|
+
loose = true;
|
|
5019
|
+
fams2.forEach((f) => famLoose.add(f));
|
|
5020
|
+
if (weight !== void 0) weightLoose.add(weight);
|
|
5021
|
+
if (italic) italicLoose = true;
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
for (const m of svg.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi)) {
|
|
5025
|
+
const css = m[1].replace(/@font-face\s*{[^}]*}/gi, "");
|
|
5026
|
+
for (const rule of css.matchAll(/{([^}]*)}/g)) {
|
|
5027
|
+
const { fams: fams2, weight, italic } = read(rule[1]);
|
|
5028
|
+
if (!fams2.length && weight === void 0 && italic === void 0) continue;
|
|
5029
|
+
loose = true;
|
|
5030
|
+
fams2.forEach((f) => famLoose.add(f));
|
|
5031
|
+
if (weight !== void 0) weightLoose.add(weight);
|
|
5032
|
+
if (italic) italicLoose = true;
|
|
5033
|
+
}
|
|
5034
|
+
}
|
|
5035
|
+
if (!loose) return exact;
|
|
5036
|
+
const fams = /* @__PURE__ */ new Set([...famLoose, ...exact.map((w) => w.family)]);
|
|
5037
|
+
const weights = /* @__PURE__ */ new Set([...weightLoose, ...exact.map((w) => w.weight)]);
|
|
5038
|
+
if (!weights.size) weights.add(400);
|
|
5039
|
+
const out = [...exact];
|
|
5040
|
+
for (const family of fams) for (const weight of weights) for (const italic of italicLoose ? [false, true] : [false]) out.push({ family, weight, italic });
|
|
5041
|
+
return out;
|
|
5042
|
+
}
|
|
5043
|
+
function facesFor(all, wants, cps) {
|
|
5044
|
+
const out = /* @__PURE__ */ new Map();
|
|
5045
|
+
for (const want of wants) {
|
|
5046
|
+
const fam = all.filter((f) => f.family.toLowerCase() === want.family.toLowerCase());
|
|
5047
|
+
if (!fam.length) continue;
|
|
5048
|
+
const slant = fam.filter((f) => f.italic === want.italic);
|
|
5049
|
+
const pool = slant.length ? slant : fam;
|
|
5050
|
+
const weight = matchWeight(
|
|
5051
|
+
pool.map((f) => f.weight),
|
|
5052
|
+
want.weight
|
|
5053
|
+
);
|
|
5054
|
+
for (const f of pool) {
|
|
5055
|
+
if (f.weight !== weight) continue;
|
|
5056
|
+
if (f.subset === "latin" || [...cps].some((cp) => covers(f.range, cp))) out.set(f.file, f);
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
return [...out.values()];
|
|
5060
|
+
}
|
|
5061
|
+
|
|
4830
5062
|
// src/browser/index.ts
|
|
4831
|
-
var VERSION = "0.
|
|
4832
|
-
var CDN_BASE = `https://cdn.jsdelivr.net/npm/${"volito"}@${"0.
|
|
5063
|
+
var VERSION = "0.3.1";
|
|
5064
|
+
var CDN_BASE = `https://cdn.jsdelivr.net/npm/${"volito"}@${"0.3.1"}/`;
|
|
4833
5065
|
var registries = /* @__PURE__ */ new Map();
|
|
4834
5066
|
function loadRegistry(base = CDN_BASE) {
|
|
4835
5067
|
const url = new URL("dist/registry.json", base).href;
|
|
@@ -4863,7 +5095,7 @@ async function preloadGlyphs(item, opts, base) {
|
|
|
4863
5095
|
if (glyphTables.has(key)) return Promise.resolve();
|
|
4864
5096
|
const pending = glyphLoads.get(key);
|
|
4865
5097
|
if (pending) return pending;
|
|
4866
|
-
const p =
|
|
5098
|
+
const p = fontBlob(new URL(`fonts/${file}`, base).href).then((b) => b.arrayBuffer()).then((buf) => {
|
|
4867
5099
|
glyphTables.set(key, glyphTable(new TrueTypeFace(new Uint8Array(buf)), key));
|
|
4868
5100
|
}).catch(() => {
|
|
4869
5101
|
glyphLoads.delete(key);
|
|
@@ -4874,7 +5106,9 @@ async function preloadGlyphs(item, opts, base) {
|
|
|
4874
5106
|
);
|
|
4875
5107
|
}
|
|
4876
5108
|
var faceLists = /* @__PURE__ */ new Map();
|
|
4877
|
-
var
|
|
5109
|
+
var fontBlobs = /* @__PURE__ */ new Map();
|
|
5110
|
+
var fontObjectUrls = /* @__PURE__ */ new Map();
|
|
5111
|
+
var fontDataUrls = /* @__PURE__ */ new Map();
|
|
4878
5112
|
function facesAt(base) {
|
|
4879
5113
|
const url = new URL("fonts/faces.json", base).href;
|
|
4880
5114
|
let p = faceLists.get(url);
|
|
@@ -4888,44 +5122,46 @@ function facesAt(base) {
|
|
|
4888
5122
|
}
|
|
4889
5123
|
return p;
|
|
4890
5124
|
}
|
|
4891
|
-
function
|
|
4892
|
-
let p =
|
|
5125
|
+
function fontBlob(url) {
|
|
5126
|
+
let p = fontBlobs.get(url);
|
|
4893
5127
|
if (!p) {
|
|
4894
5128
|
p = fetch(url).then((r) => {
|
|
4895
5129
|
if (!r.ok) throw new MotionError(`${url}: HTTP ${r.status}`);
|
|
4896
5130
|
return r.blob();
|
|
4897
|
-
})
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
fr.onerror = () => rej(fr.error);
|
|
4901
|
-
fr.readAsDataURL(b);
|
|
4902
|
-
}));
|
|
4903
|
-
faceData.set(url, p);
|
|
4904
|
-
p.catch(() => faceData.delete(url));
|
|
5131
|
+
});
|
|
5132
|
+
fontBlobs.set(url, p);
|
|
5133
|
+
p.catch(() => fontBlobs.delete(url));
|
|
4905
5134
|
}
|
|
4906
5135
|
return p;
|
|
4907
5136
|
}
|
|
4908
|
-
function
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
});
|
|
5137
|
+
function fontObjectUrl(url) {
|
|
5138
|
+
let p = fontObjectUrls.get(url);
|
|
5139
|
+
if (!p) {
|
|
5140
|
+
p = fontBlob(url).then((b) => URL.createObjectURL(b));
|
|
5141
|
+
fontObjectUrls.set(url, p);
|
|
5142
|
+
p.catch(() => fontObjectUrls.delete(url));
|
|
5143
|
+
}
|
|
5144
|
+
return p;
|
|
4917
5145
|
}
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
5146
|
+
function fontDataUrl(url) {
|
|
5147
|
+
let p = fontDataUrls.get(url);
|
|
5148
|
+
if (!p) {
|
|
5149
|
+
p = fontBlob(url).then(
|
|
5150
|
+
(b) => new Promise((res, rej) => {
|
|
5151
|
+
const fr = new FileReader();
|
|
5152
|
+
fr.onload = () => res(String(fr.result));
|
|
5153
|
+
fr.onerror = () => rej(fr.error);
|
|
5154
|
+
fr.readAsDataURL(b);
|
|
5155
|
+
})
|
|
5156
|
+
);
|
|
5157
|
+
fontDataUrls.set(url, p);
|
|
5158
|
+
p.catch(() => fontDataUrls.delete(url));
|
|
4926
5159
|
}
|
|
4927
|
-
|
|
4928
|
-
|
|
5160
|
+
return p;
|
|
5161
|
+
}
|
|
5162
|
+
var faceUrl = (base, f) => new URL(`fonts/${f.file}`, base).href;
|
|
5163
|
+
async function payloadFor(faces, base, url) {
|
|
5164
|
+
return Promise.all(faces.map(async (f) => ({ family: f.family, weight: f.weight, italic: f.italic, range: f.range, url: await url(faceUrl(base, f)) })));
|
|
4929
5165
|
}
|
|
4930
5166
|
var fontCss = (fonts) => fonts.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(${f.url});}`).join("");
|
|
4931
5167
|
var ProgramWorker = class {
|
|
@@ -5022,7 +5258,14 @@ var MotionPlayer = class _MotionPlayer {
|
|
|
5022
5258
|
opts;
|
|
5023
5259
|
built;
|
|
5024
5260
|
worker = null;
|
|
5025
|
-
|
|
5261
|
+
env;
|
|
5262
|
+
/** Every face the package has (fonts/faces.json), and the files the worker has loaded. */
|
|
5263
|
+
all = [];
|
|
5264
|
+
loaded = /* @__PURE__ */ new Set();
|
|
5265
|
+
css = /* @__PURE__ */ new Map();
|
|
5266
|
+
widening = null;
|
|
5267
|
+
gone = false;
|
|
5268
|
+
lastT = 0;
|
|
5026
5269
|
raf = 0;
|
|
5027
5270
|
started = 0;
|
|
5028
5271
|
offset = 0;
|
|
@@ -5053,20 +5296,82 @@ var MotionPlayer = class _MotionPlayer {
|
|
|
5053
5296
|
get program() {
|
|
5054
5297
|
return this.built.program;
|
|
5055
5298
|
}
|
|
5299
|
+
get base() {
|
|
5300
|
+
return this.opts.base ?? CDN_BASE;
|
|
5301
|
+
}
|
|
5056
5302
|
async load() {
|
|
5057
|
-
this.worker
|
|
5058
|
-
|
|
5303
|
+
const old = this.worker;
|
|
5304
|
+
this.worker = null;
|
|
5305
|
+
old?.stop();
|
|
5306
|
+
await preloadGlyphs(this.item, this.opts, this.base);
|
|
5059
5307
|
this.built = buildProgram(this.item, this.opts);
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
const fonts = await fontsFor(this.built.program, JSON.stringify(this.built.props), this.opts.base ?? CDN_BASE);
|
|
5063
|
-
this.css = fontCss(fonts);
|
|
5064
|
-
this.worker = new ProgramWorker(this.built.program, env, fonts, this.built.codeOffset);
|
|
5308
|
+
this.env = programEnv(this.built.program, this.built.duration, this.opts.fps ?? 30, this.built.media);
|
|
5309
|
+
this.all = await facesAt(this.base);
|
|
5065
5310
|
this.lastSvg = "";
|
|
5066
5311
|
this.report(this.built.warnings);
|
|
5067
|
-
|
|
5312
|
+
let faces = facesFor(this.all, this.built.program.fonts.map((f) => parseFontSpec(f)), wideCodepoints(JSON.stringify(this.built.props)));
|
|
5313
|
+
let worker = await this.startWorker(faces);
|
|
5314
|
+
const have = new Set(faces.map((f) => f.file));
|
|
5315
|
+
const more = [];
|
|
5316
|
+
try {
|
|
5317
|
+
for (const k of [0.1, 0.35, 0.6, 0.85]) for (const f of this.facesOf(await worker.frame(this.built.duration * k))) if (!have.has(f.file) && have.add(f.file)) more.push(f);
|
|
5318
|
+
} catch {
|
|
5319
|
+
}
|
|
5320
|
+
if (more.length) {
|
|
5321
|
+
worker.stop();
|
|
5322
|
+
faces = [...faces, ...more];
|
|
5323
|
+
worker = await this.startWorker(faces);
|
|
5324
|
+
}
|
|
5325
|
+
if (this.gone) return worker.stop();
|
|
5326
|
+
this.worker = worker;
|
|
5327
|
+
this.loaded = have;
|
|
5068
5328
|
await this.draw(this.offset);
|
|
5069
5329
|
}
|
|
5330
|
+
async startWorker(faces) {
|
|
5331
|
+
const fonts = await payloadFor(faces, this.base, fontObjectUrl);
|
|
5332
|
+
const w = new ProgramWorker(this.built.program, this.env, fonts, this.built.codeOffset);
|
|
5333
|
+
await w.ready;
|
|
5334
|
+
return w;
|
|
5335
|
+
}
|
|
5336
|
+
/** The faces a frame draws with. */
|
|
5337
|
+
facesOf(f) {
|
|
5338
|
+
const svg = f.defs + f.svg;
|
|
5339
|
+
return facesFor(this.all, fontWants(svg), wideCodepoints(svg));
|
|
5340
|
+
}
|
|
5341
|
+
/** The @font-face rules for a set of faces, as data: URLs; one string per set, made once. */
|
|
5342
|
+
cssOf(faces) {
|
|
5343
|
+
const key = faces.map((f) => f.file).sort().join("|");
|
|
5344
|
+
let p = this.css.get(key);
|
|
5345
|
+
if (!p) {
|
|
5346
|
+
p = payloadFor(faces, this.base, fontDataUrl).then(fontCss);
|
|
5347
|
+
this.css.set(key, p);
|
|
5348
|
+
p.catch(() => this.css.delete(key));
|
|
5349
|
+
}
|
|
5350
|
+
return p;
|
|
5351
|
+
}
|
|
5352
|
+
/** A frame drew with a face the worker lacks (so measured without it): restart it with that face, then redraw. */
|
|
5353
|
+
widen(faces) {
|
|
5354
|
+
if (this.widening) return this.widening;
|
|
5355
|
+
const program = this.built.program;
|
|
5356
|
+
this.widening = (async () => {
|
|
5357
|
+
const files = new Set(this.loaded);
|
|
5358
|
+
for (const f of faces) files.add(f.file);
|
|
5359
|
+
try {
|
|
5360
|
+
const next = await this.startWorker(this.all.filter((f) => files.has(f.file)));
|
|
5361
|
+
const old = this.worker;
|
|
5362
|
+
if (this.gone || !old || this.built.program !== program) return next.stop();
|
|
5363
|
+
this.worker = next;
|
|
5364
|
+
this.loaded = files;
|
|
5365
|
+
setTimeout(() => old.stop(), 2500);
|
|
5366
|
+
this.lastSvg = "";
|
|
5367
|
+
if (!this.playing) await this.draw(this.lastT);
|
|
5368
|
+
} catch {
|
|
5369
|
+
}
|
|
5370
|
+
})().finally(() => {
|
|
5371
|
+
this.widening = null;
|
|
5372
|
+
});
|
|
5373
|
+
return this.widening;
|
|
5374
|
+
}
|
|
5070
5375
|
report(w) {
|
|
5071
5376
|
const key = w.join("\n");
|
|
5072
5377
|
if (key === this.warned) return;
|
|
@@ -5089,10 +5394,13 @@ var MotionPlayer = class _MotionPlayer {
|
|
|
5089
5394
|
const p = this.built.program;
|
|
5090
5395
|
const w = this.canvas.width || p.width;
|
|
5091
5396
|
const h = this.canvas.height || Math.round(w * p.height / p.width);
|
|
5397
|
+
this.lastT = t;
|
|
5092
5398
|
try {
|
|
5093
5399
|
const f = await this.worker.frame(Math.max(0, Math.min(this.duration, t)));
|
|
5094
5400
|
this.report([...this.built.warnings, ...f.warnings]);
|
|
5095
|
-
const
|
|
5401
|
+
const faces = this.facesOf(f);
|
|
5402
|
+
if (faces.some((x) => !this.loaded.has(x.file))) void this.widen(faces);
|
|
5403
|
+
const doc = frameDocument(p, f, await this.cssOf(faces), w, h, this.opts.background);
|
|
5096
5404
|
if (doc !== this.lastSvg) {
|
|
5097
5405
|
const img = await decode(doc);
|
|
5098
5406
|
const ctx = this.canvas.getContext("2d");
|
|
@@ -5140,6 +5448,7 @@ var MotionPlayer = class _MotionPlayer {
|
|
|
5140
5448
|
else await this.draw(this.offset);
|
|
5141
5449
|
}
|
|
5142
5450
|
destroy() {
|
|
5451
|
+
this.gone = true;
|
|
5143
5452
|
this.pause();
|
|
5144
5453
|
this.worker?.stop();
|
|
5145
5454
|
this.worker = null;
|