@fabi-play/sdk 0.3.0 → 0.5.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.
@@ -1,4 +1,4 @@
1
- /*! @fabi-play/sdk 0.3.0 | Fabi Play Kit License | https://docs.fabiplay.app */
1
+ /*! @fabi-play/sdk 0.5.0 | Fabi Play Kit License | https://docs.fabiplay.app */
2
2
  (function (root, factory) {
3
3
  var script = typeof document !== 'undefined' ? document.currentScript : null;
4
4
  if (typeof define === 'function' && define.amd) define([], function () { return factory(script); });
@@ -229,6 +229,88 @@ function pointer(play, options = {}) {
229
229
  target.cursor(x, y, clicked ? 0 : progress);
230
230
  });
231
231
  }
232
+ function domCursor(doc = document) {
233
+ const win = doc.defaultView;
234
+ const Pointer = win?.PointerEvent;
235
+ const Mouse = win?.MouseEvent ?? MouseEvent;
236
+ let over = null;
237
+ let pressed = null;
238
+ const fire = (el, type, x, y, buttons, related = null) => {
239
+ const init = { bubbles: true, cancelable: true, composed: true, view: win, clientX: x, clientY: y, screenX: x, screenY: y, button: 0, buttons, relatedTarget: related, pointerId: 1, pointerType: "mouse", isPrimary: true };
240
+ if (type.startsWith("pointer")) {
241
+ if (Pointer) el.dispatchEvent(new Pointer(type, init));
242
+ } else el.dispatchEvent(new Mouse(type, init));
243
+ };
244
+ return {
245
+ // The viewport (the game's frame), not the document, which may be taller.
246
+ size: () => ({ width: win?.innerWidth ?? doc.documentElement.clientWidth, height: win?.innerHeight ?? doc.documentElement.clientHeight }),
247
+ dispatch(type, x, y, buttons) {
248
+ const el = doc.elementFromPoint(x, y) ?? doc.body ?? doc.documentElement;
249
+ if (type === "pointermove" && el !== over) {
250
+ if (over?.isConnected) for (const t of ["pointerout", "mouseout"]) fire(over, t, x, y, buttons, el);
251
+ for (const t of ["pointerover", "mouseover"]) fire(el, t, x, y, buttons, over);
252
+ over = el;
253
+ }
254
+ if (type === "pointerdown") pressed = el;
255
+ if (type === "click") {
256
+ const same = pressed === el;
257
+ pressed = null;
258
+ if (!same) return;
259
+ }
260
+ fire(el, type, x, y, buttons);
261
+ }
262
+ };
263
+ }
264
+ function cursorDriver(target) {
265
+ let last = null;
266
+ const send = (types, x, y, buttons) => {
267
+ const { width, height } = target.size();
268
+ for (const t of types) target.dispatch(t, x * width, y * height, buttons);
269
+ };
270
+ return {
271
+ update(c) {
272
+ const was = last?.down ?? false;
273
+ if (!last || last.x !== c.x || last.y !== c.y) send(["pointermove", "mousemove"], c.x, c.y, was ? 1 : 0);
274
+ if (c.down && !was) send(["pointerdown", "mousedown"], c.x, c.y, 1);
275
+ if (!c.down && was) send(["pointerup", "mouseup", "click"], c.x, c.y, 0);
276
+ last = { x: c.x, y: c.y, down: c.down };
277
+ },
278
+ cancel() {
279
+ if (!last?.down) return;
280
+ send(["pointercancel", "mouseup"], last.x, last.y, 0);
281
+ last = { ...last, down: false };
282
+ }
283
+ };
284
+ }
285
+
286
+ // ../protocol/src/picture.ts
287
+ var MAX_PICTURE_BYTES = 3 * 1024 * 1024;
288
+ var MAX_PICTURE_SIDE = 1920;
289
+ var PICTURE_JPEG_QUALITY = 0.85;
290
+ var MAX_PICTURE_TITLE = 60;
291
+ var PICTURE_RESULTS = ["sent", "unavailable", "too-big", "error"];
292
+ var BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
293
+ var MAGIC = { "image/jpeg": "/9j/", "image/png": "iVBORw0KGg" };
294
+ var PREFIX = /^data:(image\/(?:jpeg|png));base64,/;
295
+ var decodedSize = (b64) => b64.length / 4 * 3 - (b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0);
296
+ var MAX_BASE64 = Math.ceil(MAX_PICTURE_BYTES / 3) * 4;
297
+ var isImage = (mime, b64) => b64.length % 4 === 0 && b64.length > 0 && b64.startsWith(MAGIC[mime]) && BASE64.test(b64);
298
+ function checkPicture(v) {
299
+ if (typeof v !== "string") return { error: "invalid" };
300
+ const m = PREFIX.exec(v);
301
+ if (!m) return { error: "invalid" };
302
+ const mime = m[1];
303
+ const base64 = v.slice(m[0].length);
304
+ if (base64.length > MAX_BASE64) return { error: "too-big" };
305
+ if (!isImage(mime, base64)) return { error: "invalid" };
306
+ const size = decodedSize(base64);
307
+ return size > MAX_PICTURE_BYTES ? { error: "too-big" } : { ok: { mime, base64, size } };
308
+ }
309
+ function parsePictureTitle(v) {
310
+ if (typeof v !== "string") return void 0;
311
+ const t = Array.from(v.replace(/[\p{Cc}\p{Cf}]/gu, "").trim()).slice(0, MAX_PICTURE_TITLE).join("").trim();
312
+ return t || void 0;
313
+ }
232
314
 
233
315
  // ../protocol/src/index.ts
234
316
  var PROTOCOL_VERSION = 1;
@@ -285,6 +367,15 @@ function parseGameStats(v) {
285
367
  if (DEVICE_TIERS.includes(v.tier)) out.tier = v.tier;
286
368
  return out;
287
369
  }
370
+ function parseCursor(v) {
371
+ if (!isObject(v) || !isNumber(v.x) || !isNumber(v.y) || typeof v.down !== "boolean") return void 0;
372
+ const c = (n) => Math.min(Math.max(n, 0), 1);
373
+ return { type: "cursor", x: c(v.x), y: c(v.y), down: v.down };
374
+ }
375
+ function parsePictureResult(v) {
376
+ if (!isObject(v) || !PICTURE_RESULTS.includes(v.result)) return void 0;
377
+ return { type: "picture-result", ...Number.isInteger(v.id) ? { id: v.id } : {}, result: v.result };
378
+ }
288
379
  var isPlayerId = (id) => Number.isInteger(id) && id >= 1 && id <= MAX_PLAYERS;
