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.
Files changed (68) hide show
  1. package/README.md +162 -6
  2. package/dist/browser.d.ts +50 -1
  3. package/dist/browser.js +352 -43
  4. package/dist/chunks/{chunk-CFDFWZAC.js → chunk-CUQY6YI3.js} +1032 -404
  5. package/dist/chunks/chunk-PM3PLI3X.js +71 -0
  6. package/dist/chunks/chunk-QA4SEJCZ.js +601 -0
  7. package/dist/chunks/chunk-ZFG3X3DU.js +67 -0
  8. package/dist/chunks/chunk-ZHMWEZ4C.js +186 -0
  9. package/dist/chunks/init-JX2BYRKT.js +189 -0
  10. package/dist/chunks/review-RBH5EUZN.js +18 -0
  11. package/dist/cli.js +232 -32
  12. package/dist/index.d.ts +314 -2
  13. package/dist/index.js +75 -21
  14. package/dist/mcp.js +147 -22
  15. package/dist/player.d.ts +643 -0
  16. package/dist/player.js +6320 -0
  17. package/dist/registry.json +1 -1
  18. package/examples/player.html +93 -0
  19. package/examples/states.html +73 -0
  20. package/examples/volito-intro/DESIGN.md +23 -0
  21. package/examples/volito-intro/frame.js +187 -0
  22. package/examples/volito-intro/item.json +43 -0
  23. package/package.json +12 -4
  24. package/packs/annotate/emphasis/frame.js +8 -0
  25. package/packs/annotate/reactions/frame.js +12 -0
  26. package/packs/backdrops/gradient/frame.js +5 -0
  27. package/packs/effects/chromatic/frame.js +17 -0
  28. package/packs/effects/chromatic/item.json +15 -0
  29. package/packs/effects/glass/frame.js +44 -0
  30. package/packs/effects/glass/item.json +16 -0
  31. package/packs/effects/grain/frame.js +20 -0
  32. package/packs/effects/grain/item.json +16 -0
  33. package/packs/effects/light-leak/frame.js +24 -0
  34. package/packs/effects/light-leak/item.json +15 -0
  35. package/packs/effects/neon/frame.js +28 -0
  36. package/packs/effects/neon/item.json +15 -0
  37. package/packs/effects/pack.json +13 -0
  38. package/packs/effects/shared.js +23 -0
  39. package/packs/effects/warp/frame.js +13 -0
  40. package/packs/effects/warp/item.json +15 -0
  41. package/packs/essentials/wipe/frame.js +13 -0
  42. package/packs/transitions/glitch/frame.js +14 -0
  43. package/packs/transitions/shared.js +12 -0
  44. package/packs/type/word-by-word/frame.js +10 -0
  45. package/packs/ui/bell/frame.js +24 -0
  46. package/packs/ui/bell/item.json +21 -0
  47. package/packs/ui/button/frame.js +55 -0
  48. package/packs/ui/button/item.json +26 -0
  49. package/packs/ui/like/frame.js +34 -0
  50. package/packs/ui/like/item.json +21 -0
  51. package/packs/ui/loader/frame.js +34 -0
  52. package/packs/ui/loader/item.json +23 -0
  53. package/packs/ui/pack.json +13 -0
  54. package/packs/ui/shared.js +54 -0
  55. package/packs/ui/toggle/frame.js +30 -0
  56. package/packs/ui/toggle/item.json +22 -0
  57. package/{skill → skills/volito}/SKILL.md +43 -3
  58. package/skills/volito/references/art-direction.md +58 -0
  59. package/skills/volito/references/brand.md +59 -0
  60. package/skills/volito/references/delivery.md +40 -0
  61. package/skills/volito/references/matching-a-reference.md +39 -0
  62. package/skills/volito/references/motion-craft.md +56 -0
  63. package/skills/volito/references/self-review.md +44 -0
  64. package/skills/volito/references/type-and-layout.md +44 -0
  65. package/skills/volito/references/voice-and-timing.md +37 -0
  66. package/skills/volito/references/writing-items.md +44 -0
  67. /package/{skill/template → templates/item}/frame.js +0 -0
  68. /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.1.1";
4832
- var CDN_BASE = `https://cdn.jsdelivr.net/npm/${"volito"}@${"0.1.1"}/`;
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 = fetch(new URL(`fonts/${file}`, base).href).then((r) => r.ok ? r.arrayBuffer() : Promise.reject(new Error(`HTTP ${r.status}`))).then((buf) => {
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 faceData = /* @__PURE__ */ new Map();
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 dataUrl(url) {
4892
- let p = faceData.get(url);
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
- }).then((b) => new Promise((res, rej) => {
4898
- const fr = new FileReader();
4899
- fr.onload = () => res(String(fr.result));
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 covers(range, cp) {
4909
- if (!range) return true;
4910
- return range.split(",").some((part) => {
4911
- const m = /U\+([0-9A-F?]+)(?:-([0-9A-F]+))?/i.exec(part.trim());
4912
- if (!m) return false;
4913
- const lo = Number.parseInt(m[1].replace(/\?/g, "0"), 16);
4914
- const hi = m[2] ? Number.parseInt(m[2], 16) : Number.parseInt(m[1].replace(/\?/g, "F"), 16);
4915
- return cp >= lo && cp <= hi;
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
- async function fontsFor(program, text, base) {
4919
- const faces = await facesAt(base);
4920
- const fams = /* @__PURE__ */ new Set(["Inter", ...program.fonts.map((f) => f.split(":")[0])]);
4921
- const subsets = /* @__PURE__ */ new Set(["latin", "latin-ext"]);
4922
- for (const ch of text) {
4923
- const cp = ch.codePointAt(0);
4924
- if (cp < 592) continue;
4925
- for (const f of faces) if (covers(f.range, cp)) subsets.add(f.subset);
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
- const chosen = faces.filter((f) => fams.has(f.family) && subsets.has(f.subset));
4928
- return Promise.all(chosen.map(async (f) => ({ family: f.family, weight: f.weight, italic: f.italic, range: f.range, url: await dataUrl(new URL(`fonts/${f.file}`, base).href) })));
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
- css = "";
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?.stop();
5058
- await preloadGlyphs(this.item, this.opts, this.opts.base ?? CDN_BASE);
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
- const fps = this.opts.fps ?? 30;
5061
- const env = programEnv(this.built.program, this.built.duration, fps, this.built.media);
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
- await this.worker.ready;
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 doc = frameDocument(p, f, this.css, w, h, this.opts.background);
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;