@diffenderfer-games/hub 2.0.0-next.10 → 2.0.0-next.12

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.
@@ -232,12 +232,12 @@ const FRAMES = {
232
232
  206
233
233
  ]
234
234
  };
235
- /** The walker's full frame: a smaller head, and room for feet hanging below a seat. */
235
+ /** The walker's full frame: a short, chunky figure, with room for hats, jumps and feet dangling below a seat. */
236
236
  const WALKER_FULL = [
237
237
  -60,
238
- -176,
238
+ -144,
239
239
  120,
240
- 214
240
+ 170
241
241
  ];
242
242
  /** True when options draw the walker figure (full frames only). */
243
243
  function isWalker(opts) {
@@ -1650,42 +1650,49 @@ function limb(points, width, fill, ink) {
1650
1650
  cap: "round"
1651
1651
  })];
1652
1652
  }
1653
- /** A shoe at a foot point, nudged along the facing. */
1653
+ /** A shoe at a foot point, nudged along the facing: round and stubby. */
1654
1654
  function shoe(at, forward, o) {
1655
- return ellipse(at[0] + forward[0] * 2.4, at[1] + forward[1] * 1.6 - 1.5, 6.2 + Math.abs(forward[0]) * 1.4, 3.8, {
1655
+ return ellipse(at[0] + forward[0] * 2.4, at[1] + forward[1] * 1.6 - 1.6, 7.2 + Math.abs(forward[0]) * 1.4, 4.1, {
1656
1656
  fill: o.shoes,
1657
1657
  stroke: o.shoesInk,
1658
1658
  sw: LINE * .8
1659
1659
  });
1660
1660
  }
1661
- /** A hand. */
1661
+ /** A hand: a round mitten. */
1662
1662
  function hand(c, at) {
1663
- return ellipse(at[0], at[1], 4.3, 4.3, {
1663
+ return ellipse(at[0], at[1], 5.2, 5.2, {
1664
1664
  fill: c.skin,
1665
1665
  stroke: c.skinInk,
1666
1666
  sw: LINE * .8
1667
1667
  });
1668
1668
  }
1669
+ /** The torso's half-widths (before the turn's width factor): square shoulders over a round belly. */
1670
+ const TORSO = {
1671
+ shoulder: 20,
1672
+ belly: 23.5,
1673
+ hip: 21
1674
+ };
1669
1675
  /** The torso's outline: shoulders at `top`, the hem at `hem`, `w` its width factor (narrower when turned). */
1670
1676
  function torsoPath(w, top, hem) {
1671
- const shoulder = 17.5 * w;
1672
- const waist = 14.5 * w;
1673
- const hip = 15.5 * w;
1674
- const mid = (top + hem) / 2 + 4;
1675
- return new Path().m(-shoulder, top + 6).c(-shoulder, top - 1, -shoulder + 5, top - 3, -shoulder * .35, top - 3).l(shoulder * .35, top - 3).c(shoulder - 5, top - 3, shoulder, top - 1, shoulder, top + 6).l(waist, mid).l(hip, hem).q(0, hem + 3, -hip, hem).l(-waist, mid).z().d();
1677
+ const shoulder = TORSO.shoulder * w;
1678
+ const belly = TORSO.belly * w;
1679
+ const hip = TORSO.hip * w;
1680
+ const mid = (top + hem) / 2 + 2;
1681
+ return new Path().m(-shoulder, top + 5).c(-shoulder, top - 1, -shoulder + 6, top - 3, -shoulder * .35, top - 3).l(shoulder * .35, top - 3).c(shoulder - 6, top - 3, shoulder, top - 1, shoulder, top + 5).c(belly, mid - 4, belly, mid + 4, hip, hem - 1).q(0, hem + 4, -hip, hem - 1).c(-belly, mid + 4, -belly, mid - 4, -shoulder, top + 5).z().d();
1676
1682
  }
1677
1683
  /** The shirt: a shade base, the lit front, the outline, and the neckline (front) or collar (back). */
1678
1684
  function shirt(c, w, top, hem, lean, isBack) {
1679
1685
  const d = torsoPath(w, top, hem);
1680
1686
  const cy = (top + hem) / 2;
1687
+ const hemHalf = (TORSO.hip - 3) * w;
1681
1688
  const nodes = [
1682
1689
  path(d, { fill: c.bodyShade }),
1683
1690
  g([path(d, { fill: c.body })], { tf: [
1684
1691
  tr(-1.5 - lean * 2, cy - 1.5),
1685
- sc(.9, .94),
1692
+ sc(.88, .9),
1686
1693
  tr(0, -cy)
1687
1694
  ] }),
1688
- path(`M${String(-15 * w)} ${String(hem - 2.5)}Q0 ${String(hem + .5)} ${String(15 * w)} ${String(hem - 2.5)}`, {
1695
+ path(`M${String(-hemHalf)} ${String(hem - 2)}Q0 ${String(hem + 1.5)} ${String(hemHalf)} ${String(hem - 2)}`, {
1689
1696
  fill: "none",
1690
1697
  stroke: shade(c.bodyShade),
1691
1698
  sw: 2.2,
@@ -1703,7 +1710,7 @@ function shirt(c, w, top, hem, lean, isBack) {
1703
1710
  stroke: c.bodyShade,
1704
1711
  sw: 3
1705
1712
  }));
1706
- else nodes.push(path(`M${String(neckX - 6 * w)} ${String(top - 2.6)}Q${String(neckX)} ${String(top + 6)} ${String(neckX + 6 * w)} ${String(top - 2.6)}Z`, {
1713
+ else nodes.push(path(`M${String(neckX - 6 * w)} ${String(top - 2.6)}Q${String(neckX)} ${String(top + 5)} ${String(neckX + 6 * w)} ${String(top - 2.6)}Z`, {
1707
1714
  fill: c.skin,
1708
1715
  stroke: c.bodyInk,
1709
1716
  sw: LINE * .8
@@ -1712,16 +1719,16 @@ function shirt(c, w, top, hem, lean, isBack) {
1712
1719
  }
1713
1720
  /** The seat of the trousers joining the legs under the hem. */
1714
1721
  function hips(o, w, hipY) {
1715
- const half = 12 * w;
1722
+ const half = 16 * w;
1716
1723
  return path(new Path().m(-half, hipY - 4).l(half, hipY - 4).l(half, hipY + 4).q(0, hipY + 9, -half, hipY + 4).z().d(), {
1717
1724
  fill: o.trousers,
1718
1725
  stroke: o.trousersInk,
1719
1726
  sw: LINE * .8
1720
1727
  });
1721
1728
  }
1722
- /** The neck under the head. */
1729
+ /** The neck under the head (a short one: the big head sits almost on the shoulders). */
1723
1730
  function neck(c, top, dx) {
1724
- return path(new Path().m(dx - 4.5, top - 10).l(dx + 4.5, top - 10).l(dx + 4.5, top).l(dx - 4.5, top).z().d(), {
1731
+ return path(new Path().m(dx - 5, top - 6).l(dx + 5, top - 6).l(dx + 5, top).l(dx - 5, top).z().d(), {
1725
1732
  fill: c.skinShade,
1726
1733
  stroke: c.skinInk,
1727
1734
  sw: LINE * .6
@@ -1730,37 +1737,44 @@ function neck(c, top, dx) {
1730
1737
  //#endregion
1731
1738
  //#region src/avatar/render/walker.ts
1732
1739
  /**
1733
- * The world walker (`figure: 'walker'`): a small full-body character for
1734
- * maps and crowds, with legs, arms, a shirt, trousers and shoes, and the
1735
- * head drawn smaller on top ({@link WALKER_HEAD_SCALE}). It takes the blob's
1736
- * pose names: `walk` and `run` swing the legs and arms along the facing,
1737
- * `sit` folds the legs over a seat, `idle` breathes, `wave` and `cheer` lift
1738
- * the arms.
1740
+ * The world walker (`figure: 'walker'`): a small, chunky, chibi-like
1741
+ * full-body character for maps and crowds: a short round body with stubby
1742
+ * legs and arms, a shirt, trousers and shoes, and a big head on top
1743
+ * ({@link WALKER_HEAD_SCALE}). It takes the blob's pose names: `walk` and
1744
+ * `run` waddle (quick steps that bounce and rock from foot to foot), `sit`
1745
+ * folds the legs over a seat, `idle` breathes, `wave` and `cheer` lift the
1746
+ * arms.
1739
1747
  *
1740
1748
  * Standing, the origin is the feet. Sitting (`sit`, `play`), the origin is
1741
- * the seat: the hips rest on it and the feet hang {@link WALKER_SEAT_DROP}
1742
- * units below, on the floor in front of it.
1749
+ * the seat: the hips rest on it and the short legs dangle over its front
1750
+ * edge, the feet {@link WALKER_SEAT_DROP} units below it.
1743
1751
  */
1744
1752
  /** The head's scale on a walker (the blob draws it at 1). */
1745
- const WALKER_HEAD_SCALE = .58;
1746
- /** How far below the seat a sitter's feet reach (rig units). */
1747
- const WALKER_SEAT_DROP = 26;
1748
- const STANDING = {
1749
- hip: -35,
1750
- shoulder: -72,
1751
- head: -103
1753
+ const WALKER_HEAD_SCALE = .62;
1754
+ /** How far below the seat a sitter's dangling feet reach (rig units): short legs swing clear of the floor. */
1755
+ const WALKER_SEAT_DROP = 13;
1756
+ /** Standing: a short, round body (half the first walker's 72 units to the shoulders) under the head. */
1757
+ const WALKER_STANDING = {
1758
+ hip: -18,
1759
+ shoulder: -38,
1760
+ head: -70
1752
1761
  };
1753
1762
  const SEATED = {
1754
1763
  hip: -3,
1755
- shoulder: -40,
1756
- head: -71
1764
+ shoulder: -23,
1765
+ head: -55
1757
1766
  };
1758
- const LEG = 32;
1759
- const UPPER_ARM = 13;
1760
- const FOREARM = 12;
1761
- const LEG_W = 10.5;
1762
- const SLEEVE_W = 10;
1763
- const ARM_W = 7.5;
1767
+ const LEG = 18;
1768
+ const UPPER_ARM = 8;
1769
+ const FOREARM = 7;
1770
+ const LEG_W = 13;
1771
+ const SLEEVE_W = 12.5;
1772
+ const ARM_W = 9;
1773
+ /** How far out the hips and shoulders sit from the middle. */
1774
+ const HIP_X = 7.5;
1775
+ const SHOULDER_X = 19;
1776
+ /** The seated thigh, along the facing. */
1777
+ const THIGH = 9;
1764
1778
  const DEG$1 = Math.PI / 180;
1765
1779
  function viewOf(angleDeg) {
1766
1780
  const side = Math.sin(angleDeg * DEG$1);
@@ -1770,7 +1784,7 @@ function viewOf(angleDeg) {
1770
1784
  front,
1771
1785
  forward: [side, front * .45],
1772
1786
  isBack: angleDeg > 90,
1773
- width: 1 - .36 * Math.abs(side)
1787
+ width: 1 - .26 * Math.abs(side)
1774
1788
  };
1775
1789
  }
1776
1790
  /** A body side's screen x for a lateral distance (sides overlap when turned). */
@@ -1778,78 +1792,102 @@ const lateralX = (v, sideOf, distance) => sideOf * distance * v.front;
1778
1792
  /** How near the viewer a side is (> 0: drawn in front when turned). */
1779
1793
  const nearness = (v, sideOf) => -sideOf * v.side;
1780
1794
  const add = (a, b, k = 1) => [a[0] + b[0] * k, a[1] + b[1] * k];
1795
+ /** The walk's and run's step: stride angle, foot lift, bounce and side-to-side rock (degrees). */
1796
+ const GAIT = {
1797
+ walk: {
1798
+ stride: 36,
1799
+ lift: 3.6,
1800
+ bounce: 3.2,
1801
+ rock: 5
1802
+ },
1803
+ run: {
1804
+ stride: 46,
1805
+ lift: 5.4,
1806
+ bounce: 4.4,
1807
+ rock: 3.5
1808
+ }
1809
+ };
1810
+ /** The gait a pose steps with, if it steps. */
1811
+ function gaitOf(pose) {
1812
+ return pose === "run" ? GAIT.run : pose === "walk" || pose === "dance" ? GAIT.walk : null;
1813
+ }
1814
+ /** A shoulder joint: out at the torso's edge (narrower when turned), nudged along the facing. */
1815
+ const shoulderAt = (v, h, sideOf, bob) => [lateralX(v, sideOf, SHOULDER_X * v.width) + v.forward[0] * 1.5, h.shoulder + 4 + bob];
1781
1816
  function standingLimbs(v, h, pose, phase, bob) {
1782
- const isRun = pose === "run";
1783
- const isWalking = pose === "walk" || isRun || pose === "dance";
1784
- const stride = isRun ? 38 : 27;
1785
- const swingOf = (sideOf) => isWalking ? Math.sin((phase + (sideOf === 1 ? 0 : .5)) * Math.PI * 2) : 0;
1817
+ const gait = gaitOf(pose);
1818
+ const stepOf = (sideOf) => (phase + (sideOf === 1 ? 0 : .5)) * Math.PI * 2;
1819
+ const swingOf = (sideOf) => gait ? Math.sin(stepOf(sideOf)) : 0;
1786
1820
  return {
1787
1821
  leg: (sideOf) => {
1788
- const hip = [lateralX(v, sideOf, 6), h.hip + bob];
1789
- const swing = swingOf(sideOf);
1790
- const lift = isWalking ? Math.max(0, Math.cos((phase + (sideOf === 1 ? 0 : .5)) * Math.PI * 2)) * (isRun ? 7 : 4.5) : 0;
1791
- const reach = Math.sin(stride * DEG$1) * LEG * swing;
1822
+ const hip = [lateralX(v, sideOf, HIP_X), h.hip + bob];
1823
+ const lift = gait ? Math.max(0, Math.cos(stepOf(sideOf))) * gait.lift : 0;
1824
+ const reach = gait ? Math.sin(gait.stride * DEG$1) * LEG * swingOf(sideOf) : 0;
1792
1825
  const foot = [hip[0] + v.forward[0] * reach, -bob * .2 + v.forward[1] * reach - lift];
1793
1826
  return [
1794
1827
  hip,
1795
- [(hip[0] + foot[0]) / 2 + v.forward[0] * lift * .9, (hip[1] + foot[1]) / 2 + v.forward[1] * lift * .9],
1828
+ [(hip[0] + foot[0]) / 2 + v.forward[0] * lift, (hip[1] + foot[1]) / 2 + v.forward[1] * lift],
1796
1829
  foot
1797
1830
  ];
1798
1831
  },
1799
1832
  arm: (sideOf) => {
1800
- const shoulder = [lateralX(v, sideOf, 15.5 * v.width) + v.forward[0] * 1.5, h.shoulder + 4 + bob];
1801
- const out = sideOf * v.front * 2.2;
1833
+ const shoulder = shoulderAt(v, h, sideOf, bob);
1834
+ const out = sideOf * v.front * 3.4;
1802
1835
  if (pose === "cheer" || pose === "wave" && sideOf === -1) {
1803
- const wave = pose === "wave" ? Math.sin(phase * Math.PI * 2) * 4 : 0;
1804
- const elbow = [shoulder[0] + out * 3, shoulder[1] - 10];
1836
+ const wave = pose === "wave" ? Math.sin(phase * Math.PI * 2) * 3 : 0;
1837
+ const elbow = [shoulder[0] + out * 2.2, shoulder[1] - 7];
1805
1838
  return [
1806
1839
  shoulder,
1807
1840
  elbow,
1808
- [elbow[0] + out * 1.5 + wave, elbow[1] - 12]
1841
+ [elbow[0] + out * .8 + wave, elbow[1] - 10]
1809
1842
  ];
1810
1843
  }
1811
- const swing = isWalking ? -swingOf(sideOf) * (isRun ? 1.3 : 1) : Math.sin(phase * Math.PI * 2) * .06;
1812
- const reach = Math.sin(24 * DEG$1) * swing;
1844
+ const swing = gait ? -swingOf(sideOf) * (pose === "run" ? 1.4 : 1.1) : Math.sin(phase * Math.PI * 2) * .06;
1845
+ const reach = Math.sin(30 * DEG$1) * swing;
1813
1846
  const elbow = [shoulder[0] + out + v.forward[0] * UPPER_ARM * reach * .6, shoulder[1] + UPPER_ARM + v.forward[1] * UPPER_ARM * reach];
1814
1847
  return [
1815
1848
  shoulder,
1816
1849
  elbow,
1817
- [elbow[0] + out * .4 + v.forward[0] * FOREARM * reach * 1.6, elbow[1] + FOREARM - Math.abs(reach) * 3 + v.forward[1] * FOREARM * reach]
1850
+ [elbow[0] + out * .35 + v.forward[0] * FOREARM * reach * 1.6, elbow[1] + FOREARM - Math.abs(reach) * 2 + v.forward[1] * FOREARM * reach]
1818
1851
  ];
1819
1852
  }
1820
1853
  };
1821
1854
  }
1822
1855
  function seatedLimbs(v, h, pose) {
1823
- const thigh = [v.forward[0] * 15, v.forward[1] * 16];
1856
+ const thigh = [v.forward[0] * THIGH, v.forward[1] * 10];
1824
1857
  return {
1825
1858
  leg: (sideOf) => {
1826
- const hip = [lateralX(v, sideOf, 6), h.hip];
1859
+ const hip = [lateralX(v, sideOf, HIP_X), h.hip];
1827
1860
  const knee = add(hip, thigh);
1828
1861
  return [
1829
1862
  hip,
1830
1863
  knee,
1831
- [knee[0] + v.forward[0] * 3, 24]
1864
+ [knee[0] + v.forward[0] * 2, 11]
1832
1865
  ];
1833
1866
  },
1834
1867
  arm: (sideOf) => {
1835
- const shoulder = [lateralX(v, sideOf, 15.5 * v.width) + v.forward[0] * 1.5, h.shoulder + 4];
1868
+ const shoulder = shoulderAt(v, h, sideOf, 0);
1836
1869
  return [
1837
1870
  shoulder,
1838
- [shoulder[0] + sideOf * v.front * 2 + v.forward[0] * 3, shoulder[1] + UPPER_ARM],
1839
- add([lateralX(v, sideOf, pose === "play" ? 4 : 7), h.hip - 4], thigh, pose === "play" ? .9 : .6)
1871
+ [shoulder[0] + sideOf * v.front * 3 + v.forward[0] * 2, shoulder[1] + UPPER_ARM],
1872
+ add([lateralX(v, sideOf, pose === "play" ? 4 : 8), h.hip - 3], thigh, pose === "play" ? .9 : .6)
1840
1873
  ];
1841
1874
  }
1842
1875
  };
1843
1876
  }
1877
+ /** The long-hair split in head space: a little above the torso's top edge (the first walker's line sat 12 head units above it). */
1878
+ function shoulderLineOf(torsoTop, headY) {
1879
+ return (torsoTop - 3 - headY) / WALKER_HEAD_SCALE - 12;
1880
+ }
1844
1881
  /** The walker's drawing for a pose, at a facing angle (0 = S, 180 = N; west facings are mirrored by the caller). */
1845
1882
  function walkerLayout(c, pose, angleDeg, phase, outfit = outfitFor(c)) {
1846
1883
  const v = viewOf(angleDeg);
1847
1884
  const isSitting = pose === "sit" || pose === "play";
1848
- const h = isSitting ? SEATED : STANDING;
1885
+ const h = isSitting ? SEATED : WALKER_STANDING;
1849
1886
  const cycle = Math.sin(phase * Math.PI * 2);
1850
- const isWalking = pose === "walk" || pose === "run" || pose === "dance";
1851
- const hop = pose === "jump" || pose === "cheer" ? Math.abs(Math.sin(phase * Math.PI)) * (pose === "jump" ? 16 : 8) : 0;
1852
- const bob = isWalking ? -2.2 * (1 - Math.abs(cycle)) : pose === "idle" ? cycle * .6 : 0;
1887
+ const gait = isSitting ? null : gaitOf(pose);
1888
+ const hop = pose === "jump" || pose === "cheer" ? Math.abs(Math.sin(phase * Math.PI)) * (pose === "jump" ? 12 : 6) : 0;
1889
+ const bob = gait ? -gait.bounce * (1 - Math.abs(cycle)) : pose === "idle" ? cycle * .6 : 0;
1890
+ const rock = gait ? cycle * gait.rock * (.4 + .6 * Math.abs(v.front)) : 0;
1853
1891
  const limbs = isSitting ? seatedLimbs(v, h, pose) : standingLimbs(v, h, pose, phase, bob);
1854
1892
  const o = outfit;
1855
1893
  const [farSide, nearSide] = nearness(v, 1) > nearness(v, -1) ? [-1, 1] : [1, -1];
@@ -1863,7 +1901,7 @@ function walkerLayout(c, pose, angleDeg, phase, outfit = outfitFor(c)) {
1863
1901
  elbow,
1864
1902
  wrist
1865
1903
  ], ARM_W, c.skin, c.skinInk),
1866
- ...limb([shoulder, add(shoulder, [elbow[0] - shoulder[0], elbow[1] - shoulder[1]], .62)], SLEEVE_W, c.body, c.bodyInk),
1904
+ ...limb([shoulder, add(shoulder, [elbow[0] - shoulder[0], elbow[1] - shoulder[1]], .7)], SLEEVE_W, c.body, c.bodyInk),
1867
1905
  hand(c, wrist)
1868
1906
  ];
1869
1907
  };
@@ -1872,29 +1910,33 @@ function walkerLayout(c, pose, angleDeg, phase, outfit = outfitFor(c)) {
1872
1910
  const foot = points[2];
1873
1911
  return [...limb(points, LEG_W, o.trousers, o.trousersInk), shoe(foot, v.forward, o)];
1874
1912
  };
1875
- const torso = shirt(c, v.width, h.shoulder + (isWalking ? bob : 0), h.hip + 3 + (isWalking ? bob : 0), lean, v.isBack);
1913
+ const torsoTop = h.shoulder + bob;
1914
+ const torso = shirt(c, v.width, torsoTop, h.hip + 3 + bob, lean, v.isBack);
1876
1915
  const isTucked = isSitting && v.isBack;
1877
1916
  const behind = isTucked ? [...armNodes(farSide), ...armNodes(nearSide)] : isTurned ? armNodes(farSide) : [];
1878
1917
  const isLegsFirst = v.isBack || !isSitting;
1879
1918
  const legs = isTucked ? [] : [...legNodes(farSide), ...legNodes(nearSide)];
1880
1919
  const body = [
1881
1920
  ...isLegsFirst ? legs : [],
1882
- hips(o, v.width, h.hip + (isWalking ? bob : 0)),
1883
- neck(c, h.shoulder + bob, v.forward[0] * 2),
1921
+ hips(o, v.width, h.hip + bob),
1922
+ neck(c, torsoTop, v.forward[0] * 2),
1884
1923
  ...torso,
1885
1924
  ...isLegsFirst ? [] : legs,
1886
1925
  ...isTucked ? [] : isTurned ? armNodes(nearSide) : [...armNodes(-1), ...armNodes(1)]
1887
1926
  ];
1888
- const prop = add([0, h.hip - 12], v.forward, 8);
1927
+ const rocked = (nodes) => rock === 0 || nodes.length === 0 ? nodes : [g(nodes, { tf: [rot(rock)] })];
1928
+ const headY = h.head + bob * 1.1;
1929
+ const prop = add([0, h.hip - 6], v.forward, 8);
1889
1930
  return {
1890
- behind,
1891
- body,
1931
+ behind: rocked(behind),
1932
+ body: rocked(body),
1892
1933
  front: [],
1893
- headDx: v.forward[0] * 1.5,
1934
+ headDx: v.forward[0] * 1.5 - headY * Math.sin(rock * DEG$1),
1894
1935
  headDy: 0,
1895
- headY: h.head + bob * 1.1,
1936
+ headY,
1896
1937
  headScale: WALKER_HEAD_SCALE,
1897
- tilt: isWalking ? v.side * 2 : 0,
1938
+ shoulderLine: shoulderLineOf(torsoTop, headY),
1939
+ tilt: (gait ? v.side * 2 : 0) + rock,
1898
1940
  lift: hop,
1899
1941
  prop: {
1900
1942
  x: prop[0],
@@ -2021,24 +2063,37 @@ function backOfHead(phi) {
2021
2063
  half: { shapes: [rect(-200, -200, 200 + cx, 400, {})] }
2022
2064
  };
2023
2065
  }
2024
- const ABOVE_SHOULDERS = { shapes: [rect(-200, -200, 400, 237, {}), path(HEAD_PATH.d(), {})] };
2025
- const BELOW_SHOULDERS = { shapes: [rect(-200, 35, 400, 300, {})] };
2066
+ const splits = /* @__PURE__ */ new Map();
2067
+ /** The split at a shoulder line (head space), cached per line. */
2068
+ function shoulderSplit(line) {
2069
+ const at = Math.round(line * 4) / 4;
2070
+ let split = splits.get(at);
2071
+ if (!split) {
2072
+ split = {
2073
+ y: at,
2074
+ above: { shapes: [rect(-200, -200, 400, 200 + at + 1, {}), path(HEAD_PATH.d(), {})] },
2075
+ below: { shapes: [rect(-200, at - 1, 400, 300, {})] }
2076
+ };
2077
+ splits.set(at, split);
2078
+ }
2079
+ return split;
2080
+ }
2026
2081
  /** True when a layer reaches below the shoulder line. */
2027
- function hangs(shapes) {
2028
- return shapes?.some((s) => s.d.bottom() > 36) === true;
2082
+ function hangs(shapes, split) {
2083
+ return shapes?.some((s) => s.d.bottom() > split.y) === true;
2029
2084
  }
2030
2085
  /** Splits a hair layer at the shoulders: the part above stays in `over`, the part below goes in `behind`. */
2031
- function splitAtShoulders(nodes, doesHang, over, behind) {
2086
+ function splitAtShoulders(nodes, split, doesHang, over, behind) {
2032
2087
  if (nodes.length === 0) return;
2033
2088
  if (!doesHang) {
2034
2089
  over.push(...nodes);
2035
2090
  return;
2036
2091
  }
2037
- over.push(g(nodes, { clip: ABOVE_SHOULDERS }));
2038
- behind.push(g(nodes, { clip: BELOW_SHOULDERS }));
2092
+ over.push(g(nodes, { clip: split.above }));
2093
+ behind.push(g(nodes, { clip: split.below }));
2039
2094
  }
2040
2095
  /** The front and side views of the head. */
2041
- function frontHead(ctx, p, isPlaying, status, angle, gazeX) {
2096
+ function frontHead(ctx, p, isPlaying, status, angle, gazeX, split) {
2042
2097
  const hatCut = p.hat && typeof p.hat.hides === "number" ? { shapes: [rect(-120, p.hat.hides, 240, 260, {})] } : void 0;
2043
2098
  const isHairShown = !(p.hat && p.hat.hides === "all");
2044
2099
  const cut = (nodes) => hatCut && nodes.length > 0 ? [g(nodes, { clip: hatCut })] : nodes;
@@ -2058,7 +2113,7 @@ function frontHead(ctx, p, isPlaying, status, angle, gazeX) {
2058
2113
  const shift = (px, py) => [px * (.92 + .08 * co) - turnS * 5, py];
2059
2114
  const clips = backOfHead(ctx.phi);
2060
2115
  const onFarSide = (nodes) => nodes.length > 0 ? [g([g(nodes, { clip: clips.crescent })], { clip: clips.half })] : [];
2061
- if (isHairShown) splitAtShoulders(onFarSide(cut(paintShapes(ctx.c, hairTie, p.hair?.rear, shift))), hangs(p.hair?.rear), out, behindBody);
2116
+ if (isHairShown) splitAtShoulders(onFarSide(cut(paintShapes(ctx.c, hairTie, p.hair?.rear, shift))), split, hangs(p.hair?.rear, split), out, behindBody);
2062
2117
  if (p.hat) out.push(...onFarSide(paintShapes(ctx.c, hatTones, p.hat.rear, shift)));
2063
2118
  }
2064
2119
  if (!p.hat?.hidesEars) out.push(...ears(ctx, "front", p.earExtra));
@@ -2075,7 +2130,7 @@ function frontHead(ctx, p, isPlaying, status, angle, gazeX) {
2075
2130
  out.push(...nose(ctx), ...mouth(ctx));
2076
2131
  out.push(...paintShapes(ctx.c, hairTie, p.beard?.moustache, faceWarp));
2077
2132
  if (isHairShown) {
2078
- splitAtShoulders(cut(paintShapes(ctx.c, hairTie, p.hair?.front, warpFront)), hangs(p.hair?.front), out, behindBody);
2133
+ splitAtShoulders(cut(paintShapes(ctx.c, hairTie, p.hair?.front, warpFront)), split, hangs(p.hair?.front, split), out, behindBody);
2079
2134
  if (p.hair?.front?.some((s) => s.clip === "head")) out.push(path(HEAD_PATH.d(), {
2080
2135
  fill: "none",
2081
2136
  stroke: ctx.c.skinInk,
@@ -2156,21 +2211,23 @@ function anchorsFor(headX, headY, top, prop, mirror) {
2156
2211
  pod: at(0, 14)
2157
2212
  };
2158
2213
  }
2159
- /** The body for a figure: its layout, where the head centre goes, and the head's scale. */
2214
+ /** The body for a figure. */
2160
2215
  function figureLayout(c, opts, pose, phi, angle, phase) {
2161
2216
  if (isWalker(opts)) {
2162
2217
  const walker = walkerLayout(c, pose, angle, phase);
2163
2218
  return {
2164
2219
  b: walker,
2165
2220
  headY: walker.headY,
2166
- headScale: walker.headScale
2221
+ headScale: walker.headScale,
2222
+ shoulderLine: walker.shoulderLine
2167
2223
  };
2168
2224
  }
2169
2225
  const b = bodyLayout(c, pose, phi, phase, opts.stool === true);
2170
2226
  return {
2171
2227
  b,
2172
2228
  headY: -96 + b.headDy,
2173
- headScale: 1
2229
+ headScale: 1,
2230
+ shoulderLine: 36
2174
2231
  };
2175
2232
  }
2176
2233
  /** Composes a look (or `null` for the silhouette) with a pose. */
@@ -2184,7 +2241,7 @@ function composeAvatar(look, opts = {}) {
2184
2241
  if (isWalker(opts)) {
2185
2242
  const shape = walkerSilhouette();
2186
2243
  return {
2187
- nodes: [...hasShadow ? [groundShadow(0, 18)] : [], ...shape.nodes],
2244
+ nodes: [...hasShadow ? [groundShadow(0, 22)] : [], ...shape.nodes],
2188
2245
  anchors: anchorsFor(0, shape.headY, shape.top, {
2189
2246
  x: 0,
2190
2247
  y: -40
@@ -2220,12 +2277,12 @@ function composeAvatar(look, opts = {}) {
2220
2277
  blink: pose.blink ?? 0,
2221
2278
  talk: pose.talk ?? 0
2222
2279
  };
2223
- const { b, headY: bodyHeadY, headScale } = figureLayout(parts.c, opts, bodyPose, ctx.phi, angle, pose.phase ?? .25);
2280
+ const { b, headY: bodyHeadY, headScale, shoulderLine } = figureLayout(parts.c, opts, bodyPose, ctx.phi, angle, pose.phase ?? .25);
2224
2281
  const headX = b.headDx + (pose.bob?.x ?? 0);
2225
2282
  const headY = bodyHeadY + (pose.bob?.y ?? 0);
2226
2283
  const tilt = face.headTilt + b.tilt + (pose.tilt ?? 0);
2227
2284
  const sq = face.squash + (pose.squash ?? 0);
2228
- const drawn = isBack ? backHead(ctx, parts, isPlaying, status, angle) : frontHead(ctx, parts, isPlaying, status, angle, gaze.x);
2285
+ const drawn = isBack ? backHead(ctx, parts, isPlaying, status, angle) : frontHead(ctx, parts, isPlaying, status, angle, gaze.x, shoulderSplit(shoulderLine));
2229
2286
  const headNodes = drawn.head;
2230
2287
  const top = headY - (50 + Math.max(parts.hair?.top ?? 0, parts.hat?.top ?? 0)) * headScale;
2231
2288
  if (status === "crown") headNodes.push(...crown(Math.max(parts.hair?.top ?? 0, parts.hat?.top ?? 0)));
@@ -2250,7 +2307,7 @@ function composeAvatar(look, opts = {}) {
2250
2307
  ];
2251
2308
  const lifted = b.lift > 0 ? [g(character, { tf: [tr(0, -b.lift)] })] : character;
2252
2309
  const rimmed = opts.rim ? [g(lifted, { rim: true })] : lifted;
2253
- const all = [...hasShadow ? [groundShadow(b.lift, headScale === 1 ? 36 : 18)] : [], ...rimmed];
2310
+ const all = [...hasShadow ? [groundShadow(b.lift, headScale === 1 ? 36 : 22)] : [], ...rimmed];
2254
2311
  const mirror = isMirrored ? -1 : 1;
2255
2312
  return {
2256
2313
  nodes: isMirrored ? [g(all, { tf: [sc(-1, 1)] })] : all,