@doan-labs/peek 1.0.0 → 2.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.
package/dist/svg.js CHANGED
@@ -114,6 +114,34 @@ var COLORS = {
114
114
  rose: { body: "#EFCAD7", deep: "#DFA4BA" },
115
115
  aqua: { body: "#BDE1E5", deep: "#8FC7D1" }
116
116
  };
117
+ var VARIANTS = ["peek", "haze", "bloc"];
118
+ var HAZE = {
119
+ lavender: { tint: "#C8B1F5", glow: "#F2EDFD" },
120
+ fog: { tint: "#9EC3F2", glow: "#E6F1FF" },
121
+ clay: { tint: "#F4AE93", glow: "#FEECE5" },
122
+ mint: { tint: "#A3DAB5", glow: "#E5F5EA" },
123
+ butter: { tint: "#E1CC7A", glow: "#F4F0E0" },
124
+ rose: { tint: "#F0A9C4", glow: "#FDEAF1" },
125
+ aqua: { tint: "#85D3DC", glow: "#E0F5F7" }
126
+ };
127
+ var WHEEL = [
128
+ "clay",
129
+ "butter",
130
+ "mint",
131
+ "aqua",
132
+ "fog",
133
+ "lavender",
134
+ "rose"
135
+ ];
136
+ var BLOC = {
137
+ lavender: "#B093E5",
138
+ fog: "#7BA8DF",
139
+ clay: "#E38A66",
140
+ mint: "#82CB9B",
141
+ butter: "#E2CA6A",
142
+ rose: "#DF84A8",
143
+ aqua: "#5EC3CD"
144
+ };
117
145
  var PARTS = {
118
146
  eyes: ["oval", "bead", "ring"],
119
147
  brows: ["arch", "bar", "wedge", "dash"],
@@ -121,8 +149,25 @@ var PARTS = {
121
149
  cheeks: ["oval", "dots", "lines"],
122
150
  trait: ["square", "fin", "ring", "dot", "peak"]
123
151
  };
152
+ var ACCESSORY_INK = { knot: "#E5412D" };
153
+ var ACCESSORIES = {
154
+ eyewear: ["none", "glasses", "sunglasses"],
155
+ headwear: ["none", "bow", "cap", "sprout"],
156
+ neckwear: ["none", "tie"]
157
+ };
124
158
  var VERSIONS = {
125
- 1: { face: 4, color: 7, eyes: 3, brows: 4, mouth: 4, cheeks: 3, trait: 5 }
159
+ 1: {
160
+ face: 4,
161
+ color: 7,
162
+ eyes: 3,
163
+ brows: 4,
164
+ mouth: 4,
165
+ cheeks: 3,
166
+ trait: 5,
167
+ eyewear: 1,
168
+ headwear: 1,
169
+ neckwear: 1
170
+ }
126
171
  };
127
172
  var LATEST = 1;
128
173
  var BASE = {
@@ -322,6 +367,314 @@ var EXPRESSIONS = {
322
367
  })
323
368
  };
324
369
 
