@illlustrations/avatars 1.0.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.
Files changed (45) hide show
  1. package/AI.md +84 -0
  2. package/ARTWORK.md +34 -0
  3. package/CHANGELOG.md +21 -0
  4. package/LICENSE +11 -0
  5. package/LICENSE-ARTWORK +9 -0
  6. package/LICENSE-CODE +21 -0
  7. package/README.md +170 -0
  8. package/dist/core/index.d.ts +4 -0
  9. package/dist/core/index.js +88 -0
  10. package/dist/core/types.d.ts +83 -0
  11. package/dist/core/types.js +2 -0
  12. package/dist/internal/render.d.ts +5 -0
  13. package/dist/internal/render.js +74 -0
  14. package/dist/internal/style.d.ts +7 -0
  15. package/dist/internal/style.js +71 -0
  16. package/dist/react/index.d.ts +8 -0
  17. package/dist/react/index.js +15 -0
  18. package/dist/styles/croods-presets.json +530 -0
  19. package/dist/styles/croods.d.ts +2 -0
  20. package/dist/styles/croods.js +2 -0
  21. package/dist/svg/croods-001.svg +68 -0
  22. package/dist/svg/croods-002.svg +52 -0
  23. package/dist/svg/croods-003.svg +61 -0
  24. package/dist/svg/croods-004.svg +37 -0
  25. package/dist/svg/croods-005.svg +77 -0
  26. package/dist/svg/croods-006.svg +52 -0
  27. package/dist/svg/croods-007.svg +64 -0
  28. package/dist/svg/croods-008.svg +57 -0
  29. package/dist/svg/croods-009.svg +44 -0
  30. package/dist/svg/croods-010.svg +68 -0
  31. package/dist/svg/croods-011.svg +72 -0
  32. package/dist/svg/croods-012.svg +51 -0
  33. package/dist/svg/croods-013.svg +73 -0
  34. package/dist/svg/croods-014.svg +34 -0
  35. package/dist/svg/croods-015.svg +70 -0
  36. package/dist/svg/croods-016.svg +58 -0
  37. package/dist/svg/croods-017.svg +72 -0
  38. package/dist/svg/croods-018.svg +68 -0
  39. package/dist/svg/croods-019.svg +38 -0
  40. package/dist/svg/croods-020.svg +66 -0
  41. package/dist/svg/croods-021.svg +64 -0
  42. package/dist/svg/croods-022.svg +63 -0
  43. package/dist/svg/croods-023.svg +58 -0
  44. package/dist/svg/croods-024.svg +38 -0
  45. package/package.json +94 -0
