@botharness/pixel-avatar 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,5 +20,8 @@ const svg = pixelAvatarSvg(recipe); // 32×32 viewBox, crisp edges, no ids or sc
20
20
  - `AVATAR_HAIR_PARTS`, `AVATAR_RANGES`, `detailedRecipe(recipe)`: optional finer controls. A recipe may carry `bangs`, `sideHair`, `backHair`, `spacing`, `height` and `hairLength`, all six or none; `detailedRecipe` derives them from the plain `hair` so an editor can start from what is on screen. Recipes without them render exactly as before.
21
21
  - `AVATAR_SPECIES`, `AVATAR_SPECIES_SWATCHES`, `AVATAR_PIECE_COLORS`, `withSpecies(recipe, species)`: Avatar Species (`human`, `goblin`) on the same rig, as asset version 2. `withSpecies` keeps every choice, splits side hair into `sideHair` (left) and `rightSideHair`, and accepts optional `leftSideHairColor`/`rightSideHairColor`, where left and right are as seen on screen. Consumers built before version 2 reject these recipes with `isPixelAvatarRecipe` and can show a saved snapshot instead.
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
+ - `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
+ - `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
+ - `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.
23
26
  - `faceCells`, `pixelSymbolCells`, `symbolArtCells`: pixels for [`@botharness/pixel-morph`](../morph).
24
27
  - `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
@@ -72,7 +72,7 @@ type PixelAvatarRecipeV1 = Meta & {
72
72
  } & { [P in AvatarPart]: (typeof AVATAR_PARTS)[P][number] } & { [P in AvatarHairPart]?: (typeof AVATAR_PARTS)['hair'][number] } & { [P in AvatarRange]?: number } & {
73
73
  species?: never;
74
74
  rightSideHair?: never;
75
- } & { [P in AvatarPieceColor | AvatarExtraPart]?: never };
75
+ } & { [P in AvatarPieceColor | AvatarExtraPart]?: never } & { [P in CustomPartKey]?: never };
76
76
  /**
77
77
  * Asset version 2: a species, the full split hair and geometry, a separate right side hair
78
78
  * (`sideHair` is then the left side) and optional per-side hair colors.
@@ -82,8 +82,24 @@ type PixelAvatarRecipeV2 = Meta & {
82
82
  } & { [P in AvatarPart]: (typeof AVATAR_PARTS_V2)[P][number] } & { [P in AvatarHairPart]: (typeof AVATAR_PARTS)['hair'][number] } & { [P in AvatarRange]: number } & {
83
83
  species: AvatarSpecies;
84
84
  rightSideHair: (typeof AVATAR_HAIR_PARTS)['sideHair'][number];
85
- } & { [P in AvatarPieceColor]?: string } & { [P in AvatarExtraPart]?: (typeof AVATAR_EXTRA_PARTS)[P][number] };
86
- type PixelAvatarRecipe = PixelAvatarRecipeV1 | PixelAvatarRecipeV2;
85
+ } & { [P in AvatarPieceColor]?: string } & { [P in AvatarExtraPart]?: (typeof AVATAR_EXTRA_PARTS)[P][number] } & { [P in CustomPartKey]?: never };
86
+ /** The recipe key that embeds the Custom Part worn in each slot. */
87
+ declare const CUSTOM_PART_KEYS: {
88
+ readonly headpiece: "headpiece";
89
+ readonly bangs: "bangsPart";
90
+ readonly leftSideHair: "leftSideHairPart";
91
+ readonly rightSideHair: "rightSideHairPart";
92
+ readonly backHair: "backHairPart";
93
+ };
94
+ type CustomPartKey = (typeof CUSTOM_PART_KEYS)[PartSlot];
95
+ /**
96
+ * Asset version 3: version 2 wearing at least one embedded Custom Part. A drawn hair piece
97
+ * replaces the built-in piece, which stays saved and returns when the part is taken off.
98
+ */
99
+ type PixelAvatarRecipeV3 = Omit<PixelAvatarRecipeV2, 'assetVersion' | CustomPartKey> & {
100
+ assetVersion: 3;
101
+ } & { [P in CustomPartKey]?: PixelCustomPart };
102
+ type PixelAvatarRecipe = PixelAvatarRecipeV1 | PixelAvatarRecipeV2 | PixelAvatarRecipeV3;
87
103
  declare const DEFAULT_RECIPE: PixelAvatarRecipeV1;
88
104
  declare function isPixelAvatarRecipe(value: unknown): value is PixelAvatarRecipe;
89
105
  declare function canonicalRecipe(recipe: PixelAvatarRecipe): PixelAvatarRecipe;
@@ -99,7 +115,18 @@ declare function hiddenChoices(recipe: PixelAvatarRecipe): readonly string[];
99
115
  * When the skin color is one of the previous species' suggested colors, it moves to the new
