@doan-labs/peek 0.1.0 → 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Doan Labs
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 CHANGED
@@ -19,6 +19,21 @@ import { Peek } from '@doan-labs/peek'
19
19
  Use `animate` for blinking and expression transitions, and
20
20
  `gaze='pointer'` to follow the pointer. Animation respects reduced motion.
21
21
 
22
+ ## Accessories
23
+
24
+ Dress a face with three independent slots:
25
+
26
+ ```tsx
27
+ <Peek name='Linh' eyewear='glasses' headwear='bow' />
28
+ <Peek name='Linh' eyewear='sunglasses' headwear='sprout' />
29
+ <Peek name='Linh' headwear='cap' neckwear='tie' animate />
30
+ ```
31
+
32
+ Eyewear: `none`, `glasses`, `sunglasses`. Headwear: `none`, `bow`, `cap`,
33
+ `sprout`. Neckwear: `none`, `tie`. All default to `none` at `peek@1`, so
34
+ existing avatars stay unchanged. Slots combine and work with every
35
+ expression, gaze and frame. The same options work with `toSvg`.
36
+
22
37
  ## Plain SVG, without React
23
38
 
24
39
  The SVG and identity entry points have no runtime dependencies:
@@ -36,3 +51,7 @@ The same normalized name and options produce the same SVG every time.
36
51
 
