@botharness/pixel-avatar 0.9.0 → 0.10.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/README.md CHANGED
@@ -26,5 +26,6 @@ const svg = pixelAvatarSvg(recipe); // 32×32 viewBox, crisp edges, no ids or sc
26
26
  - `emptyPartLayer`, `paintPartLayer`, `fillPartLayer`, `mirrorPartX`, `partLayer`, `partCells`, `createCustomPart`: drawing on dense layers, with pencil and eraser strokes, 4-connected flood fill and mirroring across the Avatar centerline.
27
27
  - `partLinePoints`, `partRectPoints`, `gradientPartLayer`, `ditherThreshold`, `noisePartLayer`, `shadePartLayer`: line (with 0°/45°/90° snap), rectangle (with square), tone gradient with 4×4 or 2×2 ordered dithering inside the start region, seeded ±1 tone noise, and a shade stroke that moves each colored cell one tone. All mirror, and all produce ordinary cells: the seed and the tool are not part of the part.
28
28
  - `AVATAR_PIECE_COLORS_V4`, `AVATAR_STRANDS`, `AVATAR_HEADPIECES`, `withPieces`, `withBuiltInHeadpiece`, `builtInHeadpiece`, `headpieceStart`: asset version 4. Bangs, back hair and a single `strand` each take an optional color (`bangsColor`, `backHairColor`, `strandColor`) that defaults to `hairColor`, like the side hair colors. The headpiece slot holds a built-in headpiece (ears, horns, halo or small head wings) or a Custom Part, worn together with any accessory. The base sits behind the hair and the rest in front, and the far side hides when the head turns. `withPieces` upgrades a recipe when the Human edits and moves an accessory that is now a headpiece into the empty headpiece slot. Saved recipes are never migrated on their own, so they keep rendering as before. `headpieceStart` flattens the worn headpiece into a Custom Part to start drawing from.
29
+ - `AVATAR_ANIMAL_SPECIES`, `AVATAR_PATTERNS`, `isAnimalSpecies`: animal species in asset version 4. Each has a muzzle, an animal nose and its own ears, uses the skin color as fur, and takes an optional fur `pattern` drawn in fur tones. A drawn `pattern` Custom Part replaces the built-in one. Ear headpieces and ear accessories are kept but hidden on an animal.
29
30
  - `faceCells`, `pixelSymbolCells`, `symbolArtCells`: pixels for [`@botharness/pixel-morph`](../morph).
30
31
  - `pixelAvatarSvg(recipe, { turns, classPrefix })`: optional pre-rendered head turns, and rig layer classes (`<prefix>-body`, `-head`, `-face`, `-gaze`, `-blink`) for CSS animation.
package/dist/index.d.ts CHANGED
@@ -36,6 +36,17 @@ type AvatarColor = (typeof AVATAR_COLORS)[number];
36
36
  */
37
37
  declare const AVATAR_SPECIES: readonly ["human", "goblin", "elf", "dwarf", "orc", "flower"];
38
38
  type AvatarSpecies = (typeof AVATAR_SPECIES)[number];
39
+ /**
40
+ * Anthropomorphic animal species (asset version 4): a muzzle, animal ears and fur colors. The
41
+ * skin color is the fur color, and an optional `pattern` is drawn in its tones.
42
+ */
43
+ declare const AVATAR_ANIMAL_SPECIES: readonly ["cat", "dog", "fox", "rabbit", "bear"];
44
+ type AvatarAnimalSpecies = (typeof AVATAR_ANIMAL_SPECIES)[number];
45
+ type AvatarSpeciesV4 = AvatarSpecies | AvatarAnimalSpecies;
46
+ declare const isAnimalSpecies: (species: unknown) => species is AvatarAnimalSpecies;
47
+ /** Fur patterns for animal species, drawn in skin-color tones so they survive recoloring. */
48
+ declare const AVATAR_PATTERNS: readonly ["solid", "tabby", "spots", "patches", "colorpoint"];
49
+ type AvatarPattern = (typeof AVATAR_PATTERNS)[number];
39
50
  /** Optional per-piece hair colors (asset version 2); an absent piece uses `hairColor`. */
40
51
  declare const AVATAR_PIECE_COLORS: readonly ["leftSideHairColor", "rightSideHairColor"];
41
52
  type AvatarPieceColor = (typeof AVATAR_PIECE_COLORS)[number];
@@ -54,7 +65,7 @@ type AvatarStrand = (typeof AVATAR_STRANDS)[number];
54
65
  */
55
66
  declare const AVATAR_HEADPIECES: readonly ["catears", "bunnyears", "horseears", "horns", "halo", "wings"];
56
67
  type AvatarHeadpiece = (typeof AVATAR_HEADPIECES)[number];
57
- type V4Key = AvatarPieceColorV4 | 'strand';
68
+ type V4Key = AvatarPieceColorV4 | 'strand' | 'pattern';
58
69
  /** Part choices that exist only in asset version 2, added after every version 1 choice. */