370
+ // src/bloc.ts
371
+ var RAD = Math.PI / 180;
372
+ var TRI = [
373
+ [170, 12.8],
374
+ [320, 312.8],
375
+ [20, 312.8]
376
+ ];
377
+ var BODY = {
378
+ diamond: FACES.diamond,
379
+ semicircle: FACES.semicircle,
380
+ circle: FACES.circle,
381
+ triangle: {
382
+ body: { d: `M${TRI.map((p) => p.join(" ")).join("L")}Z` },
383
+ crown: { poly: TRI, r: 0 }
384
+ }
385
+ };
386
+ var strokeFor = (px) => px >= 96 ? 1 : px >= 56 ? 1.25 : px >= 40 ? 1.6 : px >= 30 ? 2 : 2.6;
387
+ var boostFor = (px) => px < 30 ? 1.25 : px < 56 ? 1.12 : 1;
388
+ var turn = (x, y, deg) => {
389
+ const c = Math.cos(deg * RAD);
390
+ const s = Math.sin(deg * RAD);
391
+ return [x * c - y * s, x * s + y * c];
392
+ };
393
+ var poly = (pts) => `M${pts.map(([x, y]) => `${f2(x)} ${f2(y)}`).join("L")}Z`;
394
+ function blocParts({ who, pose, o, defs, g }) {
395
+ const sh = FACES[who.face];
396
+ const { body, crown } = BODY[who.face];
397
+ const id = o.id;
398
+ const P = who.persona;
399
+ const spread = P.spread;
400
+ const st = strokeFor(o.size);
401
+ const up = boostFor(o.size);
402
+ const strong = BLOC[who.color];
403
+ const light = o.frame === "bone" || o.frame === "paper" ? COLORS[who.color].deep : COLORS[who.color].body;
404
+ const lineW = sh.brow.w * st;
405
+ defs.push(n("clipPath", "half", { id: `${id}-r` }, [
406
+ n("rect", "half.shape", { x: 170, y: -30, width: 200, height: 400 })
407
+ ]));
408
+ const block = (key, fill, clip) => ("d" in body) ? n("path", key, { d: body.d, fill, "clip-path": clip }) : n("circle", key, {
409
+ cx: body.circle[0],
410
+ cy: body.circle[1],
411
+ r: body.circle[2],
412
+ fill,
413
+ "clip-path": clip
414
+ });
415
+ const eyeS = g("eyeS");
416
+ const r0 = Math.sqrt(sh.rx * sh.ry) * up;
417
+ const lid0 = g("lid");
418
+ const lidAsym = g("lidAsym");
419
+ const lower = clamp(g("lower"), 0, 1);
420
+ const pupil = g("pupil");
421
+ const gx = clamp(g("gx"), -1.1, 1.1);
422
+ const gy = clamp(g("gy"), -1.1, 1.1);
423
+ const tilt = g("lidTilt");
424
+ const bead = who.eyes === "bead";
425
+ const eyes = sh.eyes.map(([ex, ey], i) => {
426
+ const side = i === 0 ? -1 : 1;
427
+ const kk = `eye${i}`;
428
+ let lid = clamp(lid0 + (i === 0 ? lidAsym : -lidAsym * 0.3), 0, 1);
429
+ lid = lid + (1 - lid) * pose.blink;
430
+ const cut = clamp((lid - 0.1) / 0.9, 0, 1);
431
+ const r = bead ? r0 * 0.7 * eyeS * (0.4 + 0.6 * pupil) : r0 * eyeS;
432
+ const [tx, ty] = bead ? [ex + side * spread + gx * 6, ey + gy * 6] : [ex + side * spread, ey];
433
+ const a = side * tilt;
434
+ const edge = -r - 1 + cut * (2 * r + 2);
435
+ const floor = r + 1 - lower * r * 2.2;
436
+ const W = r + 2;
437
+ const lidAt = (x) => x * Math.tan(a * RAD) + edge / Math.cos(a * RAD);
438
+ const [t0, t1] = [lidAt(-W), lidAt(W)];
439
+ defs.push(n("clipPath", `${kk}.clip`, { id: `${id}-e${i}` }, [
440
+ n("path", `${kk}.clip.shape`, {
441
+ d: poly([
442
+ [-W, t0],
443
+ [W, t1],
444
+ [W, Math.max(floor, t1)],
445
+ [-W, Math.max(floor, t0)]
446
+ ])
447
+ })
448
+ ]));
449
+ const pr = sh.pr * pupil * up;
450
+ const px = gx * Math.max(0, r - pr) * 0.85;
451
+ const py = gy * Math.max(0, r - pr) * 0.75;
452
+ const rim = Math.min(r * 0.27 * Math.min(st, 1.6), r * 0.4);
453
+ const inner = bead ? [n("circle", `${kk}.bead`, { cx: 0, cy: 0, r: f2(r), fill: INK.ink })] : [
454
+ n("circle", `${kk}.white`, {
455
+ cx: 0,
456
+ cy: 0,
457
+ r: f2(r),
458
+ fill: INK.paper
459
+ }),
460
+ n("circle", `${kk}.pupil`, {
461
+ cx: f2(px),
462
+ cy: f2(py),
463
+ r: f2(pr),
464
+ fill: INK.ink
465
+ })
466
+ ];
467
+ if (who.eyes === "ring")
468
+ inner.push(n("circle", `${kk}.rim`, {
469
+ cx: 0,
470
+ cy: 0,
471
+ r: f2(r - rim / 2),
472
+ fill: "none",
473
+ stroke: INK.ink,
474
+ "stroke-width": f2(rim)
475
+ }));
476
+ const co = smooth(0.86, 1, lid);
477
+ const yy = Math.min(edge, r) - lineW / 2;
478
+ return n("g", kk, { transform: `translate(${f2(tx)} ${f2(ty)})` }, [
479
+ n("g", `${kk}.inner`, { "clip-path": `url(#${id}-e${i})` }, inner),
480
+ n("path", `${kk}.closed`, {
481
+ d: `M${f2(-r * 0.95)} ${f2(yy)}H${f2(r * 0.95)}`,
482
+ transform: `rotate(${f2(a * 0.6)})`,
483
+ stroke: INK.ink,
484
+ "stroke-width": f2(lineW * 1.4),
485
+ opacity: co > 0.01 ? f2(co) : 0
486
+ })
487
+ ]);
488
+ });
489
+ const bt = pose.browTilt;
490
+ const basym = g("browAsym");
491
+ const th = lineW * 2 * g("browW");
492
+ const hw = sh.eyes[1][0] - 170 + spread + Math.sqrt(sh.rx * sh.ry) * P.mul.eyeS * up;
493
+ const gap = smooth(0, 8, Math.abs(bt)) * th * 0.9;
494
+ const slant = P.add.browTilt * 0.3 - Math.atan(1.3 * basym / (2 * hw)) / RAD;
495
+ const y0 = sh.brow.y + g("browY") - (eyeS - 1) * sh.ry * 0.9 - 0.35 * basym;
496
+ const halves = (dy, len2 = 1) => [-1, 1].map((s2) => {
497
+ const [ox, oy] = turn(s2 * gap / 2, 0, slant);
498
+ const x0 = 170 + ox;
499
+ const yy = y0 + dy + oy;
500
+ const ang = slant + s2 * bt;
501
+ const [ux, uy] = turn(s2 * (hw * len2 - gap / 2), 0, ang);
502
+ const [nx, ny] = turn(0, th / 2, ang);
503
+ return [
504
+ [x0 - nx, yy - ny],
505
+ [x0 + ux - nx, yy + uy - ny],
506
+ [x0 + ux + nx, yy + uy + ny],
507
+ [x0 + nx, yy + ny]
508
+ ];
509
+ });
510
+ const inside = (dy, len2 = 1) => halves(dy, len2).every((q) => q.every(([x, y]) => depth(crown, x, y) + 2 < 0));
511
+ let lo = 0;
512
+ let hi = 0;
513
+ if (!inside(0)) {
514
+ hi = 60;
515
+ for (let k = 0;k < 12; k++) {
516
+ const mid2 = (lo + hi) / 2;
517
+ if (inside(mid2))
518
+ hi = mid2;
519
+ else
520
+ lo = mid2;
521
+ }
522
+ }
523
+ let len = 1;
524
+ if (!inside(hi)) {
525
+ let short = 0;
526
+ for (let k = 0;k < 12; k++) {
527
+ const mid2 = (short + len) / 2;
528
+ if (inside(hi, mid2))
529
+ short = mid2;
530
+ else
531
+ len = mid2;
532
+ }
533
+ len = short;
534
+ }
535
+ const bars = halves(hi, len).map((q, i) => n("path", `bar${i}`, { d: poly(q), fill: INK.ink }));
536
+ const m = sh.mouth;
537
+ const big = (who.face === "diamond" || who.face === "triangle" ? 1.25 : 1) * up;
538
+ const mw = m.w * Math.max(0.05, g("mw")) * big;
539
+ const mt = g("mt") * m.d * big;
540
+ const mb = g("mb") * m.d * big;
541
+ const mid = (mt + mb) / 2;
542
+ const kb = 4 / 3;
543
+ let d;
544
+ if (who.mouth === "poly") {
545
+ const s2 = Math.max(mw * 0.9, (mb - mt) / 2);
546
+ d = poly([
547
+ [-s2, mid],
548
+ [0, mid - s2],
549
+ [s2, mid],
550
+ [0, mid + s2]
551
+ ]);
552
+ } else if (who.mouth === "box") {
553
+ const h = Math.max(mb - mt, lineW * 1.6) / 2;
554
+ const w = mw * 0.85;
555
+ d = poly([
556
+ [-w, mid - h],
557
+ [w, mid - h],
558
+ [w, mid + h],
559
+ [-w, mid + h]
560
+ ]);
561
+ } else if (who.mouth === "line") {
562
+ const h = lineW * 1.4 / 2;
563
+ const w = mw * 1.5;
564
+ const y = mid * 0.5;
565
+ d = poly([
566
+ [-w, y - h],
567
+ [w, y - h],
568
+ [w, y + h],
569
+ [-w, y + h]
570
+ ]);
571
+ } else {
572
+ d = `M${f2(-mw)} 0C${f2(-mw)} ${f2(mb * kb)} ${f2(mw)} ${f2(mb * kb)} ${f2(mw)} 0C${f2(mw)} ${f2(mt * kb)} ${f2(-mw)} ${f2(mt * kb)} ${f2(-mw)} 0Z`;
573
+ }
574
+ const mouth = n("path", "mouth", {
575
+ d,
576
+ transform: `translate(${f2(m.x + g("mx"))} ${f2(m.y + g("my"))}) rotate(${f2(g("mk"))})`,
577
+ fill: INK.ink
578
+ });
579
+ const hair = g("hair");
580
+ const s = seat(crown, Math.max(0, hair) * 8, 110 + Math.max(0, -hair) * 40);
581
+ const shape = who.trait === "square" ? n("rect", "trait.shape", { x: -21, y: -21, width: 42, height: 42 }) : who.trait === "dot" ? n("circle", "trait.shape", { cx: 0, cy: 0, r: 22 }) : who.trait === "ring" ? n("circle", "trait.shape", {
582
+ cx: 0,
583
+ cy: 0,
584
+ r: 17.5,
585
+ fill: "none",
586
+ stroke: strong,
587
+ "stroke-width": 13
588
+ }) : who.trait === "fin" ? n("path", "trait.shape", { d: "M-27 27V-27A54 54 0 0 1 27 27Z" }) : n("rect", "trait.shape", {
589
+ x: -16,
590
+ y: -30,
591
+ width: 32,
592
+ height: 60
593
+ });
594
+ const trait = n("g", "trait", {
595
+ transform: `translate(${f2(s.x)} ${f2(s.y + pose.lag)}) scale(${f2(up)})`,
596
+ fill: strong
597
+ }, [shape]);
598
+ return [
599
+ [block("body", strong), block("body.light", light, `url(#${id}-r)`), trait],
600
+ [...eyes, ...bars, mouth]
601
+ ];
602
+ }
603
+
604
+ // src/identity.ts
605
+ var STYLE = "peek";
606
+ var tidy = (s) => s.normalize("NFC").trim().replace(/\s+/g, " ").toLowerCase();
607
+ function fnv1a(str) {
608
+ let h = 2166136261;
609
+ for (const b of new TextEncoder().encode(str)) {
610
+ h ^= b;
611
+ h = Math.imul(h, 16777619) >>> 0;
612
+ }
613
+ return h >>> 0;
614
+ }
615
+ function mulberry32(a) {
616
+ return () => {
617
+ a = a + 1831565813 | 0;
618
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
619
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
620
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
621
+ };
622
+ }
623
+ var LISTS = {
624
+ face: Object.keys(FACES),
625
+ color: Object.keys(COLORS),
626
+ ...PARTS,
627
+ ...ACCESSORIES
628
+ };
629
+ var AXES = Object.keys(LISTS);
630
+ function seed(name, axis, version = LATEST) {
631
+ return fnv1a(`${STYLE}@${version}:${axis}:${tidy(name)}`);
632
+ }
633
+ function identify(name, { version = LATEST } = {}) {
634
+ const lengths = VERSIONS[version];
635
+ if (!lengths)
636
+ throw new Error(`peek@${version} does not exist`);
637
+ const pick = (axis) => LISTS[axis][seed(name, axis, version) % lengths[axis]];
638
+ const rnd = mulberry32(seed(name, "persona", version));
639
+ const r = (a, b) => Math.round((a + rnd() * (b - a)) * 100) / 100;
640
+ const persona = {
641
+ spread: r(-7, 7),
642
+ blink: r(0.75, 1.4),
643
+ add: {
644
+ browY: r(-4, 4),
645
+ browTilt: r(-5, 5),
646
+ browArch: r(-0.2, 0.3),
647
+ rot: r(-3, 3),
648
+ hair: r(-0.2, 0.35),
649
+ gx: r(-0.12, 0.12),
650
+ my: r(-2, 3)
651
+ },
652
+ mul: {
653
+ eyeS: r(0.92, 1.08),
654
+ pupil: r(0.9, 1.12),
655
+ browW: r(0.85, 1.2),
656
+ mw: r(0.85, 1.18)
657
+ }
658
+ };
659
+ const key = tidy(name);
660
+ return {
661
+ key,
662
+ hash: fnv1a(key),
663
+ version,
664
+ face: pick("face"),
665
+ color: pick("color"),
666
+ eyes: pick("eyes"),
667
+ brows: pick("brows"),
668
+ mouth: pick("mouth"),
669
+ cheeks: pick("cheeks"),
670
+ trait: pick("trait"),
671
+ eyewear: pick("eyewear"),
672
+ headwear: pick("headwear"),
673
+ neckwear: pick("neckwear"),
674
+ persona
675
+ };
676
+ }
677
+
325
678
  // src/draw.ts
