@originkit/avatars 0.0.0-stage → 0.2.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.
Files changed (171) hide show
  1. package/GUIDE.md +44 -0
  2. package/LICENSE +58 -0
  3. package/README.md +141 -2
  4. package/bin/avatars.js +65 -0
  5. package/dist/art.d.ts +43 -0
  6. package/dist/art.js +35 -0
  7. package/dist/auras/calming.d.ts +18 -0
  8. package/dist/auras/calming.js +30 -0
  9. package/dist/auras/controls.d.ts +2 -0
  10. package/dist/auras/controls.js +60 -0
  11. package/dist/auras/index.d.ts +119 -0
  12. package/dist/auras/index.js +355 -0
  13. package/dist/auras/skies.d.ts +13 -0
  14. package/dist/auras/skies.js +22 -0
  15. package/dist/blobs/color.d.ts +19 -0
  16. package/dist/blobs/color.js +113 -0
  17. package/dist/blobs/controls.d.ts +2 -0
  18. package/dist/blobs/controls.js +77 -0
  19. package/dist/blobs/index.d.ts +5 -0
  20. package/dist/blobs/index.js +107 -0
  21. package/dist/blobs/paths.d.ts +33 -0
  22. package/dist/blobs/paths.js +102 -0
  23. package/dist/blobs/shapes.d.ts +46 -0
  24. package/dist/blobs/shapes.js +186 -0
  25. package/dist/blobs/variance.d.ts +22 -0
  26. package/dist/blobs/variance.js +42 -0
  27. package/dist/bots/attachments.d.ts +24 -0
  28. package/dist/bots/attachments.js +119 -0
  29. package/dist/bots/calming.d.ts +4 -0
  30. package/dist/bots/calming.js +19 -0
  31. package/dist/bots/controls.d.ts +2 -0
  32. package/dist/bots/controls.js +78 -0
  33. package/dist/bots/faces.d.ts +23 -0
  34. package/dist/bots/faces.js +122 -0
  35. package/dist/bots/heads.d.ts +15 -0
  36. package/dist/bots/heads.js +66 -0
  37. package/dist/bots/index.d.ts +5 -0
  38. package/dist/bots/index.js +47 -0
  39. package/dist/bots/palette.d.ts +15 -0
  40. package/dist/bots/palette.js +30 -0
  41. package/dist/bots/paths.d.ts +25 -0
  42. package/dist/bots/paths.js +89 -0
  43. package/dist/bots/trim.d.ts +12 -0
  44. package/dist/bots/trim.js +37 -0
  45. package/dist/bubbles/calming.d.ts +7 -0
  46. package/dist/bubbles/calming.js +19 -0
  47. package/dist/bubbles/controls.d.ts +2 -0
  48. package/dist/bubbles/controls.js +69 -0
  49. package/dist/bubbles/index.d.ts +65 -0
  50. package/dist/bubbles/index.js +279 -0
  51. package/dist/buddies/calming.d.ts +8 -0
  52. package/dist/buddies/calming.js +18 -0
  53. package/dist/buddies/controls.d.ts +2 -0
  54. package/dist/buddies/controls.js +35 -0
  55. package/dist/buddies/index.d.ts +113 -0
  56. package/dist/buddies/index.js +363 -0
  57. package/dist/colors.d.ts +20 -0
  58. package/dist/colors.js +41 -0
  59. package/dist/combinations.d.ts +9 -0
  60. package/dist/combinations.js +28 -0
  61. package/dist/container.d.ts +14 -0
  62. package/dist/container.js +67 -0
  63. package/dist/controls.d.ts +125 -0
  64. package/dist/controls.js +220 -0
  65. package/dist/critters/calming.d.ts +8 -0
  66. package/dist/critters/calming.js +18 -0
  67. package/dist/critters/controls.d.ts +2 -0
  68. package/dist/critters/controls.js +49 -0
  69. package/dist/critters/index.d.ts +113 -0
  70. package/dist/critters/index.js +307 -0
  71. package/dist/doodles/calming.d.ts +7 -0
  72. package/dist/doodles/calming.js +18 -0
  73. package/dist/doodles/controls.d.ts +2 -0
  74. package/dist/doodles/controls.js +50 -0
  75. package/dist/doodles/index.d.ts +66 -0
  76. package/dist/doodles/index.js +236 -0
  77. package/dist/faces/adventurer-neutral.json +3168 -0
  78. package/dist/faces/calming.d.ts +8 -0
  79. package/dist/faces/calming.js +20 -0
  80. package/dist/faces/controls.d.ts +2 -0
  81. package/dist/faces/controls.js +38 -0
  82. package/dist/faces/index.d.ts +44 -0
  83. package/dist/faces/index.js +108 -0
  84. package/dist/glances/calming.d.ts +8 -0
  85. package/dist/glances/calming.js +16 -0
  86. package/dist/glances/controls.d.ts +2 -0
  87. package/dist/glances/controls.js +27 -0
  88. package/dist/glances/index.d.ts +54 -0
  89. package/dist/glances/index.js +79 -0
  90. package/dist/glyphs/calming.d.ts +8 -0
  91. package/dist/glyphs/calming.js +17 -0
  92. package/dist/glyphs/controls.d.ts +2 -0
  93. package/dist/glyphs/controls.js +55 -0
  94. package/dist/glyphs/index.d.ts +62 -0
  95. package/dist/glyphs/index.js +197 -0
  96. package/dist/grins/calming.d.ts +8 -0
  97. package/dist/grins/calming.js +21 -0
  98. package/dist/grins/controls.d.ts +2 -0
  99. package/dist/grins/controls.js +33 -0
  100. package/dist/grins/index.d.ts +81 -0
  101. package/dist/grins/index.js +240 -0
  102. package/dist/gummies/art.d.ts +77 -0
  103. package/dist/gummies/art.js +168 -0
  104. package/dist/gummies/calming.d.ts +8 -0
  105. package/dist/gummies/calming.js +21 -0
  106. package/dist/gummies/controls.d.ts +2 -0
  107. package/dist/gummies/controls.js +53 -0
  108. package/dist/gummies/index.d.ts +37 -0
  109. package/dist/gummies/index.js +283 -0
  110. package/dist/history.d.ts +21 -0
  111. package/dist/history.js +48 -0
  112. package/dist/index.d.ts +13 -0
  113. package/dist/index.js +16 -0
  114. package/dist/kinds.d.ts +30 -0
  115. package/dist/kinds.js +266 -0
  116. package/dist/kitties/calming.d.ts +8 -0
  117. package/dist/kitties/calming.js +19 -0
  118. package/dist/kitties/controls.d.ts +2 -0
  119. package/dist/kitties/controls.js +43 -0
  120. package/dist/kitties/index.d.ts +181 -0
  121. package/dist/kitties/index.js +673 -0
  122. package/dist/knits/calming.d.ts +7 -0
  123. package/dist/knits/calming.js +19 -0
  124. package/dist/knits/controls.d.ts +2 -0
  125. package/dist/knits/controls.js +32 -0
  126. package/dist/knits/index.d.ts +74 -0
  127. package/dist/knits/index.js +445 -0
  128. package/dist/mallows/calming.d.ts +8 -0
  129. package/dist/mallows/calming.js +19 -0
  130. package/dist/mallows/controls.d.ts +2 -0
  131. package/dist/mallows/controls.js +45 -0
  132. package/dist/mallows/index.d.ts +111 -0
  133. package/dist/mallows/index.js +454 -0
  134. package/dist/peeks/calming.d.ts +8 -0
  135. package/dist/peeks/calming.js +21 -0
  136. package/dist/peeks/controls.d.ts +2 -0
  137. package/dist/peeks/controls.js +54 -0
  138. package/dist/peeks/index.d.ts +80 -0
  139. package/dist/peeks/index.js +225 -0
  140. package/dist/pixels/calming.d.ts +8 -0
  141. package/dist/pixels/calming.js +22 -0
  142. package/dist/pixels/controls.d.ts +3 -0
  143. package/dist/pixels/controls.js +82 -0
  144. package/dist/pixels/index.d.ts +34 -0
  145. package/dist/pixels/index.js +174 -0
  146. package/dist/pixels/palette.d.ts +29 -0
  147. package/dist/pixels/palette.js +107 -0
  148. package/dist/pixels/sprites.d.ts +42 -0
  149. package/dist/pixels/sprites.js +154 -0
  150. package/dist/possibilities.d.ts +39 -0
  151. package/dist/possibilities.js +88 -0
  152. package/dist/shapes/calming.d.ts +7 -0
  153. package/dist/shapes/calming.js +19 -0
  154. package/dist/shapes/controls.d.ts +2 -0
  155. package/dist/shapes/controls.js +41 -0
  156. package/dist/shapes/index.d.ts +44 -0
  157. package/dist/shapes/index.js +104 -0
  158. package/dist/share.d.ts +21 -0
  159. package/dist/share.js +83 -0
  160. package/dist/traits.d.ts +34 -0
  161. package/dist/traits.js +58 -0
  162. package/dist/types.d.ts +99 -0
  163. package/dist/types.js +1 -0
  164. package/dist/variants.d.ts +13 -0
  165. package/dist/variants.js +43 -0
  166. package/mcp/server.js +7 -0
  167. package/mcp/tools.js +70 -0
  168. package/package.json +68 -4
  169. package/react/index.d.ts +28 -0
  170. package/react/index.js +869 -0
  171. package/react/styles.js +1 -0
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Doodles: a face drawn in a few strokes on plain paper. Bold blob eyes and a tiny mouth
3
+ * carry the whole emotion. Every line wobbles a little, as if drawn by hand, and no two
4
+ * names wobble the same way.
5
+ */
6
+ import { contrast } from "../colors.js";
7
+ import { calmed } from "../possibilities.js";
8
+ import { createTraits } from "../traits.js";
9
+ import { CALMING } from "./calming.js";
10
+ /** Paper. White is the reference, and the rest are only just tinted, to stay minimal. */
11
+ export const TILES = ["#ffffff", "#fbf6ec", "#f2f2ef", "#fdeef0", "#eaf6ee", "#eaf2fb", "#f1ecfa", "#f7efe2", "#fff7d6"];
12
+ export const INKS = ["#111111", "#2b2d42", "#4a2c1d", "#1f3a5f", "#ffffff"];
13
+ const round2 = (value) => Math.round(value * 100) / 100;
14
+ const point = (x, y) => `${round2(x)} ${round2(y)}`;
15
+ /** A filled blob, its edge wobbling gently. Points above `cut` are flattened onto it. */
16
+ function blob(pen, key, cx, cy, rx, ry, cut) {
17
+ const { t, wobble } = pen;
18
+ const waves = [2, 3, 5].map((count) => [count, t.jitter(`${key}.a${count}`, wobble), t.num(`${key}.p${count}`, 0, 2 * Math.PI)]);
19
+ const points = Array.from({ length: 32 }, (_, i) => {
20
+ const angle = (i / 32) * 2 * Math.PI;
21
+ const r = 1 + waves.reduce((sum, [count, amount, start]) => sum + amount * Math.cos(count * angle + start), 0);
22
+ const x = cx + rx * r * Math.cos(angle);
23
+ let y = cy + ry * r * Math.sin(angle);
24
+ if (cut)
25
+ y = Math.max(y, cy + cut.at * ry + cut.slope * (x - cx));
26
+ return [x, y];
27
+ });
28
+ // Through the midpoints, so even the flattened edge has softly rounded ends.
29
+ const mid = (i) => {
30
+ const [ax, ay] = points[i % points.length];
31
+ const [bx, by] = points[(i + 1) % points.length];
32
+ return point((ax + bx) / 2, (ay + by) / 2);
33
+ };
34
+ return `<path d="M${mid(0)}${points.map((_, i) => `Q${point(...points[(i + 1) % points.length])} ${mid(i + 1)}`).join("")}Z"/>`;
35
+ }
36
+ /** A stroke, with its middle nudged off true by the pen. */
37
+ function stroke(pen, key, d) {
38
+ const nudge = (index) => round2(pen.t.jitter(`${key}.n${index}`, pen.wobble * 10));
39
+ let index = 0;
40
+ // Only the curve's control points move, so ends stay where the face needs them.
41
+ const drawn = d.replace(/Q(-?[\d.]+) (-?[\d.]+)/g, (_, x, y) => `Q${round2(Number(x) + nudge(index++))} ${round2(Number(y) + nudge(index++))}`);
42
+ return `<path d="${drawn}" fill="none"/>`;
43
+ }
44
+ const EYES = {
45
+ dot: (pen, { x, y, r }, key) => blob(pen, key, x, y, r * 0.82, r * 0.92),
46
+ big: (pen, { x, y, r }, key) => blob(pen, key, x, y, r * 1.08, r * 1.14),
47
+ // A blob with its top cut away on a slant, falling towards the nose.
48
+ angry: (pen, { x, y, r, side }, key) => blob(pen, key, x, y, r * 1.02, r * 0.95, { at: -0.18, slope: -side * 0.5 }),
49
+ flat: (pen, { x, y, r }, key) => blob(pen, key, x, y, r * 1.08, r * 0.92, { at: -0.05, slope: 0 }) +
50
+ stroke(pen, `${key}.lid`, `M${point(x - r * 1.25, y - r * 0.05)}Q${point(x, y - r * 0.12)} ${point(x + r * 1.25, y - r * 0.05)}`),
51
+ ring: (pen, { x, y, r }, key) => `<circle cx="${round2(x)}" cy="${round2(y)}" r="${round2(r * 0.95)}" fill="none"/>` + blob(pen, key, x, y - r * 0.12, r * 0.44, r * 0.48),
52
+ arc: (pen, { x, y, r }, key) => stroke(pen, key, `M${point(x - r, y + r * 0.35)}Q${point(x, y - r * 1.1)} ${point(x + r, y + r * 0.35)}`),
53
+ closed: (pen, { x, y, r }, key) => stroke(pen, key, `M${point(x - r, y - r * 0.15)}Q${point(x, y + r * 0.85)} ${point(x + r, y - r * 0.15)}`),
54
+ heart: (_pen, { x, y, r }) => {
55
+ const s = r * 0.11;
56
+ return `<path d="M${point(x, y + 8 * s)}C${point(x - 11 * s, y)} ${point(x - 8 * s, y - 9 * s)} ${point(x, y - 4 * s)}C${point(x + 8 * s, y - 9 * s)} ${point(x + 11 * s, y)} ${point(x, y + 8 * s)}Z"/>`;
57
+ },
58
+ cross: (pen, { x, y, r }, key) => stroke(pen, key, `M${point(x - r * 0.75, y - r * 0.75)}Q${point(x, y)} ${point(x + r * 0.75, y + r * 0.75)}`) +
59
+ stroke(pen, `${key}.b`, `M${point(x + r * 0.75, y - r * 0.75)}Q${point(x, y)} ${point(x - r * 0.75, y + r * 0.75)}`),
60
+ teary: (pen, eye, key) => EYES.dot(pen, eye, key) +
61
+ `<path d="M${point(eye.x + eye.side * eye.r * 0.2, eye.y + eye.r * 1.2)}q-1.6 2.6 0 4q1.6 -1.4 0 -4Z" fill="none"/>`,
62
+ // Each tip reaches a little further or shorter, by hand.
63
+ star: (pen, { x, y, r }, key) => {
64
+ const tips = Array.from({ length: 10 }, (_, i) => {
65
+ const angle = -Math.PI / 2 + (i * Math.PI) / 5;
66
+ const reach = r * (i % 2 ? 0.45 : 1.05) * (1 + pen.t.jitter(`${key}.t${i}`, pen.wobble * 1.5));
67
+ return point(x + reach * Math.cos(angle), y + reach * Math.sin(angle));
68
+ });
69
+ return `<path d="M${tips.join("L")}Z"/>`;
70
+ },
71
+ // Under two turns in from the edge, in a finer line, so the gaps stay open at any weight.
72
+ spiral: (pen, { x, y, r, side }, key) => {
73
+ const turns = 1.75 * 2 * Math.PI;
74
+ const waver = pen.t.jitter(`${key}.w`, pen.wobble * 2);
75
+ const points = Array.from({ length: 33 }, (_, i) => {
76
+ const angle = (i / 32) * turns;
77
+ const reach = (r * 1.05 * (1 - i / 32) + r * 0.1) * (1 + waver * Math.sin(angle * 1.5));
78
+ return point(x + side * reach * Math.cos(angle), y + reach * Math.sin(angle));
79
+ });
80
+ return `<path d="M${points.join("L")}" fill="none" stroke-width="2.2"/>`;
81
+ },
82
+ };
83
+ const brow = (pen, { x, y, r, side }, key, lift) => stroke(pen, key, `M${point(x - r * 1.1, y - r * (1.75 + lift * side))}Q${point(x, y - r * 2.15)} ${point(x + r * 1.1, y - r * (1.75 - lift * side))}`);
84
+ /** A mouth around the origin, as path data, whether it is filled rather than drawn, and any finer line. */
85
+ const MOUTHS = {
86
+ fangs: ["M-5 0Q0 0.6 5 0M-3.3 0.2l1.3 2.8l1.3 -2.8M0.7 0.2l1.3 2.8l1.3 -2.8"],
87
+ cat: ["M-5 -1Q-2.5 3.8 0 0Q2.5 3.8 5 -1"],
88
+ frown: ["M-3.8 2.2Q0 -2.2 3.8 2.2"],
89
+ wavy: ["M-4.5 1Q-2.2 -2 0 0.6Q2.2 3 4.5 -0.6"],
90
+ smile: ["M-4.5 -1Q0 4.6 4.5 -1"],
91
+ oh: ["M-3 1.4a3 3.6 0 1 0 6 0a3 3.6 0 1 0 -6 0Z"],
92
+ line: ["M-3 0.5Q0 0.2 3 0.5"],
93
+ smirk: ["M-4.5 1Q1 2.6 4.5 -2"],
94
+ grin: ["M-5.5 -0.5Q0 0 5.5 -0.5Q5 6 0 6.2Q-5 6 -5.5 -0.5Z", true],
95
+ tongue: ["M-4.5 -1Q0 4 4.5 -1M0.5 2.2Q1 6 3.4 5.4Q5 4.4 3.6 1.4"],
96
+ kiss: ["M-1.2 -3Q3 -2.2 0 0Q3 2.2 -1.2 3"],
97
+ shout: ["M-4.2 3.2Q-4 -2.8 0 -3Q4 -2.8 4.2 3.2Q0 2 -4.2 3.2Z", true],
98
+ // In a finer line, so the gaps between teeth stay open.
99
+ teeth: ["M-5.5 -2.4Q0 -2.7 5.5 -2.4Q5.8 0 5.5 2.4Q0 2.7 -5.5 2.4Q-5.8 0 -5.5 -2.4ZM-1.8 -2.4Q-1.7 0 -1.8 2.4M1.8 -2.4Q1.9 0 1.8 2.4", false, 1.3],
100
+ };
101
+ /** Every emotion. The first five are the ones in the reference. */
102
+ export const MOODS = [
103
+ { name: "Grumpy", eyes: ["angry", "angry"], mouth: "fangs" },
104
+ { name: "Cute", eyes: ["dot", "dot"], mouth: "cat" },
105
+ { name: "Skeptical", eyes: ["ring", "dot"], mouth: "wavy", brows: [-0.15, 0.45] },
106
+ { name: "Unimpressed", eyes: ["flat", "flat"], mouth: "frown" },
107
+ { name: "Mischief", eyes: ["flat", "angry"], mouth: "fangs" },
108
+ { name: "Happy", eyes: ["arc", "arc"], mouth: "smile" },
109
+ { name: "Shocked", eyes: ["big", "big"], mouth: "oh" },
110
+ { name: "Sleepy", eyes: ["closed", "closed"], mouth: "line" },
111
+ { name: "Smug", eyes: ["flat", "flat"], mouth: "smirk", brows: [null, -0.35] },
112
+ { name: "Teary", eyes: ["teary", "teary"], mouth: "wavy", brows: [0.3, -0.3] },
113
+ { name: "In love", eyes: ["heart", "heart"], mouth: "tongue" },
114
+ { name: "Dizzy", eyes: ["cross", "cross"], mouth: "grin" },
115
+ { name: "Wink", eyes: ["dot", "closed"], mouth: "smile" },
116
+ { name: "Starstruck", eyes: ["star", "star"], mouth: "grin" },
117
+ { name: "Worried", eyes: ["dot", "dot"], mouth: "frown", brows: [0.35, -0.35] },
118
+ { name: "Laughing", eyes: ["arc", "arc"], mouth: "grin" },
119
+ { name: "Surprised", eyes: ["ring", "ring"], mouth: "oh" },
120
+ { name: "Furious", eyes: ["angry", "angry"], mouth: "shout" },
121
+ { name: "Kissy", eyes: ["closed", "closed"], mouth: "kiss" },
122
+ { name: "Neutral", eyes: ["dot", "dot"], mouth: "line" },
123
+ { name: "Hypnotised", eyes: ["spiral", "spiral"], mouth: "wavy" },
124
+ { name: "Nervous", eyes: ["big", "big"], mouth: "teeth" },
125
+ { name: "Sly", eyes: ["flat", "flat"], mouth: "cat" },
126
+ { name: "Silly", eyes: ["dot", "big"], mouth: "tongue" },
127
+ ];
128
+ export const moodOf = (t) => t.pick("mood", MOODS);
129
+ /** Extras on some faces, each drawn in the same few strokes. One face in this many hundred has them. */
130
+ export const CHEEK_ODDS = 35;
131
+ export const CHEEKS = ["Rosy", "Hatched", "Freckles"];
132
+ export const NOSE_ODDS = 30;
133
+ export const NOSES = ["Dot", "Hook", "Line"];
134
+ const present = (t, key, odds, kinds) => (t(key) * 100 < odds ? t.pick(`${key}.kind`, kinds) : undefined);
135
+ /** Under the outer side of each eye. */
136
+ function drawCheeks(pen, eyes) {
137
+ const kind = present(pen.t, "cheeks", CHEEK_ODDS, CHEEKS);
138
+ if (!kind)
139
+ return "";
140
+ return eyes
141
+ .map(({ x, y, r, side }, i) => {
142
+ const cx = x + side * r * 0.35;
143
+ const cy = y + r * 1.75;
144
+ if (kind === "Rosy")
145
+ return `<ellipse cx="${round2(cx)}" cy="${round2(cy)}" rx="${round2(r * 0.62)}" ry="${round2(r * 0.36)}" fill="#ff8fa3" fill-opacity="0.55" stroke="none"/>`;
146
+ if (kind === "Freckles") {
147
+ return [-0.4, 0, 0.4]
148
+ .map((dx, j) => `<circle cx="${round2(cx + dx * r)}" cy="${round2(cy + (j === 1 ? -0.25 : 0.1) * r)}" r="0.7" stroke="none"/>`)
149
+ .join("");
150
+ }
151
+ return [-0.45, 0, 0.45]
152
+ .map((dx, j) => stroke(pen, `cheek${i}.${j}`, `M${point(cx + dx * r - 1.2, cy + 1.6)}Q${point(cx + dx * r, cy)} ${point(cx + dx * r + 1.2, cy - 1.6)}`))
153
+ .join("")
154
+ .replace(/<path /g, `<path stroke-width="1.6" `);
155
+ })
156
+ .join("");
157
+ }
158
+ /** Between the eyes and the mouth. */
159
+ function drawNose(pen, y) {
160
+ const kind = present(pen.t, "nose", NOSE_ODDS, NOSES);
161
+ if (!kind)
162
+ return "";
163
+ if (kind === "Dot")
164
+ return blob(pen, "nose", 0, y, 1.5, 1.2);
165
+ if (kind === "Hook")
166
+ return stroke(pen, "nose", `M${point(0.6, y - 2.6)}Q${point(-2.4, y + 1.4)} ${point(1.2, y + 1.6)}`);
167
+ return stroke(pen, "nose", `M${point(0, y - 2.2)}Q${point(0.4, y)} ${point(0, y + 2)}`);
168
+ }
169
+ /**
170
+ * Colourful faces draw each eye and the mouth in a different colour of one colourway, as in
171
+ * the reference. Each name takes three of the four, in its own order. Neighbours are unlike,
172
+ * as each variant steps to the next.
173
+ */
174
+ export const COLOURWAYS = [
175
+ { label: "Sorbet", colors: ["#fc8b8a", "#38aa85", "#fcc73a", "#263252"] },
176
+ { label: "Lagoon", colors: ["#1c7ed6", "#22b8cf", "#94d82d", "#0b4f6c"] },
177
+ { label: "Ember", colors: ["#e03131", "#f76707", "#fab005", "#5c3d2e"] },
178
+ { label: "Candy", colors: ["#ff5d8f", "#a06cd5", "#4cc9f0", "#ffbe0b"] },
179
+ { label: "Forest", colors: ["#2b9348", "#80b918", "#d4a373", "#354f52"] },
180
+ { label: "Berry", colors: ["#d63384", "#7048e8", "#ffa94d", "#4a1942"] },
181
+ { label: "Citrus", colors: ["#ff7b00", "#ffd60a", "#38b000", "#0077b6"] },
182
+ { label: "Dusk", colors: ["#3d5a80", "#ee6c4d", "#98c1d9", "#293241"] },
183
+ { label: "Rose", colors: ["#e56b6f", "#b56576", "#6d597a", "#eaac8b"] },
184
+ { label: "Mint", colors: ["#20c997", "#f783ac", "#4dabf7", "#495057"] },
185
+ ];
186
+ /** Each with a colour of its own, which can be picked by hand. Cheeks and nose follow the mouth. */
187
+ export const PARTS = ["left", "right", "mouth"];
188
+ /** The colours in use, each of which can be changed by hand. `parts` only on a colourful face. */
189
+ export function coloursOf(t) {
190
+ const tile = t.color("tile") ?? t.pick("tile", TILES);
191
+ const ink = t.color("ink") ?? INKS[0];
192
+ // Every face is in one ink until switched to colourful by hand.
193
+ if ((t.forced("colourful") ?? 1) >= 0.5)
194
+ return { tile, ink };
195
+ // The spare colour stands in for one that would fade into the tile, as yellow does on yellow.
196
+ const order = t.pick("colourway", COLOURWAYS).colors
197
+ .map((color, i) => [t(`colourway.${i}`) + (contrast(color, tile) < 1.5 ? 1 : 0), color])
198
+ .sort(([a], [b]) => a - b);
199
+ return { tile, ink, parts: Object.fromEntries(PARTS.map((part, i) => [part, t.color(part) ?? order[i][1]])) };
200
+ }
201
+ export function createDoodle(name, overrides) {
202
+ const t = calmed(createTraits(name, overrides), CALMING);
203
+ const mood = moodOf(t);
204
+ const { tile, ink, parts } = coloursOf(t);
205
+ const tint = (part) => (parts ? ` color="${parts[part]}"` : "");
206
+ const pen = { t, wobble: t.num("wobble", 0.02, 0.09) };
207
+ const r = t.num("eye.size", 8.4, 11);
208
+ const gap = t.num("eye.gap", 17.5, 23);
209
+ const eyeY = t.num("eye.y", -7, -3);
210
+ const eyes = [-1, 1].map((side) => ({ x: side * gap, y: eyeY + t.jitter(`eye.dy${side}`, 0.8), r: r * (1 + t.jitter(`eye.r${side}`, 0.06)), side }));
211
+ const drawEye = (eye, i) => `<g${tint(PARTS[i])}>${EYES[mood.eyes[i]](pen, eye, `eye${i}`)}${mood.brows?.[i] != null ? brow(pen, eye, `brow${i}`, mood.brows[i]) : ""}</g>`;
212
+ const [d, filled, fine] = MOUTHS[mood.mouth];
213
+ const size = t.num("mouth.size", 1.3, 1.7);
214
+ const mouthY = eyeY + t.num("mouth.y", 16, 21);
215
+ return {
216
+ tile,
217
+ ink,
218
+ face: `translate(${round2(50 + t.jitter("face.x", 4))} ${round2(52 + t.jitter("face.y", 4))}) rotate(${round2(t.jitter("tilt", 7))})`,
219
+ weight: round2(t.num("weight", 3.2, 4.2)),
220
+ eyes: eyes.map(drawEye).join(""),
221
+ mouth: `<g transform="translate(${round2(t.jitter("mouth.x", 2))} ${round2(mouthY)}) scale(${round2(size)})"${tint("mouth")}>` +
222
+ (filled ? `<path d="${d}" stroke="none"/>` : stroke(pen, "mouth", d).replace("<path ", fine ? `<path stroke-width="${fine}" ` : "<path ")) + `</g>`,
223
+ marks: `<g${tint("mouth")}>${drawCheeks(pen, eyes)}${drawNose(pen, round2(eyeY + (mouthY - eyeY) * 0.5))}</g>`,
224
+ mood: mood.name,
225
+ blinkDelay: t.num("blink", 0, 4),
226
+ };
227
+ }
228
+ /** How every line and blob of a face is painted. Colour comes through `currentColor`. */
229
+ export const INK_STYLE = (weight) => `fill="currentColor" stroke="currentColor" stroke-width="${weight}" stroke-linecap="round" stroke-linejoin="round"`;
230
+ /** The doodle at rest, for thumbnails. */
231
+ export const doodleSvg = (name, overrides) => {
232
+ const doodle = createDoodle(name, overrides);
233
+ // Fills are ink too, so a line's "fill: none" is set on the line itself.
234
+ return (`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" rx="22.22" fill="${doodle.tile}"/>` +
235
+ `<g transform="${doodle.face}" color="${doodle.ink}" ${INK_STYLE(doodle.weight)}>${doodle.marks}${doodle.eyes}${doodle.mouth}</g></svg>`);
236
+ };