package/AI.md ADDED
@@ -0,0 +1,84 @@
1
+ # @illlustrations/avatars — notes for coding agents
2
+
3
+ Seeded SVG avatars for JavaScript and React. Everything renders locally: no
4
+ API key, network request or account. Croods is the first style.
5
+
6
+ Install: `npm install @illlustrations/avatars` (Node 20.19+; React 18.3+ or 19
7
+ for the component). Docs: https://illlustrations.co/docs
8
+
9
+ ## Imports
10
+
11
+ ```ts
12
+ import { createAvatar, fromJSON } from '@illlustrations/avatars'; // no React
13
+ import { Avatar } from '@illlustrations/avatars/react'; // React 18.3+ or 19
14
+ import { croods } from '@illlustrations/avatars/croods'; // the style
15
+ ```
16
+
17
+ There is no default export and no `/core`, `/styles/croods` or `/dist/...`
18
+ import path.
19
+
20
+ ## Use
21
+
22
+ ```tsx
23
+ // React. `assets` is required. Same seed = same avatar, on every device.
24
+ <Avatar assets={croods} seed={user.id} size={40} shape="circle" title={user.name} />
25
+
26
+ // Anywhere else: SVG string, data URI or saved state.
27
+ const avatar = createAvatar(croods, { seed: user.id, size: 128 });
28
+ avatar.toString(); // '<svg ...>'
29
+ avatar.toDataUri(); // for <img src> or CSS; add alt text yourself
30
+ avatar.toJSON(); // store this if the user customizes their avatar
31
+ fromJSON(croods, saved).toString();
32
+ ```
33
+
34
+ ## Rules
35
+
36
+ 1. Use only the part IDs below. Unknown IDs throw `Unknown <slot> part: <id>`.
37
+ `head` and `upperBody` must be real IDs; the others also accept `none`.
38
+ 2. Use a stable seed (user ID, not display name or `Math.random()`) so an
39
+ avatar does not change between renders or on the server and client.
40
+ 3. When users pick parts, save `toJSON()` and restore with `fromJSON`. Do not
41
+ store only the seed: explicit selections are not derivable from it.
42
+ 4. Pass `assets={croods}` to `<Avatar>`. `style` is the normal React CSS prop.
43
+ 5. Several inline SVG strings on one page need unique IDs:
44
+ `toString({ idPrefix: 'user-42' })`. The prefix starts with a letter and uses
45
+ only letters, digits, `_` and `-`: clean user IDs and add the list index, e.g.
46
+ `` `avatar-${i}-${id.replace(/[^A-Za-z0-9_-]/g, '')}` ``. Seed with the raw ID.
47
+ `<Avatar>` and `toDataUri()` handle IDs themselves.
48
+ 6. Colors: `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `transparent`, or
49
+ `original` for color roles. Named CSS colors throw. `size` must be above 0 and at most 8192.
50
+ 7. `<Avatar>` is a client component (`'use client'`). In a React Server
51
+ Component, render it as-is or use `createAvatar(...).toDataUri()` in an `<img>`.
52
+ 8. The artwork is CC BY 4.0: credit "Croods by illlustrations" once per app or
53
+ site, somewhere visible (footer, about or credits page). Individual avatars,
54
+ emails and exported SVGs need no caption.
55
+
56
+ ## Options
57
+
58
+ | Option | Values |
59
+ | --- | --- |
60
+ | `seed` | Any string. Omit for the default character. |
61
+ | `selections` | Partial `{ head, face, upperBody, facialHair, accessories }` |
62
+ | `colors` | Partial `{ hair, skin, clothing, stroke }` |
63
+ | `theme` | `'ink'` (blue and white) or `'neutral'` (black and white) |
64
+ | `background` | Hex or `'transparent'` (default) |
65
+ | `shape` | `'square'` (default), `'rounded'`, `'circle'` |
66
+ | `size` | Pixels. Core default 600, React default 64 |
67
+ | `seedPool` | `'v1'` (default). Only for pinning seed results across versions |
68
+
69
+ Resolution order: style defaults → seeded combination → explicit `selections`.
70
+ Explicit `colors` override `theme` colors.
71
+
72
+ ## Parts
73
+
74
+ <!-- parts:start (generated by scripts/sync-readme.mjs; do not edit) -->
75
+ | Slot | Required | Default | Valid IDs for `croods` |
76
+ | --- | --- | --- | --- |
77
+ | `head` | yes | `default` | `afro-1`, `afro-2`, `bald`, `bangs`, `bowl-cut`, `braid-1`, `braid-2`, `bun-1`, `bun-2`, `bun`, `default`, `dread`, `long-hair`, `long-hair-1`, `long-hair-2`, `messy-afro`, `messy`, `mohawk`, `no-hair`, `normal`, `pixie`, `quiff-1`, `quiff-2`, `shaggy`, `short-hair-1`, `short-hair-2`, `short-hair-3`, `short-hair-4`, `short-hair-5`, `short-hair-6`, `short-hair-7`, `short-hair-8`, `short-hair-9`, `straight-long`, `trimmed`, `wavy-curls` |
78
+ | `face` | no | `normal` | `drool`, `drool-2`, `happy`, `normal`, `open-mouth-1`, `sad`, `none` |
79
+ | `upperBody` | yes | `t-shirt-1` | `blazer`, `hoodie-1`, `shirt-1`, `t-shirt-1`, `t-shirt-bag` |
80
+ | `facialHair` | no | `none` | `beard`, `moustache`, `stubble`, `none` |
81
+ | `accessories` | no | `none` | `glass`, `none` |
82
+
83
+ Seeded avatars never pick facial hair; pass `selections.facialHair` to opt in.
84
+ <!-- parts:end -->
package/ARTWORK.md ADDED
@@ -0,0 +1,34 @@
1
+ # Croods v2 artwork snapshot
2
+
3
+ On September 16, 2026 the owner requested importing all Croods v2 parts marked
4
+ for the avatar layout. This repository now includes the 51 active variants
5
+ whose `layout_positions.avatar` has a valid positive size:
6
+
7
+ - 36 heads
8
+ - 6 faces
9
+ - 5 upper bodies
10
+ - 3 facial-hair parts
11
+ - 1 accessory
12
+
13
+ Seven lower-body parts have no avatar placement and were excluded. Originals
14
+ were downloaded read-only from the configured Supabase storage bucket, using
15
+ the catalog's references to resolve the corresponding private SVG. No database
16
+ or bucket records were changed.
17
+
18
+ `artwork/croods/catalog.json` records public aliases, display names, source file
19
+ hashes, per-part geometry, effective z-index and import time. It contains no
20
+ credentials, database UUIDs or storage URLs. `source/` holds the exact downloaded
21
+ SVG bytes for reproducible comparison. `manifest.json` contains the package
22
+ snapshot and frozen seed pool; `presets.json` contains 24 resolved portraits.
23
+
24
+ Normalization preserves source colors by default. Explicit role overrides
25
+ recolor selected source palette entries while retaining accents and coverage.
26
+ The npm file allowlist includes compiled artwork and preset SVGs, not raw
27
+ catalog/source files, import scripts or the unrelated test fixtures.
28
+
29
+ Croods v2 artwork by [illlustrations / Vijay Verma](https://illlustrations.co)
30
+ is licensed under [CC BY 4.0](LICENSE-ARTWORK). Code uses [MIT](LICENSE-CODE).
31
+ See [README.md](README.md#license) for attribution.
32
+
33
+ `private: true` and the release guard remain until the npm scope and public
34
+ release are reviewed.
package/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ First public release of `@illlustrations/avatars`.
6
+
7
+ - Entry points: `@illlustrations/avatars` (core, no React), `/react` (`<Avatar>`),
8
+ `/croods` (style), `/croods/presets.json` and `/svg/croods-001.svg`–`croods-024.svg`.
9
+ Works with `import` and `require` (Node 20.19+).
10
+ - Croods style 1.0.0: 51 parts (36 heads, 6 faces, 5 upper bodies, 3 facial hair,
11
+ 1 accessory) and 24 presets.
12
+ - Seeded avatars from a frozen `v1` pool of 256 combinations. Every seeded avatar
13
+ has a face; facial hair is opt-in through `selections.facialHair`.
14
+ - Colors per role (`hair`, `skin`, `clothing`, `stroke`), `ink` and `neutral`
15
+ themes, background, `square`/`rounded`/`circle` shapes.
16
+ - `toString`, `toDataUri`, `toJSON` and `fromJSON` with validated, versioned state.
17
+ - `AI.md` ships rules and every valid part ID for coding agents.
18
+ - Code: MIT. Croods artwork: CC BY 4.0.
19
+
20
+ Part IDs renamed from the unreleased preview: `t-shit-bag` → `t-shirt-bag`,
21
+ `pixe` → `pixie`, `drool2` → `drool-2`, facial hair `normal` → `stubble`.
package/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ Copyright (c) 2026 Vijay Verma / illlustrations
2
+
3
+ Code: MIT — see LICENSE-CODE.
4
+ Artwork: CC BY 4.0 — see LICENSE-ARTWORK.
5
+
6
+ The artwork license covers artwork/croods/, including SVG parts, manifests
7
+ and presets, plus the Croods artwork embedded in compiled styles, exported
8
+ SVGs and generated avatars. Code wrappers remain under MIT.
9
+
10
+ Third-party dependencies and other illlustrations collections retain their
11
+ own licenses.
@@ -0,0 +1,9 @@
1
+ Croods v2 artwork
2
+ Copyright (c) 2026 Vijay Verma / illlustrations
3
+ https://illlustrations.co
4
+
5
+ Licensed under Creative Commons Attribution 4.0 International (CC BY 4.0).
6
+ https://creativecommons.org/licenses/by/4.0/
7
+
8
+ Full legal terms: https://creativecommons.org/licenses/by/4.0/legalcode
9
+ See LICENSE for the scope of artwork and code licensing.
package/LICENSE-CODE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vijay Verma / illlustrations
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # @illlustrations/avatars
2
+
3
+ Composable, seeded SVG avatars for plain JavaScript and React. Croods is the
4
+ first style. Everything renders locally: no API key, account or network request.
5
+ Docs and a live playground: https://illlustrations.co/docs
6
+
7
+ ```sh
8
+ npm install @illlustrations/avatars
9
+ ```
10
+
11
+ React projects also need React 18.3.1 or 19. Node 20.19 or newer. The
12
+ Croods v2 collection includes all 51 active parts with a valid avatar
13
+ configuration in the source database: 36 heads, 6 faces, 5 upper bodies,
14
+ 3 facial-hair parts and 1 accessory.
15
+
16
+ | Import | Contents |
17
+ | --- | --- |
18
+ | `@illlustrations/avatars` | `createAvatar`, `fromJSON` and types. No React. |
19
+ | `@illlustrations/avatars/react` | The `<Avatar>` component. |
20
+ | `@illlustrations/avatars/croods` | The Croods style. Future styles get their own path. |
21
+ | `@illlustrations/avatars/croods/presets.json` | 24 resolved preset states. |
22
+ | `@illlustrations/avatars/svg/croods-001.svg` | Preset SVG files, `001` to `024`. |
23
+
24
+ Coding agents: see [AI.md](AI.md) for rules and valid part IDs.
25
+
26
+ ## Develop
27
+
28
+ ```sh
29
+ npm install
30
+ npm test
31
+ npm run verify:artwork
32
+ npm run check:package
33
+ npm run preview
34
+ ```
35
+
36
+ The playground includes every part, original/custom colors, shapes, 24 starter
37
+ portraits, SVG/JSON downloads and JSON restoration. The parts gallery displays
38
+ the saved size and offset. Everything renders locally; no account, API key,
39
+ Supabase connection or network request is required by the package at runtime.
40
+
41
+ To try an unreleased build in another project, run `npm pack` here and
42
+ install the resulting archive there.
43
+
44
+ ## React
45
+
46
+ ```tsx
47
+ import { Avatar } from '@illlustrations/avatars/react';
48
+ import { croods } from '@illlustrations/avatars/croods';
49
+
50
+ <Avatar assets={croods} seed="user-42" size={64} title="Sam's avatar" />
51
+ <Avatar assets={croods} shape="circle"
52
+ selections={{ head: 'straight-long', facialHair: 'beard', accessories: 'none' }}
53
+ colors={{ hair: '#143CFF', stroke: '#111111' }} />
54
+ ```
55
+
56
+ React accepts `title`, `className`, `style`, ARIA and SVG presentation props.
57
+ Without a title or accessible label the avatar is decorative. It uses `useId`
58
+ to isolate SVG definitions. Applications with multiple React roots must use
59
+ matching, unique React `identifierPrefix` settings on server and client.
60
+
61
+ ## Plain JavaScript
62
+
63
+ ```ts
64
+ import { createAvatar, fromJSON } from '@illlustrations/avatars';
65
+ import { croods } from '@illlustrations/avatars/croods';
66
+
67
+ const avatar = createAvatar(croods, { seed: 'user-42', background: '#EDEDFF' });
68
+ const svg = avatar.toString();
69
+ const imageUrl = avatar.toDataUri();
70
+ const saved = avatar.toJSON();
71
+ const restored = fromJSON(croods, saved);
72
+ ```
73
+
74
+ For HTML, assign `toDataUri()` to an image's `src` and supply `alt`. For multiple
75
+ inline SVG strings, use a unique `toString({ idPrefix: 'profile-42', title: 'Sam' })`
76
+ prefix for each. The root entry and `/croods` do not load React.
77
+
78
+ Individual SVG exports are available as
79
+ `@illlustrations/avatars/svg/croods-001.svg` through `croods-024.svg`. Use your
80
+ bundler's URL asset handling (for example a Vite `?url` import), or copy these
81
+ files into your public assets directory. An npm specifier is not a browser URL.
82
+ Resolved preset states are in `/croods/presets.json`.
83
+
84
+ ## Parts
85
+
86
+ Unknown IDs throw `Unknown <slot> part: <id>`. Use only these:
87
+
88
+ <!-- parts:start (generated by scripts/sync-readme.mjs; do not edit) -->
89
+ | Slot | Required | Default | Valid IDs for `croods` |
90
+ | --- | --- | --- | --- |
91
+ | `head` | yes | `default` | `afro-1`, `afro-2`, `bald`, `bangs`, `bowl-cut`, `braid-1`, `braid-2`, `bun-1`, `bun-2`, `bun`, `default`, `dread`, `long-hair`, `long-hair-1`, `long-hair-2`, `messy-afro`, `messy`, `mohawk`, `no-hair`, `normal`, `pixie`, `quiff-1`, `quiff-2`, `shaggy`, `short-hair-1`, `short-hair-2`, `short-hair-3`, `short-hair-4`, `short-hair-5`, `short-hair-6`, `short-hair-7`, `short-hair-8`, `short-hair-9`, `straight-long`, `trimmed`, `wavy-curls` |
92
+ | `face` | no | `normal` | `drool`, `drool-2`, `happy`, `normal`, `open-mouth-1`, `sad`, `none` |
93
+ | `upperBody` | yes | `t-shirt-1` | `blazer`, `hoodie-1`, `shirt-1`, `t-shirt-1`, `t-shirt-bag` |
94
+ | `facialHair` | no | `none` | `beard`, `moustache`, `stubble`, `none` |
95
+ | `accessories` | no | `none` | `glass`, `none` |
96
+
97
+ Seeded avatars never pick facial hair; pass `selections.facialHair` to opt in.
98
+ <!-- parts:end -->
99
+
100
+ ## Options
101
+
102
+ | Option | Behavior |
103
+ | --- | --- |
104
+ | `seed` | String, including empty string. Omit for the default character. |
105
+ | `seedPool` | Versioned selection pool, initially `v1`. |
106
+ | `selections` | Partial `head`, `face`, `upperBody`, `facialHair`, `accessories` overrides. Face, facial hair and accessories support `none`. |
107
+ | `colors` | Partial `hair`, `skin`, `clothing`, `stroke` overrides. Croods defaults to `original` for each role. |
108
+ | `background` | Transparent by default. |
109
+ | `size` | Positive number up to 8192; core defaults to 600, React to 64. |
110
+ | `shape` | `square` (default), `rounded`, or `circle`. |
111
+ | `theme` | Omit for original artwork. `ink` uses `#0040FC` and white; `neutral` uses black and white. Explicit `colors` override theme roles. |
112
+
113
+ Colors accept 3/4/6/8-digit hex, `transparent`, or `original` for color roles.
114
+ Themes also recolor fixed accents, while preserving mask coverage and keeping
115
+ the background independent. Use `<Avatar assets={croods} theme="ink" />` or
116
+ `createAvatar(croods, { theme: 'neutral' })`. Themes are included in saved state.
117
+ `original` preserves each element's source paint, including differently colored
118
+ regions within one role. Background accepts hex or `transparent`. Recoloring
119
+ keeps fixed accent colors, mask coverage and clip paths. The explicit source
120
+ palette mapping is in `scripts/croods-colors.mjs`.
121
+
122
+ Resolution is defaults → seeded combination → explicit selections. Invalid
123
+ values, unknown parts/roles/slots and declared incompatible combinations throw.
124
+ Inline React rendering uses `--avatar-hair`, `--avatar-skin`,
125
+ `--avatar-clothing`, `--avatar-stroke`. Portable exports resolve paints to
126
+ literals. CSS overrides are presentation-only; use `colors` for a palette that
127
+ also appears in saved state and downloads.
128
+
129
+ ## Stable state and geometry
130
+
131
+ Every part carries its exact database avatar rectangle and effective layer
132
+ order. The renderer scales the native SVG viewport to that rectangle. This
133
+ preserves each source viewBox, root paint and clipping, including tall hair.
134
+ Geometry and artwork are bundled, versioned snapshots. Database edits do not
135
+ change existing installations; re-import intentionally and update the style
136
+ version when changing rendered output.
137
+
138
+ The v1 algorithm is unsigned 32-bit FNV-1a over JavaScript UTF-16 code units
139
+ (`Math.imul(hash ^ codeUnit, 16777619)`, starting at `2166136261`), modulo the
140
+ frozen ordered pool length. Croods v1 contains 256 explicit tuples covering
141
+ all imported parts except facial hair, which is always `none` in seeded
142
+ avatars; pass `selections.facialHair` to opt in. Manual selections support
143
+ combinations outside that pool.
144
+ The pool is not a claim that every possible combination has been visually
145
+ reviewed. Never reorder/extend a published pool; add an opt-in pool instead.
146
+ Seeds can collide. Preset states are also committed explicitly.
147
+
148
+ JSON stores schema version 1, exact style ID/version, resolved selections,
149
+ colors, background, size and shape. `fromJSON` validates this data and rejects
150
+ mismatched versions. Runtime APIs accept compiled styles, never arbitrary SVG.
151
+
152
+ ## Verification and release
153
+
154
+ `npm run verify:artwork` checks source hashes and database rectangles, then
155
+ compares all 51 parts in starter compositions and 24 presets against untouched
156
+ source SVGs at 32, 64, 128 and 600 px. It generates contact sheets and a report
157
+ in `.preview/`. `npm run check:package` checks actual installed core/style/React
158
+ imports, SVG exports, TypeScript declarations, packed files and ESM/SVG sizes.
159
+
160
+ See [ARTWORK.md](ARTWORK.md) for provenance and [docs/curation.md](docs/curation.md)
161
+ for re-import instructions. See [CHANGELOG.md](CHANGELOG.md) for releases.
162
+
163
+ ## License
164
+
165
+ Code is licensed under [MIT](LICENSE-CODE). Croods v2 artwork, including parts,
166
+ presets and generated avatars, is licensed under [CC BY 4.0](LICENSE-ARTWORK).
167
+
168
+ Credit: Croods v2 by [illlustrations / Vijay Verma](https://illlustrations.co),
169
+ © 2026, [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
170
+ Retain required notices and indicate changes when sharing modified artwork.
@@ -0,0 +1,4 @@
1
+ import type { AvatarOptions, AvatarResult, AvatarStyle } from './types.js';
2
+ export type { AvatarOptions, AvatarResult, AvatarState, AvatarStyle, ColorRole, Colors, RenderOptions, Selections, Shape, Slot, Theme } from './types.js';
3
+ export declare function createAvatar(style: AvatarStyle, options?: AvatarOptions): AvatarResult;
4
+ export declare function fromJSON(style: AvatarStyle, value: unknown): AvatarResult;
@@ -0,0 +1,88 @@
1
+ import { renderDocument } from '../internal/render.js';
2
+ import { assertStyle, isColor, isRoleColor, validateSelections } from '../internal/style.js';
3
+ import { colorRoles, slots } from './types.js';
4
+ const own = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
5
+ function record(value, label) {
6
+ if (!value || typeof value !== 'object' || Array.isArray(value))
7
+ throw new Error(`Invalid ${label}`);
8
+ }
9
+ function keys(value, allowed, label) {
10
+ for (const key of Object.keys(value))
11
+ if (!allowed.includes(key))
12
+ throw new Error(`Unknown ${label}: ${key}`);
13
+ }
14
+ /** FNV-1a over UTF-16 code units, unsigned 32-bit. This algorithm is frozen for v1. */
15
+ function hash(seed) {
16
+ let value = 2166136261;
17
+ for (let i = 0; i < seed.length; i++)
18
+ value = Math.imul(value ^ seed.charCodeAt(i), 16777619);
19
+ return value >>> 0;
20
+ }
21
+ function validateState(style, state) {
22
+ record(state, 'avatar state');
23
+ keys(state, ['schemaVersion', 'style', 'styleVersion', 'selections', 'colors', 'background', 'size', 'shape', 'theme'], 'state field');
24
+ if (state.schemaVersion !== 1 || state.style !== style.id || state.styleVersion !== style.version)
25
+ throw new Error('Avatar state schema or style version does not match');
26
+ if (state.theme !== undefined && !['ink', 'neutral'].includes(state.theme))
27
+ throw new Error('Unknown avatar theme');
28
+ record(state.selections, 'selections');
29
+ keys(state.selections, slots, 'slot');
30
+ validateSelections(style, state.selections);
31
+ record(state.colors, 'colors');
32
+ keys(state.colors, colorRoles, 'color role');
33
+ for (const role of colorRoles)
34
+ if (!isRoleColor(state.colors[role]))
35
+ throw new Error(`Invalid ${role} color: use a hex color, transparent or original`);
36
+ if (!isColor(state.background))
37
+ throw new Error('Invalid background: use a hex color or transparent');
38
+ if (!Number.isFinite(state.size) || state.size <= 0 || state.size > 8192)
39
+ throw new Error('Size must be greater than 0 and at most 8192');
40
+ if (!['square', 'rounded', 'circle'].includes(state.shape))
41
+ throw new Error('Unknown avatar shape');
42
+ }
43
+ export function createAvatar(style, options = {}) {
44
+ assertStyle(style);
45
+ record(options, 'options');
46
+ keys(options, ['seed', 'seedPool', 'selections', 'colors', 'background', 'size', 'shape', 'theme'], 'option');
47
+ let selections = style.defaults.selections;
48
+ if (options.seed !== undefined && typeof options.seed !== 'string')
49
+ throw new Error('Seed must be a string');
50
+ const poolId = options.seedPool ?? style.defaultSeedPool;
51
+ if (typeof poolId !== 'string' || !own(style.seedPools, poolId))
52
+ throw new Error(`Unknown seed pool: ${poolId}`);
53
+ if (options.seed !== undefined) {
54
+ const pool = style.seedPools[poolId];
55
+ selections = pool[hash(options.seed) % pool.length];
56
+ }
57
+ if (options.selections !== undefined) {
58
+ record(options.selections, 'selections');
59
+ keys(options.selections, slots, 'slot');
60
+ }
61
+ if (options.colors !== undefined) {
62
+ record(options.colors, 'colors');
63
+ keys(options.colors, colorRoles, 'color role');
64
+ }
65
+ const state = {
66
+ schemaVersion: 1, style: style.id, styleVersion: style.version,
67
+ selections: { ...selections, ...options.selections },
68
+ colors: { ...style.defaults.colors, ...(options.theme ? { hair: options.theme === 'ink' ? '#0040FC' : '#000000', stroke: options.theme === 'ink' ? '#0040FC' : '#000000', skin: '#FFFFFF', clothing: '#FFFFFF' } : {}), ...options.colors },
69
+ ...(options.theme === undefined ? {} : { theme: options.theme }),
70
+ background: options.background ?? 'transparent', size: options.size ?? 600, shape: options.shape ?? 'square',
71
+ };
72
+ validateState(style, state);
73
+ return result(style, state);
74
+ }
75
+ export function fromJSON(style, value) {
76
+ assertStyle(style);
77
+ record(value, 'avatar state');
78
+ const state = JSON.parse(JSON.stringify(value));
79
+ validateState(style, state);
80
+ return result(style, state);
81
+ }
82
+ function result(style, state) {
83
+ return Object.freeze({
84
+ toJSON: () => JSON.parse(JSON.stringify(state)),
85
+ toString: (options = {}) => renderDocument(style, state, options),
86
+ toDataUri: (options = {}) => `data:image/svg+xml,${encodeURIComponent(renderDocument(style, state, options))}`,
87
+ });
88
+ }
@@ -0,0 +1,83 @@
1
+ export declare const slots: readonly ["upperBody", "head", "face", "facialHair", "accessories"];
2
+ export type Slot = (typeof slots)[number];
3
+ export declare const colorRoles: readonly ["hair", "skin", "clothing", "stroke"];
4
+ export type ColorRole = (typeof colorRoles)[number];
5
+ export type Colors = Record<ColorRole, string>;
6
+ export type Selections = Record<Slot, string>;
7
+ export type Theme = 'ink' | 'neutral';
8
+ export type Shape = 'square' | 'rounded' | 'circle';
9
+ export interface AvatarOptions {
10
+ theme?: Theme;
11
+ seed?: string;
12
+ seedPool?: string;
13
+ selections?: Partial<Selections>;
14
+ colors?: Partial<Colors>;
15
+ background?: string;
16
+ size?: number;
17
+ shape?: Shape;
18
+ }
19
+ export interface AvatarState {
20
+ theme?: Theme;
21
+ schemaVersion: 1;
22
+ style: string;
23
+ styleVersion: string;
24
+ selections: Selections;
25
+ colors: Colors;
26
+ background: string;
27
+ size: number;
28
+ shape: Shape;
29
+ }
30
+ export interface RenderOptions {
31
+ /** Unique on the containing page. Standalone image documents need no prefix. */
32
+ idPrefix?: string;
33
+ title?: string;
34
+ }
35
+ export interface AvatarResult {
36
+ toString(options?: RenderOptions): string;
37
+ toDataUri(options?: Omit<RenderOptions, 'idPrefix'>): string;
38
+ toJSON(): AvatarState;
39
+ }
40
+ export interface Part {
41
+ id: string;
42
+ name?: string;
43
+ /** Sanitized at build time. Never pass user-supplied SVG here. */
44
+ svg: string;
45
+ /** Snapshot of the builder's effective avatar layer order. */
46
+ zIndex: number;
47
+ /** Exact per-variant avatar placement from the builder database. */
48
+ position: {
49
+ x: number;
50
+ y: number;
51
+ width: number;
52
+ height: number;
53
+ };
54
+ }
55
+ export interface StyleDefinition {
56
+ id: string;
57
+ version: string;
58
+ parts: Record<Slot, readonly Part[]>;
59
+ defaults: {
60
+ selections: Selections;
61
+ colors: Colors;
62
+ };
63
+ /** Ordered, frozen compatible tuples. Adding parts must not alter an existing pool. */
64
+ seedPools: Record<string, readonly Selections[]>;
65
+ defaultSeedPool: string;
66
+ /** Each pair declares a combination that must never be rendered. */
67
+ incompatible?: readonly {
68
+ first: {
69
+ slot: Slot;
70
+ id: string;
71
+ };
72
+ second: {
73
+ slot: Slot;
74
+ id: string;
75
+ };
76
+ }[];
77
+ }
78
+ declare const compiledStyle: unique symbol;
79
+ /** Only use style objects shipped by the package's curated style entry points. */
80
+ export interface AvatarStyle extends Readonly<StyleDefinition> {
81
+ readonly [compiledStyle]: true;
82
+ }
83
+ export {};
@@ -0,0 +1,2 @@
1
+ export const slots = ['upperBody', 'head', 'face', 'facialHair', 'accessories'];
2
+ export const colorRoles = ['hair', 'skin', 'clothing', 'stroke'];
@@ -0,0 +1,5 @@
1
+ import type { AvatarStyle, AvatarState, RenderOptions } from '../core/types.js';
2
+ export declare const escapeXml: (value: string) => string;
3
+ /** Internal React renderer; the package export map only exposes the supported public API. */
4
+ export declare function renderContents(style: AvatarStyle, state: AvatarState, idPrefix: string, cssVariables?: boolean): string;
5
+ export declare function renderDocument(style: AvatarStyle, state: AvatarState, options: RenderOptions): string;
@@ -0,0 +1,74 @@
1
+ import { slots } from '../core/types.js';
2
+ export const escapeXml = (value) => value.replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' })[char]);
3
+ function record(value, label) {
4
+ if (!value || typeof value !== 'object' || Array.isArray(value))
5
+ throw new Error(`Invalid ${label}`);
6
+ }
7
+ function keys(value, allowed, label) {
8
+ for (const key of Object.keys(value))
9
+ if (!allowed.includes(key))
10
+ throw new Error(`Unknown ${label}: ${key}`);
11
+ }
12
+ function prefix(value) {
13
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(value))
14
+ throw new Error('idPrefix must start with a letter and contain only letters, digits, underscores or hyphens');
15
+ return value;
16
+ }
17
+ /** Recolor fixed accents while preserving mask/clip coverage and role variables. */
18
+ function themeAccents(svg, ink) {
19
+ let coverage = 0;
20
+ return svg.replace(/<[^>]+>/g, tag => {
21
+ if (/^<\/(?:mask|clipPath)\b/.test(tag)) {
22
+ coverage--;
23
+ return tag;
24
+ }
25
+ const protectedTag = /^<(?:mask|clipPath)\b/.test(tag);
26
+ if (protectedTag && !/\/>$/.test(tag))
27
+ coverage++;
28
+ if (coverage || protectedTag)
29
+ return tag;
30
+ return tag.replace(/(\s(?:fill|stroke|stop-color)=")([^"<>]*)(")/g, (match, start, paint, end) => {
31
+ if (/^(?:none|transparent|var\(|url\()/i.test(paint))
32
+ return match;
33
+ const canonical = paint.toUpperCase();
34
+ // Dark neutral accents are linework; other fixed accents become paper.
35
+ const dark = canonical === 'BLACK' || /^#(?:000|000000|424242)$/.test(canonical);
36
+ if (!/^(?:#[0-9A-F]{3,8}|BLACK|WHITE)$/.test(canonical))
37
+ return match;
38
+ return `${start}${dark ? ink : '#FFFFFF'}${end}`;
39
+ });
40
+ });
41
+ }
42
+ /** Internal React renderer; the package export map only exposes the supported public API. */
43
+ export function renderContents(style, state, idPrefix, cssVariables = false) {
44
+ const id = prefix(idPrefix);
45
+ const clip = state.shape === 'circle' ? '<circle cx="300" cy="300" r="300"/>'
46
+ : `<rect width="600" height="600" rx="${state.shape === 'rounded' ? 96 : 0}"/>`;
47
+ const layers = slots.flatMap(slot => {
48
+ const part = style.parts[slot].find(item => item.id === state.selections[slot]);
49
+ return part ? [{ slot, part }] : [];
50
+ }).sort((a, b) => a.part.zIndex - b.part.zIndex).map(({ slot, part }) => {
51
+ const source = state.theme ? themeAccents(part.svg, state.theme === 'ink' ? '#0040FC' : '#000000') : part.svg;
52
+ const svg = source.replaceAll('__AVATAR_ID__', `${id}-${slot}-`).replace(/var\(--avatar-(hair|skin|clothing|stroke)(?:,\s*([^)]*))?\)/g, (_, role, original) => {
53
+ const color = state.colors[role] === 'original' ? (original ?? '#000000') : state.colors[role];
54
+ return cssVariables ? `var(--avatar-${role}, ${color})` : color;
55
+ });
56
+ const p = part.position;
57
+ // Match the website export: scale the native viewport to the DB rectangle.
58
+ // Resizing the SVG viewport itself can introduce preserveAspectRatio letterboxing.
59
+ const root = svg.match(/^<svg\b([^>]*)>/)[1];
60
+ const width = Number(root.match(/\swidth="([\d.]+)"/)[1]);
61
+ const height = Number(root.match(/\sheight="([\d.]+)"/)[1]);
62
+ return `<g transform="translate(${p.x} ${p.y}) scale(${p.width / width} ${p.height / height})">${svg}</g>`;
63
+ }).join('');
64
+ return `<defs><clipPath id="${id}-clip">${clip}</clipPath></defs><g clip-path="url(#${id}-clip)"><rect width="600" height="600" fill="${state.background}"/>${layers}</g>`;
65
+ }
66
+ export function renderDocument(style, state, options) {
67
+ record(options, 'render options');
68
+ keys(options, ['idPrefix', 'title'], 'render option');
69
+ if (options.title !== undefined && typeof options.title !== 'string')
70
+ throw new Error('Title must be a string');
71
+ const id = prefix(options.idPrefix ?? 'avatar');
72
+ const title = options.title ? `<title id="${id}-title">${escapeXml(options.title)}</title>` : '';
73
+ return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 600 600" width="${state.size}" height="${state.size}" ${title ? `role="img" aria-labelledby="${id}-title"` : 'aria-hidden="true"'}>${title}${renderContents(style, state, id)}</svg>`;
74
+ }
@@ -0,0 +1,7 @@
1
+ import type { AvatarStyle, Selections, StyleDefinition } from '../core/types.js';
2
+ export declare const isColor: (value: unknown) => value is string;
3
+ export declare const isRoleColor: (value: unknown) => value is string;
4
+ export declare function validateSelections(style: StyleDefinition, selections: Selections): void;
5
+ /** Internal build output loader. Not a public arbitrary-SVG authoring API. */
6
+ export declare function defineStyle(definition: StyleDefinition): AvatarStyle;
7
+ export declare function assertStyle(style: AvatarStyle): void;