@botharness/pixel-avatar 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,5 +24,7 @@ const svg = pixelAvatarSvg(recipe); // 32×32 viewBox, crisp edges, no ids or sc
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
25
  - `REPLACE_PART_SLOTS`, `replacePartStart(recipe, slot)`: drawn `outfit`, `accessory`, `beard`, `glasses`, `nose`, `cheeks`, `petals` and `flowerBase` parts (front layer only). A drawn part replaces the built-in part pixel for pixel in the layer that part paints into, while the built-in choice stays saved underneath. `replacePartStart` flattens the worn built-in part for this recipe: pixels that are exactly an appearance color at a tone become that color slot, and the rest become fixed colors, so an unchanged copy renders identically. Drawn face parts never cover the speaking mouth, and drawn parts are hidden wherever the species or headwear hides the built-in part. A drawn accessory replacing a helmet or hood no longer hides the hair.
26
26
  - `emptyPartLayer`, `paintPartLayer`, `fillPartLayer`, `mirrorPartX`, `partLayer`, `partCells`, `createCustomPart`: drawing on dense layers, with pencil and eraser strokes, 4-connected flood fill and mirroring across the Avatar centerline.
27
+ - `partLinePoints`, `partRectPoints`, `gradientPartLayer`, `ditherThreshold`, `noisePartLayer`, `shadePartLayer`: line (with 0°/45°/90° snap), rectangle (with square), tone gradient with 4×4 or 2×2 ordered dithering inside the start region, seeded ±1 tone noise, and a shade stroke that moves each colored cell one tone. All mirror, and all produce ordinary cells: the seed and the tool are not part of the part.
28
+ - `AVATAR_PIECE_COLORS_V4`, `AVATAR_STRANDS`, `AVATAR_HEADPIECES`, `withPieces`, `withBuiltInHeadpiece`, `builtInHeadpiece`, `headpieceStart`: asset version 4. Bangs, back hair and a single `strand` each take an optional color (`bangsColor`, `backHairColor`, `strandColor`) that defaults to `hairColor`, like the side hair colors. The headpiece slot holds a built-in headpiece (ears, horns, halo or small head wings) or a Custom Part, worn together with any accessory. The base sits behind the hair and the rest in front, and the far side hides when the head turns. `withPieces` upgrades a recipe when the Human edits and moves an accessory that is now a headpiece into the empty headpiece slot. Saved recipes are never migrated on their own, so they keep rendering as before. `headpieceStart` flattens the worn headpiece into a Custom Part to start drawing from.
27
29
  - `faceCells`, `pixelSymbolCells`, `symbolArtCells`: pixels for [`@botharness/pixel-morph`](../morph).
28
30
  - `pixelAvatarSvg(recipe, { turns, classPrefix })`: optional pre-rendered head turns, and rig layer classes (`<prefix>-body`, `-head`, `-face`, `-gaze`, `-blink`) for CSS animation.
package/dist/index.d.ts CHANGED
@@ -39,6 +39,22 @@ type AvatarSpecies = (typeof AVATAR_SPECIES)[number];
39
39
  /** Optional per-piece hair colors (asset version 2); an absent piece uses `hairColor`. */
40
40
  declare const AVATAR_PIECE_COLORS: readonly ["leftSideHairColor", "rightSideHairColor"];
41
41
  type AvatarPieceColor = (typeof AVATAR_PIECE_COLORS)[number];
42
+ /**
43
+ * Asset version 4 adds a color for every other hair piece; an absent piece uses `hairColor`.
44
+ * `strandColor` colors the single strand (`strand`).
45
+ */
46
+ declare const AVATAR_PIECE_COLORS_V4: readonly ["bangsColor", "backHairColor", "strandColor"];
47
+ type AvatarPieceColorV4 = (typeof AVATAR_PIECE_COLORS_V4)[number];
48
+ /** A single strand of hair standing up from the crown (asset version 4). */
49
+ declare const AVATAR_STRANDS: readonly ["ahoge", "curl", "double"];
50
+ type AvatarStrand = (typeof AVATAR_STRANDS)[number];
51
+ /**
52
+ * Built-in headpieces (asset version 4), worn in the headpiece slot alongside an accessory and
53
+ * drawn partly behind the hair. The first five were accessories in earlier versions.
54
+ */
55
+ declare const AVATAR_HEADPIECES: readonly ["catears", "bunnyears", "horseears", "horns", "halo", "wings"];
56
+ type AvatarHeadpiece = (typeof AVATAR_HEADPIECES)[number];
57
+ type V4Key = AvatarPieceColorV4 | 'strand';
42
58
  /** Part choices that exist only in asset version 2, added after every version 1 choice. */
