@botharness/pixel-avatar 0.3.0 → 0.5.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 +126 -12
- package/dist/index.js +571 -46
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,191 @@
|
|
|
1
1
|
import { escapeAttribute } from "@botharness/pixel-morph";
|
|
2
2
|
|
|
3
|
+
//#region src/part.ts
|
|
4
|
+
/**
|
|
5
|
+
* Custom Part slots. A headpiece covers the top of the tile, around the hair: its back layer is
|
|
6
|
+
* drawn behind the hair and its front layer over it. Cells are in tile coordinates, so the
|
|
7
|
+
* Avatar centerline sits between x = 15 and x = 16.
|
|
8
|
+
*/
|
|
9
|
+
const PART_SLOTS = { headpiece: {
|
|
10
|
+
width: 32,
|
|
11
|
+
height: 16
|
|
12
|
+
} };
|
|
13
|
+
/** Tone steps on the rig's shade ramp: two darker, the color itself, two lighter. */
|
|
14
|
+
const PART_TONES = [
|
|
15
|
+
-2,
|
|
16
|
+
-1,
|
|
17
|
+
0,
|
|
18
|
+
1,
|
|
19
|
+
2
|
|
20
|
+
];
|
|
21
|
+
const PART_LAYERS = ["front", "back"];
|
|
22
|
+
/** Most fixed colors one part may use. */
|
|
23
|
+
const MAX_PART_FIXED_COLORS = 16;
|
|
24
|
+
const FIXED = /^#[\da-f]{6}$/iu;
|
|
25
|
+
const isColor = (value) => typeof value === "string" && (AVATAR_COLORS.includes(value) || FIXED.test(value));
|
|
26
|
+
function isPixelCustomPart(value) {
|
|
27
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
28
|
+
const r = value;
|
|
29
|
+
if (Object.keys(r).length !== 3 || !Object.hasOwn(PART_SLOTS, r["slot"])) return false;
|
|
30
|
+
const { width, height } = PART_SLOTS[r["slot"]];
|
|
31
|
+
const fixed = /* @__PURE__ */ new Set();
|
|
32
|
+
for (const name of PART_LAYERS) {
|
|
33
|
+
const cells = r[name];
|
|
34
|
+
if (!Array.isArray(cells) || cells.length > width * height) return false;
|
|
35
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36
|
+
for (const cell of cells) {
|
|
37
|
+
if (!Array.isArray(cell) || cell.length !== 4) return false;
|
|
38
|
+
const [x, y, color, tone] = cell;
|
|
39
|
+
if (!Number.isInteger(x) || !Number.isInteger(y) || x < 0 || x >= width || y < 0 || y >= height || !isColor(color) || !PART_TONES.includes(tone)) return false;
|
|
40
|
+
const key = y * width + x;
|
|
41
|
+
if (seen.has(key)) return false;
|
|
42
|
+
seen.add(key);
|
|
43
|
+
if (color.startsWith("#")) fixed.add(color.toLowerCase());
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return fixed.size <= MAX_PART_FIXED_COLORS;
|
|
47
|
+
}
|
|
48
|
+
const canonicalCells = (cells) => cells.map(([x, y, color, tone]) => [
|
|
49
|
+
x,
|
|
50
|
+
y,
|
|
51
|
+
color.startsWith("#") ? color.toLowerCase() : color,
|
|
52
|
+
tone
|
|
53
|
+
]).sort((a, b) => a[1] - b[1] || a[0] - b[0]);
|
|
54
|
+
/** The part with cells in row order and fixed colors in lowercase. */
|
|
55
|
+
function canonicalCustomPart(part) {
|
|
56
|
+
return {
|
|
57
|
+
slot: part.slot,
|
|
58
|
+
front: canonicalCells(part.front),
|
|
59
|
+
back: canonicalCells(part.back)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Content identity: a SHA-256 of the canonical slot, cells and color references. */
|
|
63
|
+
function customPartId(part) {
|
|
64
|
+
return sha256(JSON.stringify(canonicalCustomPart(part)));
|
|
65
|
+
}
|
|
66
|
+
function emptyPartLayer(slot) {
|
|
67
|
+
const { width, height } = PART_SLOTS[slot];
|
|
68
|
+
return Array.from({ length: height }, () => Array(width).fill(null));
|
|
69
|
+
}
|
|
70
|
+
function partLayer(slot, cells) {
|
|
71
|
+
const layer = emptyPartLayer(slot);
|
|
72
|
+
for (const [x, y, color, tone] of cells) layer[y][x] = {
|
|
73
|
+
color,
|
|
74
|
+
tone
|
|
75
|
+
};
|
|
76
|
+
return layer;
|
|
77
|
+
}
|
|
78
|
+
function partCells(layer) {
|
|
79
|
+
return layer.flatMap((row, y) => row.flatMap((ink, x) => ink ? [[
|
|
80
|
+
x,
|
|
81
|
+
y,
|
|
82
|
+
ink.color,
|
|
83
|
+
ink.tone
|
|
84
|
+
]] : []));
|
|
85
|
+
}
|
|
86
|
+
/** Builds a canonical part from two drawn layers. */
|
|
87
|
+
function createCustomPart(slot, layers) {
|
|
88
|
+
return canonicalCustomPart({
|
|
89
|
+
slot,
|
|
90
|
+
front: partCells(layers.front),
|
|
91
|
+
back: partCells(layers.back)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** The column mirrored across the Avatar centerline. */
|
|
95
|
+
function mirrorPartX(slot, x) {
|
|
96
|
+
return PART_SLOTS[slot].width - 1 - x;
|
|
97
|
+
}
|
|
98
|
+
const same = (a, b) => a === b || a !== null && b !== null && a.color === b.color && a.tone === b.tone;
|
|
99
|
+
/** Sets cells (pencil with ink, eraser with `null`), optionally mirrored across the centerline. */
|
|
100
|
+
function paintPartLayer(slot, layer, points, ink, mirror = false) {
|
|
101
|
+
const out = layer.map((row) => [...row]);
|
|
102
|
+
const { width, height } = PART_SLOTS[slot];
|
|
103
|
+
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;
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 4-connected flood fill from a cell: every cell reachable through cells equal to the start
|
|
108
|
+
* becomes `ink`. With `mirror`, the mirrored start is filled too.
|
|
109
|
+
*/
|
|
110
|
+
function fillPartLayer(slot, layer, x, y, ink, mirror = false) {
|
|
111
|
+
const { width, height } = PART_SLOTS[slot];
|
|
112
|
+
let out = layer.map((row) => [...row]);
|
|
113
|
+
for (const sx of mirror ? [x, mirrorPartX(slot, x)] : [x]) {
|
|
114
|
+
if (sx < 0 || sx >= width || y < 0 || y >= height) continue;
|
|
115
|
+
const target = out[y][sx];
|
|
116
|
+
if (same(target, ink)) continue;
|
|
117
|
+
const next = out.map((row) => [...row]);
|
|
118
|
+
const stack = [[sx, y]];
|
|
119
|
+
while (stack.length) {
|
|
120
|
+
const [cx, cy] = stack.pop();
|
|
121
|
+
if (cx < 0 || cx >= width || cy < 0 || cy >= height) continue;
|
|
122
|
+
if (!same(next[cy][cx], target)) continue;
|
|
123
|
+
next[cy][cx] = ink;
|
|
124
|
+
stack.push([cx + 1, cy], [cx - 1, cy], [cx, cy + 1], [cx, cy - 1]);
|
|
125
|
+
}
|
|
126
|
+
out = next;
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
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)));
|
|
131
|
+
function sha256(text) {
|
|
132
|
+
const bytes = new TextEncoder().encode(text);
|
|
133
|
+
const length = Math.ceil((bytes.length + 9) / 64) * 64;
|
|
134
|
+
const data = new Uint8Array(length);
|
|
135
|
+
data.set(bytes);
|
|
136
|
+
data[bytes.length] = 128;
|
|
137
|
+
const view = new DataView(data.buffer);
|
|
138
|
+
view.setUint32(length - 8, Math.floor(bytes.length * 8 / 2 ** 32));
|
|
139
|
+
view.setUint32(length - 4, bytes.length * 8 >>> 0);
|
|
140
|
+
const h = Uint32Array.from([
|
|
141
|
+
1779033703,
|
|
142
|
+
3144134277,
|
|
143
|
+
1013904242,
|
|
144
|
+
2773480762,
|
|
145
|
+
1359893119,
|
|
146
|
+
2600822924,
|
|
147
|
+
528734635,
|
|
148
|
+
1541459225
|
|
149
|
+
]);
|
|
150
|
+
const w = new Uint32Array(64);
|
|
151
|
+
const rotr = (v, n) => v >>> n | v << 32 - n;
|
|
152
|
+
for (let offset = 0; offset < length; offset += 64) {
|
|
153
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4);
|
|
154
|
+
for (let i = 16; i < 64; i++) {
|
|
155
|
+
const a = w[i - 15];
|
|
156
|
+
const b = w[i - 2];
|
|
157
|
+
const s0 = rotr(a, 7) ^ rotr(a, 18) ^ a >>> 3;
|
|
158
|
+
const s1 = rotr(b, 17) ^ rotr(b, 19) ^ b >>> 10;
|
|
159
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
|
|
160
|
+
}
|
|
161
|
+
let [a, b, c, d, e, f, g, k] = h;
|
|
162
|
+
for (let i = 0; i < 64; i++) {
|
|
163
|
+
const t1 = k + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K[i] + w[i] >>> 0;
|
|
164
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
|
|
165
|
+
k = g;
|
|
166
|
+
g = f;
|
|
167
|
+
f = e;
|
|
168
|
+
e = d + t1 >>> 0;
|
|
169
|
+
d = c;
|
|
170
|
+
c = b;
|
|
171
|
+
b = a;
|
|
172
|
+
a = t1 + t2 >>> 0;
|
|
173
|
+
}
|
|
174
|
+
[
|
|
175
|
+
a,
|
|
176
|
+
b,
|
|
177
|
+
c,
|
|
178
|
+
d,
|
|
179
|
+
e,
|
|
180
|
+
f,
|
|
181
|
+
g,
|
|
182
|
+
k
|
|
183
|
+
].forEach((v, i) => h[i] = h[i] + v >>> 0);
|
|
184
|
+
}
|
|
185
|
+
return [...h].map((v) => v.toString(16).padStart(8, "0")).join("");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
3
189
|
//#region src/random.ts
|
|
4
190
|
function seededRandom(seed) {
|
|
5
191
|
let h = 2166136261;
|
|
@@ -323,9 +509,48 @@ const AVATAR_COLORS = [
|
|
|
323
509
|
* Avatar Species: a base on the pixel bust rig. Species share motion and anchors; each sets its
|
|
324
510
|
* own ears, face details and suggested body colors. A recipe with a species is asset version 2.
|
|
325
511
|
*/
|
|
326
|
-
const AVATAR_SPECIES = [
|
|
512
|
+
const AVATAR_SPECIES = [
|
|
513
|
+
"human",
|
|
514
|
+
"goblin",
|
|
515
|
+
"elf",
|
|
516
|
+
"dwarf",
|
|
517
|
+
"orc",
|
|
518
|
+
"flower"
|
|
519
|
+
];
|
|
327
520
|
/** Optional per-piece hair colors (asset version 2); an absent piece uses `hairColor`. */
|
|
328
521
|
const AVATAR_PIECE_COLORS = ["leftSideHairColor", "rightSideHairColor"];
|
|
522
|
+
/** Part choices that exist only in asset version 2, added after every version 1 choice. */
|
|
523
|
+
const AVATAR_PARTS_V2 = {
|
|
524
|
+
...AVATAR_PARTS,
|
|
525
|
+
outfit: [
|
|
526
|
+
...AVATAR_PARTS.outfit,
|
|
527
|
+
"armor",
|
|
528
|
+
"robe",
|
|
529
|
+
"tunic",
|
|
530
|
+
"cloak"
|
|
531
|
+
],
|
|
532
|
+
accessory: [
|
|
533
|
+
...AVATAR_PARTS.accessory,
|
|
534
|
+
"helmet",
|
|
535
|
+
"hood"
|
|
536
|
+
]
|
|
537
|
+
};
|
|
538
|
+
/** Optional asset version 2 parts; an absent part is not drawn (or uses the first choice). */
|
|
539
|
+
const AVATAR_EXTRA_PARTS = {
|
|
540
|
+
beard: [
|
|
541
|
+
"short",
|
|
542
|
+
"full",
|
|
543
|
+
"braided"
|
|
544
|
+
],
|
|
545
|
+
petals: [
|
|
546
|
+
"trumpet",
|
|
547
|
+
"daisy",
|
|
548
|
+
"sunflower",
|
|
549
|
+
"tulip",
|
|
550
|
+
"sakura"
|
|
551
|
+
],
|
|
552
|
+
flowerBase: ["leaves", "pot"]
|
|
553
|
+
};
|
|
329
554
|
const DEFAULT_RECIPE = {
|
|
330
555
|
schemaVersion: 1,
|
|
331
556
|
family: "illustrated",
|
|
@@ -354,16 +579,20 @@ function isPixelAvatarRecipe(value) {
|
|
|
354
579
|
const r = value;
|
|
355
580
|
const detailed = DETAIL_KEYS.filter((key) => Object.hasOwn(r, key)).length;
|
|
356
581
|
if (detailed !== 0 && detailed !== DETAIL_KEYS.length) return false;
|
|
357
|
-
const
|
|
582
|
+
const v3 = r["assetVersion"] === 3;
|
|
583
|
+
const v2 = r["assetVersion"] === 2 || v3;
|
|
584
|
+
if (v3 ? !isPixelCustomPart(r["headpiece"]) : Object.hasOwn(r, "headpiece")) return false;
|
|
358
585
|
const pieces = AVATAR_PIECE_COLORS.filter((key) => Object.hasOwn(r, key));
|
|
359
|
-
const
|
|
586
|
+
const extras = Object.keys(AVATAR_EXTRA_PARTS).filter((key) => Object.hasOwn(r, key));
|
|
587
|
+
const v2Keys = v2 ? 2 + pieces.length + extras.length + (v3 ? 1 : 0) : 0;
|
|
360
588
|
if (Object.keys(r).length !== Object.keys(DEFAULT_RECIPE).length + detailed + v2Keys) return false;
|
|
361
|
-
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])))) return false;
|
|
589
|
+
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;
|
|
590
|
+
const catalog = v2 ? AVATAR_PARTS_V2 : AVATAR_PARTS;
|
|
362
591
|
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) => {
|
|
363
592
|
const v = r[key];
|
|
364
593
|
const [min, max] = AVATAR_RANGES[key];
|
|
365
594
|
return typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
|
|
366
|
-
})) && r["schemaVersion"] === 1 && r["family"] === "illustrated" && (r["assetVersion"] === 1 || v2) && r["rigVersion"] === 1 && PART_KEYS.every((key) => typeof r[key] === "string" &&
|
|
595
|
+
})) && r["schemaVersion"] === 1 && r["family"] === "illustrated" && (r["assetVersion"] === 1 || v2) && r["rigVersion"] === 1 && PART_KEYS.every((key) => typeof r[key] === "string" && catalog[key].includes(r[key])) && AVATAR_COLORS.every((key) => typeof r[key] === "string" && /^#[\da-f]{6}$/iu.test(r[key]));
|
|
367
596
|
}
|
|
368
597
|
function canonicalRecipe(recipe) {
|
|
369
598
|
const canonical = {
|
|
@@ -375,31 +604,71 @@ function canonicalRecipe(recipe) {
|
|
|
375
604
|
for (const key of PART_KEYS) canonical[key] = recipe[key];
|
|
376
605
|
for (const key of AVATAR_COLORS) canonical[key] = recipe[key].toLowerCase();
|
|
377
606
|
if (recipe.bangs !== void 0) for (const key of DETAIL_KEYS) canonical[key] = recipe[key];
|
|
378
|
-
if (recipe.assetVersion
|
|
607
|
+
if (recipe.assetVersion !== 1) {
|
|
379
608
|
canonical["species"] = recipe.species;
|
|
380
609
|
canonical["rightSideHair"] = recipe.rightSideHair;
|
|
381
610
|
for (const key of AVATAR_PIECE_COLORS) if (recipe[key] !== void 0) canonical[key] = recipe[key].toLowerCase();
|
|
611
|
+
for (const key of Object.keys(AVATAR_EXTRA_PARTS)) if (recipe[key] !== void 0) canonical[key] = recipe[key];
|
|
612
|
+
if (recipe.assetVersion === 3) canonical["headpiece"] = canonicalCustomPart(recipe.headpiece);
|
|
382
613
|
}
|
|
383
614
|
return canonical;
|
|
384
615
|
}
|
|
616
|
+
const HAIR_PIECES = [
|
|
617
|
+
"bangs",
|
|
618
|
+
"sideHair",
|
|
619
|
+
"rightSideHair",
|
|
620
|
+
"backHair"
|
|
621
|
+
];
|
|
385
622
|
/**
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
* species' first suggestion; a custom color is kept.
|
|
623
|
+
* Saved choices the recipe keeps but does not draw: a flower shows petals and a stem instead of
|
|
624
|
+
* hair, outfit, beard and headwear, and a helmet or hood covers the hair. The choices return when
|
|
625
|
+
* the species or headwear changes back.
|
|
390
626
|
*/
|
|
627
|
+
function hiddenChoices(recipe) {
|
|
628
|
+
if (recipe.species === "flower") return [
|
|
629
|
+
...HAIR_PIECES,
|
|
630
|
+
"outfit",
|
|
631
|
+
"accessory",
|
|
632
|
+
"eyes",
|
|
633
|
+
"brows",
|
|
634
|
+
"nose",
|
|
635
|
+
"cheeks",
|
|
636
|
+
"glasses",
|
|
637
|
+
...recipe.beard ? ["beard"] : []
|
|
638
|
+
];
|
|
639
|
+
const hidden = [];
|
|
640
|
+
if (recipe.accessory === "helmet" || recipe.accessory === "hood") hidden.push(...HAIR_PIECES);
|
|
641
|
+
if (recipe.species === "dwarf") hidden.push("nose");
|
|
642
|
+
return hidden;
|
|
643
|
+
}
|
|
391
644
|
function withSpecies(recipe, species) {
|
|
392
645
|
const detailed = detailedRecipe(recipe);
|
|
393
646
|
const previous = recipe.species ?? "human";
|
|
394
647
|
const suggested = AVATAR_SPECIES_SWATCHES[previous].includes(recipe.skinColor.toLowerCase());
|
|
395
648
|
return {
|
|
396
649
|
...detailed,
|
|
397
|
-
assetVersion: 2,
|
|
650
|
+
assetVersion: recipe.assetVersion === 3 ? 3 : 2,
|
|
398
651
|
species,
|
|
399
652
|
rightSideHair: detailed.rightSideHair ?? detailed.sideHair,
|
|
400
653
|
skinColor: suggested && previous !== species ? AVATAR_SPECIES_SWATCHES[species][0] : recipe.skinColor
|
|
401
654
|
};
|
|
402
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* Returns the recipe wearing a Custom Part in the headpiece slot (asset version 3), or without
|
|
658
|
+
* one (asset version 2). The recipe embeds its own copy of the part.
|
|
659
|
+
*/
|
|
660
|
+
function withHeadpiece(recipe, part) {
|
|
661
|
+
const { headpiece: _, ...rest } = recipe.assetVersion === 1 ? withSpecies(recipe, "human") : recipe;
|
|
662
|
+
if (part === void 0) return {
|
|
663
|
+
...rest,
|
|
664
|
+
assetVersion: 2
|
|
665
|
+
};
|
|
666
|
+
return {
|
|
667
|
+
...rest,
|
|
668
|
+
assetVersion: 3,
|
|
669
|
+
headpiece: canonicalCustomPart(part)
|
|
670
|
+
};
|
|
671
|
+
}
|
|
403
672
|
function detailedRecipe(recipe) {
|
|
404
673
|
if (recipe.bangs !== void 0) return recipe;
|
|
405
674
|
const pick = (part) => AVATAR_HAIR_PARTS[part].includes(recipe.hair) ? recipe.hair : HAIR_SPLIT[recipe.hair][part];
|
|
@@ -551,6 +820,38 @@ function createSeededRecipe(namespace) {
|
|
|
551
820
|
return recipe;
|
|
552
821
|
};
|
|
553
822
|
}
|
|
823
|
+
/**
|
|
824
|
+
* A version 2 name-seeded recipe factory over every species. Species are equally likely; a
|
|
825
|
+
* person may get a beard or a version 2 outfit, and a flower gets petals and a base. Version 1
|
|
826
|
+
* factories (`createSeededRecipe`) keep their faces, so a consumer chooses per identity which
|
|
827
|
+
* seed version it was created with.
|
|
828
|
+
*/
|
|
829
|
+
function createSeededRecipeV2(namespace) {
|
|
830
|
+
const base = createSeededRecipe(`${namespace}:base`);
|
|
831
|
+
return (seed) => {
|
|
832
|
+
const random = seededRandom(`${namespace}:${seed.trim().toLowerCase()}`);
|
|
833
|
+
const pick = (values) => values[Math.floor(random() * values.length)];
|
|
834
|
+
const species = pick(AVATAR_SPECIES);
|
|
835
|
+
const recipe = {
|
|
836
|
+
...withSpecies(base(seed), species),
|
|
837
|
+
skinColor: pick(AVATAR_SPECIES_SWATCHES[species])
|
|
838
|
+
};
|
|
839
|
+
if (species === "flower") return {
|
|
840
|
+
...recipe,
|
|
841
|
+
petals: pick(AVATAR_EXTRA_PARTS.petals),
|
|
842
|
+
flowerBase: pick(AVATAR_EXTRA_PARTS.flowerBase)
|
|
843
|
+
};
|
|
844
|
+
const beard = random() < .3 ? pick(AVATAR_EXTRA_PARTS.beard) : void 0;
|
|
845
|
+
const outfit = random() < .25 ? pick(AVATAR_PARTS_V2.outfit.slice(-4)) : recipe.outfit;
|
|
846
|
+
return {
|
|
847
|
+
...recipe,
|
|
848
|
+
outfit,
|
|
849
|
+
...beard ? { beard } : {}
|
|
850
|
+
};
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
/** Version 2 counterpart of `seededRecipe` (BotHarness's namespace). */
|
|
854
|
+
const seededRecipeV2 = createSeededRecipeV2("botharness-avatar-v2");
|
|
554
855
|
/** The same name always gives the same face (BotHarness's namespace). */
|
|
555
856
|
const seededRecipe = createSeededRecipe("botharness-avatar");
|
|
556
857
|
/** Suggested body colors per species; any color remains allowed. */
|
|
@@ -563,6 +864,38 @@ const AVATAR_SPECIES_SWATCHES = {
|
|
|
563
864
|
"#b3cf7a",
|
|
564
865
|
"#5c8a4a",
|
|
565
866
|
"#8fa86a"
|
|
867
|
+
],
|
|
868
|
+
elf: [
|
|
869
|
+
"#fbe7d6",
|
|
870
|
+
"#f2d3c0",
|
|
871
|
+
"#e2c9b0",
|
|
872
|
+
"#d9c7e8",
|
|
873
|
+
"#b9c6dd",
|
|
874
|
+
"#8f7fa8"
|
|
875
|
+
],
|
|
876
|
+
dwarf: [
|
|
877
|
+
"#f2c4a0",
|
|
878
|
+
"#e9b08a",
|
|
879
|
+
"#e0a87e",
|
|
880
|
+
"#c68863",
|
|
881
|
+
"#f0b8a0",
|
|
882
|
+
"#9a6142"
|
|
883
|
+
],
|
|
884
|
+
orc: [
|
|
885
|
+
"#8a9a5b",
|
|
886
|
+
"#6f8a4e",
|
|
887
|
+
"#7d8c6a",
|
|
888
|
+
"#5f6f4a",
|
|
889
|
+
"#9aa070",
|
|
890
|
+
"#6a7a7a"
|
|
891
|
+
],
|
|
892
|
+
flower: [
|
|
893
|
+
"#f6d36b",
|
|
894
|
+
"#f2b84b",
|
|
895
|
+
"#ffe3cf",
|
|
896
|
+
"#e8a85a",
|
|
897
|
+
"#c98b4a",
|
|
898
|
+
"#f4f1ec"
|
|
566
899
|
]
|
|
567
900
|
};
|
|
568
901
|
const preset = (parts) => ({
|
|
@@ -722,6 +1055,17 @@ const mix$1 = (base, tint, k) => {
|
|
|
722
1055
|
const t = channels$1(tint);
|
|
723
1056
|
return hex(channels$1(base).map((v, i) => v * (1 - k) + t[i] * k));
|
|
724
1057
|
};
|
|
1058
|
+
const TONES = {
|
|
1059
|
+
[-2]: (c) => shade(c, .62),
|
|
1060
|
+
[-1]: (c) => shade(c, .8),
|
|
1061
|
+
0: (c) => c,
|
|
1062
|
+
1: (c) => mix$1(c, WHITE, .25),
|
|
1063
|
+
2: (c) => mix$1(c, WHITE, .5)
|
|
1064
|
+
};
|
|
1065
|
+
/** The color a Custom Part cell shows at a tone step on the rig's shade ramp. */
|
|
1066
|
+
function partToneColor(color, tone) {
|
|
1067
|
+
return TONES[tone](color.toLowerCase());
|
|
1068
|
+
}
|
|
725
1069
|
function paint(grid, test, color) {
|
|
726
1070
|
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (test(x, y)) grid[y][x] = color;
|
|
727
1071
|
}
|
|
@@ -1121,6 +1465,34 @@ function outfit(recipe, g, s) {
|
|
|
1121
1465
|
paint(g, (x, y) => y === 24 && Math.abs(dx(x)) <= 4.5, WHITE);
|
|
1122
1466
|
sprite(g, ["RR.RR", ".RRR."], Math.round(C + s * .5) - 2, 24, { R: shirt });
|
|
1123
1467
|
break;
|
|
1468
|
+
case "armor": {
|
|
1469
|
+
const metal = "#9aa3ad";
|
|
1470
|
+
paint(g, torso, metal);
|
|
1471
|
+
paint(g, (x, y) => torso(x, y) && y >= 26 && Math.abs(dx(x)) <= 2.5, shirt);
|
|
1472
|
+
paint(g, (x, y) => torso(x, y) && y <= 26 && Math.abs(dx(x)) >= half(y) - 3.5, mix$1(metal, WHITE, .35));
|
|
1473
|
+
paint(g, (x, y) => torso(x, y) && y === 27 && Math.abs(dx(x)) >= half(y) - 3.5, shade(metal, .7));
|
|
1474
|
+
paint(g, (x, y) => y === 24 && Math.abs(dx(x)) <= 3.5, shade(metal, .8));
|
|
1475
|
+
sprite(g, ["G"], Math.round(C + s * .5), 28, { G: GOLD });
|
|
1476
|
+
break;
|
|
1477
|
+
}
|
|
1478
|
+
case "robe":
|
|
1479
|
+
paint(g, v(5, 4), mix$1(shirt, WHITE, .7));
|
|
1480
|
+
paint(g, (x, y) => torso(x, y) && y <= 29 && Math.abs(Math.abs(dx(x)) - (4 - (y - 24) * .8)) <= .5, GOLD);
|
|
1481
|
+
paint(g, (x, y) => torso(x, y) && y >= 28 && Math.abs(dx(x)) >= half(y) - 1.5, dark);
|
|
1482
|
+
break;
|
|
1483
|
+
case "tunic":
|
|
1484
|
+
paint(g, v(2, 2), shade(skin, .85));
|
|
1485
|
+
sprite(g, ["L.L", ".L."], Math.round(C + s * .5) - 1, 25, { L: "#6b4a2b" });
|
|
1486
|
+
paint(g, (x, y) => torso(x, y) && y === 29, "#6b4a2b");
|
|
1487
|
+
sprite(g, ["G"], Math.round(C + s * .5), 29, { G: GOLD });
|
|
1488
|
+
break;
|
|
1489
|
+
case "cloak": {
|
|
1490
|
+
const cloak = mix$1(shirt, "#2f3a4a", .55);
|
|
1491
|
+
paint(g, (x, y) => torso(x, y) && Math.abs(dx(x)) >= 2.5, cloak);
|
|
1492
|
+
paint(g, (x, y) => torso(x, y) && y === 24, shade(cloak, .8));
|
|
1493
|
+
sprite(g, ["G...G"], Math.round(C + s * .5) - 2, 25, { G: GOLD });
|
|
1494
|
+
break;
|
|
1495
|
+
}
|
|
1124
1496
|
case "jacket":
|
|
1125
1497
|
paint(g, v(6, 4.5), WHITE);
|
|
1126
1498
|
paint(g, (x, y) => torso(x, y) && Math.abs(Math.abs(dx(x)) - (4.5 - (y - 24) * .6)) <= .6, dark);
|
|
@@ -1239,6 +1611,22 @@ function accessory(recipe, g, s, face) {
|
|
|
1239
1611
|
}, true);
|
|
1240
1612
|
return;
|
|
1241
1613
|
}
|
|
1614
|
+
case "helmet": {
|
|
1615
|
+
const metal = "#9aa3ad";
|
|
1616
|
+
paint(g, (x, y) => cap(x, y) && y <= 11, metal);
|
|
1617
|
+
paint(g, (x, y) => cap(x, y) && y === 11, shade(metal, .75));
|
|
1618
|
+
paint(g, (x, y) => cap(x, y) && y >= 12 && y <= 19 && Math.abs(x + .5 - C - s) >= 7.5, metal);
|
|
1619
|
+
paint(g, (x, y) => Math.abs(x + .5 - C - s) <= .5 && y >= 11 && y <= 16, shade(metal, .7));
|
|
1620
|
+
paint(g, (x, y) => cap(x, y) && y >= 2 && y <= 5 && x + .5 < C + s - 2, mix$1(metal, WHITE, .4));
|
|
1621
|
+
for (let x = Math.round(C + s) - 9; x <= Math.round(C + s) + 9; x += 3) sprite(g, ["G"], x, 10, { G: GOLD });
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
case "hood": {
|
|
1625
|
+
const outer = ellipse(C + s, 15, 12.6, 12.4);
|
|
1626
|
+
paint(g, (x, y) => outer(x, y) && y <= 25 && !(face(x, y) && y >= 10), shirt);
|
|
1627
|
+
paint(g, (x, y) => outer(x, y) && !face(x, y) && y >= 9 && (face(x + 1, y) || face(x - 1, y) || face(x, y + 1)), shade(shirt, .7));
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1242
1630
|
case "beret":
|
|
1243
1631
|
paint(g, (x, y) => ellipse(C + s + 1.5, 4.6, 10.5, 3.6)(x, y), shirt);
|
|
1244
1632
|
paint(g, (x, y) => ellipse(C + s + 1.5, 4.6, 10.5, 3.6)(x, y) && y >= 6, shade(shirt, .78));
|
|
@@ -1334,6 +1722,75 @@ function accessory(recipe, g, s, face) {
|
|
|
1334
1722
|
return;
|
|
1335
1723
|
}
|
|
1336
1724
|
}
|
|
1725
|
+
const POINTED_EARS = {
|
|
1726
|
+
goblin: {
|
|
1727
|
+
rows: [
|
|
1728
|
+
"E...",
|
|
1729
|
+
"EE..",
|
|
1730
|
+
".EiE",
|
|
1731
|
+
"..Ee"
|
|
1732
|
+
],
|
|
1733
|
+
rise: 2
|
|
1734
|
+
},
|
|
1735
|
+
elf: {
|
|
1736
|
+
rows: [
|
|
1737
|
+
"E....",
|
|
1738
|
+
"EE...",
|
|
1739
|
+
".EE..",
|
|
1740
|
+
"..EiE",
|
|
1741
|
+
"...Ee"
|
|
1742
|
+
],
|
|
1743
|
+
rise: 3
|
|
1744
|
+
},
|
|
1745
|
+
orc: {
|
|
1746
|
+
rows: [
|
|
1747
|
+
"E..",
|
|
1748
|
+
"EE.",
|
|
1749
|
+
".Ee"
|
|
1750
|
+
],
|
|
1751
|
+
rise: 1
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
const leafShape = (cx, cy, long, wide, angle) => (x, y) => {
|
|
1755
|
+
const px = x + .5 - cx;
|
|
1756
|
+
const py = y + .5 - cy;
|
|
1757
|
+
const u = px * Math.cos(angle) + py * Math.sin(angle);
|
|
1758
|
+
const v = -px * Math.sin(angle) + py * Math.cos(angle);
|
|
1759
|
+
return (u / long) ** 2 + (v / wide) ** 2 <= 1;
|
|
1760
|
+
};
|
|
1761
|
+
function petals(style, s) {
|
|
1762
|
+
const cx = C + s;
|
|
1763
|
+
const cy = 16.6;
|
|
1764
|
+
const ring = (n, radius, long, wide, phase = 0) => (x, y) => Array.from({ length: n }, (_, k) => phase + 2 * Math.PI * k / n).some((a) => leafShape(cx + radius * Math.cos(a), cy + radius * Math.sin(a), long, wide, a)(x, y));
|
|
1765
|
+
switch (style) {
|
|
1766
|
+
case "daisy": return ring(14, 10.4, 3.4, 1.7, Math.PI / 14);
|
|
1767
|
+
case "sunflower": return (x, y) => ring(16, 11.2, 2.6, 1.5)(x, y) || ellipse(cx, cy, 10.6, 10.2)(x, y);
|
|
1768
|
+
case "tulip": return (x, y) => y <= cy + 6 && (ellipse(cx - 6.5, cy - 3, 4.5, 7.5)(x, y) || ellipse(cx + 6.5, cy - 3, 4.5, 7.5)(x, y) || ellipse(cx, cy - 6, 5, 8)(x, y));
|
|
1769
|
+
case "trumpet": return (x, y) => ellipse(cx, cy - .5, 11.6, 10.4)(x, y) || y <= cy - 6 && ellipse(cx, cy - 3, 13, 8.6)(x, y);
|
|
1770
|
+
case "sakura": return (x, y) => ring(5, 8.6, 4.6, 3.8, -Math.PI / 2)(x, y) && !ring(5, 13.4, 1.4, 1.4, -Math.PI / 2)(x, y);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
function flowerBase(recipe, g, s) {
|
|
1774
|
+
const leaf = "#5f9e4a";
|
|
1775
|
+
const stem = C + s * .5;
|
|
1776
|
+
paint(g, (x, y) => y >= 22 && Math.abs(x + .5 - stem) <= 1, shade(leaf, .82));
|
|
1777
|
+
if (recipe.flowerBase === "pot") {
|
|
1778
|
+
const pot = recipe.shirtColor;
|
|
1779
|
+
const body = (x, y) => y >= 27 && Math.abs(x + .5 - stem) <= 6.5 - (y - 27) * .45;
|
|
1780
|
+
paint(g, leafShape(stem - 3, 25, 2.8, 1.2, -.5), leaf);
|
|
1781
|
+
paint(g, leafShape(stem + 3, 24.5, 2.8, 1.2, .5), shade(leaf, .9));
|
|
1782
|
+
paint(g, body, pot);
|
|
1783
|
+
paint(g, (x, y) => body(x, y) && y === 27, mix$1(pot, WHITE, .3));
|
|
1784
|
+
paint(g, (x, y) => body(x, y) && y >= 28 && x + .5 > stem + 3, shade(pot, .8));
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
const left = leafShape(stem - 4.5, 27, 4.4, 1.9, -.55);
|
|
1788
|
+
const right = leafShape(stem + 4.5, 25.5, 4.4, 1.9, .55);
|
|
1789
|
+
paint(g, left, leaf);
|
|
1790
|
+
paint(g, right, shade(leaf, .92));
|
|
1791
|
+
paint(g, leafShape(stem - 4.5, 27, 3.6, .5, -.55), shade(leaf, .75));
|
|
1792
|
+
paint(g, leafShape(stem + 4.5, 25.5, 3.6, .5, .55), shade(leaf, .7));
|
|
1793
|
+
}
|
|
1337
1794
|
function pixelTileColor(hair) {
|
|
1338
1795
|
const [r, g, b] = channels$1(hair).map((v) => v / 255);
|
|
1339
1796
|
const max = Math.max(r, g, b);
|
|
@@ -1371,34 +1828,72 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
1371
1828
|
const tuck = y >= 17 && d * dx > 4 ? Math.min(2, (y - 16) * .35) : 0;
|
|
1372
1829
|
return front(x - Math.round(d * (1 + Math.max(0, y - 20) * .4) - d * tuck), y);
|
|
1373
1830
|
} : front;
|
|
1374
|
-
const
|
|
1831
|
+
const flower = recipe.species === "flower";
|
|
1832
|
+
const covered = recipe.accessory === "helmet" || recipe.accessory === "hood";
|
|
1833
|
+
const bare = () => false;
|
|
1834
|
+
const petalRing = flower ? petals(recipe.petals ?? "trumpet", s) : bare;
|
|
1835
|
+
const masks = flower ? {
|
|
1836
|
+
back: petalRing,
|
|
1837
|
+
front: bare,
|
|
1838
|
+
sides: [(x, y) => petalRing(x, y) && x + .5 < C + s, (x, y) => petalRing(x, y) && x + .5 >= C + s]
|
|
1839
|
+
} : covered ? {
|
|
1840
|
+
back: bare,
|
|
1841
|
+
front: bare
|
|
1842
|
+
} : recipe.bangs === void 0 ? hair(recipe, s, b) : detailedHair(recipe, s, b);
|
|
1375
1843
|
const body = blank();
|
|
1376
|
-
|
|
1844
|
+
if (flower) flowerBase(recipe, body, s);
|
|
1845
|
+
else outfit(recipe, body, s);
|
|
1377
1846
|
const back = blank();
|
|
1378
|
-
|
|
1847
|
+
if (flower) {
|
|
1848
|
+
paint(back, petalRing, hairColor);
|
|
1849
|
+
paint(back, (x, y) => petalRing(x, y) && (x + .5 - C - s) ** 2 + (y + .5 - 16.6) ** 2 < 90, shade(hairColor, .84));
|
|
1850
|
+
paint(back, (x, y) => petalRing(x, y) && x + .5 < C + s - 3 && y < 10, mix$1(hairColor, WHITE, .3));
|
|
1851
|
+
} else paint(back, masks.back, shade(hairColor, .82));
|
|
1379
1852
|
if (masks.bands) paint(back, (x, y) => masks.back(x, y) && (y + Math.floor(x / 2)) % 4 === 0, shade(hairColor, .68));
|
|
1853
|
+
const headpiece = recipe.assetVersion === 3 ? recipe.headpiece : void 0;
|
|
1854
|
+
const placeCells = (grid, cells) => {
|
|
1855
|
+
const placed = cells.map(([x, y, color, tone]) => {
|
|
1856
|
+
const dx = x + .5 - C;
|
|
1857
|
+
const far = turned && d * dx < 0 && Math.abs(dx) >= 5;
|
|
1858
|
+
const base = color.startsWith("#") ? color.toLowerCase() : recipe[color];
|
|
1859
|
+
return {
|
|
1860
|
+
x: x + s + (far ? d : 0),
|
|
1861
|
+
y,
|
|
1862
|
+
c: TONES[tone](base),
|
|
1863
|
+
far
|
|
1864
|
+
};
|
|
1865
|
+
});
|
|
1866
|
+
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;
|
|
1867
|
+
};
|
|
1868
|
+
if (headpiece) placeCells(back, headpiece.back);
|
|
1380
1869
|
const head = blank();
|
|
1381
1870
|
paint(head, face, skin);
|
|
1871
|
+
const h = recipe.height ?? 0;
|
|
1872
|
+
if (recipe.beard && !flower) {
|
|
1873
|
+
const centre = C + fs * .6;
|
|
1874
|
+
const mouthX = Math.round(C - .5 + fs * .6);
|
|
1875
|
+
const chin = (x, y) => face(x, y) || face(x, y - 2);
|
|
1876
|
+
const open = (x, y) => y >= 20 + h && y <= 22 + h && Math.abs(x - mouthX) <= 3;
|
|
1877
|
+
const full = (x, y) => chin(x, y) && (y >= 22 + h || Math.abs(x + .5 - centre) >= 4 && y >= 17 + h);
|
|
1878
|
+
const shape = recipe.beard === "short" ? (x, y) => chin(x, y) && y >= 22 + h : recipe.beard === "full" ? full : (x, y) => full(x, y) || Math.abs(x + .5 - centre) <= 1 && y >= 24 && y <= 30;
|
|
1879
|
+
paint(head, (x, y) => shape(x, y) && !open(x, y), shade(hairColor, .9));
|
|
1880
|
+
paint(head, (x, y) => shape(x, y) && !open(x, y) && (x + y * 2) % 5 === 0, shade(hairColor, .72));
|
|
1881
|
+
}
|
|
1382
1882
|
const earY = 15;
|
|
1383
1883
|
const edge = (side) => {
|
|
1384
1884
|
const xs = [...Array(N).keys()].filter((x) => face(x, earY));
|
|
1385
1885
|
return side < 0 ? xs[0] - 1 : xs.at(-1) + 1;
|
|
1386
1886
|
};
|
|
1387
|
-
const
|
|
1388
|
-
if (
|
|
1389
|
-
const ear = [
|
|
1390
|
-
"E...",
|
|
1391
|
-
"EE..",
|
|
1392
|
-
".EiE",
|
|
1393
|
-
"..Ee"
|
|
1394
|
-
];
|
|
1887
|
+
const pointed = recipe.species ? POINTED_EARS[recipe.species] : void 0;
|
|
1888
|
+
if (flower) {} else if (pointed) {
|
|
1395
1889
|
const pal = {
|
|
1396
1890
|
E: skin,
|
|
1397
1891
|
e: shade(skin, .86),
|
|
1398
1892
|
i: shade(skin, .74)
|
|
1399
1893
|
};
|
|
1894
|
+
const width = pointed.rows[0].length;
|
|
1400
1895
|
paint(head, masks.front, hairColor);
|
|
1401
|
-
for (const ex of turned ? [-d] : [-1, 1]) sprite(head,
|
|
1896
|
+
for (const ex of turned ? [-d] : [-1, 1]) sprite(head, pointed.rows, ex < 0 ? edge(ex) - (width - 1) : edge(ex), earY - pointed.rise, pal, ex > 0);
|
|
1402
1897
|
} else if (!turned) for (const ex of [-1, 1]) sprite(head, [
|
|
1403
1898
|
"E",
|
|
1404
1899
|
"E",
|
|
@@ -1416,9 +1911,9 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
1416
1911
|
E: skin,
|
|
1417
1912
|
e: shade(skin, .8)
|
|
1418
1913
|
});
|
|
1419
|
-
if (!
|
|
1914
|
+
if (!pointed && !flower) paint(head, masks.front, hairColor);
|
|
1420
1915
|
for (const [x, y] of masks.ties ?? []) sprite(back, ["T"], Math.round(x), y, { T: shade(recipe.shirtColor, .8) });
|
|
1421
|
-
accessory(recipe, head, s, face);
|
|
1916
|
+
if (!flower) accessory(recipe, head, s, face);
|
|
1422
1917
|
const shadeHair = (g) => {
|
|
1423
1918
|
const out = g.map((r) => [...r]);
|
|
1424
1919
|
const cells = g.flatMap((row, y) => row.flatMap((c, x) => c === hairColor ? [[x, y]] : []));
|
|
@@ -1459,18 +1954,20 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
1459
1954
|
}
|
|
1460
1955
|
return out;
|
|
1461
1956
|
};
|
|
1957
|
+
const crown = blank();
|
|
1958
|
+
if (headpiece) placeCells(crown, headpiece.front);
|
|
1462
1959
|
const figure = blank();
|
|
1463
1960
|
for (const layer of [
|
|
1464
1961
|
back,
|
|
1465
1962
|
body,
|
|
1466
|
-
shadeHair(head)
|
|
1963
|
+
shadeHair(head),
|
|
1964
|
+
crown
|
|
1467
1965
|
]) for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (layer[y][x]) figure[y][x] = layer[y][x];
|
|
1468
1966
|
const features = blank();
|
|
1469
1967
|
const eyes = blank();
|
|
1470
1968
|
const closed = blank();
|
|
1471
1969
|
const glasses = blank();
|
|
1472
1970
|
const sp = recipe.spacing ?? 0;
|
|
1473
|
-
const h = recipe.height ?? 0;
|
|
1474
1971
|
const eyeTop = 15 + h;
|
|
1475
1972
|
const eyeW = (side) => turned && Math.sign(fs) === side ? 3 : 4;
|
|
1476
1973
|
const eyeX = (side) => {
|
|
@@ -1493,21 +1990,47 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
1493
1990
|
I: recipe.eyeColor,
|
|
1494
1991
|
L: mix$1(recipe.eyeColor, WHITE, .45)
|
|
1495
1992
|
};
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1993
|
+
if (flower) {
|
|
1994
|
+
const centre = Math.round(C + fs * .6);
|
|
1995
|
+
const bead = {
|
|
1996
|
+
K: "#1d1b22",
|
|
1997
|
+
W: WHITE
|
|
1998
|
+
};
|
|
1999
|
+
for (const ex of [centre - 3, centre + 1]) {
|
|
2000
|
+
sprite(eyes, [
|
|
2001
|
+
"KK",
|
|
2002
|
+
"KW",
|
|
2003
|
+
"KK",
|
|
2004
|
+
"KK"
|
|
2005
|
+
], ex, eyeTop - 1, bead);
|
|
2006
|
+
sprite(closed, [
|
|
2007
|
+
"..",
|
|
2008
|
+
"..",
|
|
2009
|
+
"KK",
|
|
2010
|
+
".."
|
|
2011
|
+
], ex, eyeTop - 1, bead);
|
|
2012
|
+
}
|
|
2013
|
+
} else {
|
|
2014
|
+
const style = EYES[recipe.eyes];
|
|
2015
|
+
const extra = style[0].length - 4;
|
|
2016
|
+
sprite(eyes, fit(style, left.w + extra), left.x - extra, eyeTop, pal);
|
|
2017
|
+
sprite(eyes, recipe.eyes === "wink" ? fit(CLOSED, right.w) : fit(style, right.w + extra), right.x, eyeTop, pal, true);
|
|
2018
|
+
sprite(closed, fit(CLOSED, left.w), left.x, eyeTop, pal);
|
|
2019
|
+
sprite(closed, fit(CLOSED, right.w), right.x, eyeTop, pal, true);
|
|
2020
|
+
}
|
|
2021
|
+
const browRows = flower ? [] : BROWS[recipe.brows];
|
|
1503
2022
|
if (browRows.length) {
|
|
1504
2023
|
sprite(features, fit(browRows, left.w), left.x, eyeTop - 2, { B: shade(hairColor, .55) });
|
|
1505
2024
|
sprite(features, fit(browRows, right.w), right.x, eyeTop - 2, { B: shade(hairColor, .55) }, true);
|
|
1506
2025
|
}
|
|
1507
2026
|
const cx = Math.round(C + fs * (turned ? .85 : .6));
|
|
1508
|
-
if (recipe.
|
|
1509
|
-
|
|
1510
|
-
|
|
2027
|
+
if (flower) {} else if (recipe.species === "dwarf") sprite(features, [".N.", "NnN"], cx - 1, 17 + h, {
|
|
2028
|
+
N: shade(skin, .84),
|
|
2029
|
+
n: shade(skin, .72)
|
|
2030
|
+
});
|
|
2031
|
+
else if (recipe.nose === "dot") sprite(features, ["N"], cx, 18 + h, { N: shade(skin, .78) });
|
|
2032
|
+
else if (recipe.nose === "button") sprite(features, ["N"], cx, 18 + h, { N: shade(skin, .88) });
|
|
2033
|
+
else if (recipe.nose === "line") sprite(features, ["N", "N"], cx, 17 + h, { N: shade(skin, .82) });
|
|
1511
2034
|
const mx = C - .5 + fs * .6;
|
|
1512
2035
|
const withMouth = (mouth) => {
|
|
1513
2036
|
const layer = features.map((row) => [...row]);
|
|
@@ -1518,11 +2041,12 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
1518
2041
|
W: WHITE
|
|
1519
2042
|
});
|
|
1520
2043
|
if (recipe.species === "goblin") for (const tx of [Math.round(mx - 2.5), Math.round(mx + 2.5)]) sprite(layer, ["T"], tx, 21 + h, { T: TUSK });
|
|
1521
|
-
if (recipe.
|
|
2044
|
+
if (recipe.species === "orc") for (const tx of [Math.round(mx - 2.5), Math.round(mx + 2.5)]) sprite(layer, ["T", "T"], tx, 20 + h, { T: TUSK });
|
|
2045
|
+
if (recipe.cheeks === "blush" && !flower) {
|
|
1522
2046
|
sprite(layer, ["PP"], left.x - 1, 19 + h, { P: mix$1(BLUSH, skin, .25) });
|
|
1523
2047
|
sprite(layer, ["PP"], right.x + right.w - 1, 19 + h, { P: mix$1(BLUSH, skin, .25) });
|
|
1524
2048
|
}
|
|
1525
|
-
if (recipe.cheeks === "freckles") {
|
|
2049
|
+
if (recipe.cheeks === "freckles" && !flower) {
|
|
1526
2050
|
sprite(layer, ["F.F"], left.x, 19 + h, { F: shade(skin, .7) });
|
|
1527
2051
|
sprite(layer, ["F.F"], right.x + 1, 19 + h, { F: shade(skin, .7) });
|
|
1528
2052
|
}
|
|
@@ -1545,17 +2069,18 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
1545
2069
|
const corner = (x === fx - 1 || x === fx + w) && (y === eyeTop - 1 || y === eyeTop + 4);
|
|
1546
2070
|
return box && !inside && !(round && corner);
|
|
1547
2071
|
}, INK$1);
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
frame(
|
|
2072
|
+
const lenses = flower ? "none" : recipe.glasses;
|
|
2073
|
+
if (lenses === "round" || lenses === "square") {
|
|
2074
|
+
frame(left.x, left.w, lenses === "round");
|
|
2075
|
+
frame(right.x, right.w, lenses === "round");
|
|
1551
2076
|
paint(glasses, (x, y) => y === eyeTop + 1 && x > left.x + left.w && x < right.x - 1, INK$1);
|
|
1552
2077
|
}
|
|
1553
|
-
if (
|
|
2078
|
+
if (lenses === "shades") {
|
|
1554
2079
|
for (const e of [left, right]) paint(glasses, (x, y) => x >= e.x - 1 && x <= e.x + e.w && y >= eyeTop && y <= eyeTop + 2, INK$1);
|
|
1555
2080
|
paint(glasses, (x, y) => y === eyeTop && x > left.x + left.w && x < right.x - 1, INK$1);
|
|
1556
2081
|
for (const e of [left, right]) sprite(glasses, ["W"], e.x, eyeTop, { W: "#7d7b88" });
|
|
1557
2082
|
}
|
|
1558
|
-
if (
|
|
2083
|
+
if (lenses === "monocle") {
|
|
1559
2084
|
frame(right.x, right.w, true);
|
|
1560
2085
|
sprite(glasses, [
|
|
1561
2086
|
"G",
|
|
@@ -1583,7 +2108,7 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
1583
2108
|
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
|
|
1584
2109
|
const c = full[y][x];
|
|
1585
2110
|
if (!c) continue;
|
|
1586
|
-
if (y >= 22 && !head[y][x] && !back[y][x]) bodyCells[y][x] = c;
|
|
2111
|
+
if (y >= 22 && !head[y][x] && !back[y][x] && !crown[y][x]) bodyCells[y][x] = c;
|
|
1587
2112
|
else headCells[y][x] = c;
|
|
1588
2113
|
}
|
|
1589
2114
|
const cells = [];
|
|
@@ -2131,5 +2656,5 @@ function symbolArtCells(rows, color) {
|
|
|
2131
2656
|
}
|
|
2132
2657
|
|
|
2133
2658
|
//#endregion
|
|
2134
|
-
export { AVATAR_COLORS, AVATAR_HAIR_PARTS, AVATAR_PARTS, AVATAR_PIECE_COLORS, AVATAR_PRESETS, AVATAR_RANGES, AVATAR_SPECIES, AVATAR_SPECIES_SWATCHES, AVATAR_SWATCHES, AVATAR_TURNS, DEFAULT_RECIPE, PIXEL_SYMBOLS, canonicalRecipe, createSeededRecipe, detailedRecipe, faceCells, isPixelAvatarRecipe, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, seededRandom, seededRecipe, symbolArtCells, withSpecies };
|
|
2659
|
+
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, MAX_PART_FIXED_COLORS, PART_LAYERS, PART_SLOTS, PART_TONES, PIXEL_SYMBOLS, canonicalCustomPart, canonicalRecipe, createCustomPart, createSeededRecipe, createSeededRecipeV2, customPartId, detailedRecipe, emptyPartLayer, faceCells, fillPartLayer, hiddenChoices, isPixelAvatarRecipe, isPixelCustomPart, mirrorPartX, paintPartLayer, partCells, partLayer, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, seededRandom, seededRecipe, seededRecipeV2, symbolArtCells, withHeadpiece, withSpecies };
|
|
2135
2660
|
//# sourceMappingURL=index.js.map
|