@botharness/pixel-avatar 0.3.0 → 0.5.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,5 +1,191 @@
1
1
  import { escapeAttribute } from "@botharness/pixel-morph";
2
2
 
3
+ //#region src/part.ts
4
+ /**
5
+ * Custom Part slots. A headpiece covers the top of the tile, around the hair: its back layer is
6
+ * drawn behind the hair and its front layer over it. Cells are in tile coordinates, so the
7
+ * Avatar centerline sits between x = 15 and x = 16.
8
+ */
9
+ const PART_SLOTS = { headpiece: {
10
+ width: 32,
11
+ height: 16
12
+ } };
13
+ /** Tone steps on the rig's shade ramp: two darker, the color itself, two lighter. */
14
+ const PART_TONES = [
15
+ -2,
16
+ -1,
17
+ 0,
18
+ 1,
19
+ 2
20
+ ];
21
+ const PART_LAYERS = ["front", "back"];
22
+ /** Most fixed colors one part may use. */
23
+ const MAX_PART_FIXED_COLORS = 16;
24
+ const FIXED = /^#[\da-f]{6}$/iu;
25
+ const isColor = (value) => typeof value === "string" && (AVATAR_COLORS.includes(value) || FIXED.test(value));
26
+ function isPixelCustomPart(value) {
27
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
28
+ const r = value;
29
+ if (Object.keys(r).length !== 3 || !Object.hasOwn(PART_SLOTS, r["slot"])) return false;
30
+ const { width, height } = PART_SLOTS[r["slot"]];
31
+ const fixed = /* @__PURE__ */ new Set();
32
+ for (const name of PART_LAYERS) {
33
+ const cells = r[name];
34
+ if (!Array.isArray(cells) || cells.length > width * height) return false;
35
+ const seen = /* @__PURE__ */ new Set();
36
+ for (const cell of cells) {
37
+ if (!Array.isArray(cell) || cell.length !== 4) return false;
38
+ const [x, y, color, tone] = cell;
39
+ if (!Number.isInteger(x) || !Number.isInteger(y) || x < 0 || x >= width || y < 0 || y >= height || !isColor(color) || !PART_TONES.includes(tone)) return false;
40
+ const key = y * width + x;
41
+ if (seen.has(key)) return false;
42
+ seen.add(key);
43
+ if (color.startsWith("#")) fixed.add(color.toLowerCase());
44
+ }
45
+ }
46
+ return fixed.size <= MAX_PART_FIXED_COLORS;
47
+ }
48
+ const canonicalCells = (cells) => cells.map(([x, y, color, tone]) => [
49
+ x,
50
+ y,
51
+ color.startsWith("#") ? color.toLowerCase() : color,
52
+ tone
53
+ ]).sort((a, b) => a[1] - b[1] || a[0] - b[0]);
54
+ /** The part with cells in row order and fixed colors in lowercase. */
55
+ function canonicalCustomPart(part) {
56
+ return {
57
+ slot: part.slot,
58
+ front: canonicalCells(part.front),
59
+ back: canonicalCells(part.back)
60
+ };
61
+ }
62
+ /** Content identity: a SHA-256 of the canonical slot, cells and color references. */
63
+ function customPartId(part) {
64
+ return sha256(JSON.stringify(canonicalCustomPart(part)));
65
+ }
66
+ function emptyPartLayer(slot) {
67
+ const { width, height } = PART_SLOTS[slot];
68
+ return Array.from({ length: height }, () => Array(width).fill(null));
69
+ }
70
+ function partLayer(slot, cells) {
71
+ const layer = emptyPartLayer(slot);
72
+ for (const [x, y, color, tone] of cells) layer[y][x] = {
73
+ color,
74
+ tone
75
+ };
76
+ return layer;
77
+ }
78
+ function partCells(layer) {
79
+ return layer.flatMap((row, y) => row.flatMap((ink, x) => ink ? [[
80
+ x,
81
+ y,
82
+ ink.color,
83
+ ink.tone
84
+ ]] : []));
85
+ }
86
+ /** Builds a canonical part from two drawn layers. */
87
+ function createCustomPart(slot, layers) {
88
+ return canonicalCustomPart({
89
+ slot,
90
+ front: partCells(layers.front),
91
+ back: partCells(layers.back)
92
+ });
93
+ }
94
+ /** The column mirrored across the Avatar centerline. */
95
+ function mirrorPartX(slot, x) {
96
+ return PART_SLOTS[slot].width - 1 - x;
97
+ }
98
+ const same = (a, b) => a === b || a !== null && b !== null && a.color === b.color && a.tone === b.tone;
99
+ /** Sets cells (pencil with ink, eraser with `null`), optionally mirrored across the centerline. */
100
+ function paintPartLayer(slot, layer, points, ink, mirror = false) {
101
+ const out = layer.map((row) => [...row]);
102
+ const { width, height } = PART_SLOTS[slot];
103
+ for (const [x, y] of points) for (const px of mirror ? [x, mirrorPartX(slot, x)] : [x]) if (px >= 0 && px < width && y >= 0 && y < height) out[y][px] = ink;
104
+ return out;
105
+ }
106
+ /**
107
+ * 4-connected flood fill from a cell: every cell reachable through cells equal to the start
108
+ * becomes `ink`. With `mirror`, the mirrored start is filled too.
109
+ */
110
+ function fillPartLayer(slot, layer, x, y, ink, mirror = false) {
111
+ const { width, height } = PART_SLOTS[slot];
112
+ let out = layer.map((row) => [...row]);
113
+ for (const sx of mirror ? [x, mirrorPartX(slot, x)] : [x]) {
114
+ if (sx < 0 || sx >= width || y < 0 || y >= height) continue;
115
+ const target = out[y][sx];
116
+ if (same(target, ink)) continue;
117
+ const next = out.map((row) => [...row]);
118
+ const stack = [[sx, y]];
119
+ while (stack.length) {
120
+ const [cx, cy] = stack.pop();
121
+ if (cx < 0 || cx >= width || cy < 0 || cy >= height) continue;
122
+ if (!same(next[cy][cx], target)) continue;
123
+ next[cy][cx] = ink;
124
+ stack.push([cx + 1, cy], [cx - 1, cy], [cx, cy + 1], [cx, cy - 1]);
125
+ }
126
+ out = next;
127
+ }
128
+ return out;
129
+ }
130
+ const K = Uint32Array.from("428a2f98 71374491 b5c0fbcf e9b5dba5 3956c25b 59f111f1 923f82a4 ab1c5ed5 d807aa98 12835b01 243185be 550c7dc3 72be5d74 80deb1fe 9bdc06a7 c19bf174 e49b69c1 efbe4786 0fc19dc6 240ca1cc 2de92c6f 4a7484aa 5cb0a9dc 76f988da 983e5152 a831c66d b00327c8 bf597fc7 c6e00bf3 d5a79147 06ca6351 14292967 27b70a85 2e1b2138 4d2c6dfc 53380d13 650a7354 766a0abb 81c2c92e 92722c85 a2bfe8a1 a81a664b c24b8b70 c76c51a3 d192e819 d6990624 f40e3585 106aa070 19a4c116 1e376c08 2748774c 34b0bcb5 391c0cb3 4ed8aa4a 5b9cca4f 682e6ff3 748f82ee 78a5636f 84c87814 8cc70208 90befffa a4506ceb bef9a3f7 c67178f2".split(" ").map((h) => Number.parseInt(h, 16)));
131
+ function sha256(text) {
132
+ const bytes = new TextEncoder().encode(text);
133
+ const length = Math.ceil((bytes.length + 9) / 64) * 64;
134
+ const data = new Uint8Array(length);
135
+ data.set(bytes);
136
+ data[bytes.length] = 128;
137
+ const view = new DataView(data.buffer);
138
+ view.setUint32(length - 8, Math.floor(bytes.length * 8 / 2 ** 32));
139
+ view.setUint32(length - 4, bytes.length * 8 >>> 0);
140
+ const h = Uint32Array.from([
141
+ 1779033703,
142
+ 3144134277,
143
+ 1013904242,
144
+ 2773480762,
145
+ 1359893119,
146
+ 2600822924,
147
+ 528734635,
148
+ 1541459225
149
+ ]);
150
+ const w = new Uint32Array(64);
151
+ const rotr = (v, n) => v >>> n | v << 32 - n;
152
+ for (let offset = 0; offset < length; offset += 64) {
153
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4);
154
+ for (let i = 16; i < 64; i++) {
155
+ const a = w[i - 15];
156
+ const b = w[i - 2];
157
+ const s0 = rotr(a, 7) ^ rotr(a, 18) ^ a >>> 3;
158
+ const s1 = rotr(b, 17) ^ rotr(b, 19) ^ b >>> 10;
159
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
160
+ }
161
+ let [a, b, c, d, e, f, g, k] = h;
162
+ for (let i = 0; i < 64; i++) {
163
+ const t1 = k + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K[i] + w[i] >>> 0;
164
+ const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
165
+ k = g;
166
+ g = f;
167
+ f = e;
168
+ e = d + t1 >>> 0;
169
+ d = c;
170
+ c = b;
171
+ b = a;
172
+ a = t1 + t2 >>> 0;
173
+ }
174
+ [
175
+ a,
176
+ b,
177
+ c,
178
+ d,
179
+ e,
180
+ f,
181
+ g,
182
+ k
183
+ ].forEach((v, i) => h[i] = h[i] + v >>> 0);
184
+ }
185
+ return [...h].map((v) => v.toString(16).padStart(8, "0")).join("");
186
+ }
187
+
188
+ //#endregion
3
189
  //#region src/random.ts
