@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 +1 -0
- package/dist/index.d.ts +29 -8
- package/dist/index.js +204 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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:
|
|
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:
|
|
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<
|
|
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
|
-
|
|
448
|
-
|
|
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(
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|