43
59
  declare const AVATAR_PARTS_V2: {
44
60
  readonly outfit: readonly ["tee", "shirttie", "hoodie", "turtleneck", "sailor", "blazer", "overalls", "dress", "kimono", "cardigan", "maid", "jacket", "armor", "robe", "tunic", "cloak"];
@@ -72,7 +88,7 @@ type PixelAvatarRecipeV1 = Meta & {
72
88
  } & { [P in AvatarPart]: (typeof AVATAR_PARTS)[P][number] } & { [P in AvatarHairPart]?: (typeof AVATAR_PARTS)['hair'][number] } & { [P in AvatarRange]?: number } & {
73
89
  species?: never;
74
90
  rightSideHair?: never;
75
- } & { [P in AvatarPieceColor | AvatarExtraPart]?: never } & { [P in CustomPartKey]?: never };
91
+ } & { [P in AvatarPieceColor | AvatarExtraPart]?: never } & { [P in CustomPartKey]?: never } & { [P in V4Key]?: never };
76
92
  /**
77
93
  * Asset version 2: a species, the full split hair and geometry, a separate right side hair
78
94
  * (`sideHair` is then the left side) and optional per-side hair colors.
@@ -82,7 +98,7 @@ type PixelAvatarRecipeV2 = Meta & {
82
98
  } & { [P in AvatarPart]: (typeof AVATAR_PARTS_V2)[P][number] } & { [P in AvatarHairPart]: (typeof AVATAR_PARTS)['hair'][number] } & { [P in AvatarRange]: number } & {
83
99
  species: AvatarSpecies;
84
100
  rightSideHair: (typeof AVATAR_HAIR_PARTS)['sideHair'][number];
85
- } & { [P in AvatarPieceColor]?: string } & { [P in AvatarExtraPart]?: (typeof AVATAR_EXTRA_PARTS)[P][number] } & { [P in CustomPartKey]?: never };
101
+ } & { [P in AvatarPieceColor]?: string } & { [P in AvatarExtraPart]?: (typeof AVATAR_EXTRA_PARTS)[P][number] } & { [P in CustomPartKey]?: never } & { [P in V4Key]?: never };
86
102
  /** The recipe key that embeds the Custom Part worn in each slot. */