37
52
  See the [documentation](https://peek.doan-labs.com/docs) for options,
38
53
  expressions, identity axes and versioned styles.
54
+
55
+ ## License
56
+
57
+ [MIT](./LICENSE) © Doan Labs
@@ -1,4 +1,4 @@
1
- import { type Axis, type Brows, type Cheeks, type Color, type Eyes, type Face, type Mouth, type Trait } from './tables.js';
1
+ import { type Axis, type Brows, type Cheeks, type Color, type Eyes, type Eyewear, type Face, type Headwear, type Mouth, type Neckwear, type Trait } from './tables.js';
2
2
  export declare const STYLE = "peek";
3
3
  export declare const tidy: (s: string) => string;
4
4
  /** FNV-1a over the UTF-8 bytes. */
@@ -34,6 +34,9 @@ export type Axes = {
34
34
  mouth: Mouth;
35
35
  cheeks: Cheeks;
36
36
  trait: Trait;
37
+ eyewear: Eyewear;
38
+ headwear: Headwear;
39
+ neckwear: Neckwear;
37
40
  };
38
41
  export type Identity = Axes & {
39
42
  /** The tidied name. */
package/dist/identity.js CHANGED
@@ -116,8 +116,24 @@ var PARTS = {
116
116
  cheeks: ["oval", "dots", "lines"],
117
117
  trait: ["square", "fin", "ring", "dot", "peak"]
118
118
  };
119
+ var ACCESSORIES = {
120
+ eyewear: ["none", "glasses", "sunglasses"],
121
+ headwear: ["none", "bow", "cap", "sprout"],
122
+ neckwear: ["none", "tie"]
123
+ };
119
124
  var VERSIONS = {
120
- 1: { face: 4, color: 7, eyes: 3, brows: 4, mouth: 4, cheeks: 3, trait: 5 }
125
+ 1: {
126
+ face: 4,
127
+ color: 7,
128
+ eyes: 3,
129
+ brows: 4,
130
+ mouth: 4,
131
+ cheeks: 3,
132
+ trait: 5,
133
+ eyewear: 1,
134
+ headwear: 1,
135
+ neckwear: 1
136
+ }
121
137
  };
122
138
  var LATEST = 1;
123
139
  var BASE = {
@@ -339,7 +355,8 @@ function mulberry32(a) {
339
355
  var LISTS = {
340
356
  face: Object.keys(FACES),
341
357
  color: Object.keys(COLORS),
342
- ...PARTS
358
+ ...PARTS,
359
+ ...ACCESSORIES
343
360
  };
344
361
  var AXES = Object.keys(LISTS);
345
362
  function seed(name, axis, version = LATEST) {
@@ -383,6 +400,9 @@ function identify(name, { version = LATEST } = {}) {
383
400
  mouth: pick("mouth"),
384
401
  cheeks: pick("cheeks"),
385
402
  trait: pick("trait"),
403
+ eyewear: pick("eyewear"),
404
+ headwear: pick("headwear"),
405
+ neckwear: pick("neckwear"),
386
406
  persona
387
407
  };
388
408
  }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
- export type { Frame, Gaze } from './draw.js';
1
+ export type { Frame, Gaze, Node } from './draw.js';
2
+ /** Low level: the drawing tree that toSvg and <Peek> both render. */
3
+ export { draw } from './draw.js';
2
4
  export type { Axes, Identity, Persona } from './identity.js';
3
5
  export { AXES, identify, LISTS, tidy } from './identity.js';
4
6
  export type { PeekProps } from './peek.js';
5
7
  export { Peek } from './peek.js';
6
8
  export type { PeekOptions } from './svg.js';
7
- export { toSvg } from './svg.js';
8
- export type { Axis, Brows, Channel, Cheeks, Color, Expression, Eyes, Face, Mouth, Trait, } from './tables.js';
9
- export { COLORS, EXPRESSIONS, FACES, GROUPS, LATEST, PARTS, VERSIONS, } from './tables.js';
9
+ export { settle, toSvg } from './svg.js';
10
+ export type { AccessorySlot, Axis, Brows, Channel, Cheeks, Color, Expression, Eyes, Eyewear, Face, Headwear, Mouth, Neckwear, Trait, } from './tables.js';
11
+ export { ACCESSORIES, COLORS, EXPRESSIONS, FACES, GROUPS, LATEST, PARTS, VERSIONS, } from './tables.js';
package/dist/index.js CHANGED
@@ -121,8 +121,25 @@ var PARTS = {
121
121
  cheeks: ["oval", "dots", "lines"],
122
122
  trait: ["square", "fin", "ring", "dot", "peak"]
123
123
  };
124
+ var ACCESSORY_INK = { knot: "#E5412D" };
125
+ var ACCESSORIES = {
126
+ eyewear: ["none", "glasses", "sunglasses"],
127
+ headwear: ["none", "bow", "cap", "sprout"],
128
+ neckwear: ["none", "tie"]
129
+ };
124
130
  var VERSIONS = {
125
- 1: { face: 4, color: 7, eyes: 3, brows: 4, mouth: 4, cheeks: 3, trait: 5 }
131
+ 1: {
132
+ face: 4,
133
+ color: 7,
134
+ eyes: 3,
135
+ brows: 4,
136
+ mouth: 4,
137
+ cheeks: 3,
138
+ trait: 5,
139
+ eyewear: 1,
140
+ headwear: 1,
141
+ neckwear: 1
142
+ }
126
143
  };
127
144
  var LATEST = 1;
128
145
  var GROUPS = {
@@ -330,84 +347,6 @@ var EXPRESSIONS = {
330
347
  })
331
348
  };
332
349
 
333
- // src/identity.ts
334
- var STYLE = "peek";
335
- var tidy = (s) => s.normalize("NFC").trim().replace(/\s+/g, " ").toLowerCase();
336
- function fnv1a(str) {
337
- let h = 2166136261;
338
- for (const b of new TextEncoder().encode(str)) {
339
- h ^= b;
340
- h = Math.imul(h, 16777619) >>> 0;
341
- }
342
- return h >>> 0;
343
- }
344
- function mulberry32(a) {
345
- return () => {
346
- a = a + 1831565813 | 0;
347
- let t = Math.imul(a ^ a >>> 15, 1 | a);
348
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
349
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
350
- };
351
- }
352
- var LISTS = {
353
- face: Object.keys(FACES),
354
- color: Object.keys(COLORS),
355
- ...PARTS
356
- };
357
- var AXES = Object.keys(LISTS);
358
- function seed(name, axis, version = LATEST) {
359
- return fnv1a(`${STYLE}@${version}:${axis}:${tidy(name)}`);
360
- }
361
- function identify(name, { version = LATEST } = {}) {
362
- const lengths = VERSIONS[version];
363
- if (!lengths)
364
- throw new Error(`peek@${version} does not exist`);
365
- const pick = (axis) => LISTS[axis][seed(name, axis, version) % lengths[axis]];
366
- const rnd = mulberry32(seed(name, "persona", version));
367
- const r = (a, b) => Math.round((a + rnd() * (b - a)) * 100) / 100;
368
- const persona = {
369
- spread: r(-7, 7),
370
- blink: r(0.75, 1.4),
371
- add: {
372
- browY: r(-4, 4),
373
- browTilt: r(-5, 5),
374
- browArch: r(-0.2, 0.3),
375
- rot: r(-3, 3),
376
- hair: r(-0.2, 0.35),
377
- gx: r(-0.12, 0.12),
378
- my: r(-2, 3)
379
- },
380
- mul: {
381
- eyeS: r(0.92, 1.08),
382
- pupil: r(0.9, 1.12),
383
- browW: r(0.85, 1.2),
384
- mw: r(0.85, 1.18)
385
- }
386
- };
387
- const key = tidy(name);
388
- return {
389
- key,
390
- hash: fnv1a(key),
391
- version,
392
- face: pick("face"),
393
- color: pick("color"),
394
- eyes: pick("eyes"),
395
- brows: pick("brows"),
396
- mouth: pick("mouth"),
397
- cheeks: pick("cheeks"),
398
- trait: pick("trait"),
399
- persona
400
- };
401
- }
402
- // src/peek.tsx
403
- import {
404
- createElement,
405
- useEffect,
406
- useId,
407
- useRef,
408
- useState
409
- } from "react";
410
-
411
350
  // src/draw.ts
412
351
  var VB = [-30, -30, 400, 400];
413
352
  var FLOOR = 370;
@@ -421,6 +360,9 @@ var f2 = (v) => String(Math.round(v * 100) / 100);
421
360
  var f3 = (v) => String(Math.round(v * 1000) / 1000);
422
361
  var strokeFor = (px) => px >= 96 ? 1 : px >= 56 ? 1.25 : px >= 40 ? 1.6 : px >= 30 ? 2 : 2.4;
423
362
  var RAD = Math.PI / 180;
363
+ var STEM = "M0 0Q3 -19 0 -36";
364
+ var LEAF0 = "M0 -33C-24 -30 -38 -52 -25 -53C-12 -54 -2 -43 0 -33Z";
365
+ var LEAF1 = "M0 -33C5 -54 28 -61 27 -48C26 -36 11 -31 0 -33Z";
424
366
  function frameOf(face) {
425
367
  const baseY = FLOOR + face.sink - face.bottom;
426
368
  const eyeY = face.eyes[0][1];
@@ -795,7 +737,7 @@ function draw(who, pose, o) {
795
737
  const side = i === 0 ? -1 : 1;
796
738
  const hw = sh.brow.hw;
797
739
  const c = -sh.brow.arch * arch;
798
- const [w, ca, k2] = who.brows === "bar" ? [hw * 0.9, c * 0.3, 1.75] : who.brows === "dash" ? [hw * 0.42, c * 0.25, 1.9] : [hw, c, 1];
740
+ const [w2, ca, k2] = who.brows === "bar" ? [hw * 0.9, c * 0.3, 1.75] : who.brows === "dash" ? [hw * 0.42, c * 0.25, 1.9] : [hw, c, 1];
799
741
  const ti = bw * 1.7;
800
742
  const to = bw * 0.45;
801
743
  const [tl, tr] = side < 0 ? [to, ti] : [ti, to];
@@ -808,7 +750,7 @@ function draw(who, pose, o) {
808
750
  const sin = Math.sin(side * btilt * RAD);
809
751
  const inside = (dy) => {
810
752
  for (let t = 0;t <= 1; t += 0.25) {
811
- const lx = w * (2 * t - 1);
753
+ const lx = w2 * (2 * t - 1);
812
754
  const ly = 2 * t * (1 - t) * ca;
813
755
  const px = bx + lx * cos - ly * sin;
814
756
  const py = by + dy + lx * sin + ly * cos;
@@ -843,7 +785,7 @@ function draw(who, pose, o) {
843
785
  });
844
786
  }
845
787
  return n("path", key, {
846
- d: `M${f2(-w)} 0Q0 ${f2(ca)} ${f2(w)} 0`,
788
+ d: `M${f2(-w2)} 0Q0 ${f2(ca)} ${f2(w2)} 0`,
847
789
  transform,
848
790
  fill: "none",
849
791
  stroke: INK.ink,
@@ -933,8 +875,8 @@ function draw(who, pose, o) {
933
875
  });
934
876
  } else if (who.trait === "ring") {
935
877
  const b = 35 - hair * 14 - (hair < 0 ? hair * -21 : 0) + lag * 0.9;
936
- const w = ringWalk(sh);
937
- const s = seat(sh.crown, 8, Math.max(w.min, w.rest + 158 * (b - 35) * RAD));
878
+ const w2 = ringWalk(sh);
879
+ const s = seat(sh.crown, 8, Math.max(w2.min, w2.rest + 158 * (b - 35) * RAD));
938
880
  traitT = `translate(${f2(s.x)} ${f2(s.y)}) scale(${f2(1 + Math.max(0, hair) * 0.1)})`;
939
881
  shape = n("circle", "trait.shape", {
940
882
  cx: 0,
@@ -968,6 +910,169 @@ function draw(who, pose, o) {
968
910
  });
969
911
  }
970
912
  const trait = n("g", "trait", { transform: traitT }, [shape]);
913
+ const wardrobe = [];
914
+ const w = (px) => f2(px * Math.min(st, 1.6));
915
+ const line = {
916
+ fill: "none",
917
+ stroke: INK.ink,
918
+ "stroke-width": w(5.25),
919
+ "stroke-linecap": "round",
920
+ "stroke-linejoin": "round"
921
+ };
922
+ if (who.eyewear !== "none") {
923
+ const lx = sh.eyes[0][0] - spread;
924
+ const rr = sh.eyes[1][0] + spread;
925
+ const ey = sh.eyes[0][1];
926
+ const gap = rr - lx;
927
+ const erx = Math.min(rx + 9, (gap - 15) / 2);
928
+ const ery = ry + 9;
929
+ const glasses = who.eyewear === "glasses";
930
+ const lensX = glasses ? erx : Math.min(rx + 11, (gap - 12) / 2);
931
+ const top = glasses ? ey - 4 : ey - ry - 2;
932
+ const frames = [
933
+ n("path", "eyewear.bridge", {
934
+ ...line,
935
+ d: `M${f2(lx + lensX)} ${f2(top)}Q${f2((lx + rr) / 2)} ${f2(top - (glasses ? 13 : 0))} ${f2(rr - lensX)} ${f2(top)}`
936
+ })
937
+ ];
938
+ for (const [i, cx] of [lx, rr].entries()) {
939
+ const key = `eyewear.lens${i}`;
940
+ frames.push(n("path", `${key}.arm`, {
941
+ ...line,
942
+ d: i === 0 ? `M${f2(cx - lensX - 9)} ${f2(top + 2)}L${f2(cx - lensX)} ${f2(top)}` : `M${f2(cx + lensX)} ${f2(top)}L${f2(cx + lensX + 9)} ${f2(top + 2)}`
943
+ }));
944
+ if (glasses) {
945
+ frames.push(n("ellipse", key, {
946
+ ...line,
947
+ cx: f2(cx),
948
+ cy: ey,
949
+ rx: f2(erx),
950
+ ry: f2(ery)
951
+ }));
952
+ frames.push(n("path", `${key}.glint`, {
953
+ ...line,
954
+ stroke: INK.paper,
955
+ "stroke-width": w(3),
956
+ d: `M${f2(cx - erx * 0.65)} ${f2(ey - ery * 0.45)}Q${f2(cx - erx * 0.5)} ${f2(ey - ery * 0.8)} ${f2(cx - erx * 0.2)} ${f2(ey - ery * 0.86)}`
957
+ }));
958
+ } else {
959
+ const h = ry * 2 + 9;
960
+ frames.push(n("path", key, {
961
+ d: `M${f2(cx - lensX)} ${f2(top)}H${f2(cx + lensX)}V${f2(top + h * 0.55)}Q${f2(cx + lensX)} ${f2(top + h)} ${f2(cx)} ${f2(top + h)}Q${f2(cx - lensX)} ${f2(top + h)} ${f2(cx - lensX)} ${f2(top + h * 0.55)}Z`,
962
+ fill: INK.ink,
963
+ stroke: INK.ink,
964
+ "stroke-width": w(3),
965
+ "stroke-linejoin": "round"
966
+ }));
967
+ frames.push(n("path", `${key}.glint`, {
968
+ ...line,
969
+ stroke: INK.paper,
970
+ "stroke-width": w(4),
971
+ d: `M${f2(cx - lensX * 0.58)} ${f2(top + 19)}l8 -8m-1 15 6 -6`
972
+ }), n("circle", `${key}.pin`, {
973
+ cx: f2(cx + lensX * 0.55),
974
+ cy: f2(top + h - 9),
975
+ r: 1.7,
976
+ fill: INK.paper
977
+ }));
978
+ }
979
+ }
980
+ wardrobe.push(n("g", "eyewear", {}, frames));
981
+ }
982
+ if (who.headwear !== "none") {
983
+ const side = who.trait === "ring" || who.trait === "dot" ? -1 : 1;
984
+ const cap = who.headwear === "cap";
985
+ const sprout = who.headwear === "sprout";
986
+ const inset = sprout ? -6 : cap ? -8 : 0;
987
+ const offset = sprout ? 34 : 40;
988
+ let lo = 0;
989
+ let hi = 160;
990
+ for (let i = 0;i < 16; i++) {
991
+ const mid2 = (lo + hi) / 2;
992
+ const p = seat(sh.crown, inset, side * mid2);
993
+ if (side * (p.x - 170) < offset)
994
+ lo = mid2;
995
+ else
996
+ hi = mid2;
997
+ }
998
+ const anchor = seat(sh.crown, inset, side * (lo + hi) / 2);
999
+ const angle = clamp(anchor.a, -40, 40);
1000
+ const parts = [];
1001
+ if (cap) {
1002
+ parts.push(n("path", "headwear.crown", {
1003
+ ...line,
1004
+ fill: INK.paper,
1005
+ d: "M-33 0C-35 -20 -20 -39 0 -39C20 -39 35 -20 33 0Z"
1006
+ }), n("path", "headwear.seam", {
1007
+ ...line,
1008
+ "stroke-width": w(3.5),
1009
+ d: "M-4 -38Q9 -24 8 -1"
1010
+ }), n("path", "headwear.brim", {
1011
+ ...line,
1012
+ "stroke-width": w(7),
1013
+ d: side > 0 ? "M-35 1Q8 4 52 3" : "M-52 3Q-8 4 35 1"
1014
+ }), n("circle", "headwear.button", {
1015
+ cx: 0,
1016
+ cy: -40,
1017
+ r: 3.5,
1018
+ fill: INK.ink
1019
+ }));
1020
+ } else if (sprout) {
1021
+ parts.push(n("path", "headwear.halo", {
1022
+ ...line,
1023
+ stroke: fill,
1024
+ "stroke-width": w(11),
1025
+ d: `${STEM}${LEAF0}${LEAF1}`
1026
+ }), n("path", "headwear.stem", {
1027
+ ...line,
1028
+ "stroke-width": w(4.5),
1029
+ d: STEM
1030
+ }), n("path", "headwear.leaf0", {
1031
+ ...line,
1032
+ "stroke-width": w(4),
1033
+ fill: COLORS.mint.body,
1034
+ d: LEAF0
1035
+ }), n("path", "headwear.leaf1", {
1036
+ ...line,
1037
+ "stroke-width": w(4),
1038
+ fill: COLORS.mint.deep,
1039
+ d: LEAF1
1040
+ }));
1041
+ } else {
1042
+ parts.push(n("path", "headwear.wings", {
1043
+ d: "M-3 0C-13 -16 -28 -23 -29 -10C-31 2 -26 19 -16 13L-3 4L3 4L16 13C26 19 31 2 29 -10C28 -23 13 -16 3 0Z",
1044
+ fill: INK.ink,
1045
+ stroke: fill,
1046
+ "stroke-width": w(6),
1047
+ "stroke-linejoin": "round",
1048
+ "paint-order": "stroke"
1049
+ }), n("ellipse", "headwear.knot", {
1050
+ cx: 0,
1051
+ cy: 1,
1052
+ rx: 6,
1053
+ ry: 8,
1054
+ fill: ACCESSORY_INK.knot
1055
+ }));
1056
+ }
1057
+ wardrobe.push(n("g", "headwear", {
1058
+ transform: `translate(${f2(anchor.x)} ${f2(anchor.y)}) rotate(${f2(sprout ? angle * 0.5 : angle)})`
1059
+ }, parts));
1060
+ }
1061
+ if (who.neckwear === "tie") {
1062
+ const skew = Math.abs(Math.sin(g("mk") * RAD)) * mw;
1063
+ const bottom = Math.max(0, mt * kb, mb * kb) + skew + m.sw * st;
1064
+ wardrobe.push(n("g", "neckwear", {
1065
+ transform: `translate(${m.x} ${f2(m.y + g("my") + bottom + 8)})`,
1066
+ fill: INK.ink
1067
+ }, [
1068
+ n("path", "neckwear.knot", {
1069
+ d: "M-9 0Q-11 0 -9 4L-5 11Q0 15 5 11L9 4Q11 0 9 0Z"
1070
+ }),
1071
+ n("path", "neckwear.blade", {
1072
+ d: "M-4 15L-13 63Q-14 66 -11 69L0 79L11 69Q14 66 13 63L4 15Z"
1073
+ })
1074
+ ]));
1075
+ }
971
1076
  const off = (dx, dy) => riso ? `translate(${f2(dx * k * upp)} ${f2(dy * k * upp)})` : undefined;
972
1077
  const av = n("g", "av", {
973
1078
  transform: `translate(${f2(x)} ${f2(ty)}) rotate(${f2(rot)} 170 ${B}) translate(170 ${B}) scale(${sc(sx)} ${sc(sy)}) translate(-170 ${-B})`,
@@ -980,7 +1085,7 @@ function draw(who, pose, o) {
980
1085
  n("g", "plateB", {
981
1086
  transform: off(0.6, -0.5),
982
1087
  filter: riso ? `url(#${id}-pB)` : undefined
983
- }, [...eyes, ...brows, mouth])
1088
+ }, [...eyes, ...brows, mouth, ...wardrobe])
984
1089
  ]);
985
1090
  const scene = [];
986
1091
  if (o.frame !== "none") {
@@ -1023,6 +1128,87 @@ function draw(who, pose, o) {
1023
1128
  ]);
1024
1129
  return o.live ? root : prune(root) ?? root;
1025
1130
  }
1131
+ // src/identity.ts
1132
+ var STYLE = "peek";
1133
+ var tidy = (s) => s.normalize("NFC").trim().replace(/\s+/g, " ").toLowerCase();
1134
+ function fnv1a(str) {
1135
+ let h = 2166136261;
1136
+ for (const b of new TextEncoder().encode(str)) {
1137
+ h ^= b;
1138
+ h = Math.imul(h, 16777619) >>> 0;
1139
+ }
1140
+ return h >>> 0;
1141
+ }
1142
+ function mulberry32(a) {
1143
+ return () => {
1144
+ a = a + 1831565813 | 0;
1145
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
1146
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
1147
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
1148
+ };
1149
+ }
1150
+ var LISTS = {
1151
+ face: Object.keys(FACES),
1152
+ color: Object.keys(COLORS),
1153
+ ...PARTS,
1154
+ ...ACCESSORIES
1155
+ };
1156
+ var AXES = Object.keys(LISTS);
1157
+ function seed(name, axis, version = LATEST) {
1158
+ return fnv1a(`${STYLE}@${version}:${axis}:${tidy(name)}`);
1159
+ }
1160
+ function identify(name, { version = LATEST } = {}) {
1161
+ const lengths = VERSIONS[version];
1162
+ if (!lengths)
1163
+ throw new Error(`peek@${version} does not exist`);
1164
+ const pick = (axis) => LISTS[axis][seed(name, axis, version) % lengths[axis]];
1165
+ const rnd = mulberry32(seed(name, "persona", version));
1166
+ const r = (a, b) => Math.round((a + rnd() * (b - a)) * 100) / 100;
1167
+ const persona = {
1168
+ spread: r(-7, 7),
1169
+ blink: r(0.75, 1.4),
1170
+ add: {
1171
+ browY: r(-4, 4),
1172
+ browTilt: r(-5, 5),
1173
+ browArch: r(-0.2, 0.3),
1174
+ rot: r(-3, 3),
1175
+ hair: r(-0.2, 0.35),
1176
+ gx: r(-0.12, 0.12),
1177
+ my: r(-2, 3)
1178
+ },
1179
+ mul: {
1180
+ eyeS: r(0.92, 1.08),
1181
+ pupil: r(0.9, 1.12),
1182
+ browW: r(0.85, 1.2),
1183
+ mw: r(0.85, 1.18)
1184
+ }
1185
+ };
1186
+ const key = tidy(name);
1187
+ return {
1188
+ key,
1189
+ hash: fnv1a(key),
1190
+ version,
1191
+ face: pick("face"),
1192
+ color: pick("color"),
1193
+ eyes: pick("eyes"),
1194
+ brows: pick("brows"),
1195
+ mouth: pick("mouth"),
1196
+ cheeks: pick("cheeks"),
1197
+ trait: pick("trait"),
1198
+ eyewear: pick("eyewear"),
1199
+ headwear: pick("headwear"),
1200
+ neckwear: pick("neckwear"),
1201
+ persona
1202
+ };
1203
+ }
1204
+ // src/peek.tsx
1205
+ import {
1206
+ createElement,
1207
+ useEffect,
1208
+ useId,
1209
+ useRef,
1210
+ useState
1211
+ } from "react";
1026
1212
 
1027
1213
  // src/animate.ts
1028
1214
  var GROUP_OF = {};
@@ -1601,6 +1787,7 @@ class Live {
1601
1787
  }
1602
1788
 
1603
1789
  // src/svg.ts
1790
+ var wear = (o, base, s) => ACCESSORIES[s].includes(o[s]) ? o[s] : base[s];
1604
1791
  function settle(name, o, live = false) {
1605
1792
  const base = identify(name, { version: o.version });
1606
1793
  const who = {
@@ -1611,7 +1798,10 @@ function settle(name, o, live = false) {
1611
1798
  brows: o.brows ?? base.brows,
1612
1799
  mouth: o.mouth ?? base.mouth,
1613
1800
  cheeks: o.cheeks ?? base.cheeks,
1614
- trait: o.trait ?? base.trait
1801
+ trait: o.trait ?? base.trait,
1802
+ eyewear: wear(o, base, "eyewear"),
1803
+ headwear: wear(o, base, "headwear"),
1804
+ neckwear: wear(o, base, "neckwear")
1615
1805
  };
1616
1806
  const pose = restPose(who, o.expression ?? "normal", o.gaze);
1617
1807
  const look = {
@@ -1621,7 +1811,9 @@ function settle(name, o, live = false) {
1621
1811
  riso: o.riso ?? false,
1622
1812
  live
1623
1813
  };
1624
- const id = o.id ?? `peek-${fnv1a(JSON.stringify([who, pose, look])).toString(36)}`;
1814
+ const { eyewear, headwear, neckwear, ...legacy } = who;
1815
+ const wardrobe = eyewear !== "none" || headwear !== "none" || neckwear !== "none";
1816
+ const id = o.id ?? `peek-${fnv1a(JSON.stringify([wardrobe ? who : legacy, pose, look])).toString(36)}`;
1625
1817
  return { who, pose, opts: { ...look, id, title: o.title ?? name } };
1626
1818
  }
1627
1819
  var ILLEGAL = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
@@ -1684,6 +1876,7 @@ function Peek({
1684
1876
  return toReact(tree, { ref: svg, className, style });
1685
1877
  }
1686
1878
  export {
1879
+ ACCESSORIES,
1687
1880
  AXES,
1688
1881
  COLORS,
1689
1882
  EXPRESSIONS,
@@ -1694,7 +1887,9 @@ export {
1694
1887
  PARTS,
1695
1888
  Peek,
1696
1889
  VERSIONS,
1890
+ draw,
1697
1891
  identify,
1892
+ settle,
1698
1893
  tidy,
1699
1894
  toSvg
1700
1895
  };
package/dist/svg.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type DrawOptions, type Frame, type Gaze, type Node, type Pose } from './draw.js';
2
2
  import { type Axes, type Identity } from './identity.js';
3
- import type { Expression } from './tables.js';
3
+ import { type Expression } from './tables.js';
4
4
  export type PeekOptions = Partial<Axes> & {
5
5
  /** px. Drives the stroke ramp (thicker below 96) and the riso print. */
6
6
  size?: number;