59
70
  declare const AVATAR_PARTS_V2: {
60
71
  readonly outfit: readonly ["tee", "shirttie", "hoodie", "turtleneck", "sailor", "blazer", "overalls", "dress", "kimono", "cardigan", "maid", "jacket", "armor", "robe", "tunic", "cloak"];
@@ -114,6 +125,7 @@ declare const CUSTOM_PART_KEYS: {
114
125
  readonly cheeks: "cheeksPart";
115
126
  readonly petals: "petalsPart";
116
127
  readonly flowerBase: "flowerBasePart";
128
+ readonly pattern: "patternPart";
117
129
  };
118
130
  type CustomPartKey = (typeof CUSTOM_PART_KEYS)[PartSlot];
119
131
  /**
@@ -122,17 +134,21 @@ type CustomPartKey = (typeof CUSTOM_PART_KEYS)[PartSlot];
122
134
  */
123
135
  type PixelAvatarRecipeV3 = Omit<PixelAvatarRecipeV2, 'assetVersion' | CustomPartKey> & {
124
136
  assetVersion: 3;
125
- } & { [P in CustomPartKey]?: PixelCustomPart };
137
+ } & { [P in Exclude<CustomPartKey, 'patternPart'>]?: PixelCustomPart } & {
138
+ patternPart?: never;
139
+ };
126
140
  /**
127
141
  * Asset version 4: version 2 with a color for every hair piece, a single `strand`, and the
128
142
  * headpiece slot holding either a built-in headpiece or a Custom Part, with any number of
129
143
  * Custom Parts worn (none included).
130
144
  */
131
- type PixelAvatarRecipeV4 = Omit<PixelAvatarRecipeV2, 'assetVersion' | CustomPartKey | V4Key> & {
145
+ type PixelAvatarRecipeV4 = Omit<PixelAvatarRecipeV2, 'assetVersion' | 'species' | CustomPartKey | V4Key> & {
132
146
  assetVersion: 4;
133
147
  } & { [P in Exclude<CustomPartKey, 'headpiece'>]?: PixelCustomPart } & {
134
148
  headpiece?: PixelCustomPart | AvatarHeadpiece;
135
149
  strand?: AvatarStrand;
150
+ species: AvatarSpeciesV4;
151
+ pattern?: AvatarPattern;
136
152
  } & { [P in AvatarPieceColorV4]?: string };
137
153
  type PixelAvatarRecipe = PixelAvatarRecipeV1 | PixelAvatarRecipeV2 | PixelAvatarRecipeV3 | PixelAvatarRecipeV4;
138
154
  declare const DEFAULT_RECIPE: PixelAvatarRecipeV1;
@@ -150,10 +166,11 @@ declare function hiddenChoices(recipe: PixelAvatarRecipe): readonly string[];
150
166
  * When the skin color is one of the previous species' suggested colors, it moves to the new
151
167
  * species' first suggestion; a custom color is kept.
152
168
  */
153
- declare function withSpecies(recipe: PixelAvatarRecipeV4, species: AvatarSpecies): PixelAvatarRecipeV4;
169
+ declare function withSpecies(recipe: PixelAvatarRecipe, species: AvatarAnimalSpecies): PixelAvatarRecipeV4;
170
+ declare function withSpecies(recipe: PixelAvatarRecipeV4, species: AvatarSpeciesV4): PixelAvatarRecipeV4;
154
171
  declare function withSpecies(recipe: PixelAvatarRecipeV3, species: AvatarSpecies): PixelAvatarRecipeV3;
155
172
  declare function withSpecies(recipe: PixelAvatarRecipeV1 | PixelAvatarRecipeV2, species: AvatarSpecies): PixelAvatarRecipeV2;
156
- declare function withSpecies(recipe: PixelAvatarRecipe, species: AvatarSpecies): PixelAvatarRecipeV2 | PixelAvatarRecipeV3 | PixelAvatarRecipeV4;
173
+ declare function withSpecies(recipe: PixelAvatarRecipe, species: AvatarSpeciesV4): PixelAvatarRecipeV2 | PixelAvatarRecipeV3 | PixelAvatarRecipeV4;
157
174
  /**
158
175
  * Returns the recipe wearing `part` in `slot`, or with that slot's part taken off. The recipe
159
176
  * embeds its own copy; it is asset version 3 while it wears any part and version 2 otherwise,
@@ -194,7 +211,7 @@ declare const seededRecipeV2: (seed: string) => PixelAvatarRecipeV2;
194
211
  /** The same name always gives the same face (BotHarness's namespace). */
195
212
  declare const seededRecipe: (seed: string) => PixelAvatarRecipe;
196
213
  /** Suggested body colors per species; any color remains allowed. */
197
- declare const AVATAR_SPECIES_SWATCHES: Record<AvatarSpecies, readonly string[]>;
214
+ declare const AVATAR_SPECIES_SWATCHES: Record<AvatarSpeciesV4, readonly string[]>;
198
215
  declare const AVATAR_PRESETS: readonly PixelAvatarRecipe[];
199
216
  //#endregion
200
217
  //#region src/part.d.ts
@@ -257,6 +274,10 @@ declare const PART_SLOTS: {
257
274
  readonly width: 32;
258
275
  readonly height: 32;
259
276
  };
277
+ readonly pattern: {
278
+ readonly width: 32;
279
+ readonly height: 32;
280
+ };
260
281
  };