4
190
  function seededRandom(seed) {
5
191
  let h = 2166136261;
@@ -323,9 +509,48 @@ const AVATAR_COLORS = [
323
509
  * Avatar Species: a base on the pixel bust rig. Species share motion and anchors; each sets its
324
510
  * own ears, face details and suggested body colors. A recipe with a species is asset version 2.
325
511
  */
326
- const AVATAR_SPECIES = ["human", "goblin"];
512
+ const AVATAR_SPECIES = [
513
+ "human",
514
+ "goblin",
515
+ "elf",
516
+ "dwarf",
517
+ "orc",
518
+ "flower"
519
+ ];
327
520
  /** Optional per-piece hair colors (asset version 2); an absent piece uses `hairColor`. */
328
521
  const AVATAR_PIECE_COLORS = ["leftSideHairColor", "rightSideHairColor"];
522
+ /** Part choices that exist only in asset version 2, added after every version 1 choice. */
523
+ const AVATAR_PARTS_V2 = {
524
+ ...AVATAR_PARTS,
525
+ outfit: [
526
+ ...AVATAR_PARTS.outfit,
527
+ "armor",
528
+ "robe",
529
+ "tunic",
530
+ "cloak"
531
+ ],
532
+ accessory: [
533
+ ...AVATAR_PARTS.accessory,
534
+ "helmet",
535
+ "hood"
536
+ ]
537
+ };
538
+ /** Optional asset version 2 parts; an absent part is not drawn (or uses the first choice). */
539
+ const AVATAR_EXTRA_PARTS = {
540
+ beard: [
541
+ "short",
542
+ "full",
543
+ "braided"
544
+ ],
545
+ petals: [
546
+ "trumpet",
547
+ "daisy",
548
+ "sunflower",
549
+ "tulip",
550
+ "sakura"
551
+ ],
552
+ flowerBase: ["leaves", "pot"]
553
+ };
329
554
  const DEFAULT_RECIPE = {
330
555
  schemaVersion: 1,
331
556
  family: "illustrated",
@@ -354,16 +579,20 @@ function isPixelAvatarRecipe(value) {
354
579
  const r = value;
355
580
  const detailed = DETAIL_KEYS.filter((key) => Object.hasOwn(r, key)).length;
356
581
  if (detailed !== 0 && detailed !== DETAIL_KEYS.length) return false;
357
- const v2 = r["assetVersion"] === 2;
582
+ const v3 = r["assetVersion"] === 3;
583
+ const v2 = r["assetVersion"] === 2 || v3;
584
+ if (v3 ? !isPixelCustomPart(r["headpiece"]) : Object.hasOwn(r, "headpiece")) return false;
358
585
  const pieces = AVATAR_PIECE_COLORS.filter((key) => Object.hasOwn(r, key));
359
- const v2Keys = v2 ? 2 + pieces.length : 0;
586
+ const extras = Object.keys(AVATAR_EXTRA_PARTS).filter((key) => Object.hasOwn(r, key));
587
+ const v2Keys = v2 ? 2 + pieces.length + extras.length + (v3 ? 1 : 0) : 0;
360
588
  if (Object.keys(r).length !== Object.keys(DEFAULT_RECIPE).length + detailed + v2Keys) return false;
361
- 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])))) return false;
589
+ 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;
590
+ const catalog = v2 ? AVATAR_PARTS_V2 : AVATAR_PARTS;
362
591
  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) => {
363
592
  const v = r[key];
364
593
  const [min, max] = AVATAR_RANGES[key];
365
594
  return typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
366
- })) && r["schemaVersion"] === 1 && r["family"] === "illustrated" && (r["assetVersion"] === 1 || v2) && 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]));
595
+ })) && 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]));
367
596
  }
