@botharness/pixel-avatar 0.6.0 → 0.7.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
@@ -22,6 +22,7 @@ const svg = pixelAvatarSvg(recipe); // 32×32 viewBox, crisp edges, no ids or sc
22
22
  - `AVATAR_PARTS_V2`, `AVATAR_EXTRA_PARTS`, `hiddenChoices(recipe)`: version 2 adds elf, dwarf, orc and flower species, armor/robe/tunic/cloak outfits, helmet/hood headwear, an optional `beard`, and a flower's `petals` and `flowerBase`. `hiddenChoices` names saved choices the current species or headwear keeps but does not draw, so an editor can say so instead of discarding them.
23
23
  - `PixelCustomPart`, `withHeadpiece(recipe, part)`, `customPartId`, `isPixelCustomPart`: a Human-drawn Custom Part in the `headpiece` slot (the top 32×16 of the tile). Its back layer is drawn behind the hair and its front layer over it, and both follow the head through turns. Each cell is `[x, y, color, tone]`, where `color` is an appearance color slot (`hairColor`, `skinColor`, `eyeColor`, `shirtColor`), so the part recolors with the Avatar, or a fixed `#rrggbb`, and `tone` is a step from −2 to 2 on the rig's shade ramp (`partToneColor`). A recipe wearing a part embeds its own copy as asset version 3. `customPartId` is the SHA-256 of the canonical content; names, authors and origins belong to the consumer's library.
24
24
  - `HAIR_PART_SLOTS`, `hairPieceStart(recipe, slot)`, `withCustomPart(recipe, slot, part)`, `wornPart`: drawn hair pieces. `bangs`, `leftSideHair`, `rightSideHair` and `backHair` each take a Custom Part (front layer only) that replaces the built-in piece while it stays saved underneath. In a hair part, a `hairColor` cell at tone 0 is live hair: it is shaded with the rest of the hair exactly like the built-in piece, so `hairPieceStart` flattens the worn built-in piece into such cells and an unchanged copy renders identically. Other cells show their own color. Drawn hair follows turns and is hidden under a helmet or hood and on a flower, like built-in hair.
25
+ - `REPLACE_PART_SLOTS`, `replacePartStart(recipe, slot)`: drawn `outfit`, `accessory`, `beard`, `glasses`, `nose`, `cheeks`, `petals` and `flowerBase` parts (front layer only). A drawn part replaces the built-in part pixel for pixel in the layer that part paints into, while the built-in choice stays saved underneath. `replacePartStart` flattens the worn built-in part for this recipe: pixels that are exactly an appearance color at a tone become that color slot, and the rest become fixed colors, so an unchanged copy renders identically. Drawn face parts never cover the speaking mouth, and drawn parts are hidden wherever the species or headwear hides the built-in part. A drawn accessory replacing a helmet or hood no longer hides the hair.
25
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.
26
27
  - `faceCells`, `pixelSymbolCells`, `symbolArtCells`: pixels for [`@botharness/pixel-morph`](../morph).