289
380
  function parseTurnPlayers(v) {
290
381
  return Array.isArray(v) && v.length === 1 && isPlayerId(v[0]) ? [v[0]] : void 0;
@@ -293,6 +384,7 @@ function parseTurnOrder(v) {
293
384
  if (!Array.isArray(v) || v.length < 1 || v.length > MAX_PLAYERS || !v.every(isPlayerId) || new Set(v).size !== v.length) return void 0;
294
385
  return [...v];
295
386
  }
387
+ var COVER_ASPECT = 16 / 9;
296
388
  var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
297
389
  var isNumber = (v) => typeof v === "number" && Number.isFinite(v);
298
390
  var POSE_HEADER = 12;
@@ -415,6 +507,7 @@ var AVATARS = [
415
507
  id: "lion",
416
508
  name: { en: "Lion", it: "Leoncino" },
417
509
  band: "6-8",
510
+ alsoIn: ["3-5"],
418
511
  palette: { body: "#f6c454", limbs: "#f6c454", belly: "#fde7b0", accent: "#fde7b0", outline: INK },
419
512
  head: "round",
420
513
  ears: "round",
@@ -781,6 +874,39 @@ var AVATARS = [
781
874
  headExtras: ["hair", "domino"],
782
875
  bodyExtras: ["cape", "emblem", "boots"],
783
876
  extraColors: ["#ffd84a"]
877
+ },
878
+ // ── In both 3–5 and 6–8 (home 6–8, alsoIn 3–5: drawn with the proportions of the band shown) ──
879
+ {
880
+ // Next to the round 'dinosaur': a T-rex at a glance, a big head turned three-quarters with a long snout and a
881
+ // toothy grin, tiny arms, strong legs, a thick striped tail.
882
+ id: "trex",
883
+ name: { en: "T-Rex", it: "T-Rex" },
884
+ band: "6-8",
885
+ alsoIn: ["3-5"],
886
+ palette: { body: "#6cb84e", limbs: "#6cb84e", belly: "#f4e8a8", accent: "#f4e8a8", outline: INK },
887
+ head: "snout-long",
888
+ ears: "none",
889
+ face: { eyes: "dot", mouth: "toothy", muzzle: "none", cheeks: "#f59b8f" },
890
+ tail: "thick",
891
+ headExtras: ["stripes"],
892
+ bodyExtras: ["tiny-arms", "strong-legs"],
893
+ extraColors: ["#4a9235", "#58a23f"]
894
+ },
895
+ {
896
+ // An original child fairy: a bun with a flower crown, little pointed ears, a petal tunic, see-through wings and
897
+ // a star wand in one hand.
898
+ id: "fairy",
899
+ name: { en: "Fairy", it: "Fatina" },
900
+ band: "6-8",
901
+ alsoIn: ["3-5"],
902
+ palette: { body: "#b99af0", limbs: "#a8693f", belly: "#b99af0", accent: "#ffd84a", outline: INK, skin: "#a8693f", hair: "#2e1d17", hands: "#a8693f", feet: "#f58cbc" },
903
+ head: "round",
904
+ ears: "pixie",
905
+ face: { eyes: "dot", mouth: "smile", muzzle: "none", cheeks: "#f0857a" },
906
+ tail: "none",
907
+ headExtras: ["bun", "flower-crown"],
908
+ bodyExtras: ["wings-fairy", "petals", "wand"],
909
+ extraColors: ["#c4ecff", "#f7a8cf", "#fff3a0", "#ffffff"]
784
910
  }
785
911
  ];
786
912
 
@@ -1017,6 +1143,7 @@ function headShapes(def, ow) {
1017
1143
  if (has("locks")) {
1018
1144
  out.push({ k: "path", d: smoothClosed(pts(-1.14, 0.58, -1.24, -0.2, -0.86, -0.98, 0, -1.2, 0.86, -0.98, 1.24, -0.2, 1.14, 0.58, 0.8, 0.66, 0, 0.2, -0.8, 0.66)), fill: p.hair ?? p.outline, ...ol });
1019
1145
  }
1146
+ if (has("bun")) out.push({ k: "circle", x: 0.04, y: -1.14, r: 0.4, fill: p.hair ?? p.outline, ...ol });
1020
1147
  if (has("ninja-hood")) {
1021
1148
  for (const [rot, len2] of [
1022
1149
  [0.35, 0.5],
@@ -1054,7 +1181,15 @@ function headShapes(def, ow) {
1054
1181
  if (muzzle === "disc") for (const s of SIDES) out.push({ k: "circle", x: s * 0.4, y: 0.02, r: 0.44, fill: mc });
1055
1182
  if (muzzle === "cheeks") for (const s of SIDES) out.push({ k: "ellipse", x: s * 0.4, y: 0.44, rx: 0.44, ry: 0.32, fill: mc });
1056
1183
  if (muzzle === "band") out.push({ k: "path", d: lowerFace(def, ow), fill: mc });
1057
- if (has("stripes")) {
1184
+ if (has("stripes") && def.head === "snout-long") {
1185
+ const c = extra(0, p.outline);
1186
+ for (const [x0, y0, x1, y1] of [
1187
+ [0.46, -0.94, 0.36, -0.66],
1188
+ [0.8, -0.72, 0.62, -0.5],
1189
+ [1, -0.36, 0.78, -0.24]
1190
+ ])
1191
+ out.push({ k: "line", pts: [x0, y0, x1, y1], stroke: c, lw: 0.11 });
1192
+ } else if (has("stripes")) {
1058
1193
  const c = extra(0, p.outline);
1059
1194
  out.push({ k: "line", pts: [0, -0.97, 0, -0.66], stroke: c, lw: 0.13 });
1060
1195
  for (const s of SIDES) {
@@ -1069,6 +1204,13 @@ function headShapes(def, ow) {
1069
1204
  out.push({ k: "circle", x: 0.58, y: -0.42, r: 0.11, fill: c });
1070
1205
  out.push({ k: "circle", x: 0.22, y: -0.76, r: 0.09, fill: c });
1071
1206
  }
1207
+ if (def.head === "snout-long") {
1208
+ for (const [x, y, r] of [
1209
+ [-1.38, -0.14, 0.06],
1210
+ [-1.14, -0.22, 0.055]
1211
+ ])
1212
+ out.push({ k: "ellipse", x, y, rx: r, ry: r * 0.8, rot: -0.3, fill: p.outline, alpha: 0.85 });
1213
+ }
1072
1214
  if (has("forehead")) out.push({ k: "path", d: smoothClosed(pts(-0.62, -0.66, 0, -0.86, 0.62, -0.66, 0.26, -0.36, 0, -0.1, -0.26, -0.36), 0.9), fill: extra(0, p.accent) });
1073
1215
  if (has("eye-spot")) out.push({ k: "ellipse", x: 0.42, y: -0.06, rx: 0.3, ry: 0.33, rot: 0.3, fill: p.ears ?? p.accent });
1074
1216
  if (has("gills")) {
@@ -1077,6 +1219,7 @@ function headShapes(def, ow) {
1077
1219
  const hair = p.hair ?? p.outline;
1078
1220
  if (has("hair")) out.push({ k: "path", d: smoothClosed(pts(-1.02, -0.06, -1.02, -0.58, -0.62, -1.04, 0.08, -1.14, 0.76, -0.92, 1.05, -0.42, 1, -0.06, 0.84, -0.4, 0.42, -0.5, 0.06, -0.4, -0.34, -0.56, -0.74, -0.38)), fill: hair, ...ol });
1079
1221
  if (has("locks")) out.push({ k: "path", d: smoothClosed(pts(-1, -0.08, -0.98, -0.62, -0.56, -1.02, 0, -1.1, 0.56, -1.02, 0.98, -0.62, 1, -0.08, 0.8, -0.38, 0.36, -0.44, 0, -0.4, -0.36, -0.44, -0.8, -0.38)), fill: hair, ...ol });
1222
+ if (has("bun")) out.push({ k: "path", d: smoothClosed(pts(-1.02, -0.1, -0.98, -0.62, -0.56, -1.02, 0, -1.1, 0.56, -1.02, 0.98, -0.62, 1.02, -0.1, 0.84, -0.36, 0.5, -0.56, 0.12, -0.48, -0.22, -0.6, -0.6, -0.5, -0.86, -0.36)), fill: hair, ...ol });
1080
1223
  if (has("curls")) {
1081
1224
  for (let i = 0; i < 7; i++) {
1082
1225
  const c = polar(-1.3 + i / 6 * 2.6, 0.86);
@@ -1098,7 +1241,7 @@ function headShapes(def, ow) {
1098
1241
  }
1099
1242
  if (def.face.brows) for (const s of SIDES) out.push({ k: "line", pts: [s * 0.2, -0.2, s * 0.54, -0.28], stroke: def.face.brows, lw: 0.09 });
1100
1243
  out.push(...mouth(def, ow));
1101
- if (def.face.cheeks) for (const s of SIDES) out.push({ k: "ellipse", x: s * 0.64, y: 0.3, rx: 0.15, ry: 0.1, fill: def.face.cheeks, alpha: 0.8 });
1244
+ if (def.face.cheeks) for (const c of cheekSpots(def)) out.push({ k: "ellipse", x: c.x, y: c.y, rx: 0.15, ry: 0.1, fill: def.face.cheeks, alpha: 0.8 });
1102
1245
  if (has("whiskers")) {
1103
1246
  for (const s of SIDES)
1104
1247
  for (const dy of [-0.1, 0.06]) out.push({ k: "line", pts: [s * 0.62, 0.38 + dy, s * 1.06, 0.3 + dy * 2.2], stroke: p.outline, lw: ow * 0.7, alpha: 0.75 });
@@ -1120,6 +1263,24 @@ function headShapes(def, ow) {
1120
1263
  out.push({ k: "line", pts: [-w, y + 0.04, w, y - 0.04], stroke: p.outline, lw: ow * 0.7, alpha: 0.6 });
1121
1264
  }
1122
1265
  }
1266
+ if (has("flower-crown")) {
1267
+ const cols = [extra(1, "#f7a8cf"), extra(2, "#fff3a0"), extra(3, WHITE)];
1268
+ for (const a of [-0.84, -0.28, 0.28, 0.84]) {
1269
+ const c = polar(a, 0.9);
1270
+ out.push({ k: "ellipse", x: c.x, y: c.y, rx: 0.14, ry: 0.07, rot: a + 0.5, fill: "#7cc76a", stroke: p.outline, lw: ow * 0.7 });
1271
+ }
1272
+ ;
1273
+ [-1.12, -0.56, 0, 0.56, 1.12].forEach((a, i) => {
1274
+ const c = polar(a, 0.9);
1275
+ const r = i === 2 ? 0.13 : 0.11;
1276
+ for (let k = 0; k < 5; k++) {
1277
+ const t = a + k / 5 * Math.PI * 2;
1278
+ out.push({ k: "circle", x: c.x + Math.sin(t) * r, y: c.y - Math.cos(t) * r, r: r * 0.82, fill: cols[i % cols.length], stroke: p.outline, lw: ow * 0.7 });
1279
+ }
1280
+ out.push({ k: "circle", x: c.x, y: c.y, r: r * 0.62, fill: cols[i % cols.length] });
1281
+ out.push({ k: "circle", x: c.x, y: c.y, r: r * 0.42, fill: p.accent });
1282
+ });
1283
+ }
1123
1284
  if (has("helmet")) {
1124
1285
  for (const s of SIDES) out.push({ k: "rect", x: s * 1, y: 0.05, w: 0.26, h: 0.56, r: 0.12, fill: p.accent, ...ol });
1125
1286
  out.push({ k: "path", d: "M-0.62 -0.62 Q-0.3 -0.92 0.12 -0.9", stroke: WHITE, lw: 0.1, alpha: 0.9 });
@@ -1184,6 +1345,8 @@ function headShape(def, ol) {
1184
1345
  return { k: "rect", x: 0, y: 0, w: 2, h: 1.8, r: 0.46, fill, ...ol };
1185
1346
  case "bucket":
1186
1347
  return { k: "path", d: roundedPolygon(pts(-0.94, 0.96, -0.98, -0.36, -0.62, -1.04, 0.62, -1.04, 0.98, -0.36, 0.94, 0.96), 0.42), fill, ...ol };
1348
+ case "snout-long":
1349
+ return { k: "path", d: smoothClosed(SNOUT_HEAD, 0.95), fill, ...ol };
1187
1350
  case "bulb":
1188
1351
  return {
1189
1352
  k: "path",
@@ -1202,6 +1365,19 @@ function headShape(def, ol) {
1202
1365
  };
1203
1366
  }
1204
1367
  }
1368
+ var SNOUT_HEAD = pts(0.7, -0.88, 0.12, -1.04, -0.3, -0.98, -0.72, -0.74, -1.2, -0.56, -1.5, -0.32, -1.6, 0.04, -1.44, 0.42, -0.9, 0.72, -0.2, 0.92, 0.5, 0.88, 0.96, 0.56, 1.08, 0, 0.98, -0.5);
1369
+ function eyeSpots(def, ex, ey) {
1370
+ if (def.head === "snout-long")
1371
+ return [
1372
+ { x: -0.36, y: -0.5, k: 0.95 },
1373
+ { x: 0.26, y: -0.48, k: 1.1 }
1374
+ ];
1375
+ return SIDES.map((s) => ({ x: s * ex, y: ey, k: 1 }));
1376
+ }
1377
+ function cheekSpots(def) {
1378
+ if (def.head === "snout-long") return pts(-0.66, -0.14, 0.66, 0.1);
1379
+ return SIDES.map((s) => ({ x: s * 0.64, y: 0.3 }));
1380
+ }
1205
1381
  function headEllipse(def) {
1206
1382
  if (def.head === "round") return { cy: 0, rx: 1, ry: 1 };
1207
1383
  if (def.head === "tall") return { cy: -0.02, rx: 0.94, ry: 1.04 };
@@ -1282,6 +1458,10 @@ function ears(def, ow) {
1282
1458
  out.push({ k: "ellipse", x: s * 1.04, y: -0.22, rx: 0.4, ry: 0.18, rot: s * 0.32, fill: p.skin ?? p.body, ...ol });
1283
1459
  out.push({ k: "ellipse", x: s * 1.08, y: -0.2, rx: 0.24, ry: 0.08, rot: s * 0.32, fill: p.accent });
1284
1460
  break;
1461
+ case "pixie":
1462
+ out.push({ k: "path", d: roundedPolygon(pts(s * 0.86, -0.22, s * 1.36, -0.5, s * 1.02, 0.26), 0.28), fill: p.skin ?? p.body, ...ol });
1463
+ out.push({ k: "line", pts: [s * 0.98, -0.12, s * 1.16, -0.3], stroke: p.accent, lw: ow * 0.8, alpha: 0.5 });
1464
+ break;
1285
1465
  case "human":
1286
1466
  out.push({ k: "ellipse", x: s * 0.98, y: 0.06, rx: 0.17, ry: 0.23, fill: p.skin ?? p.body, ...ol });
1287
1467
  break;
@@ -1314,15 +1494,15 @@ function eyes(def, ow) {
1314
1494
  const shine = (x, y, r) => ({ k: "circle", x, y, r, fill: WHITE });
1315
1495
  switch (f2.eyes) {
1316
1496
  case "dot":
1317
- for (const s of SIDES) {
1318
- out.push({ k: "ellipse", x: s * ex, y: ey, rx: 0.12, ry: 0.155, fill: ink });
1319
- out.push(shine(s * ex + 0.04, ey - 0.06, 0.045));
1497
+ for (const e of eyeSpots(def, ex, ey)) {
1498
+ out.push({ k: "ellipse", x: e.x, y: e.y, rx: 0.12 * e.k, ry: 0.155 * e.k, fill: ink });
1499
+ out.push(shine(e.x + 0.04 * e.k, e.y - 0.06 * e.k, 0.045 * e.k));
1320
1500
  }
1321
1501
  break;
1322
1502
  case "bead":
1323
- for (const s of SIDES) {
1324
- out.push({ k: "ellipse", x: s * ex, y: ey, rx: 0.16, ry: 0.22, fill: ink });
1325
- out.push(shine(s * ex + 0.05, ey - 0.08, 0.065));
1503
+ for (const e of eyeSpots(def, ex, ey)) {
1504
+ out.push({ k: "ellipse", x: e.x, y: e.y, rx: 0.16 * e.k, ry: 0.22 * e.k, fill: ink });
1505
+ out.push(shine(e.x + 0.05 * e.k, e.y - 0.08 * e.k, 0.065 * e.k));
1326
1506
  }
1327
1507
  break;
1328
1508
  case "big":
@@ -1431,6 +1611,21 @@ function mouth(def, ow) {
1431
1611
  out.push({ k: "ellipse", x: 0, y: 0.48, rx: 0.16, ry: 0.11, fill: MOUTH });
1432
1612
  out.push({ k: "path", d: "M-0.3 0.12 Q0 -0.02 0.3 0.12 Q0.36 0.52 0.04 0.76 Q0.08 0.52 -0.08 0.46 Q-0.28 0.38 -0.3 0.12 Z", fill: p.accent, stroke: p.outline, lw: ow * 0.8 });
1433
1613
  break;
1614
+ case "toothy": {
1615
+ out.push({ k: "path", d: "M-1.4 0.1 Q-0.5 0.36 0.5 0.2 Q0.64 0.16 0.68 0.02 Q0.78 0.28 0.52 0.38 Q-0.4 0.78 -1.3 0.26 Z", fill: dark, stroke: dark, lw: ow * 0.6 });
1616
+ out.push({ k: "ellipse", x: -0.38, y: 0.5, rx: 0.24, ry: 0.08, fill: TONGUE });
1617
+ for (const t of [0.12, 0.27, 0.42, 0.57, 0.72, 0.86]) {
1618
+ const x = (1 - t) * (1 - t) * -1.4 + 2 * (1 - t) * t * -0.5 + t * t * 0.5;
1619
+ const y = (1 - t) * (1 - t) * 0.1 + 2 * (1 - t) * t * 0.36 + t * t * 0.2 - 0.01;
1620
+ out.push({ k: "path", d: roundedPolygon(pts(x - 0.08, y, x + 0.08, y, x, y + 0.14), 0.3), fill: WHITE });
1621
+ }
1622
+ for (const t of [0.3, 0.7]) {
1623
+ const x = (1 - t) * (1 - t) * 0.52 + 2 * (1 - t) * t * -0.4 + t * t * -1.3;
1624
+ const y = (1 - t) * (1 - t) * 0.38 + 2 * (1 - t) * t * 0.78 + t * t * 0.26 + 0.01;
1625
+ out.push({ k: "path", d: roundedPolygon(pts(x - 0.07, y, x + 0.07, y, x, y - 0.12), 0.3), fill: WHITE });
1626
+ }
1627
+ break;
1628
+ }
1434
1629
  case "jaws": {
1435
1630
  out.push({ k: "path", d: "M-0.5 0.3 Q0 0.5 0.5 0.3 Q0.38 0.72 0 0.72 Q-0.38 0.72 -0.5 0.3 Z", fill: dark, stroke: dark, lw: ow * 0.5 });
1436
1631
  for (const x of [-0.34, -0.17, 0, 0.17, 0.34]) {
@@ -1453,7 +1648,10 @@ var BAND_BODY = {
1453
1648
  "9-12": { head: 0.5, neck: 0.92, arm: 0.17, leg: 0.21, hand: 0.125, foot: 0.9, shoulderMin: 0.36, shoulderPad: 0.07, hipMin: 0.32, hipPad: 0.08, bottom: 0.1, bulge: 1, armsOverHead: false }
1454
1649
  };
1455
1650
  var DEFAULT_BAND = "6-8";
1456
- var bodyBand = (def) => def.band in BAND_BODY ? def.band : DEFAULT_BAND;
1651
+ var bodyBand = (def, shown) => {
1652
+ if (shown !== void 0 && def.band !== "all" && def.alsoIn?.includes(shown) && shown in BAND_BODY) return shown;
1653
+ return def.band in BAND_BODY ? def.band : DEFAULT_BAND;
1654
+ };
1457
1655
  var HEAD = BAND_BODY[DEFAULT_BAND].head;
1458
1656
  var MAX_TILT = 0.3;
1459
1657
  var TILT_DEAD_ZONE = 0.12;
@@ -1547,7 +1745,7 @@ function headTilt(body, at, angle, U) {
1547
1745
  return 0;
1548
1746
  }
1549
1747
  function avatarShapes(body, def, box, opts = {}) {
1550
- const band = bodyBand(def);
1748
+ const band = bodyBand(def, opts.band);
1551
1749
  const bb = BAND_BODY[band];
1552
1750
  const F = frameOf(body, box, { ...opts, band });
1553
1751
  if (!F) return [];
@@ -1579,8 +1777,8 @@ function avatarShapes(body, def, box, opts = {}) {
1579
1777
  out.push({ k: "line", pts: flat, stroke: p.outline, lw: w + ow * 2 });
1580
1778
  out.push({ k: "line", pts: flat, stroke: color, lw: w });
1581
1779
  };
1582
- const legW = bb.leg * U;
1583
- const armW = bb.arm * U;
1780
+ const legW = bb.leg * U * (has("strong-legs") ? 1.3 : 1);
1781
+ const armW = bb.arm * U * (has("tiny-arms") ? 0.6 : 1);
1584
1782
  const pad = Math.max(bb.arm / 0.22, 0.9);
1585
1783
  if (F.hipsOk) {
1586
1784
  for (const [hip, kneeI, ankleI, side] of [
@@ -1594,7 +1792,7 @@ function avatarShapes(body, def, box, opts = {}) {
1594
1792
  const pts2 = ankle.ok ? [top, knee, ankle] : [top, knee];
1595
1793
  limb(legs, pts2, legW, p.limbs);
1596
1794
  if (has("joints")) legs.push({ k: "circle", x: knee.x, y: knee.y, r: 0.15 * U * pad, fill: p.accent, ...ol });
1597
- if (ankle.ok) legs.push(foot(def, ankle, F, side, ow, bb.foot));
1795
+ if (ankle.ok) legs.push(...feet(def, ankle, F, side, ow, bb.foot));
1598
1796
  }
1599
1797
  }
1600
1798
  for (const [sh, elbowI, wristI, side] of [
@@ -1627,7 +1825,20 @@ function avatarShapes(body, def, box, opts = {}) {
1627
1825
  out.push({ k: "ellipse", x: q.x, y: q.y, rx: 0.2 * U, ry: 0.09 * U, rot: Math.atan2(fd.y, fd.x), fill: p.hands ?? p.accent, ...ol });
1628
1826
  }
1629
1827
  }
1630
- out.push({ k: "circle", x: hand.x, y: hand.y, r: bb.hand * U, fill: p.hands ?? p.limbs, ...ol });
1828
+ if (has("wand") && side === 1 && !opts.portrait) out.push(...wand(def, hand, norm(add(mul(dir, 0.4), sub(mul(r, 0.6), d))), U, ow));
1829
+ const handR = bb.hand * U * (has("tiny-arms") ? 0.72 : 1);
1830
+ if (has("tiny-arms")) {
1831
+ for (const a of [-0.45, 0.45]) {
1832
+ const c = Math.cos(a);
1833
+ const s = Math.sin(a);
1834
+ const cd = { x: dir.x * c - dir.y * s, y: dir.x * s + dir.y * c };
1835
+ const base = add(hand, mul(cd, handR * 0.75));
1836
+ const tip = add(hand, mul(cd, handR * 1.6));
1837
+ const nrm = { x: -cd.y * handR * 0.32, y: cd.x * handR * 0.32 };
1838
+ out.push({ k: "path", d: roundedPolygon([add(base, nrm), tip, sub(base, nrm)], 0.2), fill: p.belly, ...ol });
1839
+ }
1840
+ }
1841
+ out.push({ k: "circle", x: hand.x, y: hand.y, r: handR, fill: p.hands ?? p.limbs, ...ol });
1631
1842
  }
1632
1843
  if (has("wings")) {
1633
1844
  for (const s of [-1, 1]) {
@@ -1642,6 +1853,7 @@ function avatarShapes(body, def, box, opts = {}) {
1642
1853
  back.push({ k: "path", d: roundedPolygon(w, 0.18), fill: p.accent, ...ol });
1643
1854
  }
1644
1855
  }
1856
+ if (has("wings-fairy")) back.push(...fairyWings(def, T, ht, U, ow));
1645
1857
  if (has("cape")) {
1646
1858
  const yb = L + 0.8 * U;
1647
1859
  const wb = hb + 0.36 * U;
@@ -1859,6 +2071,7 @@ function avatarShapes(body, def, box, opts = {}) {
1859
2071
  const k = 0.15 * U;
1860
2072
  torso.push({ k: "group", x: c.x, y: c.y, rot: F.angle - 0.2, s: k, items: [{ k: "path", d: STAR, fill: def.extraColors?.[0] ?? "#ffd84a", stroke: p.outline, lw: ow * 0.8 / k }] });
1861
2073
  }
2074
+ if (has("petals")) torso.push(...petals(def, T, ht, hb, bulge, L, y1, U, ow, bb));
1862
2075
  if (has("scarf")) {
1863
2076
  const y = Math.max(0.04, bb.head * (1 - bb.neck) - 0.06) * U;
1864
2077
  const c = T(0, y);
@@ -1883,6 +2096,18 @@ function cachedHead(def, ow) {
1883
2096
  }
1884
2097
  return shapes;
1885
2098
  }
2099
+ function feet(def, ankle, F, side, ow, k) {
2100
+ const shoe = foot(def, ankle, F, side, ow, k);
2101
+ if (!def.bodyExtras.includes("strong-legs")) return [shoe];
2102
+ const U = F.U * k;
2103
+ const c = add(add(ankle, mul(F.d, 0.07 * U)), mul(F.r, side * 0.06 * U));
2104
+ const out = [shoe];
2105
+ for (const t of [-1, 0, 1]) {
2106
+ const q = add(add(c, mul(F.r, t * 0.12 * U)), mul(F.d, 0.09 * U));
2107
+ out.push({ k: "ellipse", x: q.x, y: q.y, rx: 0.05 * U, ry: 0.045 * U, rot: F.angle, fill: def.palette.belly, stroke: def.palette.outline, lw: ow * 0.8 });
2108
+ }
2109
+ return out;
2110
+ }
1886
2111
  function foot(def, ankle, F, side, ow, k) {
1887
2112
  const p = def.palette;
1888
2113
  const { d, r } = F;
@@ -2036,6 +2261,30 @@ function tail(def, F, hb, ow) {
2036
2261
  });
2037
2262
  break;
2038
2263
  }
2264
+ case "thick": {
2265
+ const pts2 = TP([
2266
+ [hb * 0.3, L * 0.42],
2267
+ [hb + 0.42 * U, L * 0.62],
2268
+ [hb + 0.92 * U, L * 0.96],
2269
+ [hb + 1.32 * U, L * 1.08],
2270
+ [hb + 1.44 * U, L * 0.98],
2271
+ [hb + 0.98 * U, L * 1.2],
2272
+ [hb + 0.4 * U, L * 1.22],
2273
+ [hb * 0.3, L * 1.12]
2274
+ ]);
2275
+ out.push({ k: "path", d: smoothClosed(pts2, 0.8), fill: p.body, ...ol });
2276
+ const c = def.extraColors?.[0] ?? p.accent;
2277
+ for (const [x, y, l] of [
2278
+ [0.28, 0.58, 0.26],
2279
+ [0.62, 0.8, 0.22],
2280
+ [0.96, 1, 0.16]
2281
+ ]) {
2282
+ const a = T(hb + x * U, L * y + 0.03 * U);
2283
+ const b = T(hb + (x - 0.1) * U, L * y + l * U);
2284
+ out.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: c, lw: 0.09 * U });
2285
+ }
2286
+ break;
2287
+ }
2039
2288
  case "spiky":
2040
2289
  case "arrow": {
2041
2290
  const pts2 = TP([
@@ -2068,6 +2317,81 @@ function tail(def, F, hb, ow) {
2068
2317
  }
2069
2318
  return out;
2070
2319
  }
2320
+ function wand(def, hand, wd, U, ow) {
2321
+ const p = def.palette;
2322
+ const from = add(hand, mul(wd, -0.08 * U));
2323
+ const tip = add(hand, mul(wd, 0.52 * U));
2324
+ const k = 0.2 * U;
2325
+ return [
2326
+ { k: "line", pts: [from.x, from.y, tip.x, tip.y], stroke: p.outline, lw: 0.06 * U + ow * 2 },
2327
+ { k: "line", pts: [from.x, from.y, tip.x, tip.y], stroke: "#fff3d6", lw: 0.06 * U },
2328
+ { k: "circle", x: tip.x, y: tip.y, r: 0.24 * U, fill: p.accent, alpha: 0.28 },
2329
+ { k: "group", x: tip.x, y: tip.y, rot: Math.atan2(wd.y, wd.x) + Math.PI / 2, s: k, items: [{ k: "path", d: STAR, fill: p.accent, stroke: p.outline, lw: ow * 0.9 / k }] }
2330
+ ];
2331
+ }
2332
+ function fairyWings(def, T, ht, U, ow) {
2333
+ const p = def.palette;
2334
+ const fill = def.extraColors?.[0] ?? "#cdeeff";
2335
+ const out = [];
2336
+ for (const s of [-1, 1]) {
2337
+ const lobes = [
2338
+ // Upper: big, up and out.
2339
+ [
2340
+ [s * ht * 0.3, 0.12 * U],
2341
+ [s * (ht + 0.42 * U), -0.66 * U],
2342
+ [s * (ht + 1 * U), -0.74 * U],
2343
+ [s * (ht + 1.02 * U), -0.18 * U],
2344
+ [s * (ht + 0.5 * U), 0.26 * U]
2345
+ ],
2346
+ // Lower: smaller, down and out.
2347
+ [
2348
+ [s * ht * 0.4, 0.3 * U],
2349
+ [s * (ht + 0.6 * U), 0.36 * U],
2350
+ [s * (ht + 0.74 * U), 0.78 * U],
2351
+ [s * (ht + 0.3 * U), 0.9 * U]
2352
+ ]
2353
+ ];
2354
+ for (const lobe of lobes) {
2355
+ const d = smoothClosed(lobe.map(([x, y]) => T(x, y)), 0.9);
2356
+ out.push({ k: "path", d, fill, alpha: 0.6 });
2357
+ out.push({ k: "path", d, stroke: p.outline, lw: ow * 0.9 });
2358
+ const inner = lobe[Math.floor(lobe.length / 2)];
2359
+ const a = T(lobe[0][0], lobe[0][1]);
2360
+ const b = T(lobe[0][0] + (inner[0] - lobe[0][0]) * 0.75, lobe[0][1] + (inner[1] - lobe[0][1]) * 0.75);
2361
+ out.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: "#ffffff", lw: ow * 0.9, alpha: 0.85 });
2362
+ }
2363
+ for (const [x, y, r] of [
2364
+ [ht + 0.72 * U, -0.42 * U, 0.075 * U],
2365
+ [ht + 0.44 * U, -0.16 * U, 0.05 * U],
2366
+ [ht + 0.5 * U, 0.6 * U, 0.055 * U]
2367
+ ]) {
2368
+ const q = T(s * x, y);
2369
+ out.push({ k: "group", x: q.x, y: q.y, s: r, items: [{ k: "path", d: STAR, fill: "#ffffff" }] });
2370
+ }
2371
+ }
2372
+ return out;
2373
+ }
2374
+ function petals(def, T, ht, hb, bulge, L, y1, U, ow, bb) {
2375
+ const p = def.palette;
2376
+ const second = def.extraColors?.[1] ?? p.belly;
2377
+ const out = [];
2378
+ const ang = Math.atan2(T(1, 0).y - T(0, 0).y, T(1, 0).x - T(0, 0).x);
2379
+ const n = 5;
2380
+ const w = Math.max(hb, bulge * 0.92) * 1.08;
2381
+ const y = (L * 0.8 + y1) / 2;
2382
+ const order = [0, 4, 1, 3, 2];
2383
+ for (const i of order) {
2384
+ const t = i / (n - 1) - 0.5;
2385
+ const c = T(t * 2 * w, y + (0.5 - Math.abs(t)) * 0.1 * U);
2386
+ out.push({ k: "ellipse", x: c.x, y: c.y, rx: w * 0.3, ry: 0.34 * U, rot: ang - t * 0.6, fill: i % 2 ? second : p.body, stroke: p.outline, lw: ow });
2387
+ }
2388
+ const cy = Math.max(0.04, bb.head * (1 - bb.neck) - 0.04) * U;
2389
+ for (const t of [-1, 1, 0]) {
2390
+ const c = T(t * ht * 0.32, cy + (t ? 0 : 0.05 * U));
2391
+ out.push({ k: "ellipse", x: c.x, y: c.y, rx: ht * 0.26, ry: 0.12 * U, rot: ang + t * 0.5, fill: t ? second : p.body, stroke: p.outline, lw: ow * 0.9 });
2392
+ }
2393
+ return out;
2394
+ }
2071
2395
 
2072
2396
  // ../avatars/src/poses.ts
2073
2397
  var STAND = {
@@ -2211,7 +2535,7 @@ var isAvatarId = (v) => typeof v === "string" && BY_ID.has(v);
2211
2535
  var avatarById = (id) => BY_ID.get(id);
2212
2536
  var resolve = (def) => typeof def === "string" ? BY_ID.get(def) ?? DEFAULT_AVATAR : def;
2213
2537
  function drawAvatar(ctx, body, def, box, opts = {}) {
2214
- const shapes = avatarShapes(body, resolve(def), box, { mirrored: opts.mirrored });
2538
+ const shapes = avatarShapes(body, resolve(def), box, { mirrored: opts.mirrored, band: opts.band });
2215
2539
  if (!shapes.length) return false;
2216
2540
  ctx.save();
2217
2541
  if (opts.alpha !== void 0) ctx.globalAlpha *= Math.min(Math.max(opts.alpha, 0), 1);
@@ -2219,17 +2543,19 @@ function drawAvatar(ctx, body, def, box, opts = {}) {
2219
2543
  ctx.restore();
2220
2544
  return true;
2221
2545
  }
2222
- function iconShapes(def, size) {
2546
+ function iconShapes(def, size, band) {
2223
2547
  const d = resolve(def);
2224
2548
  const body = neutralPose();
2225
- const band = bodyBand(d);
2226
- const unit = frameOf(body, { x: 0, y: 0, w: 1, h: 1 }, { band });
2227
- const top = -bounds(headShapes(d, 0.05)).minY;
2549
+ const shown = bodyBand(d, band);
2550
+ const unit = frameOf(body, { x: 0, y: 0, w: 1, h: 1 }, { band: shown });
2551
+ const hb = bounds(headShapes(d, 0.05));
2552
+ const top = -hb.minY;
2228
2553
  const headR = Math.min(0.27, 0.43 / Math.max(top, 1)) * size;
2229
- const k = headR / BAND_BODY[band].head / unit.U;
2554
+ const dx = d.head === "snout-long" ? (hb.minX + hb.maxX) / 2 * headR : 0;
2555
+ const k = headR / BAND_BODY[shown].head / unit.U;
2230
2556
  const cy = 0.5 * size + Math.min(0, (top - 1.6) * 0.06 * size);
2231
- const box = { x: size / 2 - unit.head.x * k, y: cy - unit.head.y * k, w: k, h: k };
2232
- return avatarShapes(body, d, box, { portrait: true });
2557
+ const box = { x: size / 2 - dx - unit.head.x * k, y: cy - unit.head.y * k, w: k, h: k };
2558
+ return avatarShapes(body, d, box, { portrait: true, band: shown });
2233
2559
  }
2234
2560
  function smoothBody(prev, next, amount = 0.5) {
2235
2561
  if (!prev || prev.length !== next.length) return next.slice();
@@ -2459,6 +2785,7 @@ function easy(connecting) {
2459
2785
  p.on("break-done", (m) => emit("break-end", { shown: m.shown }));
2460
2786
  p.onBackdrop((backdrop, view) => emit("backdrop", { backdrop, view }));
2461
2787
  p.on("turn", (m) => emit("turn", { players: [...m.players] }));
2788
+ p.on("capabilities", (m) => emit("capabilities", { pictures: m.pictures }));
2462
2789
  p.on("frame", (f2) => {
2463
2790
  emit("players", { players: f2.players });
2464
2791
  for (const pl of f2.players) {
@@ -2505,6 +2832,13 @@ function easy(connecting) {
2505
2832
  get hasPointer() {
2506
2833
  return play?.pointer ?? false;
2507
2834
  },
2835
+ get canSavePicture() {
2836
+ return play?.canSavePicture ?? false;
2837
+ },
2838
+ savePicture: (source, opts) => new Promise((resolve2) => withPlay((p) => void p.savePicture(source, opts).then(resolve2))),
2839
+ get muted() {
2840
+ return play?.muted ?? false;
2841
+ },
2508
2842
  get view() {
2509
2843
  return play?.view ?? null;
2510
2844
  },
@@ -2519,7 +2853,7 @@ function easy(connecting) {
2519
2853
  };
2520
2854
  }
2521
2855
  var none = Object.freeze([]);
2522
- var startInfo = (p) => ({ ageRange: p.ageRange, players: p.players, locale: p.locale, mode: p.mode, backdrop: p.backdrop, view: p.view, device: p.device, turns: p.turns, input: p.input, pointer: p.pointer });
2856
+ var startInfo = (p) => ({ ageRange: p.ageRange, players: p.players, locale: p.locale, mode: p.mode, orientation: p.orientation, backdrop: p.backdrop, view: p.view, device: p.device, turns: p.turns, input: p.input, pointer: p.pointer, canSavePicture: p.canSavePicture, muted: p.muted });
2523
2857
  var playingNow = (p) => p.input === "remote" ? p.players[0] ?? 1 : 1;
2524
2858
  function endOf(p, score, last) {
2525
2859
  const now2 = playingNow(p);
@@ -2528,6 +2862,46 @@ function endOf(p, score, last) {
2528
2862
  return [...played.map((player) => ({ player, score: last.get(player) ?? 0, stars: 2 })), { player: now2, score, stars: 2 }];
2529
2863
  }
2530
2864
 
2865
+ // src/picture.ts
2866
+ var QUALITIES = [PICTURE_JPEG_QUALITY, 0.7, 0.55];
2867
+ var SHRINK = 0.75;
2868
+ var MIN_SIDE = 320;
2869
+ function encodePicture(source) {
2870
+ if (typeof source === "string") {
2871
+ const checked = checkPicture(source);
2872
+ return "ok" in checked ? { data: source } : { error: checked.error === "too-big" ? "too-big" : "error" };
2873
+ }
2874
+ try {
2875
+ if (!source || typeof source.toDataURL !== "function") return { error: "error" };
2876
+ const { width, height } = source;
2877
+ if (!(width > 0 && height > 0)) return { error: "error" };
2878
+ const doc = source.ownerDocument ?? document;
2879
+ let scale = Math.min(1, MAX_PICTURE_SIDE / Math.max(width, height));
2880
+ for (; ; ) {
2881
+ const w = Math.max(1, Math.round(width * scale));
2882
+ const h = Math.max(1, Math.round(height * scale));
2883
+ const flat = doc.createElement("canvas");
2884
+ flat.width = w;
2885
+ flat.height = h;
2886
+ const ctx = flat.getContext("2d");
2887
+ if (!ctx) return { error: "error" };
2888
+ ctx.fillStyle = "#fff";
2889
+ ctx.fillRect(0, 0, w, h);
2890
+ ctx.drawImage(source, 0, 0, w, h);
2891
+ for (const q of QUALITIES) {
2892
+ const data = flat.toDataURL("image/jpeg", q);
2893
+ const checked = checkPicture(data);
2894
+ if ("ok" in checked) return { data };
2895
+ if (checked.error !== "too-big") return { error: "error" };
2896
+ }
2897
+ if (Math.max(w, h) * SHRINK < MIN_SIDE) return { error: "too-big" };
2898
+ scale *= SHRINK;
2899
+ }
2900
+ } catch {
2901
+ return { error: "error" };
2902
+ }
2903
+ }
2904
+
2531
2905
  // src/index.ts
2532
2906
  var windowChannel = (win = window) => ({
2533
2907
  post: (m) => win.parent.postMessage(m, "*"),
@@ -2536,7 +2910,8 @@ var windowChannel = (win = window) => ({
2536
2910
  });
2537
2911
  var INIT_TIMEOUT_MS = 1e4;
2538
2912
  var STATS_MS = 1e3;
2539
- function connect(channel = windowChannel()) {
2913
+ var PICTURE_TIMEOUT_MS = 12e4;
2914
+ function connect(channel = windowChannel(), options = {}) {
2540
2915
  return new Promise((resolve2, reject) => {
2541
2916
  const listeners = /* @__PURE__ */ new Map();
2542
2917
  const breaks = [];
@@ -2548,6 +2923,17 @@ function connect(channel = windowChannel()) {
2548
2923
  let input = "body";
2549
2924
  let players = Object.freeze([]);
2550
2925
  let turns = players;
2926
+ let pictures = false;
2927
+ let pictureId = 0;
2928
+ const pictureWaits = /* @__PURE__ */ new Map();
2929
+ let cursor;
2930
+ const cursorTarget = () => {
2931
+ if (cursor === void 0) {
2932
+ const target = options.cursor !== void 0 ? options.cursor : typeof document !== "undefined" ? domCursor() : null;
2933
+ cursor = target ? cursorDriver(target) : null;
2934
+ }
2935
+ return cursor;
2936
+ };
2551
2937
  const backdropListeners = /* @__PURE__ */ new Set();
2552
2938
  const avatars = new AvatarBook();
2553
2939
  const drawAvatar2 = avatarDrawer(avatars, () => ({ view }));
@@ -2568,6 +2954,7 @@ function connect(channel = windowChannel()) {
2568
2954
  const device = parseDeviceHint(m.device) ?? null;
2569
2955
  input = m.input === "remote" ? "remote" : "body";
2570
2956
  const pointer2 = input === "remote" && m.pointer === true;
2957
+ pictures = m.pictures === true;
2571
2958
  players = Object.freeze(Array.isArray(m.players) ? [...m.players] : []);
2572
2959
  const order = input === "remote" && players.length === 1 ? parseTurnOrder(m.turns) : void 0;
2573
2960
  turns = Object.freeze(order && order[0] === players[0] ? order : [...players]);
@@ -2591,6 +2978,8 @@ function connect(channel = windowChannel()) {
2591
2978
  input,
2592
2979
  pointer: pointer2,
2593
2980
  muted: m.muted,
2981
+ // Hosts before orientations don't send it: landscape.
2982
+ orientation: m.orientation === "portrait" ? "portrait" : "landscape",
2594
2983
  ageRange: m.ageRange ?? "6-8",
2595
2984
  get backdrop() {
2596
2985
  return backdrop;
@@ -2622,7 +3011,28 @@ function connect(channel = windowChannel()) {
2622
3011
  breaks.push({ reason, done });
2623
3012
  channel.post(wire({ type: "break", reason }));
2624
3013
  }),
2625
- end: (scores) => channel.post(wire({ type: "end", scores }))
3014
+ end: (scores) => channel.post(wire({ type: "end", scores })),
3015
+ get canSavePicture() {
3016
+ return pictures;
3017
+ },
3018
+ savePicture: async (source, opts) => {
3019
+ if (!pictures) return "unavailable";
3020
+ const encoded = encodePicture(source);
3021
+ if ("error" in encoded) return encoded.error;
3022
+ const id = ++pictureId;
3023
+ const title = parsePictureTitle(opts?.title);
3024
+ return new Promise((done) => {
3025
+ const timer2 = setTimeout(() => finish("error"), PICTURE_TIMEOUT_MS);
3026
+ timer2.unref?.();
3027
+ const finish = (r) => {
3028
+ clearTimeout(timer2);
3029
+ pictureWaits.delete(id);
3030
+ done(r);
3031
+ };
3032
+ pictureWaits.set(id, finish);
3033
+ channel.post(wire({ type: "picture", data: encoded.data, id, ...title ? { title } : {} }));
3034
+ });
3035
+ }
2626
3036
  });
2627
3037
  } else if (!started) return;
2628
3038
  else {
@@ -2645,6 +3055,23 @@ function connect(channel = windowChannel()) {
2645
3055
  players = Object.freeze(next);
2646
3056
  message = { type: "turn", players: [...next] };
2647
3057
  }
3058
+ if (m.type === "picture-result") {
3059
+ const r = parsePictureResult(m);
3060
+ if (r?.id !== void 0) pictureWaits.get(r.id)?.(r.result);
3061
+ return;
3062
+ }
3063
+ if (m.type === "capabilities") {
3064
+ if (typeof m.pictures !== "boolean" || m.pictures === pictures) return;
3065
+ pictures = m.pictures;
3066
+ message = { type: "capabilities", pictures };
3067
+ }
3068
+ if (m.type === "cursor") {
3069
+ const c = input === "remote" ? parseCursor(m) : void 0;
3070
+ if (!c) return;
3071
+ message = c;
3072
+ cursorTarget()?.update(c);
3073
+ }
3074
+ if (m.type === "pause" || m.type === "break-start" || m.type === "turn") cursor?.cancel();
2648
3075
  if (m.type === "break-start") inBreak = breaks.length > 0;
2649
3076
  for (const l of listeners.get(m.type) ?? []) l(message);
2650
3077
  if (m.type === "break-done") {