@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 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(seed) {
19
- const [a, b] = cyrb53(seed, 0);
20
- const [c, d] = cyrb53(seed, 1);
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(seed, options = {}) {
627
- const rng = createRng(seed);
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(seed) {
780
+ function stableId(seed2) {
781
781
  let h = 5381;
782
- for (let i = 0; i < seed.length; i++) h = (h << 5) + h + seed.charCodeAt(i) | 0;
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((seed, i) => {
806
+ const tiles = visibleSeeds.map((seed2, i) => {
807
807
  const x = i * step;
808
- const spec = selectAvatar(seed, options);
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(seed, options = {}) {
832
- if (typeof seed !== "string" || seed.length === 0) {
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(seed, options), options);
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