@usenavii/core 0.4.0 → 0.5.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/index.js CHANGED
@@ -187,6 +187,63 @@ var ANCHORS = {
187
187
  groundY: 94,
188
188
  cheekY: 53,
189
189
  cheekOffset: 12
190
+ },
191
+ // Squircle — FULL-BLEED. Body fills 0-100, face composed as ID portrait:
192
+ // upper-third eye line, lower-third mouth, generous cheek/eye spread.
193
+ squircle: {
194
+ cx: 50,
195
+ eyeY: 44,
196
+ eyeOffset: 13,
197
+ eyeScale: 1,
198
+ mouthY: 62,
199
+ mouthSpan: 8,
200
+ topperX: 50,
201
+ topperY: 8,
202
+ groundY: 98,
203
+ cheekY: 52,
204
+ cheekOffset: 24
205
+ },
206
+ // Pumpkin — round body w/ slight horizontal lobing. Face mid-low, stem on top.
207
+ pumpkin: {
208
+ cx: 50,
209
+ eyeY: 52,
210
+ eyeOffset: 11,
211
+ eyeScale: 1,
212
+ mouthY: 66,
213
+ mouthSpan: 9,
214
+ topperX: 50,
215
+ topperY: 18,
216
+ groundY: 88,
217
+ cheekY: 60,
218
+ cheekOffset: 20
219
+ },
220
+ // Ghost — tall wavy silhouette. Face high (under the "hood"), wavy hem below.
221
+ ghost: {
222
+ cx: 50,
223
+ eyeY: 42,
224
+ eyeOffset: 8,
225
+ eyeScale: 1.05,
226
+ mouthY: 54,
227
+ mouthSpan: 6,
228
+ topperX: 50,
229
+ topperY: 12,
230
+ groundY: 92,
231
+ cheekY: 50,
232
+ cheekOffset: 14
233
+ },
234
+ // SkullHead — slightly elongated egg w/ deep eye sockets.
235
+ skullHead: {
236
+ cx: 50,
237
+ eyeY: 50,
238
+ eyeOffset: 10,
239
+ eyeScale: 1,
240
+ mouthY: 70,
241
+ mouthSpan: 7,
242
+ topperX: 50,
243
+ topperY: 16,
244
+ groundY: 90,
245
+ cheekY: 60,
246
+ cheekOffset: 16
190
247
  }
191
248
  };
192
249
 
@@ -207,27 +264,49 @@ var BODY_PATHS = {
207
264
  // Taro — gourd shape: small head bulge, fuller bottom
208
265
  taro: "M50 14 C58 14 64 22 64 30 C64 36 60 40 60 46 C60 54 76 60 78 76 C80 88 66 91 50 91 C34 91 20 88 22 76 C24 60 40 54 40 46 C40 40 36 36 36 30 C36 22 42 14 50 14 Z",
209
266
  // Wisp — tall narrow body, slight bottom flare, ghost-like
210
- wisp: "M50 12 C60 12 66 24 66 40 C66 60 74 78 70 90 C64 96 36 96 30 90 C26 78 34 60 34 40 C34 24 40 12 50 12 Z"
267
+ wisp: "M50 12 C60 12 66 24 66 40 C66 60 74 78 70 90 C64 96 36 96 30 90 C26 78 34 60 34 40 C34 24 40 12 50 12 Z",
268
+ // Squircle — FULL-BLEED corporate plate. Fills entire viewport with tight
269
+ // corner radius (~4px). Reads as a tile / ID photo not a contained avatar.
270
+ // Pairs with `flat: true` packs (Office) — face floats on a wall of color.
271
+ squircle: "M4 0 C2 0 0 2 0 4 C0 35 0 65 0 96 C0 98 2 100 4 100 C35 100 65 100 96 100 C98 100 100 98 100 96 C100 65 100 35 100 4 C100 2 98 0 96 0 C65 0 35 0 4 0 Z",
272
+ // Pumpkin — round, slightly wider than tall, with subtle horizontal "lobes"
273
+ // for the iconic carved-pumpkin gourd shape. Stem area kept clear at top.
274
+ pumpkin: "M50 18 C70 18 86 30 86 52 C86 74 70 88 50 88 C30 88 14 74 14 52 C14 30 30 18 50 18 Z",
275
+ // Ghost — soft rounded top with wavy bottom hem (3 humps), evoking a sheet.
276
+ ghost: "M50 12 C68 12 78 24 78 42 C78 60 80 76 80 88 L74 84 L68 90 L62 84 L56 90 L50 84 L44 90 L38 84 L32 90 L26 84 L20 88 C20 76 22 60 22 42 C22 24 32 12 50 12 Z",
277
+ // SkullHead — egg-ish shape, slight pinch at jaw for skull silhouette.
278
+ skullHead: "M50 16 C68 16 80 30 80 50 C80 64 76 72 70 78 L68 86 L60 88 L60 82 L40 82 L40 88 L32 86 L30 78 C24 72 20 64 20 50 C20 30 32 16 50 16 Z"
211
279
  };
