@usenavii/core 0.1.0 → 0.2.1
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 +117 -0
- package/dist/index.cjs +65 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +132 -5
- package/dist/index.d.ts +132 -5
- package/dist/index.js +63 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @usenavii/core
|
|
2
|
+
|
|
3
|
+
**Deterministic mascot avatars from a seed.** Pure TypeScript engine. Same seed in → byte-identical SVG out, every time. No state, no uploads, no network.
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="https://navii-api.uxderrick.com/group?seeds=aria,milo,nova,kai,sage,eden,luna,rio,pip,wren,zane,iris&size=72&overlap=0.32&ring=%230a0a0b&tileBg=%23ffffff" alt="Navii cast" />
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
- [Live demo](https://navii.uxderrick.com) — interactive playground + cast
|
|
10
|
+
- [Docs](https://navii.uxderrick.com/docs)
|
|
11
|
+
- [GitHub](https://github.com/uxderrick/navii)
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm add @usenavii/core
|
|
17
|
+
pnpm add @usenavii/core
|
|
18
|
+
yarn add @usenavii/core
|
|
19
|
+
bun add @usenavii/core
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { createAvatar } from '@usenavii/core';
|
|
26
|
+
|
|
27
|
+
const svg = createAvatar(user.id, { size: 96 });
|
|
28
|
+
document.body.insertAdjacentHTML('beforeend', svg);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or use the namespace bundle:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { Navii } from '@usenavii/core';
|
|
35
|
+
|
|
36
|
+
Navii.create(user.id);
|
|
37
|
+
Navii.seed({ id: user.id, email: user.email, name: user.name });
|
|
38
|
+
Navii.build({ body: 'tall', eyes: 'star', palette: 'violet' });
|
|
39
|
+
Navii.group([user1.id, user2.id, user3.id]);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## The seed: read this once
|
|
43
|
+
|
|
44
|
+
Same seed always produces the same avatar — that's the contract.
|
|
45
|
+
|
|
46
|
+
| Pass | Result |
|
|
47
|
+
| -------------------------- | ------------------------------------------------------- |
|
|
48
|
+
| `user.id` / UUID | ✅ Best. Stable and globally unique. |
|
|
49
|
+
| `user.email` | ✅ Good. Stable, unique per user. |
|
|
50
|
+
| `user.name` alone | ⚠️ Names collide. Two "Alice"s get the same avatar. |
|
|
51
|
+
| `${name}-${createdAt}` | ✅ Fine fallback if no ID exists. Bake at signup. |
|
|
52
|
+
| `Date.now()` at render | ❌ **Don't.** Breaks determinism — changes on reload. |
|
|
53
|
+
|
|
54
|
+
If your shape is uncertain, use the helper:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const s = Navii.seed({ id: user.id, email: user.email, name: user.name, createdAt: user.createdAt });
|
|
58
|
+
const svg = Navii.create(s);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
It picks the most unique field automatically: `id` → `email` → `name + createdAt` → `name`.
|
|
62
|
+
|
|
63
|
+
## API
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
createAvatar(seed: string, options?: AvatarOptions): string
|
|
67
|
+
selectAvatar(seed: string, options?: AvatarOptions): AvatarSpec
|
|
68
|
+
renderAvatar(spec: AvatarSpec, options?: AvatarOptions): string
|
|
69
|
+
renderGroup(seeds: string[], options?: GroupOptions): string
|
|
70
|
+
|
|
71
|
+
seed(fields: SeedFields): string // pick most-unique field
|
|
72
|
+
build(spec?: BuildSpec, opts?): string // manual mix-and-match (no seed)
|
|
73
|
+
|
|
74
|
+
Navii.{ create, render, select, group, seed, build }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `AvatarOptions`
|
|
78
|
+
|
|
79
|
+
| Option | Type | Default |
|
|
80
|
+
| ------------ | ----------------------------------------------------- | ------------ |
|
|
81
|
+
| `size` | `number` (px) | `96` |
|
|
82
|
+
| `paletteId` | known palette id (e.g. `'mint'`) | seed-derived |
|
|
83
|
+
| `background` | `'none' \| 'solid' \| 'ring'` or `{ color }` | seed-derived |
|
|
84
|
+
| `tileBg` | CSS color or `'auto'` (palette accent) | none |
|
|
85
|
+
| `title` | accessible label (sets `<title>` + `aria-label`) | none |
|
|
86
|
+
| `animated` | `boolean` — idle float / blink / sway / pulse / twinkle | `false` |
|
|
87
|
+
|
|
88
|
+
### `build()` — direct construction without a seed
|
|
89
|
+
|
|
90
|
+
Use for brand mascots, logo marks, designer-curated avatars:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const svg = Navii.build({
|
|
94
|
+
body: 'tall', eyes: 'star', mouth: 'grin',
|
|
95
|
+
palette: 'violet', topper: 'crown',
|
|
96
|
+
}, { size: 192, animated: true });
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Any field omitted falls back to the first variant.
|
|
100
|
+
|
|
101
|
+
## Determinism guarantee
|
|
102
|
+
|
|
103
|
+
`createAvatar(seed)` is a pure function. Same seed + same options → byte-identical SVG.
|
|
104
|
+
|
|
105
|
+
- PRNG: `sfc32` seeded from a `cyrb53` hash of the seed string.
|
|
106
|
+
- Part picks happen in a fixed order. New parts are appended to the end of the stream, so adding variants in future releases never shifts existing seeds.
|
|
107
|
+
- No `Date.now()`, no `Math.random()`, no module-level state, no environment lookups.
|
|
108
|
+
|
|
109
|
+
Render the same avatar in Node, in the browser, on the edge — all byte-identical.
|
|
110
|
+
|
|
111
|
+
## Cast (output space)
|
|
112
|
+
|
|
113
|
+
22 palettes × 8 bodies × 10 eyes × 10 mouths × 5 antennae × 7 accessories × 3 backgrounds × 12 toppers = **22,176,000** discrete combinations. Plus continuous tweaks (hue rotation ±30°, body scale ±8%, eye gap ±2, mouth curvature ±15%, antenna tilt ±8°) → effectively unbounded.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT. See [LICENSE](https://github.com/uxderrick/navii/blob/main/LICENSE).
|
package/dist/index.cjs
CHANGED
|
@@ -15,9 +15,9 @@ function cyrb53(input, salt = 0) {
|
|
|
15
15
|
h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
16
16
|
return [h1 >>> 0, h2 >>> 0];
|
|
17
17
|
}
|
|
18
|
-
function createRng(
|
|
19
|
-
const [a, b] = cyrb53(
|
|
20
|
-
const [c, d] = cyrb53(
|
|
18
|
+
function createRng(seed2) {
|
|
19
|
+
const [a, b] = cyrb53(seed2, 0);
|
|
20
|
+
const [c, d] = cyrb53(seed2, 1);
|
|
21
21
|
let s0 = a >>> 0;
|
|
22
22
|
let s1 = b >>> 0;
|
|
23
23
|
let s2 = c >>> 0;
|
|
@@ -623,8 +623,8 @@ var TOPPER_IDS = [
|
|
|
623
623
|
];
|
|
624
624
|
|
|
625
625
|
// src/select.ts
|
|
626
|
-
function selectAvatar(
|
|
627
|
-
const rng = createRng(
|
|
626
|
+
function selectAvatar(seed2, options = {}) {
|
|
627
|
+
const rng = createRng(seed2);
|
|
628
628
|
const paletteOverride = options.paletteId ? PALETTE_BY_ID[options.paletteId] : void 0;
|
|
629
629
|
const palette = paletteOverride ?? rng.pick(PALETTES);
|
|
630
630
|
const body = rng.pick(BODY_IDS);
|
|
@@ -648,7 +648,7 @@ function selectAvatar(seed, options = {}) {
|
|
|
648
648
|
const mouthCurveScale = Number(rng.range(0.85, 1.15).toFixed(3));
|
|
649
649
|
const antennaTilt = Math.round(rng.range(-8, 8));
|
|
650
650
|
return {
|
|
651
|
-
seed,
|
|
651
|
+
seed: seed2,
|
|
652
652
|
palette,
|
|
653
653
|
body,
|
|
654
654
|
eyes,
|
|
@@ -777,9 +777,9 @@ function wrap(cls, inner) {
|
|
|
777
777
|
if (!inner) return inner;
|
|
778
778
|
return `<g class="${cls}">${inner}</g>`;
|
|
779
779
|
}
|
|
780
|
-
function stableId(
|
|
780
|
+
function stableId(seed2) {
|
|
781
781
|
let h = 5381;
|
|
782
|
-
for (let i = 0; i <
|
|
782
|
+
for (let i = 0; i < seed2.length; i++) h = (h << 5) + h + seed2.charCodeAt(i) | 0;
|
|
783
783
|
return (h >>> 0).toString(36);
|
|
784
784
|
}
|
|
785
785
|
function escapeXml(s) {
|
|
@@ -803,9 +803,9 @@ function renderGroup(seeds, options = {}) {
|
|
|
803
803
|
const tileCount = visibleSeeds.length + (overflow > 0 ? 1 : 0);
|
|
804
804
|
const step = size * (1 - overlap);
|
|
805
805
|
const totalWidth = tileCount > 0 ? step * (tileCount - 1) + size : 0;
|
|
806
|
-
const tiles = visibleSeeds.map((
|
|
806
|
+
const tiles = visibleSeeds.map((seed2, i) => {
|
|
807
807
|
const x = i * step;
|
|
808
|
-
const spec = selectAvatar(
|
|
808
|
+
const spec = selectAvatar(seed2, options);
|
|
809
809
|
const bgCircle = tileBg !== "transparent" ? `<circle cx="50" cy="50" r="50" fill="${tileBg}" />` : "";
|
|
810
810
|
return `<svg x="${x}" y="0" width="${size}" height="${size}" viewBox="0 0 100 100" overflow="visible">
|
|
811
811
|
<defs><clipPath id="navii-clip"><circle cx="50" cy="50" r="50" /></clipPath></defs>
|
|
@@ -827,20 +827,72 @@ function clamp(n, lo, hi) {
|
|
|
827
827
|
return Math.max(lo, Math.min(hi, n));
|
|
828
828
|
}
|
|
829
829
|
|
|
830
|
+
// src/seed.ts
|
|
831
|
+
function seed(fields) {
|
|
832
|
+
if (fields.id !== null && fields.id !== void 0 && String(fields.id).length > 0) {
|
|
833
|
+
return String(fields.id);
|
|
834
|
+
}
|
|
835
|
+
if (fields.email && fields.email.length > 0) {
|
|
836
|
+
return fields.email;
|
|
837
|
+
}
|
|
838
|
+
if (fields.name && fields.name.length > 0) {
|
|
839
|
+
if (fields.createdAt !== null && fields.createdAt !== void 0) {
|
|
840
|
+
const ts = fields.createdAt instanceof Date ? fields.createdAt.getTime() : typeof fields.createdAt === "number" ? fields.createdAt : Date.parse(fields.createdAt);
|
|
841
|
+
if (Number.isFinite(ts)) return `${fields.name}|${ts}`;
|
|
842
|
+
return `${fields.name}|${fields.createdAt}`;
|
|
843
|
+
}
|
|
844
|
+
return fields.name;
|
|
845
|
+
}
|
|
846
|
+
throw new Error("navii: seed() requires at least one of { id, email, name }");
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/build.ts
|
|
850
|
+
function build(spec = {}, options = {}) {
|
|
851
|
+
const palette = spec.palette ? PALETTE_BY_ID[spec.palette] ?? PALETTES[0] : PALETTES[0];
|
|
852
|
+
const resolved = {
|
|
853
|
+
seed: "__build__",
|
|
854
|
+
palette,
|
|
855
|
+
body: spec.body ?? "orb",
|
|
856
|
+
eyes: spec.eyes ?? "round",
|
|
857
|
+
mouth: spec.mouth ?? "smile",
|
|
858
|
+
antenna: spec.antenna ?? "none",
|
|
859
|
+
accessory: spec.accessory ?? "none",
|
|
860
|
+
background: spec.background ?? "none",
|
|
861
|
+
topper: spec.topper ?? "none",
|
|
862
|
+
hueShift: spec.hueShift ?? 0,
|
|
863
|
+
bodyScale: spec.bodyScale ?? 1,
|
|
864
|
+
eyeGapShift: spec.eyeGapShift ?? 0,
|
|
865
|
+
mouthCurveScale: spec.mouthCurveScale ?? 1,
|
|
866
|
+
antennaTilt: spec.antennaTilt ?? 0
|
|
867
|
+
};
|
|
868
|
+
return renderAvatar(resolved, options);
|
|
869
|
+
}
|
|
870
|
+
|
|
830
871
|
// src/index.ts
|
|
831
|
-
function createAvatar(
|
|
832
|
-
if (typeof
|
|
872
|
+
function createAvatar(seed2, options = {}) {
|
|
873
|
+
if (typeof seed2 !== "string" || seed2.length === 0) {
|
|
833
874
|
throw new Error("navii: seed must be a non-empty string");
|
|
834
875
|
}
|
|
835
|
-
return renderAvatar(selectAvatar(
|
|
876
|
+
return renderAvatar(selectAvatar(seed2, options), options);
|
|
836
877
|
}
|
|
878
|
+
var Navii = {
|
|
879
|
+
create: createAvatar,
|
|
880
|
+
render: renderAvatar,
|
|
881
|
+
select: selectAvatar,
|
|
882
|
+
group: renderGroup,
|
|
883
|
+
seed,
|
|
884
|
+
build
|
|
885
|
+
};
|
|
837
886
|
|
|
887
|
+
exports.Navii = Navii;
|
|
888
|
+
exports.build = build;
|
|
838
889
|
exports.createAvatar = createAvatar;
|
|
839
890
|
exports.createRng = createRng;
|
|
840
891
|
exports.cyrb53 = cyrb53;
|
|
841
892
|
exports.renderAvatar = renderAvatar;
|
|
842
893
|
exports.renderAvatarInner = renderAvatarInner;
|
|
843
894
|
exports.renderGroup = renderGroup;
|
|
895
|
+
exports.seed = seed;
|
|
844
896
|
exports.selectAvatar = selectAvatar;
|
|
845
897
|
//# sourceMappingURL=index.cjs.map
|
|
846
898
|
//# sourceMappingURL=index.cjs.map
|