@botharness/pixel-avatar 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,1533 +1,1909 @@
1
1
  import { escapeAttribute } from "@botharness/pixel-morph";
2
2
 
3
- //#region src/figure.ts
4
- const N = 32;
5
- const C = 16;
6
- const INK$1 = "#2a2230";
7
- const WHITE = "#ffffff";
8
- const BLUSH = "#f4879a";
9
- const MOUTH = "#7a2a38";
10
- const MOUTH_INSIDE = "#b8415a";
11
- const TONGUE = "#ef6f84";
12
- const GOLD = "#efb93f";
13
- const blank = () => Array.from({ length: N }, () => Array(N).fill(void 0));
14
- const ellipse = (cx, cy, rx, ry) => (x, y) => ((x + .5 - cx) / rx) ** 2 + ((y + .5 - cy) / ry) ** 2 <= 1;
15
- function channels$1(color) {
16
- const n = Number.parseInt(color.slice(1), 16);
17
- return [
18
- 16,
19
- 8,
20
- 0
21
- ].map((bit) => n >> bit & 255);
22
- }
23
- function hex(values) {
24
- return `#${values.map((v) => Math.min(255, Math.max(0, Math.round(v))).toString(16).padStart(2, "0")).join("")}`;
25
- }
26
- const shade = (color, k) => hex(channels$1(color).map((v) => v * k));
27
- const mix$1 = (base, tint, k) => {
28
- const t = channels$1(tint);
29
- return hex(channels$1(base).map((v, i) => v * (1 - k) + t[i] * k));
30
- };
31
- function paint(grid, test, color) {
32
- for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (test(x, y)) grid[y][x] = color;
33
- }
34
- function sprite(grid, rows, ox, oy, pal, flip = false) {
35
- rows.forEach((row, y) => [...row].forEach((code, i) => {
36
- const c = pal[code];
37
- const x = ox + (flip ? row.length - 1 - i : i);
38
- if (c && x >= 0 && x < N && oy + y >= 0 && oy + y < N) grid[oy + y][x] = c;
39
- }));
40
- }
41
- function outline(grid) {
42
- const out = grid.map((row) => [...row]);
43
- for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
44
- if (grid[y][x]) continue;
45
- const n = [
46
- grid[y]?.[x - 1],
47
- grid[y]?.[x + 1],
48
- grid[y - 1]?.[x],
49
- grid[y + 1]?.[x]
50
- ].find(Boolean);
51
- if (n) out[y][x] = mix$1(shade(n, .3), INK$1, .62);
52
- }
53
- return out;
54
- }
55
- const R = 6;
56
- const inTile = (x, y) => {
57
- const clamp = (v) => Math.min(Math.max(v, R), N - R);
58
- return (x + .5 - clamp(x + .5)) ** 2 + (y + .5 - clamp(y + .5)) ** 2 <= R * R;
59
- };
60
- const clip = (g) => g.map((row, y) => row.map((c, x) => inTile(x, y) ? c : void 0));
61
- function rects(grid) {
62
- let m = "";
63
- for (let y = 0; y < N; y++) {
64
- let x = 0;
65
- while (x < N) {
66
- const c = grid[y][x];
67
- if (!c) {
68
- x++;
69
- continue;
70
- }
71
- let w = 1;
72
- while (x + w < N && grid[y][x + w] === c) w++;
73
- m += `<rect x="${x}" y="${y}" width="${w}" height="1" fill="${escapeAttribute(c)}"/>`;
74
- x += w;
75
- }
3
+ //#region src/random.ts
4
+ function seededRandom(seed) {
5
+ let h = 2166136261;
6
+ for (const char of seed) {
7
+ h ^= char.codePointAt(0);
8
+ h = Math.imul(h, 16777619);
76
9
  }
77
- return m;
78
- }
79
- const FACE = {
80
- round: (s) => ellipse(C + s, 16.6, 8.6, 7.6),
81
- oval: (s) => ellipse(C + s, 16.6, 8, 8),
82
- chubby: (s) => ellipse(C + s, 17, 9.4, 7.2),
83
- long: (s) => ellipse(C + s, 16.6, 7.8, 8.4),
84
- square: (s) => (x, y) => {
85
- if (y < 8 || y > 23) return false;
86
- const dx = Math.abs(x + .5 - C - s);
87
- const r = 3;
88
- if (y <= 20) return dx <= 8.5;
89
- return dx <= 8.5 - (y - 20) ** 1.3 * 1.1 + .3 - (y === 23 ? 1 : 0) || dx <= 8.5 - r;
90
- },
91
- heart: (s) => taper(ellipse(C + s, 15.8, 9, 7.4), 16.5, 9, 2.6, 24.4, s),
92
- vchin: (s) => taper(ellipse(C + s, 15.8, 8.6, 7.4), 17.5, 8.6, 2.2, 24.4, s),
93
- diamond: (s) => taper((x, y) => y >= 9 && Math.abs(x + .5 - C - s) <= 6.4 + (y - 9) * .45, 15.8, 9.2, 2.6, 24.4, s)
94
- };
95
- function taper(top, from, width, end, chin, s) {
96
- return (x, y) => {
97
- const cy = y + .5;
98
- if (cy <= from) return top(x, y);
99
- if (cy > chin) return false;
100
- const t = (cy - from) / (chin - from);
101
- return Math.abs(x + .5 - C - s) <= width - (width - end) * t ** 1.5;
10
+ return () => {
11
+ h = Math.imul(h ^ h >>> 15, 2246822507);
12
+ h = Math.imul(h ^ h >>> 13, 3266489909);
13
+ h ^= h >>> 16;
14
+ return (h >>> 0) / 4294967296;
102
15
  };
103
16
  }
104
- const EYES = {
105
- round: [
106
- "KKKK",
107
- "DWDD",
108
- "IDDI",
109
- "LIIL"
17
+
18
+ //#endregion
19
+ //#region src/recipe.ts
20
+ const AVATAR_PARTS = {
21
+ head: [
22
+ "round",
23
+ "oval",
24
+ "square",
25
+ "long",
26
+ "heart",
27
+ "vchin",
28
+ "chubby",
29
+ "diamond"
110
30
  ],
111
- dot: [
112
- "....",
113
- ".KK.",
114
- ".KK.",
115
- "...."
31
+ pose: [
32
+ "front",
33
+ "left",
34
+ "right"
116
35
  ],
117
- sparkle: [
118
- "KKKK",
119
- "DWDW",
120
- "IDDI",
121
- "LIWL"
36
+ hair: [
37
+ "crop",
38
+ "sweep",
39
+ "spiky",
40
+ "buzz",
41
+ "curly",
42
+ "mohawk",
43
+ "bob",
44
+ "long",
45
+ "bun",
46
+ "pigtails",
47
+ "afro",
48
+ "twintails",
49
+ "drills",
50
+ "ponytail",
51
+ "sidetail",
52
+ "hime",
53
+ "odango",
54
+ "messy",
55
+ "wavy",
56
+ "braids",
57
+ "wolf",
58
+ "ahoge",
59
+ "none"
122
60
  ],
123
- lashes: [
124
- "KKKKK",
125
- "K.DWD",
126
- "..IDD",
127
- "..LII"
61
+ eyes: [
62
+ "round",
63
+ "dot",
64
+ "sparkle",
65
+ "lashes",
66
+ "sleepy",
67
+ "happy",
68
+ "wink",
69
+ "sharp"
128
70
  ],
129
- sleepy: [
130
- "....",
131
- "KKKK",
132
- "IDDI",
133
- "LIIL"
71
+ brows: [
72
+ "soft",
73
+ "thick",
74
+ "raised",
75
+ "angry",
76
+ "worried",
77
+ "none"
134
78
  ],
135
- happy: [
136
- "....",
137
- ".KK.",
138
- "K..K",
139
- "...."
79
+ nose: [
80
+ "button",
81
+ "dot",
82
+ "line",
83
+ "none"
140
84
  ],
141
- wink: [
142
- "KKKK",
143
- "DWDD",
144
- "IDDI",
145
- "LIIL"
85
+ mouth: [
86
+ "smile",
87
+ "grin",
88
+ "open",
89
+ "flat",
90
+ "smirk",
91
+ "cat",
92
+ "tongue",
93
+ "o"
146
94
  ],
147
- sharp: [
148
- "KK..",
149
- ".KKK",
150
- ".DWD",
151
- "..LI"
152
- ]
153
- };
154
- const CLOSED = [
155
- "....",
156
- "....",
157
- "KKKK",
158
- "...."
159
- ];
160
- const BROWS = {
161
- soft: [".BBB"],
162
- thick: ["BBBB"],
163
- raised: [".BB."],
164
- angry: ["BB..", "..BB"],
165
- worried: ["..BB", "BB.."],
166
- none: []
167
- };
168
- const MOUTHS = {
169
- smile: ["K..K", ".KK."],
170
- grin: ["KWWK", ".KK."],
171
- open: ["KKK", "MTM"],
172
- flat: [".KK."],
173
- smirk: ["...K", ".KK."],
174
- cat: ["K.K.K", ".K.K."],
175
- tongue: ["KKKK", ".TT."],
176
- o: [
177
- ".K.",
178
- "KMK",
179
- ".K."
180
- ]
181
- };
182
- function hair(recipe, s, b) {
183
- const none = () => false;
184
- const cap = ellipse(C + s, 14, 11, 10.6);
185
- const tip = (x, depth = 1) => [
186
- 0,
187
- depth,
188
- depth + 1,
189
- depth
190
- ][(x + 40 - s) % 4];
191
- const bangs = (line, depth = 1, sides = 22) => (x, y) => {
192
- if (!cap(x, Math.min(y, 13))) return false;
193
- const dx = x + .5 - C - s;
194
- if (Math.abs(dx) >= 7) return y <= sides - [
195
- 0,
196
- 1,
197
- 0,
198
- 2
199
- ][(x + 40 - s) % 4];
200
- return y <= line + tip(x, depth) - (Math.abs(dx) > 6 ? 0 : 0);
201
- };
202
- const longBack = (bottom, width = 11.4) => (x, y) => ellipse(C + b, 14, width + .4, 11)(x, y) && y <= 16 || y > 15 && y <= bottom && Math.abs(x + .5 - C - b) <= width - (y > bottom - 2 ? 1 : 0);
203
- switch (recipe.hair) {
204
- case "none": return {
205
- back: none,
206
- front: none
207
- };
208
- case "buzz": return {
209
- back: none,
210
- front: (x, y) => cap(x, y) && y <= 7
211
- };
212
- case "crop": return {
213
- back: none,
214
- front: bangs(10, 1, 15)
215
- };
216
- case "sweep": return {
217
- back: none,
218
- front: (x, y) => cap(x, Math.min(y, 13)) && (y <= 9 + Math.max(0, (x - 6 - s) * .45) || Math.abs(x + .5 - C - s) >= 7 && y <= 16)
219
- };
220
- case "spiky": return {
221
- back: none,
222
- front: (x, y) => cap(x, Math.max(y, 6)) && y >= [
223
- 1,
224
- 3,
225
- 5,
226
- 3
227
- ][(x + 40 - s) % 4] && bangs(11, 2, 15)(x, Math.max(y, 6))
228
- };
229
- case "curly": {
230
- const bumps = Array.from({ length: 9 }, (_, i) => {
231
- const a = Math.PI * (1.02 + i * .96 / 8);
232
- return [C + s + 11.5 * Math.cos(a), 14 + 11 * Math.sin(a)];
233
- });
234
- return {
235
- back: none,
236
- front: (x, y) => bangs(11, 1, 18)(x, y) || y <= 18 && bumps.some(([bx, by]) => (x + .5 - bx) ** 2 + (y + .5 - by) ** 2 <= 6.5)
237
- };
238
- }
239
- case "mohawk": return {
240
- back: none,
241
- front: (x, y) => Math.abs(x + .5 - C - s) <= 2 && y >= 1 && y <= 9
242
- };
243
- case "bob": return {
244
- back: longBack(25),
245
- front: bangs(13, 1, 24)
246
- };
247
- case "long": return {
248
- back: longBack(31),
249
- front: bangs(13, 1, 26)
250
- };
251
- case "bun": return {
252
- back: none,
253
- front: (x, y) => bangs(12, 1, 18)(x, y) || ellipse(C + s, 2.6, 4, 2.6)(x, y)
254
- };
255
- case "pigtails": return {
256
- back: (x, y) => ellipse(C + s - 11.5, 21, 3, 4.4)(x, y) || ellipse(C + s + 11.5, 21, 3, 4.4)(x, y),
257
- front: bangs(13, 1, 21),
258
- ties: [[C + s - 12, 16], [C + s + 11, 16]]
259
- };
260
- case "afro": return {
261
- back: ellipse(C + b, 12, 15, 11.5),
262
- front: (x, y) => cap(x, y) && y <= 8
263
- };
264
- case "twintails":
265
- case "drills": return {
266
- back: (x, y) => y >= 8 && (ellipse(C + s - 13.2, 20, 2.3, 12)(x, y) || ellipse(C + s + 13.2, 20, 2.3, 12)(x, y)),
267
- front: bangs(13, 1, 24),
268
- ties: [
269
- [C + s - 13, 9],
270
- [C + s - 12, 9],
271
- [C + s + 11, 9],
272
- [C + s + 12, 9]
273
- ],
274
- bands: recipe.hair === "drills"
275
- };
276
- case "ponytail": return {
277
- back: (x, y) => ellipse(C + s + 11.5, 19, 3, 9)(x, y) && y >= 8,
278
- front: bangs(12, 1, 20),
279
- ties: [[C + s + 10, 9], [C + s + 11, 9]]
280
- };
281
- case "sidetail": return {
282
- back: (x, y) => ellipse(C + s - 11.5, 21, 3, 8)(x, y) && y >= 12,
283
- front: (x, y) => cap(x, Math.min(y, 13)) && (y <= 9 + Math.max(0, (C + s - x) * .45) || Math.abs(x + .5 - C - s) >= 7 && y <= 18),
284
- ties: [[C + s - 13, 13], [C + s - 12, 13]]
285
- };
286
- case "hime": return {
287
- back: longBack(31),
288
- front: (x, y) => cap(x, Math.min(y, 13)) && (y <= 12 || Math.abs(x + .5 - C - s) >= 6.5 && y <= 24)
289
- };
290
- case "odango": return {
291
- back: none,
292
- front: (x, y) => bangs(12, 1, 18)(x, y) || ellipse(C + s - 8, 3.5, 3.3, 3)(x, y) || ellipse(C + s + 8, 3.5, 3.3, 3)(x, y)
293
- };
294
- case "messy": return {
295
- back: none,
296
- front: (x, y) => cap(x, Math.max(y, 4)) && y >= [
297
- 2,
298
- 0,
299
- 3,
300
- 1,
301
- 2
302
- ][(x + 50 - s) % 5] && bangs(11, 2, 17)(x, Math.max(y, 4))
303
- };
304
- case "wavy": return {
305
- back: (x, y) => longBack(30)(x, y) || y >= 16 && y <= 30 && Math.abs(x + .5 - C - b) <= 12.4 + Math.sin(y * .9) * .9,
306
- front: bangs(12, 2, 25)
307
- };
308
- case "braids": return {
309
- back: none,
310
- front: (x, y) => bangs(13, 1, 20)(x, y) || [C + s - 9.5, C + s + 9.5].some((bx) => y >= 20 && y <= 30 && Math.abs(x + .5 - bx) <= 1.6 - ((y + 1) % 3 === 0 ? .6 : 0)),
311
- ties: [[C + s - 10, 30], [C + s + 9, 30]]
312
- };
313
- case "wolf": return {
314
- back: (x, y) => longBack(24, 11.8)(x, y) && (y < 20 || (x + y) % 3 !== 0),
315
- front: (x, y) => cap(x, Math.max(y, 5)) && y >= [
316
- 1,
317
- 2,
318
- 0,
319
- 2
320
- ][(x + 40 - s) % 4] && bangs(12, 2, 21)(x, Math.max(y, 5))
321
- };
322
- case "ahoge": return {
323
- back: longBack(23),
324
- front: (x, y) => bangs(13, 1, 22)(x, y) || [
325
- [0, 0],
326
- [1, 0],
327
- [1, 1],
328
- [2, 1],
329
- [2, 2],
330
- [1, 2]
331
- ].some(([ax, ay]) => x === Math.round(C + s) + ax && y === ay + 1)
332
- };
333
- }
334
- }
335
- function detailedHair(recipe, s, b) {
336
- const of = (style) => hair({
337
- ...recipe,
338
- hair: style ?? "none"
339
- }, s, b);
340
- const bangs = of(recipe.bangs);
341
- const side = of(recipe.sideHair);
342
- const back = of(recipe.backHair);
343
- const length = (recipe.hairLength ?? 0) * 2;
344
- const lengthen = (mask) => (x, y) => mask(x, y > 18 ? Math.round(y - length) : y);
345
- return {
346
- front: (x, y) => y <= 7 ? bangs.front(x, y) || back.front(x, y) : Math.abs(x + .5 - C - s) < 7 ? bangs.front(x, y) : lengthen(side.front)(x, y),
347
- back: length === 0 ? back.back : lengthen(back.back),
348
- ...back.ties ? { ties: back.ties } : {},
349
- ...back.bands ? { bands: back.bands } : {}
350
- };
351
- }
352
- function outfit(recipe, g, s) {
353
- const shirt = recipe.shirtColor;
354
- const skin = recipe.skinColor;
355
- const dark = shade(shirt, .75);
356
- const light = mix$1(shirt, WHITE, .8);
357
- const half = (y) => 6.5 + (y - 24) * 1.15;
358
- const torso = (x, y) => y >= 24 && Math.abs(x + .5 - C - s * .5) <= half(y);
359
- const dx = (x) => x + .5 - C - s * .5;
360
- paint(g, (x, y) => y >= 22 && y <= 25 && Math.abs(dx(x)) <= 2.5, shade(skin, .85));
361
- paint(g, torso, shirt);
362
- const v = (depth, w) => (x, y) => y >= 24 && y <= 24 + depth && Math.abs(dx(x)) <= w - (y - 24) * (w / (depth + 1));
363
- switch (recipe.outfit) {
364
- case "tee":
365
- paint(g, v(1, 2.5), shade(skin, .85));
366
- break;
367
- case "shirttie":
368
- paint(g, v(3, 4), light);
369
- paint(g, (x, y) => y >= 24 && Math.abs(dx(x)) <= .5 && y <= 28, "#d22a5a");
370
- break;
371
- case "hoodie":
372
- paint(g, (x, y) => torso(x, y) && y === 24 && Math.abs(dx(x)) >= 3, dark);
373
- paint(g, (x, y) => y >= 25 && y <= 27 && (Math.abs(dx(x) + 1.5) <= .5 || Math.abs(dx(x) - 1.5) <= .5), WHITE);
374
- break;
375
- case "turtleneck":
376
- paint(g, (x, y) => y >= 22 && y <= 25 && Math.abs(dx(x)) <= 3, shade(shirt, .88));
377
- break;
378
- case "sailor":
379
- paint(g, (x, y) => torso(x, y) && y <= 26 && Math.abs(dx(x)) >= 2, "#2f3a5a");
380
- paint(g, (x, y) => torso(x, y) && y === 26 && Math.abs(dx(x)) >= 3, WHITE);
381
- sprite(g, ["R.R", ".R."], Math.round(C + s * .5) - 2, 26, { R: "#e2565f" });
382
- break;
383
- case "blazer":
384
- paint(g, v(4, 4), light);
385
- paint(g, (x, y) => torso(x, y) && y >= 24 && Math.abs(Math.abs(dx(x)) - (4 - (y - 24) * .8)) <= .5, dark);
386
- sprite(g, ["G"], Math.round(C + s * .5), 28, { G: GOLD });
387
- break;
388
- case "overalls":
389
- paint(g, torso, light);
390
- paint(g, (x, y) => torso(x, y) && (y >= 27 || Math.abs(Math.abs(dx(x)) - 4) <= .6), shirt);
391
- sprite(g, ["G....G"], Math.round(C + s * .5) - 3, 27, { G: GOLD });
392
- break;
393
- case "dress":
394
- paint(g, (x, y) => torso(x, y) && y <= 25 && Math.abs(dx(x)) <= 4.5, WHITE);
395
- paint(g, (x, y) => torso(x, y) && y === 25 && Math.abs(dx(x)) <= 4.5 && x % 2 === 0, shade(WHITE, .86));
396
- sprite(g, [
397
- "RR.RR",
398
- ".RRR.",
399
- "R...R"
400
- ], Math.round(C + s * .5) - 2, 25, { R: "#e5566a" });
401
- paint(g, (x, y) => torso(x, y) && y === 29, mix$1(shirt, WHITE, .5));
402
- break;
403
- case "kimono":
404
- paint(g, (x, y) => torso(x, y) && y <= 28 && Math.abs(Math.abs(dx(x)) - (y - 23.5)) <= .6, WHITE);
405
- paint(g, (x, y) => torso(x, y) && y >= 28, "#e2565f");
406
- paint(g, (x, y) => torso(x, y) && y === 28 && x % 3 === 0, GOLD);
407
- paint(g, (x, y) => torso(x, y) && y <= 27 && (x * 3 + y * 2) % 7 === 0, mix$1(shirt, WHITE, .45));
408
- break;
409
- case "cardigan":
410
- paint(g, v(5, 3.5), light);
411
- paint(g, (x, y) => torso(x, y) && Math.abs(Math.abs(dx(x)) - 2) <= .5 && y >= 25, dark);
412
- sprite(g, [
413
- "W",
414
- ".",
415
- "W",
416
- ".",
417
- "W"
418
- ], Math.round(C + s * .5) - 3, 25, { W: WHITE });
419
- paint(g, (x, y) => torso(x, y) && y === 29, dark);
420
- break;
421
- case "maid":
422
- paint(g, (x, y) => torso(x, y), "#2f2a3a");
423
- paint(g, (x, y) => torso(x, y) && y >= 25 && Math.abs(dx(x)) <= 4 - (y - 25) * .2, WHITE);
424
- paint(g, (x, y) => y === 24 && Math.abs(dx(x)) <= 4.5, WHITE);
425
- sprite(g, ["RR.RR", ".RRR."], Math.round(C + s * .5) - 2, 24, { R: shirt });
426
- break;
427
- case "jacket":
428
- paint(g, v(6, 4.5), WHITE);
429
- paint(g, (x, y) => torso(x, y) && Math.abs(Math.abs(dx(x)) - (4.5 - (y - 24) * .6)) <= .6, dark);
430
- paint(g, (x, y) => torso(x, y) && y >= 27 && Math.abs(dx(x)) <= .5, "#9aa3ad");
431
- paint(g, (x, y) => torso(x, y) && y === 28 && Math.abs(dx(x)) >= half(y) - 2.5, mix$1(shirt, WHITE, .6));
432
- break;
433
- }
434
- const neck = shade(skin, .85);
435
- for (let y = 24; y < N; y++) for (let x = 0; x < N; x++) {
436
- const cell = g[y][x];
437
- if (!cell || cell === neck || !torso(x, y)) continue;
438
- const d = dx(x);
439
- const edge = half(y) - Math.abs(d);
440
- if (y === 24 && Math.abs(d) <= 4.5) g[y][x] = shade(cell, .78);
441
- else if (y >= 26 && edge >= 1.6 && edge < 2.6) g[y][x] = shade(cell, .82);
442
- else if (d > 0 && edge < 1.6) g[y][x] = shade(cell, .84);
443
- else if (d < 0 && edge < 1.6 && y <= 26) g[y][x] = mix$1(cell, WHITE, .28);
444
- else if (d < -2 && y <= 25) g[y][x] = mix$1(cell, WHITE, .14);
445
- }
446
- }
447
- function accessory(recipe, g, s, face) {
448
- const shirt = recipe.shirtColor;
449
- const cap = ellipse(C + s, 14, 11.6, 11.2);
450
- switch (recipe.accessory) {
451
- case "none": return;
452
- case "beanie":
453
- paint(g, (x, y) => cap(x, y) && y <= 8, shirt);
454
- paint(g, (x, y) => cap(x, y) && (y === 8 || y === 9), shade(shirt, .78));
455
- paint(g, ellipse(C + s, 1.6, 2.2, 1.8), WHITE);
456
- return;
457
- case "cap":
458
- paint(g, (x, y) => cap(x, y) && y <= 8, shirt);
459
- paint(g, (x, y) => y === 9 && x + .5 >= C + s - 10 && x + .5 <= C + s + 13, shade(shirt, .72));
460
- return;
461
- case "headphones": {
462
- const band = (x, y) => ellipse(C + s, 14, 12, 11.6)(x, y) && !ellipse(C + s, 14, 10.8, 10.4)(x, y) && y <= 13;
463
- paint(g, band, "#5d5b66");
464
- paint(g, (x, y) => y >= 12 && y <= 19 && (Math.abs(x + .5 - (C + s - 11)) <= 1.6 || Math.abs(x + .5 - (C + s + 11)) <= 1.6), "#e25d6a");
465
- return;
466
- }
467
- case "flower":
468
- sprite(g, [
469
- ".P.P.",
470
- "PPYPP",
471
- ".PPP."
472
- ], Math.round(C + s) + 6, 3, {
473
- P: "#f59fba",
474
- Y: GOLD
475
- });
476
- return;
477
- case "bow":
478
- sprite(g, [
479
- "RR.RR",
480
- "RRrRR",
481
- "RR.RR"
482
- ], Math.round(C + s) + 6, 3, {
483
- R: "#e5566a",
484
- r: "#b83a50"
485
- });
486
- return;
487
- case "earring": {
488
- const xs = [...Array(N).keys()].filter((x) => face(x, 19));
489
- if (xs.length) {
490
- sprite(g, ["G", "G"], xs[0] - 1, 20, { G: GOLD });
491
- sprite(g, ["G", "G"], xs.at(-1) + 1, 20, { G: GOLD });
492
- }
493
- return;
494
- }
495
- case "crown":
496
- sprite(g, [
497
- "G.G.G.G",
498
- "GGGGGGG",
499
- "GrGbGrG"
500
- ], Math.round(C + s) - 3, 0, {
501
- G: GOLD,
502
- r: "#e5566a",
503
- b: "#5a9be0"
504
- });
505
- return;
506
- case "halo":
507
- paint(g, (x, y) => ellipse(C + s, 1.6, 6.5, 1.8)(x, y) && !ellipse(C + s, 1.6, 4.4, .8)(x, y), GOLD);
508
- return;
509
- case "catears": {
510
- const ear = [
511
- "X...",
512
- "XX..",
513
- "XPX.",
514
- "XXXX"
515
- ];
516
- sprite(g, ear, Math.round(C + s) - 9, 2, {
517
- X: recipe.hairColor,
518
- P: "#f4a3b5"
519
- });
520
- sprite(g, ear, Math.round(C + s) + 5, 2, {
521
- X: recipe.hairColor,
522
- P: "#f4a3b5"
523
- }, true);
524
- return;
525
- }
526
- case "hairclip":
527
- sprite(g, ["CC.CC", ".CCC."], Math.round(C + s) + 3, 9, { C: "#e5566a" });
528
- return;
529
- case "horns": {
530
- const horn = [
531
- ".H",
532
- "HH",
533
- "Hh"
534
- ];
535
- sprite(g, horn, Math.round(C + s) - 8, 1, {
536
- H: "#5b3a6e",
537
- h: "#7d5694"
538
- });
539
- sprite(g, horn, Math.round(C + s) + 6, 1, {
540
- H: "#5b3a6e",
541
- h: "#7d5694"
542
- }, true);
543
- return;
544
- }
545
- case "beret":
546
- paint(g, (x, y) => ellipse(C + s + 1.5, 4.6, 10.5, 3.6)(x, y), shirt);
547
- paint(g, (x, y) => ellipse(C + s + 1.5, 4.6, 10.5, 3.6)(x, y) && y >= 6, shade(shirt, .78));
548
- paint(g, (x, y) => ellipse(C + s + 1.5, 4.6, 10.5, 3.6)(x, y) && y <= 2 && x + .5 < C + s, mix$1(shirt, WHITE, .3));
549
- sprite(g, ["S"], Math.round(C + s) + 1, 0, { S: shade(shirt, .6) });
550
- return;
551
- case "ribbon":
552
- sprite(g, [
553
- "RRR.RRR",
554
- "RrRRRrR",
555
- "RRR.RRR",
556
- ".R...R."
557
- ], Math.round(C + s) - 3, 0, {
558
- R: "#e5566a",
559
- r: "#f49aa8"
560
- });
561
- return;
562
- case "headband":
563
- paint(g, (x, y) => ellipse(C + s, 14, 11.2, 10.8)(x, y) && !ellipse(C + s, 14.2, 10.2, 9.8)(x, y) && y <= 8, shirt);
564
- paint(g, (x, y) => ellipse(C + s, 14, 11.2, 10.8)(x, y) && !ellipse(C + s, 14.2, 10.2, 9.8)(x, y) && y <= 5, mix$1(shirt, WHITE, .35));
565
- return;
566
- case "bunnyears": {
567
- const ear = [
568
- "WW",
569
- "WP",
570
- "WP",
571
- "WP",
572
- "WW"
573
- ];
574
- sprite(g, ear, Math.round(C + s) - 6, 0, {
575
- W: "#f4f1ec",
576
- P: "#f4a3b5"
577
- });
578
- sprite(g, ear, Math.round(C + s) + 4, 0, {
579
- W: "#f4f1ec",
580
- P: "#f4a3b5"
581
- }, true);
582
- return;
583
- }
584
- case "horseears": {
585
- const ear = [
586
- ".X.",
587
- "XPX",
588
- "XPX",
589
- "XXX"
590
- ];
591
- sprite(g, ear, Math.round(C + s) - 9, 1, {
592
- X: shade(recipe.hairColor, .9),
593
- P: "#f4a3b5"
594
- });
595
- sprite(g, ear, Math.round(C + s) + 6, 1, {
596
- X: shade(recipe.hairColor, .9),
597
- P: "#f4a3b5"
598
- });
599
- sprite(g, ["BB", "BB"], Math.round(C + s) + 3, 7, { B: "#5a9be0" });
600
- return;
601
- }
602
- case "flowercrown":
603
- for (const [fx, color] of [
604
- [-8, "#f59fba"],
605
- [-4, GOLD],
606
- [0, "#f4f1ec"],
607
- [4, "#f59fba"],
608
- [8, GOLD]
609
- ]) sprite(g, [
610
- ".F.",
611
- "FCF",
612
- ".F."
613
- ], Math.round(C + s) + fx - 1, 4 + Math.round(Math.abs(fx) / 4), {
614
- F: color,
615
- C: color === GOLD ? "#e2565f" : GOLD
616
- });
617
- paint(g, (x, y) => y === 6 + Math.round(Math.abs(x + .5 - C - s) / 4) && Math.abs(x + .5 - C - s) <= 9 && x % 2 === 1, "#5aa36b");
618
- return;
619
- case "witch":
620
- paint(g, (x, y) => y >= 6 && y <= 7 && Math.abs(x + .5 - C - s) <= 13, "#3a2f52");
621
- paint(g, (x, y) => y <= 6 && Math.abs(x + .5 - C - s - (6 - y) * .5) <= 1 + y * .9, "#4b3d6b");
622
- paint(g, (x, y) => y === 5 && Math.abs(x + .5 - C - s - .5) <= 5.2, "#efb93f");
623
- return;
624
- case "pins":
625
- sprite(g, [
626
- "Y.Y",
627
- ".Y.",
628
- "Y.Y",
629
- "...",
630
- "B.B",
631
- ".B.",
632
- "B.B"
633
- ], Math.round(C + s) + 6, 7, {
634
- Y: GOLD,
635
- B: "#5a9be0"
636
- });
637
- return;
95
+ cheeks: [
96
+ "blush",
97
+ "freckles",
98
+ "none"
99
+ ],
100
+ glasses: [
101
+ "none",
102
+ "round",
103
+ "square",
104
+ "shades",
105
+ "monocle"
106
+ ],
107
+ outfit: [
108
+ "tee",
109
+ "shirttie",
110
+ "hoodie",
111
+ "turtleneck",
112
+ "sailor",
113
+ "blazer",
114
+ "overalls",
115
+ "dress",
116
+ "kimono",
117
+ "cardigan",
118
+ "maid",
119
+ "jacket"
120
+ ],
121
+ backdrop: [
122
+ "sparkles",
123
+ "hearts",
124
+ "stars",
125
+ "dots",
126
+ "none"
127
+ ],
128
+ accessory: [
129
+ "none",
130
+ "beanie",
131
+ "cap",
132
+ "headphones",
133
+ "flower",
134
+ "bow",
135
+ "earring",
136
+ "crown",
137
+ "halo",
138
+ "catears",
139
+ "hairclip",
140
+ "horns",
141
+ "beret",
142
+ "ribbon",
143
+ "headband",
144
+ "bunnyears",
145
+ "horseears",
146
+ "flowercrown",
147
+ "witch",
148
+ "pins"
149
+ ]
150
+ };
151
+ const AVATAR_HAIR_PARTS = {
152
+ bangs: [
153
+ "crop",
154
+ "bob",
155
+ "sweep",
156
+ "spiky",
157
+ "curly",
158
+ "messy",
159
+ "wolf",
160
+ "wavy",
161
+ "ahoge",
162
+ "hime",
163
+ "buzz",
164
+ "mohawk",
165
+ "none"
166
+ ],
167
+ sideHair: [
168
+ "crop",
169
+ "bun",
170
+ "bob",
171
+ "long",
172
+ "hime",
173
+ "curly",
174
+ "braids",
175
+ "none"
176
+ ],
177
+ backHair: [
178
+ "none",
179
+ "bob",
180
+ "long",
181
+ "wavy",
182
+ "wolf",
183
+ "bun",
184
+ "odango",
185
+ "pigtails",
186
+ "twintails",
187
+ "drills",
188
+ "ponytail",
189
+ "sidetail",
190
+ "afro"
191
+ ]
192
+ };
193
+ const HAIR_SPLIT = {
194
+ crop: {
195
+ bangs: "crop",
196
+ sideHair: "crop",
197
+ backHair: "none"
198
+ },
199
+ sweep: {
200
+ bangs: "sweep",
201
+ sideHair: "crop",
202
+ backHair: "none"
203
+ },
204
+ spiky: {
205
+ bangs: "spiky",
206
+ sideHair: "crop",
207
+ backHair: "none"
208
+ },
209
+ buzz: {
210
+ bangs: "buzz",
211
+ sideHair: "none",
212
+ backHair: "none"
213
+ },
214
+ curly: {
215
+ bangs: "curly",
216
+ sideHair: "curly",
217
+ backHair: "none"
218
+ },
219
+ mohawk: {
220
+ bangs: "mohawk",
221
+ sideHair: "none",
222
+ backHair: "none"
223
+ },
224
+ bob: {
225
+ bangs: "bob",
226
+ sideHair: "bob",
227
+ backHair: "bob"
228
+ },
229
+ long: {
230
+ bangs: "bob",
231
+ sideHair: "long",
232
+ backHair: "long"
233
+ },
234
+ bun: {
235
+ bangs: "bob",
236
+ sideHair: "bun",
237
+ backHair: "bun"
238
+ },
239
+ pigtails: {
240
+ bangs: "bob",
241
+ sideHair: "bun",
242
+ backHair: "pigtails"
243
+ },
244
+ afro: {
245
+ bangs: "buzz",
246
+ sideHair: "none",
247
+ backHair: "afro"
248
+ },
249
+ twintails: {
250
+ bangs: "bob",
251
+ sideHair: "bob",
252
+ backHair: "twintails"
253
+ },
254
+ drills: {
255
+ bangs: "bob",
256
+ sideHair: "bob",
257
+ backHair: "drills"
258
+ },
259
+ ponytail: {
260
+ bangs: "bob",
261
+ sideHair: "bun",
262
+ backHair: "ponytail"
263
+ },
264
+ sidetail: {
265
+ bangs: "sweep",
266
+ sideHair: "bun",
267
+ backHair: "sidetail"
268
+ },
269
+ hime: {
270
+ bangs: "hime",
271
+ sideHair: "hime",
272
+ backHair: "long"
273
+ },
274
+ odango: {
275
+ bangs: "bob",
276
+ sideHair: "bun",
277
+ backHair: "odango"
278
+ },
279
+ messy: {
280
+ bangs: "messy",
281
+ sideHair: "bun",
282
+ backHair: "none"
283
+ },
284
+ wavy: {
285
+ bangs: "wavy",
286
+ sideHair: "long",
287
+ backHair: "wavy"
288
+ },
289
+ braids: {
290
+ bangs: "bob",
291
+ sideHair: "braids",
292
+ backHair: "none"
293
+ },
294
+ wolf: {
295
+ bangs: "wolf",
296
+ sideHair: "bun",
297
+ backHair: "wolf"
298
+ },
299
+ ahoge: {
300
+ bangs: "ahoge",
301
+ sideHair: "bob",
302
+ backHair: "bob"
303
+ },
304
+ none: {
305
+ bangs: "none",
306
+ sideHair: "none",
307
+ backHair: "none"
638
308
  }
309
+ };
310
+ const AVATAR_RANGES = {
311
+ spacing: [-1, 1],
312
+ height: [-1, 1],
313
+ hairLength: [-2, 2]
314
+ };
315
+ const DETAIL_KEYS = [...Object.keys(AVATAR_HAIR_PARTS), ...Object.keys(AVATAR_RANGES)];
316
+ const AVATAR_COLORS = [
317
+ "skinColor",
318
+ "hairColor",
319
+ "eyeColor",
320
+ "shirtColor"
321
+ ];
322
+ /**
323
+ * Avatar Species: a base on the pixel bust rig. Species share motion and anchors; each sets its
324
+ * own ears, face details and suggested body colors. A recipe with a species is asset version 2.
325
+ */
326
+ const AVATAR_SPECIES = [
327
+ "human",
328
+ "goblin",
329
+ "elf",
330
+ "dwarf",
331
+ "orc",
332
+ "flower"
333
+ ];
334
+ /** Optional per-piece hair colors (asset version 2); an absent piece uses `hairColor`. */
335
+ const AVATAR_PIECE_COLORS = ["leftSideHairColor", "rightSideHairColor"];
336
+ /** Part choices that exist only in asset version 2, added after every version 1 choice. */
337
+ const AVATAR_PARTS_V2 = {
338
+ ...AVATAR_PARTS,
339
+ outfit: [
340
+ ...AVATAR_PARTS.outfit,
341
+ "armor",
342
+ "robe",
343
+ "tunic",
344
+ "cloak"
345
+ ],
346
+ accessory: [
347
+ ...AVATAR_PARTS.accessory,
348
+ "helmet",
349
+ "hood"
350
+ ]
351
+ };
352
+ /** Optional asset version 2 parts; an absent part is not drawn (or uses the first choice). */
353
+ const AVATAR_EXTRA_PARTS = {
354
+ beard: [
355
+ "short",
356
+ "full",
357
+ "braided"
358
+ ],
359
+ petals: [
360
+ "trumpet",
361
+ "daisy",
362
+ "sunflower",
363
+ "tulip",
364
+ "sakura"
365
+ ],
366
+ flowerBase: ["leaves", "pot"]
367
+ };
368
+ const DEFAULT_RECIPE = {
369
+ schemaVersion: 1,
370
+ family: "illustrated",
371
+ assetVersion: 1,
372
+ rigVersion: 1,
373
+ head: "round",
374
+ pose: "front",
375
+ hair: "crop",
376
+ eyes: "round",
377
+ brows: "soft",
378
+ nose: "button",
379
+ mouth: "smile",
380
+ cheeks: "blush",
381
+ glasses: "none",
382
+ outfit: "tee",
383
+ backdrop: "sparkles",
384
+ accessory: "none",
385
+ skinColor: "#f2c9a8",
386
+ hairColor: "#5a3a2a",
387
+ eyeColor: "#3f7fbf",
388
+ shirtColor: "#5b8bd6"
389
+ };
390
+ const PART_KEYS = Object.keys(AVATAR_PARTS);
391
+ function isPixelAvatarRecipe(value) {
392
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
393
+ const r = value;
394
+ const detailed = DETAIL_KEYS.filter((key) => Object.hasOwn(r, key)).length;
395
+ if (detailed !== 0 && detailed !== DETAIL_KEYS.length) return false;
396
+ const v2 = r["assetVersion"] === 2;
397
+ const pieces = AVATAR_PIECE_COLORS.filter((key) => Object.hasOwn(r, key));
398
+ const extras = Object.keys(AVATAR_EXTRA_PARTS).filter((key) => Object.hasOwn(r, key));
399
+ const v2Keys = v2 ? 2 + pieces.length + extras.length : 0;
400
+ if (Object.keys(r).length !== Object.keys(DEFAULT_RECIPE).length + detailed + v2Keys) return false;
401
+ if (v2 && (detailed === 0 || !AVATAR_SPECIES.includes(r["species"]) || !AVATAR_HAIR_PARTS.sideHair.includes(r["rightSideHair"]) || !pieces.every((key) => typeof r[key] === "string" && /^#[\da-f]{6}$/iu.test(r[key])) || !extras.every((key) => AVATAR_EXTRA_PARTS[key].includes(r[key])))) return false;
402
+ const catalog = v2 ? AVATAR_PARTS_V2 : AVATAR_PARTS;
403
+ return (detailed === 0 || Object.keys(AVATAR_HAIR_PARTS).every((key) => typeof r[key] === "string" && AVATAR_HAIR_PARTS[key].includes(r[key])) && Object.keys(AVATAR_RANGES).every((key) => {
404
+ const v = r[key];
405
+ const [min, max] = AVATAR_RANGES[key];
406
+ return typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
407
+ })) && r["schemaVersion"] === 1 && r["family"] === "illustrated" && (r["assetVersion"] === 1 || v2) && r["rigVersion"] === 1 && PART_KEYS.every((key) => typeof r[key] === "string" && catalog[key].includes(r[key])) && AVATAR_COLORS.every((key) => typeof r[key] === "string" && /^#[\da-f]{6}$/iu.test(r[key]));
639
408
  }
640
- function pixelTileColor(hair) {
641
- const [r, g, b] = channels$1(hair).map((v) => v / 255);
642
- const max = Math.max(r, g, b);
643
- const min = Math.min(r, g, b);
644
- const lightness = (max + min) / 2;
645
- const chroma = max - min;
646
- const saturation = chroma === 0 ? 0 : chroma / (1 - Math.abs(2 * lightness - 1));
647
- if (saturation < .18 || lightness > .86 || lightness < .16) return "#ece8e1";
648
- const hue = max === r ? ((g - b) / chroma + 6) % 6 : max === g ? (b - r) / chroma + 2 : (r - g) / chroma + 4;
649
- const s = Math.min(.55, Math.max(.35, saturation * .6));
650
- const l = .88;
651
- const f = (n) => {
652
- const k = (n + hue * 2) % 12;
653
- return l - s * Math.min(l, 1 - l) * Math.max(-1, Math.min(k - 3, 9 - k, 1));
409
+ function canonicalRecipe(recipe) {
410
+ const canonical = {
411
+ schemaVersion: 1,
412
+ family: "illustrated",
413
+ assetVersion: recipe.assetVersion,
414
+ rigVersion: 1
654
415
  };
655
- return hex([
656
- f(0),
657
- f(8),
658
- f(4)
659
- ].map((v) => v * 255));
416
+ for (const key of PART_KEYS) canonical[key] = recipe[key];
417
+ for (const key of AVATAR_COLORS) canonical[key] = recipe[key].toLowerCase();
418
+ if (recipe.bangs !== void 0) for (const key of DETAIL_KEYS) canonical[key] = recipe[key];
419
+ if (recipe.assetVersion === 2) {
420
+ canonical["species"] = recipe.species;
421
+ canonical["rightSideHair"] = recipe.rightSideHair;
422
+ for (const key of AVATAR_PIECE_COLORS) if (recipe[key] !== void 0) canonical[key] = recipe[key].toLowerCase();
423
+ for (const key of Object.keys(AVATAR_EXTRA_PARTS)) if (recipe[key] !== void 0) canonical[key] = recipe[key];
424
+ }
425
+ return canonical;
660
426
  }
661
- function pixelFigure(recipe, yawDeg, options = {}) {
662
- const yaw = yawDeg * Math.PI / 180;
663
- const sinY = Math.sin(yaw);
664
- const s = Math.round(sinY * 3);
665
- const fs = Math.round(sinY * 10);
666
- const b = -Math.round(sinY * 3.5);
667
- const turned = Math.abs(sinY) >= .3;
668
- const d = Math.sign(sinY);
669
- const skin = recipe.skinColor;
670
- const hairColor = recipe.hairColor;
671
- const front = FACE[recipe.head](s * .5);
672
- const face = turned ? (x, y) => {
673
- const dx = x + .5 - C;
674
- const tuck = y >= 17 && d * dx > 4 ? Math.min(2, (y - 16) * .35) : 0;
675
- return front(x - Math.round(d * (1 + Math.max(0, y - 20) * .4) - d * tuck), y);
676
- } : front;
677
- const masks = recipe.bangs === void 0 ? hair(recipe, s, b) : detailedHair(recipe, s, b);
678
- const body = blank();
679
- outfit(recipe, body, s);
680
- const back = blank();
681
- paint(back, masks.back, shade(hairColor, .82));
682
- if (masks.bands) paint(back, (x, y) => masks.back(x, y) && (y + Math.floor(x / 2)) % 4 === 0, shade(hairColor, .68));
683
- const head = blank();
684
- paint(head, face, skin);
685
- const earY = 15;
686
- const edge = (side) => {
687
- const xs = [...Array(N).keys()].filter((x) => face(x, earY));
688
- return side < 0 ? xs[0] - 1 : xs.at(-1) + 1;
689
- };
690
- if (!turned) for (const ex of [-1, 1]) sprite(head, [
691
- "E",
692
- "E",
693
- "e"
694
- ], edge(ex), earY, {
695
- E: skin,
696
- e: shade(skin, .86)
697
- });
698
- else sprite(head, [
699
- "EE",
700
- "Ee",
701
- "Ee",
702
- "E."
703
- ], d > 0 ? edge(-d) - 1 : edge(-d), earY, {
704
- E: skin,
705
- e: shade(skin, .8)
706
- });
707
- paint(head, masks.front, hairColor);
708
- for (const [x, y] of masks.ties ?? []) sprite(back, ["T"], Math.round(x), y, { T: shade(recipe.shirtColor, .8) });
709
- accessory(recipe, head, s, face);
710
- const shadeHair = (g) => {
711
- const out = g.map((r) => [...r]);
712
- const cells = g.flatMap((row, y) => row.flatMap((c, x) => c === hairColor ? [[x, y]] : []));
713
- const xs = cells.map(([x]) => x);
714
- const ys = cells.map(([, y]) => y);
715
- const left = Math.min(...xs);
716
- const right = Math.max(...xs);
717
- const top = Math.min(...ys);
718
- const mid = (left + right) / 2;
719
- const span = Math.max(1, (right - left) / 2);
720
- const light = mix$1(hairColor, WHITE, .2);
721
- const dark = shade(hairColor, .8);
722
- const middle = shade(hairColor, .9);
723
- for (const [x, y] of cells) {
724
- const side = (x + .5 - mid) / span + (y - top) * .025;
725
- if (side > .72) out[y][x] = dark;
726
- else if (side > .42) out[y][x] = middle;
727
- else if (side < -.2 && y <= top + 4) out[y][x] = light;
728
- }
729
- for (const [x, y] of cells) if ((g[y][x - 1] === skin || g[y][x + 1] === skin) && y >= 14) out[y][x] = shade(hairColor, .74);
730
- else if (Math.abs(x + .5 - C - s) >= 7.5 && y >= 14 && (x + 40 - s) % 3 === 0) out[y][x] = shade(out[y][x], .86);
731
- for (let y = 1; y < N; y++) for (let x = 0; x < N; x++) {
732
- if (g[y][x] !== skin) continue;
733
- if (g[y - 1][x] === hairColor) out[y][x] = mix$1(shade(skin, .84), BLUSH, .12);
734
- else if (x + 1 < N && g[y][x + 1] !== skin && g[y][x + 1] !== void 0 && x > C + s) out[y][x] = shade(skin, .9);
735
- else if (x + 1 < N && g[y][x + 1] === void 0 && x > C + s) out[y][x] = shade(skin, .9);
736
- }
737
- for (let y = 0; y < N - 1; y++) for (let x = 0; x < N; x++) if (g[y][x] === hairColor && g[y + 1][x] === skin) out[y][x] = shade(hairColor, .62);
738
- for (let x = 0; x < N; x++) for (let y = 3; y < N; y++) if (g[y][x] === hairColor && g[y + 1]?.[x] === skin && (x + 40 - s) % 4 === 0) {
739
- for (let k = 1; k <= 3; k++) if (g[y - k]?.[x] === hairColor) out[y - k][x] = shade(hairColor, .72);
740
- }
741
- const ring = mix$1(hairColor, WHITE, .45);
742
- for (let x = Math.round(C + s) - 7; x <= Math.round(C + s) + 3; x++) {
743
- const first = g.findIndex((row) => row[x] === hairColor);
744
- if (first < 0) continue;
745
- const y = first + 2 + Math.round(Math.abs(x + .5 - (C + s - 2)) / 4);
746
- if (g[y]?.[x] === hairColor && x % 3 !== 0) out[y][x] = ring;
747
- }
748
- return out;
749
- };
750
- const figure = blank();
751
- for (const layer of [
752
- back,
753
- body,
754
- shadeHair(head)
755
- ]) for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (layer[y][x]) figure[y][x] = layer[y][x];
756
- const features = blank();
757
- const eyes = blank();
758
- const closed = blank();
759
- const glasses = blank();
760
- const sp = recipe.spacing ?? 0;
761
- const h = recipe.height ?? 0;
762
- const eyeTop = 15 + h;
763
- const eyeW = (side) => turned && Math.sign(fs) === side ? 3 : 4;
764
- const eyeX = (side) => {
765
- const w = eyeW(side);
766
- return side < 0 ? Math.round(C + fs * .6) - 6 - sp - (w === 3 ? -1 : 0) : Math.round(C + fs * .6) + 2 + sp;
767
- };
768
- const left = {
769
- x: eyeX(-1),
770
- w: eyeW(-1)
427
+ const HAIR_PIECES = [
428
+ "bangs",
429
+ "sideHair",
430
+ "rightSideHair",
431
+ "backHair"
432
+ ];
433
+ /**
434
+ * Saved choices the recipe keeps but does not draw: a flower shows petals and a stem instead of
435
+ * hair, outfit, beard and headwear, and a helmet or hood covers the hair. The choices return when
436
+ * the species or headwear changes back.
437
+ */
438
+ function hiddenChoices(recipe) {
439
+ if (recipe.species === "flower") return [
440
+ ...HAIR_PIECES,
441
+ "outfit",
442
+ "accessory",
443
+ "eyes",
444
+ "brows",
445
+ "nose",
446
+ "cheeks",
447
+ "glasses",
448
+ ...recipe.beard ? ["beard"] : []
449
+ ];
450
+ const hidden = [];
451
+ if (recipe.accessory === "helmet" || recipe.accessory === "hood") hidden.push(...HAIR_PIECES);
452
+ if (recipe.species === "dwarf") hidden.push("nose");
453
+ return hidden;
454
+ }
455
+ /**
456
+ * Returns the recipe as asset version 2 with the given species, keeping every other choice.
457
+ * Side hair splits into left (`sideHair`) and right (`rightSideHair`) pieces that start equal.
458
+ * When the skin color is one of the previous species' suggested colors, it moves to the new
459
+ * species' first suggestion; a custom color is kept.
460
+ */
461
+ function withSpecies(recipe, species) {
462
+ const detailed = detailedRecipe(recipe);
463
+ const previous = recipe.species ?? "human";
464
+ const suggested = AVATAR_SPECIES_SWATCHES[previous].includes(recipe.skinColor.toLowerCase());
465
+ return {
466
+ ...detailed,
467
+ assetVersion: 2,
468
+ species,
469
+ rightSideHair: detailed.rightSideHair ?? detailed.sideHair,
470
+ skinColor: suggested && previous !== species ? AVATAR_SPECIES_SWATCHES[species][0] : recipe.skinColor
771
471
  };
772
- const right = {
773
- x: eyeX(1),
774
- w: eyeW(1)
472
+ }
473
+ function detailedRecipe(recipe) {
474
+ if (recipe.bangs !== void 0) return recipe;
475
+ const pick = (part) => AVATAR_HAIR_PARTS[part].includes(recipe.hair) ? recipe.hair : HAIR_SPLIT[recipe.hair][part];
476
+ return {
477
+ ...recipe,
478
+ bangs: pick("bangs"),
479
+ sideHair: pick("sideHair"),
480
+ backHair: pick("backHair"),
481
+ spacing: 0,
482
+ height: 0,
483
+ hairLength: 0
775
484
  };
776
- const fit = (rows, w) => rows.map((r) => r.slice(Math.max(0, r.length - w)));
777
- const pal = {
778
- K: INK$1,
779
- W: WHITE,
780
- D: shade(recipe.eyeColor, .55),
781
- I: recipe.eyeColor,
782
- L: mix$1(recipe.eyeColor, WHITE, .45)
485
+ }
486
+ const AVATAR_SWATCHES = {
487
+ skinColor: [
488
+ "#ffe3cf",
489
+ "#f2c9a8",
490
+ "#e0a87e",
491
+ "#c68863",
492
+ "#9a6142",
493
+ "#6e4129",
494
+ "#4a2c1c",
495
+ "#f4f1ec"
496
+ ],
497
+ hairColor: [
498
+ "#1d1b22",
499
+ "#5a3a2a",
500
+ "#8a5a36",
501
+ "#e2b04a",
502
+ "#c4452f",
503
+ "#d9475a",
504
+ "#f06292",
505
+ "#3fc1b8",
506
+ "#5a7be0",
507
+ "#9aa3ad",
508
+ "#f4f1ec"
509
+ ],
510
+ eyeColor: [
511
+ "#3f7fbf",
512
+ "#5a3a2a",
513
+ "#2f9e8f",
514
+ "#4c8a3c",
515
+ "#8a5ad0",
516
+ "#d0533f",
517
+ "#d9a13a",
518
+ "#2a2230"
519
+ ],
520
+ shirtColor: [
521
+ "#5b8bd6",
522
+ "#e07a5f",
523
+ "#3d9970",
524
+ "#7a5cc7",
525
+ "#f2c14e",
526
+ "#2f3a4a",
527
+ "#e2565f",
528
+ "#9ad0c2"
529
+ ]
530
+ };
531
+ const SEEDED_CHOICES = {
532
+ head: [
533
+ "round",
534
+ "oval",
535
+ "square",
536
+ "long",
537
+ "heart",
538
+ "vchin",
539
+ "chubby",
540
+ "diamond"
541
+ ],
542
+ pose: ["front"],
543
+ hair: AVATAR_PARTS.hair.filter((hair) => hair !== "none"),
544
+ eyes: [
545
+ "round",
546
+ "round",
547
+ "sparkle",
548
+ "lashes",
549
+ "dot",
550
+ "happy",
551
+ "sharp"
552
+ ],
553
+ brows: [
554
+ "soft",
555
+ "soft",
556
+ "thick",
557
+ "raised"
558
+ ],
559
+ nose: [
560
+ "button",
561
+ "dot",
562
+ "none"
563
+ ],
564
+ mouth: [
565
+ "smile",
566
+ "smile",
567
+ "grin",
568
+ "open",
569
+ "cat"
570
+ ],
571
+ cheeks: [
572
+ "blush",
573
+ "blush",
574
+ "freckles",
575
+ "none"
576
+ ],
577
+ glasses: [
578
+ "none",
579
+ "none",
580
+ "none",
581
+ "none",
582
+ "round",
583
+ "square"
584
+ ],
585
+ accessory: [
586
+ "none",
587
+ "none",
588
+ "none",
589
+ "flower",
590
+ "bow",
591
+ "hairclip",
592
+ "catears",
593
+ "headphones",
594
+ "beanie",
595
+ "cap"
596
+ ],
597
+ outfit: AVATAR_PARTS.outfit,
598
+ backdrop: [
599
+ "sparkles",
600
+ "hearts",
601
+ "stars",
602
+ "dots"
603
+ ]
604
+ };
605
+ /**
606
+ * Returns a name-seeded recipe factory. Different namespaces give different faces for the same
607
+ * name; the factory takes the name only, so it is safe to pass straight to `Array.map`.
608
+ */
609
+ function createSeededRecipe(namespace) {
610
+ return (seed) => {
611
+ const random = seededRandom(`${namespace}:${seed.trim().toLowerCase()}`);
612
+ const pick = (values) => values[Math.floor(random() * values.length)];
613
+ const recipe = {
614
+ schemaVersion: 1,
615
+ family: "illustrated",
616
+ assetVersion: 1,
617
+ rigVersion: 1
618
+ };
619
+ for (const part of Object.keys(AVATAR_PARTS)) recipe[part] = pick(SEEDED_CHOICES[part]);
620
+ for (const color of AVATAR_COLORS) recipe[color] = pick(AVATAR_SWATCHES[color].slice(0, 7));
621
+ return recipe;
783
622
  };
784
- const style = EYES[recipe.eyes];
785
- const extra = style[0].length - 4;
786
- sprite(eyes, fit(style, left.w + extra), left.x - extra, eyeTop, pal);
787
- sprite(eyes, recipe.eyes === "wink" ? fit(CLOSED, right.w) : fit(style, right.w + extra), right.x, eyeTop, pal, true);
788
- sprite(closed, fit(CLOSED, left.w), left.x, eyeTop, pal);
789
- sprite(closed, fit(CLOSED, right.w), right.x, eyeTop, pal, true);
790
- const browRows = BROWS[recipe.brows];
791
- if (browRows.length) {
792
- sprite(features, fit(browRows, left.w), left.x, eyeTop - 2, { B: shade(hairColor, .55) });
793
- sprite(features, fit(browRows, right.w), right.x, eyeTop - 2, { B: shade(hairColor, .55) }, true);
794
- }
795
- const cx = Math.round(C + fs * (turned ? .85 : .6));
796
- if (recipe.nose === "dot") sprite(features, ["N"], cx, 18 + h, { N: shade(skin, .78) });
797
- if (recipe.nose === "button") sprite(features, ["N"], cx, 18 + h, { N: shade(skin, .88) });
798
- if (recipe.nose === "line") sprite(features, ["N", "N"], cx, 17 + h, { N: shade(skin, .82) });
799
- const mx = C - .5 + fs * .6;
800
- const withMouth = (mouth) => {
801
- const layer = features.map((row) => [...row]);
802
- sprite(layer, mouth, Math.round(mx - (mouth[0].length - 1) / 2), 20 + h, {
803
- K: MOUTH,
804
- M: MOUTH_INSIDE,
805
- T: TONGUE,
806
- W: WHITE
807
- });
808
- if (recipe.cheeks === "blush") {
809
- sprite(layer, ["PP"], left.x - 1, 19 + h, { P: mix$1(BLUSH, skin, .25) });
810
- sprite(layer, ["PP"], right.x + right.w - 1, 19 + h, { P: mix$1(BLUSH, skin, .25) });
811
- }
812
- if (recipe.cheeks === "freckles") {
813
- sprite(layer, ["F.F"], left.x, 19 + h, { F: shade(skin, .7) });
814
- sprite(layer, ["F.F"], right.x + 1, 19 + h, { F: shade(skin, .7) });
815
- }
816
- return layer;
623
+ }
624
+ /**
625
+ * A version 2 name-seeded recipe factory over every species. Species are equally likely; a
626
+ * person may get a beard or a version 2 outfit, and a flower gets petals and a base. Version 1
627
+ * factories (`createSeededRecipe`) keep their faces, so a consumer chooses per identity which
628
+ * seed version it was created with.
629
+ */
630
+ function createSeededRecipeV2(namespace) {
631
+ const base = createSeededRecipe(`${namespace}:base`);
632
+ return (seed) => {
633
+ const random = seededRandom(`${namespace}:${seed.trim().toLowerCase()}`);
634
+ const pick = (values) => values[Math.floor(random() * values.length)];
635
+ const species = pick(AVATAR_SPECIES);
636
+ const recipe = {
637
+ ...withSpecies(base(seed), species),
638
+ skinColor: pick(AVATAR_SPECIES_SWATCHES[species])
639
+ };
640
+ if (species === "flower") return {
641
+ ...recipe,
642
+ petals: pick(AVATAR_EXTRA_PARTS.petals),
643
+ flowerBase: pick(AVATAR_EXTRA_PARTS.flowerBase)
644
+ };
645
+ const beard = random() < .3 ? pick(AVATAR_EXTRA_PARTS.beard) : void 0;
646
+ const outfit = random() < .25 ? pick(AVATAR_PARTS_V2.outfit.slice(-4)) : recipe.outfit;
647
+ return {
648
+ ...recipe,
649
+ outfit,
650
+ ...beard ? { beard } : {}
651
+ };
817
652
  };
818
- const savedFeatures = withMouth(MOUTHS[recipe.mouth]);
819
- const featureMarkup = options.mouthLayers ? [
820
- ["saved", MOUTHS[recipe.mouth]],
821
- ["closed", ["KKK"]],
822
- ["half-open", ["KKK", "MMM"]],
823
- ["open", [
824
- ".K.",
825
- "KMK",
826
- ".K."
827
- ]]
828
- ].map(([state, rows]) => `<g data-avatar-mouth="${state}" opacity="${state === "saved" ? 1 : 0}">${rects(withMouth(rows))}</g>`).join("") : rects(savedFeatures);
829
- const frame = (fx, w, round) => paint(glasses, (x, y) => {
830
- const inside = x >= fx && x < fx + w && y >= eyeTop && y <= eyeTop + 3;
831
- const box = x >= fx - 1 && x <= fx + w && y >= eyeTop - 1 && y <= eyeTop + 4;
832
- const corner = (x === fx - 1 || x === fx + w) && (y === eyeTop - 1 || y === eyeTop + 4);
833
- return box && !inside && !(round && corner);
834
- }, INK$1);
835
- if (recipe.glasses === "round" || recipe.glasses === "square") {
836
- frame(left.x, left.w, recipe.glasses === "round");
837
- frame(right.x, right.w, recipe.glasses === "round");
838
- paint(glasses, (x, y) => y === eyeTop + 1 && x > left.x + left.w && x < right.x - 1, INK$1);
839
- }
840
- if (recipe.glasses === "shades") {
841
- for (const e of [left, right]) paint(glasses, (x, y) => x >= e.x - 1 && x <= e.x + e.w && y >= eyeTop && y <= eyeTop + 2, INK$1);
842
- paint(glasses, (x, y) => y === eyeTop && x > left.x + left.w && x < right.x - 1, INK$1);
843
- for (const e of [left, right]) sprite(glasses, ["W"], e.x, eyeTop, { W: "#7d7b88" });
844
- }
845
- if (recipe.glasses === "monocle") {
846
- frame(right.x, right.w, true);
847
- sprite(glasses, [
848
- "G",
849
- "G",
850
- "G"
851
- ], right.x + right.w, eyeTop + 5, { G: GOLD });
852
- }
853
- const full = clip(outline(figure));
854
- const bodyCells = blank();
855
- const headCells = blank();
856
- for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
857
- const c = full[y][x];
858
- if (!c) continue;
859
- if (y >= 22 && !head[y][x] && !back[y][x]) bodyCells[y][x] = c;
860
- else headCells[y][x] = c;
861
- }
862
- const cells = [];
653
+ }
654
+ /** Version 2 counterpart of `seededRecipe` (BotHarness's namespace). */
655
+ const seededRecipeV2 = createSeededRecipeV2("botharness-avatar-v2");
656
+ /** The same name always gives the same face (BotHarness's namespace). */
657
+ const seededRecipe = createSeededRecipe("botharness-avatar");
658
+ /** Suggested body colors per species; any color remains allowed. */
659
+ const AVATAR_SPECIES_SWATCHES = {
660
+ human: AVATAR_SWATCHES.skinColor,
661
+ goblin: [
662
+ "#9cc464",
663
+ "#7fae4f",
664
+ "#6a9a45",
665
+ "#b3cf7a",
666
+ "#5c8a4a",
667
+ "#8fa86a"
668
+ ],
669
+ elf: [
670
+ "#fbe7d6",
671
+ "#f2d3c0",
672
+ "#e2c9b0",
673
+ "#d9c7e8",
674
+ "#b9c6dd",
675
+ "#8f7fa8"
676
+ ],
677
+ dwarf: [
678
+ "#f2c4a0",
679
+ "#e9b08a",
680
+ "#e0a87e",
681
+ "#c68863",
682
+ "#f0b8a0",
683
+ "#9a6142"
684
+ ],
685
+ orc: [
686
+ "#8a9a5b",
687
+ "#6f8a4e",
688
+ "#7d8c6a",
689
+ "#5f6f4a",
690
+ "#9aa070",
691
+ "#6a7a7a"
692
+ ],
693
+ flower: [
694
+ "#f6d36b",
695
+ "#f2b84b",
696
+ "#ffe3cf",
697
+ "#e8a85a",
698
+ "#c98b4a",
699
+ "#f4f1ec"
700
+ ]
701
+ };
702
+ const preset = (parts) => ({
703
+ ...DEFAULT_RECIPE,
704
+ ...parts
705
+ });
706
+ const AVATAR_PRESETS = [
707
+ preset({
708
+ hair: "wavy",
709
+ eyes: "lashes",
710
+ accessory: "beret",
711
+ outfit: "dress",
712
+ skinColor: "#ffe3cf",
713
+ hairColor: "#e2b04a",
714
+ eyeColor: "#3f7fbf",
715
+ shirtColor: "#e2565f",
716
+ backdrop: "stars"
717
+ }),
718
+ preset({
719
+ hair: "braids",
720
+ accessory: "flowercrown",
721
+ outfit: "cardigan",
722
+ skinColor: "#ffe3cf",
723
+ hairColor: "#8a5a36",
724
+ eyeColor: "#4c8a3c",
725
+ shirtColor: "#9ad0c2",
726
+ backdrop: "hearts"
727
+ }),
728
+ preset({
729
+ hair: "twintails",
730
+ eyes: "sparkle",
731
+ accessory: "ribbon",
732
+ outfit: "maid",
733
+ skinColor: "#ffe3cf",
734
+ hairColor: "#3fc1b8",
735
+ eyeColor: "#2f9e8f",
736
+ shirtColor: "#e2565f",
737
+ backdrop: "stars"
738
+ }),
739
+ preset({
740
+ hair: "long",
741
+ accessory: "horseears",
742
+ outfit: "jacket",
743
+ skinColor: "#ffe3cf",
744
+ hairColor: "#8a5a36",
745
+ eyeColor: "#d0533f",
746
+ shirtColor: "#5b8bd6",
747
+ backdrop: "dots"
748
+ }),
749
+ preset({
750
+ hair: "hime",
751
+ eyes: "sleepy",
752
+ accessory: "pins",
753
+ outfit: "kimono",
754
+ skinColor: "#f4f1ec",
755
+ hairColor: "#1d1b22",
756
+ eyeColor: "#d0533f",
757
+ shirtColor: "#7a5cc7",
758
+ backdrop: "none"
759
+ }),
760
+ preset({
761
+ hair: "ahoge",
762
+ accessory: "headband",
763
+ outfit: "sailor",
764
+ skinColor: "#ffe3cf",
765
+ hairColor: "#f06292",
766
+ eyeColor: "#2f9e8f",
767
+ shirtColor: "#f4f1ec",
768
+ backdrop: "hearts"
769
+ }),
770
+ preset({
771
+ hair: "wolf",
772
+ eyes: "sharp",
773
+ outfit: "jacket",
774
+ skinColor: "#e0a87e",
775
+ hairColor: "#9aa3ad",
776
+ eyeColor: "#5a7be0",
777
+ shirtColor: "#2f3a4a",
778
+ backdrop: "stars"
779
+ }),
780
+ preset({
781
+ hair: "wavy",
782
+ eyes: "sparkle",
783
+ accessory: "witch",
784
+ outfit: "dress",
785
+ skinColor: "#ffe3cf",
786
+ hairColor: "#8a5ad0",
787
+ eyeColor: "#d9a13a",
788
+ shirtColor: "#2f3a4a",
789
+ backdrop: "sparkles"
790
+ }),
791
+ preset({
792
+ hair: "bob",
793
+ eyes: "happy",
794
+ accessory: "bunnyears",
795
+ outfit: "cardigan",
796
+ skinColor: "#ffe3cf",
797
+ hairColor: "#f4f1ec",
798
+ eyeColor: "#d9475a",
799
+ shirtColor: "#f2c14e",
800
+ backdrop: "hearts"
801
+ }),
802
+ preset({
803
+ hair: "spiky",
804
+ glasses: "round",
805
+ accessory: "headphones",
806
+ outfit: "hoodie",
807
+ backdrop: "dots"
808
+ }),
809
+ preset({
810
+ hair: "odango",
811
+ eyes: "lashes",
812
+ accessory: "bow",
813
+ outfit: "kimono",
814
+ hairColor: "#e2b04a",
815
+ shirtColor: "#5b8bd6",
816
+ backdrop: "sparkles"
817
+ }),
818
+ preset({
819
+ hair: "crop",
820
+ accessory: "cap",
821
+ outfit: "shirttie",
822
+ skinColor: "#9a6142",
823
+ hairColor: "#1d1b22",
824
+ shirtColor: "#3d9970",
825
+ backdrop: "dots"
826
+ })
827
+ ];
828
+
829
+ //#endregion
830
+ //#region src/figure.ts
831
+ const N = 32;
832
+ const C = 16;
833
+ const INK$1 = "#2a2230";
834
+ const WHITE = "#ffffff";
835
+ const BLUSH = "#f4879a";
836
+ const MOUTH = "#7a2a38";
837
+ const MOUTH_INSIDE = "#b8415a";
838
+ const TONGUE = "#ef6f84";
839
+ const GOLD = "#efb93f";
840
+ const TUSK = "#f6f0d8";
841
+ const blank = () => Array.from({ length: N }, () => Array(N).fill(void 0));
842
+ const ellipse = (cx, cy, rx, ry) => (x, y) => ((x + .5 - cx) / rx) ** 2 + ((y + .5 - cy) / ry) ** 2 <= 1;
843
+ function channels$1(color) {
844
+ const n = Number.parseInt(color.slice(1), 16);
845
+ return [
846
+ 16,
847
+ 8,
848
+ 0
849
+ ].map((bit) => n >> bit & 255);
850
+ }
851
+ function hex(values) {
852
+ return `#${values.map((v) => Math.min(255, Math.max(0, Math.round(v))).toString(16).padStart(2, "0")).join("")}`;
853
+ }
854
+ const shade = (color, k) => hex(channels$1(color).map((v) => v * k));
855
+ const mix$1 = (base, tint, k) => {
856
+ const t = channels$1(tint);
857
+ return hex(channels$1(base).map((v, i) => v * (1 - k) + t[i] * k));
858
+ };
859
+ function paint(grid, test, color) {
860
+ for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (test(x, y)) grid[y][x] = color;
861
+ }
862
+ function sprite(grid, rows, ox, oy, pal, flip = false) {
863
+ rows.forEach((row, y) => [...row].forEach((code, i) => {
864
+ const c = pal[code];
865
+ const x = ox + (flip ? row.length - 1 - i : i);
866
+ if (c && x >= 0 && x < N && oy + y >= 0 && oy + y < N) grid[oy + y][x] = c;
867
+ }));
868
+ }
869
+ function outline(grid) {
870
+ const out = grid.map((row) => [...row]);
863
871
  for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
864
- const c = glasses[y][x] ?? eyes[y][x] ?? savedFeatures[y][x] ?? full[y][x];
865
- if (c) cells.push({
866
- x,
867
- y,
868
- c
869
- });
872
+ if (grid[y][x]) continue;
873
+ const n = [
874
+ grid[y]?.[x - 1],
875
+ grid[y]?.[x + 1],
876
+ grid[y - 1]?.[x],
877
+ grid[y + 1]?.[x]
878
+ ].find(Boolean);
879
+ if (n) out[y][x] = mix$1(shade(n, .3), INK$1, .62);
870
880
  }
871
- return {
872
- cells,
873
- tile: `<rect width="32" height="32" rx="6" fill="${pixelTileColor(recipe.hairColor)}"/>`,
874
- body: `<g class="bh-illustrated-body">${rects(bodyCells)}</g>`,
875
- head: [
876
- `<g class="bh-illustrated-head">${rects(headCells)}`,
877
- `<g class="bh-illustrated-face">${featureMarkup}`,
878
- `<g class="bh-illustrated-gaze">${rects(eyes)}</g>`,
879
- `<g class="bh-illustrated-blink" opacity="0">${rects(closed)}</g>`,
880
- `${rects(glasses)}</g></g>`
881
- ].join("")
882
- };
881
+ return out;
883
882
  }
884
-
885
- //#endregion
886
- //#region src/random.ts
887
- function seededRandom(seed) {
888
- let h = 2166136261;
889
- for (const char of seed) {
890
- h ^= char.codePointAt(0);
891
- h = Math.imul(h, 16777619);
883
+ const R = 6;
884
+ const inTile = (x, y) => {
885
+ const clamp = (v) => Math.min(Math.max(v, R), N - R);
886
+ return (x + .5 - clamp(x + .5)) ** 2 + (y + .5 - clamp(y + .5)) ** 2 <= R * R;
887
+ };
888
+ const clip = (g) => g.map((row, y) => row.map((c, x) => inTile(x, y) ? c : void 0));
889
+ function rects(grid) {
890
+ let m = "";
891
+ for (let y = 0; y < N; y++) {
892
+ let x = 0;
893
+ while (x < N) {
894
+ const c = grid[y][x];
895
+ if (!c) {
896
+ x++;
897
+ continue;
898
+ }
899
+ let w = 1;
900
+ while (x + w < N && grid[y][x + w] === c) w++;
901
+ m += `<rect x="${x}" y="${y}" width="${w}" height="1" fill="${escapeAttribute(c)}"/>`;
902
+ x += w;
903
+ }
892
904
  }
893
- return () => {
894
- h = Math.imul(h ^ h >>> 15, 2246822507);
895
- h = Math.imul(h ^ h >>> 13, 3266489909);
896
- h ^= h >>> 16;
897
- return (h >>> 0) / 4294967296;
905
+ return m;
906
+ }
907
+ const FACE = {
908
+ round: (s) => ellipse(C + s, 16.6, 8.6, 7.6),
909
+ oval: (s) => ellipse(C + s, 16.6, 8, 8),
910
+ chubby: (s) => ellipse(C + s, 17, 9.4, 7.2),
911
+ long: (s) => ellipse(C + s, 16.6, 7.8, 8.4),
912
+ square: (s) => (x, y) => {
913
+ if (y < 8 || y > 23) return false;
914
+ const dx = Math.abs(x + .5 - C - s);
915
+ const r = 3;
916
+ if (y <= 20) return dx <= 8.5;
917
+ return dx <= 8.5 - (y - 20) ** 1.3 * 1.1 + .3 - (y === 23 ? 1 : 0) || dx <= 8.5 - r;
918
+ },
919
+ heart: (s) => taper(ellipse(C + s, 15.8, 9, 7.4), 16.5, 9, 2.6, 24.4, s),
920
+ vchin: (s) => taper(ellipse(C + s, 15.8, 8.6, 7.4), 17.5, 8.6, 2.2, 24.4, s),
921
+ diamond: (s) => taper((x, y) => y >= 9 && Math.abs(x + .5 - C - s) <= 6.4 + (y - 9) * .45, 15.8, 9.2, 2.6, 24.4, s)
922
+ };
923
+ function taper(top, from, width, end, chin, s) {
924
+ return (x, y) => {
925
+ const cy = y + .5;
926
+ if (cy <= from) return top(x, y);
927
+ if (cy > chin) return false;
928
+ const t = (cy - from) / (chin - from);
929
+ return Math.abs(x + .5 - C - s) <= width - (width - end) * t ** 1.5;
898
930
  };
899
931
  }
900
-
901
- //#endregion
902
- //#region src/recipe.ts
903
- const AVATAR_PARTS = {
904
- head: [
905
- "round",
906
- "oval",
907
- "square",
908
- "long",
909
- "heart",
910
- "vchin",
911
- "chubby",
912
- "diamond"
913
- ],
914
- pose: [
915
- "front",
916
- "left",
917
- "right"
918
- ],
919
- hair: [
920
- "crop",
921
- "sweep",
922
- "spiky",
923
- "buzz",
924
- "curly",
925
- "mohawk",
926
- "bob",
927
- "long",
928
- "bun",
929
- "pigtails",
930
- "afro",
931
- "twintails",
932
- "drills",
933
- "ponytail",
934
- "sidetail",
935
- "hime",
936
- "odango",
937
- "messy",
938
- "wavy",
939
- "braids",
940
- "wolf",
941
- "ahoge",
942
- "none"
943
- ],
944
- eyes: [
945
- "round",
946
- "dot",
947
- "sparkle",
948
- "lashes",
949
- "sleepy",
950
- "happy",
951
- "wink",
952
- "sharp"
953
- ],
954
- brows: [
955
- "soft",
956
- "thick",
957
- "raised",
958
- "angry",
959
- "worried",
960
- "none"
932
+ const EYES = {
933
+ round: [
934
+ "KKKK",
935
+ "DWDD",
936
+ "IDDI",
937
+ "LIIL"
961
938
  ],
962
- nose: [
963
- "button",
964
- "dot",
965
- "line",
966
- "none"
939
+ dot: [
940
+ "....",
941
+ ".KK.",
942
+ ".KK.",
943
+ "...."
967
944
  ],
968
- mouth: [
969
- "smile",
970
- "grin",
971
- "open",
972
- "flat",
973
- "smirk",
974
- "cat",
975
- "tongue",
976
- "o"
945
+ sparkle: [
946
+ "KKKK",
947
+ "DWDW",
948
+ "IDDI",
949
+ "LIWL"
977
950
  ],
978
- cheeks: [
979
- "blush",
980
- "freckles",
981
- "none"
951
+ lashes: [
952
+ "KKKKK",
953
+ "K.DWD",
954
+ "..IDD",
955
+ "..LII"
982
956
  ],
983
- glasses: [
984
- "none",
985
- "round",
986
- "square",
987
- "shades",
988
- "monocle"
957
+ sleepy: [
958
+ "....",
959
+ "KKKK",
960
+ "IDDI",
961
+ "LIIL"
989
962
  ],
990
- outfit: [
991
- "tee",
992
- "shirttie",
993
- "hoodie",
994
- "turtleneck",
995
- "sailor",
996
- "blazer",
997
- "overalls",
998
- "dress",
999
- "kimono",
1000
- "cardigan",
1001
- "maid",
1002
- "jacket"
963
+ happy: [
964
+ "....",
965
+ ".KK.",
966
+ "K..K",
967
+ "...."
1003
968
  ],
1004
- backdrop: [
1005
- "sparkles",
1006
- "hearts",
1007
- "stars",
1008
- "dots",
1009
- "none"
969
+ wink: [
970
+ "KKKK",
971
+ "DWDD",
972
+ "IDDI",
973
+ "LIIL"
1010
974
  ],
1011
- accessory: [
1012
- "none",
1013
- "beanie",
1014
- "cap",
1015
- "headphones",
1016
- "flower",
1017
- "bow",
1018
- "earring",
1019
- "crown",
1020
- "halo",
1021
- "catears",
1022
- "hairclip",
1023
- "horns",
1024
- "beret",
1025
- "ribbon",
1026
- "headband",
1027
- "bunnyears",
1028
- "horseears",
1029
- "flowercrown",
1030
- "witch",
1031
- "pins"
975
+ sharp: [
976
+ "KK..",
977
+ ".KKK",
978
+ ".DWD",
979
+ "..LI"
1032
980
  ]
1033
981
  };
1034
- const AVATAR_HAIR_PARTS = {
1035
- bangs: [
1036
- "crop",
1037
- "bob",
1038
- "sweep",
1039
- "spiky",
1040
- "curly",
1041
- "messy",
1042
- "wolf",
1043
- "wavy",
1044
- "ahoge",
1045
- "hime",
1046
- "buzz",
1047
- "mohawk",
1048
- "none"
1049
- ],
1050
- sideHair: [
1051
- "crop",
1052
- "bun",
1053
- "bob",
1054
- "long",
1055
- "hime",
1056
- "curly",
1057
- "braids",
1058
- "none"
1059
- ],
1060
- backHair: [
1061
- "none",
1062
- "bob",
1063
- "long",
1064
- "wavy",
1065
- "wolf",
1066
- "bun",
1067
- "odango",
1068
- "pigtails",
1069
- "twintails",
1070
- "drills",
1071
- "ponytail",
1072
- "sidetail",
1073
- "afro"
982
+ const CLOSED = [
983
+ "....",
984
+ "....",
985
+ "KKKK",
986
+ "...."
987
+ ];
988
+ const BROWS = {
989
+ soft: [".BBB"],
990
+ thick: ["BBBB"],
991
+ raised: [".BB."],
992
+ angry: ["BB..", "..BB"],
993
+ worried: ["..BB", "BB.."],
994
+ none: []
995
+ };
996
+ const MOUTHS = {
997
+ smile: ["K..K", ".KK."],
998
+ grin: ["KWWK", ".KK."],
999
+ open: ["KKK", "MTM"],
1000
+ flat: [".KK."],
1001
+ smirk: ["...K", ".KK."],
1002
+ cat: ["K.K.K", ".K.K."],
1003
+ tongue: ["KKKK", ".TT."],
1004
+ o: [
1005
+ ".K.",
1006
+ "KMK",
1007
+ ".K."
1074
1008
  ]
1075
1009
  };
1076
- const HAIR_SPLIT = {
1077
- crop: {
1078
- bangs: "crop",
1079
- sideHair: "crop",
1080
- backHair: "none"
1081
- },
1082
- sweep: {
1083
- bangs: "sweep",
1084
- sideHair: "crop",
1085
- backHair: "none"
1086
- },
1087
- spiky: {
1088
- bangs: "spiky",
1089
- sideHair: "crop",
1090
- backHair: "none"
1091
- },
1092
- buzz: {
1093
- bangs: "buzz",
1094
- sideHair: "none",
1095
- backHair: "none"
1096
- },
1097
- curly: {
1098
- bangs: "curly",
1099
- sideHair: "curly",
1100
- backHair: "none"
1101
- },
1102
- mohawk: {
1103
- bangs: "mohawk",
1104
- sideHair: "none",
1105
- backHair: "none"
1106
- },
1107
- bob: {
1108
- bangs: "bob",
1109
- sideHair: "bob",
1110
- backHair: "bob"
1111
- },
1112
- long: {
1113
- bangs: "bob",
1114
- sideHair: "long",
1115
- backHair: "long"
1116
- },
1117
- bun: {
1118
- bangs: "bob",
1119
- sideHair: "bun",
1120
- backHair: "bun"
1121
- },
1122
- pigtails: {
1123
- bangs: "bob",
1124
- sideHair: "bun",
1125
- backHair: "pigtails"
1126
- },
1127
- afro: {
1128
- bangs: "buzz",
1129
- sideHair: "none",
1130
- backHair: "afro"
1131
- },
1132
- twintails: {
1133
- bangs: "bob",
1134
- sideHair: "bob",
1135
- backHair: "twintails"
1136
- },
1137
- drills: {
1138
- bangs: "bob",
1139
- sideHair: "bob",
1140
- backHair: "drills"
1141
- },
1142
- ponytail: {
1143
- bangs: "bob",
1144
- sideHair: "bun",
1145
- backHair: "ponytail"
1146
- },
1147
- sidetail: {
1148
- bangs: "sweep",
1149
- sideHair: "bun",
1150
- backHair: "sidetail"
1151
- },
1152
- hime: {
1153
- bangs: "hime",
1154
- sideHair: "hime",
1155
- backHair: "long"
1156
- },
1157
- odango: {
1158
- bangs: "bob",
1159
- sideHair: "bun",
1160
- backHair: "odango"
1161
- },
1162
- messy: {
1163
- bangs: "messy",
1164
- sideHair: "bun",
1165
- backHair: "none"
1166
- },
1167
- wavy: {
1168
- bangs: "wavy",
1169
- sideHair: "long",
1170
- backHair: "wavy"
1171
- },
1172
- braids: {
1173
- bangs: "bob",
1174
- sideHair: "braids",
1175
- backHair: "none"
1176
- },
1177
- wolf: {
1178
- bangs: "wolf",
1179
- sideHair: "bun",
1180
- backHair: "wolf"
1010
+ function hair(recipe, s, b) {
1011
+ const none = () => false;
1012
+ const cap = ellipse(C + s, 14, 11, 10.6);
1013
+ const tip = (x, depth = 1) => [
1014
+ 0,
1015
+ depth,
1016
+ depth + 1,
1017
+ depth
1018
+ ][(x + 40 - s) % 4];
1019
+ const bangs = (line, depth = 1, sides = 22) => (x, y) => {
1020
+ if (!cap(x, Math.min(y, 13))) return false;
1021
+ const dx = x + .5 - C - s;
1022
+ if (Math.abs(dx) >= 7) return y <= sides - [
1023
+ 0,
1024
+ 1,
1025
+ 0,
1026
+ 2
1027
+ ][(x + 40 - s) % 4];
1028
+ return y <= line + tip(x, depth) - (Math.abs(dx) > 6 ? 0 : 0);
1029
+ };
1030
+ const longBack = (bottom, width = 11.4) => (x, y) => ellipse(C + b, 14, width + .4, 11)(x, y) && y <= 16 || y > 15 && y <= bottom && Math.abs(x + .5 - C - b) <= width - (y > bottom - 2 ? 1 : 0);
1031
+ switch (recipe.hair) {
1032
+ case "none": return {
1033
+ back: none,
1034
+ front: none
1035
+ };
1036
+ case "buzz": return {
1037
+ back: none,
1038
+ front: (x, y) => cap(x, y) && y <= 7
1039
+ };
1040
+ case "crop": return {
1041
+ back: none,
1042
+ front: bangs(10, 1, 15)
1043
+ };
1044
+ case "sweep": return {
1045
+ back: none,
1046
+ front: (x, y) => cap(x, Math.min(y, 13)) && (y <= 9 + Math.max(0, (x - 6 - s) * .45) || Math.abs(x + .5 - C - s) >= 7 && y <= 16)
1047
+ };
1048
+ case "spiky": return {
1049
+ back: none,
1050
+ front: (x, y) => cap(x, Math.max(y, 6)) && y >= [
1051
+ 1,
1052
+ 3,
1053
+ 5,
1054
+ 3
1055
+ ][(x + 40 - s) % 4] && bangs(11, 2, 15)(x, Math.max(y, 6))
1056
+ };
1057
+ case "curly": {
1058
+ const bumps = Array.from({ length: 9 }, (_, i) => {
1059
+ const a = Math.PI * (1.02 + i * .96 / 8);
1060
+ return [C + s + 11.5 * Math.cos(a), 14 + 11 * Math.sin(a)];
1061
+ });
1062
+ return {
1063
+ back: none,
1064
+ front: (x, y) => bangs(11, 1, 18)(x, y) || y <= 18 && bumps.some(([bx, by]) => (x + .5 - bx) ** 2 + (y + .5 - by) ** 2 <= 6.5)
1065
+ };
1066
+ }
1067
+ case "mohawk": return {
1068
+ back: none,
1069
+ front: (x, y) => Math.abs(x + .5 - C - s) <= 2 && y >= 1 && y <= 9
1070
+ };
1071
+ case "bob": return {
1072
+ back: longBack(25),
1073
+ front: bangs(13, 1, 24)
1074
+ };
1075
+ case "long": return {
1076
+ back: longBack(31),
1077
+ front: bangs(13, 1, 26)
1078
+ };
1079
+ case "bun": return {
1080
+ back: none,
1081
+ front: (x, y) => bangs(12, 1, 18)(x, y) || ellipse(C + s, 2.6, 4, 2.6)(x, y)
1082
+ };
1083
+ case "pigtails": return {
1084
+ back: (x, y) => ellipse(C + s - 11.5, 21, 3, 4.4)(x, y) || ellipse(C + s + 11.5, 21, 3, 4.4)(x, y),
1085
+ front: bangs(13, 1, 21),
1086
+ ties: [[C + s - 12, 16], [C + s + 11, 16]]
1087
+ };
1088
+ case "afro": return {
1089
+ back: ellipse(C + b, 12, 15, 11.5),
1090
+ front: (x, y) => cap(x, y) && y <= 8
1091
+ };
1092
+ case "twintails":
1093
+ case "drills": return {
1094
+ back: (x, y) => y >= 8 && (ellipse(C + s - 13.2, 20, 2.3, 12)(x, y) || ellipse(C + s + 13.2, 20, 2.3, 12)(x, y)),
1095
+ front: bangs(13, 1, 24),
1096
+ ties: [
1097
+ [C + s - 13, 9],
1098
+ [C + s - 12, 9],
1099
+ [C + s + 11, 9],
1100
+ [C + s + 12, 9]
1101
+ ],
1102
+ bands: recipe.hair === "drills"
1103
+ };
1104
+ case "ponytail": return {
1105
+ back: (x, y) => ellipse(C + s + 11.5, 19, 3, 9)(x, y) && y >= 8,
1106
+ front: bangs(12, 1, 20),
1107
+ ties: [[C + s + 10, 9], [C + s + 11, 9]]
1108
+ };
1109
+ case "sidetail": return {
1110
+ back: (x, y) => ellipse(C + s - 11.5, 21, 3, 8)(x, y) && y >= 12,
1111
+ front: (x, y) => cap(x, Math.min(y, 13)) && (y <= 9 + Math.max(0, (C + s - x) * .45) || Math.abs(x + .5 - C - s) >= 7 && y <= 18),
1112
+ ties: [[C + s - 13, 13], [C + s - 12, 13]]
1113
+ };
1114
+ case "hime": return {
1115
+ back: longBack(31),
1116
+ front: (x, y) => cap(x, Math.min(y, 13)) && (y <= 12 || Math.abs(x + .5 - C - s) >= 6.5 && y <= 24)
1117
+ };
1118
+ case "odango": return {
1119
+ back: none,
1120
+ front: (x, y) => bangs(12, 1, 18)(x, y) || ellipse(C + s - 8, 3.5, 3.3, 3)(x, y) || ellipse(C + s + 8, 3.5, 3.3, 3)(x, y)
1121
+ };
1122
+ case "messy": return {
1123
+ back: none,
1124
+ front: (x, y) => cap(x, Math.max(y, 4)) && y >= [
1125
+ 2,
1126
+ 0,
1127
+ 3,
1128
+ 1,
1129
+ 2
1130
+ ][(x + 50 - s) % 5] && bangs(11, 2, 17)(x, Math.max(y, 4))
1131
+ };
1132
+ case "wavy": return {
1133
+ back: (x, y) => longBack(30)(x, y) || y >= 16 && y <= 30 && Math.abs(x + .5 - C - b) <= 12.4 + Math.sin(y * .9) * .9,
1134
+ front: bangs(12, 2, 25)
1135
+ };
1136
+ case "braids": return {
1137
+ back: none,
1138
+ front: (x, y) => bangs(13, 1, 20)(x, y) || [C + s - 9.5, C + s + 9.5].some((bx) => y >= 20 && y <= 30 && Math.abs(x + .5 - bx) <= 1.6 - ((y + 1) % 3 === 0 ? .6 : 0)),
1139
+ ties: [[C + s - 10, 30], [C + s + 9, 30]]
1140
+ };
1141
+ case "wolf": return {
1142
+ back: (x, y) => longBack(24, 11.8)(x, y) && (y < 20 || (x + y) % 3 !== 0),
1143
+ front: (x, y) => cap(x, Math.max(y, 5)) && y >= [
1144
+ 1,
1145
+ 2,
1146
+ 0,
1147
+ 2
1148
+ ][(x + 40 - s) % 4] && bangs(12, 2, 21)(x, Math.max(y, 5))
1149
+ };
1150
+ case "ahoge": return {
1151
+ back: longBack(23),
1152
+ front: (x, y) => bangs(13, 1, 22)(x, y) || [
1153
+ [0, 0],
1154
+ [1, 0],
1155
+ [1, 1],
1156
+ [2, 1],
1157
+ [2, 2],
1158
+ [1, 2]
1159
+ ].some(([ax, ay]) => x === Math.round(C + s) + ax && y === ay + 1)
1160
+ };
1161
+ }
1162
+ }
1163
+ function detailedHair(recipe, s, b) {
1164
+ const of = (style) => hair({
1165
+ ...recipe,
1166
+ hair: style ?? "none"
1167
+ }, s, b);
1168
+ const bangs = of(recipe.bangs);
1169
+ const side = of(recipe.sideHair);
1170
+ const right = recipe.rightSideHair === void 0 ? side : of(recipe.rightSideHair);
1171
+ const back = of(recipe.backHair);
1172
+ const length = (recipe.hairLength ?? 0) * 2;
1173
+ const lengthen = (mask) => (x, y) => mask(x, y > 18 ? Math.round(y - length) : y);
1174
+ const sideOf = (sign) => (x, y) => y > 7 && sign * (x + .5 - C - s) >= 7 && lengthen((sign < 0 ? side : right).front)(x, y);
1175
+ return {
1176
+ front: (x, y) => y <= 7 ? bangs.front(x, y) || back.front(x, y) : Math.abs(x + .5 - C - s) < 7 ? bangs.front(x, y) : lengthen((x + .5 - C - s < 0 ? side : right).front)(x, y),
1177
+ sides: [sideOf(-1), sideOf(1)],
1178
+ back: length === 0 ? back.back : lengthen(back.back),
1179
+ ...back.ties ? { ties: back.ties } : {},
1180
+ ...back.bands ? { bands: back.bands } : {}
1181
+ };
1182
+ }
1183
+ function outfit(recipe, g, s) {
1184
+ const shirt = recipe.shirtColor;
1185
+ const skin = recipe.skinColor;
1186
+ const dark = shade(shirt, .75);
1187
+ const light = mix$1(shirt, WHITE, .8);
1188
+ const half = (y) => 6.5 + (y - 24) * 1.15;
1189
+ const torso = (x, y) => y >= 24 && Math.abs(x + .5 - C - s * .5) <= half(y);
1190
+ const dx = (x) => x + .5 - C - s * .5;
1191
+ paint(g, (x, y) => y >= 22 && y <= 25 && Math.abs(dx(x)) <= 2.5, shade(skin, .85));
1192
+ paint(g, torso, shirt);
1193
+ const v = (depth, w) => (x, y) => y >= 24 && y <= 24 + depth && Math.abs(dx(x)) <= w - (y - 24) * (w / (depth + 1));
1194
+ switch (recipe.outfit) {
1195
+ case "tee":
1196
+ paint(g, v(1, 2.5), shade(skin, .85));
1197
+ break;
1198
+ case "shirttie":
1199
+ paint(g, v(3, 4), light);
1200
+ paint(g, (x, y) => y >= 24 && Math.abs(dx(x)) <= .5 && y <= 28, "#d22a5a");
1201
+ break;
1202
+ case "hoodie":
1203
+ paint(g, (x, y) => torso(x, y) && y === 24 && Math.abs(dx(x)) >= 3, dark);
1204
+ paint(g, (x, y) => y >= 25 && y <= 27 && (Math.abs(dx(x) + 1.5) <= .5 || Math.abs(dx(x) - 1.5) <= .5), WHITE);
1205
+ break;
1206
+ case "turtleneck":
1207
+ paint(g, (x, y) => y >= 22 && y <= 25 && Math.abs(dx(x)) <= 3, shade(shirt, .88));
1208
+ break;
1209
+ case "sailor":
1210
+ paint(g, (x, y) => torso(x, y) && y <= 26 && Math.abs(dx(x)) >= 2, "#2f3a5a");
1211
+ paint(g, (x, y) => torso(x, y) && y === 26 && Math.abs(dx(x)) >= 3, WHITE);
1212
+ sprite(g, ["R.R", ".R."], Math.round(C + s * .5) - 2, 26, { R: "#e2565f" });
1213
+ break;
1214
+ case "blazer":
1215
+ paint(g, v(4, 4), light);
1216
+ paint(g, (x, y) => torso(x, y) && y >= 24 && Math.abs(Math.abs(dx(x)) - (4 - (y - 24) * .8)) <= .5, dark);
1217
+ sprite(g, ["G"], Math.round(C + s * .5), 28, { G: GOLD });
1218
+ break;
1219
+ case "overalls":
1220
+ paint(g, torso, light);
1221
+ paint(g, (x, y) => torso(x, y) && (y >= 27 || Math.abs(Math.abs(dx(x)) - 4) <= .6), shirt);
1222
+ sprite(g, ["G....G"], Math.round(C + s * .5) - 3, 27, { G: GOLD });
1223
+ break;
1224
+ case "dress":
1225
+ paint(g, (x, y) => torso(x, y) && y <= 25 && Math.abs(dx(x)) <= 4.5, WHITE);
1226
+ paint(g, (x, y) => torso(x, y) && y === 25 && Math.abs(dx(x)) <= 4.5 && x % 2 === 0, shade(WHITE, .86));
1227
+ sprite(g, [
1228
+ "RR.RR",
1229
+ ".RRR.",
1230
+ "R...R"
1231
+ ], Math.round(C + s * .5) - 2, 25, { R: "#e5566a" });
1232
+ paint(g, (x, y) => torso(x, y) && y === 29, mix$1(shirt, WHITE, .5));
1233
+ break;
1234
+ case "kimono":
1235
+ paint(g, (x, y) => torso(x, y) && y <= 28 && Math.abs(Math.abs(dx(x)) - (y - 23.5)) <= .6, WHITE);
1236
+ paint(g, (x, y) => torso(x, y) && y >= 28, "#e2565f");
1237
+ paint(g, (x, y) => torso(x, y) && y === 28 && x % 3 === 0, GOLD);
1238
+ paint(g, (x, y) => torso(x, y) && y <= 27 && (x * 3 + y * 2) % 7 === 0, mix$1(shirt, WHITE, .45));
1239
+ break;
1240
+ case "cardigan":
1241
+ paint(g, v(5, 3.5), light);
1242
+ paint(g, (x, y) => torso(x, y) && Math.abs(Math.abs(dx(x)) - 2) <= .5 && y >= 25, dark);
1243
+ sprite(g, [
1244
+ "W",
1245
+ ".",
1246
+ "W",
1247
+ ".",
1248
+ "W"
1249
+ ], Math.round(C + s * .5) - 3, 25, { W: WHITE });
1250
+ paint(g, (x, y) => torso(x, y) && y === 29, dark);
1251
+ break;
1252
+ case "maid":
1253
+ paint(g, (x, y) => torso(x, y), "#2f2a3a");
1254
+ paint(g, (x, y) => torso(x, y) && y >= 25 && Math.abs(dx(x)) <= 4 - (y - 25) * .2, WHITE);
1255
+ paint(g, (x, y) => y === 24 && Math.abs(dx(x)) <= 4.5, WHITE);
1256
+ sprite(g, ["RR.RR", ".RRR."], Math.round(C + s * .5) - 2, 24, { R: shirt });
1257
+ break;
1258
+ case "armor": {
1259
+ const metal = "#9aa3ad";
1260
+ paint(g, torso, metal);
1261
+ paint(g, (x, y) => torso(x, y) && y >= 26 && Math.abs(dx(x)) <= 2.5, shirt);
1262
+ paint(g, (x, y) => torso(x, y) && y <= 26 && Math.abs(dx(x)) >= half(y) - 3.5, mix$1(metal, WHITE, .35));
1263
+ paint(g, (x, y) => torso(x, y) && y === 27 && Math.abs(dx(x)) >= half(y) - 3.5, shade(metal, .7));
1264
+ paint(g, (x, y) => y === 24 && Math.abs(dx(x)) <= 3.5, shade(metal, .8));
1265
+ sprite(g, ["G"], Math.round(C + s * .5), 28, { G: GOLD });
1266
+ break;
1267
+ }
1268
+ case "robe":
1269
+ paint(g, v(5, 4), mix$1(shirt, WHITE, .7));
1270
+ paint(g, (x, y) => torso(x, y) && y <= 29 && Math.abs(Math.abs(dx(x)) - (4 - (y - 24) * .8)) <= .5, GOLD);
1271
+ paint(g, (x, y) => torso(x, y) && y >= 28 && Math.abs(dx(x)) >= half(y) - 1.5, dark);
1272
+ break;
1273
+ case "tunic":
1274
+ paint(g, v(2, 2), shade(skin, .85));
1275
+ sprite(g, ["L.L", ".L."], Math.round(C + s * .5) - 1, 25, { L: "#6b4a2b" });
1276
+ paint(g, (x, y) => torso(x, y) && y === 29, "#6b4a2b");
1277
+ sprite(g, ["G"], Math.round(C + s * .5), 29, { G: GOLD });
1278
+ break;
1279
+ case "cloak": {
1280
+ const cloak = mix$1(shirt, "#2f3a4a", .55);
1281
+ paint(g, (x, y) => torso(x, y) && Math.abs(dx(x)) >= 2.5, cloak);
1282
+ paint(g, (x, y) => torso(x, y) && y === 24, shade(cloak, .8));
1283
+ sprite(g, ["G...G"], Math.round(C + s * .5) - 2, 25, { G: GOLD });
1284
+ break;
1285
+ }
1286
+ case "jacket":
1287
+ paint(g, v(6, 4.5), WHITE);
1288
+ paint(g, (x, y) => torso(x, y) && Math.abs(Math.abs(dx(x)) - (4.5 - (y - 24) * .6)) <= .6, dark);
1289
+ paint(g, (x, y) => torso(x, y) && y >= 27 && Math.abs(dx(x)) <= .5, "#9aa3ad");
1290
+ paint(g, (x, y) => torso(x, y) && y === 28 && Math.abs(dx(x)) >= half(y) - 2.5, mix$1(shirt, WHITE, .6));
1291
+ break;
1292
+ }
1293
+ const neck = shade(skin, .85);
1294
+ for (let y = 24; y < N; y++) for (let x = 0; x < N; x++) {
1295
+ const cell = g[y][x];
1296
+ if (!cell || cell === neck || !torso(x, y)) continue;
1297
+ const d = dx(x);
1298
+ const edge = half(y) - Math.abs(d);
1299
+ if (y === 24 && Math.abs(d) <= 4.5) g[y][x] = shade(cell, .78);
1300
+ else if (y >= 26 && edge >= 1.6 && edge < 2.6) g[y][x] = shade(cell, .82);
1301
+ else if (d > 0 && edge < 1.6) g[y][x] = shade(cell, .84);
1302
+ else if (d < 0 && edge < 1.6 && y <= 26) g[y][x] = mix$1(cell, WHITE, .28);
1303
+ else if (d < -2 && y <= 25) g[y][x] = mix$1(cell, WHITE, .14);
1304
+ }
1305
+ }
1306
+ function accessory(recipe, g, s, face) {
1307
+ const shirt = recipe.shirtColor;
1308
+ const cap = ellipse(C + s, 14, 11.6, 11.2);
1309
+ switch (recipe.accessory) {
1310
+ case "none": return;
1311
+ case "beanie":
1312
+ paint(g, (x, y) => cap(x, y) && y <= 8, shirt);
1313
+ paint(g, (x, y) => cap(x, y) && (y === 8 || y === 9), shade(shirt, .78));
1314
+ paint(g, ellipse(C + s, 1.6, 2.2, 1.8), WHITE);
1315
+ return;
1316
+ case "cap":
1317
+ paint(g, (x, y) => cap(x, y) && y <= 8, shirt);
1318
+ paint(g, (x, y) => y === 9 && x + .5 >= C + s - 10 && x + .5 <= C + s + 13, shade(shirt, .72));
1319
+ return;
1320
+ case "headphones": {
1321
+ const band = (x, y) => ellipse(C + s, 14, 12, 11.6)(x, y) && !ellipse(C + s, 14, 10.8, 10.4)(x, y) && y <= 13;
1322
+ paint(g, band, "#5d5b66");
1323
+ paint(g, (x, y) => y >= 12 && y <= 19 && (Math.abs(x + .5 - (C + s - 11)) <= 1.6 || Math.abs(x + .5 - (C + s + 11)) <= 1.6), "#e25d6a");
1324
+ return;
1325
+ }
1326
+ case "flower":
1327
+ sprite(g, [
1328
+ ".P.P.",
1329
+ "PPYPP",
1330
+ ".PPP."
1331
+ ], Math.round(C + s) + 6, 3, {
1332
+ P: "#f59fba",
1333
+ Y: GOLD
1334
+ });
1335
+ return;
1336
+ case "bow":
1337
+ sprite(g, [
1338
+ "RR.RR",
1339
+ "RRrRR",
1340
+ "RR.RR"
1341
+ ], Math.round(C + s) + 6, 3, {
1342
+ R: "#e5566a",
1343
+ r: "#b83a50"
1344
+ });
1345
+ return;
1346
+ case "earring": {
1347
+ const xs = [...Array(N).keys()].filter((x) => face(x, 19));
1348
+ if (xs.length) {
1349
+ sprite(g, ["G", "G"], xs[0] - 1, 20, { G: GOLD });
1350
+ sprite(g, ["G", "G"], xs.at(-1) + 1, 20, { G: GOLD });
1351
+ }
1352
+ return;
1353
+ }
1354
+ case "crown":
1355
+ sprite(g, [
1356
+ "G.G.G.G",
1357
+ "GGGGGGG",
1358
+ "GrGbGrG"
1359
+ ], Math.round(C + s) - 3, 0, {
1360
+ G: GOLD,
1361
+ r: "#e5566a",
1362
+ b: "#5a9be0"
1363
+ });
1364
+ return;
1365
+ case "halo":
1366
+ paint(g, (x, y) => ellipse(C + s, 1.6, 6.5, 1.8)(x, y) && !ellipse(C + s, 1.6, 4.4, .8)(x, y), GOLD);
1367
+ return;
1368
+ case "catears": {
1369
+ const ear = [
1370
+ "X...",
1371
+ "XX..",
1372
+ "XPX.",
1373
+ "XXXX"
1374
+ ];
1375
+ sprite(g, ear, Math.round(C + s) - 9, 2, {
1376
+ X: recipe.hairColor,
1377
+ P: "#f4a3b5"
1378
+ });
1379
+ sprite(g, ear, Math.round(C + s) + 5, 2, {
1380
+ X: recipe.hairColor,
1381
+ P: "#f4a3b5"
1382
+ }, true);
1383
+ return;
1384
+ }
1385
+ case "hairclip":
1386
+ sprite(g, ["CC.CC", ".CCC."], Math.round(C + s) + 3, 9, { C: "#e5566a" });
1387
+ return;
1388
+ case "horns": {
1389
+ const horn = [
1390
+ ".H",
1391
+ "HH",
1392
+ "Hh"
1393
+ ];
1394
+ sprite(g, horn, Math.round(C + s) - 8, 1, {
1395
+ H: "#5b3a6e",
1396
+ h: "#7d5694"
1397
+ });
1398
+ sprite(g, horn, Math.round(C + s) + 6, 1, {
1399
+ H: "#5b3a6e",
1400
+ h: "#7d5694"
1401
+ }, true);
1402
+ return;
1403
+ }
1404
+ case "helmet": {
1405
+ const metal = "#9aa3ad";
1406
+ paint(g, (x, y) => cap(x, y) && y <= 11, metal);
1407
+ paint(g, (x, y) => cap(x, y) && y === 11, shade(metal, .75));
1408
+ paint(g, (x, y) => cap(x, y) && y >= 12 && y <= 19 && Math.abs(x + .5 - C - s) >= 7.5, metal);
1409
+ paint(g, (x, y) => Math.abs(x + .5 - C - s) <= .5 && y >= 11 && y <= 16, shade(metal, .7));
1410
+ paint(g, (x, y) => cap(x, y) && y >= 2 && y <= 5 && x + .5 < C + s - 2, mix$1(metal, WHITE, .4));
1411
+ for (let x = Math.round(C + s) - 9; x <= Math.round(C + s) + 9; x += 3) sprite(g, ["G"], x, 10, { G: GOLD });
1412
+ return;
1413
+ }
1414
+ case "hood": {
1415
+ const outer = ellipse(C + s, 15, 12.6, 12.4);
1416
+ paint(g, (x, y) => outer(x, y) && y <= 25 && !(face(x, y) && y >= 10), shirt);
1417
+ paint(g, (x, y) => outer(x, y) && !face(x, y) && y >= 9 && (face(x + 1, y) || face(x - 1, y) || face(x, y + 1)), shade(shirt, .7));
1418
+ return;
1419
+ }
1420
+ case "beret":
1421
+ paint(g, (x, y) => ellipse(C + s + 1.5, 4.6, 10.5, 3.6)(x, y), shirt);
1422
+ paint(g, (x, y) => ellipse(C + s + 1.5, 4.6, 10.5, 3.6)(x, y) && y >= 6, shade(shirt, .78));
1423
+ paint(g, (x, y) => ellipse(C + s + 1.5, 4.6, 10.5, 3.6)(x, y) && y <= 2 && x + .5 < C + s, mix$1(shirt, WHITE, .3));
1424
+ sprite(g, ["S"], Math.round(C + s) + 1, 0, { S: shade(shirt, .6) });
1425
+ return;
1426
+ case "ribbon":
1427
+ sprite(g, [
1428
+ "RRR.RRR",
1429
+ "RrRRRrR",
1430
+ "RRR.RRR",
1431
+ ".R...R."
1432
+ ], Math.round(C + s) - 3, 0, {
1433
+ R: "#e5566a",
1434
+ r: "#f49aa8"
1435
+ });
1436
+ return;
1437
+ case "headband":
1438
+ paint(g, (x, y) => ellipse(C + s, 14, 11.2, 10.8)(x, y) && !ellipse(C + s, 14.2, 10.2, 9.8)(x, y) && y <= 8, shirt);
1439
+ paint(g, (x, y) => ellipse(C + s, 14, 11.2, 10.8)(x, y) && !ellipse(C + s, 14.2, 10.2, 9.8)(x, y) && y <= 5, mix$1(shirt, WHITE, .35));
1440
+ return;
1441
+ case "bunnyears": {
1442
+ const ear = [
1443
+ "WW",
1444
+ "WP",
1445
+ "WP",
1446
+ "WP",
1447
+ "WW"
1448
+ ];
1449
+ sprite(g, ear, Math.round(C + s) - 6, 0, {
1450
+ W: "#f4f1ec",
1451
+ P: "#f4a3b5"
1452
+ });
1453
+ sprite(g, ear, Math.round(C + s) + 4, 0, {
1454
+ W: "#f4f1ec",
1455
+ P: "#f4a3b5"
1456
+ }, true);
1457
+ return;
1458
+ }
1459
+ case "horseears": {
1460
+ const ear = [
1461
+ ".X.",
1462
+ "XPX",
1463
+ "XPX",
1464
+ "XXX"
1465
+ ];
1466
+ sprite(g, ear, Math.round(C + s) - 9, 1, {
1467
+ X: shade(recipe.hairColor, .9),
1468
+ P: "#f4a3b5"
1469
+ });
1470
+ sprite(g, ear, Math.round(C + s) + 6, 1, {
1471
+ X: shade(recipe.hairColor, .9),
1472
+ P: "#f4a3b5"
1473
+ });
1474
+ sprite(g, ["BB", "BB"], Math.round(C + s) + 3, 7, { B: "#5a9be0" });
1475
+ return;
1476
+ }
1477
+ case "flowercrown":
1478
+ for (const [fx, color] of [
1479
+ [-8, "#f59fba"],
1480
+ [-4, GOLD],
1481
+ [0, "#f4f1ec"],
1482
+ [4, "#f59fba"],
1483
+ [8, GOLD]
1484
+ ]) sprite(g, [
1485
+ ".F.",
1486
+ "FCF",
1487
+ ".F."
1488
+ ], Math.round(C + s) + fx - 1, 4 + Math.round(Math.abs(fx) / 4), {
1489
+ F: color,
1490
+ C: color === GOLD ? "#e2565f" : GOLD
1491
+ });
1492
+ paint(g, (x, y) => y === 6 + Math.round(Math.abs(x + .5 - C - s) / 4) && Math.abs(x + .5 - C - s) <= 9 && x % 2 === 1, "#5aa36b");
1493
+ return;
1494
+ case "witch":
1495
+ paint(g, (x, y) => y >= 6 && y <= 7 && Math.abs(x + .5 - C - s) <= 13, "#3a2f52");
1496
+ paint(g, (x, y) => y <= 6 && Math.abs(x + .5 - C - s - (6 - y) * .5) <= 1 + y * .9, "#4b3d6b");
1497
+ paint(g, (x, y) => y === 5 && Math.abs(x + .5 - C - s - .5) <= 5.2, "#efb93f");
1498
+ return;
1499
+ case "pins":
1500
+ sprite(g, [
1501
+ "Y.Y",
1502
+ ".Y.",
1503
+ "Y.Y",
1504
+ "...",
1505
+ "B.B",
1506
+ ".B.",
1507
+ "B.B"
1508
+ ], Math.round(C + s) + 6, 7, {
1509
+ Y: GOLD,
1510
+ B: "#5a9be0"
1511
+ });
1512
+ return;
1513
+ }
1514
+ }
1515
+ const POINTED_EARS = {
1516
+ goblin: {
1517
+ rows: [
1518
+ "E...",
1519
+ "EE..",
1520
+ ".EiE",
1521
+ "..Ee"
1522
+ ],
1523
+ rise: 2
1181
1524
  },
1182
- ahoge: {
1183
- bangs: "ahoge",
1184
- sideHair: "bob",
1185
- backHair: "bob"
1525
+ elf: {
1526
+ rows: [
1527
+ "E....",
1528
+ "EE...",
1529
+ ".EE..",
1530
+ "..EiE",
1531
+ "...Ee"
1532
+ ],
1533
+ rise: 3
1186
1534
  },
1187
- none: {
1188
- bangs: "none",
1189
- sideHair: "none",
1190
- backHair: "none"
1535
+ orc: {
1536
+ rows: [
1537
+ "E..",
1538
+ "EE.",
1539
+ ".Ee"
1540
+ ],
1541
+ rise: 1
1191
1542
  }
1192
1543
  };
1193
- const AVATAR_RANGES = {
1194
- spacing: [-1, 1],
1195
- height: [-1, 1],
1196
- hairLength: [-2, 2]
1197
- };
1198
- const DETAIL_KEYS = [...Object.keys(AVATAR_HAIR_PARTS), ...Object.keys(AVATAR_RANGES)];
1199
- const AVATAR_COLORS = [
1200
- "skinColor",
1201
- "hairColor",
1202
- "eyeColor",
1203
- "shirtColor"
1204
- ];
1205
- const DEFAULT_RECIPE = {
1206
- schemaVersion: 1,
1207
- family: "illustrated",
1208
- assetVersion: 1,
1209
- rigVersion: 1,
1210
- head: "round",
1211
- pose: "front",
1212
- hair: "crop",
1213
- eyes: "round",
1214
- brows: "soft",
1215
- nose: "button",
1216
- mouth: "smile",
1217
- cheeks: "blush",
1218
- glasses: "none",
1219
- outfit: "tee",
1220
- backdrop: "sparkles",
1221
- accessory: "none",
1222
- skinColor: "#f2c9a8",
1223
- hairColor: "#5a3a2a",
1224
- eyeColor: "#3f7fbf",
1225
- shirtColor: "#5b8bd6"
1226
- };
1227
- const PART_KEYS = Object.keys(AVATAR_PARTS);
1228
- function isPixelAvatarRecipe(value) {
1229
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1230
- const r = value;
1231
- const detailed = DETAIL_KEYS.filter((key) => Object.hasOwn(r, key)).length;
1232
- if (detailed !== 0 && detailed !== DETAIL_KEYS.length) return false;
1233
- if (Object.keys(r).length !== Object.keys(DEFAULT_RECIPE).length + detailed) return false;
1234
- return (detailed === 0 || Object.keys(AVATAR_HAIR_PARTS).every((key) => typeof r[key] === "string" && AVATAR_HAIR_PARTS[key].includes(r[key])) && Object.keys(AVATAR_RANGES).every((key) => {
1235
- const v = r[key];
1236
- const [min, max] = AVATAR_RANGES[key];
1237
- return typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
1238
- })) && r["schemaVersion"] === 1 && r["family"] === "illustrated" && r["assetVersion"] === 1 && r["rigVersion"] === 1 && PART_KEYS.every((key) => typeof r[key] === "string" && AVATAR_PARTS[key].includes(r[key])) && AVATAR_COLORS.every((key) => typeof r[key] === "string" && /^#[\da-f]{6}$/iu.test(r[key]));
1544
+ const leafShape = (cx, cy, long, wide, angle) => (x, y) => {
1545
+ const px = x + .5 - cx;
1546
+ const py = y + .5 - cy;
1547
+ const u = px * Math.cos(angle) + py * Math.sin(angle);
1548
+ const v = -px * Math.sin(angle) + py * Math.cos(angle);
1549
+ return (u / long) ** 2 + (v / wide) ** 2 <= 1;
1550
+ };
1551
+ function petals(style, s) {
1552
+ const cx = C + s;
1553
+ const cy = 16.6;
1554
+ const ring = (n, radius, long, wide, phase = 0) => (x, y) => Array.from({ length: n }, (_, k) => phase + 2 * Math.PI * k / n).some((a) => leafShape(cx + radius * Math.cos(a), cy + radius * Math.sin(a), long, wide, a)(x, y));
1555
+ switch (style) {
1556
+ case "daisy": return ring(14, 10.4, 3.4, 1.7, Math.PI / 14);
1557
+ case "sunflower": return (x, y) => ring(16, 11.2, 2.6, 1.5)(x, y) || ellipse(cx, cy, 10.6, 10.2)(x, y);
1558
+ case "tulip": return (x, y) => y <= cy + 6 && (ellipse(cx - 6.5, cy - 3, 4.5, 7.5)(x, y) || ellipse(cx + 6.5, cy - 3, 4.5, 7.5)(x, y) || ellipse(cx, cy - 6, 5, 8)(x, y));
1559
+ case "trumpet": return (x, y) => ellipse(cx, cy - .5, 11.6, 10.4)(x, y) || y <= cy - 6 && ellipse(cx, cy - 3, 13, 8.6)(x, y);
1560
+ case "sakura": return (x, y) => ring(5, 8.6, 4.6, 3.8, -Math.PI / 2)(x, y) && !ring(5, 13.4, 1.4, 1.4, -Math.PI / 2)(x, y);
1561
+ }
1239
1562
  }
1240
- function canonicalRecipe(recipe) {
1241
- const canonical = {
1242
- schemaVersion: 1,
1243
- family: "illustrated",
1244
- assetVersion: 1,
1245
- rigVersion: 1
1246
- };
1247
- for (const key of PART_KEYS) canonical[key] = recipe[key];
1248
- for (const key of AVATAR_COLORS) canonical[key] = recipe[key].toLowerCase();
1249
- if (recipe.bangs !== void 0) for (const key of DETAIL_KEYS) canonical[key] = recipe[key];
1250
- return canonical;
1563
+ function flowerBase(recipe, g, s) {
1564
+ const leaf = "#5f9e4a";
1565
+ const stem = C + s * .5;
1566
+ paint(g, (x, y) => y >= 22 && Math.abs(x + .5 - stem) <= 1, shade(leaf, .82));
1567
+ if (recipe.flowerBase === "pot") {
1568
+ const pot = recipe.shirtColor;
1569
+ const body = (x, y) => y >= 27 && Math.abs(x + .5 - stem) <= 6.5 - (y - 27) * .45;
1570
+ paint(g, leafShape(stem - 3, 25, 2.8, 1.2, -.5), leaf);
1571
+ paint(g, leafShape(stem + 3, 24.5, 2.8, 1.2, .5), shade(leaf, .9));
1572
+ paint(g, body, pot);
1573
+ paint(g, (x, y) => body(x, y) && y === 27, mix$1(pot, WHITE, .3));
1574
+ paint(g, (x, y) => body(x, y) && y >= 28 && x + .5 > stem + 3, shade(pot, .8));
1575
+ return;
1576
+ }
1577
+ const left = leafShape(stem - 4.5, 27, 4.4, 1.9, -.55);
1578
+ const right = leafShape(stem + 4.5, 25.5, 4.4, 1.9, .55);
1579
+ paint(g, left, leaf);
1580
+ paint(g, right, shade(leaf, .92));
1581
+ paint(g, leafShape(stem - 4.5, 27, 3.6, .5, -.55), shade(leaf, .75));
1582
+ paint(g, leafShape(stem + 4.5, 25.5, 3.6, .5, .55), shade(leaf, .7));
1251
1583
  }
1252
- function detailedRecipe(recipe) {
1253
- if (recipe.bangs !== void 0) return recipe;
1254
- const pick = (part) => AVATAR_HAIR_PARTS[part].includes(recipe.hair) ? recipe.hair : HAIR_SPLIT[recipe.hair][part];
1255
- return {
1256
- ...recipe,
1257
- bangs: pick("bangs"),
1258
- sideHair: pick("sideHair"),
1259
- backHair: pick("backHair"),
1260
- spacing: 0,
1261
- height: 0,
1262
- hairLength: 0
1584
+ function pixelTileColor(hair) {
1585
+ const [r, g, b] = channels$1(hair).map((v) => v / 255);
1586
+ const max = Math.max(r, g, b);
1587
+ const min = Math.min(r, g, b);
1588
+ const lightness = (max + min) / 2;
1589
+ const chroma = max - min;
1590
+ const saturation = chroma === 0 ? 0 : chroma / (1 - Math.abs(2 * lightness - 1));
1591
+ if (saturation < .18 || lightness > .86 || lightness < .16) return "#ece8e1";
1592
+ const hue = max === r ? ((g - b) / chroma + 6) % 6 : max === g ? (b - r) / chroma + 2 : (r - g) / chroma + 4;
1593
+ const s = Math.min(.55, Math.max(.35, saturation * .6));
1594
+ const l = .88;
1595
+ const f = (n) => {
1596
+ const k = (n + hue * 2) % 12;
1597
+ return l - s * Math.min(l, 1 - l) * Math.max(-1, Math.min(k - 3, 9 - k, 1));
1263
1598
  };
1599
+ return hex([
1600
+ f(0),
1601
+ f(8),
1602
+ f(4)
1603
+ ].map((v) => v * 255));
1264
1604
  }
1265
- const AVATAR_SWATCHES = {
1266
- skinColor: [
1267
- "#ffe3cf",
1268
- "#f2c9a8",
1269
- "#e0a87e",
1270
- "#c68863",
1271
- "#9a6142",
1272
- "#6e4129",
1273
- "#4a2c1c",
1274
- "#f4f1ec"
1275
- ],
1276
- hairColor: [
1277
- "#1d1b22",
1278
- "#5a3a2a",
1279
- "#8a5a36",
1280
- "#e2b04a",
1281
- "#c4452f",
1282
- "#d9475a",
1283
- "#f06292",
1284
- "#3fc1b8",
1285
- "#5a7be0",
1286
- "#9aa3ad",
1287
- "#f4f1ec"
1288
- ],
1289
- eyeColor: [
1290
- "#3f7fbf",
1291
- "#5a3a2a",
1292
- "#2f9e8f",
1293
- "#4c8a3c",
1294
- "#8a5ad0",
1295
- "#d0533f",
1296
- "#d9a13a",
1297
- "#2a2230"
1298
- ],
1299
- shirtColor: [
1300
- "#5b8bd6",
1301
- "#e07a5f",
1302
- "#3d9970",
1303
- "#7a5cc7",
1304
- "#f2c14e",
1305
- "#2f3a4a",
1306
- "#e2565f",
1307
- "#9ad0c2"
1308
- ]
1309
- };
1310
- const SEEDED_CHOICES = {
1311
- head: [
1312
- "round",
1313
- "oval",
1314
- "square",
1315
- "long",
1316
- "heart",
1317
- "vchin",
1318
- "chubby",
1319
- "diamond"
1320
- ],
1321
- pose: ["front"],
1322
- hair: AVATAR_PARTS.hair.filter((hair) => hair !== "none"),
1323
- eyes: [
1324
- "round",
1325
- "round",
1326
- "sparkle",
1327
- "lashes",
1328
- "dot",
1329
- "happy",
1330
- "sharp"
1331
- ],
1332
- brows: [
1333
- "soft",
1334
- "soft",
1335
- "thick",
1336
- "raised"
1337
- ],
1338
- nose: [
1339
- "button",
1340
- "dot",
1341
- "none"
1342
- ],
1343
- mouth: [
1344
- "smile",
1345
- "smile",
1346
- "grin",
1347
- "open",
1348
- "cat"
1349
- ],
1350
- cheeks: [
1351
- "blush",
1352
- "blush",
1353
- "freckles",
1354
- "none"
1355
- ],
1356
- glasses: [
1357
- "none",
1358
- "none",
1359
- "none",
1360
- "none",
1361
- "round",
1362
- "square"
1363
- ],
1364
- accessory: [
1365
- "none",
1366
- "none",
1367
- "none",
1368
- "flower",
1369
- "bow",
1370
- "hairclip",
1371
- "catears",
1372
- "headphones",
1373
- "beanie",
1374
- "cap"
1375
- ],
1376
- outfit: AVATAR_PARTS.outfit,
1377
- backdrop: [
1378
- "sparkles",
1379
- "hearts",
1380
- "stars",
1381
- "dots"
1382
- ]
1383
- };
1384
- /**
1385
- * Returns a name-seeded recipe factory. Different namespaces give different faces for the same
1386
- * name; the factory takes the name only, so it is safe to pass straight to `Array.map`.
1387
- */
1388
- function createSeededRecipe(namespace) {
1389
- return (seed) => {
1390
- const random = seededRandom(`${namespace}:${seed.trim().toLowerCase()}`);
1391
- const pick = (values) => values[Math.floor(random() * values.length)];
1392
- const recipe = {
1393
- schemaVersion: 1,
1394
- family: "illustrated",
1395
- assetVersion: 1,
1396
- rigVersion: 1
1605
+ function pixelFigure(recipe, yawDeg, options = {}) {
1606
+ const yaw = yawDeg * Math.PI / 180;
1607
+ const sinY = Math.sin(yaw);
1608
+ const s = Math.round(sinY * 3);
1609
+ const fs = Math.round(sinY * 10);
1610
+ const b = -Math.round(sinY * 3.5);
1611
+ const turned = Math.abs(sinY) >= .3;
1612
+ const d = Math.sign(sinY);
1613
+ const skin = recipe.skinColor;
1614
+ const hairColor = recipe.hairColor;
1615
+ const front = FACE[recipe.head](s * .5);
1616
+ const face = turned ? (x, y) => {
1617
+ const dx = x + .5 - C;
1618
+ const tuck = y >= 17 && d * dx > 4 ? Math.min(2, (y - 16) * .35) : 0;
1619
+ return front(x - Math.round(d * (1 + Math.max(0, y - 20) * .4) - d * tuck), y);
1620
+ } : front;
1621
+ const flower = recipe.species === "flower";
1622
+ const covered = recipe.accessory === "helmet" || recipe.accessory === "hood";
1623
+ const bare = () => false;
1624
+ const petalRing = flower ? petals(recipe.petals ?? "trumpet", s) : bare;
1625
+ const masks = flower ? {
1626
+ back: petalRing,
1627
+ front: bare,
1628
+ sides: [(x, y) => petalRing(x, y) && x + .5 < C + s, (x, y) => petalRing(x, y) && x + .5 >= C + s]
1629
+ } : covered ? {
1630
+ back: bare,
1631
+ front: bare
1632
+ } : recipe.bangs === void 0 ? hair(recipe, s, b) : detailedHair(recipe, s, b);
1633
+ const body = blank();
1634
+ if (flower) flowerBase(recipe, body, s);
1635
+ else outfit(recipe, body, s);
1636
+ const back = blank();
1637
+ if (flower) {
1638
+ paint(back, petalRing, hairColor);
1639
+ paint(back, (x, y) => petalRing(x, y) && (x + .5 - C - s) ** 2 + (y + .5 - 16.6) ** 2 < 90, shade(hairColor, .84));
1640
+ paint(back, (x, y) => petalRing(x, y) && x + .5 < C + s - 3 && y < 10, mix$1(hairColor, WHITE, .3));
1641
+ } else paint(back, masks.back, shade(hairColor, .82));
1642
+ if (masks.bands) paint(back, (x, y) => masks.back(x, y) && (y + Math.floor(x / 2)) % 4 === 0, shade(hairColor, .68));
1643
+ const head = blank();
1644
+ paint(head, face, skin);
1645
+ const h = recipe.height ?? 0;
1646
+ if (recipe.beard && !flower) {
1647
+ const centre = C + fs * .6;
1648
+ const mouthX = Math.round(C - .5 + fs * .6);
1649
+ const chin = (x, y) => face(x, y) || face(x, y - 2);
1650
+ const open = (x, y) => y >= 20 + h && y <= 22 + h && Math.abs(x - mouthX) <= 3;
1651
+ const full = (x, y) => chin(x, y) && (y >= 22 + h || Math.abs(x + .5 - centre) >= 4 && y >= 17 + h);
1652
+ const shape = recipe.beard === "short" ? (x, y) => chin(x, y) && y >= 22 + h : recipe.beard === "full" ? full : (x, y) => full(x, y) || Math.abs(x + .5 - centre) <= 1 && y >= 24 && y <= 30;
1653
+ paint(head, (x, y) => shape(x, y) && !open(x, y), shade(hairColor, .9));
1654
+ paint(head, (x, y) => shape(x, y) && !open(x, y) && (x + y * 2) % 5 === 0, shade(hairColor, .72));
1655
+ }
1656
+ const earY = 15;
1657
+ const edge = (side) => {
1658
+ const xs = [...Array(N).keys()].filter((x) => face(x, earY));
1659
+ return side < 0 ? xs[0] - 1 : xs.at(-1) + 1;
1660
+ };
1661
+ const pointed = recipe.species ? POINTED_EARS[recipe.species] : void 0;
1662
+ if (flower) {} else if (pointed) {
1663
+ const pal = {
1664
+ E: skin,
1665
+ e: shade(skin, .86),
1666
+ i: shade(skin, .74)
1667
+ };
1668
+ const width = pointed.rows[0].length;
1669
+ paint(head, masks.front, hairColor);
1670
+ for (const ex of turned ? [-d] : [-1, 1]) sprite(head, pointed.rows, ex < 0 ? edge(ex) - (width - 1) : edge(ex), earY - pointed.rise, pal, ex > 0);
1671
+ } else if (!turned) for (const ex of [-1, 1]) sprite(head, [
1672
+ "E",
1673
+ "E",
1674
+ "e"
1675
+ ], edge(ex), earY, {
1676
+ E: skin,
1677
+ e: shade(skin, .86)
1678
+ });
1679
+ else sprite(head, [
1680
+ "EE",
1681
+ "Ee",
1682
+ "Ee",
1683
+ "E."
1684
+ ], d > 0 ? edge(-d) - 1 : edge(-d), earY, {
1685
+ E: skin,
1686
+ e: shade(skin, .8)
1687
+ });
1688
+ if (!pointed && !flower) paint(head, masks.front, hairColor);
1689
+ for (const [x, y] of masks.ties ?? []) sprite(back, ["T"], Math.round(x), y, { T: shade(recipe.shirtColor, .8) });
1690
+ if (!flower) accessory(recipe, head, s, face);
1691
+ const shadeHair = (g) => {
1692
+ const out = g.map((r) => [...r]);
1693
+ const cells = g.flatMap((row, y) => row.flatMap((c, x) => c === hairColor ? [[x, y]] : []));
1694
+ const xs = cells.map(([x]) => x);
1695
+ const ys = cells.map(([, y]) => y);
1696
+ const left = Math.min(...xs);
1697
+ const right = Math.max(...xs);
1698
+ const top = Math.min(...ys);
1699
+ const mid = (left + right) / 2;
1700
+ const span = Math.max(1, (right - left) / 2);
1701
+ const light = mix$1(hairColor, WHITE, .2);
1702
+ const dark = shade(hairColor, .8);
1703
+ const middle = shade(hairColor, .9);
1704
+ for (const [x, y] of cells) {
1705
+ const side = (x + .5 - mid) / span + (y - top) * .025;
1706
+ if (side > .72) out[y][x] = dark;
1707
+ else if (side > .42) out[y][x] = middle;
1708
+ else if (side < -.2 && y <= top + 4) out[y][x] = light;
1709
+ }
1710
+ for (const [x, y] of cells) if ((g[y][x - 1] === skin || g[y][x + 1] === skin) && y >= 14) out[y][x] = shade(hairColor, .74);
1711
+ else if (Math.abs(x + .5 - C - s) >= 7.5 && y >= 14 && (x + 40 - s) % 3 === 0) out[y][x] = shade(out[y][x], .86);
1712
+ for (let y = 1; y < N; y++) for (let x = 0; x < N; x++) {
1713
+ if (g[y][x] !== skin) continue;
1714
+ if (g[y - 1][x] === hairColor) out[y][x] = mix$1(shade(skin, .84), BLUSH, .12);
1715
+ else if (x + 1 < N && g[y][x + 1] !== skin && g[y][x + 1] !== void 0 && x > C + s) out[y][x] = shade(skin, .9);
1716
+ else if (x + 1 < N && g[y][x + 1] === void 0 && x > C + s) out[y][x] = shade(skin, .9);
1717
+ }
1718
+ for (let y = 0; y < N - 1; y++) for (let x = 0; x < N; x++) if (g[y][x] === hairColor && g[y + 1][x] === skin) out[y][x] = shade(hairColor, .62);
1719
+ for (let x = 0; x < N; x++) for (let y = 3; y < N; y++) if (g[y][x] === hairColor && g[y + 1]?.[x] === skin && (x + 40 - s) % 4 === 0) {
1720
+ for (let k = 1; k <= 3; k++) if (g[y - k]?.[x] === hairColor) out[y - k][x] = shade(hairColor, .72);
1721
+ }
1722
+ const ring = mix$1(hairColor, WHITE, .45);
1723
+ for (let x = Math.round(C + s) - 7; x <= Math.round(C + s) + 3; x++) {
1724
+ const first = g.findIndex((row) => row[x] === hairColor);
1725
+ if (first < 0) continue;
1726
+ const y = first + 2 + Math.round(Math.abs(x + .5 - (C + s - 2)) / 4);
1727
+ if (g[y]?.[x] === hairColor && x % 3 !== 0) out[y][x] = ring;
1728
+ }
1729
+ return out;
1730
+ };
1731
+ const figure = blank();
1732
+ for (const layer of [
1733
+ back,
1734
+ body,
1735
+ shadeHair(head)
1736
+ ]) for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (layer[y][x]) figure[y][x] = layer[y][x];
1737
+ const features = blank();
1738
+ const eyes = blank();
1739
+ const closed = blank();
1740
+ const glasses = blank();
1741
+ const sp = recipe.spacing ?? 0;
1742
+ const eyeTop = 15 + h;
1743
+ const eyeW = (side) => turned && Math.sign(fs) === side ? 3 : 4;
1744
+ const eyeX = (side) => {
1745
+ const w = eyeW(side);
1746
+ return side < 0 ? Math.round(C + fs * .6) - 6 - sp - (w === 3 ? -1 : 0) : Math.round(C + fs * .6) + 2 + sp;
1747
+ };
1748
+ const left = {
1749
+ x: eyeX(-1),
1750
+ w: eyeW(-1)
1751
+ };
1752
+ const right = {
1753
+ x: eyeX(1),
1754
+ w: eyeW(1)
1755
+ };
1756
+ const fit = (rows, w) => rows.map((r) => r.slice(Math.max(0, r.length - w)));
1757
+ const pal = {
1758
+ K: INK$1,
1759
+ W: WHITE,
1760
+ D: shade(recipe.eyeColor, .55),
1761
+ I: recipe.eyeColor,
1762
+ L: mix$1(recipe.eyeColor, WHITE, .45)
1763
+ };
1764
+ if (flower) {
1765
+ const centre = Math.round(C + fs * .6);
1766
+ const bead = {
1767
+ K: "#1d1b22",
1768
+ W: WHITE
1397
1769
  };
1398
- for (const part of Object.keys(AVATAR_PARTS)) recipe[part] = pick(SEEDED_CHOICES[part]);
1399
- for (const color of AVATAR_COLORS) recipe[color] = pick(AVATAR_SWATCHES[color].slice(0, 7));
1400
- return recipe;
1770
+ for (const ex of [centre - 3, centre + 1]) {
1771
+ sprite(eyes, [
1772
+ "KK",
1773
+ "KW",
1774
+ "KK",
1775
+ "KK"
1776
+ ], ex, eyeTop - 1, bead);
1777
+ sprite(closed, [
1778
+ "..",
1779
+ "..",
1780
+ "KK",
1781
+ ".."
1782
+ ], ex, eyeTop - 1, bead);
1783
+ }
1784
+ } else {
1785
+ const style = EYES[recipe.eyes];
1786
+ const extra = style[0].length - 4;
1787
+ sprite(eyes, fit(style, left.w + extra), left.x - extra, eyeTop, pal);
1788
+ sprite(eyes, recipe.eyes === "wink" ? fit(CLOSED, right.w) : fit(style, right.w + extra), right.x, eyeTop, pal, true);
1789
+ sprite(closed, fit(CLOSED, left.w), left.x, eyeTop, pal);
1790
+ sprite(closed, fit(CLOSED, right.w), right.x, eyeTop, pal, true);
1791
+ }
1792
+ const browRows = flower ? [] : BROWS[recipe.brows];
1793
+ if (browRows.length) {
1794
+ sprite(features, fit(browRows, left.w), left.x, eyeTop - 2, { B: shade(hairColor, .55) });
1795
+ sprite(features, fit(browRows, right.w), right.x, eyeTop - 2, { B: shade(hairColor, .55) }, true);
1796
+ }
1797
+ const cx = Math.round(C + fs * (turned ? .85 : .6));
1798
+ if (flower) {} else if (recipe.species === "dwarf") sprite(features, [".N.", "NnN"], cx - 1, 17 + h, {
1799
+ N: shade(skin, .84),
1800
+ n: shade(skin, .72)
1801
+ });
1802
+ else if (recipe.nose === "dot") sprite(features, ["N"], cx, 18 + h, { N: shade(skin, .78) });
1803
+ else if (recipe.nose === "button") sprite(features, ["N"], cx, 18 + h, { N: shade(skin, .88) });
1804
+ else if (recipe.nose === "line") sprite(features, ["N", "N"], cx, 17 + h, { N: shade(skin, .82) });
1805
+ const mx = C - .5 + fs * .6;
1806
+ const withMouth = (mouth) => {
1807
+ const layer = features.map((row) => [...row]);
1808
+ sprite(layer, mouth, Math.round(mx - (mouth[0].length - 1) / 2), 20 + h, {
1809
+ K: MOUTH,
1810
+ M: MOUTH_INSIDE,
1811
+ T: TONGUE,
1812
+ W: WHITE
1813
+ });
1814
+ if (recipe.species === "goblin") for (const tx of [Math.round(mx - 2.5), Math.round(mx + 2.5)]) sprite(layer, ["T"], tx, 21 + h, { T: TUSK });
1815
+ if (recipe.species === "orc") for (const tx of [Math.round(mx - 2.5), Math.round(mx + 2.5)]) sprite(layer, ["T", "T"], tx, 20 + h, { T: TUSK });
1816
+ if (recipe.cheeks === "blush" && !flower) {
1817
+ sprite(layer, ["PP"], left.x - 1, 19 + h, { P: mix$1(BLUSH, skin, .25) });
1818
+ sprite(layer, ["PP"], right.x + right.w - 1, 19 + h, { P: mix$1(BLUSH, skin, .25) });
1819
+ }
1820
+ if (recipe.cheeks === "freckles" && !flower) {
1821
+ sprite(layer, ["F.F"], left.x, 19 + h, { F: shade(skin, .7) });
1822
+ sprite(layer, ["F.F"], right.x + 1, 19 + h, { F: shade(skin, .7) });
1823
+ }
1824
+ return layer;
1825
+ };
1826
+ const savedFeatures = withMouth(MOUTHS[recipe.mouth]);
1827
+ const featureMarkup = options.mouthLayers ? [
1828
+ ["saved", MOUTHS[recipe.mouth]],
1829
+ ["closed", ["KKK"]],
1830
+ ["half-open", ["KKK", "MMM"]],
1831
+ ["open", [
1832
+ ".K.",
1833
+ "KMK",
1834
+ ".K."
1835
+ ]]
1836
+ ].map(([state, rows]) => `<g data-avatar-mouth="${state}" opacity="${state === "saved" ? 1 : 0}">${rects(withMouth(rows))}</g>`).join("") : rects(savedFeatures);
1837
+ const frame = (fx, w, round) => paint(glasses, (x, y) => {
1838
+ const inside = x >= fx && x < fx + w && y >= eyeTop && y <= eyeTop + 3;
1839
+ const box = x >= fx - 1 && x <= fx + w && y >= eyeTop - 1 && y <= eyeTop + 4;
1840
+ const corner = (x === fx - 1 || x === fx + w) && (y === eyeTop - 1 || y === eyeTop + 4);
1841
+ return box && !inside && !(round && corner);
1842
+ }, INK$1);
1843
+ const lenses = flower ? "none" : recipe.glasses;
1844
+ if (lenses === "round" || lenses === "square") {
1845
+ frame(left.x, left.w, lenses === "round");
1846
+ frame(right.x, right.w, lenses === "round");
1847
+ paint(glasses, (x, y) => y === eyeTop + 1 && x > left.x + left.w && x < right.x - 1, INK$1);
1848
+ }
1849
+ if (lenses === "shades") {
1850
+ for (const e of [left, right]) paint(glasses, (x, y) => x >= e.x - 1 && x <= e.x + e.w && y >= eyeTop && y <= eyeTop + 2, INK$1);
1851
+ paint(glasses, (x, y) => y === eyeTop && x > left.x + left.w && x < right.x - 1, INK$1);
1852
+ for (const e of [left, right]) sprite(glasses, ["W"], e.x, eyeTop, { W: "#7d7b88" });
1853
+ }
1854
+ if (lenses === "monocle") {
1855
+ frame(right.x, right.w, true);
1856
+ sprite(glasses, [
1857
+ "G",
1858
+ "G",
1859
+ "G"
1860
+ ], right.x + right.w, eyeTop + 5, { G: GOLD });
1861
+ }
1862
+ const full = clip(outline(figure));
1863
+ AVATAR_PIECE_COLORS.forEach((key, i) => {
1864
+ const color = recipe[key];
1865
+ const mask = masks.sides?.[i];
1866
+ if (color === void 0 || !mask || color.toLowerCase() === hairColor.toLowerCase()) return;
1867
+ const variant = blank();
1868
+ const plain = {
1869
+ ...recipe,
1870
+ hairColor: color
1871
+ };
1872
+ for (const k of AVATAR_PIECE_COLORS) delete plain[k];
1873
+ for (const cell of pixelFigure(plain, yawDeg).cells) variant[cell.y][cell.x] = cell.c;
1874
+ const near = (x, y) => mask(x, y) || mask(x - 1, y) || mask(x + 1, y) || mask(x, y - 1) || mask(x, y + 1);
1875
+ for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (full[y][x] && near(x, y) && !eyes[y][x] && !glasses[y][x]) full[y][x] = variant[y][x];
1876
+ });
1877
+ const bodyCells = blank();
1878
+ const headCells = blank();
1879
+ for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
1880
+ const c = full[y][x];
1881
+ if (!c) continue;
1882
+ if (y >= 22 && !head[y][x] && !back[y][x]) bodyCells[y][x] = c;
1883
+ else headCells[y][x] = c;
1884
+ }
1885
+ const cells = [];
1886
+ for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
1887
+ const c = glasses[y][x] ?? eyes[y][x] ?? savedFeatures[y][x] ?? full[y][x];
1888
+ if (c) cells.push({
1889
+ x,
1890
+ y,
1891
+ c
1892
+ });
1893
+ }
1894
+ return {
1895
+ cells,
1896
+ tile: `<rect width="32" height="32" rx="6" fill="${pixelTileColor(recipe.hairColor)}"/>`,
1897
+ body: `<g class="bh-illustrated-body">${rects(bodyCells)}</g>`,
1898
+ head: [
1899
+ `<g class="bh-illustrated-head">${rects(headCells)}`,
1900
+ `<g class="bh-illustrated-face">${featureMarkup}`,
1901
+ `<g class="bh-illustrated-gaze">${rects(eyes)}</g>`,
1902
+ `<g class="bh-illustrated-blink" opacity="0">${rects(closed)}</g>`,
1903
+ `${rects(glasses)}</g></g>`
1904
+ ].join("")
1401
1905
  };
1402
1906
  }
1403
- /** The same name always gives the same face (BotHarness's namespace). */
1404
- const seededRecipe = createSeededRecipe("botharness-avatar");
1405
- const preset = (parts) => ({
1406
- ...DEFAULT_RECIPE,
1407
- ...parts
1408
- });
1409
- const AVATAR_PRESETS = [
1410
- preset({
1411
- hair: "wavy",
1412
- eyes: "lashes",
1413
- accessory: "beret",
1414
- outfit: "dress",
1415
- skinColor: "#ffe3cf",
1416
- hairColor: "#e2b04a",
1417
- eyeColor: "#3f7fbf",
1418
- shirtColor: "#e2565f",
1419
- backdrop: "stars"
1420
- }),
1421
- preset({
1422
- hair: "braids",
1423
- accessory: "flowercrown",
1424
- outfit: "cardigan",
1425
- skinColor: "#ffe3cf",
1426
- hairColor: "#8a5a36",
1427
- eyeColor: "#4c8a3c",
1428
- shirtColor: "#9ad0c2",
1429
- backdrop: "hearts"
1430
- }),
1431
- preset({
1432
- hair: "twintails",
1433
- eyes: "sparkle",
1434
- accessory: "ribbon",
1435
- outfit: "maid",
1436
- skinColor: "#ffe3cf",
1437
- hairColor: "#3fc1b8",
1438
- eyeColor: "#2f9e8f",
1439
- shirtColor: "#e2565f",
1440
- backdrop: "stars"
1441
- }),
1442
- preset({
1443
- hair: "long",
1444
- accessory: "horseears",
1445
- outfit: "jacket",
1446
- skinColor: "#ffe3cf",
1447
- hairColor: "#8a5a36",
1448
- eyeColor: "#d0533f",
1449
- shirtColor: "#5b8bd6",
1450
- backdrop: "dots"
1451
- }),
1452
- preset({
1453
- hair: "hime",
1454
- eyes: "sleepy",
1455
- accessory: "pins",
1456
- outfit: "kimono",
1457
- skinColor: "#f4f1ec",
1458
- hairColor: "#1d1b22",
1459
- eyeColor: "#d0533f",
1460
- shirtColor: "#7a5cc7",
1461
- backdrop: "none"
1462
- }),
1463
- preset({
1464
- hair: "ahoge",
1465
- accessory: "headband",
1466
- outfit: "sailor",
1467
- skinColor: "#ffe3cf",
1468
- hairColor: "#f06292",
1469
- eyeColor: "#2f9e8f",
1470
- shirtColor: "#f4f1ec",
1471
- backdrop: "hearts"
1472
- }),
1473
- preset({
1474
- hair: "wolf",
1475
- eyes: "sharp",
1476
- outfit: "jacket",
1477
- skinColor: "#e0a87e",
1478
- hairColor: "#9aa3ad",
1479
- eyeColor: "#5a7be0",
1480
- shirtColor: "#2f3a4a",
1481
- backdrop: "stars"
1482
- }),
1483
- preset({
1484
- hair: "wavy",
1485
- eyes: "sparkle",
1486
- accessory: "witch",
1487
- outfit: "dress",
1488
- skinColor: "#ffe3cf",
1489
- hairColor: "#8a5ad0",
1490
- eyeColor: "#d9a13a",
1491
- shirtColor: "#2f3a4a",
1492
- backdrop: "sparkles"
1493
- }),
1494
- preset({
1495
- hair: "bob",
1496
- eyes: "happy",
1497
- accessory: "bunnyears",
1498
- outfit: "cardigan",
1499
- skinColor: "#ffe3cf",
1500
- hairColor: "#f4f1ec",
1501
- eyeColor: "#d9475a",
1502
- shirtColor: "#f2c14e",
1503
- backdrop: "hearts"
1504
- }),
1505
- preset({
1506
- hair: "spiky",
1507
- glasses: "round",
1508
- accessory: "headphones",
1509
- outfit: "hoodie",
1510
- backdrop: "dots"
1511
- }),
1512
- preset({
1513
- hair: "odango",
1514
- eyes: "lashes",
1515
- accessory: "bow",
1516
- outfit: "kimono",
1517
- hairColor: "#e2b04a",
1518
- shirtColor: "#5b8bd6",
1519
- backdrop: "sparkles"
1520
- }),
1521
- preset({
1522
- hair: "crop",
1523
- accessory: "cap",
1524
- outfit: "shirttie",
1525
- skinColor: "#9a6142",
1526
- hairColor: "#1d1b22",
1527
- shirtColor: "#3d9970",
1528
- backdrop: "dots"
1529
- })
1530
- ];
1531
1907
 
1532
1908
  //#endregion
1533
1909
  //#region src/svg.ts
@@ -2051,5 +2427,5 @@ function symbolArtCells(rows, color) {
2051
2427
  }
2052
2428
 
2053
2429
  //#endregion
2054
- export { AVATAR_COLORS, AVATAR_HAIR_PARTS, AVATAR_PARTS, AVATAR_PRESETS, AVATAR_RANGES, AVATAR_SWATCHES, AVATAR_TURNS, DEFAULT_RECIPE, PIXEL_SYMBOLS, canonicalRecipe, createSeededRecipe, detailedRecipe, faceCells, isPixelAvatarRecipe, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, seededRandom, seededRecipe, symbolArtCells };
2430
+ export { AVATAR_COLORS, AVATAR_EXTRA_PARTS, AVATAR_HAIR_PARTS, AVATAR_PARTS, AVATAR_PARTS_V2, AVATAR_PIECE_COLORS, AVATAR_PRESETS, AVATAR_RANGES, AVATAR_SPECIES, AVATAR_SPECIES_SWATCHES, AVATAR_SWATCHES, AVATAR_TURNS, DEFAULT_RECIPE, PIXEL_SYMBOLS, canonicalRecipe, createSeededRecipe, createSeededRecipeV2, detailedRecipe, faceCells, hiddenChoices, isPixelAvatarRecipe, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, seededRandom, seededRecipe, seededRecipeV2, symbolArtCells, withSpecies };
2055
2431
  //# sourceMappingURL=index.js.map