@botharness/pixel-avatar 0.8.0 → 0.9.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 +58 -8
- package/dist/index.js +293 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,5 +25,6 @@ const svg = pixelAvatarSvg(recipe); // 32×32 viewBox, crisp edges, no ids or sc
|
|
|
25
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.
|
|
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
|
+
- `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.
|
|
28
29
|
- `faceCells`, `pixelSymbolCells`, `symbolArtCells`: pixels for [`@botharness/pixel-morph`](../morph).
|
|
29
30
|
- `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
|
@@ -39,6 +39,22 @@ type AvatarSpecies = (typeof AVATAR_SPECIES)[number];
|
|
|
39
39
|
/** Optional per-piece hair colors (asset version 2); an absent piece uses `hairColor`. */
|
|
40
40
|
declare const AVATAR_PIECE_COLORS: readonly ["leftSideHairColor", "rightSideHairColor"];
|
|
41
41
|
type AvatarPieceColor = (typeof AVATAR_PIECE_COLORS)[number];
|
|
42
|
+
/**
|
|
43
|
+
* Asset version 4 adds a color for every other hair piece; an absent piece uses `hairColor`.
|
|
44
|
+
* `strandColor` colors the single strand (`strand`).
|
|
45
|
+
*/
|
|
46
|
+
declare const AVATAR_PIECE_COLORS_V4: readonly ["bangsColor", "backHairColor", "strandColor"];
|
|
47
|
+
type AvatarPieceColorV4 = (typeof AVATAR_PIECE_COLORS_V4)[number];
|
|
48
|
+
/** A single strand of hair standing up from the crown (asset version 4). */
|
|
49
|
+
declare const AVATAR_STRANDS: readonly ["ahoge", "curl", "double"];
|
|
50
|
+
type AvatarStrand = (typeof AVATAR_STRANDS)[number];
|
|
51
|
+
/**
|
|
52
|
+
* Built-in headpieces (asset version 4), worn in the headpiece slot alongside an accessory and
|
|
53
|
+
* drawn partly behind the hair. The first five were accessories in earlier versions.
|
|
54
|
+
*/
|
|
55
|
+
declare const AVATAR_HEADPIECES: readonly ["catears", "bunnyears", "horseears", "horns", "halo", "wings"];
|
|
56
|
+
type AvatarHeadpiece = (typeof AVATAR_HEADPIECES)[number];
|
|
57
|
+
type V4Key = AvatarPieceColorV4 | 'strand';
|
|
42
58
|
/** Part choices that exist only in asset version 2, added after every version 1 choice. */
|
|
43
59
|
declare const AVATAR_PARTS_V2: {
|
|
44
60
|
readonly outfit: readonly ["tee", "shirttie", "hoodie", "turtleneck", "sailor", "blazer", "overalls", "dress", "kimono", "cardigan", "maid", "jacket", "armor", "robe", "tunic", "cloak"];
|
|
@@ -72,7 +88,7 @@ type PixelAvatarRecipeV1 = Meta & {
|
|
|
72
88
|
} & { [P in AvatarPart]: (typeof AVATAR_PARTS)[P][number] } & { [P in AvatarHairPart]?: (typeof AVATAR_PARTS)['hair'][number] } & { [P in AvatarRange]?: number } & {
|
|
73
89
|
species?: never;
|
|
74
90
|
rightSideHair?: never;
|
|
75
|
-
} & { [P in AvatarPieceColor | AvatarExtraPart]?: never } & { [P in CustomPartKey]?: never };
|
|
91
|
+
} & { [P in AvatarPieceColor | AvatarExtraPart]?: never } & { [P in CustomPartKey]?: never } & { [P in V4Key]?: never };
|
|
76
92
|
/**
|
|
77
93
|
* Asset version 2: a species, the full split hair and geometry, a separate right side hair
|
|
78
94
|
* (`sideHair` is then the left side) and optional per-side hair colors.
|
|
@@ -82,7 +98,7 @@ type PixelAvatarRecipeV2 = Meta & {
|
|
|
82
98
|
} & { [P in AvatarPart]: (typeof AVATAR_PARTS_V2)[P][number] } & { [P in AvatarHairPart]: (typeof AVATAR_PARTS)['hair'][number] } & { [P in AvatarRange]: number } & {
|
|
83
99
|
species: AvatarSpecies;
|
|
84
100
|
rightSideHair: (typeof AVATAR_HAIR_PARTS)['sideHair'][number];
|
|
85
|
-
} & { [P in AvatarPieceColor]?: string } & { [P in AvatarExtraPart]?: (typeof AVATAR_EXTRA_PARTS)[P][number] } & { [P in CustomPartKey]?: never };
|
|
101
|
+
} & { [P in AvatarPieceColor]?: string } & { [P in AvatarExtraPart]?: (typeof AVATAR_EXTRA_PARTS)[P][number] } & { [P in CustomPartKey]?: never } & { [P in V4Key]?: never };
|
|
86
102
|
/** The recipe key that embeds the Custom Part worn in each slot. */
|
|
87
103
|
declare const CUSTOM_PART_KEYS: {
|
|
88
104
|
readonly headpiece: "headpiece";
|
|
@@ -107,7 +123,18 @@ type CustomPartKey = (typeof CUSTOM_PART_KEYS)[PartSlot];
|
|
|
107
123
|
type PixelAvatarRecipeV3 = Omit<PixelAvatarRecipeV2, 'assetVersion' | CustomPartKey> & {
|
|
108
124
|
assetVersion: 3;
|
|
109
125
|
} & { [P in CustomPartKey]?: PixelCustomPart };
|
|
110
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Asset version 4: version 2 with a color for every hair piece, a single `strand`, and the
|
|
128
|
+
* headpiece slot holding either a built-in headpiece or a Custom Part, with any number of
|
|
129
|
+
* Custom Parts worn (none included).
|
|
130
|
+
*/
|
|
131
|
+
type PixelAvatarRecipeV4 = Omit<PixelAvatarRecipeV2, 'assetVersion' | CustomPartKey | V4Key> & {
|
|
132
|
+
assetVersion: 4;
|
|
133
|
+
} & { [P in Exclude<CustomPartKey, 'headpiece'>]?: PixelCustomPart } & {
|
|
134
|
+
headpiece?: PixelCustomPart | AvatarHeadpiece;
|
|
135
|
+
strand?: AvatarStrand;
|
|
136
|
+
} & { [P in AvatarPieceColorV4]?: string };
|
|
137
|
+
type PixelAvatarRecipe = PixelAvatarRecipeV1 | PixelAvatarRecipeV2 | PixelAvatarRecipeV3 | PixelAvatarRecipeV4;
|
|
111
138
|
declare const DEFAULT_RECIPE: PixelAvatarRecipeV1;
|
|
112
139
|
declare function isPixelAvatarRecipe(value: unknown): value is PixelAvatarRecipe;
|
|
113
140
|
declare function canonicalRecipe(recipe: PixelAvatarRecipe): PixelAvatarRecipe;
|
|
@@ -123,16 +150,29 @@ declare function hiddenChoices(recipe: PixelAvatarRecipe): readonly string[];
|
|
|
123
150
|
* When the skin color is one of the previous species' suggested colors, it moves to the new
|
|
124
151
|
* species' first suggestion; a custom color is kept.
|
|
125
152
|
*/
|
|
153
|
+
declare function withSpecies(recipe: PixelAvatarRecipeV4, species: AvatarSpecies): PixelAvatarRecipeV4;
|
|
126
154
|
declare function withSpecies(recipe: PixelAvatarRecipeV3, species: AvatarSpecies): PixelAvatarRecipeV3;
|
|
127
155
|
declare function withSpecies(recipe: PixelAvatarRecipeV1 | PixelAvatarRecipeV2, species: AvatarSpecies): PixelAvatarRecipeV2;
|
|
128
|
-
declare function withSpecies(recipe: PixelAvatarRecipe, species: AvatarSpecies): PixelAvatarRecipeV2 | PixelAvatarRecipeV3;
|
|
156
|
+
declare function withSpecies(recipe: PixelAvatarRecipe, species: AvatarSpecies): PixelAvatarRecipeV2 | PixelAvatarRecipeV3 | PixelAvatarRecipeV4;
|
|
129
157
|
/**
|
|
130
158
|
* Returns the recipe wearing `part` in `slot`, or with that slot's part taken off. The recipe
|
|
131
|
-
* embeds its own copy; it is asset version 3 while it wears any part and version 2 otherwise
|
|
159
|
+
* embeds its own copy; it is asset version 3 while it wears any part and version 2 otherwise,
|
|
160
|
+
* and a version 4 recipe stays version 4.
|
|
132
161
|
*/
|
|
133
|
-
declare function withCustomPart(recipe:
|
|
162
|
+
declare function withCustomPart(recipe: PixelAvatarRecipeV4, slot: PartSlot, part: PixelCustomPart | undefined): PixelAvatarRecipeV4;
|
|
163
|
+
declare function withCustomPart(recipe: PixelAvatarRecipe, slot: PartSlot, part: PixelCustomPart | undefined): PixelAvatarRecipeV2 | PixelAvatarRecipeV3 | PixelAvatarRecipeV4;
|
|
164
|
+
/**
|
|
165
|
+
* Returns the recipe as asset version 4, keeping every choice. An accessory that is now a
|
|
166
|
+
* headpiece moves into the empty headpiece slot, freeing the accessory slot; call this only when
|
|
167
|
+
* the Human edits, so saved recipes keep rendering as they were.
|
|
168
|
+
*/
|
|
169
|
+
declare function withPieces(recipe: PixelAvatarRecipe): PixelAvatarRecipeV4;
|
|
170
|
+
/** Returns the recipe as asset version 4 wearing a built-in headpiece, or none. */
|
|
171
|
+
declare function withBuiltInHeadpiece(recipe: PixelAvatarRecipe, headpiece: AvatarHeadpiece | undefined): PixelAvatarRecipeV4;
|
|
172
|
+
/** The built-in headpiece worn, if any. */
|
|
173
|
+
declare function builtInHeadpiece(recipe: PixelAvatarRecipe): AvatarHeadpiece | undefined;
|
|
134
174
|
/** `withCustomPart` for the headpiece slot. */
|
|
135
|
-
declare function withHeadpiece(recipe: PixelAvatarRecipe, part: PixelCustomPart | undefined): PixelAvatarRecipeV2 | PixelAvatarRecipeV3;
|
|
175
|
+
declare function withHeadpiece(recipe: PixelAvatarRecipe, part: PixelCustomPart | undefined): PixelAvatarRecipeV2 | PixelAvatarRecipeV3 | PixelAvatarRecipeV4;
|
|
136
176
|
/** The Custom Part worn in a slot, if any. */
|
|
137
177
|
declare function wornPart(recipe: PixelAvatarRecipe, slot: PartSlot): PixelCustomPart | undefined;
|
|
138
178
|
declare function detailedRecipe(recipe: PixelAvatarRecipe): PixelAvatarRecipe;
|
|
@@ -330,6 +370,16 @@ declare function hairPieceStart(recipe: PixelAvatarRecipe, slot: HairPartSlot):
|
|
|
330
370
|
front: PartCell[];
|
|
331
371
|
back: PartCell[];
|
|
332
372
|
};
|
|
373
|
+
/**
|
|
374
|
+
* The headpiece as a Custom Part to start drawing from: the drawn headpiece worn, or the
|
|
375
|
+
* built-in headpiece flattened for this recipe in the front pose, its base on the back layer and
|
|
376
|
+
* the rest on the front layer, so an unchanged copy renders identically facing front.
|
|
377
|
+
*/
|
|
378
|
+
declare function headpieceStart(recipe: PixelAvatarRecipe): {
|
|
379
|
+
slot: 'headpiece';
|
|
380
|
+
front: PartCell[];
|
|
381
|
+
back: PartCell[];
|
|
382
|
+
};
|
|
333
383
|
/**
|
|
334
384
|
* The part in a replacement slot as a Custom Part to start drawing from: the drawn part worn
|
|
335
385
|
* there, or the built-in part flattened for this recipe in the front pose. Each pixel becomes a
|
|
@@ -391,5 +441,5 @@ declare function pixelSymbolCells(symbol: PixelSymbol, color: string): PixelCell
|
|
|
391
441
|
*/
|
|
392
442
|
declare function symbolArtCells(rows: readonly string[], color: string): PixelCell$1[];
|
|
393
443
|
//#endregion
|
|
394
|
-
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, ditherThreshold, emptyPartLayer, faceCells, fillPartLayer, gradientPartLayer, hairPieceStart, hiddenChoices, isHairPartSlot, isPixelAvatarRecipe, isPixelCustomPart, isReplacePartSlot, mirrorPartX, noisePartLayer, paintPartLayer, partCells, partLayer, partLinePoints, partRectPoints, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, replacePartStart, seededRandom, seededRecipe, seededRecipeV2, shadePartLayer, symbolArtCells, withCustomPart, withHeadpiece, withSpecies, wornPart };
|
|
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 };
|
|
395
445
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -333,6 +333,33 @@ const AVATAR_SPECIES = [
|
|
|
333
333
|
];
|
|
334
334
|
/** Optional per-piece hair colors (asset version 2); an absent piece uses `hairColor`. */
|
|
335
335
|
const AVATAR_PIECE_COLORS = ["leftSideHairColor", "rightSideHairColor"];
|
|
336
|
+
/**
|
|
337
|
+
* Asset version 4 adds a color for every other hair piece; an absent piece uses `hairColor`.
|
|
338
|
+
* `strandColor` colors the single strand (`strand`).
|
|
339
|
+
*/
|
|
340
|
+
const AVATAR_PIECE_COLORS_V4 = [
|
|
341
|
+
"bangsColor",
|
|
342
|
+
"backHairColor",
|
|
343
|
+
"strandColor"
|
|
344
|
+
];
|
|
345
|
+
/** A single strand of hair standing up from the crown (asset version 4). */
|
|
346
|
+
const AVATAR_STRANDS = [
|
|
347
|
+
"ahoge",
|
|
348
|
+
"curl",
|
|
349
|
+
"double"
|
|
350
|
+
];
|
|
351
|
+
/**
|
|
352
|
+
* Built-in headpieces (asset version 4), worn in the headpiece slot alongside an accessory and
|
|
353
|
+
* drawn partly behind the hair. The first five were accessories in earlier versions.
|
|
354
|
+
*/
|
|
355
|
+
const AVATAR_HEADPIECES = [
|
|
356
|
+
"catears",
|
|
357
|
+
"bunnyears",
|
|
358
|
+
"horseears",
|
|
359
|
+
"horns",
|
|
360
|
+
"halo",
|
|
361
|
+
"wings"
|
|
362
|
+
];
|
|
336
363
|
/** Part choices that exist only in asset version 2, added after every version 1 choice. */
|
|
337
364
|
const AVATAR_PARTS_V2 = {
|
|
338
365
|
...AVATAR_PARTS,
|
|
@@ -410,17 +437,22 @@ function isPixelAvatarRecipe(value) {
|
|
|
410
437
|
const r = value;
|
|
411
438
|
const detailed = DETAIL_KEYS.filter((key) => Object.hasOwn(r, key)).length;
|
|
412
439
|
if (detailed !== 0 && detailed !== DETAIL_KEYS.length) return false;
|
|
440
|
+
const v4 = r["assetVersion"] === 4;
|
|
413
441
|
const v3 = r["assetVersion"] === 3;
|
|
414
|
-
const v2 = r["assetVersion"] === 2 || v3;
|
|
415
|
-
const
|
|
416
|
-
if (
|
|
442
|
+
const v2 = r["assetVersion"] === 2 || v3 || v4;
|
|
443
|
+
const builtInHead = v4 && typeof r["headpiece"] === "string";
|
|
444
|
+
if (builtInHead && !AVATAR_HEADPIECES.includes(r["headpiece"])) return false;
|
|
445
|
+
const worn = PART_KEY_ENTRIES.filter(([slot, key]) => Object.hasOwn(r, key) && !(slot === "headpiece" && builtInHead));
|
|
446
|
+
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;
|
|
417
449
|
for (const [slot, key] of worn) {
|
|
418
450
|
const part = r[key];
|
|
419
451
|
if (!isPixelCustomPart(part) || part.slot !== slot) return false;
|
|
420
452
|
}
|
|
421
453
|
const pieces = AVATAR_PIECE_COLORS.filter((key) => Object.hasOwn(r, key));
|
|
422
454
|
const extras = Object.keys(AVATAR_EXTRA_PARTS).filter((key) => Object.hasOwn(r, key));
|
|
423
|
-
const v2Keys = v2 ? 2 + pieces.length + extras.length + worn.length : 0;
|
|
455
|
+
const v2Keys = v2 ? 2 + pieces.length + extras.length + worn.length + newer.length + (builtInHead ? 1 : 0) : 0;
|
|
424
456
|
if (Object.keys(r).length !== Object.keys(DEFAULT_RECIPE).length + detailed + v2Keys) return false;
|
|
425
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;
|
|
426
458
|
const catalog = v2 ? AVATAR_PARTS_V2 : AVATAR_PARTS;
|
|
@@ -445,9 +477,13 @@ function canonicalRecipe(recipe) {
|
|
|
445
477
|
canonical["rightSideHair"] = recipe.rightSideHair;
|
|
446
478
|
for (const key of AVATAR_PIECE_COLORS) if (recipe[key] !== void 0) canonical[key] = recipe[key].toLowerCase();
|
|
447
479
|
for (const key of Object.keys(AVATAR_EXTRA_PARTS)) if (recipe[key] !== void 0) canonical[key] = recipe[key];
|
|
448
|
-
if (recipe.assetVersion === 3) for (const [, key] of PART_KEY_ENTRIES) {
|
|
480
|
+
if (recipe.assetVersion === 3 || recipe.assetVersion === 4) for (const [, key] of PART_KEY_ENTRIES) {
|
|
449
481
|
const part = recipe[key];
|
|
450
|
-
if (part !== void 0) canonical[key] = canonicalCustomPart(part);
|
|
482
|
+
if (part !== void 0) canonical[key] = typeof part === "string" ? part : canonicalCustomPart(part);
|
|
483
|
+
}
|
|
484
|
+
if (recipe.assetVersion === 4) {
|
|
485
|
+
if (recipe.strand !== void 0) canonical["strand"] = recipe.strand;
|
|
486
|
+
for (const key of AVATAR_PIECE_COLORS_V4) if (recipe[key] !== void 0) canonical[key] = recipe[key].toLowerCase();
|
|
451
487
|
}
|
|
452
488
|
}
|
|
453
489
|
return canonical;
|
|
@@ -464,8 +500,12 @@ const HAIR_PIECES = [
|
|
|
464
500
|
* the species or headwear changes back.
|
|
465
501
|
*/
|
|
466
502
|
function hiddenChoices(recipe) {
|
|
503
|
+
const strand = recipe.strand && (recipe.species === "flower" || recipe.bangs !== "none") ? ["strand"] : [];
|
|
504
|
+
const bald = recipe.strand && recipe.bangs === "none" ? ["strand"] : [];
|
|
467
505
|
if (recipe.species === "flower") return [
|
|
468
506
|
...HAIR_PIECES,
|
|
507
|
+
...strand,
|
|
508
|
+
...typeof recipe.headpiece === "string" ? ["headpiece"] : [],
|
|
469
509
|
"outfit",
|
|
470
510
|
"accessory",
|
|
471
511
|
"eyes",
|
|
@@ -476,8 +516,9 @@ function hiddenChoices(recipe) {
|
|
|
476
516
|
...recipe.beard || wornPart(recipe, "beard") ? ["beard"] : []
|
|
477
517
|
];
|
|
478
518
|
const hidden = [];
|
|
479
|
-
if ((recipe.accessory === "helmet" || recipe.accessory === "hood") && !wornPart(recipe, "accessory")) hidden.push(...HAIR_PIECES);
|
|
519
|
+
if ((recipe.accessory === "helmet" || recipe.accessory === "hood") && !wornPart(recipe, "accessory")) hidden.push(...HAIR_PIECES, ...strand);
|
|
480
520
|
if (recipe.species === "dwarf") hidden.push("nose");
|
|
521
|
+
if (!hidden.includes("strand")) hidden.push(...bald);
|
|
481
522
|
return hidden;
|
|
482
523
|
}
|
|
483
524
|
function withSpecies(recipe, species) {
|
|
@@ -486,16 +527,12 @@ function withSpecies(recipe, species) {
|
|
|
486
527
|
const suggested = AVATAR_SPECIES_SWATCHES[previous].includes(recipe.skinColor.toLowerCase());
|
|
487
528
|
return {
|
|
488
529
|
...detailed,
|
|
489
|
-
assetVersion: recipe.assetVersion ===
|
|
530
|
+
assetVersion: recipe.assetVersion === 1 ? 2 : recipe.assetVersion,
|
|
490
531
|
species,
|
|
491
532
|
rightSideHair: detailed.rightSideHair ?? detailed.sideHair,
|
|
492
533
|
skinColor: suggested && previous !== species ? AVATAR_SPECIES_SWATCHES[species][0] : recipe.skinColor
|
|
493
534
|
};
|
|
494
535
|
}
|
|
495
|
-
/**
|
|
496
|
-
* Returns the recipe wearing `part` in `slot`, or with that slot's part taken off. The recipe
|
|
497
|
-
* embeds its own copy; it is asset version 3 while it wears any part and version 2 otherwise.
|
|
498
|
-
*/
|
|
499
536
|
function withCustomPart(recipe, slot, part) {
|
|
500
537
|
const base = { ...recipe.assetVersion === 1 ? withSpecies(recipe, "human") : recipe };
|
|
501
538
|
const key = CUSTOM_PART_KEYS[slot];
|
|
@@ -504,19 +541,51 @@ function withCustomPart(recipe, slot, part) {
|
|
|
504
541
|
...part,
|
|
505
542
|
slot
|
|
506
543
|
});
|
|
544
|
+
if (recipe.assetVersion === 4) return base;
|
|
507
545
|
const wearing = PART_KEY_ENTRIES.some(([, k]) => base[k] !== void 0);
|
|
508
546
|
return {
|
|
509
547
|
...base,
|
|
510
548
|
assetVersion: wearing ? 3 : 2
|
|
511
549
|
};
|
|
512
550
|
}
|
|
551
|
+
const MOVED_HEADPIECES = AVATAR_HEADPIECES;
|
|
552
|
+
/**
|
|
553
|
+
* Returns the recipe as asset version 4, keeping every choice. An accessory that is now a
|
|
554
|
+
* headpiece moves into the empty headpiece slot, freeing the accessory slot; call this only when
|
|
555
|
+
* the Human edits, so saved recipes keep rendering as they were.
|
|
556
|
+
*/
|
|
557
|
+
function withPieces(recipe) {
|
|
558
|
+
if (recipe.assetVersion === 4) return recipe;
|
|
559
|
+
const base = {
|
|
560
|
+
...recipe.assetVersion === 1 ? withSpecies(recipe, "human") : recipe,
|
|
561
|
+
assetVersion: 4
|
|
562
|
+
};
|
|
563
|
+
if (base["headpiece"] === void 0 && MOVED_HEADPIECES.includes(recipe.accessory)) {
|
|
564
|
+
base["headpiece"] = recipe.accessory;
|
|
565
|
+
base["accessory"] = "none";
|
|
566
|
+
}
|
|
567
|
+
return base;
|
|
568
|
+
}
|
|
569
|
+
/** Returns the recipe as asset version 4 wearing a built-in headpiece, or none. */
|
|
570
|
+
function withBuiltInHeadpiece(recipe, headpiece) {
|
|
571
|
+
const base = { ...withPieces(recipe) };
|
|
572
|
+
delete base["headpiece"];
|
|
573
|
+
if (headpiece !== void 0) base["headpiece"] = headpiece;
|
|
574
|
+
return base;
|
|
575
|
+
}
|
|
576
|
+
/** The built-in headpiece worn, if any. */
|
|
577
|
+
function builtInHeadpiece(recipe) {
|
|
578
|
+
return typeof recipe.headpiece === "string" ? recipe.headpiece : void 0;
|
|
579
|
+
}
|
|
513
580
|
/** `withCustomPart` for the headpiece slot. */
|
|
514
581
|
function withHeadpiece(recipe, part) {
|
|
515
582
|
return withCustomPart(recipe, "headpiece", part);
|
|
516
583
|
}
|
|
517
584
|
/** The Custom Part worn in a slot, if any. */
|
|
518
585
|
function wornPart(recipe, slot) {
|
|
519
|
-
|
|
586
|
+
if (recipe.assetVersion !== 3 && recipe.assetVersion !== 4) return void 0;
|
|
587
|
+
const part = recipe[CUSTOM_PART_KEYS[slot]];
|
|
588
|
+
return typeof part === "string" ? void 0 : part;
|
|
520
589
|
}
|
|
521
590
|
function detailedRecipe(recipe) {
|
|
522
591
|
if (recipe.bangs !== void 0) return recipe;
|
|
@@ -1659,6 +1728,39 @@ function hair(recipe, s, b) {
|
|
|
1659
1728
|
};
|
|
1660
1729
|
}
|
|
1661
1730
|
}
|
|
1731
|
+
const STRANDS = {
|
|
1732
|
+
ahoge: {
|
|
1733
|
+
rows: [
|
|
1734
|
+
"XX.",
|
|
1735
|
+
".XX",
|
|
1736
|
+
".XX"
|
|
1737
|
+
],
|
|
1738
|
+
dx: 0
|
|
1739
|
+
},
|
|
1740
|
+
curl: {
|
|
1741
|
+
rows: [
|
|
1742
|
+
".XX.",
|
|
1743
|
+
"X..X",
|
|
1744
|
+
"..XX",
|
|
1745
|
+
".X.."
|
|
1746
|
+
],
|
|
1747
|
+
dx: -1
|
|
1748
|
+
},
|
|
1749
|
+
double: {
|
|
1750
|
+
rows: [
|
|
1751
|
+
"X...X",
|
|
1752
|
+
".X.X.",
|
|
1753
|
+
".X.X."
|
|
1754
|
+
],
|
|
1755
|
+
dx: -2
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
function strandMask(style, s) {
|
|
1759
|
+
const { rows, dx } = STRANDS[style];
|
|
1760
|
+
const top = 4 - rows.length;
|
|
1761
|
+
const left = Math.round(C + s) + dx;
|
|
1762
|
+
return (x, y) => rows[y - top]?.[x - left] === "X";
|
|
1763
|
+
}
|
|
1662
1764
|
function detailedHair(recipe, s, b) {
|
|
1663
1765
|
const of = (style) => hair({
|
|
1664
1766
|
...recipe,
|
|
@@ -1673,8 +1775,11 @@ function detailedHair(recipe, s, b) {
|
|
|
1673
1775
|
const sideOf = (sign) => (x, y) => y > 7 && sign * (x + .5 - C - s) >= 7 && lengthen((sign < 0 ? side : right).front)(x, y);
|
|
1674
1776
|
const fringe = (x, y) => y <= 7 ? bangs.front(x, y) || back.front(x, y) : Math.abs(x + .5 - C - s) < 7 && bangs.front(x, y);
|
|
1675
1777
|
const behind = length === 0 ? back.back : lengthen(back.back);
|
|
1778
|
+
const tuft = recipe.strand && recipe.bangs !== "none" ? strandMask(recipe.strand, s) : void 0;
|
|
1779
|
+
const strand = tuft ? (x, y) => tuft(x, y) && !fringe(x, y) && !sideOf(-1)(x, y) && !sideOf(1)(x, y) : void 0;
|
|
1676
1780
|
return {
|
|
1677
|
-
front: (x, y) => fringe(x, y) || sideOf(-1)(x, y) || sideOf(1)(x, y),
|
|
1781
|
+
front: (x, y) => fringe(x, y) || sideOf(-1)(x, y) || sideOf(1)(x, y) || (strand?.(x, y) ?? false),
|
|
1782
|
+
...strand ? { strand } : {},
|
|
1678
1783
|
sides: [sideOf(-1), sideOf(1)],
|
|
1679
1784
|
pieces: {
|
|
1680
1785
|
bangs: fringe,
|
|
@@ -2019,6 +2124,87 @@ function accessory(recipe, g, s, face) {
|
|
|
2019
2124
|
return;
|
|
2020
2125
|
}
|
|
2021
2126
|
}
|
|
2127
|
+
/**
|
|
2128
|
+
* A built-in headpiece: the row that meets the head goes behind the hair, the rest in front.
|
|
2129
|
+
* Turned, the far side of a pair is hidden, like a turned ear.
|
|
2130
|
+
*/
|
|
2131
|
+
function drawHeadpiece(style, recipe, behind, front, s, far) {
|
|
2132
|
+
const cx = Math.round(C + s);
|
|
2133
|
+
const pair = (rows, left, right, y, pal, mirror = true) => {
|
|
2134
|
+
const tips = rows.slice(0, -1);
|
|
2135
|
+
if (far >= 0) {
|
|
2136
|
+
sprite(behind, rows, cx + left, y, pal);
|
|
2137
|
+
sprite(front, tips, cx + left, y, pal);
|
|
2138
|
+
}
|
|
2139
|
+
if (far <= 0) {
|
|
2140
|
+
sprite(behind, rows, cx + right, y, pal, mirror);
|
|
2141
|
+
sprite(front, tips, cx + right, y, pal, mirror);
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
switch (style) {
|
|
2145
|
+
case "catears":
|
|
2146
|
+
pair([
|
|
2147
|
+
"X...",
|
|
2148
|
+
"XX..",
|
|
2149
|
+
"XPX.",
|
|
2150
|
+
"XXXX"
|
|
2151
|
+
], -9, 5, 2, {
|
|
2152
|
+
X: recipe.hairColor,
|
|
2153
|
+
P: "#f4a3b5"
|
|
2154
|
+
});
|
|
2155
|
+
return;
|
|
2156
|
+
case "bunnyears":
|
|
2157
|
+
pair([
|
|
2158
|
+
"WW",
|
|
2159
|
+
"WP",
|
|
2160
|
+
"WP",
|
|
2161
|
+
"WP",
|
|
2162
|
+
"WW"
|
|
2163
|
+
], -6, 4, 0, {
|
|
2164
|
+
W: "#f4f1ec",
|
|
2165
|
+
P: "#f4a3b5"
|
|
2166
|
+
});
|
|
2167
|
+
return;
|
|
2168
|
+
case "horseears":
|
|
2169
|
+
pair([
|
|
2170
|
+
".X.",
|
|
2171
|
+
"XPX",
|
|
2172
|
+
"XPX",
|
|
2173
|
+
"XXX"
|
|
2174
|
+
], -9, 6, 1, {
|
|
2175
|
+
X: shade(recipe.hairColor, .9),
|
|
2176
|
+
P: "#f4a3b5"
|
|
2177
|
+
}, false);
|
|
2178
|
+
return;
|
|
2179
|
+
case "horns":
|
|
2180
|
+
pair([
|
|
2181
|
+
".H",
|
|
2182
|
+
"HH",
|
|
2183
|
+
"Hh"
|
|
2184
|
+
], -8, 6, 1, {
|
|
2185
|
+
H: "#5b3a6e",
|
|
2186
|
+
h: "#7d5694"
|
|
2187
|
+
});
|
|
2188
|
+
return;
|
|
2189
|
+
case "halo": {
|
|
2190
|
+
const ring = (x, y) => ellipse(C + s, 1.6, 6.5, 1.8)(x, y) && !ellipse(C + s, 1.6, 4.4, .8)(x, y);
|
|
2191
|
+
paint(behind, (x, y) => ring(x, y) && y < 2, GOLD);
|
|
2192
|
+
paint(front, (x, y) => ring(x, y) && y >= 2, GOLD);
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
case "wings":
|
|
2196
|
+
pair([
|
|
2197
|
+
"W..",
|
|
2198
|
+
"WW.",
|
|
2199
|
+
"WwW",
|
|
2200
|
+
".ww"
|
|
2201
|
+
], -13, 10, 7, {
|
|
2202
|
+
W: "#f4f1ec",
|
|
2203
|
+
w: "#cfd6e2"
|
|
2204
|
+
});
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2022
2208
|
const POINTED_EARS = {
|
|
2023
2209
|
goblin: {
|
|
2024
2210
|
rows: [
|
|
@@ -2205,7 +2391,7 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2205
2391
|
};
|
|
2206
2392
|
const masks = drawnHair.length === 0 ? builtin : {
|
|
2207
2393
|
...builtin,
|
|
2208
|
-
front: (x, y) => piece("bangs")(x, y) || piece("leftSideHair")(x, y) || piece("rightSideHair")(x, y),
|
|
2394
|
+
front: (x, y) => piece("bangs")(x, y) || piece("leftSideHair")(x, y) || piece("rightSideHair")(x, y) || (builtin.strand?.(x, y) ?? false),
|
|
2209
2395
|
sides: [piece("leftSideHair"), piece("rightSideHair")],
|
|
2210
2396
|
back: piece("backHair"),
|
|
2211
2397
|
...drawnHair.some((drawn) => drawn.slot === "backHair") ? { ties: [] } : {}
|
|
@@ -2244,6 +2430,9 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2244
2430
|
placeCells(back, inked("backHair"), false);
|
|
2245
2431
|
overlay(back, "petals");
|
|
2246
2432
|
if (headpiece) placeCells(back, headpiece.back);
|
|
2433
|
+
const wornHead = flower ? void 0 : builtInHeadpiece(recipe);
|
|
2434
|
+
const headFront = blank();
|
|
2435
|
+
if (wornHead) drawHeadpiece(wornHead, recipe, back, headFront, s, turned ? d : 0);
|
|
2247
2436
|
const head = blank();
|
|
2248
2437
|
paint(head, face, skin);
|
|
2249
2438
|
const h = recipe.height ?? 0;
|
|
@@ -2295,8 +2484,10 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2295
2484
|
const shadeHair = (g) => {
|
|
2296
2485
|
const out = g.map((r) => [...r]);
|
|
2297
2486
|
const cells = g.flatMap((row, y) => row.flatMap((c, x) => c === hairColor ? [[x, y]] : []));
|
|
2298
|
-
const
|
|
2299
|
-
const
|
|
2487
|
+
const framed = cells.filter(([x, y]) => !builtin.strand?.(x, y));
|
|
2488
|
+
const bounds = framed.length ? framed : cells;
|
|
2489
|
+
const xs = bounds.map(([x]) => x);
|
|
2490
|
+
const ys = bounds.map(([, y]) => y);
|
|
2300
2491
|
const left = Math.min(...xs);
|
|
2301
2492
|
const right = Math.max(...xs);
|
|
2302
2493
|
const top = Math.min(...ys);
|
|
@@ -2325,7 +2516,7 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2325
2516
|
}
|
|
2326
2517
|
const ring = mix$1(hairColor, WHITE, .45);
|
|
2327
2518
|
for (let x = Math.round(C + s) - 7; x <= Math.round(C + s) + 3; x++) {
|
|
2328
|
-
const first = g.findIndex((row) => row[x] === hairColor);
|
|
2519
|
+
const first = g.findIndex((row, y) => row[x] === hairColor && !builtin.strand?.(x, y));
|
|
2329
2520
|
if (first < 0) continue;
|
|
2330
2521
|
const y = first + 2 + Math.round(Math.abs(x + .5 - (C + s - 2)) / 4);
|
|
2331
2522
|
if (g[y]?.[x] === hairColor && x % 3 !== 0) out[y][x] = ring;
|
|
@@ -2338,6 +2529,7 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2338
2529
|
const crown = blank();
|
|
2339
2530
|
placeCells(crown, inked("all-front"), false);
|
|
2340
2531
|
if (headpiece) placeCells(crown, headpiece.front);
|
|
2532
|
+
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (headFront[y][x]) crown[y][x] = headFront[y][x];
|
|
2341
2533
|
const figure = blank();
|
|
2342
2534
|
for (const layer of [
|
|
2343
2535
|
back,
|
|
@@ -2475,17 +2667,42 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2475
2667
|
], right.x + right.w, eyeTop + 5, { G: GOLD });
|
|
2476
2668
|
}
|
|
2477
2669
|
const full = clip(outline(figure));
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
const
|
|
2481
|
-
if (
|
|
2670
|
+
const variants = /* @__PURE__ */ new Map();
|
|
2671
|
+
const recolor = (color) => {
|
|
2672
|
+
const cached = variants.get(color.toLowerCase());
|
|
2673
|
+
if (cached) return cached;
|
|
2482
2674
|
const variant = blank();
|
|
2675
|
+
variants.set(color.toLowerCase(), variant);
|
|
2483
2676
|
const plain = {
|
|
2484
2677
|
...recipe,
|
|
2485
2678
|
hairColor: color
|
|
2486
2679
|
};
|
|
2487
|
-
for (const k of AVATAR_PIECE_COLORS) delete plain[k];
|
|
2680
|
+
for (const k of [...AVATAR_PIECE_COLORS, ...AVATAR_PIECE_COLORS_V4]) delete plain[k];
|
|
2488
2681
|
for (const cell of renderFigure(plain, yawDeg, {}, omit).cells) variant[cell.y][cell.x] = cell.c;
|
|
2682
|
+
return variant;
|
|
2683
|
+
};
|
|
2684
|
+
if (recipe.assetVersion === 4) {
|
|
2685
|
+
const over = (x, y) => !!crown[y]?.[x] || !!drawnOver[y]?.[x] || !!eyes[y]?.[x] || !!glasses[y]?.[x];
|
|
2686
|
+
const hairAt = (x, y) => head[y]?.[x] === hairColor && !over(x, y);
|
|
2687
|
+
const backAt = (x, y) => back[y]?.[x] !== void 0 && figure[y][x] === back[y][x];
|
|
2688
|
+
const owners = [
|
|
2689
|
+
[flower ? void 0 : recipe.backHairColor, (x, y) => masks.back(x, y) && backAt(x, y)],
|
|
2690
|
+
[recipe.leftSideHairColor, (x, y) => !!masks.sides?.[0](x, y) && (hairAt(x, y) || backAt(x, y))],
|
|
2691
|
+
[recipe.rightSideHairColor, (x, y) => !!masks.sides?.[1](x, y) && (hairAt(x, y) || backAt(x, y))],
|
|
2692
|
+
[recipe.bangsColor, (x, y) => (builtin.pieces ? piece("bangs")(x, y) : false) && hairAt(x, y)],
|
|
2693
|
+
[recipe.strandColor, (x, y) => (builtin.strand?.(x, y) ?? false) && hairAt(x, y)]
|
|
2694
|
+
];
|
|
2695
|
+
for (const [color, owns] of owners) {
|
|
2696
|
+
if (color === void 0 || color.toLowerCase() === hairColor.toLowerCase()) continue;
|
|
2697
|
+
const variant = recolor(color);
|
|
2698
|
+
const near = (x, y) => owns(x - 1, y) || owns(x + 1, y) || owns(x, y - 1) || owns(x, y + 1);
|
|
2699
|
+
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (full[y][x] && !over(x, y) && (owns(x, y) || !figure[y][x] && near(x, y))) full[y][x] = variant[y][x];
|
|
2700
|
+
}
|
|
2701
|
+
} else AVATAR_PIECE_COLORS.forEach((key, i) => {
|
|
2702
|
+
const color = recipe[key];
|
|
2703
|
+
const mask = masks.sides?.[i];
|
|
2704
|
+
if (color === void 0 || !mask || color.toLowerCase() === hairColor.toLowerCase()) return;
|
|
2705
|
+
const variant = recolor(color);
|
|
2489
2706
|
const near = (x, y) => mask(x, y) || mask(x - 1, y) || mask(x + 1, y) || mask(x, y - 1) || mask(x, y + 1);
|
|
2490
2707
|
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];
|
|
2491
2708
|
});
|
|
@@ -2560,6 +2777,57 @@ function hairPieceStart(recipe, slot) {
|
|
|
2560
2777
|
back: []
|
|
2561
2778
|
};
|
|
2562
2779
|
}
|
|
2780
|
+
function cellSource(recipe) {
|
|
2781
|
+
return (c) => {
|
|
2782
|
+
for (const key of AVATAR_COLORS) for (const tone of [
|
|
2783
|
+
0,
|
|
2784
|
+
-1,
|
|
2785
|
+
1,
|
|
2786
|
+
-2,
|
|
2787
|
+
2
|
|
2788
|
+
]) if (TONES[tone](recipe[key].toLowerCase()) === c) return [key, tone];
|
|
2789
|
+
return [c, 0];
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
/**
|
|
2793
|
+
* The headpiece as a Custom Part to start drawing from: the drawn headpiece worn, or the
|
|
2794
|
+
* built-in headpiece flattened for this recipe in the front pose, its base on the back layer and
|
|
2795
|
+
* the rest on the front layer, so an unchanged copy renders identically facing front.
|
|
2796
|
+
*/
|
|
2797
|
+
function headpieceStart(recipe) {
|
|
2798
|
+
const worn = wornPart(recipe, "headpiece");
|
|
2799
|
+
if (worn) return {
|
|
2800
|
+
slot: "headpiece",
|
|
2801
|
+
front: [...worn.front],
|
|
2802
|
+
back: [...worn.back]
|
|
2803
|
+
};
|
|
2804
|
+
const style = builtInHeadpiece(recipe);
|
|
2805
|
+
const behind = blank();
|
|
2806
|
+
const over = blank();
|
|
2807
|
+
if (style) drawHeadpiece(style, recipe, behind, over, 0, 0);
|
|
2808
|
+
const source = cellSource(recipe);
|
|
2809
|
+
const front = [];
|
|
2810
|
+
const back = [];
|
|
2811
|
+
for (let y = 0; y < 16; y++) for (let x = 0; x < N; x++) {
|
|
2812
|
+
const c = over[y][x];
|
|
2813
|
+
const b = behind[y][x];
|
|
2814
|
+
if (c) front.push([
|
|
2815
|
+
x,
|
|
2816
|
+
y,
|
|
2817
|
+
...source(c)
|
|
2818
|
+
]);
|
|
2819
|
+
if (b && b !== c) back.push([
|
|
2820
|
+
x,
|
|
2821
|
+
y,
|
|
2822
|
+
...source(b)
|
|
2823
|
+
]);
|
|
2824
|
+
}
|
|
2825
|
+
return {
|
|
2826
|
+
slot: "headpiece",
|
|
2827
|
+
front,
|
|
2828
|
+
back
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2563
2831
|
/**
|
|
2564
2832
|
* The part in a replacement slot as a Custom Part to start drawing from: the drawn part worn
|
|
2565
2833
|
* there, or the built-in part flattened for this recipe in the front pose. Each pixel becomes a
|
|
@@ -2577,16 +2845,7 @@ function replacePartStart(recipe, slot) {
|
|
|
2577
2845
|
const layer = slot === "glasses" ? "glasses" : slot === "nose" || slot === "cheeks" ? "features" : slot === "petals" ? "back" : slot === "outfit" || slot === "flowerBase" ? "body" : "figure";
|
|
2578
2846
|
const shown = renderFigure(recipe, 0, {}, /* @__PURE__ */ new Set()).grids[layer];
|
|
2579
2847
|
const without = renderFigure(recipe, 0, {}, new Set([slot])).grids[layer];
|
|
2580
|
-
const source = (
|
|
2581
|
-
for (const key of AVATAR_COLORS) for (const tone of [
|
|
2582
|
-
0,
|
|
2583
|
-
-1,
|
|
2584
|
-
1,
|
|
2585
|
-
-2,
|
|
2586
|
-
2
|
|
2587
|
-
]) if (TONES[tone](recipe[key].toLowerCase()) === c) return [key, tone];
|
|
2588
|
-
return [c, 0];
|
|
2589
|
-
};
|
|
2848
|
+
const source = cellSource(recipe);
|
|
2590
2849
|
const front = [];
|
|
2591
2850
|
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
|
|
2592
2851
|
const c = shown[y][x];
|
|
@@ -3125,5 +3384,5 @@ function symbolArtCells(rows, color) {
|
|
|
3125
3384
|
}
|
|
3126
3385
|
|
|
3127
3386
|
//#endregion
|
|
3128
|
-
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, ditherThreshold, emptyPartLayer, faceCells, fillPartLayer, gradientPartLayer, hairPieceStart, hiddenChoices, isHairPartSlot, isPixelAvatarRecipe, isPixelCustomPart, isReplacePartSlot, mirrorPartX, noisePartLayer, paintPartLayer, partCells, partLayer, partLinePoints, partRectPoints, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, replacePartStart, seededRandom, seededRecipe, seededRecipeV2, shadePartLayer, symbolArtCells, withCustomPart, withHeadpiece, withSpecies, wornPart };
|
|
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 };
|
|
3129
3388
|
//# sourceMappingURL=index.js.map
|