368
597
  function canonicalRecipe(recipe) {
369
598
  const canonical = {
@@ -375,31 +604,71 @@ function canonicalRecipe(recipe) {
375
604
  for (const key of PART_KEYS) canonical[key] = recipe[key];
376
605
  for (const key of AVATAR_COLORS) canonical[key] = recipe[key].toLowerCase();
377
606
  if (recipe.bangs !== void 0) for (const key of DETAIL_KEYS) canonical[key] = recipe[key];
378
- if (recipe.assetVersion === 2) {
607
+ if (recipe.assetVersion !== 1) {
379
608
  canonical["species"] = recipe.species;
380
609
  canonical["rightSideHair"] = recipe.rightSideHair;
381
610
  for (const key of AVATAR_PIECE_COLORS) if (recipe[key] !== void 0) canonical[key] = recipe[key].toLowerCase();
611
+ for (const key of Object.keys(AVATAR_EXTRA_PARTS)) if (recipe[key] !== void 0) canonical[key] = recipe[key];
612
+ if (recipe.assetVersion === 3) canonical["headpiece"] = canonicalCustomPart(recipe.headpiece);
382
613
  }
383
614
  return canonical;
384
615
  }
616
+ const HAIR_PIECES = [
617
+ "bangs",
618
+ "sideHair",
619
+ "rightSideHair",
620
+ "backHair"
621
+ ];
385
622
  /**
386
- * Returns the recipe as asset version 2 with the given species, keeping every other choice.
387
- * Side hair splits into left (`sideHair`) and right (`rightSideHair`) pieces that start equal.
388
- * When the skin color is one of the previous species' suggested colors, it moves to the new
389
- * species' first suggestion; a custom color is kept.
623
+ * Saved choices the recipe keeps but does not draw: a flower shows petals and a stem instead of
624
+ * hair, outfit, beard and headwear, and a helmet or hood covers the hair. The choices return when
625
+ * the species or headwear changes back.
390
626
  */
627
+ function hiddenChoices(recipe) {
628
+ if (recipe.species === "flower") return [
629
+ ...HAIR_PIECES,
630
+ "outfit",
631
+ "accessory",
632
+ "eyes",
633
+ "brows",
634
+ "nose",
635
+ "cheeks",
636
+ "glasses",
637
+ ...recipe.beard ? ["beard"] : []
638
+ ];
639
+ const hidden = [];
640
+ if (recipe.accessory === "helmet" || recipe.accessory === "hood") hidden.push(...HAIR_PIECES);
641
+ if (recipe.species === "dwarf") hidden.push("nose");
642
+ return hidden;
643
+ }
391
644
  function withSpecies(recipe, species) {
392
645
  const detailed = detailedRecipe(recipe);
393
646
  const previous = recipe.species ?? "human";
394
647
  const suggested = AVATAR_SPECIES_SWATCHES[previous].includes(recipe.skinColor.toLowerCase());
395
648
  return {
396
649
  ...detailed,
397
- assetVersion: 2,
650
+ assetVersion: recipe.assetVersion === 3 ? 3 : 2,
398
651
  species,
399
652
  rightSideHair: detailed.rightSideHair ?? detailed.sideHair,
400
653
  skinColor: suggested && previous !== species ? AVATAR_SPECIES_SWATCHES[species][0] : recipe.skinColor
401
654
  };
402
655
  }
656
+ /**
657
+ * Returns the recipe wearing a Custom Part in the headpiece slot (asset version 3), or without
658
+ * one (asset version 2). The recipe embeds its own copy of the part.
659
+ */
660
+ function withHeadpiece(recipe, part) {
661
+ const { headpiece: _, ...rest } = recipe.assetVersion === 1 ? withSpecies(recipe, "human") : recipe;
662
+ if (part === void 0) return {
663
+ ...rest,
664
+ assetVersion: 2
665
+ };
666
+ return {
667
+ ...rest,
668
+ assetVersion: 3,
669
+ headpiece: canonicalCustomPart(part)
670
+ };
671
+ }
403
672
  function detailedRecipe(recipe) {
404
673
  if (recipe.bangs !== void 0) return recipe;
405
674
  const pick = (part) => AVATAR_HAIR_PARTS[part].includes(recipe.hair) ? recipe.hair : HAIR_SPLIT[recipe.hair][part];
@@ -551,6 +820,38 @@ function createSeededRecipe(namespace) {
551
820
  return recipe;
552
821
  };
553
822
  }
823
+ /**
824
+ * A version 2 name-seeded recipe factory over every species. Species are equally likely; a
825
+ * person may get a beard or a version 2 outfit, and a flower gets petals and a base. Version 1
826
+ * factories (`createSeededRecipe`) keep their faces, so a consumer chooses per identity which
827
+ * seed version it was created with.
828
+ */
829
+ function createSeededRecipeV2(namespace) {
830
+ const base = createSeededRecipe(`${namespace}:base`);
831
+ return (seed) => {
832
+ const random = seededRandom(`${namespace}:${seed.trim().toLowerCase()}`);
833
+ const pick = (values) => values[Math.floor(random() * values.length)];
834
+ const species = pick(AVATAR_SPECIES);
835
+ const recipe = {
836
+ ...withSpecies(base(seed), species),
837
+ skinColor: pick(AVATAR_SPECIES_SWATCHES[species])
838
+ };
839
+ if (species === "flower") return {
840
+ ...recipe,
841
+ petals: pick(AVATAR_EXTRA_PARTS.petals),
842
+ flowerBase: pick(AVATAR_EXTRA_PARTS.flowerBase)
843
+ };
844
+ const beard = random() < .3 ? pick(AVATAR_EXTRA_PARTS.beard) : void 0;
845
+ const outfit = random() < .25 ? pick(AVATAR_PARTS_V2.outfit.slice(-4)) : recipe.outfit;
846
+ return {
847
+ ...recipe,
848
+ outfit,
849
+ ...beard ? { beard } : {}
850
+ };
851
+ };
852
+ }
853
+ /** Version 2 counterpart of `seededRecipe` (BotHarness's namespace). */
854
+ const seededRecipeV2 = createSeededRecipeV2("botharness-avatar-v2");
554
855
  /** The same name always gives the same face (BotHarness's namespace). */
555
856
  const seededRecipe = createSeededRecipe("botharness-avatar");
556
857
  /** Suggested body colors per species; any color remains allowed. */
@@ -563,6 +864,38 @@ const AVATAR_SPECIES_SWATCHES = {
563
864
  "#b3cf7a",
564
865
  "#5c8a4a",
565
866
  "#8fa86a"
867
+ ],
868
+ elf: [
869
+ "#fbe7d6",
870
+ "#f2d3c0",
871
+ "#e2c9b0",
872
+ "#d9c7e8",
873
+ "#b9c6dd",
874
+ "#8f7fa8"
875
+ ],
876
+ dwarf: [
877
+ "#f2c4a0",
878
+ "#e9b08a",
879
+ "#e0a87e",
880
+ "#c68863",
881
+ "#f0b8a0",
882
+ "#9a6142"
883
+ ],
884
+ orc: [
885
+ "#8a9a5b",
886
+ "#6f8a4e",
887
+ "#7d8c6a",
888
+ "#5f6f4a",
889
+ "#9aa070",
890
+ "#6a7a7a"
891
+ ],
892
+ flower: [
893
+ "#f6d36b",
894
+ "#f2b84b",
895
+ "#ffe3cf",
896
+ "#e8a85a",
897
+ "#c98b4a",
898
+ "#f4f1ec"
566
899
  ]
567
900
  };
568
901
  const preset = (parts) => ({
@@ -722,6 +1055,17 @@ const mix$1 = (base, tint, k) => {
722
1055
  const t = channels$1(tint);
723
1056
  return hex(channels$1(base).map((v, i) => v * (1 - k) + t[i] * k));
724
1057
  };
1058
+ const TONES = {
1059
+ [-2]: (c) => shade(c, .62),
1060
+ [-1]: (c) => shade(c, .8),
1061
+ 0: (c) => c,
1062
+ 1: (c) => mix$1(c, WHITE, .25),
1063
+ 2: (c) => mix$1(c, WHITE, .5)
1064
+ };
1065
+ /** The color a Custom Part cell shows at a tone step on the rig's shade ramp. */
1066
+ function partToneColor(color, tone) {
1067
+ return TONES[tone](color.toLowerCase());
1068
+ }
725
1069
  function paint(grid, test, color) {
726
1070
  for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (test(x, y)) grid[y][x] = color;
727
1071
  }
@@ -1121,6 +1465,34 @@ function outfit(recipe, g, s) {
1121
1465
  paint(g, (x, y) => y === 24 && Math.abs(dx(x)) <= 4.5, WHITE);
1122
1466
  sprite(g, ["RR.RR", ".RRR."], Math.round(C + s * .5) - 2, 24, { R: shirt });
1123
1467
  break;
1468
+ case "armor": {
1469
+ const metal = "#9aa3ad";
1470
+ paint(g, torso, metal);
1471
+ paint(g, (x, y) => torso(x, y) && y >= 26 && Math.abs(dx(x)) <= 2.5, shirt);
1472
+ paint(g, (x, y) => torso(x, y) && y <= 26 && Math.abs(dx(x)) >= half(y) - 3.5, mix$1(metal, WHITE, .35));
1473
+ paint(g, (x, y) => torso(x, y) && y === 27 && Math.abs(dx(x)) >= half(y) - 3.5, shade(metal, .7));
1474
+ paint(g, (x, y) => y === 24 && Math.abs(dx(x)) <= 3.5, shade(metal, .8));
1475
+ sprite(g, ["G"], Math.round(C + s * .5), 28, { G: GOLD });
1476
+ break;
1477
+ }
1478
+ case "robe":
1479
+ paint(g, v(5, 4), mix$1(shirt, WHITE, .7));
1480
+ paint(g, (x, y) => torso(x, y) && y <= 29 && Math.abs(Math.abs(dx(x)) - (4 - (y - 24) * .8)) <= .5, GOLD);
1481
+ paint(g, (x, y) => torso(x, y) && y >= 28 && Math.abs(dx(x)) >= half(y) - 1.5, dark);
1482
+ break;
1483
+ case "tunic":
1484
+ paint(g, v(2, 2), shade(skin, .85));
1485
+ sprite(g, ["L.L", ".L."], Math.round(C + s * .5) - 1, 25, { L: "#6b4a2b" });
1486
+ paint(g, (x, y) => torso(x, y) && y === 29, "#6b4a2b");
1487
+ sprite(g, ["G"], Math.round(C + s * .5), 29, { G: GOLD });
1488
+ break;
1489
+ case "cloak": {
1490
+ const cloak = mix$1(shirt, "#2f3a4a", .55);
1491
+ paint(g, (x, y) => torso(x, y) && Math.abs(dx(x)) >= 2.5, cloak);
1492
+ paint(g, (x, y) => torso(x, y) && y === 24, shade(cloak, .8));
1493
+ sprite(g, ["G...G"], Math.round(C + s * .5) - 2, 25, { G: GOLD });
1494
+ break;
1495
+ }
1124
1496
  case "jacket":
1125
1497
  paint(g, v(6, 4.5), WHITE);
1126
1498
  paint(g, (x, y) => torso(x, y) && Math.abs(Math.abs(dx(x)) - (4.5 - (y - 24) * .6)) <= .6, dark);
@@ -1239,6 +1611,22 @@ function accessory(recipe, g, s, face) {
1239
1611
  }, true);
1240
1612
  return;
1241
1613
  }
1614
+ case "helmet": {
1615
+ const metal = "#9aa3ad";
1616
+ paint(g, (x, y) => cap(x, y) && y <= 11, metal);
1617
+ paint(g, (x, y) => cap(x, y) && y === 11, shade(metal, .75));
1618
+ paint(g, (x, y) => cap(x, y) && y >= 12 && y <= 19 && Math.abs(x + .5 - C - s) >= 7.5, metal);
1619
+ paint(g, (x, y) => Math.abs(x + .5 - C - s) <= .5 && y >= 11 && y <= 16, shade(metal, .7));
1620
+ paint(g, (x, y) => cap(x, y) && y >= 2 && y <= 5 && x + .5 < C + s - 2, mix$1(metal, WHITE, .4));
1621
+ for (let x = Math.round(C + s) - 9; x <= Math.round(C + s) + 9; x += 3) sprite(g, ["G"], x, 10, { G: GOLD });
1622
+ return;
1623
+ }
1624
+ case "hood": {
1625
+ const outer = ellipse(C + s, 15, 12.6, 12.4);
1626
+ paint(g, (x, y) => outer(x, y) && y <= 25 && !(face(x, y) && y >= 10), shirt);
1627
+ 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));
1628
+ return;
1629
+ }
1242
1630
  case "beret":
1243
1631
  paint(g, (x, y) => ellipse(C + s + 1.5, 4.6, 10.5, 3.6)(x, y), shirt);
1244
1632
  paint(g, (x, y) => ellipse(C + s + 1.5, 4.6, 10.5, 3.6)(x, y) && y >= 6, shade(shirt, .78));
@@ -1334,6 +1722,75 @@ function accessory(recipe, g, s, face) {
1334
1722
  return;
1335
1723
  }
1336
1724
  }
