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