@originkit/avatars 0.0.0-stage → 0.1.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 (168) hide show
  1. package/GUIDE.md +44 -0
  2. package/LICENSE +58 -0
  3. package/README.md +119 -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 +128 -0
  64. package/dist/controls.js +170 -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 +11 -0
  165. package/dist/variants.js +41 -0
  166. package/mcp/server.js +7 -0
  167. package/mcp/tools.js +70 -0
  168. package/package.json +54 -4
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Critters: original creatures in the style of close-up flat face tiles. The face fills the
3
+ * tile and runs off its edges, in one bold body colour with a darker tone for lids, brows and
4
+ * creases and a lighter one for patches, muzzles and jaws. Eyes are big whites with an iris
5
+ * and a glint, or glossy beads, under lids that set the mood. Each type is a creature of its
6
+ * own, as the animals are, and every type can feel every feeling.
7
+ *
8
+ * Every part is drawn from numbers in the 100×100 frame, so the live avatar can move its lids,
9
+ * pupils and brows, and the thumbnail is the very same picture at rest.
10
+ */
11
+ import { BLACK, contrast, mix, WHITE } from "../colors.js";
12
+ import { calmed } from "../possibilities.js";
13
+ import { createTraits } from "../traits.js";
14
+ import { CALMING } from "./calming.js";
15
+ const r2 = (value) => Math.round(value * 100) / 100;
16
+ /** Bold flat body colours. Neighbours are unlike. */
17
+ export const COLOURS = ["#4fcfd6", "#e2483d", "#8fd94a", "#f0b821", "#7a6cf0", "#ff8a3d", "#e8569a", "#4f8ff0", "#3fbf8a", "#c06bdb"];
18
+ /** Rich iris colours, as the references' are, rather than a dull tint of the body's. */
19
+ export const IRISES = ["#1f5f6b", "#5b3a1f", "#2f6b2a", "#3b2a6e", "#7a2e22", "#1d3f7a", "#2a2a33", "#6b4a12"];
20
+ /** Neighbours are as unlike as they can be. */
21
+ export const FEELINGS = [
22
+ { name: "Happy", lid: 0.14, tilt: 0, lift: 2, browTilt: -6, pupil: 1, look: [0, 0], mouth: "smile" },
23
+ { name: "Grumpy", lid: 0.44, tilt: 14, lift: -1, browTilt: 18, pupil: 1, look: [0, 0.1], mouth: "flat" },
24
+ { name: "Surprised", lid: 0, tilt: 0, lift: 7, browTilt: -4, pupil: 0.72, look: [0, 0], mouth: "o" },
25
+ { name: "Sleepy", lid: 0.56, tilt: -4, lift: -2, browTilt: 0, pupil: 1, look: [0, 0.3], mouth: "smile" },
26
+ { name: "Grin", lid: 0.06, tilt: 0, lift: 4, browTilt: -8, pupil: 1, look: [0.2, -0.2], mouth: "grin" },
27
+ { name: "Sad", lid: 0.24, tilt: -16, lift: 1, browTilt: -18, pupil: 1.05, look: [0, 0.35], mouth: "frown" },
28
+ { name: "Smug", lid: 0.4, tilt: -10, lift: 0, browTilt: -4, pupil: 0.95, look: [0.4, 0], mouth: "smirk" },
29
+ { name: "Joy", lid: 0, tilt: 0, lift: 5, browTilt: -8, pupil: 1, look: [0, 0], mouth: "laugh", closed: true },
30
+ { name: "Angry", lid: 0.34, tilt: 24, lift: -2, browTilt: 26, pupil: 0.85, look: [0, 0], mouth: "grit" },
31
+ { name: "Bored", lid: 0.52, tilt: 0, lift: -1, browTilt: 2, pupil: 0.9, look: [-0.4, 0], mouth: "flat" },
32
+ ];
33
+ /** The creatures, each its own build. Neighbours are as unlike as they can be. */
34
+ export const KINDS = [
35
+ { name: "Monster", eyes: "white", r: 13, y: 33, gap: 18, patch: 1.28, brows: "arcs", feature: "nose", mouthY: 73, mouthW: 33, toothy: true },
36
+ { name: "Duck", eyes: "bead", r: 9.5, y: 39, gap: 22, brows: "none", feature: "bill", mouthY: 70, mouthW: 26, toothy: false },
37
+ { name: "Cyclops", eyes: "white", single: true, r: 21, y: 33, gap: 0, patch: 1.15, brows: "arcs", feature: "none", mouthY: 75, mouthW: 33, toothy: true },
38
+ { name: "Bear", eyes: "white", r: 7.6, y: 30, gap: 13.5, brows: "pads", feature: "bear", mouthY: 80, mouthW: 15, toothy: false },
39
+ { name: "Imp", eyes: "bead", r: 10.5, y: 35, gap: 19.5, patch: 1.55, brows: "none", feature: "nose", mouthY: 73, mouthW: 34, toothy: true },
40
+ { name: "Chick", eyes: "white", r: 13, y: 38, gap: 19, brows: "none", feature: "beak", mouthY: 60, mouthW: 10, toothy: false },
41
+ { name: "Grump", eyes: "white", r: 10, y: 30, gap: 18, brows: "wedges", feature: "jaw", mouthY: 72, mouthW: 16, toothy: false },
42
+ ];
43
+ export const critterTraits = (name, overrides) => calmed(createTraits(name, overrides), CALMING);
44
+ /** The name a variant was made from, and how many steps on it is. */
45
+ function named(name) {
46
+ const variant = /^(.*)#(\d+)$/.exec(name);
47
+ return { from: variant ? variant[1] : name, step: variant ? Number(variant[2]) - 1 : 0 };
48
+ }
49
+ /**
50
+ * Its creature. Variants step on through them from the name's own, as the animals' are each a
51
+ * different animal, keeping to those Possibilities offers. Set by hand, it's that one.
52
+ */
53
+ export function kindOf(name, overrides) {
54
+ const { from, step } = named(name);
55
+ const t = createTraits(from, overrides);
56
+ const own = Math.min(KINDS.length - 1, Math.floor(t("kind") * KINDS.length));
57
+ if (t.forced("kind") !== undefined)
58
+ return KINDS[own];
59
+ return KINDS[(own + step) % KINDS.length];
60
+ }
61
+ export const feelingOf = (t) => t.pick("feeling", FEELINGS);
62
+ export const EYE_STYLES = ["white", "bead"];
63
+ export const BROWS = ["arcs", "pads", "wedges", "none"];
64
+ /** How big a patch is round each eye when one is switched on for a creature with none of its own. */
65
+ const PATCH = 1.3;
66
+ /** How often a name is spotted or blushes, in percent. */
67
+ export const SPOTS_PERCENT = 25;
68
+ export const BLUSH_PERCENT = 30;
69
+ /**
70
+ * Its creature as drawn: its own build, with its eyes, brows and patches as picked by hand if
71
+ * they are. Unpicked, every creature keeps its own, as each is built to.
72
+ */
73
+ export function styledOf(name, overrides) {
74
+ const t = critterTraits(name, overrides);
75
+ const kind = kindOf(name, overrides);
76
+ const picked = (key, list) => {
77
+ const set = t.forced(key);
78
+ return set === undefined ? undefined : list[Math.min(list.length - 1, Math.floor(set * list.length))];
79
+ };
80
+ const patch = t.forced("patch");
81
+ return {
82
+ ...kind,
83
+ eyes: picked("eye.style", EYE_STYLES) ?? kind.eyes,
84
+ brows: picked("brows", BROWS) ?? kind.brows,
85
+ patch: patch === undefined ? kind.patch : patch < 0.5 ? (kind.patch ?? PATCH) : undefined,
86
+ };
87
+ }
88
+ /** Its colours: the body, and the tones every part is drawn in, all from the body's. */
89
+ export function coloursOf(t) {
90
+ const body = t.color("body") ?? t.pick("body", COLOURS);
91
+ return {
92
+ body,
93
+ lid: mix(body, BLACK, 0.1),
94
+ shade: mix(body, BLACK, 0.16),
95
+ dark: mix(body, "#120d1c", 0.78),
96
+ light: mix(body, WHITE, 0.32),
97
+ iris: t.color("iris") ?? t.pick("iris", IRISES),
98
+ cream: mix("#f4ead6", body, 0.12),
99
+ plate: mix(body, "#ffd76a", 0.62),
100
+ // Whichever stands out most from the body, so a red chick's comb or an orange duck's bill still shows.
101
+ comb: standout(body, ["#e2392f", "#ffd23f", "#ff8a3d"]),
102
+ beak: standout(body, ["#ff9d2e", "#ffd84a", "#3f6fd8"]),
103
+ bill: standout(body, ["#ff7a2f", "#e8452b", "#ffb02e"]),
104
+ };
105
+ }
106
+ const standout = (body, choices) => choices.reduce((best, choice) => (contrast(choice, body) > contrast(best, body) ? choice : best));
107
+ /** An eye, its lid set by `--lid` as a share of the rect it scales, so the live lid can blink. */
108
+ function eyeMarkup(id, k, f, c, cx, cy, r, side) {
109
+ if (f.closed) {
110
+ // Shut in happiness: in the iris's colour for white eyes, and the bead's for beads.
111
+ const lash = k.eyes === "bead" ? mix(c.iris, "#0b0b12", 0.62) : c.iris;
112
+ return `<path class="closed" d="M${r2(cx - r * 0.82)} ${r2(cy + r * 0.25)}Q${r2(cx)} ${r2(cy - r * 0.75)} ${r2(cx + r * 0.82)} ${r2(cy + r * 0.25)}" fill="none" stroke="${lash}" stroke-width="${r2(Math.max(1.8, r * 0.26))}" stroke-linecap="round"/>`;
113
+ }
114
+ const bead = k.eyes === "bead";
115
+ const [rx, ry] = bead ? [r * 0.86, r] : [r, r];
116
+ const clip = `${id}-eye${side}`;
117
+ const reach = r * (bead ? 0.12 : 0.32);
118
+ const [lx, ly] = [cx + f.look[0] * reach * 1.6, cy + f.look[1] * reach * 1.6];
119
+ // The lid's rect runs from above the eye to below it; scaling it from its top drops its edge.
120
+ const top = cy - ry * 1.3;
121
+ const height = ry * 2.6;
122
+ const scale = (0.3 + 2 * f.lid) / 2.6;
123
+ const ink = "#0b0b12";
124
+ // A bead is glossy black, with a big glint and a small one. A white eye has an iris with a
125
+ // darker ring, a pupil, and the same two glints, as in the references.
126
+ const beadColour = mix(c.iris, ink, 0.62);
127
+ const pupil = bead
128
+ ? `<ellipse cx="${r2(lx + rx * 0.3)}" cy="${r2(ly - ry * 0.36)}" rx="${r2(r * 0.26)}" ry="${r2(r * 0.22)}" fill="#ffffff"/>` +
129
+ `<circle cx="${r2(lx - rx * 0.3)}" cy="${r2(ly + ry * 0.38)}" r="${r2(r * 0.1)}" fill="#ffffff"/>`
130
+ : `<circle cx="${r2(lx)}" cy="${r2(ly)}" r="${r2(r * 0.58 * f.pupil)}" fill="${c.iris}" stroke="${mix(c.iris, ink, 0.45)}" stroke-width="${r2(r * 0.07)}"/>` +
131
+ `<circle cx="${r2(lx)}" cy="${r2(ly)}" r="${r2(r * 0.3 * f.pupil)}" fill="${ink}"/>` +
132
+ `<circle cx="${r2(lx + r * 0.22)}" cy="${r2(ly - r * 0.24)}" r="${r2(r * 0.17 * f.pupil)}" fill="#ffffff"/>` +
133
+ `<circle cx="${r2(lx - r * 0.2)}" cy="${r2(ly + r * 0.22)}" r="${r2(r * 0.07 * f.pupil)}" fill="#ffffff"/>`;
134
+ // Three layers make the lid, each scaled from the same top: a grey shadow it casts on the
135
+ // white, its dark edge, and the lid itself in the body's colour.
136
+ const layer = (fill, stretch) => `<rect class="lid" x="${r2(cx - rx * 1.4)}" y="${r2(top)}" width="${r2(rx * 2.8)}" height="${r2(height * stretch)}" fill="${fill}" style="--lid:${r2(scale)}" transform="translate(0 ${r2(top)}) scale(1 ${r2(scale)}) translate(0 ${r2(-top)})"/>`;
137
+ const lid = (bead ? "" : layer("#cfd6de", 1.09)) + layer(c.dark, 1.03) + layer(c.body, 1);
138
+ return (`<clipPath id="${clip}"><ellipse cx="${r2(cx)}" cy="${r2(cy)}" rx="${r2(rx)}" ry="${r2(ry)}"/></clipPath>` +
139
+ `<ellipse cx="${r2(cx)}" cy="${r2(cy)}" rx="${r2(rx)}" ry="${r2(ry)}" fill="${bead ? beadColour : "#ffffff"}"/>` +
140
+ `<g clip-path="url(#${clip})">` +
141
+ `<g class="pupil" style="--reach:${r2(reach)}">${pupil}</g>` +
142
+ `<g transform="rotate(${r2(side * f.tilt)} ${r2(cx)} ${r2(cy)})"><g class="lids">${lid}</g></g></g>` +
143
+ // A crisp rim, so the eye sits in the face rather than on it.
144
+ `<ellipse cx="${r2(cx)}" cy="${r2(cy)}" rx="${r2(rx)}" ry="${r2(ry)}" fill="none" stroke="${c.shade}" stroke-width="${r2(Math.max(0.9, r * 0.08))}"/>`);
145
+ }
146
+ function browMarkup(k, f, c, cx, cy, r, side) {
147
+ if (k.brows === "none")
148
+ return "";
149
+ const y = cy - r * (k.brows === "pads" ? 1.75 : 1.42) - f.lift * 0.7;
150
+ const turn = `rotate(${r2(side * f.browTilt)} ${r2(cx)} ${r2(y)})`;
151
+ if (k.brows === "pads") {
152
+ return `<ellipse cx="${r2(cx)}" cy="${r2(y)}" rx="${r2(r * 1.2)}" ry="${r2(r * 0.62)}" fill="${c.dark}" transform="${turn}"/>`;
153
+ }
154
+ if (k.brows === "wedges") {
155
+ // Heavy, meeting over the nose in a V when cross.
156
+ return `<path d="M${r2(cx - side * r * 1.5)} ${r2(y - r * 0.2)}L${r2(cx + side * r * 1.25)} ${r2(y + r * 0.2)}L${r2(cx + side * r * 1.2)} ${r2(y + r * 0.75)}L${r2(cx - side * r * 1.45)} ${r2(y + r * 0.45)}Z" fill="${c.shade}" stroke="${c.shade}" stroke-width="1.6" stroke-linejoin="round" transform="${turn}"/>`;
157
+ }
158
+ // Thick in the middle and thin at its ends, as the references' brows are.
159
+ const thick = Math.max(2.4, r * 0.3);
160
+ return (`<path d="M${r2(cx - r * 1.02)} ${r2(y + r * 0.22)}Q${r2(cx)} ${r2(y - r * 0.42 - thick)} ${r2(cx + r * 1.02)} ${r2(y + r * 0.22)}` +
161
+ `Q${r2(cx)} ${r2(y - r * 0.42 + thick * 0.4)} ${r2(cx - r * 1.02)} ${r2(y + r * 0.22)}Z" fill="${c.dark}" stroke="${c.dark}" stroke-width="0.9" stroke-linejoin="round" transform="${turn}"/>`);
162
+ }
163
+ const TONGUE = "#f2849f";
164
+ const GUMS = "#d8577a";
165
+ /** Where a tooth meets the lip, a little shade, as the references' teeth have. */
166
+ const TOOTH_SHADE = "#d9dee6";
167
+ /** A mouth at `y`, `w` either side of the middle. Its inside is clipped to itself, teeth and tongue too. */
168
+ function mouthMarkup(id, mouth, c, y, w, teeth) {
169
+ const ink = `fill="none" stroke="${c.dark}" stroke-width="2.6" stroke-linecap="round"`;
170
+ const clip = `${id}-mouth`;
171
+ // The soft crease of the chin under an open mouth.
172
+ const chinUnder = (at) => `<path d="M${r2(50 - w * 0.4)} ${r2(at)}Q50 ${r2(at + 2)} ${r2(50 + w * 0.4)} ${r2(at)}" fill="none" stroke="${c.shade}" stroke-width="1.6" stroke-linecap="round"/>`;
173
+ const inside = (shape, fill) => `<clipPath id="${clip}">${shape}</clipPath>${shape.replace("/>", ` fill="${c.dark}"/>`)}<g clip-path="url(#${clip})">${fill}</g>`;
174
+ const row = (count, from, to, base, tip) => Array.from({ length: count }, (_, i) => {
175
+ const a = from + ((to - from) * i) / count;
176
+ const b = from + ((to - from) * (i + 1)) / count;
177
+ return `<path d="M${r2(a)} ${r2(base)}L${r2((a + b) / 2)} ${r2(tip)}L${r2(b)} ${r2(base)}Z" fill="#ffffff" stroke="#ffffff" stroke-width="0.8" stroke-linejoin="round"/>`;
178
+ }).join("");
179
+ switch (mouth) {
180
+ case "smile":
181
+ return `<path d="M${r2(50 - w * 0.62)} ${r2(y - 1)}Q50 ${r2(y + w * 0.34)} ${r2(50 + w * 0.62)} ${r2(y - 1)}" ${ink}/>`;
182
+ case "flat":
183
+ return `<path d="M${r2(50 - w * 0.5)} ${r2(y + 1)}Q50 ${r2(y + 1.6)} ${r2(50 + w * 0.5)} ${r2(y + 0.4)}" ${ink}/>`;
184
+ case "frown":
185
+ return `<path d="M${r2(50 - w * 0.5)} ${r2(y + 4)}Q50 ${r2(y - w * 0.22)} ${r2(50 + w * 0.5)} ${r2(y + 4)}" ${ink}/>`;
186
+ case "smirk":
187
+ return `<path d="M${r2(50 - w * 0.45)} ${r2(y + 1.5)}Q${r2(50 + w * 0.15)} ${r2(y + 5)} ${r2(50 + w * 0.6)} ${r2(y - 3.5)}" ${ink}/>`;
188
+ case "o":
189
+ return inside(`<ellipse cx="50" cy="${r2(y + 2)}" rx="${r2(Math.max(3.4, w * 0.18))}" ry="${r2(Math.max(4.4, w * 0.22))}"/>`, `<ellipse cx="50" cy="${r2(y + 2 + w * 0.2)}" rx="${r2(w * 0.16)}" ry="${r2(w * 0.1)}" fill="${TONGUE}"/>`);
190
+ case "grin": {
191
+ // A wide crescent, a row of teeth from its top edge and a few from its foot.
192
+ const h = w * 0.62;
193
+ const shape = `<path d="M${r2(50 - w)} ${r2(y - 2)}Q50 ${r2(y + 3)} ${r2(50 + w)} ${r2(y - 2)}Q50 ${r2(y + h * 2)} ${r2(50 - w)} ${r2(y - 2)}Z"/>`;
194
+ // A soft crease under it, the chin, as the toothy grins in the references have.
195
+ const chin = `<path d="M${r2(50 - w * 0.55)} ${r2(y + h * 1.35)}Q50 ${r2(y + h * 1.55)} ${r2(50 + w * 0.55)} ${r2(y + h * 1.35)}" fill="none" stroke="${c.shade}" stroke-width="1.8" stroke-linecap="round"/>`;
196
+ // The gums at the back of the lower teeth, the teeth, and a grey shade where the upper teeth meet the lip.
197
+ const gums = `<ellipse cx="50" cy="${r2(y + h * 1.25)}" rx="${r2(w * 0.62)}" ry="${r2(h * 0.42)}" fill="${GUMS}"/>`;
198
+ const lip = `<path d="M${r2(50 - w)} ${r2(y - 2)}Q50 ${r2(y + 3)} ${r2(50 + w)} ${r2(y - 2)}" fill="none" stroke="${TOOTH_SHADE}" stroke-width="2.6"/>`;
199
+ return chin + inside(shape, gums + row(teeth, 50 - w * 0.92, 50 + w * 0.92, y - 2.5, y + h * 0.36) + lip + row(Math.max(2, teeth - 2), 50 - w * 0.5, 50 + w * 0.5, y + h * 1.12, y + h * 0.86));
200
+ }
201
+ case "laugh": {
202
+ const h = w * 0.55;
203
+ const shape = `<path d="M${r2(50 - w * 0.85)} ${r2(y - 2)}Q50 ${r2(y - 4)} ${r2(50 + w * 0.85)} ${r2(y - 2)}Q50 ${r2(y + h * 2.2)} ${r2(50 - w * 0.85)} ${r2(y - 2)}Z"/>`;
204
+ // The upper teeth with their grey shade, and a tongue with its middle line.
205
+ const tongue = `<ellipse cx="50" cy="${r2(y + h * 1.12)}" rx="${r2(w * 0.5)}" ry="${r2(h * 0.48)}" fill="${TONGUE}"/><path d="M50 ${r2(y + h * 0.78)}V${r2(y + h * 1.2)}" stroke="${mix(TONGUE, BLACK, 0.18)}" stroke-width="1.2" stroke-linecap="round"/>`;
206
+ return chinUnder(y + h * 1.6) + inside(shape, `<rect x="${r2(50 - w)}" y="${r2(y - 6)}" width="${r2(w * 2)}" height="6.4" fill="#ffffff"/><rect x="${r2(50 - w)}" y="${r2(y - 6)}" width="${r2(w * 2)}" height="3.4" fill="${TOOTH_SHADE}"/>${tongue}`);
207
+ }
208
+ case "grit": {
209
+ const shape = `<rect x="${r2(50 - w * 0.72)}" y="${r2(y - 4.5)}" width="${r2(w * 1.44)}" height="9" rx="4.5"/>`;
210
+ const gaps = Array.from({ length: 5 }, (_, i) => `<path d="M${r2(50 - w * 0.72 + ((i + 1) * w * 1.44) / 6)} ${r2(y - 4)}V${r2(y + 4)}" stroke="${c.dark}" stroke-width="1.1"/>`).join("");
211
+ return chinUnder(y + 9) + inside(shape, `<rect x="${r2(50 - w * 0.72)}" y="${r2(y - 3.2)}" width="${r2(w * 1.44)}" height="6.4" fill="#ffffff"/><rect x="${r2(50 - w * 0.72)}" y="${r2(y - 3.2)}" width="${r2(w * 1.44)}" height="1.6" fill="${TOOTH_SHADE}"/>${gaps}`);
212
+ }
213
+ }
214
+ }
215
+ /** The bill, beak, muzzle, nose or jaw each creature has, drawn under or over the mouth. */
216
+ function featureMarkup(k, f, c, t, mouthY) {
217
+ const open = f.mouth === "grin" || f.mouth === "laugh" || f.mouth === "o";
218
+ switch (k.feature) {
219
+ case "nose": {
220
+ const [w, h, y] = k.name === "Imp" ? [12, 8.5, 50] : [9, 6, 52];
221
+ const s = t.num("nose.size", 0.85, 1.2);
222
+ return `<ellipse cx="50" cy="${y}" rx="${r2(w * s)}" ry="${r2(h * s)}" fill="${c.dark}"/><ellipse cx="${r2(50 + w * 0.15 * s)}" cy="${r2(y - h * 0.42 * s)}" rx="${r2(w * 0.4 * s)}" ry="${r2(h * 0.22 * s)}" fill="${mix(c.dark, WHITE, 0.32)}"/>`;
223
+ }
224
+ case "bear":
225
+ // A cream muzzle low in the face, and a big nose on top of it.
226
+ return (`<ellipse cx="50" cy="83" rx="40" ry="31" fill="${c.cream}"/>` +
227
+ `<ellipse cx="50" cy="60" rx="${r2(t.num("nose.size", 11, 14))}" ry="9" fill="${c.dark}"/>` +
228
+ `<ellipse cx="53" cy="56" rx="5" ry="2.2" fill="${mix(c.dark, WHITE, 0.34)}"/>`);
229
+ case "bill": {
230
+ // A broad bill, opening to show its dark inside when the mouth would.
231
+ const drop = open ? 7 : 0;
232
+ const curve = f.mouth === "frown" ? -5 : f.mouth === "flat" || f.mouth === "grit" ? 0 : 5;
233
+ return (`<path d="M24 ${66 + drop * 0.3}Q34 ${84 + drop} 56 ${86 + drop}Q80 ${86 + drop} 86 ${70 + drop * 0.4}Q88 ${64} 78 ${64}Z" fill="${mix(c.bill, BLACK, 0.22)}"/>` +
234
+ (open ? `<path d="M30 68Q50 ${80 + drop} 80 68Z" fill="${c.dark}"/>` : "") +
235
+ `<path d="M22 62Q24 54 40 55Q62 50 82 58Q92 62 86 68Q72 ${74 - curve * 0.4} 50 ${74 + curve * 0.2}Q30 ${76 + curve * 0.4} 24 70Q20 66 22 62Z" fill="${c.bill}" stroke="${mix(c.bill, BLACK, 0.3)}" stroke-width="1.4" stroke-linejoin="round"/>` +
236
+ `<path d="M36 59Q50 55 64 57" fill="none" stroke="${mix(c.bill, WHITE, 0.45)}" stroke-width="2.6" stroke-linecap="round"/>`);
237
+ }
238
+ case "beak": {
239
+ const open = f.mouth === "grin" || f.mouth === "laugh" || f.mouth === "o" ? 3 : 0;
240
+ return (
241
+ // A comb at the top, a little beak and the wattle hanging under it.
242
+ `<path d="M6 -6Q20 2 18 14Q28 4 36 10Q40 0 34 -8Z" fill="${c.comb}"/>` +
243
+ `<path d="M8 -4Q20 4 18 12" fill="none" stroke="${mix(c.comb, BLACK, 0.2)}" stroke-width="2" stroke-linecap="round"/>` +
244
+ `<path d="M49 ${66 + open}Q44 ${72 + open} 46 ${78 + open}Q50 ${82 + open} 54 ${78 + open}Q56 ${72 + open} 51 ${66 + open}Z" fill="${c.comb}"/>` +
245
+ (open ? `<path d="M42 ${mouthY + 1}Q50 ${mouthY + 7} 58 ${mouthY + 1}Z" fill="${c.dark}"/>` : "") +
246
+ `<path d="M40 ${mouthY - 2}Q50 ${mouthY - 10} 60 ${mouthY - 2}Q50 ${mouthY + 4 + open} 40 ${mouthY - 2}Z" fill="${c.beak}"/>`);
247
+ }
248
+ case "jaw":
249
+ // A broad plate for a jaw, lighter than the face, with the chin's crease.
250
+ return (`<path d="M14 54L50 60L86 54L88 104L12 104Z" fill="${c.plate}"/>` +
251
+ `<path d="M14 54L50 60L86 54" fill="none" stroke="${mix(c.plate, BLACK, 0.12)}" stroke-width="1.2"/>` +
252
+ `<path d="M24 66Q22 72 26 78M76 66Q78 72 74 78" fill="none" stroke="${mix(c.plate, BLACK, 0.12)}" stroke-width="1.4" stroke-linecap="round"/>`);
253
+ default:
254
+ return "";
255
+ }
256
+ }
257
+ /** Freckled spots across the cheeks and brow, and a blush under each eye, for those that have them. */
258
+ function marks(t, c, eyes, y, r) {
259
+ const spots = t("spots") * 100 < SPOTS_PERCENT
260
+ ? [[-34, 60, 2.2], [-29, 66, 1.6], [-38, 67, 1.4], [34, 60, 2.2], [29, 66, 1.6], [38, 67, 1.4], [-8, 10, 1.8], [6, 7, 1.4], [14, 12, 1.2]]
261
+ .map(([x, sy, size]) => `<circle cx="${r2(50 + x)}" cy="${r2(sy)}" r="${r2(size)}" fill="${c.shade}"/>`)
262
+ .join("")
263
+ : "";
264
+ const blush = t("blush") * 100 < BLUSH_PERCENT
265
+ ? eyes.map((x) => `<ellipse cx="${r2(x + (x < 50 ? -r * 0.35 : x > 50 ? r * 0.35 : 0))}" cy="${r2(y + r * 1.45)}" rx="${r2(Math.max(4, r * 0.55))}" ry="${r2(Math.max(2.2, r * 0.28))}" fill="${mix(c.body, "#ff6f91", 0.55)}"/>`).join("")
266
+ : "";
267
+ return spots + blush;
268
+ }
269
+ /** `id` keeps each critter's clips its own where many share a page. */
270
+ export function createCritter(name, overrides, id = "critter") {
271
+ const t = critterTraits(name, overrides);
272
+ const k = styledOf(name, overrides);
273
+ const f = feelingOf(t);
274
+ const c = coloursOf(t);
275
+ const size = t.num("eye.size", 0.88, 1.14);
276
+ const r = k.r * size;
277
+ const gap = k.gap * t.num("eye.gap", 0.92, 1.1);
278
+ const y = k.y + t.jitter("eye.y", 2.5);
279
+ const sides = k.single ? [0] : [-1, 1];
280
+ const at = (side) => [50 + side * gap, y];
281
+ const mouthW = k.mouthW * t.num("mouth.size", 0.85, 1.15);
282
+ const teeth = t.int("teeth", 4, 7);
283
+ const front = k.feature === "bill" || k.feature === "beak";
284
+ const mouth = front ? "" : mouthMarkup(id, f.mouth === "smile" && k.toothy && t("toothy") < 0.6 ? "grin" : f.mouth, c, k.mouthY + t.jitter("mouth.y", 2), mouthW, teeth);
285
+ const patches = k.patch
286
+ ? sides.map((side) => `<ellipse cx="${r2(at(side)[0])}" cy="${r2(y)}" rx="${r2(r * k.patch)}" ry="${r2(r * k.patch * 0.92)}" fill="${c.light}"/>`).join("")
287
+ : "";
288
+ const eye = (side) => eyeMarkup(id, k, f, c, at(side)[0], y, r, side || 1);
289
+ return {
290
+ kind: k.name,
291
+ feeling: f.name,
292
+ body: c.body,
293
+ back: patches + marks(t, c, sides.map((side) => at(side)[0]), y, r) + (front ? "" : featureMarkup(k, f, c, t, k.mouthY)),
294
+ eyes: { left: eye(sides[0]), right: sides.length > 1 ? eye(sides[1]) : "" },
295
+ brows: sides.map((side) => browMarkup(k, f, c, at(side)[0], y, r, side || 1)).join(""),
296
+ mouth,
297
+ front: front ? featureMarkup(k, f, c, t, k.mouthY) : "",
298
+ blink: { period: r2(t.num("blink.period", 3.4, 6)), delay: r2(t.num("blink.delay", 0, 3)) },
299
+ };
300
+ }
301
+ export const CORNER = 22.22;
302
+ /** The critter at rest, for thumbnails. */
303
+ export function critterSvg(name, overrides) {
304
+ const k = createCritter(name, overrides);
305
+ return (`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><clipPath id="critter-tile"><rect width="100" height="100" rx="${CORNER}"/></clipPath></defs>` +
306
+ `<g clip-path="url(#critter-tile)"><rect width="100" height="100" fill="${k.body}"/>${k.back}${k.eyes.left}${k.eyes.right}${k.brows}${k.mouth}${k.front}</g></svg>`);
307
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * What Possibilities calms on a doodle. Low keeps to the eight mildest emotions, one for
3
+ * each variant, with steadier lines. Indices are into `MOODS`, mildest first.
4
+ */
5
+ import type { Calming } from "../possibilities.ts";
6
+ export declare const CALMING: Calming;
7
+ export declare const NOTES: [string, string, string];
@@ -0,0 +1,18 @@
1
+ /**
2
+ * What Possibilities calms on a doodle. Low keeps to the eight mildest emotions, one for
3
+ * each variant, with steadier lines. Indices are into `MOODS`, mildest first.
4
+ */
5
+ export const CALMING = {
6
+ features: /^(eye|face|mouth)\.|^(wobble|tilt|weight)$/,
7
+ lists: {
8
+ // Cute, Happy, Sleepy, Unimpressed, Shocked, Smug, Skeptical, Grumpy, then the livelier ones,
9
+ // from Neutral and Wink up to Starstruck, Hypnotised and Dizzy.
10
+ mood: [24, 8, [1, 5, 7, 3, 6, 8, 2, 0, 19, 12, 15, 16, 22, 4, 14, 18, 9, 23, 21, 17, 10, 13, 20, 11]],
11
+ },
12
+ rarer: ["cheeks", "nose"],
13
+ };
14
+ export const NOTES = [
15
+ "mild emotions like cute, happy and sleepy, drawn with a steady hand.",
16
+ "adds winks, laughs, mischief and tears, cheeks and noses, with looser lines.",
17
+ "every emotion, up to starstruck, hypnotised and dizzy, at its wobbliest.",
18
+ ];
@@ -0,0 +1,2 @@
1
+ import { type ControlBuilder } from "../controls.ts";
2
+ export declare const controls: ControlBuilder;
@@ -0,0 +1,50 @@
1
+ import { choice, clearingColor, fixedPalette, optional, palette, slider, toggle, } from "../controls.js";
2
+ import { calmed, narrowed, possibilitiesSlider } from "../possibilities.js";
3
+ import { colorKey, createTraits } from "../traits.js";
4
+ import { CALMING, NOTES } from "./calming.js";
5
+ import { CHEEK_ODDS, CHEEKS, coloursOf, COLOURWAYS, INKS, MOODS, NOSE_ODDS, NOSES, PARTS, TILES } from "./index.js";
6
+ const labelled = (labels) => labels.map((label) => ({ label }));
7
+ export const controls = (name, overrides) => {
8
+ const t = calmed(createTraits(name, overrides), CALMING);
9
+ const list = (control) => narrowed(control, t, CALMING);
10
+ const { parts } = coloursOf(t);
11
+ return [
12
+ {
13
+ title: "Face",
14
+ controls: [
15
+ list(choice(t, "mood", "Emotion", "list", MOODS.map(({ name: label }) => ({ label })))),
16
+ possibilitiesSlider(t, NOTES),
17
+ optional(t, "cheeks", CHEEK_ODDS, "cheeks.kind", "Cheeks", "list", labelled(CHEEKS)),
18
+ optional(t, "nose", NOSE_ODDS, "nose.kind", "Nose", "list", labelled(NOSES)),
19
+ slider(t, "eye.size", "Eye size", 0, 100, "%"),
20
+ slider(t, "eye.gap", "Eye gap", 0, 100, "%"),
21
+ slider(t, "mouth.size", "Mouth size", 0, 100, "%"),
22
+ slider(t, "face.y", "Face height", -100, 100, "%"),
23
+ slider(t, "face.x", "Face across", -100, 100, "%"),
24
+ slider(t, "tilt", "Tilt", -7, 7, "°"),
25
+ ],
26
+ },
27
+ {
28
+ title: "Lines",
29
+ controls: [slider(t, "wobble", "Hand-drawn", 0, 100, "%"), slider(t, "weight", "Line weight", 0, 100, "%")],
30
+ },
31
+ {
32
+ title: "Colour",
33
+ controls: [
34
+ palette(t, "tile", "Colour", TILES),
35
+ { ...toggle(t, "colourful", 50, "Colourful"), selected: parts ? "on" : "off" },
36
+ ...(parts
37
+ ? [
38
+ // A new colourway starts from its own colours, not the ones set by hand for the last.
39
+ PARTS.reduce(clearingColor, choice(t, "colourway", "Colourway", "swatches", COLOURWAYS.map(({ label, colors }) => ({
40
+ label,
41
+ color: `conic-gradient(${colors.map((color, i) => `${color} 0 ${(i + 1) * 25}%`).join(", ")})`,
42
+ })))),
43
+ // In Ink's place, as it is the ink: one for each part.
44
+ { type: "colors", id: "ink", label: "Inks", colors: PARTS.map((part) => ({ id: colorKey(part), color: parts[part] })) },
45
+ ]
46
+ : [fixedPalette(t, "ink", "Ink", INKS)]),
47
+ ],
48
+ },
49
+ ];
50
+ };
@@ -0,0 +1,66 @@
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 { type Overrides, type Traits } from "../traits.ts";
7
+ /** Paper. White is the reference, and the rest are only just tinted, to stay minimal. */
8
+ export declare const TILES: string[];
9
+ export declare const INKS: string[];
10
+ interface Mood {
11
+ name: string;
12
+ eyes: [string, string];
13
+ mouth: string;
14
+ /** Brows over either eye, as how far they tilt, if it has them. */
15
+ brows?: [number | null, number | null];
16
+ }
17
+ /** Every emotion. The first five are the ones in the reference. */
18
+ export declare const MOODS: Mood[];
19
+ export declare const moodOf: (t: Traits) => Mood;
20
+ /** Extras on some faces, each drawn in the same few strokes. One face in this many hundred has them. */
21
+ export declare const CHEEK_ODDS = 35;
22
+ export declare const CHEEKS: string[];
23
+ export declare const NOSE_ODDS = 30;
24
+ export declare const NOSES: string[];
25
+ /**
26
+ * Colourful faces draw each eye and the mouth in a different colour of one colourway, as in
27
+ * the reference. Each name takes three of the four, in its own order. Neighbours are unlike,
28
+ * as each variant steps to the next.
29
+ */
30
+ export declare const COLOURWAYS: {
31
+ label: string;
32
+ colors: string[];
33
+ }[];
34
+ /** Each with a colour of its own, which can be picked by hand. Cheeks and nose follow the mouth. */
35
+ export declare const PARTS: string[];
36
+ /** The colours in use, each of which can be changed by hand. `parts` only on a colourful face. */
37
+ export declare function coloursOf(t: Traits): {
38
+ tile: string;
39
+ ink: string;
40
+ parts?: undefined;
41
+ } | {
42
+ tile: string;
43
+ ink: string;
44
+ parts: {
45
+ [k: string]: string;
46
+ };
47
+ };
48
+ export interface Doodle {
49
+ tile: string;
50
+ ink: string;
51
+ /** Where the face sits, its tilt, and how heavy its lines are. */
52
+ face: string;
53
+ weight: number;
54
+ eyes: string;
55
+ mouth: string;
56
+ /** Cheeks and nose, which stay still while the eyes blink. */
57
+ marks: string;
58
+ mood: string;
59
+ blinkDelay: number;
60
+ }
61
+ export declare function createDoodle(name: string, overrides?: Overrides): Doodle;
62
+ /** How every line and blob of a face is painted. Colour comes through `currentColor`. */
63
+ export declare const INK_STYLE: (weight: number) => string;
64
+ /** The doodle at rest, for thumbnails. */
65
+ export declare const doodleSvg: (name: string, overrides?: Overrides) => string;
66
+ export {};