1725
+ const POINTED_EARS = {
1726
+ goblin: {
1727
+ rows: [
1728
+ "E...",
1729
+ "EE..",
1730
+ ".EiE",
1731
+ "..Ee"
1732
+ ],
1733
+ rise: 2
1734
+ },
1735
+ elf: {
1736
+ rows: [
1737
+ "E....",
1738
+ "EE...",
1739
+ ".EE..",
1740
+ "..EiE",
1741
+ "...Ee"
1742
+ ],
1743
+ rise: 3
1744
+ },
1745
+ orc: {
1746
+ rows: [
1747
+ "E..",
1748
+ "EE.",
1749
+ ".Ee"
1750
+ ],
1751
+ rise: 1
1752
+ }
1753
+ };
1754
+ const leafShape = (cx, cy, long, wide, angle) => (x, y) => {
1755
+ const px = x + .5 - cx;
1756
+ const py = y + .5 - cy;
1757
+ const u = px * Math.cos(angle) + py * Math.sin(angle);
1758
+ const v = -px * Math.sin(angle) + py * Math.cos(angle);
1759
+ return (u / long) ** 2 + (v / wide) ** 2 <= 1;
1760
+ };
1761
+ function petals(style, s) {
1762
+ const cx = C + s;
1763
+ const cy = 16.6;
1764
+ 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));
1765
+ switch (style) {
1766
+ case "daisy": return ring(14, 10.4, 3.4, 1.7, Math.PI / 14);
1767
+ case "sunflower": return (x, y) => ring(16, 11.2, 2.6, 1.5)(x, y) || ellipse(cx, cy, 10.6, 10.2)(x, y);
1768
+ 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));
1769
+ 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);
1770
+ 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);
1771
+ }
1772
+ }
1773
+ function flowerBase(recipe, g, s) {
1774
+ const leaf = "#5f9e4a";
1775
+ const stem = C + s * .5;
1776
+ paint(g, (x, y) => y >= 22 && Math.abs(x + .5 - stem) <= 1, shade(leaf, .82));
1777
+ if (recipe.flowerBase === "pot") {
1778
+ const pot = recipe.shirtColor;
1779
+ const body = (x, y) => y >= 27 && Math.abs(x + .5 - stem) <= 6.5 - (y - 27) * .45;
1780
+ paint(g, leafShape(stem - 3, 25, 2.8, 1.2, -.5), leaf);
1781
+ paint(g, leafShape(stem + 3, 24.5, 2.8, 1.2, .5), shade(leaf, .9));
1782
+ paint(g, body, pot);
1783
+ paint(g, (x, y) => body(x, y) && y === 27, mix$1(pot, WHITE, .3));
1784
+ paint(g, (x, y) => body(x, y) && y >= 28 && x + .5 > stem + 3, shade(pot, .8));
1785
+ return;
1786
+ }
1787
+ const left = leafShape(stem - 4.5, 27, 4.4, 1.9, -.55);
1788
+ const right = leafShape(stem + 4.5, 25.5, 4.4, 1.9, .55);
1789
+ paint(g, left, leaf);
1790
+ paint(g, right, shade(leaf, .92));
1791
+ paint(g, leafShape(stem - 4.5, 27, 3.6, .5, -.55), shade(leaf, .75));
1792
+ paint(g, leafShape(stem + 4.5, 25.5, 3.6, .5, .55), shade(leaf, .7));
1793
+ }
1337
1794
  function pixelTileColor(hair) {
1338
1795
  const [r, g, b] = channels$1(hair).map((v) => v / 255);
1339
1796
  const max = Math.max(r, g, b);
@@ -1371,34 +1828,72 @@ function pixelFigure(recipe, yawDeg, options = {}) {
1371
1828
  const tuck = y >= 17 && d * dx > 4 ? Math.min(2, (y - 16) * .35) : 0;
1372
1829
  return front(x - Math.round(d * (1 + Math.max(0, y - 20) * .4) - d * tuck), y);
1373
1830
  } : front;
1374
- const masks = recipe.bangs === void 0 ? hair(recipe, s, b) : detailedHair(recipe, s, b);
1831
+ const flower = recipe.species === "flower";
1832
+ const covered = recipe.accessory === "helmet" || recipe.accessory === "hood";
1833
+ const bare = () => false;
1834
+ const petalRing = flower ? petals(recipe.petals ?? "trumpet", s) : bare;
1835
+ const masks = flower ? {
1836
+ back: petalRing,
1837
+ front: bare,
1838
+ sides: [(x, y) => petalRing(x, y) && x + .5 < C + s, (x, y) => petalRing(x, y) && x + .5 >= C + s]
1839
+ } : covered ? {
1840
+ back: bare,
1841
+ front: bare
1842
+ } : recipe.bangs === void 0 ? hair(recipe, s, b) : detailedHair(recipe, s, b);
1375
1843
  const body = blank();
1376
- outfit(recipe, body, s);
1844
+ if (flower) flowerBase(recipe, body, s);
1845
+ else outfit(recipe, body, s);
1377
1846
  const back = blank();
1378
- paint(back, masks.back, shade(hairColor, .82));
1847
+ if (flower) {
1848
+ paint(back, petalRing, hairColor);
1849
+ paint(back, (x, y) => petalRing(x, y) && (x + .5 - C - s) ** 2 + (y + .5 - 16.6) ** 2 < 90, shade(hairColor, .84));
1850
+ paint(back, (x, y) => petalRing(x, y) && x + .5 < C + s - 3 && y < 10, mix$1(hairColor, WHITE, .3));
1851
+ } else paint(back, masks.back, shade(hairColor, .82));
1379
1852
  if (masks.bands) paint(back, (x, y) => masks.back(x, y) && (y + Math.floor(x / 2)) % 4 === 0, shade(hairColor, .68));
1853
+ const headpiece = recipe.assetVersion === 3 ? recipe.headpiece : void 0;
1854
+ const placeCells = (grid, cells) => {
1855
+ const placed = cells.map(([x, y, color, tone]) => {
1856
+ const dx = x + .5 - C;
1857
+ const far = turned && d * dx < 0 && Math.abs(dx) >= 5;
1858
+ const base = color.startsWith("#") ? color.toLowerCase() : recipe[color];
1859
+ return {
1860
+ x: x + s + (far ? d : 0),
1861
+ y,
1862
+ c: TONES[tone](base),
1863
+ far
1864
+ };
1865
+ });
1866
+ for (const cell of [...placed.filter((p) => p.far), ...placed.filter((p) => !p.far)]) if (cell.x >= 0 && cell.x < N) grid[cell.y][cell.x] = cell.c;
1867
+ };
1868
+ if (headpiece) placeCells(back, headpiece.back);
1380
1869
  const head = blank();
1381
1870
  paint(head, face, skin);
1871
+ const h = recipe.height ?? 0;
1872
+ if (recipe.beard && !flower) {
1873
+ const centre = C + fs * .6;
1874
+ const mouthX = Math.round(C - .5 + fs * .6);
1875
+ const chin = (x, y) => face(x, y) || face(x, y - 2);
1876
+ const open = (x, y) => y >= 20 + h && y <= 22 + h && Math.abs(x - mouthX) <= 3;
1877
+ const full = (x, y) => chin(x, y) && (y >= 22 + h || Math.abs(x + .5 - centre) >= 4 && y >= 17 + h);
1878
+ 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;
1879
+ paint(head, (x, y) => shape(x, y) && !open(x, y), shade(hairColor, .9));
1880
+ paint(head, (x, y) => shape(x, y) && !open(x, y) && (x + y * 2) % 5 === 0, shade(hairColor, .72));
1881
+ }
1382
1882
  const earY = 15;
1383
1883
  const edge = (side) => {
1384
1884
  const xs = [...Array(N).keys()].filter((x) => face(x, earY));
1385
1885
  return side < 0 ? xs[0] - 1 : xs.at(-1) + 1;
1386
1886
  };
1387
- const goblin = recipe.species === "goblin";
1388
- if (goblin) {
1389
- const ear = [
1390
- "E...",
1391
- "EE..",
1392
- ".EiE",
1393
- "..Ee"
1394
- ];
1887
+ const pointed = recipe.species ? POINTED_EARS[recipe.species] : void 0;
1888
+ if (flower) {} else if (pointed) {
1395
1889
  const pal = {
1396
1890
  E: skin,
1397
1891
  e: shade(skin, .86),
1398
1892
  i: shade(skin, .74)
1399
1893
  };
1894
+ const width = pointed.rows[0].length;
1400
1895
  paint(head, masks.front, hairColor);
1401
- for (const ex of turned ? [-d] : [-1, 1]) sprite(head, ear, ex < 0 ? edge(ex) - 3 : edge(ex), earY - 2, pal, ex > 0);
1896
+ 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);
1402
1897
  } else if (!turned) for (const ex of [-1, 1]) sprite(head, [
1403
1898
  "E",
1404
1899
  "E",
@@ -1416,9 +1911,9 @@ function pixelFigure(recipe, yawDeg, options = {}) {
1416
1911
  E: skin,
1417
1912
  e: shade(skin, .8)
1418
1913
  });
1419
- if (!goblin) paint(head, masks.front, hairColor);
1914
+ if (!pointed && !flower) paint(head, masks.front, hairColor);
1420
1915
  for (const [x, y] of masks.ties ?? []) sprite(back, ["T"], Math.round(x), y, { T: shade(recipe.shirtColor, .8) });
1421
- accessory(recipe, head, s, face);
1916
+ if (!flower) accessory(recipe, head, s, face);
1422
1917
  const shadeHair = (g) => {
1423
1918
  const out = g.map((r) => [...r]);
1424
1919
  const cells = g.flatMap((row, y) => row.flatMap((c, x) => c === hairColor ? [[x, y]] : []));
@@ -1459,18 +1954,20 @@ function pixelFigure(recipe, yawDeg, options = {}) {
1459
1954
  }
1460
1955
  return out;
1461
1956
  };
1957
+ const crown = blank();
1958
+ if (headpiece) placeCells(crown, headpiece.front);
1462
1959
  const figure = blank();
1463
1960
  for (const layer of [
1464
1961
  back,
1465
1962
  body,
1466
- shadeHair(head)
1963
+ shadeHair(head),
1964
+ crown
1467
1965
  ]) for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (layer[y][x]) figure[y][x] = layer[y][x];
1468
1966
  const features = blank();
1469
1967
  const eyes = blank();
1470
1968
  const closed = blank();
1471
1969
  const glasses = blank();
1472
1970
  const sp = recipe.spacing ?? 0;
1473
- const h = recipe.height ?? 0;
1474
1971
  const eyeTop = 15 + h;
1475
1972
  const eyeW = (side) => turned && Math.sign(fs) === side ? 3 : 4;
1476
1973
  const eyeX = (side) => {
@@ -1493,21 +1990,47 @@ function pixelFigure(recipe, yawDeg, options = {}) {
1493
1990
  I: recipe.eyeColor,
1494
1991
  L: mix$1(recipe.eyeColor, WHITE, .45)
1495
1992
  };
1496
- const style = EYES[recipe.eyes];
1497
- const extra = style[0].length - 4;
1498
- sprite(eyes, fit(style, left.w + extra), left.x - extra, eyeTop, pal);
1499
- sprite(eyes, recipe.eyes === "wink" ? fit(CLOSED, right.w) : fit(style, right.w + extra), right.x, eyeTop, pal, true);
1500
- sprite(closed, fit(CLOSED, left.w), left.x, eyeTop, pal);
1501
- sprite(closed, fit(CLOSED, right.w), right.x, eyeTop, pal, true);
1502
- const browRows = BROWS[recipe.brows];
1993
+ if (flower) {
1994
+ const centre = Math.round(C + fs * .6);
1995
+ const bead = {
1996
+ K: "#1d1b22",
1997
+ W: WHITE
1998
+ };
1999
+ for (const ex of [centre - 3, centre + 1]) {
2000
+ sprite(eyes, [
2001
+ "KK",
2002
+ "KW",
2003
+ "KK",
2004
+ "KK"
2005
+ ], ex, eyeTop - 1, bead);
2006
+ sprite(closed, [
2007
+ "..",
2008
+ "..",
2009
+ "KK",
2010
+ ".."
2011
+ ], ex, eyeTop - 1, bead);
2012
+ }
2013
+ } else {
2014
+ const style = EYES[recipe.eyes];
2015
+ const extra = style[0].length - 4;
2016
+ sprite(eyes, fit(style, left.w + extra), left.x - extra, eyeTop, pal);
2017
+ sprite(eyes, recipe.eyes === "wink" ? fit(CLOSED, right.w) : fit(style, right.w + extra), right.x, eyeTop, pal, true);
2018
+ sprite(closed, fit(CLOSED, left.w), left.x, eyeTop, pal);
2019
+ sprite(closed, fit(CLOSED, right.w), right.x, eyeTop, pal, true);
2020
+ }
2021
+ const browRows = flower ? [] : BROWS[recipe.brows];
1503
2022
  if (browRows.length) {
1504
2023
  sprite(features, fit(browRows, left.w), left.x, eyeTop - 2, { B: shade(hairColor, .55) });
1505
2024
  sprite(features, fit(browRows, right.w), right.x, eyeTop - 2, { B: shade(hairColor, .55) }, true);
1506
2025
  }
1507
2026
  const cx = Math.round(C + fs * (turned ? .85 : .6));
1508
- if (recipe.nose === "dot") sprite(features, ["N"], cx, 18 + h, { N: shade(skin, .78) });
1509
- if (recipe.nose === "button") sprite(features, ["N"], cx, 18 + h, { N: shade(skin, .88) });
1510
- if (recipe.nose === "line") sprite(features, ["N", "N"], cx, 17 + h, { N: shade(skin, .82) });
2027
+ if (flower) {} else if (recipe.species === "dwarf") sprite(features, [".N.", "NnN"], cx - 1, 17 + h, {
2028
+ N: shade(skin, .84),
2029
+ n: shade(skin, .72)
2030
+ });
2031
+ else if (recipe.nose === "dot") sprite(features, ["N"], cx, 18 + h, { N: shade(skin, .78) });
2032
+ else if (recipe.nose === "button") sprite(features, ["N"], cx, 18 + h, { N: shade(skin, .88) });
2033
+ else if (recipe.nose === "line") sprite(features, ["N", "N"], cx, 17 + h, { N: shade(skin, .82) });
1511
2034
  const mx = C - .5 + fs * .6;
1512
2035
  const withMouth = (mouth) => {
1513
2036
  const layer = features.map((row) => [...row]);
@@ -1518,11 +2041,12 @@ function pixelFigure(recipe, yawDeg, options = {}) {
1518
2041
  W: WHITE
1519
2042
  });
1520
2043
  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 });
1521
- if (recipe.cheeks === "blush") {
2044
+ 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 });
2045
+ if (recipe.cheeks === "blush" && !flower) {
1522
2046
  sprite(layer, ["PP"], left.x - 1, 19 + h, { P: mix$1(BLUSH, skin, .25) });
1523
2047
  sprite(layer, ["PP"], right.x + right.w - 1, 19 + h, { P: mix$1(BLUSH, skin, .25) });
1524
2048
  }