261
282
  type PartSlot = keyof typeof PART_SLOTS;
262
283
  /**
@@ -270,7 +291,7 @@ declare const isHairPartSlot: (slot: PartSlot) => slot is HairPartSlot;
270
291
  * Slots whose drawn part replaces a built-in part pixel for pixel: the part's cells are painted
271
292
  * where the built-in part would be, and every cell shows its own color and tone.
272
293
  */
273
- declare const REPLACE_PART_SLOTS: readonly ["outfit", "accessory", "beard", "glasses", "nose", "cheeks", "petals", "flowerBase"];
294
+ declare const REPLACE_PART_SLOTS: readonly ["outfit", "accessory", "beard", "glasses", "nose", "cheeks", "petals", "flowerBase", "pattern"];
274
295
  type ReplacePartSlot = (typeof REPLACE_PART_SLOTS)[number];
275
296
  declare const isReplacePartSlot: (slot: PartSlot) => slot is ReplacePartSlot;
276
297
  /** Tone steps on the rig's shade ramp: two darker, the color itself, two lighter. */
@@ -441,5 +462,5 @@ declare function pixelSymbolCells(symbol: PixelSymbol, color: string): PixelCell
441
462
  */
442
463
  declare function symbolArtCells(rows: readonly string[], color: string): PixelCell$1[];
443
464
  //#endregion
444
- export { AVATAR_COLORS, AVATAR_EXTRA_PARTS, AVATAR_HAIR_PARTS, AVATAR_HEADPIECES, AVATAR_PARTS, AVATAR_PARTS_V2, AVATAR_PIECE_COLORS, AVATAR_PIECE_COLORS_V4, AVATAR_PRESETS, AVATAR_RANGES, AVATAR_SPECIES, AVATAR_SPECIES_SWATCHES, AVATAR_STRANDS, AVATAR_SWATCHES, AVATAR_TURNS, type AvatarColor, type AvatarExtraPart, type AvatarHairPart, type AvatarHeadpiece, type AvatarPart, type AvatarPieceColor, type AvatarPieceColorV4, type AvatarRange, type AvatarSpecies, type AvatarStrand, CUSTOM_PART_KEYS, type CustomPartKey, DEFAULT_RECIPE, HAIR_PART_SLOTS, type HairPartSlot, MAX_PART_FIXED_COLORS, PART_LAYERS, PART_SLOTS, PART_TONES, PIXEL_SYMBOLS, type PartCell, type PartColor, type PartInk, type PartLayer, type PartLayerName, type PartSlot, type PartTone, type PixelAvatarRecipe, type PixelAvatarRecipeV1, type PixelAvatarRecipeV2, type PixelAvatarRecipeV3, type PixelAvatarRecipeV4, type PixelAvatarSvgOptions, type PixelCell, type PixelCustomPart, type PixelFigureOptions, type PixelGrid, type PixelMouthState, type PixelSymbol, REPLACE_PART_SLOTS, type ReplacePartSlot, builtInHeadpiece, canonicalCustomPart, canonicalRecipe, createCustomPart, createSeededRecipe, createSeededRecipeV2, customPartId, detailedRecipe, ditherThreshold, emptyPartLayer, faceCells, fillPartLayer, gradientPartLayer, hairPieceStart, headpieceStart, hiddenChoices, isHairPartSlot, isPixelAvatarRecipe, isPixelCustomPart, isReplacePartSlot, mirrorPartX, noisePartLayer, paintPartLayer, partCells, partLayer, partLinePoints, partRectPoints, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, replacePartStart, seededRandom, seededRecipe, seededRecipeV2, shadePartLayer, symbolArtCells, withBuiltInHeadpiece, withCustomPart, withHeadpiece, withPieces, withSpecies, wornPart };
465
+ export { AVATAR_ANIMAL_SPECIES, AVATAR_COLORS, AVATAR_EXTRA_PARTS, AVATAR_HAIR_PARTS, AVATAR_HEADPIECES, AVATAR_PARTS, AVATAR_PARTS_V2, AVATAR_PATTERNS, AVATAR_PIECE_COLORS, AVATAR_PIECE_COLORS_V4, AVATAR_PRESETS, AVATAR_RANGES, AVATAR_SPECIES, AVATAR_SPECIES_SWATCHES, AVATAR_STRANDS, AVATAR_SWATCHES, AVATAR_TURNS, type AvatarAnimalSpecies, type AvatarColor, type AvatarExtraPart, type AvatarHairPart, type AvatarHeadpiece, type AvatarPart, type AvatarPattern, type AvatarPieceColor, type AvatarPieceColorV4, type AvatarRange, type AvatarSpecies, type AvatarSpeciesV4, type AvatarStrand, CUSTOM_PART_KEYS, type CustomPartKey, DEFAULT_RECIPE, HAIR_PART_SLOTS, type HairPartSlot, MAX_PART_FIXED_COLORS, PART_LAYERS, PART_SLOTS, PART_TONES, PIXEL_SYMBOLS, type PartCell, type PartColor, type PartInk, type PartLayer, type PartLayerName, type PartSlot, type PartTone, type PixelAvatarRecipe, type PixelAvatarRecipeV1, type PixelAvatarRecipeV2, type PixelAvatarRecipeV3, type PixelAvatarRecipeV4, type PixelAvatarSvgOptions, type PixelCell, type PixelCustomPart, type PixelFigureOptions, type PixelGrid, type PixelMouthState, type PixelSymbol, REPLACE_PART_SLOTS, type ReplacePartSlot, builtInHeadpiece, canonicalCustomPart, canonicalRecipe, createCustomPart, createSeededRecipe, createSeededRecipeV2, customPartId, detailedRecipe, ditherThreshold, emptyPartLayer, faceCells, fillPartLayer, gradientPartLayer, hairPieceStart, headpieceStart, hiddenChoices, isAnimalSpecies, isHairPartSlot, isPixelAvatarRecipe, isPixelCustomPart, isReplacePartSlot, mirrorPartX, noisePartLayer, paintPartLayer, partCells, partLayer, partLinePoints, partRectPoints, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, replacePartStart, seededRandom, seededRecipe, seededRecipeV2, shadePartLayer, symbolArtCells, withBuiltInHeadpiece, withCustomPart, withHeadpiece, withPieces, withSpecies, wornPart };
445
466
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -331,6 +331,26 @@ const AVATAR_SPECIES = [
331
331
  "orc",
332
332
  "flower"
333
333
  ];