27
28
  - `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
@@ -90,6 +90,14 @@ declare const CUSTOM_PART_KEYS: {
90
90
  readonly leftSideHair: "leftSideHairPart";
91
91
  readonly rightSideHair: "rightSideHairPart";
92
92
  readonly backHair: "backHairPart";
93
+ readonly outfit: "outfitPart";
94
+ readonly accessory: "accessoryPart";
95
+ readonly beard: "beardPart";
96
+ readonly glasses: "glassesPart";
97
+ readonly nose: "nosePart";
98
+ readonly cheeks: "cheeksPart";
99
+ readonly petals: "petalsPart";
100
+ readonly flowerBase: "flowerBasePart";
93
101
  };
94
102
  type CustomPartKey = (typeof CUSTOM_PART_KEYS)[PartSlot];
95
103
  /**
@@ -177,6 +185,38 @@ declare const PART_SLOTS: {
177
185
  readonly width: 32;
178
186
  readonly height: 32;
179
187
  };
188
+ readonly outfit: {
189
+ readonly width: 32;
190
+ readonly height: 32;
191
+ };
192
+ readonly accessory: {
193
+ readonly width: 32;
194
+ readonly height: 32;
195
+ };
196
+ readonly beard: {
197
+ readonly width: 32;
198
+ readonly height: 32;
199
+ };
200
+ readonly glasses: {
201
+ readonly width: 32;
202
+ readonly height: 32;
203
+ };
204
+ readonly nose: {
205
+ readonly width: 32;
206
+ readonly height: 32;
207
+ };
208
+ readonly cheeks: {
209
+ readonly width: 32;
210
+ readonly height: 32;
211
+ };
212
+ readonly petals: {
213
+ readonly width: 32;
214
+ readonly height: 32;
215
+ };
216
+ readonly flowerBase: {
217
+ readonly width: 32;
218
+ readonly height: 32;
219
+ };
180
220
  };
181
221
  type PartSlot = keyof typeof PART_SLOTS;
182
222
  /**
@@ -186,6 +226,13 @@ type PartSlot = keyof typeof PART_SLOTS;
186
226
  declare const HAIR_PART_SLOTS: readonly ["bangs", "leftSideHair", "rightSideHair", "backHair"];
187
227
  type HairPartSlot = (typeof HAIR_PART_SLOTS)[number];
188
228
  declare const isHairPartSlot: (slot: PartSlot) => slot is HairPartSlot;
229
+ /**
230
+ * Slots whose drawn part replaces a built-in part pixel for pixel: the part's cells are painted
231
+ * where the built-in part would be, and every cell shows its own color and tone.
232
+ */
233
+ declare const REPLACE_PART_SLOTS: readonly ["outfit", "accessory", "beard", "glasses", "nose", "cheeks", "petals", "flowerBase"];
234
+ type ReplacePartSlot = (typeof REPLACE_PART_SLOTS)[number];
235
+ declare const isReplacePartSlot: (slot: PartSlot) => slot is ReplacePartSlot;
189
236
  /** Tone steps on the rig's shade ramp: two darker, the color itself, two lighter. */
190
237
  declare const PART_TONES: readonly [-2, -1, 0, 1, 2];
191
238
  type PartTone = (typeof PART_TONES)[number];
@@ -196,7 +243,7 @@ type PartCell = readonly [x: number, y: number, color: PartColor, tone: PartTone
196
243
  declare const PART_LAYERS: readonly ["front", "back"];
197
244
  type PartLayerName = (typeof PART_LAYERS)[number];
198
245
  /** Most fixed colors one part may use. */
199
- declare const MAX_PART_FIXED_COLORS = 16;
246
+ declare const MAX_PART_FIXED_COLORS = 32;
200
247
  /**
201
248
  * A Human-drawn Custom Part. Its content is immutable and its identity is `customPartId`;
202
249
  * name, author, origin and parent are metadata kept outside it.
@@ -260,6 +307,18 @@ declare function hairPieceStart(recipe: PixelAvatarRecipe, slot: HairPartSlot):
260
307
  front: PartCell[];
261
308
  back: PartCell[];
262
309
  };
310
+ /**
311
+ * The part in a replacement slot as a Custom Part to start drawing from: the drawn part worn
312
+ * there, or the built-in part flattened for this recipe in the front pose. Each pixel becomes a
313
+ * color slot and tone when it is exactly one, so it recolors with the Avatar, and a fixed color
314
+ * otherwise; an unchanged copy renders identically. A flattened helmet or hood no longer hides
315
+ * the hair: only the built-in ones do.
316
+ */
317
+ declare function replacePartStart(recipe: PixelAvatarRecipe, slot: ReplacePartSlot): {
318
+ slot: ReplacePartSlot;
319
+ front: PartCell[];
320
+ back: PartCell[];
321
+ };
263
322
  //#endregion
264
323
  //#region src/random.d.ts
265
324
  declare function seededRandom(seed: string): () => number;
@@ -309,5 +368,5 @@ declare function pixelSymbolCells(symbol: PixelSymbol, color: string): PixelCell
309
368
  */
310
369
  declare function symbolArtCells(rows: readonly string[], color: string): PixelCell$1[];
311
370
  //#endregion
312
- 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, type AvatarColor, type AvatarExtraPart, type AvatarHairPart, type AvatarPart, type AvatarPieceColor, type AvatarRange, type AvatarSpecies, 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 PixelAvatarSvgOptions, type PixelCell, type PixelCustomPart, type PixelFigureOptions, type PixelGrid, type PixelMouthState, type PixelSymbol, canonicalCustomPart, canonicalRecipe, createCustomPart, createSeededRecipe, createSeededRecipeV2, customPartId, detailedRecipe, emptyPartLayer, faceCells, fillPartLayer, hairPieceStart, hiddenChoices, isHairPartSlot, isPixelAvatarRecipe, isPixelCustomPart, mirrorPartX, paintPartLayer, partCells, partLayer, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, seededRandom, seededRecipe, seededRecipeV2, symbolArtCells, withCustomPart, withHeadpiece, withSpecies, wornPart };
371
+ 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, type AvatarColor, type AvatarExtraPart, type AvatarHairPart, type AvatarPart, type AvatarPieceColor, type AvatarRange, type AvatarSpecies, 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 PixelAvatarSvgOptions, type PixelCell, type PixelCustomPart, type PixelFigureOptions, type PixelGrid, type PixelMouthState, type PixelSymbol, REPLACE_PART_SLOTS, type ReplacePartSlot, canonicalCustomPart, canonicalRecipe, createCustomPart, createSeededRecipe, createSeededRecipeV2, customPartId, detailedRecipe, emptyPartLayer, faceCells, fillPartLayer, hairPieceStart, hiddenChoices, isHairPartSlot, isPixelAvatarRecipe, isPixelCustomPart, isReplacePartSlot, mirrorPartX, paintPartLayer, partCells, partLayer, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, replacePartStart, seededRandom, seededRecipe, seededRecipeV2, symbolArtCells, withCustomPart, withHeadpiece, withSpecies, wornPart };
313
372
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -371,7 +371,15 @@ const CUSTOM_PART_KEYS = {
371
371
  bangs: "bangsPart",
372
372
  leftSideHair: "leftSideHairPart",
373
373
  rightSideHair: "rightSideHairPart",
374
- backHair: "backHairPart"
374
+ backHair: "backHairPart",
375
+ outfit: "outfitPart",
376
+ accessory: "accessoryPart",
377
+ beard: "beardPart",
378
+ glasses: "glassesPart",
379
+ nose: "nosePart",
380
+ cheeks: "cheeksPart",
381
+ petals: "petalsPart",
382
+ flowerBase: "flowerBasePart"
375
383
  };
376
384
  const PART_KEY_ENTRIES = Object.entries(CUSTOM_PART_KEYS);
377
385
  const DEFAULT_RECIPE = {
@@ -465,10 +473,10 @@ function hiddenChoices(recipe) {
465
473
  "nose",
466
474
  "cheeks",
467
475
  "glasses",
468
- ...recipe.beard ? ["beard"] : []
476
+ ...recipe.beard || wornPart(recipe, "beard") ? ["beard"] : []
469
477
  ];
470
478
  const hidden = [];
471
- if (recipe.accessory === "helmet" || recipe.accessory === "hood") hidden.push(...HAIR_PIECES);
479
+ if ((recipe.accessory === "helmet" || recipe.accessory === "hood") && !wornPart(recipe, "accessory")) hidden.push(...HAIR_PIECES);
472
480
  if (recipe.species === "dwarf") hidden.push("nose");
473
481
  return hidden;
474
482
  }
@@ -894,6 +902,38 @@ const PART_SLOTS = {
894
902
  backHair: {
895
903
  width: 32,
896
904
  height: 32
905
+ },
906
+ outfit: {
907
+ width: 32,
908
+ height: 32
909
+ },
910
+ accessory: {
911
+ width: 32,
912
+ height: 32
913
+ },
914
+ beard: {
915
+ width: 32,
916
+ height: 32
917
+ },
918
+ glasses: {
919
+ width: 32,
920
+ height: 32
921
+ },
922
+ nose: {
923
+ width: 32,
924
+ height: 32
925
+ },
926
+ cheeks: {
927
+ width: 32,
928
+ height: 32
929
+ },
930
+ petals: {
931
+ width: 32,
932
+ height: 32
933
+ },
934
+ flowerBase: {
935
+ width: 32,
936
+ height: 32
897
937
  }
898
938
  };
899
939
  /**
@@ -907,6 +947,21 @@ const HAIR_PART_SLOTS = [
907
947
  "backHair"
908
948
  ];
909
949
  const isHairPartSlot = (slot) => HAIR_PART_SLOTS.includes(slot);
950
+ /**
951
+ * Slots whose drawn part replaces a built-in part pixel for pixel: the part's cells are painted
952
+ * where the built-in part would be, and every cell shows its own color and tone.
953
+ */
954
+ const REPLACE_PART_SLOTS = [
955
+ "outfit",
956
+ "accessory",
957
+ "beard",
958
+ "glasses",
959
+ "nose",
960
+ "cheeks",
961
+ "petals",
962
+ "flowerBase"
963
+ ];
964
+ const isReplacePartSlot = (slot) => REPLACE_PART_SLOTS.includes(slot);
910
965
  /** Tone steps on the rig's shade ramp: two darker, the color itself, two lighter. */
911
966
  const PART_TONES = [
912
967
  -2,
@@ -917,7 +972,7 @@ const PART_TONES = [
917
972
  ];
918
973
  const PART_LAYERS = ["front", "back"];
919
974
  /** Most fixed colors one part may use. */
920
- const MAX_PART_FIXED_COLORS = 16;
975
+ const MAX_PART_FIXED_COLORS = 32;
921
976
  const FIXED = /^#[\da-f]{6}$/iu;
922
977
  const isColor = (value) => typeof value === "string" && (AVATAR_COLORS.includes(value) || FIXED.test(value));
923
978
  function isPixelCustomPart(value) {
@@ -925,7 +980,7 @@ function isPixelCustomPart(value) {
925
980
  const r = value;
926
981
  if (Object.keys(r).length !== 3 || !Object.hasOwn(PART_SLOTS, r["slot"])) return false;
927
982
  const { width, height } = PART_SLOTS[r["slot"]];
928
- if (isHairPartSlot(r["slot"]) && r["back"]?.length !== 0) return false;
983
+ if (r["slot"] !== "headpiece" && r["back"]?.length !== 0) return false;
929
984
  const fixed = /* @__PURE__ */ new Set();
930
985
  for (const name of PART_LAYERS) {
931
986
  const cells = r[name];
@@ -1879,6 +1934,15 @@ function pixelTileColor(hair) {
1879
1934
  ].map((v) => v * 255));
1880
1935
  }
1881
1936
  function pixelFigure(recipe, yawDeg, options = {}) {
1937
+ const { grids: _, ...out } = renderFigure(recipe, yawDeg, options, /* @__PURE__ */ new Set());
1938
+ return out;
1939
+ }
1940
+ const FACE_SLOTS = [
1941
+ "nose",
1942
+ "cheeks",
1943
+ "glasses"
1944
+ ];
1945
+ function renderFigure(recipe, yawDeg, options, omit) {
1882
1946
  const yaw = yawDeg * Math.PI / 180;
1883
1947
  const sinY = Math.sin(yaw);
1884
1948
  const s = Math.round(sinY * 3);
@@ -1895,9 +1959,40 @@ function pixelFigure(recipe, yawDeg, options = {}) {
1895
1959
  return front(x - Math.round(d * (1 + Math.max(0, y - 20) * .4) - d * tuck), y);
1896
1960
  } : front;
1897
1961
  const flower = recipe.species === "flower";
1898
- const covered = recipe.accessory === "helmet" || recipe.accessory === "hood";
1962
+ const hiddenNow = hiddenChoices(recipe);
1963
+ const drawnParts = new Map(REPLACE_PART_SLOTS.flatMap((slot) => {
1964
+ const part = wornPart(recipe, slot);
1965
+ const shown = slot === "petals" || slot === "flowerBase" ? flower : !hiddenNow.includes(slot);
1966
+ return part && shown ? [[slot, part.front]] : [];
1967
+ }));
1968
+ const skip = (slot) => omit.has(slot) || drawnParts.has(slot);
1969
+ const covered = !skip("accessory") && (recipe.accessory === "helmet" || recipe.accessory === "hood");
1899
1970
  const bare = () => false;
1900
- const petalRing = flower ? petals(recipe.petals ?? "trumpet", s) : bare;
1971
+ const petalRing = flower && !skip("petals") ? petals(recipe.petals ?? "trumpet", s) : bare;
1972
+ const faceShift = Math.round(fs * .6);
1973
+ const mouthCells = /* @__PURE__ */ new Set();
1974
+ for (const rows of [...Object.values(MOUTHS), [
1975
+ ".K.",
1976
+ "KMK",
1977
+ ".K."
1978
+ ]]) {
1979
+ const left = Math.round(C - .5 + fs * .6 - (rows[0].length - 1) / 2);
1980
+ rows.forEach((row, dy) => [...row].forEach((code, dx) => {
1981
+ if (code !== ".") mouthCells.add(`${left + dx},${20 + (recipe.height ?? 0) + dy}`);
1982
+ }));
1983
+ }
1984
+ const mouthBox = (x, y) => mouthCells.has(`${x},${y}`);
1985
+ const overlay = (grid, slot, keepOutMouth = false) => {
1986
+ const cells = drawnParts.get(slot);
1987
+ if (!cells) return;
1988
+ const shift = FACE_SLOTS.includes(slot) ? faceShift : s;
1989
+ for (const [x, y, color, tone] of cells) {
1990
+ const px = x + shift;
1991
+ if (px < 0 || px >= N || keepOutMouth && mouthBox(px, y)) continue;
1992
+ const base = color.startsWith("#") ? color.toLowerCase() : recipe[color];
1993
+ grid[y][px] = TONES[tone](base);
1994
+ }
1995
+ };
1901
1996
  const builtin = flower ? {
1902
1997
  back: petalRing,
1903
1998
  front: bare,
@@ -1941,8 +2036,13 @@ function pixelFigure(recipe, yawDeg, options = {}) {
1941
2036
  };
1942
2037
  const inked = (slot) => drawnHair.filter((drawn) => slot === "all-front" ? drawn.slot !== "backHair" : drawn.slot === slot).flatMap((drawn) => drawn.part.front.filter(([, , color, tone]) => !(color === "hairColor" && tone === 0)));
1943
2038
  const body = blank();
1944
- if (flower) flowerBase(recipe, body, s);
1945
- else outfit(recipe, body, s);
2039
+ if (flower) {
2040
+ if (!skip("flowerBase")) flowerBase(recipe, body, s);
2041
+ overlay(body, "flowerBase");
2042
+ } else {
2043
+ if (!skip("outfit")) outfit(recipe, body, s);
2044
+ overlay(body, "outfit");
2045
+ }
1946
2046
  const back = blank();
1947
2047
  if (flower) {
1948
2048
  paint(back, petalRing, hairColor);
@@ -1966,11 +2066,12 @@ function pixelFigure(recipe, yawDeg, options = {}) {
1966
2066
  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;
1967
2067
  };
1968
2068
  placeCells(back, inked("backHair"), false);
2069
+ overlay(back, "petals");
1969
2070
  if (headpiece) placeCells(back, headpiece.back);
1970
2071
  const head = blank();
1971
2072
  paint(head, face, skin);
1972
2073
  const h = recipe.height ?? 0;
1973
- if (recipe.beard && !flower) {
2074
+ if (recipe.beard && !flower && !skip("beard")) {
1974
2075
  const centre = C + fs * .6;
1975
2076
  const mouthX = Math.round(C - .5 + fs * .6);
1976
2077
  const chin = (x, y) => face(x, y) || face(x, y - 2);
@@ -2014,7 +2115,7 @@ function pixelFigure(recipe, yawDeg, options = {}) {
2014
2115
  });
2015
2116
  if (!pointed && !flower) paint(head, masks.front, hairColor);
2016
2117
  for (const [x, y] of masks.ties ?? []) sprite(back, ["T"], Math.round(x), y, { T: shade(recipe.shirtColor, .8) });
2017
- if (!flower) accessory(recipe, head, s, face);
2118
+ if (!flower && !skip("accessory")) accessory(recipe, head, s, face);
2018
2119
  const shadeHair = (g) => {
2019
2120
  const out = g.map((r) => [...r]);
2020
2121
  const cells = g.flatMap((row, y) => row.flatMap((c, x) => c === hairColor ? [[x, y]] : []));
@@ -2055,6 +2156,9 @@ function pixelFigure(recipe, yawDeg, options = {}) {
2055
2156
  }
2056
2157
  return out;
2057
2158
  };
2159
+ const drawnOver = blank();
2160
+ overlay(drawnOver, "beard", true);
2161
+ overlay(drawnOver, "accessory");
2058
2162
  const crown = blank();
2059
2163
  placeCells(crown, inked("all-front"), false);
2060
2164
  if (headpiece) placeCells(crown, headpiece.front);
@@ -2063,6 +2167,7 @@ function pixelFigure(recipe, yawDeg, options = {}) {
2063
2167
  back,
2064
2168
  body,
2065
2169
  shadeHair(head),
2170
+ drawnOver,
2066
2171
  crown
2067
2172
  ]) for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (layer[y][x]) figure[y][x] = layer[y][x];
2068
2173
  const features = blank();
@@ -2130,6 +2235,7 @@ function pixelFigure(recipe, yawDeg, options = {}) {
2130
2235
  N: shade(skin, .84),
2131
2236
  n: shade(skin, .72)
2132
2237
  });
2238
+ else if (skip("nose")) overlay(features, "nose", true);
2133
2239
  else if (recipe.nose === "dot") sprite(features, ["N"], cx, 18 + h, { N: shade(skin, .78) });
2134
2240
  else if (recipe.nose === "button") sprite(features, ["N"], cx, 18 + h, { N: shade(skin, .88) });
2135
2241
  else if (recipe.nose === "line") sprite(features, ["N", "N"], cx, 17 + h, { N: shade(skin, .82) });
@@ -2144,11 +2250,12 @@ function pixelFigure(recipe, yawDeg, options = {}) {
2144
2250
  });
2145
2251
  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 });
2146
2252
  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 });
2147
- if (recipe.cheeks === "blush" && !flower) {
2253
+ if (skip("cheeks")) overlay(layer, "cheeks", true);
2254
+ if (recipe.cheeks === "blush" && !flower && !skip("cheeks")) {
2148
2255
  sprite(layer, ["PP"], left.x - 1, 19 + h, { P: mix$1(BLUSH, skin, .25) });
2149
2256
  sprite(layer, ["PP"], right.x + right.w - 1, 19 + h, { P: mix$1(BLUSH, skin, .25) });
2150
2257
  }
2151
- if (recipe.cheeks === "freckles" && !flower) {
2258
+ if (recipe.cheeks === "freckles" && !flower && !skip("cheeks")) {
2152
2259
  sprite(layer, ["F.F"], left.x, 19 + h, { F: shade(skin, .7) });
2153
2260
  sprite(layer, ["F.F"], right.x + 1, 19 + h, { F: shade(skin, .7) });
2154
2261
  }
@@ -2171,7 +2278,8 @@ function pixelFigure(recipe, yawDeg, options = {}) {
2171
2278
  const corner = (x === fx - 1 || x === fx + w) && (y === eyeTop - 1 || y === eyeTop + 4);
2172
2279
  return box && !inside && !(round && corner);
2173
2280
  }, INK$1);
2174
- const lenses = flower ? "none" : recipe.glasses;
2281
+ const lenses = flower || skip("glasses") ? "none" : recipe.glasses;
2282
+ overlay(glasses, "glasses", true);
2175
2283
  if (lenses === "round" || lenses === "square") {
2176
2284
  frame(left.x, left.w, lenses === "round");
2177
2285
  frame(right.x, right.w, lenses === "round");
@@ -2201,7 +2309,7 @@ function pixelFigure(recipe, yawDeg, options = {}) {
2201
2309
  hairColor: color
2202
2310
  };
2203
2311
  for (const k of AVATAR_PIECE_COLORS) delete plain[k];
2204
- for (const cell of pixelFigure(plain, yawDeg).cells) variant[cell.y][cell.x] = cell.c;
2312
+ for (const cell of renderFigure(plain, yawDeg, {}, omit).cells) variant[cell.y][cell.x] = cell.c;
2205
2313
  const near = (x, y) => mask(x, y) || mask(x - 1, y) || mask(x + 1, y) || mask(x, y - 1) || mask(x, y + 1);
2206
2314
  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];
2207
2315
  });
@@ -2210,7 +2318,7 @@ function pixelFigure(recipe, yawDeg, options = {}) {
2210
2318
  for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
2211
2319
  const c = full[y][x];
2212
2320
  if (!c) continue;
2213
- if (y >= 22 && !head[y][x] && !back[y][x] && !crown[y][x]) bodyCells[y][x] = c;
2321
+ if (y >= 22 && !head[y][x] && !back[y][x] && !crown[y][x] && !drawnOver[y][x]) bodyCells[y][x] = c;
2214
2322
  else headCells[y][x] = c;
2215
2323
  }
2216
2324
  const cells = [];
@@ -2223,6 +2331,13 @@ function pixelFigure(recipe, yawDeg, options = {}) {
2223
2331
  });
2224
2332
  }
2225
2333
  return {
2334
+ grids: {
2335
+ figure,
2336
+ features: savedFeatures,
2337
+ glasses,
2338
+ back,
2339
+ body
2340
+ },
2226
2341
  cells,
2227
2342
  tile: `<rect width="32" height="32" rx="6" fill="${pixelTileColor(recipe.hairColor)}"/>`,
2228
2343
  body: `<g class="bh-illustrated-body">${rects(bodyCells)}</g>`,
@@ -2269,6 +2384,48 @@ function hairPieceStart(recipe, slot) {
2269
2384
  back: []
2270
2385
  };
2271
2386
  }
2387
+ /**
2388
+ * The part in a replacement slot as a Custom Part to start drawing from: the drawn part worn
2389
+ * there, or the built-in part flattened for this recipe in the front pose. Each pixel becomes a
2390
+ * color slot and tone when it is exactly one, so it recolors with the Avatar, and a fixed color
2391
+ * otherwise; an unchanged copy renders identically. A flattened helmet or hood no longer hides
2392
+ * the hair: only the built-in ones do.
2393
+ */
2394
+ function replacePartStart(recipe, slot) {
2395
+ const worn = wornPart(recipe, slot);
2396
+ if (worn) return {
2397
+ slot,
2398
+ front: [...worn.front],
2399
+ back: []
2400
+ };
2401
+ const layer = slot === "glasses" ? "glasses" : slot === "nose" || slot === "cheeks" ? "features" : slot === "petals" ? "back" : slot === "outfit" || slot === "flowerBase" ? "body" : "figure";
2402
+ const shown = renderFigure(recipe, 0, {}, /* @__PURE__ */ new Set()).grids[layer];
2403
+ const without = renderFigure(recipe, 0, {}, new Set([slot])).grids[layer];
2404
+ const source = (c) => {
2405
+ for (const key of AVATAR_COLORS) for (const tone of [
2406
+ 0,
2407
+ -1,
2408
+ 1,
2409
+ -2,
2410
+ 2
2411
+ ]) if (TONES[tone](recipe[key].toLowerCase()) === c) return [key, tone];
2412
+ return [c, 0];
2413
+ };
2414
+ const front = [];
2415
+ for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
2416
+ const c = shown[y][x];
2417
+ if (c && c !== without[y][x]) front.push([
2418
+ x,
2419
+ y,
2420
+ ...source(c)
2421
+ ]);
2422
+ }
2423
+ return {
2424
+ slot,
2425
+ front,
2426
+ back: []
2427
+ };
2428
+ }
2272
2429
 
2273
2430
  //#endregion
2274
2431
  //#region src/svg.ts
@@ -2792,5 +2949,5 @@ function symbolArtCells(rows, color) {
2792
2949
  }
2793
2950
 
2794
2951
  //#endregion
2795
- 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, CUSTOM_PART_KEYS, DEFAULT_RECIPE, HAIR_PART_SLOTS, MAX_PART_FIXED_COLORS, PART_LAYERS, PART_SLOTS, PART_TONES, PIXEL_SYMBOLS, canonicalCustomPart, canonicalRecipe, createCustomPart, createSeededRecipe, createSeededRecipeV2, customPartId, detailedRecipe, emptyPartLayer, faceCells, fillPartLayer, hairPieceStart, hiddenChoices, isHairPartSlot, isPixelAvatarRecipe, isPixelCustomPart, mirrorPartX, paintPartLayer, partCells, partLayer, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, seededRandom, seededRecipe, seededRecipeV2, symbolArtCells, withCustomPart, withHeadpiece, withSpecies, wornPart };
2952
+ 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, CUSTOM_PART_KEYS, DEFAULT_RECIPE, HAIR_PART_SLOTS, MAX_PART_FIXED_COLORS, PART_LAYERS, PART_SLOTS, PART_TONES, PIXEL_SYMBOLS, REPLACE_PART_SLOTS, canonicalCustomPart, canonicalRecipe, createCustomPart, createSeededRecipe, createSeededRecipeV2, customPartId, detailedRecipe, emptyPartLayer, faceCells, fillPartLayer, hairPieceStart, hiddenChoices, isHairPartSlot, isPixelAvatarRecipe, isPixelCustomPart, isReplacePartSlot, mirrorPartX, paintPartLayer, partCells, partLayer, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, replacePartStart, seededRandom, seededRecipe, seededRecipeV2, symbolArtCells, withCustomPart, withHeadpiece, withSpecies, wornPart };
2796
2953
  //# sourceMappingURL=index.js.map