100
116
  * species' first suggestion; a custom color is kept.
101
117
  */
102
- declare function withSpecies(recipe: PixelAvatarRecipe, species: AvatarSpecies): PixelAvatarRecipeV2;
118
+ declare function withSpecies(recipe: PixelAvatarRecipeV3, species: AvatarSpecies): PixelAvatarRecipeV3;
119
+ declare function withSpecies(recipe: PixelAvatarRecipeV1 | PixelAvatarRecipeV2, species: AvatarSpecies): PixelAvatarRecipeV2;
120
+ declare function withSpecies(recipe: PixelAvatarRecipe, species: AvatarSpecies): PixelAvatarRecipeV2 | PixelAvatarRecipeV3;
121
+ /**
122
+ * Returns the recipe wearing `part` in `slot`, or with that slot's part taken off. The recipe
123
+ * embeds its own copy; it is asset version 3 while it wears any part and version 2 otherwise.
124
+ */
125
+ declare function withCustomPart(recipe: PixelAvatarRecipe, slot: PartSlot, part: PixelCustomPart | undefined): PixelAvatarRecipeV2 | PixelAvatarRecipeV3;
126
+ /** `withCustomPart` for the headpiece slot. */
127
+ declare function withHeadpiece(recipe: PixelAvatarRecipe, part: PixelCustomPart | undefined): PixelAvatarRecipeV2 | PixelAvatarRecipeV3;
128
+ /** The Custom Part worn in a slot, if any. */
129
+ declare function wornPart(recipe: PixelAvatarRecipe, slot: PartSlot): PixelCustomPart | undefined;
103
130
  declare function detailedRecipe(recipe: PixelAvatarRecipe): PixelAvatarRecipe;
104
131
  declare const AVATAR_SWATCHES: Record<AvatarColor, readonly string[]>;
105
132
  /**
@@ -122,6 +149,89 @@ declare const seededRecipe: (seed: string) => PixelAvatarRecipe;
122
149
  declare const AVATAR_SPECIES_SWATCHES: Record<AvatarSpecies, readonly string[]>;
123
150
  declare const AVATAR_PRESETS: readonly PixelAvatarRecipe[];
124
151
  //#endregion
152
+ //#region src/part.d.ts
153
+ /**
154
+ * Custom Part slots. A headpiece covers the top of the tile, around the hair: its back layer is
155
+ * drawn behind the hair and its front layer over it. Hair slots replace one built-in hair piece
156
+ * and use only the front layer. Cells are in tile coordinates, so the Avatar centerline sits
157
+ * between x = 15 and x = 16.
158
+ */
159
+ declare const PART_SLOTS: {
160
+ readonly headpiece: {
161
+ readonly width: 32;
162
+ readonly height: 16;
163
+ };
164
+ readonly bangs: {
165
+ readonly width: 32;
166
+ readonly height: 32;
167
+ };
168
+ readonly leftSideHair: {
169
+ readonly width: 32;
170
+ readonly height: 32;
171
+ };
172
+ readonly rightSideHair: {
173
+ readonly width: 32;
174
+ readonly height: 32;
175
+ };
176
+ readonly backHair: {
177
+ readonly width: 32;
178
+ readonly height: 32;
179
+ };
180
+ };
181
+ type PartSlot = keyof typeof PART_SLOTS;
182
+ /**
183
+ * Hair slots. In a hair part, a `hairColor` cell at tone 0 is live hair: it is shaded with the
184
+ * rest of the hair, exactly like a built-in piece. Every other cell shows its own color.
185
+ */
186
+ declare const HAIR_PART_SLOTS: readonly ["bangs", "leftSideHair", "rightSideHair", "backHair"];
187
+ type HairPartSlot = (typeof HAIR_PART_SLOTS)[number];
188
+ declare const isHairPartSlot: (slot: PartSlot) => slot is HairPartSlot;
189
+ /** Tone steps on the rig's shade ramp: two darker, the color itself, two lighter. */
190
+ declare const PART_TONES: readonly [-2, -1, 0, 1, 2];
191
+ type PartTone = (typeof PART_TONES)[number];
192
+ /** An appearance color slot (it follows the Avatar's color) or a fixed `#rrggbb` color. */
193
+ type PartColor = AvatarColor | `#${string}`;
194
+ /** One painted cell: `[x, y, color, tone]`. */
195
+ type PartCell = readonly [x: number, y: number, color: PartColor, tone: PartTone];
196
+ declare const PART_LAYERS: readonly ["front", "back"];
197
+ type PartLayerName = (typeof PART_LAYERS)[number];
198
+ /** Most fixed colors one part may use. */
199
+ declare const MAX_PART_FIXED_COLORS = 16;
200
+ /**
201
+ * A Human-drawn Custom Part. Its content is immutable and its identity is `customPartId`;
202
+ * name, author, origin and parent are metadata kept outside it.
203
+ */
204
+ interface PixelCustomPart {
205
+ slot: PartSlot;
206
+ front: readonly PartCell[];
207
+ back: readonly PartCell[];
208
+ }
209
+ /** A dense layer for drawing: `layer[y][x]` is a color and tone, or `null` when empty. */
210
+ type PartInk = {
211
+ color: PartColor;
212
+ tone: PartTone;
213
+ };
214
+ type PartLayer = (PartInk | null)[][];
215
+ declare function isPixelCustomPart(value: unknown): value is PixelCustomPart;
216
+ /** The part with cells in row order and fixed colors in lowercase. */
217
+ declare function canonicalCustomPart(part: PixelCustomPart): PixelCustomPart;
218
+ /** Content identity: a SHA-256 of the canonical slot, cells and color references. */
219
+ declare function customPartId(part: PixelCustomPart): string;
220
+ declare function emptyPartLayer(slot: PartSlot): PartLayer;
221
+ declare function partLayer(slot: PartSlot, cells: readonly PartCell[]): PartLayer;
222
+ declare function partCells(layer: PartLayer): PartCell[];
223
+ /** Builds a canonical part from two drawn layers. */
224
+ declare function createCustomPart(slot: PartSlot, layers: Record<PartLayerName, PartLayer>): PixelCustomPart;
225
+ /** The column mirrored across the Avatar centerline. */
226
+ declare function mirrorPartX(slot: PartSlot, x: number): number;
227
+ /** Sets cells (pencil with ink, eraser with `null`), optionally mirrored across the centerline. */
228
+ declare function paintPartLayer(slot: PartSlot, layer: PartLayer, points: readonly (readonly [number, number])[], ink: PartInk | null, mirror?: boolean): PartLayer;
229
+ /**
230
+ * 4-connected flood fill from a cell: every cell reachable through cells equal to the start
231
+ * becomes `ink`. With `mirror`, the mirrored start is filled too.
232
+ */
233
+ declare function fillPartLayer(slot: PartSlot, layer: PartLayer, x: number, y: number, ink: PartInk | null, mirror?: boolean): PartLayer;
234
+ //#endregion
125
235
  //#region src/figure.d.ts