326
679
  var VB = [-30, -30, 400, 400];
327
680
  var FLOOR = 370;
@@ -333,8 +686,11 @@ var smooth = (a, b, v) => {
333
686
  };
334
687
  var f2 = (v) => String(Math.round(v * 100) / 100);
335
688
  var f3 = (v) => String(Math.round(v * 1000) / 1000);
336
- var strokeFor = (px) => px >= 96 ? 1 : px >= 56 ? 1.25 : px >= 40 ? 1.6 : px >= 30 ? 2 : 2.4;
337
- var RAD = Math.PI / 180;
689
+ var strokeFor2 = (px) => px >= 96 ? 1 : px >= 56 ? 1.25 : px >= 40 ? 1.6 : px >= 30 ? 2 : 2.4;
690
+ var RAD2 = Math.PI / 180;
691
+ var STEM = "M0 0Q3 -19 0 -36";
692
+ var LEAF0 = "M0 -33C-24 -30 -38 -52 -25 -53C-12 -54 -2 -43 0 -33Z";
693
+ var LEAF1 = "M0 -33C5 -54 28 -61 27 -48C26 -36 11 -31 0 -33Z";
338
694
  function frameOf(face) {
339
695
  const baseY = FLOOR + face.sink - face.bottom;
340
696
  const eyeY = face.eyes[0][1];
@@ -363,18 +719,18 @@ function seat(crown, d, u) {
363
719
  const at = ([x, y], b2) => ({
364
720
  x: x + R * Math.sin(b2),
365
721
  y: y - R * Math.cos(b2),
366
- a: b2 / RAD
722
+ a: b2 / RAD2
367
723
  });
368
724
  const P = crown.poly;
369
725
  if (P.length === 1)
370
726
  return at(P[0], R ? u / R : 0);
371
- const n = P.length;
727
+ const n2 = P.length;
372
728
  const dir = u >= 0 ? 1 : -1;
373
729
  let left = Math.abs(u);
374
730
  let i = 0;
375
731
  let b = 0;
376
- for (let step = 0;step < n; step++) {
377
- const j = (i + dir + n) % n;
732
+ for (let step = 0;step < n2; step++) {
733
+ const j = (i + dir + n2) % n2;
378
734
  const [x0, y0] = P[i];
379
735
  const [x1, y1] = P[j];
380
736
  const ex = x1 - x0;
@@ -394,7 +750,7 @@ function seat(crown, d, u) {
394
750
  return {
395
751
  x: x0 + ex / L * left + R * nx,
396
752
  y: y0 + ey / L * left + R * ny,
397
- a: be / RAD
753
+ a: be / RAD2
398
754
  };
399
755
  left -= L;
400
756
  i = j;
@@ -421,7 +777,7 @@ function depth(crown, x, y) {
421
777
  }
422
778
  return (out <= 0 ? out : near) - crown.r;
423
779
  }
424
- var RING_U = 158 * 35 * RAD;
780
+ var RING_U = 158 * 35 * RAD2;
425
781
  function ringWalk(sh) {
426
782
  const ex = sh.eyes[1][0] + sh.brow.hw + 7;
427
783
  const top = sh.brow.y - 30;
@@ -455,14 +811,11 @@ function prune(node) {
455
811
  }
456
812
  function draw(who, pose, o) {
457
813
  const sh = FACES[who.face];
458
- const { body: fill, deep } = COLORS[who.color];
459
814
  const P = who.persona;
460
815
  const mul = P.mul;
461
816
  const add = P.add;
462
817
  const g = (c) => pose[c] * (mul[c] ?? 1) + (add[c] ?? 0);
463
- const spread = P.spread;
464
818
  const id = o.id;
465
- const st = strokeFor(o.size);
466
819
  const { baseY, R } = frameOf(sh);
467
820
  const defs = [];
468
821
  const VBr = { x: VB[0], y: VB[1], width: VB[2], height: VB[3] };
@@ -556,7 +909,165 @@ function draw(who, pose, o) {
556
909
  const B = sh.bottom;
557
910
  const ty = baseY - alt * R;
558
911
  const sc = (v) => Math.round(v * 1e5) / 1e5;
559
- const bodyNode = "d" in sh.body ? n("path", "body", {
912
+ const [front, back] = (o.variant === "bloc" ? blocParts : peekParts)({
913
+ who,
914
+ pose,
915
+ o,
916
+ defs,
917
+ g
918
+ });
919
+ const off = (dx, dy) => riso ? `translate(${f2(dx * k * upp)} ${f2(dy * k * upp)})` : undefined;
920
+ const av = n("g", "av", {
921
+ transform: `translate(${f2(x)} ${f2(ty)}) rotate(${f2(rot)} 170 ${B}) translate(170 ${B}) scale(${sc(sx)} ${sc(sy)}) translate(-170 ${-B})`,
922
+ filter: dim > 0.01 ? `url(#${id}-dim)` : undefined
923
+ }, [
924
+ n("g", "plateA", {
925
+ transform: off(-0.55, 0.45),
926
+ filter: riso ? `url(#${id}-pA)` : undefined
927
+ }, front),
928
+ n("g", "plateB", {
929
+ transform: off(0.6, -0.5),
930
+ filter: riso ? `url(#${id}-pB)` : undefined
931
+ }, back)
932
+ ]);
933
+ const scene = [];
934
+ if (o.frame !== "none") {
935
+ const bg = o.frame === "ink" ? INK.ink : o.frame === "bone" ? INK.bone : INK.paper;
936
+ scene.push(n("rect", "bg", { ...VBr, fill: bg }));
937
+ const th = clamp(g("thing"), 0, 1);
938
+ const dots = [];
939
+ for (let j = 0;j < 3; j++)
940
+ for (let i = 0;i < 3; i++) {
941
+ const q = j * 3 + i;
942
+ const kk = smooth(q / 12, q / 12 + 0.35, th);
943
+ dots.push(n("circle", `lattice.${q}`, {
944
+ cx: 312 + i * 18,
945
+ cy: 2 + j * 18,
946
+ r: f2(1.2 + 2.2 * kk),
947
+ opacity: f3(kk * 0.55)
948
+ }));
949
+ }
950
+ scene.push(n("g", "lattice", { fill: o.frame === "ink" ? INK.bone : INK.ink }, dots));
951
+ }
952
+ scene.push(av);
953
+ if (grainy)
954
+ scene.push(n("rect", "grain", {
955
+ ...VBr,
956
+ filter: `url(#${id}-grain)`,
957
+ opacity: o.frame === "ink" ? 0.3 : 0.16,
958
+ "pointer-events": "none"
959
+ }));
960
+ const root = n("svg", "root", {
961
+ xmlns: "http://www.w3.org/2000/svg",
962
+ viewBox: VB.join(" "),
963
+ width: o.size,
964
+ height: o.size,
965
+ role: o.title === false ? undefined : "img",
966
+ "aria-label": o.title === false ? undefined : o.title,
967
+ "aria-hidden": o.title === false ? "true" : undefined
968
+ }, [
969
+ n("defs", "defs", {}, defs),
970
+ n("g", "frame", { "clip-path": `url(#${id}-f)` }, scene)
971
+ ]);
972
+ return o.live ? root : prune(root) ?? root;
973
+ }
974
+ var HAZE_LEVEL = {
975
+ medium: {
976
+ deep: [0.502, 0.224],
977
+ glow: [0.749, 0.337],
978
+ tint: [0.4, 0.5],
979
+ tintStops: [
980
+ [0, 0.878],
981
+ [0.15, 0.878],
982
+ [0.618, 0.396]
983
+ ]
984
+ },
985
+ light: {
986
+ deep: [0.298, 0.133],
987
+ glow: [0.549, 0.247],
988
+ tint: [0.31, 0.387],
989
+ tintStops: [
990
+ [0, 0.549],
991
+ [0.55, 0.247]
992
+ ]
993
+ }
994
+ };
995
+ function hazePaint(who, o) {
996
+ const sh = FACES[who.face];
997
+ const id = o.id;
998
+ const L = HAZE_LEVEL[o.size < 32 ? "light" : "medium"];
999
+ const xs = sh.crown.poly.map((p) => p[0]);
1000
+ const ys = sh.crown.poly.map((p) => p[1]);
1001
+ const x0 = Math.min(...xs) - sh.crown.r;
1002
+ const y0 = Math.min(...ys) - sh.crown.r;
1003
+ const W = Math.max(...xs) + sh.crown.r - x0;
1004
+ const H = sh.bottom - y0;
1005
+ const rnd = mulberry32(fnv1a(`haze:${who.key ?? ""}`));
1006
+ const warm = rnd() < 0.5;
1007
+ const left = rnd() < 0.5;
1008
+ const t = rnd();
1009
+ const tintX = 0.04 + 0.174 * t;
1010
+ const tintY = 0.649 + 0.202 * t;
1011
+ const glowX = 0.38 - 0.12 * rnd();
1012
+ const glowY = 0.12 + 0.099 * rnd();
1013
+ const deepX = 0.251 - 0.151 * rnd();
1014
+ const tintA = 90 - 180 * rnd();
1015
+ const at = (inset, onLeft) => x0 + W * (onLeft ? inset : 1 - inset);
1016
+ const i = WHEEL.indexOf(who.color);
1017
+ const near = WHEEL[(i + (warm ? WHEEL.length - 1 : 1)) % WHEEL.length];
1018
+ const { body, deep } = COLORS[who.color];
1019
+ const blob = (k, color, [cx, cy, a, rx, ry], stops) => n("radialGradient", `haze.${k}`, {
1020
+ id: `${id}-h${k[0]}`,
1021
+ gradientUnits: "userSpaceOnUse",
1022
+ cx: 0,
1023
+ cy: 0,
1024
+ r: 1,
1025
+ gradientTransform: `translate(${f2(cx)} ${f2(cy)}) rotate(${f2(a)}) scale(${f2(rx)} ${f2(ry)})`
1026
+ }, [...stops, [1, 0]].map(([at2, op], j) => n("stop", `haze.${k}.${j}`, {
1027
+ offset: at2,
1028
+ "stop-color": color,
1029
+ "stop-opacity": op
1030
+ })));
1031
+ const fade = ([a, b]) => [
1032
+ [0, a],
1033
+ [0.55, b]
1034
+ ];
1035
+ const blobs = [
1036
+ blob("deep", deep, [at(deepX, !left), y0 + H * 0.95, 0, W * 0.5, W * 0.35], fade(L.deep)),
1037
+ blob("tint", HAZE[near].tint, [at(tintX, left), y0 + H * tintY, tintA, W * L.tint[0], W * L.tint[1]], L.tintStops),
1038
+ blob("glow", HAZE[who.color].glow, [at(glowX, !left), y0 + H * glowY, 0, W * 0.475, W * 0.356], fade(L.glow))
1039
+ ];
1040
+ const shape = (key, paint) => ("d" in sh.body) ? n("path", key, {
1041
+ d: sh.body.d,
1042
+ fill: paint,
1043
+ stroke: sh.body.stroke ? paint : undefined,
1044
+ "stroke-width": sh.body.stroke,
1045
+ "stroke-linejoin": sh.body.stroke ? "round" : undefined
1046
+ }) : n("circle", key, {
1047
+ cx: sh.body.circle[0],
1048
+ cy: sh.body.circle[1],
1049
+ r: sh.body.circle[2],
1050
+ fill: paint
1051
+ });
1052
+ return [
1053
+ ...blobs,
1054
+ n("g", "haze", { id: `${id}-hz` }, [
1055
+ shape("haze.body", body),
1056
+ ...["deep", "tint", "glow"].map((k) => shape(`haze.${k}.fill`, `url(#${id}-h${k[0]})`))
1057
+ ])
1058
+ ];
1059
+ }
1060
+ function peekParts({ who, pose, o, defs, g }) {
1061
+ const sh = FACES[who.face];
1062
+ const { body: fill, deep } = COLORS[who.color];
1063
+ const P = who.persona;
1064
+ const spread = P.spread;
1065
+ const id = o.id;
1066
+ const st = strokeFor2(o.size);
1067
+ const haze = o.variant === "haze";
1068
+ if (haze)
1069
+ defs.push(...hazePaint(who, o));
1070
+ const bodyNode = haze ? n("use", "body", { href: `#${id}-hz` }) : ("d" in sh.body) ? n("path", "body", {
560
1071
  d: sh.body.d,
561
1072
  fill,
562
1073
  stroke: sh.body.stroke ? fill : undefined,
@@ -644,25 +1155,36 @@ function draw(who, pose, o) {
644
1155
  const lrx = rx * 1.55;
645
1156
  const lry = ry * 1.15;
646
1157
  const top = ry + 1.6 - lower * ry * 1.5;
1158
+ const [etx, ety] = bead ? [ex + side * spread + gx * 6, ey + gy * 6] : [ex + side * spread, ey];
1159
+ const lids = [
1160
+ n("rect", `${kk}.lid`, {
1161
+ x: f2(-rx * 2.2),
1162
+ y: f2(-ry * 3),
1163
+ width: f2(rx * 4.4),
1164
+ height: f2(Math.max(0, edge + ry * 3)),
1165
+ transform: `rotate(${f2(a)})`,
1166
+ fill
1167
+ }),
1168
+ n("ellipse", `${kk}.lower`, {
1169
+ cx: 0,
1170
+ cy: f2(top + lry),
1171
+ rx: f2(lrx),
1172
+ ry: f2(lry),
1173
+ fill
1174
+ })
1175
+ ];
1176
+ if (haze)
1177
+ defs.push(n("clipPath", `${kk}.lidshape`, { id: `${id}-s${i}` }, lids));
647
1178
  const parts = [
648
1179
  n("g", `${kk}.inner`, { "clip-path": `url(#${id}-e${i})` }, inner),
649
- n("g", `${kk}.lids`, { "clip-path": `url(#${id}-l${i})` }, [
650
- n("rect", `${kk}.lid`, {
651
- x: f2(-rx * 2.2),
652
- y: f2(-ry * 3),
653
- width: f2(rx * 4.4),
654
- height: f2(Math.max(0, edge + ry * 3)),
655
- transform: `rotate(${f2(a)})`,
656
- fill
657
- }),
658
- n("ellipse", `${kk}.lower`, {
659
- cx: 0,
660
- cy: f2(top + lry),
661
- rx: f2(lrx),
662
- ry: f2(lry),
663
- fill
664
- })
665
- ])
1180
+ n("g", `${kk}.lids`, { "clip-path": `url(#${id}-l${i})` }, haze ? [
1181
+ n("g", `${kk}.lidpaint`, { "clip-path": `url(#${id}-s${i})` }, [
1182
+ n("use", `${kk}.lidbody`, {
1183
+ href: `#${id}-hz`,
1184
+ transform: `translate(${f2(-etx)} ${f2(-ety)})`
1185
+ })
1186
+ ])
1187
+ ] : lids)
666
1188
  ];
667
1189
  if (who.eyes === "ring")
668
1190
  parts.push(n("g", `${kk}.edges`, {
@@ -696,9 +1218,7 @@ function draw(who, pose, o) {
696
1218
  "stroke-width": f2(sw * 0.85),
697
1219
  opacity: co > 0.01 ? f2(co) : 0
698
1220
  }));
699
- return n("g", kk, {
700
- transform: bead ? `translate(${f2(ex + side * spread + gx * 6)} ${f2(ey + gy * 6)})` : `translate(${f2(ex + side * spread)} ${ey})`
701
- }, parts);
1221
+ return n("g", kk, { transform: `translate(${f2(etx)} ${f2(ety)})` }, parts);
702
1222
  });
703
1223
  const browY = g("browY") - (eyeS - 1) * sh.ry * 0.9;
704
1224
  const btilt = g("browTilt");
@@ -709,7 +1229,7 @@ function draw(who, pose, o) {
709
1229
  const side = i === 0 ? -1 : 1;
710
1230
  const hw = sh.brow.hw;
711
1231
  const c = -sh.brow.arch * arch;
712
- 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];
1232
+ 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];
713
1233
  const ti = bw * 1.7;
714
1234
  const to = bw * 0.45;
715
1235
  const [tl, tr] = side < 0 ? [to, ti] : [ti, to];
@@ -718,11 +1238,11 @@ function draw(who, pose, o) {
718
1238
  const half = (t) => wedge ? hl + (hr - hl) * t : bw * k2 / 2;
719
1239
  const bx = ex + side * spread;
720
1240
  const by = sh.brow.y + browY + (i === 1 ? -basym : basym * 0.3);
721
- const cos = Math.cos(side * btilt * RAD);
722
- const sin = Math.sin(side * btilt * RAD);
1241
+ const cos = Math.cos(side * btilt * RAD2);
1242
+ const sin = Math.sin(side * btilt * RAD2);
723
1243
  const inside = (dy) => {
724
1244
  for (let t = 0;t <= 1; t += 0.25) {
725
- const lx = w * (2 * t - 1);
1245
+ const lx = w2 * (2 * t - 1);
726
1246
  const ly = 2 * t * (1 - t) * ca;
727
1247
  const px = bx + lx * cos - ly * sin;
728
1248
  const py = by + dy + lx * sin + ly * cos;
@@ -735,7 +1255,7 @@ function draw(who, pose, o) {
735
1255
  let hi = 0;
736
1256
  if (!inside(0)) {
737
1257
  hi = 40;
738
- for (let k3 = 0;k3 < 12; k3++) {
1258
+ for (let k = 0;k < 12; k++) {
739
1259
  const mid2 = (lo + hi) / 2;
740
1260
  if (inside(mid2))
741
1261
  hi = mid2;
@@ -757,7 +1277,7 @@ function draw(who, pose, o) {
757
1277
  });
758
1278
  }
759
1279
  return n("path", key, {
760
- d: `M${f2(-w)} 0Q0 ${f2(ca)} ${f2(w)} 0`,
1280
+ d: `M${f2(-w2)} 0Q0 ${f2(ca)} ${f2(w2)} 0`,
761
1281
  transform,
762
1282
  fill: "none",
763
1283
  stroke: INK.ink,
@@ -838,7 +1358,7 @@ function draw(who, pose, o) {
838
1358
  fill: deep
839
1359
  });
840
1360
  } else if (who.trait === "fin") {
841
- const s = seat(sh.crown, -6, -164 * 22 * RAD);
1361
+ const s = seat(sh.crown, -6, -164 * 22 * RAD2);
842
1362
  const r = s.a + (hair >= 0 ? hair * 20 : hair * 55) - lag * 0.8;
843
1363
  traitT = `translate(${f2(s.x)} ${f2(s.y + lag * 0.25)}) rotate(${f2(r)})`;
844
1364
  shape = n("path", "trait.shape", {
@@ -847,8 +1367,8 @@ function draw(who, pose, o) {
847
1367
  });
848
1368
  } else if (who.trait === "ring") {
849
1369
  const b = 35 - hair * 14 - (hair < 0 ? hair * -21 : 0) + lag * 0.9;
850
- const w = ringWalk(sh);
851
- const s = seat(sh.crown, 8, Math.max(w.min, w.rest + 158 * (b - 35) * RAD));
1370
+ const w2 = ringWalk(sh);
1371
+ const s = seat(sh.crown, 8, Math.max(w2.min, w2.rest + 158 * (b - 35) * RAD2));
852
1372
  traitT = `translate(${f2(s.x)} ${f2(s.y)}) scale(${f2(1 + Math.max(0, hair) * 0.1)})`;
853
1373
  shape = n("circle", "trait.shape", {
854
1374
  cx: 0,
@@ -860,7 +1380,7 @@ function draw(who, pose, o) {
860
1380
  });
861
1381
  } else if (who.trait === "dot") {
862
1382
  const s = clamp(-hair, 0, 1.2);
863
- const th = Math.min(1, s / 0.35) * 61.05 * RAD;
1383
+ const th = Math.min(1, s / 0.35) * 61.05 * RAD2;
864
1384
  const travel = Math.max(0, s - 0.35) / 0.65 * 92;
865
1385
  const p = seat(sh.crown, 12.5, 26.1 * th + travel);
866
1386
  let cy = p.y;
@@ -882,133 +1402,177 @@ function draw(who, pose, o) {
882
1402
  });
883
1403
  }
884
1404
  const trait = n("g", "trait", { transform: traitT }, [shape]);
885
- const off = (dx, dy) => riso ? `translate(${f2(dx * k * upp)} ${f2(dy * k * upp)})` : undefined;
886
- const av = n("g", "av", {
887
- transform: `translate(${f2(x)} ${f2(ty)}) rotate(${f2(rot)} 170 ${B}) translate(170 ${B}) scale(${sc(sx)} ${sc(sy)}) translate(-170 ${-B})`,
888
- filter: dim > 0.01 ? `url(#${id}-dim)` : undefined
889
- }, [
890
- n("g", "plateA", {
891
- transform: off(-0.55, 0.45),
892
- filter: riso ? `url(#${id}-pA)` : undefined
893
- }, [bodyNode, ...cheeks, trait]),
894
- n("g", "plateB", {
895
- transform: off(0.6, -0.5),
896
- filter: riso ? `url(#${id}-pB)` : undefined
897
- }, [...eyes, ...brows, mouth])
898
- ]);
899
- const scene = [];
900
- if (o.frame !== "none") {
901
- const bg = o.frame === "ink" ? INK.ink : o.frame === "bone" ? INK.bone : INK.paper;
902
- scene.push(n("rect", "bg", { ...VBr, fill: bg }));
903
- const th = clamp(g("thing"), 0, 1);
904
- const dots = [];
905
- for (let j = 0;j < 3; j++)
906
- for (let i = 0;i < 3; i++) {
907
- const q = j * 3 + i;
908
- const kk = smooth(q / 12, q / 12 + 0.35, th);
909
- dots.push(n("circle", `lattice.${q}`, {
910
- cx: 312 + i * 18,
911
- cy: 2 + j * 18,
912
- r: f2(1.2 + 2.2 * kk),
913
- opacity: f3(kk * 0.55)
1405
+ const wardrobe = [];
1406
+ const w = (px) => f2(px * Math.min(st, 1.6));
1407
+ const line = {
1408
+ fill: "none",
1409
+ stroke: INK.ink,
1410
+ "stroke-width": w(5.25),
1411
+ "stroke-linecap": "round",
1412
+ "stroke-linejoin": "round"
1413
+ };
1414
+ if (who.eyewear !== "none") {
1415
+ const lx = sh.eyes[0][0] - spread;
1416
+ const rr = sh.eyes[1][0] + spread;
1417
+ const ey = sh.eyes[0][1];
1418
+ const gap = rr - lx;
1419
+ const erx = Math.min(rx + 9, (gap - 15) / 2);
1420
+ const ery = ry + 9;
1421
+ const glasses = who.eyewear === "glasses";
1422
+ const lensX = glasses ? erx : Math.min(rx + 11, (gap - 12) / 2);
1423
+ const top = glasses ? ey - 4 : ey - ry - 2;
1424
+ const frames = [
1425
+ n("path", "eyewear.bridge", {
1426
+ ...line,
1427
+ d: `M${f2(lx + lensX)} ${f2(top)}Q${f2((lx + rr) / 2)} ${f2(top - (glasses ? 13 : 0))} ${f2(rr - lensX)} ${f2(top)}`
1428
+ })
1429
+ ];
1430
+ for (const [i, cx] of [lx, rr].entries()) {
1431
+ const key = `eyewear.lens${i}`;
1432
+ frames.push(n("path", `${key}.arm`, {
1433
+ ...line,
1434
+ 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)}`
1435
+ }));
1436
+ if (glasses) {
1437
+ frames.push(n("ellipse", key, {
1438
+ ...line,
1439
+ cx: f2(cx),
1440
+ cy: ey,
1441
+ rx: f2(erx),
1442
+ ry: f2(ery)
1443
+ }));
1444
+ frames.push(n("path", `${key}.glint`, {
1445
+ ...line,
1446
+ stroke: INK.paper,
1447
+ "stroke-width": w(3),
1448
+ 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)}`
1449
+ }));
1450
+ } else {
1451
+ const h = ry * 2 + 9;
1452
+ frames.push(n("path", key, {
1453
+ 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`,
1454
+ fill: INK.ink,
1455
+ stroke: INK.ink,
1456
+ "stroke-width": w(3),
1457
+ "stroke-linejoin": "round"
1458
+ }));
1459
+ frames.push(n("path", `${key}.glint`, {
1460
+ ...line,
1461
+ stroke: INK.paper,
1462
+ "stroke-width": w(4),
1463
+ d: `M${f2(cx - lensX * 0.58)} ${f2(top + 19)}l8 -8m-1 15 6 -6`
1464
+ }), n("circle", `${key}.pin`, {
1465
+ cx: f2(cx + lensX * 0.55),
1466
+ cy: f2(top + h - 9),
1467
+ r: 1.7,
1468
+ fill: INK.paper
914
1469
  }));
915
1470
  }
916
- scene.push(n("g", "lattice", { fill: o.frame === "ink" ? INK.bone : INK.ink }, dots));
917
- }
918
- scene.push(av);
919
- if (grainy)
920
- scene.push(n("rect", "grain", {
921
- ...VBr,
922
- filter: `url(#${id}-grain)`,
923
- opacity: o.frame === "ink" ? 0.3 : 0.16,
924
- "pointer-events": "none"
925
- }));
926
- const root = n("svg", "root", {
927
- xmlns: "http://www.w3.org/2000/svg",
928
- viewBox: VB.join(" "),
929
- width: o.size,
930
- height: o.size,
931
- role: o.title === false ? undefined : "img",
932
- "aria-label": o.title === false ? undefined : o.title,
933
- "aria-hidden": o.title === false ? "true" : undefined
934
- }, [
935
- n("defs", "defs", {}, defs),
936
- n("g", "frame", { "clip-path": `url(#${id}-f)` }, scene)
937
- ]);
938
- return o.live ? root : prune(root) ?? root;
939
- }
940
-
941
- // src/identity.ts
942
- var STYLE = "peek";
943
- var tidy = (s) => s.normalize("NFC").trim().replace(/\s+/g, " ").toLowerCase();
944
- function fnv1a(str) {
945
- let h = 2166136261;
946
- for (const b of new TextEncoder().encode(str)) {
947
- h ^= b;
948
- h = Math.imul(h, 16777619) >>> 0;
1471
+ }
1472
+ wardrobe.push(n("g", "eyewear", {}, frames));
949
1473
  }
950
- return h >>> 0;
951
- }
952
- function mulberry32(a) {
953
- return () => {
954
- a = a + 1831565813 | 0;
955
- let t = Math.imul(a ^ a >>> 15, 1 | a);
956
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
957
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
958
- };
959
- }
960
- var LISTS = {
961
- face: Object.keys(FACES),
962
- color: Object.keys(COLORS),
963
- ...PARTS
964
- };
965
- var AXES = Object.keys(LISTS);
966
- function seed(name, axis, version = LATEST) {
967
- return fnv1a(`${STYLE}@${version}:${axis}:${tidy(name)}`);
968
- }
969
- function identify(name, { version = LATEST } = {}) {
970
- const lengths = VERSIONS[version];
971
- if (!lengths)
972
- throw new Error(`peek@${version} does not exist`);
973
- const pick = (axis) => LISTS[axis][seed(name, axis, version) % lengths[axis]];
974
- const rnd = mulberry32(seed(name, "persona", version));
975
- const r = (a, b) => Math.round((a + rnd() * (b - a)) * 100) / 100;
976
- const persona = {
977
- spread: r(-7, 7),
978
- blink: r(0.75, 1.4),
979
- add: {
980
- browY: r(-4, 4),
981
- browTilt: r(-5, 5),
982
- browArch: r(-0.2, 0.3),
983
- rot: r(-3, 3),
984
- hair: r(-0.2, 0.35),
985
- gx: r(-0.12, 0.12),
986
- my: r(-2, 3)
987
- },
988
- mul: {
989
- eyeS: r(0.92, 1.08),
990
- pupil: r(0.9, 1.12),
991
- browW: r(0.85, 1.2),
992
- mw: r(0.85, 1.18)
1474
+ if (who.headwear !== "none") {
1475
+ const side = who.trait === "ring" || who.trait === "dot" ? -1 : 1;
1476
+ const cap = who.headwear === "cap";
1477
+ const sprout = who.headwear === "sprout";
1478
+ const inset = sprout ? -6 : cap ? -8 : 0;
1479
+ const offset = sprout ? 34 : 40;
1480
+ let lo = 0;
1481
+ let hi = 160;
1482
+ for (let i = 0;i < 16; i++) {
1483
+ const mid2 = (lo + hi) / 2;
1484
+ const p = seat(sh.crown, inset, side * mid2);
1485
+ if (side * (p.x - 170) < offset)
1486
+ lo = mid2;
1487
+ else
1488
+ hi = mid2;
993
1489
  }
994
- };
995
- const key = tidy(name);
996
- return {
997
- key,
998
- hash: fnv1a(key),
999
- version,
1000
- face: pick("face"),
1001
- color: pick("color"),
1002
- eyes: pick("eyes"),
1003
- brows: pick("brows"),
1004
- mouth: pick("mouth"),
1005
- cheeks: pick("cheeks"),
1006
- trait: pick("trait"),
1007
- persona
1008
- };
1490
+ const anchor = seat(sh.crown, inset, side * (lo + hi) / 2);
1491
+ const angle = clamp(anchor.a, -40, 40);
1492
+ const parts = [];
1493
+ if (cap) {
1494
+ parts.push(n("path", "headwear.crown", {
1495
+ ...line,
1496
+ fill: INK.paper,
1497
+ d: "M-33 0C-35 -20 -20 -39 0 -39C20 -39 35 -20 33 0Z"
1498
+ }), n("path", "headwear.seam", {
1499
+ ...line,
1500
+ "stroke-width": w(3.5),
1501
+ d: "M-4 -38Q9 -24 8 -1"
1502
+ }), n("path", "headwear.brim", {
1503
+ ...line,
1504
+ "stroke-width": w(7),
1505
+ d: side > 0 ? "M-35 1Q8 4 52 3" : "M-52 3Q-8 4 35 1"
1506
+ }), n("circle", "headwear.button", {
1507
+ cx: 0,
1508
+ cy: -40,
1509
+ r: 3.5,
1510
+ fill: INK.ink
1511
+ }));
1512
+ } else if (sprout) {
1513
+ parts.push(n("path", "headwear.halo", {
1514
+ ...line,
1515
+ stroke: fill,
1516
+ "stroke-width": w(11),
1517
+ d: `${STEM}${LEAF0}${LEAF1}`
1518
+ }), n("path", "headwear.stem", {
1519
+ ...line,
1520
+ "stroke-width": w(4.5),
1521
+ d: STEM
1522
+ }), n("path", "headwear.leaf0", {
1523
+ ...line,
1524
+ "stroke-width": w(4),
1525
+ fill: COLORS.mint.body,
1526
+ d: LEAF0
1527
+ }), n("path", "headwear.leaf1", {
1528
+ ...line,
1529
+ "stroke-width": w(4),
1530
+ fill: COLORS.mint.deep,
1531
+ d: LEAF1
1532
+ }));
1533
+ } else {
1534
+ parts.push(n("path", "headwear.wings", {
1535
+ 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",
1536
+ fill: INK.ink,
1537
+ stroke: fill,
1538
+ "stroke-width": w(6),
1539
+ "stroke-linejoin": "round",
1540
+ "paint-order": "stroke"
1541
+ }), n("ellipse", "headwear.knot", {
1542
+ cx: 0,
1543
+ cy: 1,
1544
+ rx: 6,
1545
+ ry: 8,
1546
+ fill: ACCESSORY_INK.knot
1547
+ }));
1548
+ }
1549
+ wardrobe.push(n("g", "headwear", {
1550
+ transform: `translate(${f2(anchor.x)} ${f2(anchor.y)}) rotate(${f2(sprout ? angle * 0.5 : angle)})`
1551
+ }, parts));
1552
+ }
1553
+ if (who.neckwear === "tie") {
1554
+ const skew = Math.abs(Math.sin(g("mk") * RAD2)) * mw;
1555
+ const bottom = Math.max(0, mt * kb, mb * kb) + skew + m.sw * st;
1556
+ wardrobe.push(n("g", "neckwear", {
1557
+ transform: `translate(${m.x} ${f2(m.y + g("my") + bottom + 8)})`,
1558
+ fill: INK.ink
1559
+ }, [
1560
+ n("path", "neckwear.knot", {
1561
+ d: "M-9 0Q-11 0 -9 4L-5 11Q0 15 5 11L9 4Q11 0 9 0Z"
1562
+ }),
1563
+ n("path", "neckwear.blade", {
1564
+ d: "M-4 15L-13 63Q-14 66 -11 69L0 79L11 69Q14 66 13 63L4 15Z"
1565
+ })
1566
+ ]));
1567
+ }
1568
+ return [
1569
+ [bodyNode, ...cheeks, trait],
1570
+ [...eyes, ...brows, mouth, ...wardrobe]
1571
+ ];
1009
1572
  }
1010
1573
 
1011
1574
  // src/svg.ts
1575
+ var wear = (o, base, s) => ACCESSORIES[s].includes(o[s]) ? o[s] : base[s];
1012
1576
  function settle(name, o, live = false) {
1013
1577
  const base = identify(name, { version: o.version });
1014
1578
  const who = {
@@ -1019,17 +1583,24 @@ function settle(name, o, live = false) {
1019
1583
  brows: o.brows ?? base.brows,
1020
1584
  mouth: o.mouth ?? base.mouth,
1021
1585
  cheeks: o.cheeks ?? base.cheeks,
1022
- trait: o.trait ?? base.trait
1586
+ trait: o.trait ?? base.trait,
1587
+ eyewear: wear(o, base, "eyewear"),
1588
+ headwear: wear(o, base, "headwear"),
1589
+ neckwear: wear(o, base, "neckwear")
1023
1590
  };
1024
1591
  const pose = restPose(who, o.expression ?? "normal", o.gaze);
1592
+ const variant = VARIANTS.includes(o.variant) ? o.variant : "peek";
1025
1593
  const look = {
1026
1594
  size: o.size ?? 64,
1027
1595
  frame: o.frame ?? "ink",
1028
1596
  square: o.square ?? true,
1029
1597
  riso: o.riso ?? false,
1030
- live
1598
+ live,
1599
+ ...variant !== "peek" && { variant }
1031
1600
  };
1032
- const id = o.id ?? `peek-${fnv1a(JSON.stringify([who, pose, look])).toString(36)}`;
1601
+ const { eyewear, headwear, neckwear, ...legacy } = who;
1602
+ const wardrobe = eyewear !== "none" || headwear !== "none" || neckwear !== "none";
1603
+ const id = o.id ?? `peek-${fnv1a(JSON.stringify([wardrobe ? who : legacy, pose, look])).toString(36)}`;
1033
1604
  return { who, pose, opts: { ...look, id, title: o.title ?? name } };
1034
1605
  }
1035
1606
  var ILLEGAL = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;