1525
- if (recipe.cheeks === "freckles") {
2049
+ if (recipe.cheeks === "freckles" && !flower) {
1526
2050
  sprite(layer, ["F.F"], left.x, 19 + h, { F: shade(skin, .7) });
1527
2051
  sprite(layer, ["F.F"], right.x + 1, 19 + h, { F: shade(skin, .7) });
1528
2052
  }
@@ -1545,17 +2069,18 @@ function pixelFigure(recipe, yawDeg, options = {}) {
1545
2069
  const corner = (x === fx - 1 || x === fx + w) && (y === eyeTop - 1 || y === eyeTop + 4);
1546
2070
  return box && !inside && !(round && corner);
1547
2071
  }, INK$1);
1548
- if (recipe.glasses === "round" || recipe.glasses === "square") {
1549
- frame(left.x, left.w, recipe.glasses === "round");
1550
- frame(right.x, right.w, recipe.glasses === "round");
2072
+ const lenses = flower ? "none" : recipe.glasses;
2073
+ if (lenses === "round" || lenses === "square") {
2074
+ frame(left.x, left.w, lenses === "round");
2075
+ frame(right.x, right.w, lenses === "round");
1551
2076
  paint(glasses, (x, y) => y === eyeTop + 1 && x > left.x + left.w && x < right.x - 1, INK$1);
1552
2077
  }
1553
- if (recipe.glasses === "shades") {
2078
+ if (lenses === "shades") {
1554
2079
  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);
1555
2080
  paint(glasses, (x, y) => y === eyeTop && x > left.x + left.w && x < right.x - 1, INK$1);
1556
2081
  for (const e of [left, right]) sprite(glasses, ["W"], e.x, eyeTop, { W: "#7d7b88" });
1557
2082
  }
1558
- if (recipe.glasses === "monocle") {
2083
+ if (lenses === "monocle") {
1559
2084
  frame(right.x, right.w, true);
1560
2085
  sprite(glasses, [
1561
2086
  "G",
@@ -1583,7 +2108,7 @@ function pixelFigure(recipe, yawDeg, options = {}) {
1583
2108
  for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
1584
2109
  const c = full[y][x];
1585
2110
  if (!c) continue;
1586
- if (y >= 22 && !head[y][x] && !back[y][x]) bodyCells[y][x] = c;
2111
+ if (y >= 22 && !head[y][x] && !back[y][x] && !crown[y][x]) bodyCells[y][x] = c;
1587
2112
  else headCells[y][x] = c;
1588
2113
  }
1589
2114
  const cells = [];
@@ -2131,5 +2656,5 @@ function symbolArtCells(rows, color) {
2131
2656
  }
2132
2657
 
2133
2658
  //#endregion
2134
- export { AVATAR_COLORS, AVATAR_HAIR_PARTS, AVATAR_PARTS, AVATAR_PIECE_COLORS, AVATAR_PRESETS, AVATAR_RANGES, AVATAR_SPECIES, AVATAR_SPECIES_SWATCHES, AVATAR_SWATCHES, AVATAR_TURNS, DEFAULT_RECIPE, PIXEL_SYMBOLS, canonicalRecipe, createSeededRecipe, detailedRecipe, faceCells, isPixelAvatarRecipe, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, seededRandom, seededRecipe, symbolArtCells, withSpecies };
2659
+ 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, MAX_PART_FIXED_COLORS, PART_LAYERS, PART_SLOTS, PART_TONES, PIXEL_SYMBOLS, canonicalCustomPart, canonicalRecipe, createCustomPart, createSeededRecipe, createSeededRecipeV2, customPartId, detailedRecipe, emptyPartLayer, faceCells, fillPartLayer, hiddenChoices, isPixelAvatarRecipe, isPixelCustomPart, mirrorPartX, paintPartLayer, partCells, partLayer, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, seededRandom, seededRecipe, seededRecipeV2, symbolArtCells, withHeadpiece, withSpecies };
2135
2660
  //# sourceMappingURL=index.js.map