334
+ /**
335
+ * Anthropomorphic animal species (asset version 4): a muzzle, animal ears and fur colors. The
336
+ * skin color is the fur color, and an optional `pattern` is drawn in its tones.
337
+ */
338
+ const AVATAR_ANIMAL_SPECIES = [
339
+ "cat",
340
+ "dog",
341
+ "fox",
342
+ "rabbit",
343
+ "bear"
344
+ ];
345
+ const isAnimalSpecies = (species) => AVATAR_ANIMAL_SPECIES.includes(species);
346
+ /** Fur patterns for animal species, drawn in skin-color tones so they survive recoloring. */
347
+ const AVATAR_PATTERNS = [
348
+ "solid",
349
+ "tabby",
350
+ "spots",
351
+ "patches",
352
+ "colorpoint"
353
+ ];
334
354
  /** Optional per-piece hair colors (asset version 2); an absent piece uses `hairColor`. */
335
355
  const AVATAR_PIECE_COLORS = ["leftSideHairColor", "rightSideHairColor"];
336
356
  /**
@@ -406,7 +426,8 @@ const CUSTOM_PART_KEYS = {
406
426
  nose: "nosePart",
407
427
  cheeks: "cheeksPart",
408
428
  petals: "petalsPart",
409
- flowerBase: "flowerBasePart"
429
+ flowerBase: "flowerBasePart",
430
+ pattern: "patternPart"
410
431
  };
411
432
  const PART_KEY_ENTRIES = Object.entries(CUSTOM_PART_KEYS);
412
433
  const DEFAULT_RECIPE = {
@@ -444,8 +465,13 @@ function isPixelAvatarRecipe(value) {
444
465
  if (builtInHead && !AVATAR_HEADPIECES.includes(r["headpiece"])) return false;
445
466
  const worn = PART_KEY_ENTRIES.filter(([slot, key]) => Object.hasOwn(r, key) && !(slot === "headpiece" && builtInHead));
446
467
  if (v3 ? worn.length === 0 : !v4 && worn.length !== 0) return false;
447
- const newer = [...AVATAR_PIECE_COLORS_V4, "strand"].filter((key) => Object.hasOwn(r, key));
448
- if (newer.length !== 0 && (!v4 || !AVATAR_PIECE_COLORS_V4.every((key) => !Object.hasOwn(r, key) || typeof r[key] === "string" && /^#[\da-f]{6}$/iu.test(r[key])) || Object.hasOwn(r, "strand") && !AVATAR_STRANDS.includes(r["strand"]))) return false;
468
+ if (!v4 && Object.hasOwn(r, CUSTOM_PART_KEYS.pattern)) return false;
469
+ const newer = [
470
+ ...AVATAR_PIECE_COLORS_V4,
471
+ "strand",
472
+ "pattern"
473
+ ].filter((key) => Object.hasOwn(r, key));
474
+ if (newer.length !== 0 && (!v4 || !AVATAR_PIECE_COLORS_V4.every((key) => !Object.hasOwn(r, key) || typeof r[key] === "string" && /^#[\da-f]{6}$/iu.test(r[key])) || Object.hasOwn(r, "strand") && !AVATAR_STRANDS.includes(r["strand"]) || Object.hasOwn(r, "pattern") && !AVATAR_PATTERNS.includes(r["pattern"]))) return false;
449
475
  for (const [slot, key] of worn) {
450
476
  const part = r[key];
451
477
  if (!isPixelCustomPart(part) || part.slot !== slot) return false;
@@ -454,7 +480,7 @@ function isPixelAvatarRecipe(value) {
454
480
  const extras = Object.keys(AVATAR_EXTRA_PARTS).filter((key) => Object.hasOwn(r, key));
455
481
  const v2Keys = v2 ? 2 + pieces.length + extras.length + worn.length + newer.length + (builtInHead ? 1 : 0) : 0;
456
482
  if (Object.keys(r).length !== Object.keys(DEFAULT_RECIPE).length + detailed + v2Keys) return false;
457
- 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;
483
+ if (v2 && (detailed === 0 || !(AVATAR_SPECIES.includes(r["species"]) || v4 && isAnimalSpecies(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;
458
484
  const catalog = v2 ? AVATAR_PARTS_V2 : AVATAR_PARTS;
459
485
  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) => {
460
486
  const v = r[key];
@@ -483,6 +509,7 @@ function canonicalRecipe(recipe) {
483
509
  }
484
510
  if (recipe.assetVersion === 4) {
485
511
  if (recipe.strand !== void 0) canonical["strand"] = recipe.strand;
512
+ if (recipe.pattern !== void 0 && recipe.pattern !== "solid") canonical["pattern"] = recipe.pattern;
486
513
  for (const key of AVATAR_PIECE_COLORS_V4) if (recipe[key] !== void 0) canonical[key] = recipe[key].toLowerCase();
487
514
  }
488
515
  }
@@ -494,6 +521,11 @@ const HAIR_PIECES = [
494
521
  "rightSideHair",
495
522
  "backHair"
496
523
  ];
524
+ const EAR_PIECES = [
525
+ "catears",
526
+ "bunnyears",
527
+ "horseears"
528
+ ];
497
529
  /**
498
530
  * Saved choices the recipe keeps but does not draw: a flower shows petals and a stem instead of
499
531
  * hair, outfit, beard and headwear, and a helmet or hood covers the hair. The choices return when
@@ -513,15 +545,22 @@ function hiddenChoices(recipe) {
513
545
  "nose",
514
546
  "cheeks",
515
547
  "glasses",
516
- ...recipe.beard || wornPart(recipe, "beard") ? ["beard"] : []
548
+ ...recipe.beard || wornPart(recipe, "beard") ? ["beard"] : [],
549
+ ...recipe.pattern || wornPart(recipe, "pattern") ? ["pattern"] : []
517
550
  ];
518
551
  const hidden = [];
519
552
  if ((recipe.accessory === "helmet" || recipe.accessory === "hood") && !wornPart(recipe, "accessory")) hidden.push(...HAIR_PIECES, ...strand);
520
553
  if (recipe.species === "dwarf") hidden.push("nose");
521
554
  if (!hidden.includes("strand")) hidden.push(...bald);
555
+ if (isAnimalSpecies(recipe.species)) {
556
+ hidden.push("nose");
557
+ if (EAR_PIECES.includes(builtInHeadpiece(recipe) ?? "")) hidden.push("headpiece");
558
+ if (EAR_PIECES.includes(recipe.accessory) && !wornPart(recipe, "accessory")) hidden.push("accessory");
559
+ } else if (recipe.pattern || wornPart(recipe, "pattern")) hidden.push("pattern");
522
560
  return hidden;
523
561
  }
524
- function withSpecies(recipe, species) {
562
+ function withSpecies(source, species) {
563
+ const recipe = isAnimalSpecies(species) && source.assetVersion !== 4 ? withPieces(source) : source;
525
564
  const detailed = detailedRecipe(recipe);
526
565
  const previous = recipe.species ?? "human";
527
566
  const suggested = AVATAR_SPECIES_SWATCHES[previous].includes(recipe.skinColor.toLowerCase());
@@ -534,14 +573,14 @@ function withSpecies(recipe, species) {
534
573
  };
535
574
  }
536
575
  function withCustomPart(recipe, slot, part) {
537
- const base = { ...recipe.assetVersion === 1 ? withSpecies(recipe, "human") : recipe };
576
+ const base = { ...slot === "pattern" && part !== void 0 ? withPieces(recipe) : recipe.assetVersion === 1 ? withSpecies(recipe, "human") : recipe };
538
577
  const key = CUSTOM_PART_KEYS[slot];
539
578
  delete base[key];
540
579
  if (part !== void 0) base[key] = canonicalCustomPart({
541
580
  ...part,
542
581
  slot
543
582
  });
544
- if (recipe.assetVersion === 4) return base;
583
+ if (base["assetVersion"] === 4) return base;
545
584
  const wearing = PART_KEY_ENTRIES.some(([, k]) => base[k] !== void 0);
546
585
  return {
547
586
  ...base,
@@ -814,6 +853,46 @@ const AVATAR_SPECIES_SWATCHES = {
814
853
  "#e8a85a",
815
854
  "#c98b4a",
816
855
  "#f4f1ec"
856
+ ],
857
+ cat: [
858
+ "#f2a65a",
859
+ "#9aa3ad",
860
+ "#3a3438",
861
+ "#f4f1ec",
862
+ "#c98b4a",
863
+ "#e8d2b0"
864
+ ],
865
+ dog: [
866
+ "#c98b4a",
867
+ "#e8d2b0",
868
+ "#8a5a36",
869
+ "#f4f1ec",
870
+ "#3a3438",
871
+ "#d9a066"
872
+ ],
873
+ fox: [
874
+ "#e0763a",
875
+ "#c4452f",
876
+ "#f2a65a",
877
+ "#f4f1ec",
878
+ "#8a5a36",
879
+ "#9aa3ad"
880
+ ],
881
+ rabbit: [
882
+ "#f4f1ec",
883
+ "#d9c7b8",
884
+ "#9aa3ad",
885
+ "#8a5a36",
886
+ "#e8d2b0",
887
+ "#3a3438"
888
+ ],
889
+ bear: [
890
+ "#8a5a36",
891
+ "#5a3a2a",
892
+ "#c98b4a",
893
+ "#3a3438",
894
+ "#f4f1ec",
895
+ "#d9a066"
817
896
  ]
818
897
  };
819
898
  const preset = (parts) => ({
@@ -1003,6 +1082,10 @@ const PART_SLOTS = {
1003
1082
  flowerBase: {
1004
1083
  width: 32,
1005
1084
  height: 32
1085
+ },
1086
+ pattern: {
1087
+ width: 32,
1088
+ height: 32
1006
1089
  }
1007
1090
  };
1008
1091
  /**
@@ -1028,7 +1111,8 @@ const REPLACE_PART_SLOTS = [
1028
1111
  "nose",
1029
1112
  "cheeks",
1030
1113
  "petals",
1031
- "flowerBase"
1114
+ "flowerBase",
1115
+ "pattern"
1032
1116
  ];
1033
1117
  const isReplacePartSlot = (slot) => REPLACE_PART_SLOTS.includes(slot);
1034
1118
  /** Tone steps on the rig's shade ramp: two darker, the color itself, two lighter. */