87
103
  declare const CUSTOM_PART_KEYS: {
88
104
  readonly headpiece: "headpiece";
@@ -107,7 +123,18 @@ type CustomPartKey = (typeof CUSTOM_PART_KEYS)[PartSlot];
107
123
  type PixelAvatarRecipeV3 = Omit<PixelAvatarRecipeV2, 'assetVersion' | CustomPartKey> & {
108
124
  assetVersion: 3;
109
125
  } & { [P in CustomPartKey]?: PixelCustomPart };
110
- type PixelAvatarRecipe = PixelAvatarRecipeV1 | PixelAvatarRecipeV2 | PixelAvatarRecipeV3;
126
+ /**
127
+ * Asset version 4: version 2 with a color for every hair piece, a single `strand`, and the
128
+ * headpiece slot holding either a built-in headpiece or a Custom Part, with any number of
129
+ * Custom Parts worn (none included).
130
+ */
131
+ type PixelAvatarRecipeV4 = Omit<PixelAvatarRecipeV2, 'assetVersion' | CustomPartKey | V4Key> & {
132
+ assetVersion: 4;
133
+ } & { [P in Exclude<CustomPartKey, 'headpiece'>]?: PixelCustomPart } & {
134
+ headpiece?: PixelCustomPart | AvatarHeadpiece;
135
+ strand?: AvatarStrand;
136
+ } & { [P in AvatarPieceColorV4]?: string };
137
+ type PixelAvatarRecipe = PixelAvatarRecipeV1 | PixelAvatarRecipeV2 | PixelAvatarRecipeV3 | PixelAvatarRecipeV4;
111
138
  declare const DEFAULT_RECIPE: PixelAvatarRecipeV1;
112
139
  declare function isPixelAvatarRecipe(value: unknown): value is PixelAvatarRecipe;
113
140
  declare function canonicalRecipe(recipe: PixelAvatarRecipe): PixelAvatarRecipe;
@@ -123,16 +150,29 @@ declare function hiddenChoices(recipe: PixelAvatarRecipe): readonly string[];
123
150
  * When the skin color is one of the previous species' suggested colors, it moves to the new
124
151
  * species' first suggestion; a custom color is kept.
125
152
  */
153
+ declare function withSpecies(recipe: PixelAvatarRecipeV4, species: AvatarSpecies): PixelAvatarRecipeV4;
126
154
  declare function withSpecies(recipe: PixelAvatarRecipeV3, species: AvatarSpecies): PixelAvatarRecipeV3;
127
155
  declare function withSpecies(recipe: PixelAvatarRecipeV1 | PixelAvatarRecipeV2, species: AvatarSpecies): PixelAvatarRecipeV2;
128
- declare function withSpecies(recipe: PixelAvatarRecipe, species: AvatarSpecies): PixelAvatarRecipeV2 | PixelAvatarRecipeV3;
156
+ declare function withSpecies(recipe: PixelAvatarRecipe, species: AvatarSpecies): PixelAvatarRecipeV2 | PixelAvatarRecipeV3 | PixelAvatarRecipeV4;
129
157
  /**
130
158
  * Returns the recipe wearing `part` in `slot`, or with that slot's part taken off. The recipe
131
- * embeds its own copy; it is asset version 3 while it wears any part and version 2 otherwise.
159
+ * embeds its own copy; it is asset version 3 while it wears any part and version 2 otherwise,
160
+ * and a version 4 recipe stays version 4.
161
+ */
162
+ declare function withCustomPart(recipe: PixelAvatarRecipeV4, slot: PartSlot, part: PixelCustomPart | undefined): PixelAvatarRecipeV4;
163
+ declare function withCustomPart(recipe: PixelAvatarRecipe, slot: PartSlot, part: PixelCustomPart | undefined): PixelAvatarRecipeV2 | PixelAvatarRecipeV3 | PixelAvatarRecipeV4;
164
+ /**
165
+ * Returns the recipe as asset version 4, keeping every choice. An accessory that is now a
166
+ * headpiece moves into the empty headpiece slot, freeing the accessory slot; call this only when
167
+ * the Human edits, so saved recipes keep rendering as they were.
132
168
  */
133
- declare function withCustomPart(recipe: PixelAvatarRecipe, slot: PartSlot, part: PixelCustomPart | undefined): PixelAvatarRecipeV2 | PixelAvatarRecipeV3;
169
+ declare function withPieces(recipe: PixelAvatarRecipe): PixelAvatarRecipeV4;
170
+ /** Returns the recipe as asset version 4 wearing a built-in headpiece, or none. */
171
+ declare function withBuiltInHeadpiece(recipe: PixelAvatarRecipe, headpiece: AvatarHeadpiece | undefined): PixelAvatarRecipeV4;
172
+ /** The built-in headpiece worn, if any. */
173
+ declare function builtInHeadpiece(recipe: PixelAvatarRecipe): AvatarHeadpiece | undefined;
134
174
  /** `withCustomPart` for the headpiece slot. */
135
- declare function withHeadpiece(recipe: PixelAvatarRecipe, part: PixelCustomPart | undefined): PixelAvatarRecipeV2 | PixelAvatarRecipeV3;
175
+ declare function withHeadpiece(recipe: PixelAvatarRecipe, part: PixelCustomPart | undefined): PixelAvatarRecipeV2 | PixelAvatarRecipeV3 | PixelAvatarRecipeV4;
136
176
  /** The Custom Part worn in a slot, if any. */
137
177
  declare function wornPart(recipe: PixelAvatarRecipe, slot: PartSlot): PixelCustomPart | undefined;
138
178
  declare function detailedRecipe(recipe: PixelAvatarRecipe): PixelAvatarRecipe;
@@ -278,6 +318,29 @@ declare function paintPartLayer(slot: PartSlot, layer: PartLayer, points: readon
278
318
  * becomes `ink`. With `mirror`, the mirrored start is filled too.
279
319
  */
280
320
  declare function fillPartLayer(slot: PartSlot, layer: PartLayer, x: number, y: number, ink: PartInk | null, mirror?: boolean): PartLayer;
321
+ /** Pixel-perfect line points (Bresenham). With `snap`, the end snaps to 0°, 45° or 90°. */
322
+ declare function partLinePoints(from: readonly [number, number], to: readonly [number, number], snap?: boolean): [number, number][];
323
+ /** Rectangle outline points between two corners. With `square`, the far corner makes a square. */
324
+ declare function partRectPoints(from: readonly [number, number], to: readonly [number, number], square?: boolean): [number, number][];
325
+ /** The ordered-dither threshold in [0, 1) for a cell. */
326
+ declare function ditherThreshold(x: number, y: number, size?: 2 | 4): number;
327
+ /**
328
+ * A linear gradient over the 4-connected region of the start cell: each cell's position along
329
+ * `from`→`to` steps through the tones between `fromTone` and `toTone`, with ordered dithering
330
+ * between neighboring tones. No RGB is blended; the result is ordinary cells.
331
+ */
332
+ declare function gradientPartLayer(slot: PartSlot, layer: PartLayer, from: readonly [number, number], to: readonly [number, number], color: PartColor, fromTone: PartTone, toTone: PartTone, options?: {
333
+ dither?: 2 | 4;
334
+ mirror?: boolean;
335
+ }): PartLayer;
336
+ /**
337
+ * Moves the tone of colored cells by ±1 at random, for texture. The same seed gives the same
338
+ * result; the seed is used only now, and the result is ordinary cells. With `mirror`, each cell
339
+ * and its mirror get the same change.
340
+ */
341
+ declare function noisePartLayer(slot: PartSlot, layer: PartLayer, amount: number, seed: string, mirror?: boolean): PartLayer;
342
+ /** Moves the tone of each colored cell on the points by `delta` once; empty cells are skipped. */
343
+ declare function shadePartLayer(slot: PartSlot, layer: PartLayer, points: readonly (readonly [number, number])[], delta: 1 | -1, mirror?: boolean): PartLayer;
281
344
  //#endregion
282
345
  //#region src/figure.d.ts
283
346
  type Cell = string | undefined;
@@ -307,6 +370,16 @@ declare function hairPieceStart(recipe: PixelAvatarRecipe, slot: HairPartSlot):
307
370
  front: PartCell[];
308
371
  back: PartCell[];
309
372
  };
373
+ /**
374
+ * The headpiece as a Custom Part to start drawing from: the drawn headpiece worn, or the
375
+ * built-in headpiece flattened for this recipe in the front pose, its base on the back layer and
376
+ * the rest on the front layer, so an unchanged copy renders identically facing front.
377
+ */
378
+ declare function headpieceStart(recipe: PixelAvatarRecipe): {
379
+ slot: 'headpiece';
380
+ front: PartCell[];
381
+ back: PartCell[];
382
+ };
310
383
  /**
311
384
  * The part in a replacement slot as a Custom Part to start drawing from: the drawn part worn
312
385
  * there, or the built-in part flattened for this recipe in the front pose. Each pixel becomes a
@@ -368,5 +441,5 @@ declare function pixelSymbolCells(symbol: PixelSymbol, color: string): PixelCell
368
441
  */
369
442
  declare function symbolArtCells(rows: readonly string[], color: string): PixelCell$1[];
370
443
  //#endregion
371
- 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, emptyPartLayer, faceCells, fillPartLayer, hairPieceStart, hiddenChoices, isHairPartSlot, isPixelAvatarRecipe, isPixelCustomPart, isReplacePartSlot, mirrorPartX, paintPartLayer, partCells, partLayer, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, replacePartStart, seededRandom, seededRecipe, seededRecipeV2, symbolArtCells, withCustomPart, withHeadpiece, withSpecies, wornPart };
444
+ export { AVATAR_COLORS, AVATAR_EXTRA_PARTS, AVATAR_HAIR_PARTS, AVATAR_HEADPIECES, AVATAR_PARTS, AVATAR_PARTS_V2, AVATAR_PIECE_COLORS, AVATAR_PIECE_COLORS_V4, AVATAR_PRESETS, AVATAR_RANGES, AVATAR_SPECIES, AVATAR_SPECIES_SWATCHES, AVATAR_STRANDS, AVATAR_SWATCHES, AVATAR_TURNS, type AvatarColor, type AvatarExtraPart, type AvatarHairPart, type AvatarHeadpiece, type AvatarPart, type AvatarPieceColor, type AvatarPieceColorV4, type AvatarRange, type AvatarSpecies, type AvatarStrand, CUSTOM_PART_KEYS, type CustomPartKey, DEFAULT_RECIPE, HAIR_PART_SLOTS, type HairPartSlot, MAX_PART_FIXED_COLORS, PART_LAYERS, PART_SLOTS, PART_TONES, PIXEL_SYMBOLS, type PartCell, type PartColor, type PartInk, type PartLayer, type PartLayerName, type PartSlot, type PartTone, type PixelAvatarRecipe, type PixelAvatarRecipeV1, type PixelAvatarRecipeV2, type PixelAvatarRecipeV3, type PixelAvatarRecipeV4, type PixelAvatarSvgOptions, type PixelCell, type PixelCustomPart, type PixelFigureOptions, type PixelGrid, type PixelMouthState, type PixelSymbol, REPLACE_PART_SLOTS, type ReplacePartSlot, builtInHeadpiece, canonicalCustomPart, canonicalRecipe, createCustomPart, createSeededRecipe, createSeededRecipeV2, customPartId, detailedRecipe, ditherThreshold, emptyPartLayer, faceCells, fillPartLayer, gradientPartLayer, hairPieceStart, headpieceStart, hiddenChoices, isHairPartSlot, isPixelAvatarRecipe, isPixelCustomPart, isReplacePartSlot, mirrorPartX, noisePartLayer, paintPartLayer, partCells, partLayer, partLinePoints, partRectPoints, partToneColor, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, replacePartStart, seededRandom, seededRecipe, seededRecipeV2, shadePartLayer, symbolArtCells, withBuiltInHeadpiece, withCustomPart, withHeadpiece, withPieces, withSpecies, wornPart };
372
445
  //# sourceMappingURL=index.d.ts.map