@botharness/pixel-avatar 0.8.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 +2 -0
- package/dist/index.d.ts +82 -11
- package/dist/index.js +492 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -331,8 +331,55 @@ 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"];
|
|
356
|
+
/**
|
|
357
|
+
* Asset version 4 adds a color for every other hair piece; an absent piece uses `hairColor`.
|
|
358
|
+
* `strandColor` colors the single strand (`strand`).
|
|
359
|
+
*/
|
|
360
|
+
const AVATAR_PIECE_COLORS_V4 = [
|
|
361
|
+
"bangsColor",
|
|
362
|
+
"backHairColor",
|
|
363
|
+
"strandColor"
|
|
364
|
+
];
|
|
365
|
+
/** A single strand of hair standing up from the crown (asset version 4). */
|
|
366
|
+
const AVATAR_STRANDS = [
|
|
367
|
+
"ahoge",
|
|
368
|
+
"curl",
|
|
369
|
+
"double"
|
|
370
|
+
];
|
|
371
|
+
/**
|
|
372
|
+
* Built-in headpieces (asset version 4), worn in the headpiece slot alongside an accessory and
|
|
373
|
+
* drawn partly behind the hair. The first five were accessories in earlier versions.
|
|
374
|
+
*/
|
|
375
|
+
const AVATAR_HEADPIECES = [
|
|
376
|
+
"catears",
|
|
377
|
+
"bunnyears",
|
|
378
|
+
"horseears",
|
|
379
|
+
"horns",
|
|
380
|
+
"halo",
|
|
381
|
+
"wings"
|
|
382
|
+
];
|
|
336
383
|
/** Part choices that exist only in asset version 2, added after every version 1 choice. */
|
|
337
384
|
const AVATAR_PARTS_V2 = {
|
|
338
385
|
...AVATAR_PARTS,
|
|
@@ -379,7 +426,8 @@ const CUSTOM_PART_KEYS = {
|
|
|
379
426
|
nose: "nosePart",
|
|
380
427
|
cheeks: "cheeksPart",
|
|
381
428
|
petals: "petalsPart",
|
|
382
|
-
flowerBase: "flowerBasePart"
|
|
429
|
+
flowerBase: "flowerBasePart",
|
|
430
|
+
pattern: "patternPart"
|
|
383
431
|
};
|
|
384
432
|
const PART_KEY_ENTRIES = Object.entries(CUSTOM_PART_KEYS);
|
|
385
433
|
const DEFAULT_RECIPE = {
|
|
@@ -410,19 +458,29 @@ function isPixelAvatarRecipe(value) {
|
|
|
410
458
|
const r = value;
|
|
411
459
|
const detailed = DETAIL_KEYS.filter((key) => Object.hasOwn(r, key)).length;
|
|
412
460
|
if (detailed !== 0 && detailed !== DETAIL_KEYS.length) return false;
|
|
461
|
+
const v4 = r["assetVersion"] === 4;
|
|
413
462
|
const v3 = r["assetVersion"] === 3;
|
|
414
|
-
const v2 = r["assetVersion"] === 2 || v3;
|
|
415
|
-
const
|
|
416
|
-
if (
|
|
463
|
+
const v2 = r["assetVersion"] === 2 || v3 || v4;
|
|
464
|
+
const builtInHead = v4 && typeof r["headpiece"] === "string";
|
|
465
|
+
if (builtInHead && !AVATAR_HEADPIECES.includes(r["headpiece"])) return false;
|
|
466
|
+
const worn = PART_KEY_ENTRIES.filter(([slot, key]) => Object.hasOwn(r, key) && !(slot === "headpiece" && builtInHead));
|
|
467
|
+
if (v3 ? worn.length === 0 : !v4 && worn.length !== 0) return false;
|
|
468
|
+
if (!v4 && Object.hasOwn(r, CUSTOM_PART_KEYS.pattern)) return false;
|
|
469
|
+
const newer = [
|
|
470
|
+
...AVATAR_PIECE_COLORS_V4,
|
|
471
|
+
"strand",
|
|
472
|
+
"pattern"
|
|
473
|
+
].filter((key) => Object.hasOwn(r, key));
|
|
474
|
+
if (newer.length !== 0 && (!v4 || !AVATAR_PIECE_COLORS_V4.every((key) => !Object.hasOwn(r, key) || typeof r[key] === "string" && /^#[\da-f]{6}$/iu.test(r[key])) || Object.hasOwn(r, "strand") && !AVATAR_STRANDS.includes(r["strand"]) || Object.hasOwn(r, "pattern") && !AVATAR_PATTERNS.includes(r["pattern"]))) return false;
|
|
417
475
|
for (const [slot, key] of worn) {
|
|
418
476
|
const part = r[key];
|
|
419
477
|
if (!isPixelCustomPart(part) || part.slot !== slot) return false;
|
|
420
478
|
}
|
|
421
479
|
const pieces = AVATAR_PIECE_COLORS.filter((key) => Object.hasOwn(r, key));
|
|
422
480
|
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;
|
|
481
|
+
const v2Keys = v2 ? 2 + pieces.length + extras.length + worn.length + newer.length + (builtInHead ? 1 : 0) : 0;
|
|
424
482
|
if (Object.keys(r).length !== Object.keys(DEFAULT_RECIPE).length + detailed + v2Keys) return false;
|
|
425
|
-
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;
|
|
426
484
|
const catalog = v2 ? AVATAR_PARTS_V2 : AVATAR_PARTS;
|
|
427
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) => {
|
|
428
486
|
const v = r[key];
|
|
@@ -445,9 +503,14 @@ function canonicalRecipe(recipe) {
|
|
|
445
503
|
canonical["rightSideHair"] = recipe.rightSideHair;
|
|
446
504
|
for (const key of AVATAR_PIECE_COLORS) if (recipe[key] !== void 0) canonical[key] = recipe[key].toLowerCase();
|
|
447
505
|
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) {
|
|
506
|
+
if (recipe.assetVersion === 3 || recipe.assetVersion === 4) for (const [, key] of PART_KEY_ENTRIES) {
|
|
449
507
|
const part = recipe[key];
|
|
450
|
-
if (part !== void 0) canonical[key] = canonicalCustomPart(part);
|
|
508
|
+
if (part !== void 0) canonical[key] = typeof part === "string" ? part : canonicalCustomPart(part);
|
|
509
|
+
}
|
|
510
|
+
if (recipe.assetVersion === 4) {
|
|
511
|
+
if (recipe.strand !== void 0) canonical["strand"] = recipe.strand;
|
|
512
|
+
if (recipe.pattern !== void 0 && recipe.pattern !== "solid") canonical["pattern"] = recipe.pattern;
|
|
513
|
+
for (const key of AVATAR_PIECE_COLORS_V4) if (recipe[key] !== void 0) canonical[key] = recipe[key].toLowerCase();
|
|
451
514
|
}
|
|
452
515
|
}
|
|
453
516
|
return canonical;
|
|
@@ -458,14 +521,23 @@ const HAIR_PIECES = [
|
|
|
458
521
|
"rightSideHair",
|
|
459
522
|
"backHair"
|
|
460
523
|
];
|
|
524
|
+
const EAR_PIECES = [
|
|
525
|
+
"catears",
|
|
526
|
+
"bunnyears",
|
|
527
|
+
"horseears"
|
|
528
|
+
];
|
|
461
529
|
/**
|
|
462
530
|
* Saved choices the recipe keeps but does not draw: a flower shows petals and a stem instead of
|
|
463
531
|
* hair, outfit, beard and headwear, and a helmet or hood covers the hair. The choices return when
|
|
464
532
|
* the species or headwear changes back.
|
|
465
533
|
*/
|
|
466
534
|
function hiddenChoices(recipe) {
|
|
535
|
+
const strand = recipe.strand && (recipe.species === "flower" || recipe.bangs !== "none") ? ["strand"] : [];
|
|
536
|
+
const bald = recipe.strand && recipe.bangs === "none" ? ["strand"] : [];
|
|
467
537
|
if (recipe.species === "flower") return [
|
|
468
538
|
...HAIR_PIECES,
|
|
539
|
+
...strand,
|
|
540
|
+
...typeof recipe.headpiece === "string" ? ["headpiece"] : [],
|
|
469
541
|
"outfit",
|
|
470
542
|
"accessory",
|
|
471
543
|
"eyes",
|
|
@@ -473,50 +545,86 @@ function hiddenChoices(recipe) {
|
|
|
473
545
|
"nose",
|
|
474
546
|
"cheeks",
|
|
475
547
|
"glasses",
|
|
476
|
-
...recipe.beard || wornPart(recipe, "beard") ? ["beard"] : []
|
|
548
|
+
...recipe.beard || wornPart(recipe, "beard") ? ["beard"] : [],
|
|
549
|
+
...recipe.pattern || wornPart(recipe, "pattern") ? ["pattern"] : []
|
|
477
550
|
];
|
|
478
551
|
const hidden = [];
|
|
479
|
-
if ((recipe.accessory === "helmet" || recipe.accessory === "hood") && !wornPart(recipe, "accessory")) hidden.push(...HAIR_PIECES);
|
|
552
|
+
if ((recipe.accessory === "helmet" || recipe.accessory === "hood") && !wornPart(recipe, "accessory")) hidden.push(...HAIR_PIECES, ...strand);
|
|
480
553
|
if (recipe.species === "dwarf") hidden.push("nose");
|
|
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");
|
|
481
560
|
return hidden;
|
|
482
561
|
}
|
|
483
|
-
function withSpecies(
|
|
562
|
+
function withSpecies(source, species) {
|
|
563
|
+
const recipe = isAnimalSpecies(species) && source.assetVersion !== 4 ? withPieces(source) : source;
|
|
484
564
|
const detailed = detailedRecipe(recipe);
|
|
485
565
|
const previous = recipe.species ?? "human";
|
|
486
566
|
const suggested = AVATAR_SPECIES_SWATCHES[previous].includes(recipe.skinColor.toLowerCase());
|
|
487
567
|
return {
|
|
488
568
|
...detailed,
|
|
489
|
-
assetVersion: recipe.assetVersion ===
|
|
569
|
+
assetVersion: recipe.assetVersion === 1 ? 2 : recipe.assetVersion,
|
|
490
570
|
species,
|
|
491
571
|
rightSideHair: detailed.rightSideHair ?? detailed.sideHair,
|
|
492
572
|
skinColor: suggested && previous !== species ? AVATAR_SPECIES_SWATCHES[species][0] : recipe.skinColor
|
|
493
573
|
};
|
|
494
574
|
}
|
|
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
575
|
function withCustomPart(recipe, slot, part) {
|
|
500
|
-
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 };
|
|
501
577
|
const key = CUSTOM_PART_KEYS[slot];
|
|
502
578
|
delete base[key];
|
|
503
579
|
if (part !== void 0) base[key] = canonicalCustomPart({
|
|
504
580
|
...part,
|
|
505
581
|
slot
|
|
506
582
|
});
|
|
583
|
+
if (base["assetVersion"] === 4) return base;
|
|
507
584
|
const wearing = PART_KEY_ENTRIES.some(([, k]) => base[k] !== void 0);
|
|
508
585
|
return {
|
|
509
586
|
...base,
|
|
510
587
|
assetVersion: wearing ? 3 : 2
|
|
511
588
|
};
|
|
512
589
|
}
|
|
590
|
+
const MOVED_HEADPIECES = AVATAR_HEADPIECES;
|
|
591
|
+
/**
|
|
592
|
+
* Returns the recipe as asset version 4, keeping every choice. An accessory that is now a
|
|
593
|
+
* headpiece moves into the empty headpiece slot, freeing the accessory slot; call this only when
|
|
594
|
+
* the Human edits, so saved recipes keep rendering as they were.
|
|
595
|
+
*/
|
|
596
|
+
function withPieces(recipe) {
|
|
597
|
+
if (recipe.assetVersion === 4) return recipe;
|
|
598
|
+
const base = {
|
|
599
|
+
...recipe.assetVersion === 1 ? withSpecies(recipe, "human") : recipe,
|
|
600
|
+
assetVersion: 4
|
|
601
|
+
};
|
|
602
|
+
if (base["headpiece"] === void 0 && MOVED_HEADPIECES.includes(recipe.accessory)) {
|
|
603
|
+
base["headpiece"] = recipe.accessory;
|
|
604
|
+
base["accessory"] = "none";
|
|
605
|
+
}
|
|
606
|
+
return base;
|
|
607
|
+
}
|
|
608
|
+
/** Returns the recipe as asset version 4 wearing a built-in headpiece, or none. */
|
|
609
|
+
function withBuiltInHeadpiece(recipe, headpiece) {
|
|
610
|
+
const base = { ...withPieces(recipe) };
|
|
611
|
+
delete base["headpiece"];
|
|
612
|
+
if (headpiece !== void 0) base["headpiece"] = headpiece;
|
|
613
|
+
return base;
|
|
614
|
+
}
|
|
615
|
+
/** The built-in headpiece worn, if any. */
|
|
616
|
+
function builtInHeadpiece(recipe) {
|
|
617
|
+
return typeof recipe.headpiece === "string" ? recipe.headpiece : void 0;
|
|
618
|
+
}
|
|
513
619
|
/** `withCustomPart` for the headpiece slot. */
|
|
514
620
|
function withHeadpiece(recipe, part) {
|
|
515
621
|
return withCustomPart(recipe, "headpiece", part);
|
|
516
622
|
}
|
|
517
623
|
/** The Custom Part worn in a slot, if any. */
|
|
518
624
|
function wornPart(recipe, slot) {
|
|
519
|
-
|
|
625
|
+
if (recipe.assetVersion !== 3 && recipe.assetVersion !== 4) return void 0;
|
|
626
|
+
const part = recipe[CUSTOM_PART_KEYS[slot]];
|
|
627
|
+
return typeof part === "string" ? void 0 : part;
|
|
520
628
|
}
|
|
521
629
|
function detailedRecipe(recipe) {
|
|
522
630
|
if (recipe.bangs !== void 0) return recipe;
|
|
@@ -745,6 +853,46 @@ const AVATAR_SPECIES_SWATCHES = {
|
|
|
745
853
|
"#e8a85a",
|
|
746
854
|
"#c98b4a",
|
|
747
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"
|
|
748
896
|
]
|
|
749
897
|
};
|
|
750
898
|
const preset = (parts) => ({
|
|
@@ -934,6 +1082,10 @@ const PART_SLOTS = {
|
|
|
934
1082
|
flowerBase: {
|
|
935
1083
|
width: 32,
|
|
936
1084
|
height: 32
|
|
1085
|
+
},
|
|
1086
|
+
pattern: {
|
|
1087
|
+
width: 32,
|
|
1088
|
+
height: 32
|
|
937
1089
|
}
|
|
938
1090
|
};
|
|
939
1091
|
/**
|
|
@@ -959,7 +1111,8 @@ const REPLACE_PART_SLOTS = [
|
|
|
959
1111
|
"nose",
|
|
960
1112
|
"cheeks",
|
|
961
1113
|
"petals",
|
|
962
|
-
"flowerBase"
|
|
1114
|
+
"flowerBase",
|
|
1115
|
+
"pattern"
|
|
963
1116
|
];
|
|
964
1117
|
const isReplacePartSlot = (slot) => REPLACE_PART_SLOTS.includes(slot);
|
|
965
1118
|
/** Tone steps on the rig's shade ramp: two darker, the color itself, two lighter. */
|
|
@@ -1659,6 +1812,39 @@ function hair(recipe, s, b) {
|
|
|
1659
1812
|
};
|
|
1660
1813
|
}
|
|
1661
1814
|
}
|
|
1815
|
+
const STRANDS = {
|
|
1816
|
+
ahoge: {
|
|
1817
|
+
rows: [
|
|
1818
|
+
"XX.",
|
|
1819
|
+
".XX",
|
|
1820
|
+
".XX"
|
|
1821
|
+
],
|
|
1822
|
+
dx: 0
|
|
1823
|
+
},
|
|
1824
|
+
curl: {
|
|
1825
|
+
rows: [
|
|
1826
|
+
".XX.",
|
|
1827
|
+
"X..X",
|
|
1828
|
+
"..XX",
|
|
1829
|
+
".X.."
|
|
1830
|
+
],
|
|
1831
|
+
dx: -1
|
|
1832
|
+
},
|
|
1833
|
+
double: {
|
|
1834
|
+
rows: [
|
|
1835
|
+
"X...X",
|
|
1836
|
+
".X.X.",
|
|
1837
|
+
".X.X."
|
|
1838
|
+
],
|
|
1839
|
+
dx: -2
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
function strandMask(style, s) {
|
|
1843
|
+
const { rows, dx } = STRANDS[style];
|
|
1844
|
+
const top = 4 - rows.length;
|
|
1845
|
+
const left = Math.round(C + s) + dx;
|
|
1846
|
+
return (x, y) => rows[y - top]?.[x - left] === "X";
|
|
1847
|
+
}
|
|
1662
1848
|
function detailedHair(recipe, s, b) {
|
|
1663
1849
|
const of = (style) => hair({
|
|
1664
1850
|
...recipe,
|
|
@@ -1673,8 +1859,11 @@ function detailedHair(recipe, s, b) {
|
|
|
1673
1859
|
const sideOf = (sign) => (x, y) => y > 7 && sign * (x + .5 - C - s) >= 7 && lengthen((sign < 0 ? side : right).front)(x, y);
|
|
1674
1860
|
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
1861
|
const behind = length === 0 ? back.back : lengthen(back.back);
|
|
1862
|
+
const tuft = recipe.strand && recipe.bangs !== "none" ? strandMask(recipe.strand, s) : void 0;
|
|
1863
|
+
const strand = tuft ? (x, y) => tuft(x, y) && !fringe(x, y) && !sideOf(-1)(x, y) && !sideOf(1)(x, y) : void 0;
|
|
1676
1864
|
return {
|
|
1677
|
-
front: (x, y) => fringe(x, y) || sideOf(-1)(x, y) || sideOf(1)(x, y),
|
|
1865
|
+
front: (x, y) => fringe(x, y) || sideOf(-1)(x, y) || sideOf(1)(x, y) || (strand?.(x, y) ?? false),
|
|
1866
|
+
...strand ? { strand } : {},
|
|
1678
1867
|
sides: [sideOf(-1), sideOf(1)],
|
|
1679
1868
|
pieces: {
|
|
1680
1869
|
bangs: fringe,
|
|
@@ -2019,6 +2208,172 @@ function accessory(recipe, g, s, face) {
|
|
|
2019
2208
|
return;
|
|
2020
2209
|
}
|
|
2021
2210
|
}
|
|
2211
|
+
/**
|
|
2212
|
+
* A built-in headpiece: the row that meets the head goes behind the hair, the rest in front.
|
|
2213
|
+
* Turned, the far side of a pair is hidden, like a turned ear.
|
|
2214
|
+
*/
|
|
2215
|
+
function drawHeadpiece(style, recipe, behind, front, s, far) {
|
|
2216
|
+
const cx = Math.round(C + s);
|
|
2217
|
+
const pair = (rows, left, right, y, pal, mirror = true) => {
|
|
2218
|
+
const tips = rows.slice(0, -1);
|
|
2219
|
+
if (far >= 0) {
|
|
2220
|
+
sprite(behind, rows, cx + left, y, pal);
|
|
2221
|
+
sprite(front, tips, cx + left, y, pal);
|
|
2222
|
+
}
|
|
2223
|
+
if (far <= 0) {
|
|
2224
|
+
sprite(behind, rows, cx + right, y, pal, mirror);
|
|
2225
|
+
sprite(front, tips, cx + right, y, pal, mirror);
|
|
2226
|
+
}
|
|
2227
|
+
};
|
|
2228
|
+
switch (style) {
|
|
2229
|
+
case "catears":
|
|
2230
|
+
pair([
|
|
2231
|
+
"X...",
|
|
2232
|
+
"XX..",
|
|
2233
|
+
"XPX.",
|
|
2234
|
+
"XXXX"
|
|
2235
|
+
], -9, 5, 2, {
|
|
2236
|
+
X: recipe.hairColor,
|
|
2237
|
+
P: "#f4a3b5"
|
|
2238
|
+
});
|
|
2239
|
+
return;
|
|
2240
|
+
case "bunnyears":
|
|
2241
|
+
pair([
|
|
2242
|
+
"WW",
|
|
2243
|
+
"WP",
|
|
2244
|
+
"WP",
|
|
2245
|
+
"WP",
|
|
2246
|
+
"WW"
|
|
2247
|
+
], -6, 4, 0, {
|
|
2248
|
+
W: "#f4f1ec",
|
|
2249
|
+
P: "#f4a3b5"
|
|
2250
|
+
});
|
|
2251
|
+
return;
|
|
2252
|
+
case "horseears":
|
|
2253
|
+
pair([
|
|
2254
|
+
".X.",
|
|
2255
|
+
"XPX",
|
|
2256
|
+
"XPX",
|
|
2257
|
+
"XXX"
|
|
2258
|
+
], -9, 6, 1, {
|
|
2259
|
+
X: shade(recipe.hairColor, .9),
|
|
2260
|
+
P: "#f4a3b5"
|
|
2261
|
+
}, false);
|
|
2262
|
+
return;
|
|
2263
|
+
case "horns":
|
|
2264
|
+
pair([
|
|
2265
|
+
".H",
|
|
2266
|
+
"HH",
|
|
2267
|
+
"Hh"
|
|
2268
|
+
], -8, 6, 1, {
|
|
2269
|
+
H: "#5b3a6e",
|
|
2270
|
+
h: "#7d5694"
|
|
2271
|
+
});
|
|
2272
|
+
return;
|
|
2273
|
+
case "halo": {
|
|
2274
|
+
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);
|
|
2275
|
+
paint(behind, (x, y) => ring(x, y) && y < 2, GOLD);
|
|
2276
|
+
paint(front, (x, y) => ring(x, y) && y >= 2, GOLD);
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
case "wings":
|
|
2280
|
+
pair([
|
|
2281
|
+
"W..",
|
|
2282
|
+
"WW.",
|
|
2283
|
+
"WwW",
|
|
2284
|
+
".ww"
|
|
2285
|
+
], -13, 10, 7, {
|
|
2286
|
+
W: "#f4f1ec",
|
|
2287
|
+
w: "#cfd6e2"
|
|
2288
|
+
});
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
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
|
+
}
|
|
2022
2377
|
const POINTED_EARS = {
|
|
2023
2378
|
goblin: {
|
|
2024
2379
|
rows: [
|
|
@@ -2135,10 +2490,11 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2135
2490
|
return front(x - Math.round(d * (1 + Math.max(0, y - 20) * .4) - d * tuck), y);
|
|
2136
2491
|
} : front;
|
|
2137
2492
|
const flower = recipe.species === "flower";
|
|
2493
|
+
const animal = isAnimalSpecies(recipe.species) ? ANIMALS[recipe.species] : void 0;
|
|
2138
2494
|
const hiddenNow = hiddenChoices(recipe);
|
|
2139
2495
|
const drawnParts = new Map(REPLACE_PART_SLOTS.flatMap((slot) => {
|
|
2140
2496
|
const part = wornPart(recipe, slot);
|
|
2141
|
-
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);
|
|
2142
2498
|
return part && shown ? [[slot, part.front]] : [];
|
|
2143
2499
|
}));
|
|
2144
2500
|
const skip = (slot) => omit.has(slot) || drawnParts.has(slot);
|
|
@@ -2205,7 +2561,7 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2205
2561
|
};
|
|
2206
2562
|
const masks = drawnHair.length === 0 ? builtin : {
|
|
2207
2563
|
...builtin,
|
|
2208
|
-
front: (x, y) => piece("bangs")(x, y) || piece("leftSideHair")(x, y) || piece("rightSideHair")(x, y),
|
|
2564
|
+
front: (x, y) => piece("bangs")(x, y) || piece("leftSideHair")(x, y) || piece("rightSideHair")(x, y) || (builtin.strand?.(x, y) ?? false),
|
|
2209
2565
|
sides: [piece("leftSideHair"), piece("rightSideHair")],
|
|
2210
2566
|
back: piece("backHair"),
|
|
2211
2567
|
...drawnHair.some((drawn) => drawn.slot === "backHair") ? { ties: [] } : {}
|
|
@@ -2244,8 +2600,18 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2244
2600
|
placeCells(back, inked("backHair"), false);
|
|
2245
2601
|
overlay(back, "petals");
|
|
2246
2602
|
if (headpiece) placeCells(back, headpiece.back);
|
|
2603
|
+
const wornHead = flower || hiddenNow.includes("headpiece") ? void 0 : builtInHeadpiece(recipe);
|
|
2604
|
+
const headFront = blank();
|
|
2605
|
+
if (wornHead) drawHeadpiece(wornHead, recipe, back, headFront, s, turned ? d : 0);
|
|
2247
2606
|
const head = blank();
|
|
2248
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
|
+
}
|
|
2249
2615
|
const h = recipe.height ?? 0;
|
|
2250
2616
|
if (recipe.beard && !flower && !skip("beard")) {
|
|
2251
2617
|
const centre = C + fs * .6;
|
|
@@ -2263,7 +2629,17 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2263
2629
|
return side < 0 ? xs[0] - 1 : xs.at(-1) + 1;
|
|
2264
2630
|
};
|
|
2265
2631
|
const pointed = recipe.species ? POINTED_EARS[recipe.species] : void 0;
|
|
2266
|
-
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) {
|
|
2267
2643
|
const pal = {
|
|
2268
2644
|
E: skin,
|
|
2269
2645
|
e: shade(skin, .86),
|
|
@@ -2289,14 +2665,16 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2289
2665
|
E: skin,
|
|
2290
2666
|
e: shade(skin, .8)
|
|
2291
2667
|
});
|
|
2292
|
-
if (!pointed && !flower) paint(head, masks.front, hairColor);
|
|
2668
|
+
if (!pointed && !flower && !animal) paint(head, masks.front, hairColor);
|
|
2293
2669
|
for (const [x, y] of masks.ties ?? []) sprite(back, ["T"], Math.round(x), y, { T: shade(recipe.shirtColor, .8) });
|
|
2294
|
-
if (!flower && !skip("accessory")) accessory(recipe, head, s, face);
|
|
2670
|
+
if (!flower && !skip("accessory") && !hiddenNow.includes("accessory")) accessory(recipe, head, s, face);
|
|
2295
2671
|
const shadeHair = (g) => {
|
|
2296
2672
|
const out = g.map((r) => [...r]);
|
|
2297
2673
|
const cells = g.flatMap((row, y) => row.flatMap((c, x) => c === hairColor ? [[x, y]] : []));
|
|
2298
|
-
const
|
|
2299
|
-
const
|
|
2674
|
+
const framed = cells.filter(([x, y]) => !builtin.strand?.(x, y));
|
|
2675
|
+
const bounds = framed.length ? framed : cells;
|
|
2676
|
+
const xs = bounds.map(([x]) => x);
|
|
2677
|
+
const ys = bounds.map(([, y]) => y);
|
|
2300
2678
|
const left = Math.min(...xs);
|
|
2301
2679
|
const right = Math.max(...xs);
|
|
2302
2680
|
const top = Math.min(...ys);
|
|
@@ -2325,7 +2703,7 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2325
2703
|
}
|
|
2326
2704
|
const ring = mix$1(hairColor, WHITE, .45);
|
|
2327
2705
|
for (let x = Math.round(C + s) - 7; x <= Math.round(C + s) + 3; x++) {
|
|
2328
|
-
const first = g.findIndex((row) => row[x] === hairColor);
|
|
2706
|
+
const first = g.findIndex((row, y) => row[x] === hairColor && !builtin.strand?.(x, y));
|
|
2329
2707
|
if (first < 0) continue;
|
|
2330
2708
|
const y = first + 2 + Math.round(Math.abs(x + .5 - (C + s - 2)) / 4);
|
|
2331
2709
|
if (g[y]?.[x] === hairColor && x % 3 !== 0) out[y][x] = ring;
|
|
@@ -2338,6 +2716,7 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2338
2716
|
const crown = blank();
|
|
2339
2717
|
placeCells(crown, inked("all-front"), false);
|
|
2340
2718
|
if (headpiece) placeCells(crown, headpiece.front);
|
|
2719
|
+
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
2720
|
const figure = blank();
|
|
2342
2721
|
for (const layer of [
|
|
2343
2722
|
back,
|
|
@@ -2407,7 +2786,8 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2407
2786
|
sprite(features, fit(browRows, right.w), right.x, eyeTop - 2, { B: shade(hairColor, .55) }, true);
|
|
2408
2787
|
}
|
|
2409
2788
|
const cx = Math.round(C + fs * (turned ? .85 : .6));
|
|
2410
|
-
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, {
|
|
2411
2791
|
N: shade(skin, .84),
|
|
2412
2792
|
n: shade(skin, .72)
|
|
2413
2793
|
});
|
|
@@ -2475,17 +2855,42 @@ function renderFigure(recipe, yawDeg, options, omit) {
|
|
|
2475
2855
|
], right.x + right.w, eyeTop + 5, { G: GOLD });
|
|
2476
2856
|
}
|
|
2477
2857
|
const full = clip(outline(figure));
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
const
|
|
2481
|
-
if (
|
|
2858
|
+
const variants = /* @__PURE__ */ new Map();
|
|
2859
|
+
const recolor = (color) => {
|
|
2860
|
+
const cached = variants.get(color.toLowerCase());
|
|
2861
|
+
if (cached) return cached;
|
|
2482
2862
|
const variant = blank();
|
|
2863
|
+
variants.set(color.toLowerCase(), variant);
|
|
2483
2864
|
const plain = {
|
|
2484
2865
|
...recipe,
|
|
2485
2866
|
hairColor: color
|
|
2486
2867
|
};
|
|
2487
|
-
for (const k of AVATAR_PIECE_COLORS) delete plain[k];
|
|
2868
|
+
for (const k of [...AVATAR_PIECE_COLORS, ...AVATAR_PIECE_COLORS_V4]) delete plain[k];
|
|
2488
2869
|
for (const cell of renderFigure(plain, yawDeg, {}, omit).cells) variant[cell.y][cell.x] = cell.c;
|
|
2870
|
+
return variant;
|
|
2871
|
+
};
|
|
2872
|
+
if (recipe.assetVersion === 4) {
|
|
2873
|
+
const over = (x, y) => !!crown[y]?.[x] || !!drawnOver[y]?.[x] || !!eyes[y]?.[x] || !!glasses[y]?.[x];
|
|
2874
|
+
const hairAt = (x, y) => head[y]?.[x] === hairColor && !over(x, y);
|
|
2875
|
+
const backAt = (x, y) => back[y]?.[x] !== void 0 && figure[y][x] === back[y][x];
|
|
2876
|
+
const owners = [
|
|
2877
|
+
[flower ? void 0 : recipe.backHairColor, (x, y) => masks.back(x, y) && backAt(x, y)],
|
|
2878
|
+
[recipe.leftSideHairColor, (x, y) => !!masks.sides?.[0](x, y) && (hairAt(x, y) || backAt(x, y))],
|
|
2879
|
+
[recipe.rightSideHairColor, (x, y) => !!masks.sides?.[1](x, y) && (hairAt(x, y) || backAt(x, y))],
|
|
2880
|
+
[recipe.bangsColor, (x, y) => (builtin.pieces ? piece("bangs")(x, y) : false) && hairAt(x, y)],
|
|
2881
|
+
[recipe.strandColor, (x, y) => (builtin.strand?.(x, y) ?? false) && hairAt(x, y)]
|
|
2882
|
+
];
|
|
2883
|
+
for (const [color, owns] of owners) {
|
|
2884
|
+
if (color === void 0 || color.toLowerCase() === hairColor.toLowerCase()) continue;
|
|
2885
|
+
const variant = recolor(color);
|
|
2886
|
+
const near = (x, y) => owns(x - 1, y) || owns(x + 1, y) || owns(x, y - 1) || owns(x, y + 1);
|
|
2887
|
+
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];
|
|
2888
|
+
}
|
|
2889
|
+
} else AVATAR_PIECE_COLORS.forEach((key, i) => {
|
|
2890
|
+
const color = recipe[key];
|
|
2891
|
+
const mask = masks.sides?.[i];
|
|
2892
|
+
if (color === void 0 || !mask || color.toLowerCase() === hairColor.toLowerCase()) return;
|
|
2893
|
+
const variant = recolor(color);
|
|
2489
2894
|
const near = (x, y) => mask(x, y) || mask(x - 1, y) || mask(x + 1, y) || mask(x, y - 1) || mask(x, y + 1);
|
|
2490
2895
|
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
2896
|
});
|
|
@@ -2560,6 +2965,57 @@ function hairPieceStart(recipe, slot) {
|
|
|
2560
2965
|
back: []
|
|
2561
2966
|
};
|
|
2562
2967
|
}
|
|
2968
|
+
function cellSource(recipe) {
|
|
2969
|
+
return (c) => {
|
|
2970
|
+
for (const key of AVATAR_COLORS) for (const tone of [
|
|
2971
|
+
0,
|
|
2972
|
+
-1,
|
|
2973
|
+
1,
|
|
2974
|
+
-2,
|
|
2975
|
+
2
|
|
2976
|
+
]) if (TONES[tone](recipe[key].toLowerCase()) === c) return [key, tone];
|
|
2977
|
+
return [c, 0];
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
/**
|
|
2981
|
+
* The headpiece as a Custom Part to start drawing from: the drawn headpiece worn, or the
|
|
2982
|
+
* built-in headpiece flattened for this recipe in the front pose, its base on the back layer and
|
|
2983
|
+
* the rest on the front layer, so an unchanged copy renders identically facing front.
|
|
2984
|
+
*/
|
|
2985
|
+
function headpieceStart(recipe) {
|
|
2986
|
+
const worn = wornPart(recipe, "headpiece");
|
|
2987
|
+
if (worn) return {
|
|
2988
|
+
slot: "headpiece",
|
|
2989
|
+
front: [...worn.front],
|
|
2990
|
+
back: [...worn.back]
|
|
2991
|
+
};
|
|
2992
|
+
const style = builtInHeadpiece(recipe);
|
|
2993
|
+
const behind = blank();
|
|
2994
|
+
const over = blank();
|
|
2995
|
+
if (style) drawHeadpiece(style, recipe, behind, over, 0, 0);
|
|
2996
|
+
const source = cellSource(recipe);
|
|
2997
|
+
const front = [];
|
|
2998
|
+
const back = [];
|
|
2999
|
+
for (let y = 0; y < 16; y++) for (let x = 0; x < N; x++) {
|
|
3000
|
+
const c = over[y][x];
|
|
3001
|
+
const b = behind[y][x];
|
|
3002
|
+
if (c) front.push([
|
|
3003
|
+
x,
|
|
3004
|
+
y,
|
|
3005
|
+
...source(c)
|
|
3006
|
+
]);
|
|
3007
|
+
if (b && b !== c) back.push([
|
|
3008
|
+
x,
|
|
3009
|
+
y,
|
|
3010
|
+
...source(b)
|
|
3011
|
+
]);
|
|
3012
|
+
}
|
|
3013
|
+
return {
|
|
3014
|
+
slot: "headpiece",
|
|
3015
|
+
front,
|
|
3016
|
+
back
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
2563
3019
|
/**
|
|
2564
3020
|
* The part in a replacement slot as a Custom Part to start drawing from: the drawn part worn
|
|
2565
3021
|
* there, or the built-in part flattened for this recipe in the front pose. Each pixel becomes a
|
|
@@ -2577,16 +3033,7 @@ function replacePartStart(recipe, slot) {
|
|
|
2577
3033
|
const layer = slot === "glasses" ? "glasses" : slot === "nose" || slot === "cheeks" ? "features" : slot === "petals" ? "back" : slot === "outfit" || slot === "flowerBase" ? "body" : "figure";
|
|
2578
3034
|
const shown = renderFigure(recipe, 0, {}, /* @__PURE__ */ new Set()).grids[layer];
|
|
2579
3035
|
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
|
-
};
|
|
3036
|
+
const source = cellSource(recipe);
|
|
2590
3037
|
const front = [];
|
|
2591
3038
|
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
|
|
2592
3039
|
const c = shown[y][x];
|
|
@@ -3125,5 +3572,5 @@ function symbolArtCells(rows, color) {
|
|
|
3125
3572
|
}
|
|
3126
3573
|
|
|
3127
3574
|
//#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 };
|
|
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 };
|
|
3129
3576
|
//# sourceMappingURL=index.js.map
|