212
- function renderBodyDefs(_id, _palette, gradId) {
280
+ function renderBodyDefs(_id, palette, gradId, opts) {
281
+ if (opts?.flat) {
282
+ return `<radialGradient id="${gradId}"><stop offset="0%" stop-color="${palette.bodyFrom}" /><stop offset="100%" stop-color="${palette.bodyFrom}" /></radialGradient>`;
283
+ }
213
284
  return `
214
285
  <radialGradient id="${gradId}" cx="42%" cy="32%" r="68%">
215
- <stop offset="0%" stop-color="${_palette.bodyFrom}" />
216
- <stop offset="100%" stop-color="${_palette.bodyTo}" />
286
+ <stop offset="0%" stop-color="${palette.bodyFrom}" />
287
+ <stop offset="100%" stop-color="${palette.bodyTo}" />
217
288
  </radialGradient>`.trim();
218
289
  }
219
- function renderBody(id, palette, gradId) {
290
+ function renderBody(id, palette, gradId, opts) {
220
291
  const d = BODY_PATHS[id];
221
292
  const a = ANCHORS[id];
293
+ const flat = opts?.flat === true;
222
294
  const outlineColor = withAlpha(palette.ink, 0.18);
223
- return [
224
- // Ground shadow — soft ellipse just below body, grounds the figure
225
- `<ellipse cx="${a.cx}" cy="${a.groundY + 4}" rx="22" ry="2.6" fill="${palette.ink}" opacity="0.16" />`,
226
- // Body fill
227
- `<path d="${d}" fill="url(#${gradId})" stroke="${outlineColor}" stroke-width="0.7" />`,
228
- // Sheen — small light spot upper-left, scaled to body
229
- `<ellipse cx="${a.cx - 12}" cy="${a.eyeY - 14}" rx="11" ry="7" fill="#FFFFFF" opacity="0.22" transform="rotate(-18 ${a.cx - 12} ${a.eyeY - 14})" />`
230
- ].join("");
295
+ const parts = [];
296
+ if (!flat) {
297
+ parts.push(
298
+ `<ellipse cx="${a.cx}" cy="${a.groundY + 4}" rx="22" ry="2.6" fill="${palette.ink}" opacity="0.16" />`
299
+ );
300
+ }
301
+ parts.push(
302
+ flat ? `<path d="${d}" fill="url(#${gradId})" />` : `<path d="${d}" fill="url(#${gradId})" stroke="${outlineColor}" stroke-width="0.7" />`
303
+ );
304
+ if (!flat) {
305
+ parts.push(
306
+ `<ellipse cx="${a.cx - 12}" cy="${a.eyeY - 14}" rx="11" ry="7" fill="#FFFFFF" opacity="0.22" transform="rotate(-18 ${a.cx - 12} ${a.eyeY - 14})" />`
307
+ );
308
+ }
309
+ return parts.join("");
231
310
  }
232
311
  function withAlpha(hex, alpha) {
233
312
  const h = hex.replace("#", "");
@@ -240,12 +319,13 @@ function withAlpha(hex, alpha) {
240
319
  }
241
320
 
242
321
  // src/parts/eyes.ts
243
- function renderEyes(id, palette, anchor) {
322
+ function renderEyes(id, palette, anchor, opts) {
244
323
  const lx = anchor.cx - anchor.eyeOffset;
245
324
  const rx = anchor.cx + anchor.eyeOffset;
246
325
  const y = anchor.eyeY;
247
326
  const s = anchor.eyeScale;
248
327
  const ink = palette.ink;
328
+ const sw = opts?.strokeMul ?? 1;
249
329
  switch (id) {
250
330
  case "round":
251
331
  return [
@@ -267,24 +347,24 @@ function renderEyes(id, palette, anchor) {
267
347
  ].join("");
268
348
  case "squint":
269
349
  return [
270
- arc(lx - 4.5, y, lx, y - 3.5, lx + 4.5, y, ink, 1.8),
271
- arc(rx - 4.5, y, rx, y - 3.5, rx + 4.5, y, ink, 1.8)
350
+ arc(lx - 4.5, y, lx, y - 3.5, lx + 4.5, y, ink, 1.8 * sw),
351
+ arc(rx - 4.5, y, rx, y - 3.5, rx + 4.5, y, ink, 1.8 * sw)
272
352
  ].join("");
273
353
  case "wink":
274
354
  return [
275
355
  sclera(lx, y, 4 * s, 4.5 * s),
276
356
  pupil(lx, y, 2.2 * s, ink),
277
357
  glint(lx + 1, y - 1),
278
- arc(rx - 4, y, rx, y - 3.5, rx + 4, y, ink, 1.8)
358
+ arc(rx - 4, y, rx, y - 3.5, rx + 4, y, ink, 1.8 * sw)
279
359
  ].join("");
280
360
  case "sleepy":
281
361
  return [
282
362
  // Heavier upper lid — half-closed
283
- `<path d="M${lx - 4} ${y - 0.5} Q${lx} ${y + 2} ${lx + 4} ${y - 0.5}" stroke="${ink}" stroke-width="1.7" stroke-linecap="round" fill="none" />`,
284
- `<path d="M${rx - 4} ${y - 0.5} Q${rx} ${y + 2} ${rx + 4} ${y - 0.5}" stroke="${ink}" stroke-width="1.7" stroke-linecap="round" fill="none" />`,
363
+ `<path d="M${lx - 4} ${y - 0.5} Q${lx} ${y + 2} ${lx + 4} ${y - 0.5}" stroke="${ink}" stroke-width="${1.7 * sw}" stroke-linecap="round" fill="none" />`,
364
+ `<path d="M${rx - 4} ${y - 0.5} Q${rx} ${y + 2} ${rx + 4} ${y - 0.5}" stroke="${ink}" stroke-width="${1.7 * sw}" stroke-linecap="round" fill="none" />`,
285
365
  // tiny visible pupils
286
- `<circle cx="${lx}" cy="${y + 0.5}" r="0.9" fill="${ink}" />`,
287
- `<circle cx="${rx}" cy="${y + 0.5}" r="0.9" fill="${ink}" />`
366
+ `<circle cx="${lx}" cy="${y + 0.5}" r="${0.9 * sw}" fill="${ink}" />`,
367
+ `<circle cx="${rx}" cy="${y + 0.5}" r="${0.9 * sw}" fill="${ink}" />`
288
368
  ].join("");
289
369
  case "star":
290
370
  return [starEye(lx, y, ink), starEye(rx, y, ink)].join("");
@@ -301,20 +381,20 @@ function renderEyes(id, palette, anchor) {
301
381
  ].join("");
302
382
  case "dot":
303
383
  return [
304
- `<circle cx="${lx}" cy="${y}" r="${1.4 * s}" fill="${ink}" />`,
305
- `<circle cx="${rx}" cy="${y}" r="${1.4 * s}" fill="${ink}" />`
384
+ `<circle cx="${lx}" cy="${y}" r="${1.4 * s * sw}" fill="${ink}" />`,
385
+ `<circle cx="${rx}" cy="${y}" r="${1.4 * s * sw}" fill="${ink}" />`
306
386
  ].join("");
307
387
  case "cross":
308
- return [crossEye(lx, y, ink), crossEye(rx, y, ink)].join("");
388
+ return [crossEye(lx, y, ink, sw), crossEye(rx, y, ink, sw)].join("");
309
389
  }
310
390
  }
311
391
  function heartEye(cx, cy, color) {
312
392
  const s = 2;
313
393
  return `<path d="M${cx} ${cy + s * 1.4} L${cx - s * 1.8} ${cy - s * 0.2} A${s} ${s} 0 0 1 ${cx} ${cy - s * 0.6} A${s} ${s} 0 0 1 ${cx + s * 1.8} ${cy - s * 0.2} Z" fill="${color}" />`;
314
394
  }
315
- function crossEye(cx, cy, color) {
395
+ function crossEye(cx, cy, color, sw = 1) {
316
396
  const s = 2.4;
317
- return `<g stroke="${color}" stroke-width="1.6" stroke-linecap="round"><line x1="${cx - s}" y1="${cy - s}" x2="${cx + s}" y2="${cy + s}" /><line x1="${cx - s}" y1="${cy + s}" x2="${cx + s}" y2="${cy - s}" /></g>`;
397
+ return `<g stroke="${color}" stroke-width="${1.6 * sw}" stroke-linecap="round"><line x1="${cx - s}" y1="${cy - s}" x2="${cx + s}" y2="${cy + s}" /><line x1="${cx - s}" y1="${cy + s}" x2="${cx + s}" y2="${cy - s}" /></g>`;
318
398
  }
319
399
  function sclera(cx, cy, rx, ry) {
320
400
  return `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" fill="#FFFFFF" />`;
@@ -334,41 +414,68 @@ function starEye(cx, cy, color) {
334
414
  }
335
415
 
336
416
  // src/parts/mouth.ts
337
- function renderMouth(id, palette, anchor, curveScale = 1) {
417
+ function renderMouth(id, palette, anchor, curveScale = 1, opts) {
338
418
  const cx = anchor.cx;
339
419
  const y = anchor.mouthY;
340
420
  const w = anchor.mouthSpan * curveScale;
341
421
  const ink = palette.ink;
422
+ const sw = opts?.strokeMul ?? 1;
423
+ const base = 1.8 * sw;
342
424
  switch (id) {
343
425
  case "smile":
344
- return `<path d="M${cx - w} ${y} Q${cx} ${y + 5} ${cx + w} ${y}" stroke="${ink}" stroke-width="1.8" stroke-linecap="round" fill="none" />`;
426
+ return `<path d="M${cx - w} ${y} Q${cx} ${y + 5} ${cx + w} ${y}" stroke="${ink}" stroke-width="${base}" stroke-linecap="round" fill="none" />`;
345
427
  case "grin":
346
- return `<path d="M${cx - w - 1} ${y - 2} Q${cx} ${y + 7} ${cx + w + 1} ${y - 2}" stroke="${ink}" stroke-width="1.8" stroke-linecap="round" fill="none" />`;
428
+ return `<path d="M${cx - w - 1} ${y - 2} Q${cx} ${y + 7} ${cx + w + 1} ${y - 2}" stroke="${ink}" stroke-width="${base}" stroke-linecap="round" fill="none" />`;
347
429
  case "open":
348
430
  return [
349
- `<path d="M${cx - w - 1} ${y - 2} Q${cx} ${y + 9} ${cx + w + 1} ${y - 2}" stroke="${ink}" stroke-width="1.8" stroke-linecap="round" fill="${ink}" fill-opacity="0.55" />`,
431
+ `<path d="M${cx - w - 1} ${y - 2} Q${cx} ${y + 9} ${cx + w + 1} ${y - 2}" stroke="${ink}" stroke-width="${base}" stroke-linecap="round" fill="${ink}" fill-opacity="0.55" />`,
350
432
  `<ellipse cx="${cx}" cy="${y + 3}" rx="${w * 0.55}" ry="1.8" fill="#F472B6" opacity="0.75" />`
351
433
  ].join("");
352
434
  case "flat":
353
- return `<path d="M${cx - w + 1} ${y} L${cx + w - 1} ${y}" stroke="${ink}" stroke-width="1.8" stroke-linecap="round" fill="none" />`;
435
+ return `<path d="M${cx - w + 1} ${y} L${cx + w - 1} ${y}" stroke="${ink}" stroke-width="${base}" stroke-linecap="round" fill="none" />`;
354
436
  case "smirk":
355
- return `<path d="M${cx - w} ${y} Q${cx} ${y + 3} ${cx + w + 1} ${y - 2}" stroke="${ink}" stroke-width="1.8" stroke-linecap="round" fill="none" />`;
437
+ return `<path d="M${cx - w} ${y} Q${cx} ${y + 3} ${cx + w + 1} ${y - 2}" stroke="${ink}" stroke-width="${base}" stroke-linecap="round" fill="none" />`;
356
438
  case "awe":
357
439
  return `<ellipse cx="${cx}" cy="${y + 1}" rx="${w * 0.45}" ry="3.2" fill="${ink}" opacity="0.85" />`;
358
440
  case "tongue":
359
441
  return [
360
- `<path d="M${cx - w} ${y} Q${cx} ${y + 6} ${cx + w} ${y}" stroke="${ink}" stroke-width="1.8" stroke-linecap="round" fill="none" />`,
361
- `<path d="M${cx - 2} ${y + 4} Q${cx} ${y + 9} ${cx + 2} ${y + 4} Z" fill="#F472B6" stroke="${ink}" stroke-width="0.6" />`
442
+ `<path d="M${cx - w} ${y} Q${cx} ${y + 6} ${cx + w} ${y}" stroke="${ink}" stroke-width="${base}" stroke-linecap="round" fill="none" />`,
443
+ `<path d="M${cx - 2} ${y + 4} Q${cx} ${y + 9} ${cx + 2} ${y + 4} Z" fill="#F472B6" stroke="${ink}" stroke-width="${0.6 * sw}" />`
362
444
  ].join("");
363
445
  case "tooth":
364
446
  return [
365
- `<path d="M${cx - w} ${y} Q${cx} ${y + 5} ${cx + w} ${y}" stroke="${ink}" stroke-width="1.8" stroke-linecap="round" fill="none" />`,
366
- `<rect x="${cx - 1.2}" y="${y + 0.4}" width="2.4" height="2.6" rx="0.4" fill="#FFFFFF" stroke="${ink}" stroke-width="0.4" />`
447
+ `<path d="M${cx - w} ${y} Q${cx} ${y + 5} ${cx + w} ${y}" stroke="${ink}" stroke-width="${base}" stroke-linecap="round" fill="none" />`,
448
+ `<rect x="${cx - 1.2}" y="${y + 0.4}" width="2.4" height="2.6" rx="0.4" fill="#FFFFFF" stroke="${ink}" stroke-width="${0.4 * sw}" />`
367
449
  ].join("");
368
450
  case "wave":
369
- return `<path d="M${cx - w} ${y + 1} Q${cx - w / 2} ${y - 1.5} ${cx} ${y + 1} Q${cx + w / 2} ${y + 3.5} ${cx + w} ${y + 1}" stroke="${ink}" stroke-width="1.8" stroke-linecap="round" fill="none" />`;
451
+ return `<path d="M${cx - w} ${y + 1} Q${cx - w / 2} ${y - 1.5} ${cx} ${y + 1} Q${cx + w / 2} ${y + 3.5} ${cx + w} ${y + 1}" stroke="${ink}" stroke-width="${base}" stroke-linecap="round" fill="none" />`;
370
452
  case "dot":
371
- return `<circle cx="${cx}" cy="${y + 1}" r="1.2" fill="${ink}" />`;
453
+ return `<circle cx="${cx}" cy="${y + 1}" r="${1.2 * sw}" fill="${ink}" />`;
454
+ case "jagged": {
455
+ const half = w + 1;
456
+ const top = y - 1;
457
+ const bot = y + 5;
458
+ const step = half * 2 / 8;
459
+ const x0 = cx - half;
460
+ const points = [];
461
+ points.push(`${x0} ${top}`);
462
+ for (let i = 1; i <= 8; i++) {
463
+ const px = x0 + step * i;
464
+ const py = i % 2 === 1 ? bot : top;
465
+ points.push(`${px.toFixed(2)} ${py}`);
466
+ }
467
+ return `<path d="M${points.join(" L")} L${cx + half} ${top} Z" fill="${ink}" />`;
468
+ }
469
+ case "fangs": {
470
+ const half = w - 1;
471
+ return [
472
+ `<path d="M${cx - half} ${y} L${cx + half} ${y}" stroke="${ink}" stroke-width="${base}" stroke-linecap="round" fill="none" />`,
473
+ // Left fang
474
+ `<path d="M${cx - 2.5} ${y + 0.4} L${cx - 1.2} ${y + 4.5} L${cx - 0.2} ${y + 0.4} Z" fill="#FFFFFF" stroke="${ink}" stroke-width="${0.5 * sw}" />`,
475
+ // Right fang
476
+ `<path d="M${cx + 0.2} ${y + 0.4} L${cx + 1.2} ${y + 4.5} L${cx + 2.5} ${y + 0.4} Z" fill="#FFFFFF" stroke="${ink}" stroke-width="${0.5 * sw}" />`
477
+ ].join("");
478
+ }
372
479
  }
373
480
  }
374
481
 
@@ -405,7 +512,8 @@ function renderAntenna(id, anchor, palette) {
405
512
  }
406
513
 
407
514
  // src/parts/accessory.ts
408
- function renderAccessory(id, palette, anchor) {
515
+ function renderAccessory(id, palette, anchor, opts) {
516
+ const sw = opts?.strokeMul ?? 1;
409
517
  switch (id) {
410
518
  case "none":
411
519
  return "";
@@ -440,10 +548,11 @@ function renderAccessory(id, palette, anchor) {
440
548
  const rx = anchor.cx + anchor.eyeOffset;
441
549
  const y = anchor.eyeY;
442
550
  const r = 6;
551
+ const gw = 1.2 * sw;
443
552
  return [
444
- `<circle cx="${lx}" cy="${y}" r="${r}" fill="none" stroke="${palette.ink}" stroke-width="1.2" />`,
445
- `<circle cx="${rx}" cy="${y}" r="${r}" fill="none" stroke="${palette.ink}" stroke-width="1.2" />`,
446
- `<line x1="${lx + r}" y1="${y}" x2="${rx - r}" y2="${y}" stroke="${palette.ink}" stroke-width="1.2" />`,
553
+ `<circle cx="${lx}" cy="${y}" r="${r}" fill="none" stroke="${palette.ink}" stroke-width="${gw}" />`,
554
+ `<circle cx="${rx}" cy="${y}" r="${r}" fill="none" stroke="${palette.ink}" stroke-width="${gw}" />`,
555
+ `<line x1="${lx + r}" y1="${y}" x2="${rx - r}" y2="${y}" stroke="${palette.ink}" stroke-width="${gw}" />`,
447
556
  // subtle lens fill
448
557
  `<circle cx="${lx}" cy="${y}" r="${r - 1}" fill="#FFFFFF" opacity="0.18" />`,
449
558
  `<circle cx="${rx}" cy="${y}" r="${r - 1}" fill="#FFFFFF" opacity="0.18" />`
@@ -461,6 +570,20 @@ function renderAccessory(id, palette, anchor) {
461
570
  case "mole": {
462
571
  return `<circle cx="${anchor.cx - anchor.cheekOffset * 0.6}" cy="${anchor.cheekY + 2}" r="0.9" fill="${palette.ink}" />`;
463
572
  }
573
+ case "earring": {
574
+ const ex = anchor.cheekOffset + 4;
575
+ const ey = anchor.cheekY + 4;
576
+ const lx = anchor.cx - ex;
577
+ const rx = anchor.cx + ex;
578
+ return [
579
+ // Left earring — small stud + drop
580
+ `<circle cx="${lx}" cy="${ey}" r="${1.1 * sw}" fill="${palette.accent}" stroke="${palette.ink}" stroke-width="${0.4 * sw}" />`,
581
+ `<ellipse cx="${lx}" cy="${ey + 3.2}" rx="${1.3 * sw}" ry="${2 * sw}" fill="${palette.accent}" stroke="${palette.ink}" stroke-width="${0.4 * sw}" />`,
582
+ // Right earring
583
+ `<circle cx="${rx}" cy="${ey}" r="${1.1 * sw}" fill="${palette.accent}" stroke="${palette.ink}" stroke-width="${0.4 * sw}" />`,
584
+ `<ellipse cx="${rx}" cy="${ey + 3.2}" rx="${1.3 * sw}" ry="${2 * sw}" fill="${palette.accent}" stroke="${palette.ink}" stroke-width="${0.4 * sw}" />`
585
+ ].join("");
586
+ }
464
587
  }
465
588
  function dot(cx, cy, color) {
466
589
  return `<circle cx="${cx}" cy="${cy}" r="0.85" fill="${color}" opacity="0.55" />`;
@@ -554,6 +677,84 @@ function renderTopper(id, anchor, palette) {
554
677
  `<path d="M${cx - 5} ${topY + 4} L${cx - 6} ${topY - 4} M${cx - 6} ${topY - 4} L${cx - 10} ${topY - 6} M${cx - 6} ${topY - 4} L${cx - 6} ${topY - 9} M${cx - 6} ${topY - 9} L${cx - 8} ${topY - 11} M${cx - 6} ${topY - 9} L${cx - 3} ${topY - 11}" stroke="${ink}" stroke-width="1.3" stroke-linecap="round" fill="none" />`,
555
678
  `<path d="M${cx + 5} ${topY + 4} L${cx + 6} ${topY - 4} M${cx + 6} ${topY - 4} L${cx + 10} ${topY - 6} M${cx + 6} ${topY - 4} L${cx + 6} ${topY - 9} M${cx + 6} ${topY - 9} L${cx + 8} ${topY - 11} M${cx + 6} ${topY - 9} L${cx + 3} ${topY - 11}" stroke="${ink}" stroke-width="1.3" stroke-linecap="round" fill="none" />`
556
679
  ].join("");
680
+ case "bob": {
681
+ const eyeY = anchor.eyeY;
682
+ const tt = topY - 4;
683
+ const bt = eyeY + 6;
684
+ return [
685
+ // Main hair cap — slightly asymmetric for soft look
686
+ `<path d="M${cx - 24} ${bt} Q${cx - 26} ${eyeY - 4} ${cx - 22} ${tt + 4} Q${cx - 14} ${tt - 2} ${cx} ${tt - 3} Q${cx + 14} ${tt - 2} ${cx + 22} ${tt + 4} Q${cx + 26} ${eyeY - 4} ${cx + 24} ${bt} Q${cx + 18} ${eyeY + 2} ${cx + 14} ${eyeY - 2} Q${cx} ${eyeY - 8} ${cx - 14} ${eyeY - 2} Q${cx - 18} ${eyeY + 2} ${cx - 24} ${bt} Z" fill="${ink}" opacity="0.92" />`,
687
+ // Subtle highlight strand
688
+ `<path d="M${cx - 14} ${tt + 4} Q${cx - 6} ${tt + 2} ${cx + 2} ${tt + 6}" stroke="${palette.accent}" stroke-width="0.6" fill="none" opacity="0.25" />`
689
+ ].join("");
690
+ }
691
+ case "bun": {
692
+ const baseY = topY + 4;
693
+ const bunY = topY - 8;
694
+ return [
695
+ // Hair base on crown
696
+ `<path d="M${cx - 16} ${baseY} Q${cx} ${topY - 4} ${cx + 16} ${baseY} Q${cx + 12} ${baseY - 4} ${cx} ${baseY - 6} Q${cx - 12} ${baseY - 4} ${cx - 16} ${baseY} Z" fill="${ink}" opacity="0.92" />`,
697
+ // Bun disc
698
+ `<ellipse cx="${cx}" cy="${bunY}" rx="6" ry="5" fill="${ink}" opacity="0.95" />`,
699
+ // Bun wrap detail
700
+ `<ellipse cx="${cx}" cy="${bunY - 0.5}" rx="3.5" ry="2.5" fill="none" stroke="${palette.accent}" stroke-width="0.4" opacity="0.4" />`
701
+ ].join("");
702
+ }
703
+ case "witchHat": {
704
+ const tipY = topY - 26;
705
+ const baseY = topY + 2;
706
+ return [
707
+ // Cone — slight curve, tilts right
708
+ `<path d="M${cx - 14} ${baseY} Q${cx - 4} ${baseY - 10} ${cx + 4} ${tipY} Q${cx + 2} ${baseY - 4} ${cx + 14} ${baseY} Z" fill="${ink}" opacity="0.96" />`,
709
+ // Brim — wide flat oval w/ slight curve
710
+ `<ellipse cx="${cx}" cy="${baseY + 2}" rx="22" ry="3.4" fill="${ink}" opacity="0.96" />`,
711
+ // Band across cone base
712
+ `<rect x="${cx - 14}" y="${baseY - 4}" width="28" height="3" fill="${palette.accent}" opacity="0.85" />`,
713
+ // Buckle
714
+ `<rect x="${cx - 2}" y="${baseY - 4}" width="4" height="3" fill="${palette.bodyFrom}" stroke="${ink}" stroke-width="0.4" />`,
715
+ // Star/moon sparkle near tip
716
+ `<circle cx="${cx + 2}" cy="${tipY + 6}" r="0.9" fill="${palette.accent}" opacity="0.9" />`
717
+ ].join("");
718
+ }
719
+ case "pumpkinStem": {
720
+ return [
721
+ // Main stem — slightly curved
722
+ `<path d="M${cx - 2} ${topY + 4} Q${cx} ${topY - 2} ${cx + 1} ${topY - 8} L${cx + 3} ${topY - 8} Q${cx + 4} ${topY} ${cx + 2} ${topY + 4} Z" fill="#3F6F2C" stroke="${ink}" stroke-width="0.5" />`,
723
+ // Leaf curling off
724
+ `<path d="M${cx + 3} ${topY - 4} Q${cx + 9} ${topY - 8} ${cx + 12} ${topY - 4} Q${cx + 8} ${topY - 2} ${cx + 3} ${topY - 2} Z" fill="#4A8035" stroke="${ink}" stroke-width="0.4" />`,
725
+ // Vein on leaf
726
+ `<path d="M${cx + 5} ${topY - 3} L${cx + 11} ${topY - 5}" stroke="#2D5020" stroke-width="0.4" />`
727
+ ].join("");
728
+ }
729
+ case "ghostSheet": {
730
+ return [
731
+ // Sheet cap — wider than body, hangs lower at sides
732
+ `<path d="M${cx - 22} ${topY + 8} Q${cx - 26} ${topY - 4} ${cx - 14} ${topY - 10} Q${cx} ${topY - 14} ${cx + 14} ${topY - 10} Q${cx + 26} ${topY - 4} ${cx + 22} ${topY + 8} Q${cx + 12} ${topY + 4} ${cx} ${topY + 6} Q${cx - 12} ${topY + 4} ${cx - 22} ${topY + 8} Z" fill="${palette.accent}" stroke="${ink}" stroke-width="0.6" opacity="0.9" />`,
733
+ // Fold shadows
734
+ `<path d="M${cx - 12} ${topY - 6} Q${cx - 10} ${topY - 2} ${cx - 14} ${topY + 4}" stroke="${ink}" stroke-width="0.45" fill="none" opacity="0.35" />`,
735
+ `<path d="M${cx + 12} ${topY - 6} Q${cx + 10} ${topY - 2} ${cx + 14} ${topY + 4}" stroke="${ink}" stroke-width="0.45" fill="none" opacity="0.35" />`
736
+ ].join("");
737
+ }
738
+ case "ponytail": {
739
+ const eyeY = anchor.eyeY;
740
+ const fh = eyeY - 7;
741
+ const crownY = topY;
742
+ const baseX = cx + 18;
743
+ const baseY = crownY + 6;
744
+ return [
745
+ // Sleek hair cap — narrower than bob, hugs the crown, soft hairline.
746
+ `<path d="M${cx - 22} ${fh} Q${cx - 24} ${crownY - 2} ${cx - 12} ${crownY - 4} L${cx + 14} ${crownY - 4} Q${cx + 24} ${crownY} ${cx + 22} ${fh} Q${cx + 10} ${fh - 1} ${cx} ${fh + 2} Q${cx - 10} ${fh - 1} ${cx - 22} ${fh} Z" fill="${ink}" opacity="0.94" />`,
747
+ // Subtle highlight sweeping back toward the tie
748
+ `<path d="M${cx - 12} ${crownY - 2} Q${cx} ${crownY - 3} ${baseX - 2} ${baseY - 2}" stroke="${palette.accent}" stroke-width="0.5" fill="none" opacity="0.3" />`,
749
+ // Ponytail tie — small ring where the hair gathers
750
+ `<ellipse cx="${baseX}" cy="${baseY}" rx="3" ry="2.4" fill="${ink}" opacity="0.95" />`,
751
+ `<ellipse cx="${baseX}" cy="${baseY}" rx="1.4" ry="1.1" fill="${palette.accent}" opacity="0.32" />`,
752
+ // Tail — long tapered strand curving down and slightly out
753
+ `<path d="M${baseX - 1} ${baseY + 2} Q${baseX + 5} ${baseY + 10} ${baseX + 8} ${baseY + 20} Q${baseX + 9} ${baseY + 28} ${baseX + 4} ${baseY + 30} Q${baseX + 1} ${baseY + 22} ${baseX - 3} ${baseY + 12} Z" fill="${ink}" opacity="0.92" />`,
754
+ // Inner highlight following the tail's flow direction
755
+ `<path d="M${baseX + 2} ${baseY + 6} Q${baseX + 5} ${baseY + 16} ${baseX + 6} ${baseY + 24}" stroke="${palette.accent}" stroke-width="0.5" fill="none" opacity="0.25" />`
756
+ ].join("");
757
+ }
557
758
  }
558
759
  }
559
760
 
@@ -624,6 +825,20 @@ function renderOutfit(id, anchor, palette) {
624
825
  `<circle cx="${cx}" cy="${cy + 7}" r="1.6" fill="${accent}" stroke="${ink}" stroke-width="0.5" />`,
625
826
  `<circle cx="${cx}" cy="${cy + 7}" r="0.7" fill="${palette.blush}" />`
626
827
  ].join("");
828
+ case "tie": {
829
+ const knotTop = cy - 3;
830
+ const knotBot = cy + 1;
831
+ return [
832
+ // Shirt-collar peek behind the tie (so tie reads as worn over a shirt)
833
+ `<path d="M${cx - 11} ${cy - 2} L${cx - 3} ${knotBot} L${cx + 3} ${knotBot} L${cx + 11} ${cy - 2} L${cx + 6} ${cy + 6} L${cx - 6} ${cy + 6} Z" fill="${accent}" stroke="${ink}" stroke-width="0.55" />`,
834
+ // Knot — small trapezoid centered
835
+ `<path d="M${cx - 3.2} ${knotTop} L${cx + 3.2} ${knotTop} L${cx + 2.4} ${knotBot} L${cx - 2.4} ${knotBot} Z" fill="${palette.bodyTo}" stroke="${ink}" stroke-width="0.5" />`,
836
+ // Blade — narrower at top, widens, then pointed tip at bottom
837
+ `<path d="M${cx - 2.4} ${knotBot} L${cx + 2.4} ${knotBot} L${cx + 3.4} ${cy + 6} L${cx + 2.8} ${cy + 12} L${cx} ${cy + 15} L${cx - 2.8} ${cy + 12} L${cx - 3.4} ${cy + 6} Z" fill="${palette.bodyTo}" stroke="${ink}" stroke-width="0.5" />`,
838
+ // Subtle highlight stripe down the blade
839
+ `<path d="M${cx} ${knotBot + 0.5} L${cx} ${cy + 13.5}" stroke="${ink}" stroke-width="0.35" opacity="0.35" />`
840
+ ].join("");
841
+ }
627
842
  }
628
843
  }
629
844
 
@@ -637,6 +852,9 @@ var BODY_IDS = [
637
852
  "dumpling",
638
853
  "taro",
639
854
  "wisp"
855
+ // NOTE: 'squircle' is intentionally NOT in the base pool — pack-only body so
856
+ // existing seeds keep their original picks (no determinism shift). Packs can
857
+ // opt-in via `picks.body: ['squircle']`.
640
858
  ];
641
859
  var EYE_IDS = [
642
860
  "round",
@@ -690,31 +908,550 @@ var TOPPER_IDS = [
690
908
  "antlers"
691
909
  ];
692
910
 
911
+ // src/packs/office.ts
912
+ var palettes = [
913
+ // Near-grayscale with cool blue undertone
914
+ { id: "office:graphite", bodyFrom: "#989BA2", bodyTo: "#696D72", accent: "#FFFFFF", ink: "#1A1C1F", blush: "#A09D99" },
915
+ // Warm stone — barely tinted neutral
916
+ { id: "office:stone", bodyFrom: "#A5A29D", bodyTo: "#74726F", accent: "#FFFFFF", ink: "#1C1B19", blush: "#A8A5A0" },
917
+ // Muted sage — barely-green grey
918
+ { id: "office:sage", bodyFrom: "#9EA299", bodyTo: "#6F726B", accent: "#FFFFFF", ink: "#1B1D19", blush: "#A3A099" },
919
+ // Taupe — desaturated warm grey
920
+ { id: "office:taupe", bodyFrom: "#ACA098", bodyTo: "#78716C", accent: "#FFFFFF", ink: "#1E1B18", blush: "#AFA59C" },
921
+ // Cool slate — neutral grey with hint of blue
922
+ { id: "office:slate", bodyFrom: "#9DA2AA", bodyTo: "#6C7077", accent: "#FFFFFF", ink: "#1A1C20", blush: "#A0A2A6" }
923
+ ];
924
+ var officePack = {
925
+ id: "office",
926
+ name: "Office",
927
+ description: "Corporate ID-badge. Flat, muted, square silhouette. Mix of professional outfits + hairstyles.",
928
+ emoji: "\u{1F4BC}",
929
+ palettes,
930
+ paletteExclusive: true,
931
+ flat: true,
932
+ bgColor: "#FFFFFF",
933
+ featureStroke: 1.35,
934
+ picks: {
935
+ body: ["squircle"],
936
+ eyes: ["dot", "sleepy", "squint"],
937
+ mouth: ["flat", "dot"],
938
+ antenna: ["none"],
939
+ // Gender + style variety comes from accessory + outfit mix, NOT hair toppers
940
+ // (hair rendered as solid silhouettes reads as helmets at small sizes).
941
+ accessory: ["none", "glasses", "mole", "freckles", "earring", "earring"],
942
+ topper: ["none"],
943
+ background: ["solid"],
944
+ // Outfit mix — tie reads masc, necklace/bowtie/collar neutral-or-femme. No scarf (too bulky).
945
+ outfit: ["tie", "necklace", "collar", "bowtie"]
946
+ },
947
+ styleHints: {
948
+ masc: {
949
+ outfit: ["tie", "bowtie"],
950
+ accessory: ["none", "glasses", "mole", "freckles"]
951
+ },
952
+ femme: {
953
+ outfit: ["necklace"],
954
+ accessory: ["earring", "glasses", "mole"]
955
+ },
956
+ neutral: {
957
+ outfit: ["collar", "bowtie", "necklace"],
958
+ accessory: ["none", "glasses", "freckles"]
959
+ }
960
+ }
961
+ };
962
+
963
+ // src/packs/office-bright.ts
964
+ var palettes2 = [
965
+ // Cobalt — vibrant deep blue
966
+ { id: "office-bright:cobalt", bodyFrom: "#4F8DFF", bodyTo: "#1E40AF", accent: "#FFFFFF", ink: "#0A1638", blush: "#7BA7FF" },
967
+ // Emerald — vivid green
968
+ { id: "office-bright:emerald", bodyFrom: "#3CCB85", bodyTo: "#047857", accent: "#FFFFFF", ink: "#062418", blush: "#76DAA8" },
969
+ // Fuchsia — hot magenta-pink
970
+ { id: "office-bright:fuchsia", bodyFrom: "#F0529C", bodyTo: "#BE185D", accent: "#FFFFFF", ink: "#3A0820", blush: "#F58FBC" },
971
+ // Amber — vivid orange-yellow
972
+ { id: "office-bright:amber", bodyFrom: "#FFB347", bodyTo: "#C2700B", accent: "#FFFFFF", ink: "#3B1F03", blush: "#FFC97A" },
973
+ // Violet — saturated purple
974
+ { id: "office-bright:violet", bodyFrom: "#9F7AEA", bodyTo: "#6D28D9", accent: "#FFFFFF", ink: "#1A0A3A", blush: "#BFA0F0" }
975
+ ];
976
+ var officeBrightPack = {
977
+ id: "office-bright",
978
+ name: "Office Bright",
979
+ description: "Vivid corporate variant of Office. Saturated brand-style palettes, same flat ID-badge composition.",
980
+ emoji: "\u{1F3A8}",
981
+ palettes: palettes2,
982
+ paletteExclusive: true,
983
+ flat: true,
984
+ bgColor: "#FFFFFF",
985
+ featureStroke: 1.35,
986
+ picks: {
987
+ body: ["squircle"],
988
+ eyes: ["dot", "sleepy", "squint"],
989
+ mouth: ["flat", "dot"],
990
+ antenna: ["none"],
991
+ accessory: ["none", "glasses", "mole", "freckles", "earring", "earring"],
992
+ topper: ["none"],
993
+ background: ["solid"],
994
+ outfit: ["tie", "necklace", "collar", "bowtie"]
995
+ },
996
+ styleHints: {
997
+ masc: {
998
+ outfit: ["tie", "bowtie"],
999
+ accessory: ["none", "glasses", "mole", "freckles"]
1000
+ },
1001
+ femme: {
1002
+ outfit: ["necklace"],
1003
+ accessory: ["earring", "glasses", "mole"]
1004
+ },
1005
+ neutral: {
1006
+ outfit: ["collar", "bowtie", "necklace"],
1007
+ accessory: ["none", "glasses", "freckles"]
1008
+ }
1009
+ }
1010
+ };
1011
+
1012
+ // src/packs/halloween.ts
1013
+ var palettes3 = [
1014
+ // Vivid pumpkin orange — high saturation against dark plate
1015
+ { id: "halloween:pumpkin", bodyFrom: "#FF8C2A", bodyTo: "#D85A00", accent: "#FFE9C4", ink: "#1A0700", blush: "#FFA76A" },
1016
+ // Electric witch purple
1017
+ { id: "halloween:witch", bodyFrom: "#B266FF", bodyTo: "#6A1FB8", accent: "#F0D9FF", ink: "#0E031F", blush: "#D6A0FF" },
1018
+ // Acid slime — radioactive Halloween green
1019
+ { id: "halloween:slime", bodyFrom: "#B3F23A", bodyTo: "#5E9F00", accent: "#E8FFB8", ink: "#091300", blush: "#D8FF7C" },
1020
+ // Blood crimson — deep saturated red
1021
+ { id: "halloween:blood", bodyFrom: "#E83248", bodyTo: "#8E0014", accent: "#FFD6DA", ink: "#1F0004", blush: "#FF8A98" },
1022
+ // Bone — pale warm white, ghost-like
1023
+ { id: "halloween:bone", bodyFrom: "#F5EEDB", bodyTo: "#C8B89A", accent: "#FFFEF5", ink: "#1A1612", blush: "#E8D9B8" }
1024
+ ];
1025
+ var halloweenPack = {
1026
+ id: "halloween",
1027
+ name: "Halloween",
1028
+ description: "Spooky themed pack. Pumpkin/ghost/skull bodies, jagged grins, witch hats, dark night plate.",
1029
+ emoji: "\u{1F383}",
1030
+ palettes: palettes3,
1031
+ paletteExclusive: true,
1032
+ flat: true,
1033
+ bgColor: "#0E0A1A",
1034
+ // deep night-purple
1035
+ featureStroke: 1.4,
1036
+ picks: {
1037
+ body: ["pumpkin", "ghost", "skullHead"],
1038
+ eyes: ["cross", "dot", "sleepy", "wide"],
1039
+ mouth: ["jagged", "fangs", "flat", "dot"],
1040
+ antenna: ["none"],
1041
+ accessory: ["none", "sparkle", "eyepatch", "mole"],
1042
+ topper: ["none", "witchHat", "pumpkinStem", "ghostSheet", "horns", "antlers"],
1043
+ background: ["solid"],
1044
+ outfit: ["none", "collar", "scarf"]
1045
+ },
1046
+ styleHints: {
1047
+ // 'masc' → creepy / skeletal vibe
1048
+ masc: {
1049
+ outfit: ["none", "collar"],
1050
+ accessory: ["none", "eyepatch"],
1051
+ topper: ["horns", "antlers", "witchHat"]
1052
+ },
1053
+ // 'femme' → witchy / sparkly vibe
1054
+ femme: {
1055
+ outfit: ["none", "scarf"],
1056
+ accessory: ["sparkle", "mole"],
1057
+ topper: ["witchHat", "ghostSheet", "pumpkinStem"]
1058
+ },
1059
+ // 'neutral' → balanced classic Halloween — pumpkin or none topper
1060
+ neutral: {
1061
+ outfit: ["none"],
1062
+ accessory: ["none", "sparkle"],
1063
+ topper: ["none", "witchHat", "pumpkinStem"]
1064
+ }
1065
+ }
1066
+ };
1067
+
1068
+ // src/packs/pastel.ts
1069
+ var palettes4 = [
1070
+ // Cotton candy — soft pink, the most kawaii signal
1071
+ { id: "pastel:cotton-candy", bodyFrom: "#FFB4D4", bodyTo: "#FF7AAB", accent: "#FFF0F6", ink: "#5E1C40", blush: "#FFC6DC" },
1072
+ // Butter — warm pastel yellow
1073
+ { id: "pastel:butter", bodyFrom: "#FFE07F", bodyTo: "#E6B12E", accent: "#FFFBE6", ink: "#5C4810", blush: "#FFE5A0" },
1074
+ // Mint cream — soft green
1075
+ { id: "pastel:mint-cream", bodyFrom: "#A8E9C8", bodyTo: "#5BC499", accent: "#EFFEF5", ink: "#194433", blush: "#C2EED4" },
1076
+ // Lavender — gentle purple
1077
+ { id: "pastel:lavender", bodyFrom: "#C8B5F0", bodyTo: "#9275D4", accent: "#F6F0FE", ink: "#321F58", blush: "#D8C6F0" },
1078
+ // Peach bloom — warm coral peach
1079
+ { id: "pastel:peach-bloom", bodyFrom: "#FFC1A0", bodyTo: "#F08F6A", accent: "#FFF1E8", ink: "#6E2F12", blush: "#FFC6AC" },
1080
+ // Sky drop — pastel baby blue
1081
+ { id: "pastel:sky-drop", bodyFrom: "#A8D9F5", bodyTo: "#6BB1E0", accent: "#EFF8FE", ink: "#15324A", blush: "#BCDDF0" }
1082
+ ];
1083
+ var pastelPack = {
1084
+ id: "pastel",
1085
+ name: "Pastel",
1086
+ description: "Kawaii cozy companion. Round chubby bodies, big eyes, blushy cheeks, soft cream plate. Finch-like.",
1087
+ emoji: "\u{1F338}",
1088
+ palettes: palettes4,
1089
+ paletteExclusive: true,
1090
+ flat: true,
1091
+ bgColor: "#FBF6F0",
1092
+ // warm cream plate
1093
+ featureStroke: 1.3,
1094
+ picks: {
1095
+ body: ["dumpling", "orb", "pebble"],
1096
+ // Big expressive eyes — wide/round read as kawaii, sleepy/wink soften it.
1097
+ // Drop oval (too anime) + cross (too aggressive) for cohesion.
1098
+ eyes: ["round", "wide", "sleepy", "wink", "heart", "star"],
1099
+ // Tiny mouths — smile/dot/tongue. No grin/awe (too loud).
1100
+ mouth: ["smile", "dot", "tongue", "smirk"],
1101
+ antenna: ["none", "classic", "curl"],
1102
+ // Blush weighted heavy — every pastel avatar gets blushy by default
1103
+ accessory: ["blush", "blush", "blush", "freckles", "sparkle", "none"],
1104
+ // Round ears / headband / halo / tuft give character variety
1105
+ topper: ["none", "roundEars", "ears", "headband", "halo", "tuft"],
1106
+ background: ["solid"],
1107
+ // Outfit stays minimal — collar gives "shirt" peek
1108
+ outfit: ["none", "collar", "sunflower", "necklace"]
1109
+ },
1110
+ styleHints: {
1111
+ // 'masc' → softer kawaii: minimal toppers, dot/sleepy eyes
1112
+ masc: {
1113
+ eyes: ["round", "sleepy", "wink"],
1114
+ accessory: ["blush", "freckles", "none"],
1115
+ topper: ["none", "tuft", "cap"],
1116
+ outfit: ["none", "collar"]
1117
+ },
1118
+ // 'femme' → full kawaii: heart/star eyes, ears+halo, sparkles
1119
+ femme: {
1120
+ eyes: ["heart", "star", "wide"],
1121
+ accessory: ["blush", "sparkle"],
1122
+ topper: ["roundEars", "ears", "halo", "headband"],
1123
+ outfit: ["none", "sunflower", "necklace"]
1124
+ },
1125
+ // 'neutral' → balanced
1126
+ neutral: {
1127
+ eyes: ["round", "wide", "sleepy"],
1128
+ accessory: ["blush", "freckles"],
1129
+ topper: ["none", "roundEars", "tuft"],
1130
+ outfit: ["none", "collar"]
1131
+ }
1132
+ }
1133
+ };
1134
+
1135
+ // src/packs/neon.ts
1136
+ var palettes5 = [
1137
+ // Hot pink — synthwave signature
1138
+ { id: "neon:pink", bodyFrom: "#FF6FD8", bodyTo: "#D6168C", accent: "#FFE5F5", ink: "#1F0214", blush: "#FFB3DD" },
1139
+ // Acid lime — radioactive green
1140
+ { id: "neon:lime", bodyFrom: "#D9FF3D", bodyTo: "#5FB800", accent: "#F5FFD0", ink: "#0F1500", blush: "#E5FF8A" },
1141
+ // Electric cyan — Tron blue
1142
+ { id: "neon:cyan", bodyFrom: "#4DEFFF", bodyTo: "#0099C9", accent: "#D6FBFF", ink: "#00141C", blush: "#A5EEFA" },
1143
+ // Sodium — bright street-lamp yellow-orange
1144
+ { id: "neon:sodium", bodyFrom: "#FFEC4D", bodyTo: "#E59700", accent: "#FFFADB", ink: "#221400", blush: "#FFE082" },
1145
+ // Magenta violet — saturated purple
1146
+ { id: "neon:violet", bodyFrom: "#C66DFF", bodyTo: "#6B14D6", accent: "#EFDDFF", ink: "#0F0024", blush: "#D8B0FF" },
1147
+ // Plasma orange-red — fiery
1148
+ { id: "neon:plasma", bodyFrom: "#FF6A3D", bodyTo: "#C42500", accent: "#FFD9CC", ink: "#1F0500", blush: "#FFA98A" }
1149
+ ];
1150
+ var neonPack = {
1151
+ id: "neon",
1152
+ name: "Neon",
1153
+ description: "Cyberpunk gaming PFP. Vivid neon bodies on a dark plate with soft glow halo. Sharp + intense.",
1154
+ emoji: "\u26A1",
1155
+ palettes: palettes5,
1156
+ paletteExclusive: true,
1157
+ flat: true,
1158
+ bgColor: "#0A0A14",
1159
+ // deep night plate
1160
+ featureStroke: 1.5,
1161
+ glow: true,
1162
+ picks: {
1163
+ body: ["tall", "wisp", "taro"],
1164
+ eyes: ["star", "wide", "cross", "dot"],
1165
+ mouth: ["grin", "awe", "tooth", "smirk"],
1166
+ antenna: ["spike", "double", "none"],
1167
+ accessory: ["sparkle", "glasses", "eyepatch", "none"],
1168
+ topper: ["none", "antlers", "horn", "horns", "tuft", "cap"],
1169
+ background: ["solid"],
1170
+ outfit: ["none", "scarf", "necklace"]
1171
+ },
1172
+ styleHints: {
1173
+ // 'masc' → aggressive: cross eyes, horns, spike antenna, eyepatch
1174
+ masc: {
1175
+ accessory: ["eyepatch", "glasses", "none"],
1176
+ topper: ["horns", "horn", "antlers"],
1177
+ outfit: ["none"]
1178
+ },
1179
+ // 'femme' → glamour: sparkle, halo, soft tuft
1180
+ femme: {
1181
+ accessory: ["sparkle", "glasses"],
1182
+ topper: ["tuft", "halo"],
1183
+ outfit: ["necklace", "scarf", "none"]
1184
+ },
1185
+ // 'neutral' → balanced edgy
1186
+ neutral: {
1187
+ accessory: ["glasses", "sparkle", "none"],
1188
+ topper: ["none", "cap", "tuft"],
1189
+ outfit: ["none"]
1190
+ }
1191
+ }
1192
+ };
1193
+
1194
+ // src/packs/mono.ts
1195
+ var palettes6 = [
1196
+ // Obsidian — near-black body, WHITE face features for contrast
1197
+ { id: "mono:obsidian", bodyFrom: "#2A2A2A", bodyTo: "#0A0A0A", accent: "#FAFAFA", ink: "#F2F2F2", blush: "#5A5A5A" },
1198
+ // Graphite — dark warm grey body, light face
1199
+ { id: "mono:graphite", bodyFrom: "#4A4A4A", bodyTo: "#222222", accent: "#F4F4F4", ink: "#EBEBEB", blush: "#7A7A7A" },
1200
+ // Slate — mid-grey body, dark face (good contrast either way; going dark for editorial)
1201
+ { id: "mono:slate", bodyFrom: "#8E8E8E", bodyTo: "#5A5A5A", accent: "#F8F8F8", ink: "#0A0A0A", blush: "#A8A8A8" },
1202
+ // Silver — mid-light grey, dark face
1203
+ { id: "mono:silver", bodyFrom: "#B8B8B8", bodyTo: "#888888", accent: "#FCFCFC", ink: "#1A1A1A", blush: "#D0D0D0" },
1204
+ // Fog — palest grey, dark face
1205
+ { id: "mono:fog", bodyFrom: "#E0E0E0", bodyTo: "#B0B0B0", accent: "#FFFFFF", ink: "#2A2A2A", blush: "#EDEDED" },
1206
+ // Ash — warm sand-grey, dark face with brown hint
1207
+ { id: "mono:ash", bodyFrom: "#A39A8E", bodyTo: "#6A6055", accent: "#F8F4EF", ink: "#1A1612", blush: "#B8AFA5" }
1208
+ ];
1209
+ var monoPack = {
1210
+ id: "mono",
1211
+ name: "Mono",
1212
+ description: "Editorial minimal. Clean grayscale silhouette on warm white plate, thin lines, no decoration.",
1213
+ emoji: "\u{1F5A4}",
1214
+ palettes: palettes6,
1215
+ paletteExclusive: true,
1216
+ flat: true,
1217
+ bgColor: "#FAFAF8",
1218
+ // warm near-white plate
1219
+ featureStroke: 1.15,
1220
+ // subtle but visible (vs Office 1.35, Neon 1.5)
1221
+ picks: {
1222
+ body: ["squircle"],
1223
+ eyes: ["dot"],
1224
+ mouth: ["flat", "dot"],
1225
+ antenna: ["none"],
1226
+ accessory: ["none", "none", "mole", "glasses", "freckles"],
1227
+ topper: ["none"],
1228
+ background: ["solid"],
1229
+ outfit: ["none"]
1230
+ },
1231
+ styleHints: {
1232
+ // 'masc' → bare silhouette, nothing
1233
+ masc: {
1234
+ accessory: ["none"],
1235
+ outfit: ["none"],
1236
+ topper: ["none"]
1237
+ },
1238
+ // 'femme' → one subtle accent (mole or freckles)
1239
+ femme: {
1240
+ accessory: ["mole", "freckles"],
1241
+ outfit: ["none"],
1242
+ topper: ["none"]
1243
+ },
1244
+ // 'neutral' → glasses option
1245
+ neutral: {
1246
+ accessory: ["none", "glasses"],
1247
+ outfit: ["none"],
1248
+ topper: ["none"]
1249
+ }
1250
+ }
1251
+ };
1252
+
1253
+ // src/packs/earth.ts
1254
+ var palettes7 = [
1255
+ // Sage — calm soft green
1256
+ { id: "earth:sage", bodyFrom: "#A8C49A", bodyTo: "#6D8E60", accent: "#F2F7EA", ink: "#1F2E18", blush: "#C8D7B4" },
1257
+ // Clay — terracotta warmth
1258
+ { id: "earth:clay", bodyFrom: "#D49072", bodyTo: "#9C4F2E", accent: "#FBEBDD", ink: "#3D1A0B", blush: "#E5BCA5" },
1259
+ // Sand — warm beige
1260
+ { id: "earth:sand", bodyFrom: "#E1CC9F", bodyTo: "#A88B5A", accent: "#FFF5E0", ink: "#4A3818", blush: "#EBD8B0" },
1261
+ // Moss — deeper forest green
1262
+ { id: "earth:moss", bodyFrom: "#82A570", bodyTo: "#3F6038", accent: "#EAF4DC", ink: "#0D1F08", blush: "#B9D2A1" },
1263
+ // Terracotta — fiery clay, more saturated
1264
+ { id: "earth:terracotta", bodyFrom: "#D87555", bodyTo: "#A53F22", accent: "#FFE5D8", ink: "#380F02", blush: "#EBA188" },
1265
+ // Mushroom — warm dusty mauve-brown
1266
+ { id: "earth:mushroom", bodyFrom: "#B59B8A", bodyTo: "#7D6353", accent: "#F4ECE3", ink: "#2C1C13", blush: "#C6B0A0" }
1267
+ ];
1268
+ var earthPack = {
1269
+ id: "earth",
1270
+ name: "Earth",
1271
+ description: "Wellness companion. Warm earthy palettes, soft round body, sleepy meditative face, leaf + halo toppers.",
1272
+ emoji: "\u{1F33F}",
1273
+ palettes: palettes7,
1274
+ paletteExclusive: true,
1275
+ flat: true,
1276
+ bgColor: "#FBF6EE",
1277
+ // warm cream plate
1278
+ featureStroke: 1.2,
1279
+ picks: {
1280
+ body: ["orb", "dumpling", "pebble"],
1281
+ eyes: ["sleepy", "sleepy", "dot"],
1282
+ mouth: ["smile", "dot", "flat"],
1283
+ antenna: ["none"],
1284
+ accessory: ["blush", "blush", "freckles", "mole", "sparkle", "none"],
1285
+ topper: ["none", "leaf", "halo", "headband", "tuft"],
1286
+ background: ["solid"],
1287
+ outfit: ["none", "collar", "scarf", "necklace"]
1288
+ },
1289
+ styleHints: {
1290
+ // 'masc' → minimal earth: dot eyes, no topper, no accessory
1291
+ masc: {
1292
+ accessory: ["none", "freckles", "mole"],
1293
+ topper: ["none", "tuft"],
1294
+ outfit: ["none", "collar"]
1295
+ },
1296
+ // 'femme' → wellness teacher: sleepy + leaf/halo + blush + sparkle
1297
+ femme: {
1298
+ accessory: ["blush", "sparkle"],
1299
+ topper: ["leaf", "halo", "headband"],
1300
+ outfit: ["none", "scarf", "necklace"]
1301
+ },
1302
+ // 'neutral' → balanced calm
1303
+ neutral: {
1304
+ accessory: ["blush", "freckles"],
1305
+ topper: ["none", "leaf", "tuft"],
1306
+ outfit: ["none", "collar"]
1307
+ }
1308
+ }
1309
+ };
1310
+
1311
+ // src/packs/index.ts
1312
+ var BUILT_IN_PACKS = [
1313
+ officePack,
1314
+ officeBrightPack,
1315
+ halloweenPack,
1316
+ pastelPack,
1317
+ neonPack,
1318
+ monoPack,
1319
+ earthPack
1320
+ ];
1321
+ var PACK_REGISTRY = Object.fromEntries(
1322
+ BUILT_IN_PACKS.map((p) => [p.id, p])
1323
+ );
1324
+ function resolvePacks(ids) {
1325
+ if (!ids || ids.length === 0) return [];
1326
+ const seen = /* @__PURE__ */ new Set();
1327
+ const result = [];
1328
+ for (const id of ids) {
1329
+ if (seen.has(id)) continue;
1330
+ const pack = PACK_REGISTRY[id];
1331
+ if (pack) {
1332
+ seen.add(id);
1333
+ result.push(pack);
1334
+ }
1335
+ }
1336
+ return result;
1337
+ }
1338
+
693
1339
  // src/select.ts
1340
+ function applyStyleHint(pool, packs, hint, partKey) {
1341
+ for (const pack of packs) {
1342
+ const subset = pack.styleHints?.[hint]?.[partKey];
1343
+ if (subset && subset.length > 0) {
1344
+ const narrowed = pool.filter((id) => subset.includes(id));
1345
+ if (narrowed.length > 0) return narrowed;
1346
+ }
1347
+ }
1348
+ return pool;
1349
+ }
1350
+ function resolvePartPool(basePool, packs, partKey) {
1351
+ const constraints = packs.map((p) => p.picks?.[partKey]).filter((list) => Array.isArray(list) && list.length > 0);
1352
+ if (constraints.length === 0) return basePool;
1353
+ let pool = constraints[0];
1354
+ for (let i = 1; i < constraints.length; i++) {
1355
+ pool = pool.filter((id) => constraints[i].includes(id));
1356
+ }
1357
+ if (pool.length > 0) return pool;
1358
+ const seen = /* @__PURE__ */ new Set();
1359
+ const union = [];
1360
+ for (const list of constraints) {
1361
+ for (const id of list) {
1362
+ if (!seen.has(id)) {
1363
+ seen.add(id);
1364
+ union.push(id);
1365
+ }
1366
+ }
1367
+ }
1368
+ return union.length > 0 ? union : basePool;
1369
+ }
694
1370
  function selectAvatar(seed2, options = {}) {
695
1371
  const rng = createRng(seed2);
696
- const paletteOverride = options.paletteId ? PALETTE_BY_ID[options.paletteId] : void 0;
697
- const palette = paletteOverride ?? rng.pick(PALETTES);
698
- const body = rng.pick(BODY_IDS);
699
- const eyes = rng.pick(EYE_IDS);
700
- const mouth = rng.pick(MOUTH_IDS);
701
- const antenna = rng.pick(ANTENNA_IDS);
702
- const accessory = rng.pick(ACCESSORY_IDS);
1372
+ const enabledPacks = resolvePacks(options.packs);
1373
+ const packPalettes = [];
1374
+ let exclusivePackPalettes = null;
1375
+ for (const pack of enabledPacks) {
1376
+ if (pack.palettes && pack.palettes.length > 0) {
1377
+ packPalettes.push(...pack.palettes);
1378
+ if (pack.paletteExclusive) {
1379
+ exclusivePackPalettes = exclusivePackPalettes ?? [];
1380
+ exclusivePackPalettes.push(...pack.palettes);
1381
+ }
1382
+ }
1383
+ }
1384
+ const palettePool = exclusivePackPalettes ? exclusivePackPalettes : packPalettes.length > 0 ? [...PALETTES, ...packPalettes] : PALETTES;
1385
+ const paletteByIdLookup = options.paletteId ? PALETTE_BY_ID[options.paletteId] ?? packPalettes.find((p) => p.id === options.paletteId) : void 0;
1386
+ const paletteOverride = options.palette ?? paletteByIdLookup;
1387
+ const palette = paletteOverride ?? rng.pick(palettePool);
1388
+ const bodyPool = resolvePartPool(BODY_IDS, enabledPacks, "body");
1389
+ const eyesPool = resolvePartPool(EYE_IDS, enabledPacks, "eyes");
1390
+ const mouthPool = resolvePartPool(MOUTH_IDS, enabledPacks, "mouth");
1391
+ const antennaPool = resolvePartPool(ANTENNA_IDS, enabledPacks, "antenna");
1392
+ let accessoryPool = resolvePartPool(ACCESSORY_IDS, enabledPacks, "accessory");
1393
+ const backgroundPool = resolvePartPool(BACKGROUND_IDS, enabledPacks, "background");
1394
+ let topperPool = resolvePartPool(TOPPER_IDS, enabledPacks, "topper");
1395
+ const styleHint = options.style;
1396
+ if (styleHint) {
1397
+ accessoryPool = applyStyleHint(accessoryPool, enabledPacks, styleHint, "accessory");
1398
+ topperPool = applyStyleHint(topperPool, enabledPacks, styleHint, "topper");
1399
+ }
1400
+ const body = rng.pick(bodyPool);
1401
+ const eyes = rng.pick(eyesPool);
1402
+ const mouth = rng.pick(mouthPool);
1403
+ const antenna = rng.pick(antennaPool);
1404
+ const accessory = rng.pick(accessoryPool);
703
1405
  let background;
704
1406
  if (typeof options.background === "string") {
705
1407
  background = options.background;
706
1408
  } else if (options.background && typeof options.background === "object") {
707
1409
  background = "solid";
708
1410
  } else {
709
- background = rng.pick(BACKGROUND_IDS);
1411
+ background = rng.pick(backgroundPool);
710
1412
  }
711
- const topperRaw = rng.pick(TOPPER_IDS);
1413
+ const topperRaw = rng.pick(topperPool);
712
1414
  const topper = antenna !== "none" && topperRaw !== "none" && topperRaw !== "leaf" ? "none" : topperRaw;
713
1415
  const hueShift = Math.round(rng.range(-30, 30));
714
1416
  const bodyScale = Number(rng.range(0.92, 1.08).toFixed(3));
715
1417
  const eyeGapShift = Number(rng.range(-2, 2).toFixed(2));
716
1418
  const mouthCurveScale = Number(rng.range(0.85, 1.15).toFixed(3));
717
1419
  const antennaTilt = Math.round(rng.range(-8, 8));
1420
+ const outfitConstraints = enabledPacks.map((p) => p.picks?.outfit).filter((list) => Array.isArray(list) && list.length > 0);
1421
+ let outfit = "none";
1422
+ if (outfitConstraints.length > 0) {
1423
+ let pool = outfitConstraints[0];
1424
+ for (let i = 1; i < outfitConstraints.length; i++) {
1425
+ pool = pool.filter((id) => outfitConstraints[i].includes(id));
1426
+ }
1427
+ if (pool.length === 0) {
1428
+ const seen = /* @__PURE__ */ new Set();
1429
+ const union = [];
1430
+ for (const list of outfitConstraints) {
1431
+ for (const id of list) {
1432
+ if (!seen.has(id)) {
1433
+ seen.add(id);
1434
+ union.push(id);
1435
+ }
1436
+ }
1437
+ }
1438
+ pool = union;
1439
+ }
1440
+ if (styleHint) {
1441
+ pool = applyStyleHint(pool, enabledPacks, styleHint, "outfit");
1442
+ }
1443
+ outfit = pool.length > 0 ? rng.pick(pool) : "none";
1444
+ }
1445
+ let flat;
1446
+ let bgColor;
1447
+ let featureStroke;
1448
+ let glow;
1449
+ for (const pack of enabledPacks) {
1450
+ if (pack.flat) flat = true;
1451
+ if (pack.bgColor) bgColor = pack.bgColor;
1452
+ if (pack.featureStroke) featureStroke = pack.featureStroke;
1453
+ if (pack.glow) glow = true;
1454
+ }
718
1455
  return {
719
1456
  seed: seed2,
720
1457
  palette,
@@ -725,12 +1462,16 @@ function selectAvatar(seed2, options = {}) {
725
1462
  accessory,
726
1463
  background,
727
1464
  topper,
728
- outfit: "none",
1465
+ outfit,
729
1466
  hueShift,
730
1467
  bodyScale,
731
1468
  eyeGapShift,
732
1469
  mouthCurveScale,
733
- antennaTilt
1470
+ antennaTilt,
1471
+ ...flat ? { flat: true } : {},
1472
+ ...bgColor ? { bgColor } : {},
1473
+ ...featureStroke ? { featureStroke } : {},
1474
+ ...glow ? { glow: true } : {}
734
1475
  };
735
1476
  }
736
1477
 
@@ -800,31 +1541,42 @@ function renderAvatarInner(spec, options = {}) {
800
1541
  ...baseAnchor,
801
1542
  eyeOffset: baseAnchor.eyeOffset + (spec.eyeGapShift ?? 0)
802
1543
  };
1544
+ const strokeMul = spec.featureStroke ?? 1;
803
1545
  const antennaSvg = renderAntenna(spec.antenna, anchor, spec.palette);
804
- const accessorySvg = renderAccessory(spec.accessory, spec.palette, anchor);
805
- const bodyMarkup = renderBody(spec.body, spec.palette, gradId);
806
- const bodyTransform = transformBody(spec.bodyScale ?? 1, anchor);
1546
+ const accessorySvg = renderAccessory(spec.accessory, spec.palette, anchor, { strokeMul });
1547
+ const flat = spec.flat === true;
1548
+ const glow = spec.glow === true;
1549
+ const glowId = `navii-glow-${id}`;
1550
+ const bodyMarkup = renderBody(spec.body, spec.palette, gradId, { flat });
1551
+ const effectiveBodyScale = flat ? 1 : spec.bodyScale ?? 1;
1552
+ const bodyTransform = transformBody(effectiveBodyScale, anchor);
807
1553
  const bodyFilter = spec.hueShift && spec.hueShift !== 0 ? ` filter="url(#${hueId})"` : "";
808
1554
  const bodyWrapped = `<g${bodyTransform}${bodyFilter}><g class="body">${bodyMarkup}</g></g>`;
1555
+ const glowLayer = glow ? `<g${bodyTransform} filter="url(#${glowId})" opacity="0.85"><g class="body-glow">${bodyMarkup.replace(/fill="url\(#[^"]+\)"/g, `fill="${spec.palette.bodyFrom}"`)}</g></g>` : "";
809
1556
  const antennaWrapped = antennaSvg ? `<g${transformAntenna(spec.antennaTilt ?? 0, anchor)}><g class="antenna">${antennaSvg}</g></g>` : "";
810
1557
  const tileBg = resolveTileBg(options.tileBg, spec.palette);
811
1558
  const tileCircle = tileBg ? `<circle cx="50" cy="50" r="50" fill="${tileBg}" />` : "";
812
1559
  const outfitSvg = renderOutfit(spec.outfit ?? "none", anchor, spec.palette);
1560
+ const packPlate = spec.bgColor ? `<rect x="0" y="0" width="100" height="100" fill="${spec.bgColor}" />` : "";
1561
+ const backgroundMarkup = spec.bgColor ? "" : renderBackground(spec.background, spec.palette, bgOverride);
813
1562
  const parts = [
814
1563
  tileCircle,
815
- renderBackground(spec.background, spec.palette, bgOverride),
1564
+ packPlate,
1565
+ backgroundMarkup,
1566
+ glowLayer,
816
1567
  bodyWrapped,
817
1568
  // outfit sits on the body but below the face, so face features stay readable
818
1569
  outfitSvg,
819
1570
  renderTopper(spec.topper, anchor, spec.palette),
820
- wrap("eyes", renderEyes(spec.eyes, spec.palette, anchor)),
821
- renderMouth(spec.mouth, spec.palette, anchor, spec.mouthCurveScale ?? 1),
1571
+ wrap("eyes", renderEyes(spec.eyes, spec.palette, anchor, { strokeMul })),
1572
+ renderMouth(spec.mouth, spec.palette, anchor, spec.mouthCurveScale ?? 1, { strokeMul }),
822
1573
  antennaWrapped,
823
1574
  accessorySvg && spec.accessory === "sparkle" ? wrap("sparkle", accessorySvg) : accessorySvg
824
1575
  ].join("");
825
1576
  const defs = [
826
- renderBodyDefs(spec.body, spec.palette, gradId),
827
- spec.hueShift && spec.hueShift !== 0 ? `<filter id="${hueId}" color-interpolation-filters="sRGB"><feColorMatrix type="hueRotate" values="${spec.hueShift}" /></filter>` : ""
1577
+ renderBodyDefs(spec.body, spec.palette, gradId, { flat }),
1578
+ spec.hueShift && spec.hueShift !== 0 ? `<filter id="${hueId}" color-interpolation-filters="sRGB"><feColorMatrix type="hueRotate" values="${spec.hueShift}" /></filter>` : "",
1579
+ glow ? `<filter id="${glowId}" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="4" /></filter>` : ""
828
1580
  ].join("");
829
1581
  return [
830
1582
  `<defs>${defs}</defs>`,
@@ -967,6 +1719,6 @@ var Navii = {
967
1719
  build
968
1720
  };
969
1721
 
970
- export { Navii, build, createAvatar, createRng, cyrb53, random, renderAvatar, renderAvatarInner, renderGroup, seed, selectAvatar };
1722
+ export { BUILT_IN_PACKS, Navii, PACK_REGISTRY, build, createAvatar, createRng, cyrb53, random, renderAvatar, renderAvatarInner, renderGroup, resolvePacks, seed, selectAvatar };
971
1723
  //# sourceMappingURL=index.js.map
972
1724
  //# sourceMappingURL=index.js.map