126
236
  type Cell = string | undefined;
127
237
  type Grid = Cell[][];
@@ -130,6 +240,8 @@ type PixelMouthState = 'saved' | 'closed' | 'half-open' | 'open';
130
240
  interface PixelFigureOptions {
131
241
  mouthLayers?: boolean;
132
242
  }
243
+ /** The color a Custom Part cell shows at a tone step on the rig's shade ramp. */
244
+ declare function partToneColor(color: string, tone: PartTone): string;
133
245
  type Recipe = PixelAvatarRecipe;
134
246
  declare function pixelTileColor(hair: string): string;
135
247
  declare function pixelFigure(recipe: Recipe, yawDeg: number, options?: PixelFigureOptions): {
@@ -138,6 +250,16 @@ declare function pixelFigure(recipe: Recipe, yawDeg: number, options?: PixelFigu
138
250
  head: string;
139
251
  cells: PixelCell$1[];
140
252
  };
253
+ /**
254
+ * The hair piece in `slot` as a Custom Part to start drawing from: the drawn part worn there,
255
+ * or the built-in piece flattened for this recipe in the front pose as live `hairColor` cells.
256
+ * A flattened piece keeps this shape and no longer follows face shape or hair length.
257
+ */
258
+ declare function hairPieceStart(recipe: PixelAvatarRecipe, slot: HairPartSlot): {
259
+ slot: HairPartSlot;
260
+ front: PartCell[];
261
+ back: PartCell[];
262
+ };
141
263
  //#endregion
142
264
  //#region src/random.d.ts
143
265
  declare function seededRandom(seed: string): () => number;
@@ -187,5 +309,5 @@ declare function pixelSymbolCells(symbol: PixelSymbol, color: string): PixelCell
187
309
  */
188
310
  declare function symbolArtCells(rows: readonly string[], color: string): PixelCell$1[];
189
311
  //#endregion
190
- 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, DEFAULT_RECIPE, PIXEL_SYMBOLS, type PixelAvatarRecipe, type PixelAvatarRecipeV1, type PixelAvatarRecipeV2, type PixelAvatarSvgOptions, type PixelCell, type PixelFigureOptions, type PixelGrid, type PixelMouthState, type PixelSymbol, canonicalRecipe, createSeededRecipe, createSeededRecipeV2, detailedRecipe, faceCells, hiddenChoices, isPixelAvatarRecipe, pixelAvatarSvg, pixelFigure, pixelSymbolCells, pixelTileColor, seededRandom, seededRecipe, seededRecipeV2, symbolArtCells, withSpecies };
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 };
191
313
  //# sourceMappingURL=index.d.ts.map