@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 +1 -0
- package/dist/index.d.ts +61 -2
- package/dist/index.js +174 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 =
|
|
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 =
|
|
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 (
|
|
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
|
|
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)
|
|
1945
|
-
|
|
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 (
|
|
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
|
|
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
|