@@ -2205,6 +2289,91 @@ function drawHeadpiece(style, recipe, behind, front, s, far) {
2205
2289
  return;
2206
2290
  }
2207
2291
  }
2292
+ const ANIMALS = {
2293
+ cat: {
2294
+ ears: [
2295
+ "E...",
2296
+ "EE..",
2297
+ "EiE.",
2298
+ "EEEE"
2299
+ ],
2300
+ x: -9,
2301
+ y: 2,
2302
+ inner: "pink"
2303
+ },
2304
+ fox: {
2305
+ ears: [
2306
+ "D....",
2307
+ "EE...",
2308
+ "EiE..",
2309
+ "EiiE.",
2310
+ "EEEEE"
2311
+ ],
2312
+ x: -10,
2313
+ y: 1,
2314
+ inner: "fur"
2315
+ },
2316
+ rabbit: {
2317
+ ears: [
2318
+ "EE",
2319
+ "Ei",
2320
+ "Ei",
2321
+ "Ei",
2322
+ "Ei",
2323
+ "EE"
2324
+ ],
2325
+ x: -6,
2326
+ y: 0,
2327
+ inner: "pink"
2328
+ },
2329
+ bear: {
2330
+ ears: [
2331
+ ".EE.",
2332
+ "EiiE",
2333
+ "EEEE"
2334
+ ],
2335
+ x: -10,
2336
+ y: 3,
2337
+ inner: "fur"
2338
+ },
2339
+ dog: {
2340
+ ears: [
2341
+ "EEE.",
2342
+ "EEEE",
2343
+ "EEEE",
2344
+ ".EEE",
2345
+ "..EE"
2346
+ ],
2347
+ x: -13,
2348
+ y: 7,
2349
+ inner: "fur",
2350
+ side: true
2351
+ }
2352
+ };
2353
+ function patternMask(recipe, s, fs, face) {
2354
+ const cx = C + s;
2355
+ const mid = C + fs * .6;
2356
+ switch (recipe.pattern) {
2357
+ case "tabby": {
2358
+ let top = 0;
2359
+ while (top < N && !face(Math.round(cx), top)) top++;
2360
+ return (x, y) => y >= top && y <= top + 3 && Math.abs(x + .5 - cx) <= 3 && (x + 40 - Math.round(cx)) % 2 === 0 || (y === 17 || y === 19) && Math.abs(x + .5 - mid) >= 6.5;
2361
+ }
2362
+ case "spots": return (x, y) => [
2363
+ [-7, 21],
2364
+ [-6, 22],
2365
+ [-5, 21],
2366
+ [5, 21],
2367
+ [6, 22],
2368
+ [7, 21],
2369
+ [-2, 23],
2370
+ [2, 23]
2371
+ ].some(([dx, dy]) => x === Math.round(cx) + dx && y === dy);
2372
+ case "patches": return (x, y) => ellipse(cx - 4, 14, 3.6, 3.2)(x, y) || ellipse(cx + 5, 9, 2.6, 2)(x, y);
2373
+ case "colorpoint": return (x, y) => (x + .5 - cx) ** 2 / 64 + (y + .5 - 16.6) ** 2 / 49 > .62;
2374
+ default: return () => false;
2375
+ }
2376
+ }
2208
2377
  const POINTED_EARS = {
2209
2378
  goblin: {
2210
2379
  rows: [
@@ -2321,10 +2490,11 @@ function renderFigure(recipe, yawDeg, options, omit) {
2321
2490
  return front(x - Math.round(d * (1 + Math.max(0, y - 20) * .4) - d * tuck), y);
2322
2491
  } : front;
2323
2492
  const flower = recipe.species === "flower";
2493
+ const animal = isAnimalSpecies(recipe.species) ? ANIMALS[recipe.species] : void 0;
2324
2494
  const hiddenNow = hiddenChoices(recipe);
2325
2495
  const drawnParts = new Map(REPLACE_PART_SLOTS.flatMap((slot) => {
2326
2496
  const part = wornPart(recipe, slot);
2327
- const shown = slot === "petals" || slot === "flowerBase" ? flower : !hiddenNow.includes(slot);
2497
+ const shown = slot === "petals" || slot === "flowerBase" ? flower : slot === "pattern" ? animal !== void 0 : !hiddenNow.includes(slot);
2328
2498
  return part && shown ? [[slot, part.front]] : [];
2329
2499
  }));
2330
2500
  const skip = (slot) => omit.has(slot) || drawnParts.has(slot);
@@ -2430,11 +2600,18 @@ function renderFigure(recipe, yawDeg, options, omit) {
2430
2600
  placeCells(back, inked("backHair"), false);
2431
2601
  overlay(back, "petals");
2432
2602
  if (headpiece) placeCells(back, headpiece.back);
2433
- const wornHead = flower ? void 0 : builtInHeadpiece(recipe);
2603
+ const wornHead = flower || hiddenNow.includes("headpiece") ? void 0 : builtInHeadpiece(recipe);
2434
2604
  const headFront = blank();
2435
2605
  if (wornHead) drawHeadpiece(wornHead, recipe, back, headFront, s, turned ? d : 0);
2436
2606
  const head = blank();
2437
2607
  paint(head, face, skin);
2608
+ if (animal) {
2609
+ const pattern = patternMask(recipe, s, fs, face);
2610
+ if (!skip("pattern")) paint(head, (x, y) => face(x, y) && pattern(x, y), TONES[recipe.pattern === "patches" ? -2 : -1](skin));
2611
+ overlay(head, "pattern");
2612
+ const muzzle = ellipse(C + fs * .6, 20.6 + (recipe.height ?? 0), 3.9, 2.6);
2613
+ paint(head, (x, y) => face(x, y) && muzzle(x, y), TONES[2](skin));
2614
+ }
2438
2615
  const h = recipe.height ?? 0;
2439
2616
  if (recipe.beard && !flower && !skip("beard")) {
2440
2617
  const centre = C + fs * .6;
@@ -2452,7 +2629,17 @@ function renderFigure(recipe, yawDeg, options, omit) {
2452
2629
  return side < 0 ? xs[0] - 1 : xs.at(-1) + 1;
2453
2630
  };
2454
2631
  const pointed = recipe.species ? POINTED_EARS[recipe.species] : void 0;
2455
- if (flower) {} else if (pointed) {
2632
+ if (flower) {} else if (animal) {
2633
+ paint(head, masks.front, hairColor);
2634
+ const pal = {
2635
+ E: recipe.pattern === "colorpoint" || animal.side ? shade(skin, .72) : skin,
2636
+ i: animal.inner === "pink" ? "#f4a3b5" : shade(skin, .72),
2637
+ D: shade(skin, .45)
2638
+ };
2639
+ const cx = Math.round(C + s);
2640
+ const sides = animal.side && turned ? [-d] : [-1, 1];
2641
+ for (const side of sides) sprite(head, animal.ears, side < 0 ? cx + animal.x : cx - animal.x - animal.ears[0].length, animal.y, pal, side > 0);
2642
+ } else if (pointed) {
2456
2643
  const pal = {
2457
2644
  E: skin,
2458
2645
  e: shade(skin, .86),
@@ -2478,9 +2665,9 @@ function renderFigure(recipe, yawDeg, options, omit) {
2478
2665
  E: skin,
2479
2666
  e: shade(skin, .8)
2480
2667
  });
2481
- if (!pointed && !flower) paint(head, masks.front, hairColor);
2668
+ if (!pointed && !flower && !animal) paint(head, masks.front, hairColor);
2482
2669
  for (const [x, y] of masks.ties ?? []) sprite(back, ["T"], Math.round(x), y, { T: shade(recipe.shirtColor, .8) });
2483
- if (!flower && !skip("accessory")) accessory(recipe, head, s, face);
2670
+ if (!flower && !skip("accessory") && !hiddenNow.includes("accessory")) accessory(recipe, head, s, face);
2484
2671
  const shadeHair = (g) => {
2485
2672
  const out = g.map((r) => [...r]);
2486
2673
  const cells = g.flatMap((row, y) => row.flatMap((c, x) => c === hairColor ? [[x, y]] : []));
@@ -2599,7 +2786,8 @@ function renderFigure(recipe, yawDeg, options, omit) {
2599
2786
  sprite(features, fit(browRows, right.w), right.x, eyeTop - 2, { B: shade(hairColor, .55) }, true);
2600
2787
  }
2601
2788
  const cx = Math.round(C + fs * (turned ? .85 : .6));
2602
- if (flower) {} else if (recipe.species === "dwarf") sprite(features, [".N.", "NnN"], cx - 1, 17 + h, {
2789
+ if (flower) {} else if (animal) sprite(features, ["NNN", ".N."], Math.round(C + fs * .6) - 1, 18 + h, { N: mix$1(INK$1, skin, .2) });
2790
+ else if (recipe.species === "dwarf") sprite(features, [".N.", "NnN"], cx - 1, 17 + h, {
2603
2791
  N: shade(skin, .84),
2604
2792
  n: shade(skin, .72)
2605
2793
  });
@@ -3384,5 +3572,5 @@ function symbolArtCells(rows, color) {
3384
3572
  }
3385
3573
 
3386
3574
  //#endregion
3387
- export { AVATAR_COLORS, AVATAR_EXTRA_PARTS, AVATAR_HAIR_PARTS, AVATAR_HEADPIECES, AVATAR_PARTS, AVATAR_PARTS_V2, AVATAR_PIECE_COLORS, AVATAR_PIECE_COLORS_V4, AVATAR_PRESETS, AVATAR_RANGES, AVATAR_SPECIES, AVATAR_SPECIES_SWATCHES, AVATAR_STRANDS, AVATAR_SWATCHES, AVATAR_TURNS, CUSTOM_PART_KEYS, DEFAULT_RECIPE, HAIR_PART_SLOTS, MAX_PART_FIXED_COLORS, PART_LAYERS, PART_SLOTS, PART_TONES, PIXEL_SYMBOLS, REPLACE_PART_SLOTS, builtInHeadpiece, canonicalCustomPart, canonicalRecipe, createCustomPart, createSeededRecipe, createSeededRecipeV2, customPartId, detailedRecipe, ditherThreshold, emptyPartLayer, faceCells, fillPartLayer, gradientPartLayer, hairPieceStart, headpieceStart, hiddenChoices, isHairPartSlot, isPixelAvatarRecipe, isPixelCustomPart, isReplacePartSlot, mirrorPartX, noisePartLayer, paintPartLayer, partCells, partLayer, partLinePoints, partRectPoints, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, replacePartStart, seededRandom, seededRecipe, seededRecipeV2, shadePartLayer, symbolArtCells, withBuiltInHeadpiece, withCustomPart, withHeadpiece, withPieces, withSpecies, wornPart };
3575
+ export { AVATAR_ANIMAL_SPECIES, AVATAR_COLORS, AVATAR_EXTRA_PARTS, AVATAR_HAIR_PARTS, AVATAR_HEADPIECES, AVATAR_PARTS, AVATAR_PARTS_V2, AVATAR_PATTERNS, AVATAR_PIECE_COLORS, AVATAR_PIECE_COLORS_V4, AVATAR_PRESETS, AVATAR_RANGES, AVATAR_SPECIES, AVATAR_SPECIES_SWATCHES, AVATAR_STRANDS, AVATAR_SWATCHES, AVATAR_TURNS, CUSTOM_PART_KEYS, DEFAULT_RECIPE, HAIR_PART_SLOTS, MAX_PART_FIXED_COLORS, PART_LAYERS, PART_SLOTS, PART_TONES, PIXEL_SYMBOLS, REPLACE_PART_SLOTS, builtInHeadpiece, canonicalCustomPart, canonicalRecipe, createCustomPart, createSeededRecipe, createSeededRecipeV2, customPartId, detailedRecipe, ditherThreshold, emptyPartLayer, faceCells, fillPartLayer, gradientPartLayer, hairPieceStart, headpieceStart, hiddenChoices, isAnimalSpecies, isHairPartSlot, isPixelAvatarRecipe, isPixelCustomPart, isReplacePartSlot, mirrorPartX, noisePartLayer, paintPartLayer, partCells, partLayer, partLinePoints, partRectPoints, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, replacePartStart, seededRandom, seededRecipe, seededRecipeV2, shadePartLayer, symbolArtCells, withBuiltInHeadpiece, withCustomPart, withHeadpiece, withPieces, withSpecies, wornPart };
3388
3576
  //# sourceMappingURL=index.js.map