@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,174 @@
1
+ /**
2
+ * Pixel people. Parts from `sprites.ts` are stacked on a 20×20 grid, then each
3
+ * layer becomes SVG rectangles so it can move on its own without leaving the grid.
4
+ */
5
+ import { calmed } from "../possibilities.js";
6
+ import { createTraits } from "../traits.js";
7
+ import { CALMING } from "./calming.js";
8
+ import { COLOR_TILE, isPaleHair, paints, pickLook, SANTA_RED } from "./palette.js";
9
+ import { BEARDS, EARRING, EYE_SPACINGS, EYEBROWS, GLASSES, HAIRS, HATS, HEAD, MARKS, MOUTHS, NOSE, } from "./sprites.js";
10
+ export const GRID = 20;
11
+ /** Empty cells around the person, so tall hair never touches the tile's edge. */
12
+ export const MARGIN = 2;
13
+ /** The mouths a person can have, in the order they are picked from. */
14
+ export const MOUTH_NAMES = ["line", "small", "smirk", "smile", "frown", "tiny"];
15
+ const key = (x, y) => `${x},${y}`;
16
+ function toCells({ top, rows }, paint, belowRow = -1, mirrored = false) {
17
+ const cells = new Map();
18
+ rows.forEach((row, line) => {
19
+ const y = top + line;
20
+ if (y <= belowRow)
21
+ return;
22
+ [...row].forEach((letter, x) => {
23
+ if (letter !== ".")
24
+ cells.set(key(mirrored ? GRID - 1 - x : x, y), paint[letter]);
25
+ });
26
+ });
27
+ return cells;
28
+ }
29
+ /** A row with ink at the given columns. */
30
+ const inkAt = (...columns) => Array.from({ length: GRID }, (_, x) => (columns.includes(x) ? "x" : ".")).join("");
31
+ /** Both eyes in each state. Built from their position, so any spacing can blink. */
32
+ function eyeSprites([left, right], tall) {
33
+ const pair = inkAt(left, right);
34
+ return {
35
+ open: { top: tall ? 9 : 10, rows: tall ? [pair, pair] : [pair] },
36
+ wide: { top: tall ? 8 : 9, rows: tall ? [pair, pair, pair] : [pair, pair] },
37
+ shut: { top: 10, rows: [inkAt(left, left + 1, right, right + 1)] },
38
+ };
39
+ }
40
+ const merged = (...layers) => new Map(layers.flatMap((layer) => [...layer]));
41
+ const NEIGHBOURS = [[1, 0], [-1, 0], [0, 1], [0, -1]];
42
+ /** Paints a one-cell border around a shape, leaving the shape itself alone. */
43
+ function outlineOf(shape, color) {
44
+ const outline = new Map();
45
+ for (const cell of shape.keys()) {
46
+ const [x, y] = cell.split(",").map(Number);
47
+ for (const [dx, dy] of NEIGHBOURS) {
48
+ const next = key(x + dx, y + dy);
49
+ if (!shape.has(next))
50
+ outline.set(next, color);
51
+ }
52
+ }
53
+ return outline;
54
+ }
55
+ /**
56
+ * Darkens where a shape meets another, so light hair stays readable against the
57
+ * face. Its outer edge is left alone, because the outline already draws that.
58
+ */
59
+ function rimOf(shape, against, color) {
60
+ const rimmed = new Map(shape);
61
+ for (const cell of shape.keys()) {
62
+ const [x, y] = cell.split(",").map(Number);
63
+ const touches = NEIGHBOURS.some(([dx, dy]) => {
64
+ const next = key(x + dx, y + dy);
65
+ return !shape.has(next) && against.has(next);
66
+ });
67
+ if (touches)
68
+ rimmed.set(cell, color);
69
+ }
70
+ return rimmed;
71
+ }
72
+ /** Writes cells as rectangles, joining runs so a face stays a few hundred bytes. */
73
+ function toRects(cells) {
74
+ let out = "";
75
+ for (let y = -MARGIN; y < GRID + MARGIN; y++) {
76
+ let x = -MARGIN;
77
+ while (x < GRID + MARGIN) {
78
+ const color = cells.get(key(x, y));
79
+ if (!color) {
80
+ x++;
81
+ continue;
82
+ }
83
+ let width = 1;
84
+ while (cells.get(key(x + width, y)) === color)
85
+ width++;
86
+ out += `<rect x="${x}" y="${y}" width="${width}" height="1" fill="${color}"/>`;
87
+ x += width;
88
+ }
89
+ }
90
+ return out;
91
+ }
92
+ const chance = (t, key, percent) => t(key) * 100 < percent;
93
+ /** The traits are read first, so both themes of one name are the same person. */
94
+ function pickTraits(t) {
95
+ const hair = t.pick("hair", HAIRS);
96
+ const wantsHat = hair.takesHat && chance(t, "hat", 38);
97
+ const hat = wantsHat ? t.pick("hat.kind", HATS) : null;
98
+ const facialHair = hair.name === "hijab" || !chance(t, "beard", 34) ? null : t.pick("beard.kind", Object.keys(BEARDS));
99
+ const glasses = chance(t, "glasses", 24) ? t.pick("glasses.kind", ["frames", "frames", "shades"]) : null;
100
+ const earring = !hair.wraps && chance(t, "earring", 16);
101
+ return {
102
+ hair,
103
+ hat,
104
+ facialHair,
105
+ glasses,
106
+ earring,
107
+ mouth: t.pick("mouth", MOUTH_NAMES),
108
+ mirrored: t("mirrored") < 0.5,
109
+ brows: chance(t, "brows", 55) ? t.pick("brows.kind", Object.keys(EYEBROWS)) : null,
110
+ mark: chance(t, "mark", 30) ? t.pick("mark.kind", Object.keys(MARKS)) : null,
111
+ // Lenses only frame small, centred eyes. Anything else reads as stripes.
112
+ eyeSpacing: glasses ? EYE_SPACINGS[0] : t.pick("eyes.spacing", EYE_SPACINGS),
113
+ tallEyes: !glasses && chance(t, "eyes.tall", 65),
114
+ };
115
+ }
116
+ /** A tile with the same corners as a peek, the person centred on it. Smooth, though the cells are crisp. */
117
+ function tileOf(color) {
118
+ const span = GRID + MARGIN * 2;
119
+ return `<rect x="${-MARGIN}" y="${-MARGIN}" width="${span}" height="${span}" rx="5.33" fill="${color}" shape-rendering="auto"/>`;
120
+ }
121
+ export function createPixelFace(name, theme, overrides) {
122
+ const t = calmed(createTraits(name, overrides), CALMING);
123
+ const picked = pickLook(t);
124
+ const traits = pickTraits(t);
125
+ const { hair, hat, facialHair, glasses, earring, mirrored, brows, mark } = traits;
126
+ const retro = theme === "retro";
127
+ // A hijab is cloth, and a santa hat is only ever red.
128
+ const look = {
129
+ ...picked,
130
+ hair: hair.name === "hijab" ? picked.hat : picked.hair,
131
+ hat: hat?.name === "santa" ? SANTA_RED : picked.hat,
132
+ };
133
+ const paint = paints(theme, look);
134
+ const cellsOf = (sprite, belowRow = -1) => toCells(sprite, paint, belowRow, mirrored);
135
+ const { inks } = look;
136
+ const hairLine = hat ? hat.bottom : -1;
137
+ const headCells = cellsOf(HEAD);
138
+ const rim = (cells) => retro && isPaleHair(look.hair) ? rimOf(cells, headCells, inks.dark) : cells;
139
+ const back = hair.back ? rim(cellsOf(hair.back)) : new Map();
140
+ const front = rim(cellsOf(hair.front, hairLine));
141
+ const head = merged(headCells, earring ? cellsOf(EARRING) : new Map());
142
+ const hatCells = hat ? cellsOf(hat.sprite) : null;
143
+ const body = merged(back, head);
144
+ // Retro's outline goes around the person, and the hat has its own.
145
+ const outline = retro ? outlineOf(merged(body, front), inks.dark) : new Map();
146
+ const hatOutline = retro && hatCells ? outlineOf(hatCells, inks.dark) : new Map();
147
+ const rects = (sprite) => toRects(cellsOf(sprite));
148
+ const optional = (sprite) => (sprite ? cellsOf(sprite) : new Map());
149
+ const beard = optional(facialHair ? BEARDS[facialHair] : undefined);
150
+ const eyes = eyeSprites(traits.eyeSpacing, traits.tallEyes);
151
+ // Retro's two tones draw a nose as a dark smudge, so it goes without one.
152
+ const nose = retro ? new Map() : cellsOf(NOSE);
153
+ return {
154
+ tile: tileOf(retro ? inks.tile : COLOR_TILE),
155
+ headMask: toRects(headCells),
156
+ behind: toRects(merged(outline, body)),
157
+ face: toRects(merged(nose, optional(mark ? MARKS[mark] : undefined), beard, optional(brows ? EYEBROWS[brows] : undefined))),
158
+ front: toRects(front),
159
+ hat: hatCells ? toRects(merged(hatOutline, hatCells)) : null,
160
+ glasses: glasses ? rects(GLASSES[glasses]) : null,
161
+ eyes: { open: rects(eyes.open), wide: rects(eyes.wide), shut: rects(eyes.shut) },
162
+ mouths: { rest: rects(MOUTHS[traits.mouth]), hover: rects(MOUTHS.smile) },
163
+ eyesVisible: glasses !== "shades",
164
+ blinkDelay: t.num("blink", 0, 4),
165
+ };
166
+ }
167
+ /** The whole face at rest, for sheets and tests. */
168
+ export const pixelSvg = (name, theme, overrides) => {
169
+ const f = createPixelFace(name, theme, overrides);
170
+ const span = GRID + MARGIN * 2;
171
+ return (`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${-MARGIN} ${-MARGIN} ${span} ${span}" shape-rendering="crispEdges">` +
172
+ f.tile + f.behind + f.face + f.eyes.open + f.mouths.rest + f.front + (f.glasses ?? "") + (f.hat ?? "") +
173
+ `</svg>`);
174
+ };
@@ -0,0 +1,29 @@
1
+ /** The paints for each theme. A theme maps every letter in `sprites.ts` to a colour. */
2
+ import type { Traits } from "../traits.ts";
3
+ export type Theme = "color" | "retro";
4
+ export declare const SKINS: [string, string][];
5
+ /** `[colour, highlight]`. */
6
+ export declare const HAIR_COLORS: [string, string][];
7
+ /** `[colour, shade]`. */
8
+ export declare const HAT_COLORS: [string, string][];
9
+ export declare const EARRINGS: string[];
10
+ export declare const COLOR_TILE = "#f6f4ee";
11
+ export declare const SANTA_RED: [string, string];
12
+ export declare const RETRO: {
13
+ tile: string;
14
+ pale: string;
15
+ dark: string;
16
+ };
17
+ /** The screen colours Retro offers. The first is the Game Boy green it starts on. */
18
+ export declare const SCREENS: string[];
19
+ export interface Look {
20
+ skin: [string, string];
21
+ hair: [string, string];
22
+ hat: [string, string];
23
+ earring: string;
24
+ inks: typeof RETRO;
25
+ }
26
+ export declare const pickLook: (t: Traits) => Look;
27
+ /** Whether Retro draws this hair light with an outline, rather than solid dark. */
28
+ export declare const isPaleHair: ([color]: [string, string]) => boolean;
29
+ export declare function paints(theme: Theme, { skin, hair, hat, earring, inks }: Look): Record<string, string>;
@@ -0,0 +1,107 @@
1
+ /** The paints for each theme. A theme maps every letter in `sprites.ts` to a colour. */
2
+ import { BLACK, mix, WHITE } from "../colors.js";
3
+ export const SKINS = [
4
+ ["#fde3cf", "#eebf9f"],
5
+ ["#f5cda9", "#dfa97f"],
6
+ ["#eab98e", "#cf9468"],
7
+ ["#d9a273", "#b97f52"],
8
+ ["#c18a5e", "#9c6a42"],
9
+ ["#9b6a45", "#7b4f30"],
10
+ ["#74492e", "#5a3621"],
11
+ ["#4e3020", "#3a2316"],
12
+ ["#eda4ab", "#d67f8a"],
13
+ ];
14
+ /** `[colour, highlight]`. */
15
+ export const HAIR_COLORS = [
16
+ ["#17151a", "#35313b"],
17
+ ["#2f1e16", "#4e3424"],
18
+ ["#5e3a22", "#7e5433"],
19
+ ["#8a3f1f", "#b05f33"],
20
+ ["#d8ae62", "#ecca8d"],
21
+ ["#e8d9bb", "#f6edd9"],
22
+ ["#8e8f96", "#b1b2b8"],
23
+ ["#e588ad", "#f4afc9"],
24
+ ["#3f7fc2", "#68a4dd"],
25
+ ];
26
+ /** `[colour, shade]`. */
27
+ export const HAT_COLORS = [
28
+ ["#d0353a", "#a82329"],
29
+ ["#2e7ea8", "#1f5f82"],
30
+ ["#10525c", "#0b3a42"],
31
+ ["#e4b23f", "#c0902a"],
32
+ ["#3e8f5b", "#2d6c44"],
33
+ ["#6b4bb8", "#503693"],
34
+ ["#22242c", "#3a3d48"],
35
+ ];
36
+ export const EARRINGS = ["#ffd84a", "#e23a78", "#35c4cf"];
37
+ const INK = "#1b171b";
38
+ const RED = "#c2353f";
39
+ const FRAMES = "#1c1a22";
40
+ const TINT = "#2f3140";
41
+ export const COLOR_TILE = "#f6f4ee";
42
+ export const SANTA_RED = HAT_COLORS[0];
43
+ export const RETRO = {
44
+ tile: "#86bd72",
45
+ pale: "#def3c6",
46
+ dark: "#0f3a30",
47
+ };
48
+ /** The screen colours Retro offers. The first is the Game Boy green it starts on. */
49
+ export const SCREENS = [RETRO.tile, "#e0a458", "#7fa8d9", "#e39ab5", "#b4b0a4"];
50
+ /** A paint from the list, or one picked by hand with its second tone worked out. */
51
+ function paintOf(t, key, colors, towards, amount) {
52
+ const picked = t.color(key);
53
+ return picked ? [picked, mix(picked, towards, amount)] : t.pick(key, colors);
54
+ }
55
+ /** Retro's two inks are shades of its screen, so any screen stays a two-tone handheld. */
56
+ function inksOn(tile = RETRO.tile) {
57
+ return tile === RETRO.tile ? RETRO : { tile, pale: mix(tile, WHITE, 0.72), dark: mix(tile, BLACK, 0.74) };
58
+ }
59
+ export const pickLook = (t) => ({
60
+ skin: paintOf(t, "skin", SKINS, BLACK, 0.18),
61
+ hair: paintOf(t, "hair.color", HAIR_COLORS, WHITE, 0.2),
62
+ hat: paintOf(t, "hat.color", HAT_COLORS, BLACK, 0.2),
63
+ earring: t.pick("earring.color", EARRINGS),
64
+ inks: inksOn(t.color("screen")),
65
+ });
66
+ /** Whether Retro draws this hair light with an outline, rather than solid dark. */
67
+ export const isPaleHair = ([color]) => parseInt(color.slice(1, 3), 16) > 0xb0;
68
+ export function paints(theme, { skin, hair, hat, earring, inks }) {
69
+ if (theme === "color") {
70
+ return {
71
+ s: skin[0],
72
+ d: skin[1],
73
+ h: hair[0],
74
+ H: hair[1],
75
+ f: hair[0],
76
+ c: hat[0],
77
+ C: hat[1],
78
+ b: hat[1],
79
+ w: "#dcd9d1",
80
+ g: FRAMES,
81
+ L: TINT,
82
+ x: INK,
83
+ r: RED,
84
+ e: earring,
85
+ m: skin[1],
86
+ };
87
+ }
88
+ const { pale, dark } = inks;
89
+ const paleHair = isPaleHair(hair);
90
+ return {
91
+ s: pale,
92
+ d: pale,
93
+ h: paleHair ? pale : dark,
94
+ H: pale,
95
+ f: dark,
96
+ c: dark,
97
+ C: pale,
98
+ b: dark,
99
+ w: pale,
100
+ g: dark,
101
+ L: dark,
102
+ x: dark,
103
+ r: pale,
104
+ e: dark,
105
+ m: dark,
106
+ };
107
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The hand-drawn parts, on a 20×20 grid. Each sprite is drawn as text so it can be
3
+ * edited by eye. `.` is empty, and the other letters are paints:
4
+ *
5
+ * s skin, d skin shadow, h hair, H hair highlight, f facial hair
6
+ * c hat, C hat shade, b hat brim, w hat trim, g glasses, L tinted lens
7
+ * x ink, r mouth red, e earring, m skin mark
8
+ */
9
+ export interface Sprite {
10
+ /** The grid row of the first line. */
11
+ top: number;
12
+ rows: string[];
13
+ }
14
+ export declare const HEAD: Sprite;
15
+ export declare const NOSE: Sprite;
16
+ export interface Hair {
17
+ name: string;
18
+ front: Sprite;
19
+ /** Drawn behind the head, for hair that falls past it. */
20
+ back?: Sprite;
21
+ /** Covers the ears, so no earring shows. */
22
+ wraps?: boolean;
23
+ /** Has the room up top for a hat. */
24
+ takesHat: boolean;
25
+ }
26
+ export declare const HAIRS: Hair[];
27
+ export interface Hat {
28
+ name: string;
29
+ sprite: Sprite;
30
+ /** Hair above this row is under the hat, so it is not drawn. */
31
+ bottom: number;
32
+ }
33
+ export declare const HATS: Hat[];
34
+ export declare const BEARDS: Record<string, Sprite>;
35
+ export declare const GLASSES: Record<string, Sprite>;
36
+ export declare const EARRING: Sprite;
37
+ export declare const EYEBROWS: Record<string, Sprite>;
38
+ export declare const MARKS: Record<string, Sprite>;
39
+ /** Where the eyes can sit: the columns of the left and right eye. */
40
+ export declare const EYE_SPACINGS: [number, number][];
41
+ export declare const EYE_SPACING_NAMES: string[];
42
+ export declare const MOUTHS: Record<string, Sprite>;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * The hand-drawn parts, on a 20×20 grid. Each sprite is drawn as text so it can be
3
+ * edited by eye. `.` is empty, and the other letters are paints:
4
+ *
5
+ * s skin, d skin shadow, h hair, H hair highlight, f facial hair
6
+ * c hat, C hat shade, b hat brim, w hat trim, g glasses, L tinted lens
7
+ * x ink, r mouth red, e earring, m skin mark
8
+ */
9
+ const sprite = (top, ...rows) => ({ top, rows });
10
+ export const HEAD = sprite(5, "......ssssssss......", ".....ssssssssss.....", ".....ssssssssss.....", ".....ssssssssss.....", ".....ssssssssssdd...", ".....ssssssssssdd...", ".....ssssssssssd....", ".....ssssssssss.....", ".....ssssssssss.....", "......sssssssdd.....", ".......sssssddd.....", ".........ddddd......", ".........ddddd......");
11
+ export const NOSE = sprite(11, ".........d..........", ".........dd.........");
12
+ export const HAIRS = [
13
+ {
14
+ name: "crop",
15
+ takesHat: true,
16
+ front: sprite(3, ".......hhhhhhh......", ".....hhhhhhhhhh.....", "....hhhhhhhhhhhh....", "....hhHhhhhhhhhh....", "....hh.....hhhhh....", "....h........hhh....", ".............hh.....", ".............h......"),
17
+ },
18
+ {
19
+ name: "sidePart",
20
+ takesHat: true,
21
+ front: sprite(2, "......hhhhhhhh......", "....hhhhhhhhhhhh....", "...hhhhhhhhhhhhhh...", "...hhhhhhhhHhhhhh...", "...hhhhhhh...hhhh...", "...hhh........hhh...", "....h.........hh....", "..............h....."),
22
+ },
23
+ {
24
+ name: "spiky",
25
+ takesHat: false,
26
+ front: sprite(1, "........h..h........", ".....h.hh.hh.h......", ".....hhhhhhhhhh.h...", "....hhhhhhHhhhhhh...", "....hhhhhhhhhhhh....", "....hh.h..h.hhhh....", "....h........hhh....", ".............hh....."),
27
+ },
28
+ {
29
+ name: "curly",
30
+ takesHat: false,
31
+ front: sprite(2, ".......hh..hh.......", ".....hhhhhhhhhh.....", "....hhhhhHhhHhhh....", "...hhHhhhhhhhhhhh...", "...hhhhhHhhHhhhhh...", "...hhh..h..h..hhh...", "....h.........hhh...", "...............hh..."),
32
+ },
33
+ {
34
+ name: "afro",
35
+ takesHat: false,
36
+ front: sprite(0, "......hhhhhhhh......", "....hhhhhhhhhhhh....", "...hhhhhhhhhhhhhh...", "..hhhhhHhhhhHhhhhh..", "..hhhhhhhhhhhhhhhh..", "..hhhHhhhhhhhhHhhh..", "..hhhhhhhhhhhhhhhh..", "..hhhh........hhhh..", "..hhh..........hhh..", "..hhh...........hh..", "...hh...........hh..", "...h............h..."),
37
+ wraps: true,
38
+ },
39
+ {
40
+ name: "long",
41
+ takesHat: true,
42
+ wraps: true,
43
+ front: sprite(3, "......hhhhhhhh......", "....hhhhhhhhhhh.....", "....hhhhhHhhhhhh....", "....hhhh....hhhh....", "....hh.......hhh....", "....h.........hh...."),
44
+ back: sprite(4, "....hhhhhhhhhhhh....", "...hhhhhhhhhhhhhh...", "...hhhhhhhhhhhhhh...", "...hhhhhhhhhhhhhh...", "...hhhhhhhhhhhhhhh..", "...hhhhhhhhhhhhhhh..", "...hhhhhhhhhhhhhhh..", "...hhhhhhhhhhhhhhh..", "...hhhhhhhhhhhhhhh..", "...hhhhhhhhhhhhhhh..", "...hhhhhhhhhhhhhhh..", "...hhhhh.....hhhhh..", "...hhhh......hhhhh..", "...hh.........hhh..."),
45
+ },
46
+ {
47
+ name: "bob",
48
+ takesHat: true,
49
+ wraps: true,
50
+ front: sprite(3, "......hhhhhhhh......", "....hhhhhhhhhhh.....", "....hhhhhhHhhhhh....", "....hhhhhh..hhhh....", "....hhh......hhh....", "....hh........hh...."),
51
+ back: sprite(4, "....hhhhhhhhhhhh....", "...hhhhhhhhhhhhhh...", "...hhhhhhhhhhhhhh...", "...hhhhhhhhhhhhhh...", "...hhhhhhhhhhhhhhh..", "...hhhhhhhhhhhhhhh..", "...hhhhhhhhhhhhhhh..", "...hhhhhhhhhhhhhhh..", "...hhhhhhhhhhhhhhh..", "...hhh.........hhh.."),
52
+ },
53
+ {
54
+ name: "bun",
55
+ takesHat: false,
56
+ front: sprite(0, "........hhhh........", ".......hhHhhh.......", ".......hhhhhh.......", ".......hhhhhh.......", ".....hhhhhhhhhh.....", "....hhhhhhHhhhhh....", "....hhhhh...hhhh....", "....hh.......hhh....", "....h.........hh...."),
57
+ },
58
+ {
59
+ name: "ponytail",
60
+ takesHat: true,
61
+ front: sprite(3, "......hhhhhhhh......", "....hhhhhhhhhhh.....", "....hhhhhhHhhhhh....", "....hhh.....hhhh....", "....h........hhh....", ".............hh....."),
62
+ back: sprite(5, "...............hhh..", "..............hhhhh.", "..............hhhhh.", "...............hhhh.", "...............hhhh.", "................hhh.", "................hhh.", "................hh..", "................h..."),
63
+ },
64
+ {
65
+ name: "mohawk",
66
+ takesHat: false,
67
+ front: sprite(0, "........hhh.........", ".......hhhhh........", ".......hhHhh........", ".......hhhhh........", ".......hhHhh........", ".......hhhhh........"),
68
+ },
69
+ {
70
+ name: "dreads",
71
+ takesHat: false,
72
+ wraps: true,
73
+ front: sprite(0, "...........h..h.h...", ".........h.hh.h.h.h.", "......hh.hhhhhhhh...", "....hhhhhhhhhhhhhh..", "...hhhhhhhhhhhhhhhh.", "...hhhhhhHhhhhhhhhh.", "...hhh.hh....hhhhh..", "...hh.........hhhh..", "...hh.........hh.hh.", "...h..........hh.hh.", "...h..........h..hh.", "...h.............h.."),
74
+ },
75
+ {
76
+ name: "balding",
77
+ takesHat: true,
78
+ front: sprite(7, "....hh........hhh...", "...hhh........hhhh..", "...hh..........hhh..", "....h...........h..."),
79
+ },
80
+ {
81
+ name: "slick",
82
+ takesHat: true,
83
+ front: sprite(3, ".....hhhhhhhhhh.....", "....hhhhhHhhhhhh....", "....hhhhhhhhhhhh....", "....h.......hhhh....", "....h.........hh....", "..............h....."),
84
+ },
85
+ {
86
+ name: "hijab",
87
+ takesHat: false,
88
+ wraps: true,
89
+ front: sprite(2, "......hhhhhhhh......", "....hhhhhhhhhhhh....", "...hhhhhhhhhhhhhh...", "...hhhhhHhhhhhhhhh..", "...hhhhhhhhhhhhhhh..", "...hhhh.....hhhhhh..", "...hhh.......hhhhh..", "...hh.........hhhh..", "...hh.........hhhh..", "...hh.........hhhh..", "...hh.........hhhh..", "...hh.........hhhh..", "...hhh.......hhhhh..", "...hhhh.....hhhhhh..", "....hhhhhhhhhhhhh...", "....hhhhhhhhhhhhh...", ".....hhhhhhhhhhh...."),
90
+ },
91
+ ];
92
+ export const HATS = [
93
+ {
94
+ name: "cap",
95
+ bottom: 6,
96
+ sprite: sprite(2, "......cccccccc......", "....cccccccccccc....", "....ccccCCcccccc....", "....cccccccccccc....", ".bbbbbbbbbbbcccc...."),
97
+ },
98
+ {
99
+ name: "backwardsCap",
100
+ bottom: 6,
101
+ sprite: sprite(2, "......cccccccc......", "....cccccccccccc....", "....ccccccCCcccc....", "....cccccccccccc....", "....ccccbbbbbbbbbbb."),
102
+ },
103
+ {
104
+ name: "beanie",
105
+ bottom: 6,
106
+ sprite: sprite(2, ".......cccccc.......", ".....cccccccccc.....", "....cccccccccccc....", "....cCcCcCcCcCcc....", "....CCCCCCCCCCCC...."),
107
+ },
108
+ {
109
+ name: "bucket",
110
+ bottom: 6,
111
+ sprite: sprite(2, ".......cccccc.......", ".....cccccccccc.....", ".....cccccccccc.....", ".....CCCCCCCCCC.....", "...bbbbbbbbbbbbbb..."),
112
+ },
113
+ {
114
+ name: "santa",
115
+ bottom: 6,
116
+ sprite: sprite(0, ".......ccccccc......", ".....ccccccccccc....", "....cccccccccccccww.", "....cccccccccccc.www", "...ccccccccccccc.www", "...wwwwwwwwwwwwww...", "...wwwwwwwwwwwwww..."),
117
+ },
118
+ ];
119
+ export const BEARDS = {
120
+ moustache: sprite(12, ".......fffff........"),
121
+ goatee: sprite(15, "........fff........."),
122
+ full: sprite(11, ".....f........f.....", ".....ff.fffff.f.....", ".....fff...ffff.....", "......ffffffff......", ".......fffff........"),
123
+ };
124
+ export const GLASSES = {
125
+ frames: sprite(8, ".....ggggggggggggg..", ".....g...g...g......", ".....g...g...g......", ".....ggggggggg......"),
126
+ shades: sprite(8, ".....ggggggggggggg..", ".....gLLLgLLLg......", ".....gLLLgLLLg......", ".....ggggggggg......"),
127
+ };
128
+ export const EARRING = sprite(12, "................e...");
129
+ export const EYEBROWS = {
130
+ flat: sprite(7, "......fff.fff......."),
131
+ arched: sprite(7, ".......f...f........", "......f.f.f.f......."),
132
+ worried: sprite(7, "........f.f.........", "......ff...ff......."),
133
+ stern: sprite(7, "......f.....f.......", ".......ff.ff........"),
134
+ };
135
+ export const MARKS = {
136
+ freckles: sprite(11, "......m.....m......."),
137
+ mole: sprite(12, "............m......."),
138
+ cheek: sprite(11, ".............m......"),
139
+ };
140
+ /** Where the eyes can sit: the columns of the left and right eye. */
141
+ export const EYE_SPACINGS = [
142
+ [7, 11],
143
+ [6, 12],
144
+ [7, 12],
145
+ ];
146
+ export const EYE_SPACING_NAMES = ["Close", "Wide", "Uneven"];
147
+ export const MOUTHS = {
148
+ line: sprite(13, "........xxx........."),
149
+ small: sprite(13, ".........xx........."),
150
+ smirk: sprite(13, "...........x........", "........xxx........."),
151
+ smile: sprite(13, ".......x...x........", "........xxx........."),
152
+ frown: sprite(13, "........xxx.........", ".......x...x........"),
153
+ tiny: sprite(13, ".........x.........."),
154
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Possibilities: how far an avatar may stray from the plainest ones. Low narrows each
3
+ * list of parts to its first few, makes extras rare, and draws proportions towards their
4
+ * middle. High, or a slider left alone, is the full range, exactly as it always was.
5
+ *
6
+ * It works on the traits rather than in each generator, so a section only says which of
7
+ * its keys are which, and its controls read the same calmed traits to stay in step.
8
+ */
9
+ import type { ChoiceControl, SliderControl } from "./controls.ts";
10
+ import type { Traits } from "./traits.ts";
11
+ export declare const VARIANCE = "variance";
12
+ export interface Calming {
13
+ /** Proportions and placements, whose spread shrinks as the level falls. */
14
+ features?: RegExp;
15
+ /**
16
+ * Lists picked by position, as `[how many, how few at the lowest]`. Low offers the first
17
+ * few, or the last few with `"end"` for lists where the plainest choice comes last. For
18
+ * lists in no useful order, a third entry gives the plainest first, by index.
19
+ */
20
+ lists?: Record<string, List>;
21
+ /** Extras that appear when their trait falls under the odds, and do so less as it falls. */
22
+ rarer?: string[];
23
+ }
24
+ type List = [number, number] | [number, number, "end" | number[]];
25
+ /** The level, or none while the slider hasn't been touched. */
26
+ export declare const varianceSet: (t: Traits) => number | undefined;
27
+ export declare const varianceOf: (t: Traits) => number;
28
+ /** How many of a list's `count` entries are on offer at `variance`. */
29
+ export declare const keptOf: (count: number, fewest: number, variance: number) => number;
30
+ /** The same traits, calmed to the level set. Anything set by hand is left exactly as set. */
31
+ export declare function calmed(t: Traits, { features, lists, rarer }: Calming): Traits;
32
+ /**
33
+ * A list control with only the options the level offers, plus whichever is selected. Built
34
+ * from calmed traits, so what it shows selected is what gets drawn.
35
+ */
36
+ export declare function narrowed(control: ChoiceControl, t: Traits, { lists }: Calming): ChoiceControl;
37
+ /** The slider itself, with what its ends mean and what the current level gives. */
38
+ export declare function possibilitiesSlider(t: Traits, [low, medium, high]: [string, string, string]): SliderControl;
39
+ export {};
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Possibilities: how far an avatar may stray from the plainest ones. Low narrows each
3
+ * list of parts to its first few, makes extras rare, and draws proportions towards their
4
+ * middle. High, or a slider left alone, is the full range, exactly as it always was.
5
+ *
6
+ * It works on the traits rather than in each generator, so a section only says which of
7
+ * its keys are which, and its controls read the same calmed traits to stay in step.
8
+ */
9
+ export const VARIANCE = "variance";
10
+ /** How much of their usual spread proportions keep, at the very lowest. */
11
+ const CALMEST = 0.2;
12
+ /** Every index of a list from plainest to wildest: those named first, then the rest. */
13
+ function plainFirst([count, , from]) {
14
+ const named = Array.isArray(from) ? from : [];
15
+ return [...named, ...Array.from({ length: count }, (_, i) => i).filter((i) => !named.includes(i))];
16
+ }
17
+ /** The level, or none while the slider hasn't been touched. */
18
+ export const varianceSet = (t) => t.forced(VARIANCE);
19
+ export const varianceOf = (t) => varianceSet(t) ?? 1;
20
+ /** How many of a list's `count` entries are on offer at `variance`. */
21
+ export const keptOf = (count, fewest, variance) => Math.min(count, fewest + Math.floor(variance * (count - fewest + 1)));
22
+ /** The same traits, calmed to the level set. Anything set by hand is left exactly as set. */
23
+ export function calmed(t, { features, lists = {}, rarer = [] }) {
24
+ const variance = varianceSet(t);
25
+ if (variance === undefined)
26
+ return t;
27
+ const keep = CALMEST + (1 - CALMEST) * variance;
28
+ const calm = ((key) => {
29
+ const value = t(key);
30
+ if (t.forced(key) !== undefined)
31
+ return value;
32
+ const list = lists[key];
33
+ if (list) {
34
+ const [count, fewest, from] = list;
35
+ const kept = keptOf(count, fewest, variance);
36
+ if (from === "end")
37
+ return 1 - (1 - value) * (kept / count);
38
+ // The middle of the kept entry's band, so an equal-odds pick lands on exactly it.
39
+ return (plainFirst(list)[Math.floor(value * kept)] + 0.5) / count;
40
+ }
41
+ // Spread over a wider range, so an extra shows up that much less often: never at 0.
42
+ if (rarer.includes(key))
43
+ return variance === 0 ? 0.999 : Math.min(0.999, value / variance);
44
+ if (features?.test(key))
45
+ return 0.5 + (value - 0.5) * keep;
46
+ return value;
47
+ });
48
+ calm.num = (key, min, max) => min + calm(key) * (max - min);
49
+ calm.int = (key, min, max) => min + Math.floor(calm(key) * (max - min + 1));
50
+ calm.jitter = (key, amount) => (calm(key) * 2 - 1) * amount;
51
+ calm.pick = (key, options) => options[Math.floor(calm(key) * options.length)];
52
+ calm.forced = t.forced;
53
+ calm.color = t.color;
54
+ calm.text = t.text;
55
+ return calm;
56
+ }
57
+ /**
58
+ * A list control with only the options the level offers, plus whichever is selected. Built
59
+ * from calmed traits, so what it shows selected is what gets drawn.
60
+ */
61
+ export function narrowed(control, t, { lists = {} }) {
62
+ const variance = varianceSet(t);
63
+ const list = lists[control.id];
64
+ if (variance === undefined || !list || t.forced(control.id) !== undefined)
65
+ return control;
66
+ const [count, fewest, from] = list;
67
+ const kept = keptOf(count, fewest, variance);
68
+ const plain = plainFirst(list).slice(0, kept);
69
+ const offered = (position) => from === "end" ? position >= 1 - kept / count : plain.includes(Math.floor(position * count));
70
+ return {
71
+ ...control,
72
+ options: control.options.filter((option) => option.id === control.selected || offered(Number(option.set[control.id]))),
73
+ };
74
+ }
75
+ /** The slider itself, with what its ends mean and what the current level gives. */
76
+ export function possibilitiesSlider(t, [low, medium, high]) {
77
+ const variance = Math.min(varianceOf(t), 0.999);
78
+ return {
79
+ type: "slider",
80
+ id: VARIANCE,
81
+ label: "Possibilities",
82
+ value: variance,
83
+ display: `${Math.round(variance * 100)}%`,
84
+ range: [0, 100],
85
+ ends: ["Low", "High"],
86
+ note: variance < 1 / 3 ? `Low: ${low}` : variance < 2 / 3 ? `Medium: ${medium}` : `High: ${high}`,
87
+ };
88
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * What Possibilities calms on a shape face. Low keeps to the eight plainest eyes and
3
+ * mouths, one for each variant, set evenly. Indices are into `EYES` and `MOUTHS`.
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,19 @@
1
+ /**
2
+ * What Possibilities calms on a shape face. Low keeps to the eight plainest eyes and
3
+ * mouths, one for each variant, set evenly. Indices are into `EYES` and `MOUTHS`.
4
+ */
5
+ export const CALMING = {
6
+ features: /^(eye|mouth)\.(size|gap|y)$|^eye[01]\.(size|y|turn)$|^(weight|tilt)$/,
7
+ lists: {
8
+ // Disc, Donut, Pill, Dash, Arch, Cloud, Square, Dome, then Triangle, Chevron, Heart, Leaf, Plus.
9
+ "eye.left": [13, 8, [2, 0, 7, 9, 5, 3, 6, 12, 10, 1, 8, 11, 4]],
10
+ "eye.right": [13, 8, [2, 0, 7, 9, 5, 3, 6, 12, 10, 1, 8, 11, 4]],
11
+ // Smile, Flat, Oh, Grin, Peep, Frown, Smirk, Wave, then Cat and Shout.
12
+ mouth: [10, 8, [0, 2, 1, 3, 5, 6, 8, 4, 9, 7]],
13
+ },
14
+ };
15
+ export const NOTES = [
16
+ "the plainest eyes and mouths, like discs, donuts, pills and smiles, set evenly.",
17
+ "adds triangles, chevrons, hearts and a cat mouth, sized and placed more freely.",
18
+ "every shape, up to leaves, pluses and a shout, at the widest spread of sizes and tilts.",
19
+ ];