@botharness/pixel-avatar 0.4.0 → 0.6.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 +3 -0
- package/dist/index.d.ts +127 -5
- package/dist/index.js +381 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -365,6 +365,15 @@ const AVATAR_EXTRA_PARTS = {
|
|
|
365
365
|
],
|
|
366
366
|
flowerBase: ["leaves", "pot"]
|
|
367
367
|
};
|
|
368
|
+
/** The recipe key that embeds the Custom Part worn in each slot. */
|
|
369
|
+
const CUSTOM_PART_KEYS = {
|
|
370
|
+
headpiece: "headpiece",
|
|
371
|
+
bangs: "bangsPart",
|
|
372
|
+
leftSideHair: "leftSideHairPart",
|
|
373
|
+
rightSideHair: "rightSideHairPart",
|
|
374
|
+
backHair: "backHairPart"
|
|
375
|
+
};
|
|
376
|
+
const PART_KEY_ENTRIES = Object.entries(CUSTOM_PART_KEYS);
|
|
368
377
|
const DEFAULT_RECIPE = {
|
|
369
378
|
schemaVersion: 1,
|
|
370
379
|
family: "illustrated",
|
|
@@ -393,10 +402,17 @@ function isPixelAvatarRecipe(value) {
|
|
|
393
402
|
const r = value;
|
|
394
403
|
const detailed = DETAIL_KEYS.filter((key) => Object.hasOwn(r, key)).length;
|
|
395
404
|
if (detailed !== 0 && detailed !== DETAIL_KEYS.length) return false;
|
|
396
|
-
const
|
|
405
|
+
const v3 = r["assetVersion"] === 3;
|
|
406
|
+
const v2 = r["assetVersion"] === 2 || v3;
|
|
407
|
+
const worn = PART_KEY_ENTRIES.filter(([, key]) => Object.hasOwn(r, key));
|
|
408
|
+
if (v3 ? worn.length === 0 : worn.length !== 0) return false;
|
|
409
|
+
for (const [slot, key] of worn) {
|
|
410
|
+
const part = r[key];
|
|
411
|
+
if (!isPixelCustomPart(part) || part.slot !== slot) return false;
|
|
412
|
+
}
|
|
397
413
|
const pieces = AVATAR_PIECE_COLORS.filter((key) => Object.hasOwn(r, key));
|
|
398
414
|
const extras = Object.keys(AVATAR_EXTRA_PARTS).filter((key) => Object.hasOwn(r, key));
|
|
399
|
-
const v2Keys = v2 ? 2 + pieces.length + extras.length : 0;
|
|
415
|
+
const v2Keys = v2 ? 2 + pieces.length + extras.length + worn.length : 0;
|
|
400
416
|
if (Object.keys(r).length !== Object.keys(DEFAULT_RECIPE).length + detailed + v2Keys) return false;
|
|
401
417
|
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;
|
|
402
418
|
const catalog = v2 ? AVATAR_PARTS_V2 : AVATAR_PARTS;
|
|
@@ -416,11 +432,15 @@ function canonicalRecipe(recipe) {
|
|
|
416
432
|
for (const key of PART_KEYS) canonical[key] = recipe[key];
|
|
417
433
|
for (const key of AVATAR_COLORS) canonical[key] = recipe[key].toLowerCase();
|
|
418
434
|
if (recipe.bangs !== void 0) for (const key of DETAIL_KEYS) canonical[key] = recipe[key];
|
|
419
|
-
if (recipe.assetVersion
|
|
435
|
+
if (recipe.assetVersion !== 1) {
|
|
420
436
|
canonical["species"] = recipe.species;
|
|
421
437
|
canonical["rightSideHair"] = recipe.rightSideHair;
|
|
422
438
|
for (const key of AVATAR_PIECE_COLORS) if (recipe[key] !== void 0) canonical[key] = recipe[key].toLowerCase();
|
|
423
439
|
for (const key of Object.keys(AVATAR_EXTRA_PARTS)) if (recipe[key] !== void 0) canonical[key] = recipe[key];
|
|
440
|
+
if (recipe.assetVersion === 3) for (const [, key] of PART_KEY_ENTRIES) {
|
|
441
|
+
const part = recipe[key];
|
|
442
|
+
if (part !== void 0) canonical[key] = canonicalCustomPart(part);
|
|
443
|
+
}
|
|
424
444
|
}
|
|
425
445
|
return canonical;
|
|
426
446
|
}
|
|
@@ -452,24 +472,44 @@ function hiddenChoices(recipe) {
|
|
|
452
472
|
if (recipe.species === "dwarf") hidden.push("nose");
|
|
453
473
|
return hidden;
|
|
454
474
|
}
|
|
455
|
-
/**
|
|
456
|
-
* Returns the recipe as asset version 2 with the given species, keeping every other choice.
|
|
457
|
-
* Side hair splits into left (`sideHair`) and right (`rightSideHair`) pieces that start equal.
|
|
458
|
-
* When the skin color is one of the previous species' suggested colors, it moves to the new
|
|
459
|
-
* species' first suggestion; a custom color is kept.
|
|
460
|
-
*/
|
|
461
475
|
function withSpecies(recipe, species) {
|
|
462
476
|
const detailed = detailedRecipe(recipe);
|
|
463
477
|
const previous = recipe.species ?? "human";
|
|
464
478
|
const suggested = AVATAR_SPECIES_SWATCHES[previous].includes(recipe.skinColor.toLowerCase());
|
|
465
479
|
return {
|
|
466
480
|
...detailed,
|
|
467
|
-
assetVersion: 2,
|
|
481
|
+
assetVersion: recipe.assetVersion === 3 ? 3 : 2,
|
|
468
482
|
species,
|
|
469
483
|
rightSideHair: detailed.rightSideHair ?? detailed.sideHair,
|
|
470
484
|
skinColor: suggested && previous !== species ? AVATAR_SPECIES_SWATCHES[species][0] : recipe.skinColor
|
|
471
485
|
};
|
|
472
486
|
}
|
|
487
|
+
/**
|
|
488
|
+
* Returns the recipe wearing `part` in `slot`, or with that slot's part taken off. The recipe
|
|
489
|
+
* embeds its own copy; it is asset version 3 while it wears any part and version 2 otherwise.
|
|
490
|
+
*/
|
|
491
|
+
function withCustomPart(recipe, slot, part) {
|
|
492
|
+
const base = { ...recipe.assetVersion === 1 ? withSpecies(recipe, "human") : recipe };
|
|
493
|
+
const key = CUSTOM_PART_KEYS[slot];
|
|
494
|
+
delete base[key];
|
|
495
|
+
if (part !== void 0) base[key] = canonicalCustomPart({
|
|
496
|
+
...part,
|
|
497
|
+
slot
|
|
498
|
+
});
|
|
499
|
+
const wearing = PART_KEY_ENTRIES.some(([, k]) => base[k] !== void 0);
|
|
500
|
+
return {
|
|
501
|
+
...base,
|
|
502
|
+
assetVersion: wearing ? 3 : 2
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
/** `withCustomPart` for the headpiece slot. */
|
|
506
|
+
function withHeadpiece(recipe, part) {
|
|
507
|
+
return withCustomPart(recipe, "headpiece", part);
|
|
508
|
+
}
|
|
509
|
+
/** The Custom Part worn in a slot, if any. */
|
|
510
|
+
function wornPart(recipe, slot) {
|
|
511
|
+
return recipe.assetVersion === 3 ? recipe[CUSTOM_PART_KEYS[slot]] : void 0;
|
|
512
|
+
}
|
|
473
513
|
function detailedRecipe(recipe) {
|
|
474
514
|
if (recipe.bangs !== void 0) return recipe;
|
|
475
515
|
const pick = (part) => AVATAR_HAIR_PARTS[part].includes(recipe.hair) ? recipe.hair : HAIR_SPLIT[recipe.hair][part];
|
|
@@ -826,6 +866,223 @@ const AVATAR_PRESETS = [
|
|
|
826
866
|
})
|
|
827
867
|
];
|
|
828
868
|
|
|
869
|
+
//#endregion
|
|
870
|
+
//#region src/part.ts
|
|
871
|
+
/**
|
|
872
|
+
* Custom Part slots. A headpiece covers the top of the tile, around the hair: its back layer is
|
|
873
|
+
* drawn behind the hair and its front layer over it. Hair slots replace one built-in hair piece
|
|
874
|
+
* and use only the front layer. Cells are in tile coordinates, so the Avatar centerline sits
|
|
875
|
+
* between x = 15 and x = 16.
|
|
876
|
+
*/
|
|
877
|
+
const PART_SLOTS = {
|
|
878
|
+
headpiece: {
|
|
879
|
+
width: 32,
|
|
880
|
+
height: 16
|
|
881
|
+
},
|
|
882
|
+
bangs: {
|
|
883
|
+
width: 32,
|
|
884
|
+
height: 32
|
|
885
|
+
},
|
|
886
|
+
leftSideHair: {
|
|
887
|
+
width: 32,
|
|
888
|
+
height: 32
|
|
889
|
+
},
|
|
890
|
+
rightSideHair: {
|
|
891
|
+
width: 32,
|
|
892
|
+
height: 32
|
|
893
|
+
},
|
|
894
|
+
backHair: {
|
|
895
|
+
width: 32,
|
|
896
|
+
height: 32
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
/**
|
|
900
|
+
* Hair slots. In a hair part, a `hairColor` cell at tone 0 is live hair: it is shaded with the
|
|
901
|
+
* rest of the hair, exactly like a built-in piece. Every other cell shows its own color.
|
|
902
|
+
*/
|
|
903
|
+
const HAIR_PART_SLOTS = [
|
|
904
|
+
"bangs",
|
|
905
|
+
"leftSideHair",
|
|
906
|
+
"rightSideHair",
|
|
907
|
+
"backHair"
|
|
908
|
+
];
|
|
909
|
+
const isHairPartSlot = (slot) => HAIR_PART_SLOTS.includes(slot);
|
|
910
|
+
/** Tone steps on the rig's shade ramp: two darker, the color itself, two lighter. */
|
|
911
|
+
const PART_TONES = [
|
|
912
|
+
-2,
|
|
913
|
+
-1,
|
|
914
|
+
0,
|
|
915
|
+
1,
|
|
916
|
+
2
|
|
917
|
+
];
|
|
918
|
+
const PART_LAYERS = ["front", "back"];
|
|
919
|
+
/** Most fixed colors one part may use. */
|
|
920
|
+
const MAX_PART_FIXED_COLORS = 16;
|
|
921
|
+
const FIXED = /^#[\da-f]{6}$/iu;
|
|
922
|
+
const isColor = (value) => typeof value === "string" && (AVATAR_COLORS.includes(value) || FIXED.test(value));
|
|
923
|
+
function isPixelCustomPart(value) {
|
|
924
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
925
|
+
const r = value;
|
|
926
|
+
if (Object.keys(r).length !== 3 || !Object.hasOwn(PART_SLOTS, r["slot"])) return false;
|
|
927
|
+
const { width, height } = PART_SLOTS[r["slot"]];
|
|
928
|
+
if (isHairPartSlot(r["slot"]) && r["back"]?.length !== 0) return false;
|
|
929
|
+
const fixed = /* @__PURE__ */ new Set();
|
|
930
|
+
for (const name of PART_LAYERS) {
|
|
931
|
+
const cells = r[name];
|
|
932
|
+
if (!Array.isArray(cells) || cells.length > width * height) return false;
|
|
933
|
+
const seen = /* @__PURE__ */ new Set();
|
|
934
|
+
for (const cell of cells) {
|
|
935
|
+
if (!Array.isArray(cell) || cell.length !== 4) return false;
|
|
936
|
+
const [x, y, color, tone] = cell;
|
|
937
|
+
if (!Number.isInteger(x) || !Number.isInteger(y) || x < 0 || x >= width || y < 0 || y >= height || !isColor(color) || !PART_TONES.includes(tone)) return false;
|
|
938
|
+
const key = y * width + x;
|
|
939
|
+
if (seen.has(key)) return false;
|
|
940
|
+
seen.add(key);
|
|
941
|
+
if (color.startsWith("#")) fixed.add(color.toLowerCase());
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return fixed.size <= MAX_PART_FIXED_COLORS;
|
|
945
|
+
}
|
|
946
|
+
const canonicalCells = (cells) => cells.map(([x, y, color, tone]) => [
|
|
947
|
+
x,
|
|
948
|
+
y,
|
|
949
|
+
color.startsWith("#") ? color.toLowerCase() : color,
|
|
950
|
+
tone
|
|
951
|
+
]).sort((a, b) => a[1] - b[1] || a[0] - b[0]);
|
|
952
|
+
/** The part with cells in row order and fixed colors in lowercase. */
|
|
953
|
+
function canonicalCustomPart(part) {
|
|
954
|
+
return {
|
|
955
|
+
slot: part.slot,
|
|
956
|
+
front: canonicalCells(part.front),
|
|
957
|
+
back: canonicalCells(part.back)
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
/** Content identity: a SHA-256 of the canonical slot, cells and color references. */
|
|
961
|
+
function customPartId(part) {
|
|
962
|
+
return sha256(JSON.stringify(canonicalCustomPart(part)));
|
|
963
|
+
}
|
|
964
|
+
function emptyPartLayer(slot) {
|
|
965
|
+
const { width, height } = PART_SLOTS[slot];
|
|
966
|
+
return Array.from({ length: height }, () => Array(width).fill(null));
|
|
967
|
+
}
|
|
968
|
+
function partLayer(slot, cells) {
|
|
969
|
+
const layer = emptyPartLayer(slot);
|
|
970
|
+
for (const [x, y, color, tone] of cells) layer[y][x] = {
|
|
971
|
+
color,
|
|
972
|
+
tone
|
|
973
|
+
};
|
|
974
|
+
return layer;
|
|
975
|
+
}
|
|
976
|
+
function partCells(layer) {
|
|
977
|
+
return layer.flatMap((row, y) => row.flatMap((ink, x) => ink ? [[
|
|
978
|
+
x,
|
|
979
|
+
y,
|
|
980
|
+
ink.color,
|
|
981
|
+
ink.tone
|
|
982
|
+
]] : []));
|
|
983
|
+
}
|
|
984
|
+
/** Builds a canonical part from two drawn layers. */
|
|
985
|
+
function createCustomPart(slot, layers) {
|
|
986
|
+
return canonicalCustomPart({
|
|
987
|
+
slot,
|
|
988
|
+
front: partCells(layers.front),
|
|
989
|
+
back: partCells(layers.back)
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
/** The column mirrored across the Avatar centerline. */
|
|
993
|
+
function mirrorPartX(slot, x) {
|
|
994
|
+
return PART_SLOTS[slot].width - 1 - x;
|
|
995
|
+
}
|
|
996
|
+
const same = (a, b) => a === b || a !== null && b !== null && a.color === b.color && a.tone === b.tone;
|
|
997
|
+
/** Sets cells (pencil with ink, eraser with `null`), optionally mirrored across the centerline. */
|
|
998
|
+
function paintPartLayer(slot, layer, points, ink, mirror = false) {
|
|
999
|
+
const out = layer.map((row) => [...row]);
|
|
1000
|
+
const { width, height } = PART_SLOTS[slot];
|
|
1001
|
+
for (const [x, y] of points) for (const px of mirror ? [x, mirrorPartX(slot, x)] : [x]) if (px >= 0 && px < width && y >= 0 && y < height) out[y][px] = ink;
|
|
1002
|
+
return out;
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* 4-connected flood fill from a cell: every cell reachable through cells equal to the start
|
|
1006
|
+
* becomes `ink`. With `mirror`, the mirrored start is filled too.
|
|
1007
|
+
*/
|
|
1008
|
+
function fillPartLayer(slot, layer, x, y, ink, mirror = false) {
|
|
1009
|
+
const { width, height } = PART_SLOTS[slot];
|
|
1010
|
+
let out = layer.map((row) => [...row]);
|
|
1011
|
+
for (const sx of mirror ? [x, mirrorPartX(slot, x)] : [x]) {
|
|
1012
|
+
if (sx < 0 || sx >= width || y < 0 || y >= height) continue;
|
|
1013
|
+
const target = out[y][sx];
|
|
1014
|
+
if (same(target, ink)) continue;
|
|
1015
|
+
const next = out.map((row) => [...row]);
|
|
1016
|
+
const stack = [[sx, y]];
|
|
1017
|
+
while (stack.length) {
|
|
1018
|
+
const [cx, cy] = stack.pop();
|
|
1019
|
+
if (cx < 0 || cx >= width || cy < 0 || cy >= height) continue;
|
|
1020
|
+
if (!same(next[cy][cx], target)) continue;
|
|
1021
|
+
next[cy][cx] = ink;
|
|
1022
|
+
stack.push([cx + 1, cy], [cx - 1, cy], [cx, cy + 1], [cx, cy - 1]);
|
|
1023
|
+
}
|
|
1024
|
+
out = next;
|
|
1025
|
+
}
|
|
1026
|
+
return out;
|
|
1027
|
+
}
|
|
1028
|
+
const K = Uint32Array.from("428a2f98 71374491 b5c0fbcf e9b5dba5 3956c25b 59f111f1 923f82a4 ab1c5ed5 d807aa98 12835b01 243185be 550c7dc3 72be5d74 80deb1fe 9bdc06a7 c19bf174 e49b69c1 efbe4786 0fc19dc6 240ca1cc 2de92c6f 4a7484aa 5cb0a9dc 76f988da 983e5152 a831c66d b00327c8 bf597fc7 c6e00bf3 d5a79147 06ca6351 14292967 27b70a85 2e1b2138 4d2c6dfc 53380d13 650a7354 766a0abb 81c2c92e 92722c85 a2bfe8a1 a81a664b c24b8b70 c76c51a3 d192e819 d6990624 f40e3585 106aa070 19a4c116 1e376c08 2748774c 34b0bcb5 391c0cb3 4ed8aa4a 5b9cca4f 682e6ff3 748f82ee 78a5636f 84c87814 8cc70208 90befffa a4506ceb bef9a3f7 c67178f2".split(" ").map((h) => Number.parseInt(h, 16)));
|
|
1029
|
+
function sha256(text) {
|
|
1030
|
+
const bytes = new TextEncoder().encode(text);
|
|
1031
|
+
const length = Math.ceil((bytes.length + 9) / 64) * 64;
|
|
1032
|
+
const data = new Uint8Array(length);
|
|
1033
|
+
data.set(bytes);
|
|
1034
|
+
data[bytes.length] = 128;
|
|
1035
|
+
const view = new DataView(data.buffer);
|
|
1036
|
+
view.setUint32(length - 8, Math.floor(bytes.length * 8 / 2 ** 32));
|
|
1037
|
+
view.setUint32(length - 4, bytes.length * 8 >>> 0);
|
|
1038
|
+
const h = Uint32Array.from([
|
|
1039
|
+
1779033703,
|
|
1040
|
+
3144134277,
|
|
1041
|
+
1013904242,
|
|
1042
|
+
2773480762,
|
|
1043
|
+
1359893119,
|
|
1044
|
+
2600822924,
|
|
1045
|
+
528734635,
|
|
1046
|
+
1541459225
|
|
1047
|
+
]);
|
|
1048
|
+
const w = new Uint32Array(64);
|
|
1049
|
+
const rotr = (v, n) => v >>> n | v << 32 - n;
|
|
1050
|
+
for (let offset = 0; offset < length; offset += 64) {
|
|
1051
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4);
|
|
1052
|
+
for (let i = 16; i < 64; i++) {
|
|
1053
|
+
const a = w[i - 15];
|
|
1054
|
+
const b = w[i - 2];
|
|
1055
|
+
const s0 = rotr(a, 7) ^ rotr(a, 18) ^ a >>> 3;
|
|
1056
|
+
const s1 = rotr(b, 17) ^ rotr(b, 19) ^ b >>> 10;
|
|
1057
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
|
|
1058
|
+
}
|
|
1059
|
+
let [a, b, c, d, e, f, g, k] = h;
|
|
1060
|
+
for (let i = 0; i < 64; i++) {
|
|
1061
|
+
const t1 = k + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K[i] + w[i] >>> 0;
|
|
1062
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
|
|
1063
|
+
k = g;
|
|
1064
|
+
g = f;
|
|
1065
|
+
f = e;
|
|
1066
|
+
e = d + t1 >>> 0;
|
|
1067
|
+
d = c;
|
|
1068
|
+
c = b;
|
|
1069
|
+
b = a;
|
|
1070
|
+
a = t1 + t2 >>> 0;
|
|
1071
|
+
}
|
|
1072
|
+
[
|
|
1073
|
+
a,
|
|
1074
|
+
b,
|
|
1075
|
+
c,
|
|
1076
|
+
d,
|
|
1077
|
+
e,
|
|
1078
|
+
f,
|
|
1079
|
+
g,
|
|
1080
|
+
k
|
|
1081
|
+
].forEach((v, i) => h[i] = h[i] + v >>> 0);
|
|
1082
|
+
}
|
|
1083
|
+
return [...h].map((v) => v.toString(16).padStart(8, "0")).join("");
|
|
1084
|
+
}
|
|
1085
|
+
|
|
829
1086
|
//#endregion
|
|
830
1087
|
//#region src/figure.ts
|
|
831
1088
|
const N = 32;
|
|
@@ -856,6 +1113,17 @@ const mix$1 = (base, tint, k) => {
|
|
|
856
1113
|
const t = channels$1(tint);
|
|
857
1114
|
return hex(channels$1(base).map((v, i) => v * (1 - k) + t[i] * k));
|
|
858
1115
|
};
|
|
1116
|
+
const TONES = {
|
|
1117
|
+
[-2]: (c) => shade(c, .62),
|
|
1118
|
+
[-1]: (c) => shade(c, .8),
|
|
1119
|
+
0: (c) => c,
|
|
1120
|
+
1: (c) => mix$1(c, WHITE, .25),
|
|
1121
|
+
2: (c) => mix$1(c, WHITE, .5)
|
|
1122
|
+
};
|
|
1123
|
+
/** The color a Custom Part cell shows at a tone step on the rig's shade ramp. */
|
|
1124
|
+
function partToneColor(color, tone) {
|
|
1125
|
+
return TONES[tone](color.toLowerCase());
|
|
1126
|
+
}
|
|
859
1127
|
function paint(grid, test, color) {
|
|
860
1128
|
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (test(x, y)) grid[y][x] = color;
|
|
861
1129
|
}
|
|
@@ -1172,10 +1440,18 @@ function detailedHair(recipe, s, b) {
|
|
|
1172
1440
|
const length = (recipe.hairLength ?? 0) * 2;
|
|
1173
1441
|
const lengthen = (mask) => (x, y) => mask(x, y > 18 ? Math.round(y - length) : y);
|
|
1174
1442
|
const sideOf = (sign) => (x, y) => y > 7 && sign * (x + .5 - C - s) >= 7 && lengthen((sign < 0 ? side : right).front)(x, y);
|
|
1443
|
+
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);
|
|
1444
|
+
const behind = length === 0 ? back.back : lengthen(back.back);
|
|
1175
1445
|
return {
|
|
1176
|
-
front: (x, y) =>
|
|
1446
|
+
front: (x, y) => fringe(x, y) || sideOf(-1)(x, y) || sideOf(1)(x, y),
|
|
1177
1447
|
sides: [sideOf(-1), sideOf(1)],
|
|
1178
|
-
|
|
1448
|
+
pieces: {
|
|
1449
|
+
bangs: fringe,
|
|
1450
|
+
leftSideHair: sideOf(-1),
|
|
1451
|
+
rightSideHair: sideOf(1),
|
|
1452
|
+
backHair: behind
|
|
1453
|
+
},
|
|
1454
|
+
back: behind,
|
|
1179
1455
|
...back.ties ? { ties: back.ties } : {},
|
|
1180
1456
|
...back.bands ? { bands: back.bands } : {}
|
|
1181
1457
|
};
|
|
@@ -1622,7 +1898,7 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
1622
1898
|
const covered = recipe.accessory === "helmet" || recipe.accessory === "hood";
|
|
1623
1899
|
const bare = () => false;
|
|
1624
1900
|
const petalRing = flower ? petals(recipe.petals ?? "trumpet", s) : bare;
|
|
1625
|
-
const
|
|
1901
|
+
const builtin = flower ? {
|
|
1626
1902
|
back: petalRing,
|
|
1627
1903
|
front: bare,
|
|
1628
1904
|
sides: [(x, y) => petalRing(x, y) && x + .5 < C + s, (x, y) => petalRing(x, y) && x + .5 >= C + s]
|
|
@@ -1630,6 +1906,40 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
1630
1906
|
back: bare,
|
|
1631
1907
|
front: bare
|
|
1632
1908
|
} : recipe.bangs === void 0 ? hair(recipe, s, b) : detailedHair(recipe, s, b);
|
|
1909
|
+
const drawnHair = HAIR_PART_SLOTS.flatMap((slot) => {
|
|
1910
|
+
const part = builtin.pieces ? wornPart(recipe, slot) : void 0;
|
|
1911
|
+
return part ? [{
|
|
1912
|
+
slot,
|
|
1913
|
+
part
|
|
1914
|
+
}] : [];
|
|
1915
|
+
});
|
|
1916
|
+
const live = (cells, shift) => {
|
|
1917
|
+
const keys = new Set(cells.flatMap(([x, y, color, tone]) => color === "hairColor" && tone === 0 ? [`${x + shift},${y}`] : []));
|
|
1918
|
+
return (x, y) => keys.has(`${x},${y}`);
|
|
1919
|
+
};
|
|
1920
|
+
const backShift = () => {
|
|
1921
|
+
if (s === b || !builtin.pieces) return s;
|
|
1922
|
+
const front = detailedHair(detailedRecipe(recipe), 0, 0).pieces.backHair;
|
|
1923
|
+
const misses = (shift) => {
|
|
1924
|
+
let count = 0;
|
|
1925
|
+
for (let y = 0; y < N; y++) for (let x = -4; x < N + 4; x++) if (front(x, y) !== builtin.pieces.backHair(x + shift, y)) count++;
|
|
1926
|
+
return count;
|
|
1927
|
+
};
|
|
1928
|
+
return misses(b) < misses(s) ? b : s;
|
|
1929
|
+
};
|
|
1930
|
+
const piece = (slot) => {
|
|
1931
|
+
const part = drawnHair.find((drawn) => drawn.slot === slot)?.part;
|
|
1932
|
+
if (!part) return builtin.pieces[slot];
|
|
1933
|
+
return live(part.front, slot === "backHair" ? backShift() : s);
|
|
1934
|
+
};
|
|
1935
|
+
const masks = drawnHair.length === 0 ? builtin : {
|
|
1936
|
+
...builtin,
|
|
1937
|
+
front: (x, y) => piece("bangs")(x, y) || piece("leftSideHair")(x, y) || piece("rightSideHair")(x, y),
|
|
1938
|
+
sides: [piece("leftSideHair"), piece("rightSideHair")],
|
|
1939
|
+
back: piece("backHair"),
|
|
1940
|
+
...drawnHair.some((drawn) => drawn.slot === "backHair") ? { ties: [] } : {}
|
|
1941
|
+
};
|
|
1942
|
+
const inked = (slot) => drawnHair.filter((drawn) => slot === "all-front" ? drawn.slot !== "backHair" : drawn.slot === slot).flatMap((drawn) => drawn.part.front.filter(([, , color, tone]) => !(color === "hairColor" && tone === 0)));
|
|
1633
1943
|
const body = blank();
|
|
1634
1944
|
if (flower) flowerBase(recipe, body, s);
|
|
1635
1945
|
else outfit(recipe, body, s);
|
|
@@ -1640,6 +1950,23 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
1640
1950
|
paint(back, (x, y) => petalRing(x, y) && x + .5 < C + s - 3 && y < 10, mix$1(hairColor, WHITE, .3));
|
|
1641
1951
|
} else paint(back, masks.back, shade(hairColor, .82));
|
|
1642
1952
|
if (masks.bands) paint(back, (x, y) => masks.back(x, y) && (y + Math.floor(x / 2)) % 4 === 0, shade(hairColor, .68));
|
|
1953
|
+
const headpiece = wornPart(recipe, "headpiece");
|
|
1954
|
+
const placeCells = (grid, cells, compress = true) => {
|
|
1955
|
+
const placed = cells.map(([x, y, color, tone]) => {
|
|
1956
|
+
const dx = x + .5 - C;
|
|
1957
|
+
const far = compress && turned && d * dx < 0 && Math.abs(dx) >= 5;
|
|
1958
|
+
const base = color.startsWith("#") ? color.toLowerCase() : recipe[color];
|
|
1959
|
+
return {
|
|
1960
|
+
x: x + s + (far ? d : 0),
|
|
1961
|
+
y,
|
|
1962
|
+
c: TONES[tone](base),
|
|
1963
|
+
far
|
|
1964
|
+
};
|
|
1965
|
+
});
|
|
1966
|
+
for (const cell of [...placed.filter((p) => p.far), ...placed.filter((p) => !p.far)]) if (cell.x >= 0 && cell.x < N) grid[cell.y][cell.x] = cell.c;
|
|
1967
|
+
};
|
|
1968
|
+
placeCells(back, inked("backHair"), false);
|
|
1969
|
+
if (headpiece) placeCells(back, headpiece.back);
|
|
1643
1970
|
const head = blank();
|
|
1644
1971
|
paint(head, face, skin);
|
|
1645
1972
|
const h = recipe.height ?? 0;
|
|
@@ -1728,11 +2055,15 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
1728
2055
|
}
|
|
1729
2056
|
return out;
|
|
1730
2057
|
};
|
|
2058
|
+
const crown = blank();
|
|
2059
|
+
placeCells(crown, inked("all-front"), false);
|
|
2060
|
+
if (headpiece) placeCells(crown, headpiece.front);
|
|
1731
2061
|
const figure = blank();
|
|
1732
2062
|
for (const layer of [
|
|
1733
2063
|
back,
|
|
1734
2064
|
body,
|
|
1735
|
-
shadeHair(head)
|
|
2065
|
+
shadeHair(head),
|
|
2066
|
+
crown
|
|
1736
2067
|
]) for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (layer[y][x]) figure[y][x] = layer[y][x];
|
|
1737
2068
|
const features = blank();
|
|
1738
2069
|
const eyes = blank();
|
|
@@ -1879,7 +2210,7 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
1879
2210
|
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
|
|
1880
2211
|
const c = full[y][x];
|
|
1881
2212
|
if (!c) continue;
|
|
1882
|
-
if (y >= 22 && !head[y][x] && !back[y][x]) bodyCells[y][x] = c;
|
|
2213
|
+
if (y >= 22 && !head[y][x] && !back[y][x] && !crown[y][x]) bodyCells[y][x] = c;
|
|
1883
2214
|
else headCells[y][x] = c;
|
|
1884
2215
|
}
|
|
1885
2216
|
const cells = [];
|
|
@@ -1904,6 +2235,40 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
1904
2235
|
].join("")
|
|
1905
2236
|
};
|
|
1906
2237
|
}
|
|
2238
|
+
/**
|
|
2239
|
+
* The hair piece in `slot` as a Custom Part to start drawing from: the drawn part worn there,
|
|
2240
|
+
* or the built-in piece flattened for this recipe in the front pose as live `hairColor` cells.
|
|
2241
|
+
* A flattened piece keeps this shape and no longer follows face shape or hair length.
|
|
2242
|
+
*/
|
|
2243
|
+
function hairPieceStart(recipe, slot) {
|
|
2244
|
+
const worn = wornPart(recipe, slot);
|
|
2245
|
+
if (worn) return {
|
|
2246
|
+
slot,
|
|
2247
|
+
front: [...worn.front],
|
|
2248
|
+
back: []
|
|
2249
|
+
};
|
|
2250
|
+
const built = detailedHair(detailedRecipe(recipe), 0, 0);
|
|
2251
|
+
const mask = built.pieces[slot];
|
|
2252
|
+
const ties = new Set(slot === "backHair" ? (built.ties ?? []).map(([x, y]) => `${Math.round(x)},${y}`) : []);
|
|
2253
|
+
const front = [];
|
|
2254
|
+
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (ties.has(`${x},${y}`)) front.push([
|
|
2255
|
+
x,
|
|
2256
|
+
y,
|
|
2257
|
+
"shirtColor",
|
|
2258
|
+
-1
|
|
2259
|
+
]);
|
|
2260
|
+
else if (mask(x, y)) front.push([
|
|
2261
|
+
x,
|
|
2262
|
+
y,
|
|
2263
|
+
"hairColor",
|
|
2264
|
+
0
|
|
2265
|
+
]);
|
|
2266
|
+
return {
|
|
2267
|
+
slot,
|
|
2268
|
+
front,
|
|
2269
|
+
back: []
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
1907
2272
|
|
|
1908
2273
|
//#endregion
|
|
1909
2274
|
//#region src/svg.ts
|
|
@@ -2427,5 +2792,5 @@ function symbolArtCells(rows, color) {
|
|
|
2427
2792
|
}
|
|
2428
2793
|
|
|
2429
2794
|
//#endregion
|
|
2430
|
-
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, DEFAULT_RECIPE, PIXEL_SYMBOLS, canonicalRecipe, createSeededRecipe, createSeededRecipeV2, detailedRecipe, faceCells, hiddenChoices, isPixelAvatarRecipe, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, seededRandom, seededRecipe, seededRecipeV2, symbolArtCells, withSpecies };
|
|
2795
|
+
export { AVATAR_COLORS, AVATAR_EXTRA_PARTS, AVATAR_HAIR_PARTS, AVATAR_PARTS, AVATAR_PARTS_V2, AVATAR_PIECE_COLORS, AVATAR_PRESETS, AVATAR_RANGES, AVATAR_SPECIES, AVATAR_SPECIES_SWATCHES, AVATAR_SWATCHES, AVATAR_TURNS, CUSTOM_PART_KEYS, DEFAULT_RECIPE, HAIR_PART_SLOTS, MAX_PART_FIXED_COLORS, PART_LAYERS, PART_SLOTS, PART_TONES, PIXEL_SYMBOLS, canonicalCustomPart, canonicalRecipe, createCustomPart, createSeededRecipe, createSeededRecipeV2, customPartId, detailedRecipe, emptyPartLayer, faceCells, fillPartLayer, hairPieceStart, hiddenChoices, isHairPartSlot, isPixelAvatarRecipe, isPixelCustomPart, mirrorPartX, paintPartLayer, partCells, partLayer, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, seededRandom, seededRecipe, seededRecipeV2, symbolArtCells, withCustomPart, withHeadpiece, withSpecies, wornPart };
|
|
2431
2796
|
//# sourceMappingURL=index.js.map
|