@botharness/pixel-avatar 0.6.0 → 0.8.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 +84 -2
- package/dist/index.js +350 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ const svg = pixelAvatarSvg(recipe); // 32×32 viewBox, crisp edges, no ids or sc
|
|
|
22
22
|
- `AVATAR_PARTS_V2`, `AVATAR_EXTRA_PARTS`, `hiddenChoices(recipe)`: version 2 adds elf, dwarf, orc and flower species, armor/robe/tunic/cloak outfits, helmet/hood headwear, an optional `beard`, and a flower's `petals` and `flowerBase`. `hiddenChoices` names saved choices the current species or headwear keeps but does not draw, so an editor can say so instead of discarding them.
|
|
23
23
|
- `PixelCustomPart`, `withHeadpiece(recipe, part)`, `customPartId`, `isPixelCustomPart`: a Human-drawn Custom Part in the `headpiece` slot (the top 32×16 of the tile). Its back layer is drawn behind the hair and its front layer over it, and both follow the head through turns. Each cell is `[x, y, color, tone]`, where `color` is an appearance color slot (`hairColor`, `skinColor`, `eyeColor`, `shirtColor`), so the part recolors with the Avatar, or a fixed `#rrggbb`, and `tone` is a step from −2 to 2 on the rig's shade ramp (`partToneColor`). A recipe wearing a part embeds its own copy as asset version 3. `customPartId` is the SHA-256 of the canonical content; names, authors and origins belong to the consumer's library.
|
|
24
24
|
- `HAIR_PART_SLOTS`, `hairPieceStart(recipe, slot)`, `withCustomPart(recipe, slot, part)`, `wornPart`: drawn hair pieces. `bangs`, `leftSideHair`, `rightSideHair` and `backHair` each take a Custom Part (front layer only) that replaces the built-in piece while it stays saved underneath. In a hair part, a `hairColor` cell at tone 0 is live hair: it is shaded with the rest of the hair exactly like the built-in piece, so `hairPieceStart` flattens the worn built-in piece into such cells and an unchanged copy renders identically. Other cells show their own color. Drawn hair follows turns and is hidden under a helmet or hood and on a flower, like built-in hair.
|
|
25
|
+
- `REPLACE_PART_SLOTS`, `replacePartStart(recipe, slot)`: drawn `outfit`, `accessory`, `beard`, `glasses`, `nose`, `cheeks`, `petals` and `flowerBase` parts (front layer only). A drawn part replaces the built-in part pixel for pixel in the layer that part paints into, while the built-in choice stays saved underneath. `replacePartStart` flattens the worn built-in part for this recipe: pixels that are exactly an appearance color at a tone become that color slot, and the rest become fixed colors, so an unchanged copy renders identically. Drawn face parts never cover the speaking mouth, and drawn parts are hidden wherever the species or headwear hides the built-in part. A drawn accessory replacing a helmet or hood no longer hides the hair.
|
|
25
26
|
- `emptyPartLayer`, `paintPartLayer`, `fillPartLayer`, `mirrorPartX`, `partLayer`, `partCells`, `createCustomPart`: drawing on dense layers, with pencil and eraser strokes, 4-connected flood fill and mirroring across the Avatar centerline.
|
|
27
|
+
- `partLinePoints`, `partRectPoints`, `gradientPartLayer`, `ditherThreshold`, `noisePartLayer`, `shadePartLayer`: line (with 0°/45°/90° snap), rectangle (with square), tone gradient with 4×4 or 2×2 ordered dithering inside the start region, seeded ±1 tone noise, and a shade stroke that moves each colored cell one tone. All mirror, and all produce ordinary cells: the seed and the tool are not part of the part.
|
|
26
28
|
- `faceCells`, `pixelSymbolCells`, `symbolArtCells`: pixels for [`@botharness/pixel-morph`](../morph).
|
|
27
29
|
- `pixelAvatarSvg(recipe, { turns, classPrefix })`: optional pre-rendered head turns, and rig layer classes (`<prefix>-body`, `-head`, `-face`, `-gaze`, `-blink`) for CSS animation.
|
package/dist/index.d.ts
CHANGED
|
@@ -90,6 +90,14 @@ declare const CUSTOM_PART_KEYS: {
|
|
|
90
90
|
readonly leftSideHair: "leftSideHairPart";
|
|
91
91
|
readonly rightSideHair: "rightSideHairPart";
|
|
92
92
|
readonly backHair: "backHairPart";
|
|
93
|
+
readonly outfit: "outfitPart";
|
|
94
|
+
readonly accessory: "accessoryPart";
|
|
95
|
+
readonly beard: "beardPart";
|
|
96
|
+
readonly glasses: "glassesPart";
|
|
97
|
+
readonly nose: "nosePart";
|
|
98
|
+
readonly cheeks: "cheeksPart";
|
|
99
|
+
readonly petals: "petalsPart";
|
|
100
|
+
readonly flowerBase: "flowerBasePart";
|
|
93
101
|
};
|
|
94
102
|
type CustomPartKey = (typeof CUSTOM_PART_KEYS)[PartSlot];
|
|
95
103
|
/**
|
|
@@ -177,6 +185,38 @@ declare const PART_SLOTS: {
|
|
|
177
185
|
readonly width: 32;
|
|
178
186
|
readonly height: 32;
|
|
179
187
|
};
|
|
188
|
+
readonly outfit: {
|
|
189
|
+
readonly width: 32;
|
|
190
|
+
readonly height: 32;
|
|
191
|
+
};
|
|
192
|
+
readonly accessory: {
|
|
193
|
+
readonly width: 32;
|
|
194
|
+
readonly height: 32;
|
|
195
|
+
};
|
|
196
|
+
readonly beard: {
|
|
197
|
+
readonly width: 32;
|
|
198
|
+
readonly height: 32;
|
|
199
|
+
};
|
|
200
|
+
readonly glasses: {
|
|
201
|
+
readonly width: 32;
|
|
202
|
+
readonly height: 32;
|
|
203
|
+
};
|
|
204
|
+
readonly nose: {
|
|
205
|
+
readonly width: 32;
|
|
206
|
+
readonly height: 32;
|
|
207
|
+
};
|
|
208
|
+
readonly cheeks: {
|
|
209
|
+
readonly width: 32;
|
|
210
|
+
readonly height: 32;
|
|
211
|
+
};
|
|
212
|
+
readonly petals: {
|
|
213
|
+
readonly width: 32;
|
|
214
|
+
readonly height: 32;
|
|
215
|
+
};
|
|
216
|
+
readonly flowerBase: {
|
|
217
|
+
readonly width: 32;
|
|
218
|
+
readonly height: 32;
|
|
219
|
+
};
|
|
180
220
|
};
|
|
181
221
|
type PartSlot = keyof typeof PART_SLOTS;
|
|
182
222
|
/**
|
|
@@ -186,6 +226,13 @@ type PartSlot = keyof typeof PART_SLOTS;
|
|
|
186
226
|
declare const HAIR_PART_SLOTS: readonly ["bangs", "leftSideHair", "rightSideHair", "backHair"];
|
|
187
227
|
type HairPartSlot = (typeof HAIR_PART_SLOTS)[number];
|
|
188
228
|
declare const isHairPartSlot: (slot: PartSlot) => slot is HairPartSlot;
|
|
229
|
+
/**
|
|
230
|
+
* Slots whose drawn part replaces a built-in part pixel for pixel: the part's cells are painted
|
|
231
|
+
* where the built-in part would be, and every cell shows its own color and tone.
|
|
232
|
+
*/
|
|
233
|
+
declare const REPLACE_PART_SLOTS: readonly ["outfit", "accessory", "beard", "glasses", "nose", "cheeks", "petals", "flowerBase"];
|
|
234
|
+
type ReplacePartSlot = (typeof REPLACE_PART_SLOTS)[number];
|
|
235
|
+
declare const isReplacePartSlot: (slot: PartSlot) => slot is ReplacePartSlot;
|
|
189
236
|
/** Tone steps on the rig's shade ramp: two darker, the color itself, two lighter. */
|
|
190
237
|
declare const PART_TONES: readonly [-2, -1, 0, 1, 2];
|
|
191
238
|
type PartTone = (typeof PART_TONES)[number];
|
|
@@ -196,7 +243,7 @@ type PartCell = readonly [x: number, y: number, color: PartColor, tone: PartTone
|
|
|
196
243
|
declare const PART_LAYERS: readonly ["front", "back"];
|
|
197
244
|
type PartLayerName = (typeof PART_LAYERS)[number];
|
|
198
245
|
/** Most fixed colors one part may use. */
|
|
199
|
-
declare const MAX_PART_FIXED_COLORS =
|
|
246
|
+
declare const MAX_PART_FIXED_COLORS = 32;
|
|
200
247
|
/**
|
|
201
248
|
* A Human-drawn Custom Part. Its content is immutable and its identity is `customPartId`;
|
|
202
249
|
* name, author, origin and parent are metadata kept outside it.
|
|
@@ -231,6 +278,29 @@ declare function paintPartLayer(slot: PartSlot, layer: PartLayer, points: readon
|
|
|
231
278
|
* becomes `ink`. With `mirror`, the mirrored start is filled too.
|
|
232
279
|
*/
|
|
233
280
|
declare function fillPartLayer(slot: PartSlot, layer: PartLayer, x: number, y: number, ink: PartInk | null, mirror?: boolean): PartLayer;
|
|
281
|
+
/** Pixel-perfect line points (Bresenham). With `snap`, the end snaps to 0°, 45° or 90°. */
|
|
282
|
+
declare function partLinePoints(from: readonly [number, number], to: readonly [number, number], snap?: boolean): [number, number][];
|
|
283
|
+
/** Rectangle outline points between two corners. With `square`, the far corner makes a square. */
|
|
284
|
+
declare function partRectPoints(from: readonly [number, number], to: readonly [number, number], square?: boolean): [number, number][];
|
|
285
|
+
/** The ordered-dither threshold in [0, 1) for a cell. */
|
|
286
|
+
declare function ditherThreshold(x: number, y: number, size?: 2 | 4): number;
|
|
287
|
+
/**
|
|
288
|
+
* A linear gradient over the 4-connected region of the start cell: each cell's position along
|
|
289
|
+
* `from`→`to` steps through the tones between `fromTone` and `toTone`, with ordered dithering
|
|
290
|
+
* between neighboring tones. No RGB is blended; the result is ordinary cells.
|
|
291
|
+
*/
|
|
292
|
+
declare function gradientPartLayer(slot: PartSlot, layer: PartLayer, from: readonly [number, number], to: readonly [number, number], color: PartColor, fromTone: PartTone, toTone: PartTone, options?: {
|
|
293
|
+
dither?: 2 | 4;
|
|
294
|
+
mirror?: boolean;
|
|
295
|
+
}): PartLayer;
|
|
296
|
+
/**
|
|
297
|
+
* Moves the tone of colored cells by ±1 at random, for texture. The same seed gives the same
|
|
298
|
+
* result; the seed is used only now, and the result is ordinary cells. With `mirror`, each cell
|
|
299
|
+
* and its mirror get the same change.
|
|
300
|
+
*/
|
|
301
|
+
declare function noisePartLayer(slot: PartSlot, layer: PartLayer, amount: number, seed: string, mirror?: boolean): PartLayer;
|
|
302
|
+
/** Moves the tone of each colored cell on the points by `delta` once; empty cells are skipped. */
|
|
303
|
+
declare function shadePartLayer(slot: PartSlot, layer: PartLayer, points: readonly (readonly [number, number])[], delta: 1 | -1, mirror?: boolean): PartLayer;
|
|
234
304
|
//#endregion
|
|
235
305
|
//#region src/figure.d.ts
|
|
236
306
|
type Cell = string | undefined;
|
|
@@ -260,6 +330,18 @@ declare function hairPieceStart(recipe: PixelAvatarRecipe, slot: HairPartSlot):
|
|
|
260
330
|
front: PartCell[];
|
|
261
331
|
back: PartCell[];
|
|
262
332
|
};
|
|
333
|
+
/**
|
|
334
|
+
* The part in a replacement slot as a Custom Part to start drawing from: the drawn part worn
|
|
335
|
+
* there, or the built-in part flattened for this recipe in the front pose. Each pixel becomes a
|
|
336
|
+
* color slot and tone when it is exactly one, so it recolors with the Avatar, and a fixed color
|
|
337
|
+
* otherwise; an unchanged copy renders identically. A flattened helmet or hood no longer hides
|
|
338
|
+
* the hair: only the built-in ones do.
|
|
339
|
+
*/
|
|
340
|
+
declare function replacePartStart(recipe: PixelAvatarRecipe, slot: ReplacePartSlot): {
|
|
341
|
+
slot: ReplacePartSlot;
|
|
342
|
+
front: PartCell[];
|
|
343
|
+
back: PartCell[];
|
|
344
|
+
};
|
|
263
345
|
//#endregion
|
|
264
346
|
//#region src/random.d.ts
|
|
265
347
|
declare function seededRandom(seed: string): () => number;
|
|
@@ -309,5 +391,5 @@ declare function pixelSymbolCells(symbol: PixelSymbol, color: string): PixelCell
|
|
|
309
391
|
*/
|
|
310
392
|
declare function symbolArtCells(rows: readonly string[], color: string): PixelCell$1[];
|
|
311
393
|
//#endregion
|
|
312
|
-
export { AVATAR_COLORS, AVATAR_EXTRA_PARTS, AVATAR_HAIR_PARTS, AVATAR_PARTS, AVATAR_PARTS_V2, AVATAR_PIECE_COLORS, AVATAR_PRESETS, AVATAR_RANGES, AVATAR_SPECIES, AVATAR_SPECIES_SWATCHES, AVATAR_SWATCHES, AVATAR_TURNS, type AvatarColor, type AvatarExtraPart, type AvatarHairPart, type AvatarPart, type AvatarPieceColor, type AvatarRange, type AvatarSpecies, CUSTOM_PART_KEYS, type CustomPartKey, DEFAULT_RECIPE, HAIR_PART_SLOTS, type HairPartSlot, MAX_PART_FIXED_COLORS, PART_LAYERS, PART_SLOTS, PART_TONES, PIXEL_SYMBOLS, type PartCell, type PartColor, type PartInk, type PartLayer, type PartLayerName, type PartSlot, type PartTone, type PixelAvatarRecipe, type PixelAvatarRecipeV1, type PixelAvatarRecipeV2, type PixelAvatarRecipeV3, type PixelAvatarSvgOptions, type PixelCell, type PixelCustomPart, type PixelFigureOptions, type PixelGrid, type PixelMouthState, type PixelSymbol, 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 };
|
|
394
|
+
export { AVATAR_COLORS, AVATAR_EXTRA_PARTS, AVATAR_HAIR_PARTS, AVATAR_PARTS, AVATAR_PARTS_V2, AVATAR_PIECE_COLORS, AVATAR_PRESETS, AVATAR_RANGES, AVATAR_SPECIES, AVATAR_SPECIES_SWATCHES, AVATAR_SWATCHES, AVATAR_TURNS, type AvatarColor, type AvatarExtraPart, type AvatarHairPart, type AvatarPart, type AvatarPieceColor, type AvatarRange, type AvatarSpecies, CUSTOM_PART_KEYS, type CustomPartKey, DEFAULT_RECIPE, HAIR_PART_SLOTS, type HairPartSlot, MAX_PART_FIXED_COLORS, PART_LAYERS, PART_SLOTS, PART_TONES, PIXEL_SYMBOLS, type PartCell, type PartColor, type PartInk, type PartLayer, type PartLayerName, type PartSlot, type PartTone, type PixelAvatarRecipe, type PixelAvatarRecipeV1, type PixelAvatarRecipeV2, type PixelAvatarRecipeV3, type PixelAvatarSvgOptions, type PixelCell, type PixelCustomPart, type PixelFigureOptions, type PixelGrid, type PixelMouthState, type PixelSymbol, REPLACE_PART_SLOTS, type ReplacePartSlot, canonicalCustomPart, canonicalRecipe, createCustomPart, createSeededRecipe, createSeededRecipeV2, customPartId, detailedRecipe, ditherThreshold, emptyPartLayer, faceCells, fillPartLayer, gradientPartLayer, hairPieceStart, hiddenChoices, isHairPartSlot, isPixelAvatarRecipe, isPixelCustomPart, isReplacePartSlot, mirrorPartX, noisePartLayer, paintPartLayer, partCells, partLayer, partLinePoints, partRectPoints, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, replacePartStart, seededRandom, seededRecipe, seededRecipeV2, shadePartLayer, symbolArtCells, withCustomPart, withHeadpiece, withSpecies, wornPart };
|
|
313
395
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -371,7 +371,15 @@ const CUSTOM_PART_KEYS = {
|
|
|
371
371
|
bangs: "bangsPart",
|
|
372
372
|
leftSideHair: "leftSideHairPart",
|
|
373
373
|
rightSideHair: "rightSideHairPart",
|
|
374
|
-
backHair: "backHairPart"
|
|
374
|
+
backHair: "backHairPart",
|
|
375
|
+
outfit: "outfitPart",
|
|
376
|
+
accessory: "accessoryPart",
|
|
377
|
+
beard: "beardPart",
|
|
378
|
+
glasses: "glassesPart",
|
|
379
|
+
nose: "nosePart",
|
|
380
|
+
cheeks: "cheeksPart",
|
|
381
|
+
petals: "petalsPart",
|
|
382
|
+
flowerBase: "flowerBasePart"
|
|
375
383
|
};
|
|
376
384
|
const PART_KEY_ENTRIES = Object.entries(CUSTOM_PART_KEYS);
|
|
377
385
|
const DEFAULT_RECIPE = {
|
|
@@ -465,10 +473,10 @@ function hiddenChoices(recipe) {
|
|
|
465
473
|
"nose",
|
|
466
474
|
"cheeks",
|
|
467
475
|
"glasses",
|
|
468
|
-
...recipe.beard ? ["beard"] : []
|
|
476
|
+
...recipe.beard || wornPart(recipe, "beard") ? ["beard"] : []
|
|
469
477
|
];
|
|
470
478
|
const hidden = [];
|
|
471
|
-
if (recipe.accessory === "helmet" || recipe.accessory === "hood") hidden.push(...HAIR_PIECES);
|
|
479
|
+
if ((recipe.accessory === "helmet" || recipe.accessory === "hood") && !wornPart(recipe, "accessory")) hidden.push(...HAIR_PIECES);
|
|
472
480
|
if (recipe.species === "dwarf") hidden.push("nose");
|
|
473
481
|
return hidden;
|
|
474
482
|
}
|
|
@@ -894,6 +902,38 @@ const PART_SLOTS = {
|
|
|
894
902
|
backHair: {
|
|
895
903
|
width: 32,
|
|
896
904
|
height: 32
|
|
905
|
+
},
|
|
906
|
+
outfit: {
|
|
907
|
+
width: 32,
|
|
908
|
+
height: 32
|
|
909
|
+
},
|
|
910
|
+
accessory: {
|
|
911
|
+
width: 32,
|
|
912
|
+
height: 32
|
|
913
|
+
},
|
|
914
|
+
beard: {
|
|
915
|
+
width: 32,
|
|
916
|
+
height: 32
|
|
917
|
+
},
|
|
918
|
+
glasses: {
|
|
919
|
+
width: 32,
|
|
920
|
+
height: 32
|
|
921
|
+
},
|
|
922
|
+
nose: {
|
|
923
|
+
width: 32,
|
|
924
|
+
height: 32
|
|
925
|
+
},
|
|
926
|
+
cheeks: {
|
|
927
|
+
width: 32,
|
|
928
|
+
height: 32
|
|
929
|
+
},
|
|
930
|
+
petals: {
|
|
931
|
+
width: 32,
|
|
932
|
+
height: 32
|
|
933
|
+
},
|
|
934
|
+
flowerBase: {
|
|
935
|
+
width: 32,
|
|
936
|
+
height: 32
|
|
897
937
|
}
|
|
898
938
|
};
|
|
899
939
|
/**
|
|
@@ -907,6 +947,21 @@ const HAIR_PART_SLOTS = [
|
|
|
907
947
|
"backHair"
|
|
908
948
|
];
|
|
909
949
|
const isHairPartSlot = (slot) => HAIR_PART_SLOTS.includes(slot);
|
|
950
|
+
/**
|
|
951
|
+
* Slots whose drawn part replaces a built-in part pixel for pixel: the part's cells are painted
|
|
952
|
+
* where the built-in part would be, and every cell shows its own color and tone.
|
|
953
|
+
*/
|
|
954
|
+
const REPLACE_PART_SLOTS = [
|
|
955
|
+
"outfit",
|
|
956
|
+
"accessory",
|
|
957
|
+
"beard",
|
|
958
|
+
"glasses",
|
|
959
|
+
"nose",
|
|
960
|
+
"cheeks",
|
|
961
|
+
"petals",
|
|
962
|
+
"flowerBase"
|
|
963
|
+
];
|
|
964
|
+
const isReplacePartSlot = (slot) => REPLACE_PART_SLOTS.includes(slot);
|
|
910
965
|
/** Tone steps on the rig's shade ramp: two darker, the color itself, two lighter. */
|
|
911
966
|
const PART_TONES = [
|
|
912
967
|
-2,
|
|
@@ -917,7 +972,7 @@ const PART_TONES = [
|
|
|
917
972
|
];
|
|
918
973
|
const PART_LAYERS = ["front", "back"];
|
|
919
974
|
/** Most fixed colors one part may use. */
|
|
920
|
-
const MAX_PART_FIXED_COLORS =
|
|
975
|
+
const MAX_PART_FIXED_COLORS = 32;
|
|
921
976
|
const FIXED = /^#[\da-f]{6}$/iu;
|
|
922
977
|
const isColor = (value) => typeof value === "string" && (AVATAR_COLORS.includes(value) || FIXED.test(value));
|
|
923
978
|
function isPixelCustomPart(value) {
|
|
@@ -925,7 +980,7 @@ function isPixelCustomPart(value) {
|
|
|
925
980
|
const r = value;
|
|
926
981
|
if (Object.keys(r).length !== 3 || !Object.hasOwn(PART_SLOTS, r["slot"])) return false;
|
|
927
982
|
const { width, height } = PART_SLOTS[r["slot"]];
|
|
928
|
-
if (
|
|
983
|
+
if (r["slot"] !== "headpiece" && r["back"]?.length !== 0) return false;
|
|
929
984
|
const fixed = /* @__PURE__ */ new Set();
|
|
930
985
|
for (const name of PART_LAYERS) {
|
|
931
986
|
const cells = r[name];
|
|
@@ -1025,6 +1080,182 @@ function fillPartLayer(slot, layer, x, y, ink, mirror = false) {
|
|
|
1025
1080
|
}
|
|
1026
1081
|
return out;
|
|
1027
1082
|
}
|
|
1083
|
+
/** Pixel-perfect line points (Bresenham). With `snap`, the end snaps to 0°, 45° or 90°. */
|
|
1084
|
+
function partLinePoints(from, to, snap = false) {
|
|
1085
|
+
let [x1, y1] = to;
|
|
1086
|
+
const [x0, y0] = from;
|
|
1087
|
+
if (snap) {
|
|
1088
|
+
const dx = x1 - x0;
|
|
1089
|
+
const dy = y1 - y0;
|
|
1090
|
+
const ax = Math.abs(dx);
|
|
1091
|
+
const ay = Math.abs(dy);
|
|
1092
|
+
if (ax > ay * 2) y1 = y0;
|
|
1093
|
+
else if (ay > ax * 2) x1 = x0;
|
|
1094
|
+
else {
|
|
1095
|
+
const m = Math.max(ax, ay);
|
|
1096
|
+
x1 = x0 + Math.sign(dx) * m;
|
|
1097
|
+
y1 = y0 + Math.sign(dy) * m;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
const points = [];
|
|
1101
|
+
const dx = Math.abs(x1 - x0);
|
|
1102
|
+
const dy = -Math.abs(y1 - y0);
|
|
1103
|
+
const sx = x0 < x1 ? 1 : -1;
|
|
1104
|
+
const sy = y0 < y1 ? 1 : -1;
|
|
1105
|
+
let error = dx + dy;
|
|
1106
|
+
let [x, y] = [x0, y0];
|
|
1107
|
+
for (;;) {
|
|
1108
|
+
points.push([x, y]);
|
|
1109
|
+
if (x === x1 && y === y1) break;
|
|
1110
|
+
const e2 = 2 * error;
|
|
1111
|
+
if (e2 >= dy) {
|
|
1112
|
+
error += dy;
|
|
1113
|
+
x += sx;
|
|
1114
|
+
}
|
|
1115
|
+
if (e2 <= dx) {
|
|
1116
|
+
error += dx;
|
|
1117
|
+
y += sy;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return points;
|
|
1121
|
+
}
|
|
1122
|
+
/** Rectangle outline points between two corners. With `square`, the far corner makes a square. */
|
|
1123
|
+
function partRectPoints(from, to, square = false) {
|
|
1124
|
+
const [x0, y0] = from;
|
|
1125
|
+
let [x1, y1] = to;
|
|
1126
|
+
if (square) {
|
|
1127
|
+
const m = Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0));
|
|
1128
|
+
x1 = x0 + (x1 < x0 ? -m : m);
|
|
1129
|
+
y1 = y0 + (y1 < y0 ? -m : m);
|
|
1130
|
+
}
|
|
1131
|
+
const [left, right] = [Math.min(x0, x1), Math.max(x0, x1)];
|
|
1132
|
+
const [top, bottom] = [Math.min(y0, y1), Math.max(y0, y1)];
|
|
1133
|
+
const points = /* @__PURE__ */ new Map();
|
|
1134
|
+
for (let x = left; x <= right; x++) for (const y of [top, bottom]) points.set(`${x},${y}`, [x, y]);
|
|
1135
|
+
for (let y = top; y <= bottom; y++) for (const x of [left, right]) points.set(`${x},${y}`, [x, y]);
|
|
1136
|
+
return [...points.values()];
|
|
1137
|
+
}
|
|
1138
|
+
const BAYER4 = [
|
|
1139
|
+
[
|
|
1140
|
+
0,
|
|
1141
|
+
8,
|
|
1142
|
+
2,
|
|
1143
|
+
10
|
|
1144
|
+
],
|
|
1145
|
+
[
|
|
1146
|
+
12,
|
|
1147
|
+
4,
|
|
1148
|
+
14,
|
|
1149
|
+
6
|
|
1150
|
+
],
|
|
1151
|
+
[
|
|
1152
|
+
3,
|
|
1153
|
+
11,
|
|
1154
|
+
1,
|
|
1155
|
+
9
|
|
1156
|
+
],
|
|
1157
|
+
[
|
|
1158
|
+
15,
|
|
1159
|
+
7,
|
|
1160
|
+
13,
|
|
1161
|
+
5
|
|
1162
|
+
]
|
|
1163
|
+
];
|
|
1164
|
+
const BAYER2 = [[0, 2], [3, 1]];
|
|
1165
|
+
/** The ordered-dither threshold in [0, 1) for a cell. */
|
|
1166
|
+
function ditherThreshold(x, y, size = 4) {
|
|
1167
|
+
return size === 4 ? (BAYER4[y & 3][x & 3] + .5) / 16 : (BAYER2[y & 1][x & 1] + .5) / 4;
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* A linear gradient over the 4-connected region of the start cell: each cell's position along
|
|
1171
|
+
* `from`→`to` steps through the tones between `fromTone` and `toTone`, with ordered dithering
|
|
1172
|
+
* between neighboring tones. No RGB is blended; the result is ordinary cells.
|
|
1173
|
+
*/
|
|
1174
|
+
function gradientPartLayer(slot, layer, from, to, color, fromTone, toTone, options = {}) {
|
|
1175
|
+
const { width, height } = PART_SLOTS[slot];
|
|
1176
|
+
const out = layer.map((row) => [...row]);
|
|
1177
|
+
const [fx, fy] = from;
|
|
1178
|
+
const region = (sx, sy) => {
|
|
1179
|
+
if (sx < 0 || sx >= width || sy < 0 || sy >= height) return [];
|
|
1180
|
+
const target = layer[sy][sx];
|
|
1181
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1182
|
+
const cells = [];
|
|
1183
|
+
const stack = [[sx, sy]];
|
|
1184
|
+
while (stack.length) {
|
|
1185
|
+
const [x, y] = stack.pop();
|
|
1186
|
+
if (x < 0 || x >= width || y < 0 || y >= height || seen.has(y * width + x)) continue;
|
|
1187
|
+
if (!same(layer[y][x], target)) continue;
|
|
1188
|
+
seen.add(y * width + x);
|
|
1189
|
+
cells.push([x, y]);
|
|
1190
|
+
stack.push([x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]);
|
|
1191
|
+
}
|
|
1192
|
+
return cells;
|
|
1193
|
+
};
|
|
1194
|
+
const paint = (cells, start, end, ditherX) => {
|
|
1195
|
+
const vx = end[0] - start[0];
|
|
1196
|
+
const vy = end[1] - start[1];
|
|
1197
|
+
const length = vx * vx + vy * vy;
|
|
1198
|
+
const steps = toTone - fromTone;
|
|
1199
|
+
for (const [x, y] of cells) {
|
|
1200
|
+
const position = (length === 0 ? 0 : Math.min(1, Math.max(0, ((x - start[0]) * vx + (y - start[1]) * vy) / length))) * Math.abs(steps);
|
|
1201
|
+
const base = Math.floor(position);
|
|
1202
|
+
const up = position - base > ditherThreshold(ditherX(x), y, options.dither ?? 4) ? 1 : 0;
|
|
1203
|
+
const tone = fromTone + Math.sign(steps) * Math.min(Math.abs(steps), base + up);
|
|
1204
|
+
out[y][x] = {
|
|
1205
|
+
color,
|
|
1206
|
+
tone
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
const own = region(fx, fy);
|
|
1211
|
+
if (!options.mirror) {
|
|
1212
|
+
paint(own, from, to, (x) => x);
|
|
1213
|
+
return out;
|
|
1214
|
+
}
|
|
1215
|
+
const mx = mirrorPartX(slot, fx);
|
|
1216
|
+
const mirroredCells = region(mx, fy);
|
|
1217
|
+
const inOwn = new Set(own.map(([x, y]) => y * width + x));
|
|
1218
|
+
const inMirrored = new Set(mirroredCells.map(([x, y]) => y * width + x));
|
|
1219
|
+
const startSide = (x) => x < width / 2 === fx < width / 2;
|
|
1220
|
+
paint(own.filter(([x, y]) => startSide(x) || !inMirrored.has(y * width + x)), from, to, (x) => x);
|
|
1221
|
+
paint(mirroredCells.filter(([x, y]) => !startSide(x) || !inOwn.has(y * width + x)), [mx, fy], [mirrorPartX(slot, to[0]), to[1]], (x) => mirrorPartX(slot, x));
|
|
1222
|
+
return out;
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Moves the tone of colored cells by ±1 at random, for texture. The same seed gives the same
|
|
1226
|
+
* result; the seed is used only now, and the result is ordinary cells. With `mirror`, each cell
|
|
1227
|
+
* and its mirror get the same change.
|
|
1228
|
+
*/
|
|
1229
|
+
function noisePartLayer(slot, layer, amount, seed, mirror = false) {
|
|
1230
|
+
const random = seededRandom(seed);
|
|
1231
|
+
const rolls = layer.map((row) => row.map(() => [random(), random() < .5 ? -1 : 1]));
|
|
1232
|
+
return layer.map((row, y) => row.map((ink, x) => {
|
|
1233
|
+
const source = mirror ? Math.min(x, mirrorPartX(slot, x)) : x;
|
|
1234
|
+
const [roll, direction] = rolls[y][source];
|
|
1235
|
+
if (!ink || roll >= amount) return ink;
|
|
1236
|
+
return {
|
|
1237
|
+
color: ink.color,
|
|
1238
|
+
tone: Math.max(-2, Math.min(2, ink.tone + direction))
|
|
1239
|
+
};
|
|
1240
|
+
}));
|
|
1241
|
+
}
|
|
1242
|
+
/** Moves the tone of each colored cell on the points by `delta` once; empty cells are skipped. */
|
|
1243
|
+
function shadePartLayer(slot, layer, points, delta, mirror = false) {
|
|
1244
|
+
const { width, height } = PART_SLOTS[slot];
|
|
1245
|
+
const out = layer.map((row) => [...row]);
|
|
1246
|
+
const done = /* @__PURE__ */ new Set();
|
|
1247
|
+
for (const [x, y] of points) for (const px of mirror ? [x, mirrorPartX(slot, x)] : [x]) {
|
|
1248
|
+
const key = `${px},${y}`;
|
|
1249
|
+
if (done.has(key) || px < 0 || px >= width || y < 0 || y >= height) continue;
|
|
1250
|
+
done.add(key);
|
|
1251
|
+
const ink = layer[y][px];
|
|
1252
|
+
if (ink) out[y][px] = {
|
|
1253
|
+
color: ink.color,
|
|
1254
|
+
tone: Math.max(-2, Math.min(2, ink.tone + delta))
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
return out;
|
|
1258
|
+
}
|
|
1028
1259
|
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
1260
|
function sha256(text) {
|
|
1030
1261
|
const bytes = new TextEncoder().encode(text);
|
|
@@ -1879,6 +2110,15 @@ function pixelTileColor(hair) {
|
|
|
1879
2110
|
].map((v) => v * 255));
|
|
1880
2111
|
}
|
|
1881
2112
|
function pixelFigure(recipe, yawDeg, options = {}) {
|
|
2113
|
+
const { grids: _, ...out } = renderFigure(recipe, yawDeg, options, /* @__PURE__ */ new Set());
|
|
2114
|
+
return out;
|
|
2115
|
+
}
|
|
2116
|
+
const FACE_SLOTS = [
|
|
2117
|
+
"nose",
|
|
2118
|
+
"cheeks",
|
|
2119
|
+
"glasses"
|
|
2120
|
+
];
|
|
2121
|
+
function renderFigure(recipe, yawDeg, options, omit) {
|
|
1882
2122
|
const yaw = yawDeg * Math.PI / 180;
|
|
1883
2123
|
const sinY = Math.sin(yaw);
|
|
1884
2124
|
const s = Math.round(sinY * 3);
|
|
@@ -1895,9 +2135,40 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
1895
2135
|
return front(x - Math.round(d * (1 + Math.max(0, y - 20) * .4) - d * tuck), y);
|
|
1896
2136
|
} : front;
|
|
1897
2137
|
const flower = recipe.species === "flower";
|
|
1898
|
-
const
|
|
2138
|
+
const hiddenNow = hiddenChoices(recipe);
|
|
2139
|
+
const drawnParts = new Map(REPLACE_PART_SLOTS.flatMap((slot) => {
|
|
2140
|
+
const part = wornPart(recipe, slot);
|
|
2141
|
+
const shown = slot === "petals" || slot === "flowerBase" ? flower : !hiddenNow.includes(slot);
|
|
2142
|
+
return part && shown ? [[slot, part.front]] : [];
|
|
2143
|
+
}));
|
|
2144
|
+
const skip = (slot) => omit.has(slot) || drawnParts.has(slot);
|
|
2145
|
+
const covered = !skip("accessory") && (recipe.accessory === "helmet" || recipe.accessory === "hood");
|
|
1899
2146
|
const bare = () => false;
|
|
1900
|
-
const petalRing = flower ? petals(recipe.petals ?? "trumpet", s) : bare;
|
|
2147
|
+
const petalRing = flower && !skip("petals") ? petals(recipe.petals ?? "trumpet", s) : bare;
|
|
2148
|
+
const faceShift = Math.round(fs * .6);
|
|
2149
|
+
const mouthCells = /* @__PURE__ */ new Set();
|
|
2150
|
+
for (const rows of [...Object.values(MOUTHS), [
|
|
2151
|
+
".K.",
|
|
2152
|
+
"KMK",
|
|
2153
|
+
".K."
|
|
2154
|
+
]]) {
|
|
2155
|
+
const left = Math.round(C - .5 + fs * .6 - (rows[0].length - 1) / 2);
|
|
2156
|
+
rows.forEach((row, dy) => [...row].forEach((code, dx) => {
|
|
2157
|
+
if (code !== ".") mouthCells.add(`${left + dx},${20 + (recipe.height ?? 0) + dy}`);
|
|
2158
|
+
}));
|
|
2159
|
+
}
|
|
2160
|
+
const mouthBox = (x, y) => mouthCells.has(`${x},${y}`);
|
|
2161
|
+
const overlay = (grid, slot, keepOutMouth = false) => {
|
|
2162
|
+
const cells = drawnParts.get(slot);
|
|
2163
|
+
if (!cells) return;
|
|
2164
|
+
const shift = FACE_SLOTS.includes(slot) ? faceShift : s;
|
|
2165
|
+
for (const [x, y, color, tone] of cells) {
|
|
2166
|
+
const px = x + shift;
|
|
2167
|
+
if (px < 0 || px >= N || keepOutMouth && mouthBox(px, y)) continue;
|
|
2168
|
+
const base = color.startsWith("#") ? color.toLowerCase() : recipe[color];
|
|
2169
|
+
grid[y][px] = TONES[tone](base);
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
1901
2172
|
const builtin = flower ? {
|
|
1902
2173
|
back: petalRing,
|
|
1903
2174
|
front: bare,
|
|
@@ -1941,8 +2212,13 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
1941
2212
|
};
|
|
1942
2213
|
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)));
|
|
1943
2214
|
const body = blank();
|
|
1944
|
-
if (flower)
|
|
1945
|
-
|
|
2215
|
+
if (flower) {
|
|
2216
|
+
if (!skip("flowerBase")) flowerBase(recipe, body, s);
|
|
2217
|
+
overlay(body, "flowerBase");
|
|
2218
|
+
} else {
|
|
2219
|
+
if (!skip("outfit")) outfit(recipe, body, s);
|
|
2220
|
+
overlay(body, "outfit");
|
|
2221
|
+
}
|
|
1946
2222
|
const back = blank();
|
|
1947
2223
|
if (flower) {
|
|
1948
2224
|
paint(back, petalRing, hairColor);
|
|
@@ -1966,11 +2242,12 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
1966
2242
|
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
2243
|
};
|
|
1968
2244
|
placeCells(back, inked("backHair"), false);
|
|
2245
|
+
overlay(back, "petals");
|
|
1969
2246
|
if (headpiece) placeCells(back, headpiece.back);
|
|
1970
2247
|
const head = blank();
|
|
1971
2248
|
paint(head, face, skin);
|
|
1972
2249
|
const h = recipe.height ?? 0;
|
|
1973
|
-
if (recipe.beard && !flower) {
|
|
2250
|
+
if (recipe.beard && !flower && !skip("beard")) {
|
|
1974
2251
|
const centre = C + fs * .6;
|
|
1975
2252
|
const mouthX = Math.round(C - .5 + fs * .6);
|
|
1976
2253
|
const chin = (x, y) => face(x, y) || face(x, y - 2);
|
|
@@ -2014,7 +2291,7 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
2014
2291
|
});
|
|
2015
2292
|
if (!pointed && !flower) paint(head, masks.front, hairColor);
|
|
2016
2293
|
for (const [x, y] of masks.ties ?? []) sprite(back, ["T"], Math.round(x), y, { T: shade(recipe.shirtColor, .8) });
|
|
2017
|
-
if (!flower) accessory(recipe, head, s, face);
|
|
2294
|
+
if (!flower && !skip("accessory")) accessory(recipe, head, s, face);
|
|
2018
2295
|
const shadeHair = (g) => {
|
|
2019
2296
|
const out = g.map((r) => [...r]);
|
|
2020
2297
|
const cells = g.flatMap((row, y) => row.flatMap((c, x) => c === hairColor ? [[x, y]] : []));
|
|
@@ -2055,6 +2332,9 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
2055
2332
|
}
|
|
2056
2333
|
return out;
|
|
2057
2334
|
};
|
|
2335
|
+
const drawnOver = blank();
|
|
2336
|
+
overlay(drawnOver, "beard", true);
|
|
2337
|
+
overlay(drawnOver, "accessory");
|
|
2058
2338
|
const crown = blank();
|
|
2059
2339
|
placeCells(crown, inked("all-front"), false);
|
|
2060
2340
|
if (headpiece) placeCells(crown, headpiece.front);
|
|
@@ -2063,6 +2343,7 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
2063
2343
|
back,
|
|
2064
2344
|
body,
|
|
2065
2345
|
shadeHair(head),
|
|
2346
|
+
drawnOver,
|
|
2066
2347
|
crown
|
|
2067
2348
|
]) for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (layer[y][x]) figure[y][x] = layer[y][x];
|
|
2068
2349
|
const features = blank();
|
|
@@ -2130,6 +2411,7 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
2130
2411
|
N: shade(skin, .84),
|
|
2131
2412
|
n: shade(skin, .72)
|
|
2132
2413
|
});
|
|
2414
|
+
else if (skip("nose")) overlay(features, "nose", true);
|
|
2133
2415
|
else if (recipe.nose === "dot") sprite(features, ["N"], cx, 18 + h, { N: shade(skin, .78) });
|
|
2134
2416
|
else if (recipe.nose === "button") sprite(features, ["N"], cx, 18 + h, { N: shade(skin, .88) });
|
|
2135
2417
|
else if (recipe.nose === "line") sprite(features, ["N", "N"], cx, 17 + h, { N: shade(skin, .82) });
|
|
@@ -2144,11 +2426,12 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
2144
2426
|
});
|
|
2145
2427
|
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 });
|
|
2146
2428
|
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 });
|
|
2147
|
-
if (
|
|
2429
|
+
if (skip("cheeks")) overlay(layer, "cheeks", true);
|
|
2430
|
+
if (recipe.cheeks === "blush" && !flower && !skip("cheeks")) {
|
|
2148
2431
|
sprite(layer, ["PP"], left.x - 1, 19 + h, { P: mix$1(BLUSH, skin, .25) });
|
|
2149
2432
|
sprite(layer, ["PP"], right.x + right.w - 1, 19 + h, { P: mix$1(BLUSH, skin, .25) });
|
|
2150
2433
|
}
|
|
2151
|
-
if (recipe.cheeks === "freckles" && !flower) {
|
|
2434
|
+
if (recipe.cheeks === "freckles" && !flower && !skip("cheeks")) {
|
|
2152
2435
|
sprite(layer, ["F.F"], left.x, 19 + h, { F: shade(skin, .7) });
|
|
2153
2436
|
sprite(layer, ["F.F"], right.x + 1, 19 + h, { F: shade(skin, .7) });
|
|
2154
2437
|
}
|
|
@@ -2171,7 +2454,8 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
2171
2454
|
const corner = (x === fx - 1 || x === fx + w) && (y === eyeTop - 1 || y === eyeTop + 4);
|
|
2172
2455
|
return box && !inside && !(round && corner);
|
|
2173
2456
|
}, INK$1);
|
|
2174
|
-
const lenses = flower ? "none" : recipe.glasses;
|
|
2457
|
+
const lenses = flower || skip("glasses") ? "none" : recipe.glasses;
|
|
2458
|
+
overlay(glasses, "glasses", true);
|
|
2175
2459
|
if (lenses === "round" || lenses === "square") {
|
|
2176
2460
|
frame(left.x, left.w, lenses === "round");
|
|
2177
2461
|
frame(right.x, right.w, lenses === "round");
|
|
@@ -2201,7 +2485,7 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
2201
2485
|
hairColor: color
|
|
2202
2486
|
};
|
|
2203
2487
|
for (const k of AVATAR_PIECE_COLORS) delete plain[k];
|
|
2204
|
-
for (const cell of
|
|
2488
|
+
for (const cell of renderFigure(plain, yawDeg, {}, omit).cells) variant[cell.y][cell.x] = cell.c;
|
|
2205
2489
|
const near = (x, y) => mask(x, y) || mask(x - 1, y) || mask(x + 1, y) || mask(x, y - 1) || mask(x, y + 1);
|
|
2206
2490
|
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];
|
|
2207
2491
|
});
|
|
@@ -2210,7 +2494,7 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
2210
2494
|
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
|
|
2211
2495
|
const c = full[y][x];
|
|
2212
2496
|
if (!c) continue;
|
|
2213
|
-
if (y >= 22 && !head[y][x] && !back[y][x] && !crown[y][x]) bodyCells[y][x] = c;
|
|
2497
|
+
if (y >= 22 && !head[y][x] && !back[y][x] && !crown[y][x] && !drawnOver[y][x]) bodyCells[y][x] = c;
|
|
2214
2498
|
else headCells[y][x] = c;
|
|
2215
2499
|
}
|
|
2216
2500
|
const cells = [];
|
|
@@ -2223,6 +2507,13 @@ function pixelFigure(recipe, yawDeg, options = {}) {
|
|
|
2223
2507
|
});
|
|
2224
2508
|
}
|
|
2225
2509
|
return {
|
|
2510
|
+
grids: {
|
|
2511
|
+
figure,
|
|
2512
|
+
features: savedFeatures,
|
|
2513
|
+
glasses,
|
|
2514
|
+
back,
|
|
2515
|
+
body
|
|
2516
|
+
},
|
|
2226
2517
|
cells,
|
|
2227
2518
|
tile: `<rect width="32" height="32" rx="6" fill="${pixelTileColor(recipe.hairColor)}"/>`,
|
|
2228
2519
|
body: `<g class="bh-illustrated-body">${rects(bodyCells)}</g>`,
|
|
@@ -2269,6 +2560,48 @@ function hairPieceStart(recipe, slot) {
|
|
|
2269
2560
|
back: []
|
|
2270
2561
|
};
|
|
2271
2562
|
}
|
|
2563
|
+
/**
|
|
2564
|
+
* The part in a replacement slot as a Custom Part to start drawing from: the drawn part worn
|
|
2565
|
+
* there, or the built-in part flattened for this recipe in the front pose. Each pixel becomes a
|
|
2566
|
+
* color slot and tone when it is exactly one, so it recolors with the Avatar, and a fixed color
|
|
2567
|
+
* otherwise; an unchanged copy renders identically. A flattened helmet or hood no longer hides
|
|
2568
|
+
* the hair: only the built-in ones do.
|
|
2569
|
+
*/
|
|
2570
|
+
function replacePartStart(recipe, slot) {
|
|
2571
|
+
const worn = wornPart(recipe, slot);
|
|
2572
|
+
if (worn) return {
|
|
2573
|
+
slot,
|
|
2574
|
+
front: [...worn.front],
|
|
2575
|
+
back: []
|
|
2576
|
+
};
|
|
2577
|
+
const layer = slot === "glasses" ? "glasses" : slot === "nose" || slot === "cheeks" ? "features" : slot === "petals" ? "back" : slot === "outfit" || slot === "flowerBase" ? "body" : "figure";
|
|
2578
|
+
const shown = renderFigure(recipe, 0, {}, /* @__PURE__ */ new Set()).grids[layer];
|
|
2579
|
+
const without = renderFigure(recipe, 0, {}, new Set([slot])).grids[layer];
|
|
2580
|
+
const source = (c) => {
|
|
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
|
+
};
|
|
2590
|
+
const front = [];
|
|
2591
|
+
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) {
|
|
2592
|
+
const c = shown[y][x];
|
|
2593
|
+
if (c && c !== without[y][x]) front.push([
|
|
2594
|
+
x,
|
|
2595
|
+
y,
|
|
2596
|
+
...source(c)
|
|
2597
|
+
]);
|
|
2598
|
+
}
|
|
2599
|
+
return {
|
|
2600
|
+
slot,
|
|
2601
|
+
front,
|
|
2602
|
+
back: []
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2272
2605
|
|
|
2273
2606
|
//#endregion
|
|
2274
2607
|
//#region src/svg.ts
|
|
@@ -2792,5 +3125,5 @@ function symbolArtCells(rows, color) {
|
|
|
2792
3125
|
}
|
|
2793
3126
|
|
|
2794
3127
|
//#endregion
|
|
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 };
|
|
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 };
|
|
2796
3129
|
//# sourceMappingURL=index.js.map
|