@fabi-play/engine 0.2.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/engine 0.2.0 (full) | Fabi Play Kit License | https://docs.fabiplay.app */
1
+ /*! @fabi-play/engine 0.5.0 (full) | 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); });
@@ -119,6 +119,7 @@ var defaultAvatarFor = (slot, ageRange) => {
119
119
  return list[Number.isInteger(slot) && slot >= 0 ? slot % list.length : 0];
120
120
  };
121
121
  var toScreen = (view, p) => view ? { x: view.x + p.x * view.w, y: view.y + p.y * view.h } : { x: p.x, y: p.y };
122
+ var COVER_ASPECT = 16 / 9;
122
123
  var POSE_HEADER = 12;
123
124
  var POSE_BODY_BYTES = LANDMARKS * 4;
124
125
  var poseBytes = (bodies) => POSE_HEADER + bodies * POSE_BODY_BYTES;
@@ -250,6 +251,7 @@ var AVATARS = [
250
251
  id: "lion",
251
252
  name: { en: "Lion", it: "Leoncino" },
252
253
  band: "6-8",
254
+ alsoIn: ["3-5"],
253
255
  palette: { body: "#f6c454", limbs: "#f6c454", belly: "#fde7b0", accent: "#fde7b0", outline: INK },
254
256
  head: "round",
255
257
  ears: "round",
@@ -616,6 +618,39 @@ var AVATARS = [
616
618
  headExtras: ["hair", "domino"],
617
619
  bodyExtras: ["cape", "emblem", "boots"],
618
620
  extraColors: ["#ffd84a"]
621
+ },
622
+ // ── In both 3–5 and 6–8 (home 6–8, alsoIn 3–5: drawn with the proportions of the band shown) ──
623
+ {
624
+ // Next to the round 'dinosaur': a T-rex at a glance, a big head turned three-quarters with a long snout and a
625
+ // toothy grin, tiny arms, strong legs, a thick striped tail.
626
+ id: "trex",
627
+ name: { en: "T-Rex", it: "T-Rex" },
628
+ band: "6-8",
629
+ alsoIn: ["3-5"],
630
+ palette: { body: "#6cb84e", limbs: "#6cb84e", belly: "#f4e8a8", accent: "#f4e8a8", outline: INK },
631
+ head: "snout-long",
632
+ ears: "none",
633
+ face: { eyes: "dot", mouth: "toothy", muzzle: "none", cheeks: "#f59b8f" },
634
+ tail: "thick",
635
+ headExtras: ["stripes"],
636
+ bodyExtras: ["tiny-arms", "strong-legs"],
637
+ extraColors: ["#4a9235", "#58a23f"]
638
+ },
639
+ {
640
+ // An original child fairy: a bun with a flower crown, little pointed ears, a petal tunic, see-through wings and
641
+ // a star wand in one hand.
642
+ id: "fairy",
643
+ name: { en: "Fairy", it: "Fatina" },
644
+ band: "6-8",
645
+ alsoIn: ["3-5"],
646
+ palette: { body: "#b99af0", limbs: "#a8693f", belly: "#b99af0", accent: "#ffd84a", outline: INK, skin: "#a8693f", hair: "#2e1d17", hands: "#a8693f", feet: "#f58cbc" },
647
+ head: "round",
648
+ ears: "pixie",
649
+ face: { eyes: "dot", mouth: "smile", muzzle: "none", cheeks: "#f0857a" },
650
+ tail: "none",
651
+ headExtras: ["bun", "flower-crown"],
652
+ bodyExtras: ["wings-fairy", "petals", "wand"],
653
+ extraColors: ["#c4ecff", "#f7a8cf", "#fff3a0", "#ffffff"]
619
654
  }
620
655
  ];
621
656
 
@@ -852,6 +887,7 @@ function headShapes(def, ow) {
852
887
  if (has("locks")) {
853
888
  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 });
854
889
  }
890
+ if (has("bun")) out.push({ k: "circle", x: 0.04, y: -1.14, r: 0.4, fill: p.hair ?? p.outline, ...ol });
855
891
  if (has("ninja-hood")) {
856
892
  for (const [rot, len3] of [
857
893
  [0.35, 0.5],
@@ -889,7 +925,15 @@ function headShapes(def, ow) {
889
925
  if (muzzle === "disc") for (const s of SIDES) out.push({ k: "circle", x: s * 0.4, y: 0.02, r: 0.44, fill: mc });
890
926
  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 });
891
927
  if (muzzle === "band") out.push({ k: "path", d: lowerFace(def, ow), fill: mc });
892
- if (has("stripes")) {
928
+ if (has("stripes") && def.head === "snout-long") {
929
+ const c = extra(0, p.outline);
930
+ for (const [x0, y0, x1, y1] of [
931
+ [0.46, -0.94, 0.36, -0.66],
932
+ [0.8, -0.72, 0.62, -0.5],
933
+ [1, -0.36, 0.78, -0.24]
934
+ ])
935
+ out.push({ k: "line", pts: [x0, y0, x1, y1], stroke: c, lw: 0.11 });
936
+ } else if (has("stripes")) {
893
937
  const c = extra(0, p.outline);
894
938
  out.push({ k: "line", pts: [0, -0.97, 0, -0.66], stroke: c, lw: 0.13 });
895
939
  for (const s of SIDES) {
@@ -904,6 +948,13 @@ function headShapes(def, ow) {
904
948
  out.push({ k: "circle", x: 0.58, y: -0.42, r: 0.11, fill: c });
905
949
  out.push({ k: "circle", x: 0.22, y: -0.76, r: 0.09, fill: c });
906
950
  }
951
+ if (def.head === "snout-long") {
952
+ for (const [x, y, r] of [
953
+ [-1.38, -0.14, 0.06],
954
+ [-1.14, -0.22, 0.055]
955
+ ])
956
+ out.push({ k: "ellipse", x, y, rx: r, ry: r * 0.8, rot: -0.3, fill: p.outline, alpha: 0.85 });
957
+ }
907
958
  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) });
908
959
  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 });
909
960
  if (has("gills")) {
@@ -912,6 +963,7 @@ function headShapes(def, ow) {
912
963
  const hair = p.hair ?? p.outline;
913
964
  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 });
914
965
  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 });
966
+ 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 });
915
967
  if (has("curls")) {
916
968
  for (let i = 0; i < 7; i++) {
917
969
  const c = polar(-1.3 + i / 6 * 2.6, 0.86);
@@ -933,7 +985,7 @@ function headShapes(def, ow) {
933
985
  }
934
986
  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 });
935
987
  out.push(...mouth(def, ow));
936
- 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 });
988
+ 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 });
937
989
  if (has("whiskers")) {
938
990
  for (const s of SIDES)
939
991
  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 });
@@ -955,6 +1007,24 @@ function headShapes(def, ow) {
955
1007
  out.push({ k: "line", pts: [-w2, y + 0.04, w2, y - 0.04], stroke: p.outline, lw: ow * 0.7, alpha: 0.6 });
956
1008
  }
957
1009
  }
1010
+ if (has("flower-crown")) {
1011
+ const cols2 = [extra(1, "#f7a8cf"), extra(2, "#fff3a0"), extra(3, WHITE)];
1012
+ for (const a of [-0.84, -0.28, 0.28, 0.84]) {
1013
+ const c = polar(a, 0.9);
1014
+ 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 });
1015
+ }
1016
+ ;
1017
+ [-1.12, -0.56, 0, 0.56, 1.12].forEach((a, i) => {
1018
+ const c = polar(a, 0.9);
1019
+ const r = i === 2 ? 0.13 : 0.11;
1020
+ for (let k = 0; k < 5; k++) {
1021
+ const t = a + k / 5 * Math.PI * 2;
1022
+ out.push({ k: "circle", x: c.x + Math.sin(t) * r, y: c.y - Math.cos(t) * r, r: r * 0.82, fill: cols2[i % cols2.length], stroke: p.outline, lw: ow * 0.7 });
1023
+ }
1024
+ out.push({ k: "circle", x: c.x, y: c.y, r: r * 0.62, fill: cols2[i % cols2.length] });
1025
+ out.push({ k: "circle", x: c.x, y: c.y, r: r * 0.42, fill: p.accent });
1026
+ });
1027
+ }
958
1028
  if (has("helmet")) {
959
1029
  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 });
960
1030
  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 });
@@ -1019,6 +1089,8 @@ function headShape(def, ol) {
1019
1089
  return { k: "rect", x: 0, y: 0, w: 2, h: 1.8, r: 0.46, fill, ...ol };
1020
1090
  case "bucket":
1021
1091
  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 };
1092
+ case "snout-long":
1093
+ return { k: "path", d: smoothClosed(SNOUT_HEAD, 0.95), fill, ...ol };
1022
1094
  case "bulb":
1023
1095
  return {
1024
1096
  k: "path",
@@ -1037,6 +1109,19 @@ function headShape(def, ol) {
1037
1109
  };
1038
1110
  }
1039
1111
  }
1112
+ 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);
1113
+ function eyeSpots(def, ex, ey) {
1114
+ if (def.head === "snout-long")
1115
+ return [
1116
+ { x: -0.36, y: -0.5, k: 0.95 },
1117
+ { x: 0.26, y: -0.48, k: 1.1 }
1118
+ ];
1119
+ return SIDES.map((s) => ({ x: s * ex, y: ey, k: 1 }));
1120
+ }
1121
+ function cheekSpots(def) {
1122
+ if (def.head === "snout-long") return pts(-0.66, -0.14, 0.66, 0.1);
1123
+ return SIDES.map((s) => ({ x: s * 0.64, y: 0.3 }));
1124
+ }
1040
1125
  function headEllipse(def) {
1041
1126
  if (def.head === "round") return { cy: 0, rx: 1, ry: 1 };
1042
1127
  if (def.head === "tall") return { cy: -0.02, rx: 0.94, ry: 1.04 };
@@ -1117,6 +1202,10 @@ function ears(def, ow) {
1117
1202
  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 });
1118
1203
  out.push({ k: "ellipse", x: s * 1.08, y: -0.2, rx: 0.24, ry: 0.08, rot: s * 0.32, fill: p.accent });
1119
1204
  break;
1205
+ case "pixie":
1206
+ 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 });
1207
+ 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 });
1208
+ break;
1120
1209
  case "human":
1121
1210
  out.push({ k: "ellipse", x: s * 0.98, y: 0.06, rx: 0.17, ry: 0.23, fill: p.skin ?? p.body, ...ol });
1122
1211
  break;
@@ -1149,15 +1238,15 @@ function eyes(def, ow) {
1149
1238
  const shine = (x, y, r) => ({ k: "circle", x, y, r, fill: WHITE });
1150
1239
  switch (f2.eyes) {
1151
1240
  case "dot":
1152
- for (const s of SIDES) {
1153
- out.push({ k: "ellipse", x: s * ex, y: ey, rx: 0.12, ry: 0.155, fill: ink });
1154
- out.push(shine(s * ex + 0.04, ey - 0.06, 0.045));
1241
+ for (const e of eyeSpots(def, ex, ey)) {
1242
+ out.push({ k: "ellipse", x: e.x, y: e.y, rx: 0.12 * e.k, ry: 0.155 * e.k, fill: ink });
1243
+ out.push(shine(e.x + 0.04 * e.k, e.y - 0.06 * e.k, 0.045 * e.k));
1155
1244
  }
1156
1245
  break;
1157
1246
  case "bead":
1158
- for (const s of SIDES) {
1159
- out.push({ k: "ellipse", x: s * ex, y: ey, rx: 0.16, ry: 0.22, fill: ink });
1160
- out.push(shine(s * ex + 0.05, ey - 0.08, 0.065));
1247
+ for (const e of eyeSpots(def, ex, ey)) {
1248
+ out.push({ k: "ellipse", x: e.x, y: e.y, rx: 0.16 * e.k, ry: 0.22 * e.k, fill: ink });
1249
+ out.push(shine(e.x + 0.05 * e.k, e.y - 0.08 * e.k, 0.065 * e.k));
1161
1250
  }
1162
1251
  break;
1163
1252
  case "big":
@@ -1266,6 +1355,21 @@ function mouth(def, ow) {
1266
1355
  out.push({ k: "ellipse", x: 0, y: 0.48, rx: 0.16, ry: 0.11, fill: MOUTH });
1267
1356
  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 });
1268
1357
  break;
1358
+ case "toothy": {
1359
+ 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 });
1360
+ out.push({ k: "ellipse", x: -0.38, y: 0.5, rx: 0.24, ry: 0.08, fill: TONGUE });
1361
+ for (const t of [0.12, 0.27, 0.42, 0.57, 0.72, 0.86]) {
1362
+ const x = (1 - t) * (1 - t) * -1.4 + 2 * (1 - t) * t * -0.5 + t * t * 0.5;
1363
+ const y = (1 - t) * (1 - t) * 0.1 + 2 * (1 - t) * t * 0.36 + t * t * 0.2 - 0.01;
1364
+ out.push({ k: "path", d: roundedPolygon(pts(x - 0.08, y, x + 0.08, y, x, y + 0.14), 0.3), fill: WHITE });
1365
+ }
1366
+ for (const t of [0.3, 0.7]) {
1367
+ const x = (1 - t) * (1 - t) * 0.52 + 2 * (1 - t) * t * -0.4 + t * t * -1.3;
1368
+ const y = (1 - t) * (1 - t) * 0.38 + 2 * (1 - t) * t * 0.78 + t * t * 0.26 + 0.01;
1369
+ out.push({ k: "path", d: roundedPolygon(pts(x - 0.07, y, x + 0.07, y, x, y - 0.12), 0.3), fill: WHITE });
1370
+ }
1371
+ break;
1372
+ }
1269
1373
  case "jaws": {
1270
1374
  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 });
1271
1375
  for (const x of [-0.34, -0.17, 0, 0.17, 0.34]) {
@@ -1288,7 +1392,10 @@ var BAND_BODY = {
1288
1392
  "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 }
1289
1393
  };
1290
1394
  var DEFAULT_BAND = "6-8";
1291
- var bodyBand = (def) => def.band in BAND_BODY ? def.band : DEFAULT_BAND;
1395
+ var bodyBand = (def, shown) => {
1396
+ if (shown !== void 0 && def.band !== "all" && def.alsoIn?.includes(shown) && shown in BAND_BODY) return shown;
1397
+ return def.band in BAND_BODY ? def.band : DEFAULT_BAND;
1398
+ };
1292
1399
  var HEAD = BAND_BODY[DEFAULT_BAND].head;
1293
1400
  var MAX_TILT = 0.3;
1294
1401
  var TILT_DEAD_ZONE = 0.12;
@@ -1382,7 +1489,7 @@ function headTilt(body, at, angle, U) {
1382
1489
  return 0;
1383
1490
  }
1384
1491
  function avatarShapes(body, def, box, opts = {}) {
1385
- const band = bodyBand(def);
1492
+ const band = bodyBand(def, opts.band);
1386
1493
  const bb = BAND_BODY[band];
1387
1494
  const F = frameOf(body, box, { ...opts, band });
1388
1495
  if (!F) return [];
@@ -1414,8 +1521,8 @@ function avatarShapes(body, def, box, opts = {}) {
1414
1521
  out.push({ k: "line", pts: flat2, stroke: p.outline, lw: w2 + ow * 2 });
1415
1522
  out.push({ k: "line", pts: flat2, stroke: color, lw: w2 });
1416
1523
  };
1417
- const legW = bb.leg * U;
1418
- const armW = bb.arm * U;
1524
+ const legW = bb.leg * U * (has("strong-legs") ? 1.3 : 1);
1525
+ const armW = bb.arm * U * (has("tiny-arms") ? 0.6 : 1);
1419
1526
  const pad = Math.max(bb.arm / 0.22, 0.9);
1420
1527
  if (F.hipsOk) {
1421
1528
  for (const [hip, kneeI, ankleI, side] of [
@@ -1429,7 +1536,7 @@ function avatarShapes(body, def, box, opts = {}) {
1429
1536
  const pts2 = ankle.ok ? [top, knee, ankle] : [top, knee];
1430
1537
  limb(legs, pts2, legW, p.limbs);
1431
1538
  if (has("joints")) legs.push({ k: "circle", x: knee.x, y: knee.y, r: 0.15 * U * pad, fill: p.accent, ...ol });
1432
- if (ankle.ok) legs.push(foot(def, ankle, F, side, ow, bb.foot));
1539
+ if (ankle.ok) legs.push(...feet(def, ankle, F, side, ow, bb.foot));
1433
1540
  }
1434
1541
  }
1435
1542
  for (const [sh, elbowI, wristI, side] of [
@@ -1462,7 +1569,20 @@ function avatarShapes(body, def, box, opts = {}) {
1462
1569
  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 });
1463
1570
  }
1464
1571
  }
1465
- out.push({ k: "circle", x: hand.x, y: hand.y, r: bb.hand * U, fill: p.hands ?? p.limbs, ...ol });
1572
+ 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));
1573
+ const handR = bb.hand * U * (has("tiny-arms") ? 0.72 : 1);
1574
+ if (has("tiny-arms")) {
1575
+ for (const a of [-0.45, 0.45]) {
1576
+ const c = Math.cos(a);
1577
+ const s = Math.sin(a);
1578
+ const cd = { x: dir.x * c - dir.y * s, y: dir.x * s + dir.y * c };
1579
+ const base = add(hand, mul(cd, handR * 0.75));
1580
+ const tip = add(hand, mul(cd, handR * 1.6));
1581
+ const nrm = { x: -cd.y * handR * 0.32, y: cd.x * handR * 0.32 };
1582
+ out.push({ k: "path", d: roundedPolygon([add(base, nrm), tip, sub(base, nrm)], 0.2), fill: p.belly, ...ol });
1583
+ }
1584
+ }
1585
+ out.push({ k: "circle", x: hand.x, y: hand.y, r: handR, fill: p.hands ?? p.limbs, ...ol });
1466
1586
  }
1467
1587
  if (has("wings")) {
1468
1588
  for (const s of [-1, 1]) {
@@ -1477,6 +1597,7 @@ function avatarShapes(body, def, box, opts = {}) {
1477
1597
  back.push({ k: "path", d: roundedPolygon(w2, 0.18), fill: p.accent, ...ol });
1478
1598
  }
1479
1599
  }
1600
+ if (has("wings-fairy")) back.push(...fairyWings(def, T2, ht, U, ow));
1480
1601
  if (has("cape")) {
1481
1602
  const yb = L + 0.8 * U;
1482
1603
  const wb = hb + 0.36 * U;
@@ -1694,6 +1815,7 @@ function avatarShapes(body, def, box, opts = {}) {
1694
1815
  const k = 0.15 * U;
1695
1816
  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 }] });
1696
1817
  }
1818
+ if (has("petals")) torso.push(...petals(def, T2, ht, hb, bulge, L, y1, U, ow, bb));
1697
1819
  if (has("scarf")) {
1698
1820
  const y = Math.max(0.04, bb.head * (1 - bb.neck) - 0.06) * U;
1699
1821
  const c = T2(0, y);
@@ -1718,6 +1840,18 @@ function cachedHead(def, ow) {
1718
1840
  }
1719
1841
  return shapes;
1720
1842
  }
1843
+ function feet(def, ankle, F, side, ow, k) {
1844
+ const shoe = foot(def, ankle, F, side, ow, k);
1845
+ if (!def.bodyExtras.includes("strong-legs")) return [shoe];
1846
+ const U = F.U * k;
1847
+ const c = add(add(ankle, mul(F.d, 0.07 * U)), mul(F.r, side * 0.06 * U));
1848
+ const out = [shoe];
1849
+ for (const t of [-1, 0, 1]) {
1850
+ const q = add(add(c, mul(F.r, t * 0.12 * U)), mul(F.d, 0.09 * U));
1851
+ 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 });
1852
+ }
1853
+ return out;
1854
+ }
1721
1855
  function foot(def, ankle, F, side, ow, k) {
1722
1856
  const p = def.palette;
1723
1857
  const { d, r } = F;
@@ -1871,6 +2005,30 @@ function tail(def, F, hb, ow) {
1871
2005
  });
1872
2006
  break;
1873
2007
  }
2008
+ case "thick": {
2009
+ const pts2 = TP([
2010
+ [hb * 0.3, L * 0.42],
2011
+ [hb + 0.42 * U, L * 0.62],
2012
+ [hb + 0.92 * U, L * 0.96],
2013
+ [hb + 1.32 * U, L * 1.08],
2014
+ [hb + 1.44 * U, L * 0.98],
2015
+ [hb + 0.98 * U, L * 1.2],
2016
+ [hb + 0.4 * U, L * 1.22],
2017
+ [hb * 0.3, L * 1.12]
2018
+ ]);
2019
+ out.push({ k: "path", d: smoothClosed(pts2, 0.8), fill: p.body, ...ol });
2020
+ const c = def.extraColors?.[0] ?? p.accent;
2021
+ for (const [x, y, l] of [
2022
+ [0.28, 0.58, 0.26],
2023
+ [0.62, 0.8, 0.22],
2024
+ [0.96, 1, 0.16]
2025
+ ]) {
2026
+ const a = T2(hb + x * U, L * y + 0.03 * U);
2027
+ const b = T2(hb + (x - 0.1) * U, L * y + l * U);
2028
+ out.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: c, lw: 0.09 * U });
2029
+ }
2030
+ break;
2031
+ }
1874
2032
  case "spiky":
1875
2033
  case "arrow": {
1876
2034
  const pts2 = TP([
@@ -1903,6 +2061,81 @@ function tail(def, F, hb, ow) {
1903
2061
  }
1904
2062
  return out;
1905
2063
  }
2064
+ function wand(def, hand, wd, U, ow) {
2065
+ const p = def.palette;
2066
+ const from = add(hand, mul(wd, -0.08 * U));
2067
+ const tip = add(hand, mul(wd, 0.52 * U));
2068
+ const k = 0.2 * U;
2069
+ return [
2070
+ { k: "line", pts: [from.x, from.y, tip.x, tip.y], stroke: p.outline, lw: 0.06 * U + ow * 2 },
2071
+ { k: "line", pts: [from.x, from.y, tip.x, tip.y], stroke: "#fff3d6", lw: 0.06 * U },
2072
+ { k: "circle", x: tip.x, y: tip.y, r: 0.24 * U, fill: p.accent, alpha: 0.28 },
2073
+ { 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 }] }
2074
+ ];
2075
+ }
2076
+ function fairyWings(def, T2, ht, U, ow) {
2077
+ const p = def.palette;
2078
+ const fill = def.extraColors?.[0] ?? "#cdeeff";
2079
+ const out = [];
2080
+ for (const s of [-1, 1]) {
2081
+ const lobes = [
2082
+ // Upper: big, up and out.
2083
+ [
2084
+ [s * ht * 0.3, 0.12 * U],
2085
+ [s * (ht + 0.42 * U), -0.66 * U],
2086
+ [s * (ht + 1 * U), -0.74 * U],
2087
+ [s * (ht + 1.02 * U), -0.18 * U],
2088
+ [s * (ht + 0.5 * U), 0.26 * U]
2089
+ ],
2090
+ // Lower: smaller, down and out.
2091
+ [
2092
+ [s * ht * 0.4, 0.3 * U],
2093
+ [s * (ht + 0.6 * U), 0.36 * U],
2094
+ [s * (ht + 0.74 * U), 0.78 * U],
2095
+ [s * (ht + 0.3 * U), 0.9 * U]
2096
+ ]
2097
+ ];
2098
+ for (const lobe of lobes) {
2099
+ const d = smoothClosed(lobe.map(([x, y]) => T2(x, y)), 0.9);
2100
+ out.push({ k: "path", d, fill, alpha: 0.6 });
2101
+ out.push({ k: "path", d, stroke: p.outline, lw: ow * 0.9 });
2102
+ const inner = lobe[Math.floor(lobe.length / 2)];
2103
+ const a = T2(lobe[0][0], lobe[0][1]);
2104
+ const b = T2(lobe[0][0] + (inner[0] - lobe[0][0]) * 0.75, lobe[0][1] + (inner[1] - lobe[0][1]) * 0.75);
2105
+ out.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: "#ffffff", lw: ow * 0.9, alpha: 0.85 });
2106
+ }
2107
+ for (const [x, y, r] of [
2108
+ [ht + 0.72 * U, -0.42 * U, 0.075 * U],
2109
+ [ht + 0.44 * U, -0.16 * U, 0.05 * U],
2110
+ [ht + 0.5 * U, 0.6 * U, 0.055 * U]
2111
+ ]) {
2112
+ const q = T2(s * x, y);
2113
+ out.push({ k: "group", x: q.x, y: q.y, s: r, items: [{ k: "path", d: STAR, fill: "#ffffff" }] });
2114
+ }
2115
+ }
2116
+ return out;
2117
+ }
2118
+ function petals(def, T2, ht, hb, bulge, L, y1, U, ow, bb) {
2119
+ const p = def.palette;
2120
+ const second = def.extraColors?.[1] ?? p.belly;
2121
+ const out = [];
2122
+ const ang = Math.atan2(T2(1, 0).y - T2(0, 0).y, T2(1, 0).x - T2(0, 0).x);
2123
+ const n = 5;
2124
+ const w2 = Math.max(hb, bulge * 0.92) * 1.08;
2125
+ const y = (L * 0.8 + y1) / 2;
2126
+ const order = [0, 4, 1, 3, 2];
2127
+ for (const i of order) {
2128
+ const t = i / (n - 1) - 0.5;
2129
+ const c = T2(t * 2 * w2, y + (0.5 - Math.abs(t)) * 0.1 * U);
2130
+ out.push({ k: "ellipse", x: c.x, y: c.y, rx: w2 * 0.3, ry: 0.34 * U, rot: ang - t * 0.6, fill: i % 2 ? second : p.body, stroke: p.outline, lw: ow });
2131
+ }
2132
+ const cy = Math.max(0.04, bb.head * (1 - bb.neck) - 0.04) * U;
2133
+ for (const t of [-1, 1, 0]) {
2134
+ const c = T2(t * ht * 0.32, cy + (t ? 0 : 0.05 * U));
2135
+ 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 });
2136
+ }
2137
+ return out;
2138
+ }
1906
2139
 
1907
2140
  // ../avatars/src/poses.ts
1908
2141
  var STAND = {
@@ -2045,17 +2278,19 @@ var DEFAULT_AVATAR = BY_ID.get("fox");
2045
2278
  var isAvatarId = (v) => typeof v === "string" && BY_ID.has(v);
2046
2279
  var avatarById = (id) => BY_ID.get(id);
2047
2280
  var resolve = (def) => typeof def === "string" ? BY_ID.get(def) ?? DEFAULT_AVATAR : def;
2048
- function iconShapes(def, size) {
2281
+ function iconShapes(def, size, band) {
2049
2282
  const d = resolve(def);
2050
2283
  const body = neutralPose();
2051
- const band = bodyBand(d);
2052
- const unit = frameOf(body, { x: 0, y: 0, w: 1, h: 1 }, { band });
2053
- const top = -bounds(headShapes(d, 0.05)).minY;
2284
+ const shown = bodyBand(d, band);
2285
+ const unit = frameOf(body, { x: 0, y: 0, w: 1, h: 1 }, { band: shown });
2286
+ const hb = bounds(headShapes(d, 0.05));
2287
+ const top = -hb.minY;
2054
2288
  const headR = Math.min(0.27, 0.43 / Math.max(top, 1)) * size;
2055
- const k = headR / BAND_BODY[band].head / unit.U;
2289
+ const dx = d.head === "snout-long" ? (hb.minX + hb.maxX) / 2 * headR : 0;
2290
+ const k = headR / BAND_BODY[shown].head / unit.U;
2056
2291
  const cy = 0.5 * size + Math.min(0, (top - 1.6) * 0.06 * size);
2057
- const box = { x: size / 2 - unit.head.x * k, y: cy - unit.head.y * k, w: k, h: k };
2058
- return avatarShapes(body, d, box, { portrait: true });
2292
+ const box = { x: size / 2 - dx - unit.head.x * k, y: cy - unit.head.y * k, w: k, h: k };
2293
+ return avatarShapes(body, d, box, { portrait: true, band: shown });
2059
2294
  }
2060
2295
  function smoothBody(prev, next, amount = 0.5) {
2061
2296
  if (!prev || prev.length !== next.length) return next.slice();
@@ -2066,9 +2301,11 @@ function smoothBody(prev, next, amount = 0.5) {
2066
2301
  const wasSeen = pv >= 0.5;
2067
2302
  const isSeen = nv >= 0.5;
2068
2303
  if (wasSeen && nv >= 0.3) out[i + 2] = Math.max(nv, 0.5);
2069
- if (wasSeen && isSeen && Math.hypot(next[i] - prev[i], next[i + 1] - prev[i + 1]) < 0.15) {
2070
- out[i] = prev[i] + (next[i] - prev[i]) * amount;
2071
- out[i + 1] = prev[i + 1] + (next[i + 1] - prev[i + 1]) * amount;
2304
+ const step = Math.hypot(next[i] - prev[i], next[i + 1] - prev[i + 1]);
2305
+ if (wasSeen && isSeen && step < 0.15) {
2306
+ const k = amount * Math.min(1, STILL_SHARE + (1 - STILL_SHARE) * step / STILL_FULL);
2307
+ out[i] = prev[i] + (next[i] - prev[i]) * k;
2308
+ out[i + 1] = prev[i + 1] + (next[i + 1] - prev[i + 1]) * k;
2072
2309
  } else if (wasSeen && !isSeen && out[i + 2] >= 0.5) {
2073
2310
  out[i] = prev[i];
2074
2311
  out[i + 1] = prev[i + 1];
@@ -2077,6 +2314,8 @@ function smoothBody(prev, next, amount = 0.5) {
2077
2314
  smoothFace(prev, next, out, amount * FACE_SMOOTHING);
2078
2315
  return out;
2079
2316
  }
2317
+ var STILL_SHARE = 0.1;
2318
+ var STILL_FULL = 0.03;
2080
2319
  var FACE_SMOOTHING = 0.35;
2081
2320
  var FACE_POINTS = 11;
2082
2321
  function smoothFace(prev, next, out, amount) {
@@ -7365,6 +7604,10 @@ async function createEngine(opts = {}) {
7365
7604
  };
7366
7605
  unlisten.push(
7367
7606
  play.on("start", (m) => lateHint(m?.device)),
7607
+ // The simple API knows muted only from the start (its getter is false before): read it again then.
7608
+ play.on("start", (m) => {
7609
+ if (typeof m?.muted === "boolean") sound.muted = m.muted;
7610
+ }),
7368
7611
  play.on("frame", (m) => setPlayers(m?.players)),
7369
7612
  play.on("players", (m) => setPlayers(m?.players)),
7370
7613
  play.on("backdrop", (m) => setBackdrop(m?.backdrop, m?.view)),