@usenavii/core 0.2.1 → 0.4.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
@@ -64,6 +64,7 @@ It picks the most unique field automatically: `id` → `email` → `name + creat
64
64
 
65
65
  ```ts
66
66
  createAvatar(seed: string, options?: AvatarOptions): string
67
+ random(options?: AvatarOptions): { svg: string; seed: string }
67
68
  selectAvatar(seed: string, options?: AvatarOptions): AvatarSpec
68
69
  renderAvatar(spec: AvatarSpec, options?: AvatarOptions): string
69
70
  renderGroup(seeds: string[], options?: GroupOptions): string
@@ -71,7 +72,7 @@ renderGroup(seeds: string[], options?: GroupOptions): string
71
72
  seed(fields: SeedFields): string // pick most-unique field
72
73
  build(spec?: BuildSpec, opts?): string // manual mix-and-match (no seed)
73
74
 
74
- Navii.{ create, render, select, group, seed, build }
75
+ Navii.{ create, random, render, select, group, seed, build }
75
76
  ```
76
77
 
77
78
  ### `AvatarOptions`
@@ -85,6 +86,22 @@ Navii.{ create, render, select, group, seed, build }
85
86
  | `title` | accessible label (sets `<title>` + `aria-label`) | none |
86
87
  | `animated` | `boolean` — idle float / blink / sway / pulse / twinkle | `false` |
87
88
 
89
+ ### `random()` — pick a seed for you
90
+
91
+ For "spin again" UX, onboarding before the user picks an avatar, dev/demo seeding. Returns the chosen seed so you can persist it:
92
+
93
+ ```ts
94
+ const { svg, seed } = Navii.random({ size: 96 });
95
+ await db.users.update(user.id, { naviiSeed: seed });
96
+ ```
97
+
98
+ Calling `random()` in a React render gives a new avatar every re-render. Stabilize with `useState`:
99
+
100
+ ```tsx
101
+ const [{ seed }] = useState(() => Navii.random());
102
+ return <Navii seed={seed} />;
103
+ ```
104
+
88
105
  ### `build()` — direct construction without a seed
89
106
 
90
107
  Use for brand mascots, logo marks, designer-curated avatars:
package/dist/index.cjs CHANGED
@@ -559,6 +559,76 @@ function renderTopper(id, anchor, palette) {
559
559
  }
560
560
  }
561
561
 
562
+ // src/parts/outfit.ts
563
+ function renderOutfit(id, anchor, palette) {
564
+ if (id === "none") return "";
565
+ const cx = anchor.cx;
566
+ const cy = anchor.mouthY + (anchor.groundY - anchor.mouthY) * 0.55;
567
+ const ink = palette.ink;
568
+ const accent = palette.accent;
569
+ switch (id) {
570
+ case "collar":
571
+ return [
572
+ `<path d="M${cx - 9} ${cy} L${cx - 2} ${cy - 4} L${cx - 2} ${cy + 5} Z" fill="${accent}" stroke="${ink}" stroke-width="0.7" />`,
573
+ `<path d="M${cx + 9} ${cy} L${cx + 2} ${cy - 4} L${cx + 2} ${cy + 5} Z" fill="${accent}" stroke="${ink}" stroke-width="0.7" />`,
574
+ // tiny button at center
575
+ `<circle cx="${cx}" cy="${cy + 4}" r="0.9" fill="${ink}" />`
576
+ ].join("");
577
+ case "scarf":
578
+ return [
579
+ // wrap band
580
+ `<path d="M${cx - 14} ${cy - 2} Q${cx} ${cy + 3} ${cx + 14} ${cy - 2} L${cx + 14} ${cy + 3} Q${cx} ${cy + 8} ${cx - 14} ${cy + 3} Z" fill="${palette.bodyTo}" stroke="${ink}" stroke-width="0.6" />`,
581
+ // tail 1 (left)
582
+ `<path d="M${cx - 6} ${cy + 5} L${cx - 9} ${cy + 12} L${cx - 4} ${cy + 12} L${cx - 2} ${cy + 5} Z" fill="${palette.bodyTo}" stroke="${ink}" stroke-width="0.5" />`,
583
+ // tail 2 (slightly right)
584
+ `<path d="M${cx + 1} ${cy + 6} L${cx + 4} ${cy + 13} L${cx - 1} ${cy + 13} L${cx - 2} ${cy + 6} Z" fill="${palette.bodyFrom}" stroke="${ink}" stroke-width="0.5" />`
585
+ ].join("");
586
+ case "bowtie":
587
+ return [
588
+ // left wing
589
+ `<path d="M${cx - 1} ${cy} L${cx - 9} ${cy - 4} L${cx - 9} ${cy + 4} Z" fill="${palette.bodyTo}" stroke="${ink}" stroke-width="0.7" />`,
590
+ // right wing
591
+ `<path d="M${cx + 1} ${cy} L${cx + 9} ${cy - 4} L${cx + 9} ${cy + 4} Z" fill="${palette.bodyTo}" stroke="${ink}" stroke-width="0.7" />`,
592
+ // center knot
593
+ `<rect x="${cx - 1.4}" y="${cy - 2.4}" width="2.8" height="4.8" rx="0.8" fill="${palette.bodyFrom}" stroke="${ink}" stroke-width="0.5" />`
594
+ ].join("");
595
+ case "sunflower": {
596
+ const fx = cx - 8;
597
+ const fy = cy + 2;
598
+ const petals = [];
599
+ for (let i = 0; i < 8; i++) {
600
+ const a = i / 8 * Math.PI * 2;
601
+ const px = fx + Math.cos(a) * 3.2;
602
+ const py = fy + Math.sin(a) * 3.2;
603
+ petals.push(
604
+ `<ellipse cx="${px.toFixed(2)}" cy="${py.toFixed(2)}" rx="2.4" ry="1.3" fill="#FACC15" stroke="${ink}" stroke-width="0.35" transform="rotate(${(a * 180 / Math.PI).toFixed(1)} ${px.toFixed(2)} ${py.toFixed(2)})" />`
605
+ );
606
+ }
607
+ return [
608
+ // stem (tucked behind)
609
+ `<path d="M${fx + 2} ${fy + 2} Q${fx + 4} ${fy + 6} ${fx + 1} ${fy + 10}" stroke="#16A34A" stroke-width="1.1" fill="none" stroke-linecap="round" />`,
610
+ // leaf
611
+ `<path d="M${fx + 3} ${fy + 6} Q${fx + 7} ${fy + 4} ${fx + 6} ${fy + 8} Q${fx + 4} ${fy + 8} ${fx + 3} ${fy + 6} Z" fill="#22C55E" stroke="${ink}" stroke-width="0.35" />`,
612
+ ...petals,
613
+ // center disc
614
+ `<circle cx="${fx}" cy="${fy}" r="2" fill="#92400E" stroke="${ink}" stroke-width="0.4" />`,
615
+ // texture dots on disc
616
+ `<circle cx="${fx - 0.6}" cy="${fy - 0.5}" r="0.4" fill="#451A03" />`,
617
+ `<circle cx="${fx + 0.7}" cy="${fy + 0.3}" r="0.4" fill="#451A03" />`,
618
+ `<circle cx="${fx - 0.4}" cy="${fy + 0.8}" r="0.4" fill="#451A03" />`
619
+ ].join("");
620
+ }
621
+ case "necklace":
622
+ return [
623
+ // chain (curve from collarbone left → drop → right)
624
+ `<path d="M${cx - 10} ${cy} Q${cx} ${cy + 8} ${cx + 10} ${cy}" stroke="${accent}" stroke-width="0.8" fill="none" stroke-linecap="round" />`,
625
+ // pendant
626
+ `<circle cx="${cx}" cy="${cy + 7}" r="1.6" fill="${accent}" stroke="${ink}" stroke-width="0.5" />`,
627
+ `<circle cx="${cx}" cy="${cy + 7}" r="0.7" fill="${palette.blush}" />`
628
+ ].join("");
629
+ }
630
+ }
631
+
562
632
  // src/parts/index.ts
563
633
  var BODY_IDS = [
564
634
  "orb",
@@ -657,6 +727,7 @@ function selectAvatar(seed2, options = {}) {
657
727
  accessory,
658
728
  background,
659
729
  topper,
730
+ outfit: "none",
660
731
  hueShift,
661
732
  bodyScale,
662
733
  eyeGapShift,
@@ -740,10 +811,13 @@ function renderAvatarInner(spec, options = {}) {
740
811
  const antennaWrapped = antennaSvg ? `<g${transformAntenna(spec.antennaTilt ?? 0, anchor)}><g class="antenna">${antennaSvg}</g></g>` : "";
741
812
  const tileBg = resolveTileBg(options.tileBg, spec.palette);
742
813
  const tileCircle = tileBg ? `<circle cx="50" cy="50" r="50" fill="${tileBg}" />` : "";
814
+ const outfitSvg = renderOutfit(spec.outfit ?? "none", anchor, spec.palette);
743
815
  const parts = [
744
816
  tileCircle,
745
817
  renderBackground(spec.background, spec.palette, bgOverride),
746
818
  bodyWrapped,
819
+ // outfit sits on the body but below the face, so face features stay readable
820
+ outfitSvg,
747
821
  renderTopper(spec.topper, anchor, spec.palette),
748
822
  wrap("eyes", renderEyes(spec.eyes, spec.palette, anchor)),
749
823
  renderMouth(spec.mouth, spec.palette, anchor, spec.mouthCurveScale ?? 1),
@@ -859,6 +933,7 @@ function build(spec = {}, options = {}) {
859
933
  accessory: spec.accessory ?? "none",
860
934
  background: spec.background ?? "none",
861
935
  topper: spec.topper ?? "none",
936
+ outfit: spec.outfit ?? "none",
862
937
  hueShift: spec.hueShift ?? 0,
863
938
  bodyScale: spec.bodyScale ?? 1,
864
939
  eyeGapShift: spec.eyeGapShift ?? 0,
@@ -875,8 +950,18 @@ function createAvatar(seed2, options = {}) {
875
950
  }
876
951
  return renderAvatar(selectAvatar(seed2, options), options);
877
952
  }
953
+ function random(options = {}) {
954
+ const seed2 = randomSeed();
955
+ return { svg: createAvatar(seed2, options), seed: seed2 };
956
+ }
957
+ function randomSeed() {
958
+ const c = globalThis.crypto;
959
+ if (c && typeof c.randomUUID === "function") return c.randomUUID();
960
+ return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
961
+ }
878
962
  var Navii = {
879
963
  create: createAvatar,
964
+ random,
880
965
  render: renderAvatar,
881
966
  select: selectAvatar,
882
967
  group: renderGroup,
@@ -889,6 +974,7 @@ exports.build = build;
889
974
  exports.createAvatar = createAvatar;
890
975
  exports.createRng = createRng;
891
976
  exports.cyrb53 = cyrb53;
977
+ exports.random = random;
892
978
  exports.renderAvatar = renderAvatar;
893
979
  exports.renderAvatarInner = renderAvatarInner;
894
980
  exports.renderGroup = renderGroup;