privateboard 0.1.37 → 0.1.40

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.
Files changed (76) hide show
  1. package/dist/boot.js +1415 -91
  2. package/dist/boot.js.map +1 -1
  3. package/dist/cli.js +1415 -91
  4. package/dist/cli.js.map +1 -1
  5. package/dist/server.js +1271 -81
  6. package/dist/server.js.map +1 -1
  7. package/dist/version.d.ts +1 -1
  8. package/dist/version.js +1 -1
  9. package/dist/version.js.map +1 -1
  10. package/package.json +1 -1
  11. package/public/__avatar3d_test.html +156 -0
  12. package/public/adjourn-overlay.css +2 -2
  13. package/public/agent-overlay.css +27 -15
  14. package/public/agent-overlay.js +3 -1
  15. package/public/agent-profile.css +331 -41
  16. package/public/agent-profile.js +499 -75
  17. package/public/app-updater.css +1 -1
  18. package/public/app.js +2090 -547
  19. package/public/avatar-3d-snap.js +205 -0
  20. package/public/avatar-3d.js +792 -0
  21. package/public/avatar-customizer.html +274 -0
  22. package/public/avatar3d-editor.css +240 -0
  23. package/public/avatar3d-editor.js +481 -0
  24. package/public/avatars/3d/chair.png +0 -0
  25. package/public/avatars/3d/first-principles.png +0 -0
  26. package/public/avatars/3d/historian.png +0 -0
  27. package/public/avatars/3d/long-horizon.png +0 -0
  28. package/public/avatars/3d/phenomenologist.png +0 -0
  29. package/public/avatars/3d/socrates.png +0 -0
  30. package/public/avatars/3d/user-empathy.png +0 -0
  31. package/public/avatars/3d/value-investor.png +0 -0
  32. package/public/core-avatars.js +86 -0
  33. package/public/home-3d-loader.js +15 -4
  34. package/public/home-3d-mock.js +18 -7
  35. package/public/home.html +80 -18
  36. package/public/i18n.js +279 -4
  37. package/public/icons/avatar_1779855104027.glb +0 -0
  38. package/public/icons/logo.png +0 -0
  39. package/public/icons/new-style.glb +0 -0
  40. package/public/icons/new-style2.glb +0 -0
  41. package/public/icons/new-style3.glb +0 -0
  42. package/public/icons/new-style4.glb +0 -0
  43. package/public/icons/new-style5.glb +0 -0
  44. package/public/icons/office.glb +0 -0
  45. package/public/icons/stuff.glb +0 -0
  46. package/public/index.html +203 -182
  47. package/public/mention-picker.js +1 -1
  48. package/public/new-agent.css +7 -7
  49. package/public/new-agent.js +46 -20
  50. package/public/office-viewer.html +340 -0
  51. package/public/onboarding.css +5 -5
  52. package/public/quote-cta.css +5 -4
  53. package/public/quote-cta.js +50 -5
  54. package/public/room-settings.css +24 -9
  55. package/public/stuff-viewer.html +330 -0
  56. package/public/thread.css +1211 -0
  57. package/public/user-settings.css +16 -19
  58. package/public/user-settings.js +86 -78
  59. package/public/vendor/BufferGeometryUtils.js +1434 -0
  60. package/public/vendor/DRACOLoader.js +739 -0
  61. package/public/vendor/GLTFLoader.js +4860 -0
  62. package/public/vendor/RoomEnvironment.js +185 -0
  63. package/public/vendor/SkeletonUtils.js +496 -0
  64. package/public/vendor/draco/draco_decoder.js +34 -0
  65. package/public/vendor/draco/draco_decoder.wasm +0 -0
  66. package/public/vendor/draco/draco_encoder.js +33 -0
  67. package/public/vendor/draco/draco_wasm_wrapper.js +117 -0
  68. package/public/vendor/meshopt_decoder.module.js +196 -0
  69. package/public/voice-3d-banner.js +12 -0
  70. package/public/voice-3d.js +1407 -432
  71. package/public/voice-clone.css +875 -0
  72. package/public/voice-clone.js +1351 -0
  73. package/public/voice-replay.css +3 -3
  74. package/public/voice-replay.js +21 -0
  75. package/public/avatar-skill.js +0 -629
  76. package/public/icons/folded-sidebar.png +0 -0
@@ -59,7 +59,7 @@
59
59
  }
60
60
  .vr-kicker-glyph {
61
61
  font-family: var(--font-human);
62
- font-size: 13px;
62
+ font-size: 14px;
63
63
  letter-spacing: 0;
64
64
  }
65
65
  .vr-head-actions {
@@ -274,7 +274,7 @@
274
274
  }
275
275
  .vr-avatar-placeholder {
276
276
  font-family: var(--mono);
277
- font-size: 13px;
277
+ font-size: 14px;
278
278
  font-weight: 700;
279
279
  color: var(--text-soft);
280
280
  }
@@ -427,7 +427,7 @@
427
427
  }
428
428
  .vr-key-title {
429
429
  font-family: var(--font-human, var(--mono));
430
- font-size: 13px;
430
+ font-size: 14px;
431
431
  font-weight: 700;
432
432
  color: var(--text);
433
433
  letter-spacing: -0.005em;
@@ -378,6 +378,15 @@
378
378
  close();
379
379
  return;
380
380
  }
381
+ // Stop button mirrored into the room header (.head-actions) ·
382
+ // app.js renders it statically + toggles its visibility on the
383
+ // replay-* events, but the stop ACTION stays owned here so all
384
+ // replay controls funnel through close().
385
+ if (target.closest("[data-vr-header-stop]")) {
386
+ ev.preventDefault();
387
+ close();
388
+ return;
389
+ }
381
390
  if (target.closest("[data-vr-inline-expand]")) {
382
391
  ev.preventDefault();
383
392
  toggleCollapsed();
@@ -405,6 +414,17 @@
405
414
  else removeInlineExpand();
406
415
  }
407
416
 
417
+ /** Force the floating panel into its collapsed posture (idempotent).
418
+ * Exposed on the public API so the recorder can tuck the player
419
+ * out of the captured stage region · the panel floats bottom-right
420
+ * over the stage, so leaving it expanded would bleed into the
421
+ * recording. Collapsing surfaces the inline controls in the
422
+ * input-bar (below the captured region) instead. */
423
+ function collapse() {
424
+ if (!STATE.overlay) return;
425
+ if (!STATE.overlay.classList.contains("is-collapsed")) toggleCollapsed();
426
+ }
427
+
408
428
  /** Slot the inline replay control group into the bottom-bar
409
429
  * action group right after the existing Voice Replay anchor.
410
430
  * Three buttons: Next (skip current message), Pause/Resume
@@ -974,6 +994,7 @@
974
994
  getActiveAudio: getActiveAudio,
975
995
  getRoomId: getRoomId,
976
996
  togglePause: togglePause,
997
+ collapse: collapse,
977
998
  // Exposed for testing.
978
999
  _internals: { buildPlaylist, PROCEDURAL_KINDS },
979
1000
  };
@@ -1,629 +0,0 @@
1
- /* ═══════════════════════════════════════════════════════════════
2
- AVATAR SKILL · BRICK pet generator
3
- ─────────────────────────────────────────────────────────────
4
- Composable 8-bit pixel-art mascots based on the BRICK silhouette
5
- (chunky terracotta block + stub legs + tool belt). Five dimensions
6
- combined by seed:
7
-
8
- · body color (12) — terracotta default; chair always terracotta
9
- · glasses (9) — none, round, horn, monocle, sun, aviator,
10
- cyber-visor, pince-nez, 3d
11
- · mustache (10) — clean, handlebar, walrus, pencil, chevron,
12
- horseshoe, imperial, soul-patch, goatee, full
13
- · expression (7) — default, focused, wink, surprised, sleepy,
14
- side-look, happy
15
- · prop (9) — none, coffee, notebook, magnifier, lightbulb,
16
- gavel, scroll, pen, lantern
17
- (held in front of body, doesn't break silhouette)
18
-
19
- Hats were deliberately removed — hatted vs. un-hatted avatars made
20
- the rendered head size inconsistent (per-avatar viewBox includes
21
- the hat in its bbox, so the body shrank inside the fixed-size
22
- container). Now every avatar has identical content bounds → body
23
- size is stable across the whole sidebar.
24
-
25
- Public API · window.AvatarSkill:
26
- generate(seed?, opts?) → SVG markup string
27
- generateDataUrl(seed?, opts?) → "data:image/svg+xml;…"
28
- randomSeed() → fresh seed string
29
- attach({ frame, button, onSeed }) → wire a UI in one line.
30
-
31
- opts.placeholder = true → grey silhouette w/ "?".
32
- opts.variant = "classic" → forces CLASSIC (default expression + clean
33
- + no glasses / mustache / prop, terracotta).
34
- Used by chair.svg.
35
-
36
- Grid: 32×32 cells, 16px each, viewBox computed per-avatar.
37
- ═══════════════════════════════════════════════════════════════ */
38
-
39
- (function () {
40
- if (window.AvatarSkill) return;
41
-
42
- // ── Grid constants ───────────────────────────────────────
43
- const GRID = 32;
44
- const PX = 16;
45
- const SIZE = GRID * PX; // 512
46
-
47
- // ── Body color palettes (12) ─────────────────────────────
48
- // Each: { name, base, hi, sh, deep, eye, belt, buckle }
49
- const BODY_COLORS = [
50
- { name: "terracotta", base: "#a35a32", hi: "#ce8a5a", sh: "#6e3814", deep: "#3e1c08", eye: "#1a0805", belt: "#5a2a10", buckle: "#cfb56a" },
51
- { name: "slate", base: "#5a6f88", hi: "#8294aa", sh: "#3a4e64", deep: "#202d3e", eye: "#15101c", belt: "#2a3848", buckle: "#c0c8d8" },
52
- { name: "moss", base: "#5a7a3a", hi: "#8aaa5a", sh: "#3a5418", deep: "#1a2a08", eye: "#15140c", belt: "#2a3a18", buckle: "#d4c870" },
53
- { name: "plum", base: "#6a3a6a", hi: "#9a5a9a", sh: "#3a1a3a", deep: "#1a081a", eye: "#15081a", belt: "#2a142a", buckle: "#d4a8d8" },
54
- { name: "dusty-rose", base: "#c47a7a", hi: "#e8a8a8", sh: "#8a4848", deep: "#5a2828", eye: "#2a1414", belt: "#6a3a3a", buckle: "#e8c8a8" },
55
- { name: "mustard", base: "#c4a040", hi: "#e8c870", sh: "#886820", deep: "#4a3808", eye: "#2a1c05", belt: "#685428", buckle: "#e8d8a8" },
56
- { name: "charcoal", base: "#4a4a52", hi: "#6a6a78", sh: "#2a2a32", deep: "#15151a", eye: "#08080c", belt: "#1a1a22", buckle: "#a8a8b0" },
57
- { name: "copper", base: "#b86a3a", hi: "#e08a5a", sh: "#7a3a18", deep: "#3a1a08", eye: "#1a0805", belt: "#5a2810", buckle: "#f5d870" },
58
- { name: "teal", base: "#3a8a8a", hi: "#5aaaaa", sh: "#1a4848", deep: "#082828", eye: "#051818", belt: "#1a3838", buckle: "#c8e8d8" },
59
- { name: "burgundy", base: "#7a2828", hi: "#a85050", sh: "#4a1010", deep: "#1a0808", eye: "#15050a", belt: "#380a0a", buckle: "#d8a8a8" },
60
- { name: "amber", base: "#d4923a", hi: "#f5b85a", sh: "#8a5818", deep: "#4a2808", eye: "#1c0d05", belt: "#6a3a10", buckle: "#fff0a8" },
61
- { name: "navy", base: "#2a4a78", hi: "#4a6aa8", sh: "#15284a", deep: "#08152a", eye: "#050a18", belt: "#15203a", buckle: "#c0d0e8" },
62
- ];
63
-
64
- // ── RNG ──────────────────────────────────────────────────
65
- function makeRng(seed) {
66
- let s = 0;
67
- const str = String(seed || "default");
68
- for (let i = 0; i < str.length; i++) s = (s * 31 + str.charCodeAt(i)) >>> 0;
69
- return function () {
70
- s = (s * 1664525 + 1013904223) >>> 0;
71
- return s / 0xFFFFFFFF;
72
- };
73
- }
74
- const pick = (rng, arr) => arr[Math.floor(rng() * arr.length)];
75
- function weighted(rng, items, weights) {
76
- let total = 0;
77
- for (const w of weights) total += w;
78
- let r = rng() * total;
79
- for (let i = 0; i < items.length; i++) {
80
- r -= weights[i];
81
- if (r <= 0) return items[i];
82
- }
83
- return items[items.length - 1];
84
- }
85
-
86
- // ── Pixel grid ───────────────────────────────────────────
87
- function makeGrid() {
88
- const g = new Array(GRID);
89
- for (let y = 0; y < GRID; y++) g[y] = new Array(GRID).fill(null);
90
- return g;
91
- }
92
- function px(g, x, y, c) {
93
- if (x < 0 || x >= GRID || y < 0 || y >= GRID) return;
94
- g[y][x] = c;
95
- }
96
- function row(g, r, c1, c2, c) {
97
- for (let x = c1; x <= c2; x++) px(g, x, r, c);
98
- }
99
- function colp(g, c, r1, r2, color) {
100
- for (let y = r1; y <= r2; y++) px(g, c, y, color);
101
- }
102
- function rect(g, x, y, w, h, c) {
103
- for (let dy = 0; dy < h; dy++)
104
- for (let dx = 0; dx < w; dx++) px(g, x + dx, y + dy, c);
105
- }
106
-
107
- // ════════════════════════════════════════════════════════
108
- // BRICK BODY (rows 10-22, cols 10-21)
109
- // ════════════════════════════════════════════════════════
110
-
111
- const BODY_SHAPE = [
112
- [10, 12, 19], [11, 11, 20], [12, 10, 21], [13, 10, 21],
113
- [14, 10, 21], [15, 10, 21], [16, 10, 21], [17, 10, 21],
114
- [18, 10, 21], [19, 10, 21], [20, 11, 20],
115
- ];
116
-
117
- function drawBody(g, p) {
118
- for (const [r, c1, c2] of BODY_SHAPE) row(g, r, c1, c2, p.base);
119
- // Stub legs
120
- rect(g, 12, 21, 2, 2, p.base);
121
- rect(g, 18, 21, 2, 2, p.base);
122
- // Foot bottom
123
- row(g, 22, 12, 13, p.deep);
124
- row(g, 22, 18, 19, p.deep);
125
- // Top-left highlight
126
- [[12,10],[13,10],[11,11],[12,11],[10,12],[11,12]].forEach(([x,y])=>px(g,x,y,p.hi));
127
- // Bottom-right shadow
128
- [[20,17],[21,17],[20,18],[21,18],[19,19],[20,19],[20,20]].forEach(([x,y])=>px(g,x,y,p.sh));
129
- px(g, 19, 21, p.sh);
130
- // Tool belt
131
- row(g, 17, 10, 21, p.belt);
132
- px(g, 15, 17, p.buckle);
133
- px(g, 16, 17, p.buckle);
134
- }
135
-
136
- // ════════════════════════════════════════════════════════
137
- // EXPRESSIONS (eyes + brows; row 12-14)
138
- // ════════════════════════════════════════════════════════
139
-
140
- const EXPRESSIONS = {
141
- // Default: angled inward brows + dot eyes (the OG BRICK look)
142
- default(g, p) {
143
- px(g, 12, 12, p.eye); px(g, 13, 13, p.eye);
144
- px(g, 19, 12, p.eye); px(g, 18, 13, p.eye);
145
- px(g, 13, 14, p.eye); px(g, 18, 14, p.eye);
146
- },
147
- // Focused: flat brows + narrowed eyes
148
- focused(g, p) {
149
- row(g, 12, 12, 14, p.eye);
150
- row(g, 12, 17, 19, p.eye);
151
- px(g, 13, 14, p.eye); px(g, 14, 14, p.eye);
152
- px(g, 17, 14, p.eye); px(g, 18, 14, p.eye);
153
- },
154
- // Wink: left eye dot, right eye closed line
155
- wink(g, p) {
156
- px(g, 12, 12, p.eye); px(g, 13, 13, p.eye);
157
- px(g, 19, 12, p.eye); px(g, 18, 13, p.eye);
158
- px(g, 13, 14, p.eye);
159
- row(g, 14, 17, 19, p.eye); // closed wink line
160
- },
161
- // Surprised: raised brows + bigger eyes (2x2)
162
- surprised(g, p) {
163
- row(g, 11, 12, 14, p.eye);
164
- row(g, 11, 17, 19, p.eye);
165
- rect(g, 13, 13, 1, 2, p.eye);
166
- rect(g, 18, 13, 1, 2, p.eye);
167
- },
168
- // Sleepy: low brows + closed-line eyes
169
- sleepy(g, p) {
170
- px(g, 12, 13, p.eye); px(g, 13, 13, p.eye);
171
- px(g, 18, 13, p.eye); px(g, 19, 13, p.eye);
172
- row(g, 14, 12, 14, p.eye);
173
- row(g, 14, 17, 19, p.eye);
174
- },
175
- // Side-look: eyes shifted right
176
- sideLook(g, p) {
177
- px(g, 12, 12, p.eye); px(g, 13, 13, p.eye);
178
- px(g, 19, 12, p.eye); px(g, 18, 13, p.eye);
179
- px(g, 14, 14, p.eye); px(g, 19, 14, p.eye);
180
- },
181
- // Happy: ^_^ curved-up eyes
182
- happy(g, p) {
183
- // brow tips up
184
- px(g, 12, 12, p.eye); px(g, 13, 12, p.eye);
185
- px(g, 18, 12, p.eye); px(g, 19, 12, p.eye);
186
- // ^_^
187
- px(g, 12, 14, p.eye); px(g, 14, 14, p.eye); px(g, 13, 13, p.eye);
188
- px(g, 17, 14, p.eye); px(g, 19, 14, p.eye); px(g, 18, 13, p.eye);
189
- },
190
- };
191
-
192
- // ════════════════════════════════════════════════════════
193
- // GLASSES (rows 12-15)
194
- // ════════════════════════════════════════════════════════
195
-
196
- const GLASSES = {
197
- none(g, p) {},
198
-
199
- round(g, p) {
200
- const F = "#c9a040";
201
- px(g, 13, 13, F); px(g, 13, 15, F); px(g, 12, 14, F); px(g, 14, 14, F);
202
- px(g, 18, 13, F); px(g, 18, 15, F); px(g, 17, 14, F); px(g, 19, 14, F);
203
- px(g, 15, 14, F); px(g, 16, 14, F);
204
- px(g, 13, 13, "#ffd870"); px(g, 18, 13, "#ffd870");
205
- },
206
-
207
- horn(g, p) {
208
- const F = "#0a0512";
209
- row(g, 13, 12, 14, F); row(g, 15, 12, 14, F);
210
- px(g, 12, 14, F); px(g, 14, 14, F);
211
- row(g, 13, 17, 19, F); row(g, 15, 17, 19, F);
212
- px(g, 17, 14, F); px(g, 19, 14, F);
213
- row(g, 14, 15, 16, F);
214
- },
215
-
216
- monocle(g, p) {
217
- const F = "#c9a040";
218
- row(g, 12, 17, 18, F); row(g, 16, 17, 18, F);
219
- colp(g, 16, 13, 15, F); colp(g, 19, 13, 15, F);
220
- px(g, 17, 12, "#ffd870");
221
- // Chain hanging down
222
- px(g, 20, 14, "#7a5a18");
223
- px(g, 20, 15, F);
224
- px(g, 21, 15, "#7a5a18");
225
- px(g, 21, 16, F);
226
- },
227
-
228
- sun(g, p) {
229
- const F = "#0a0512";
230
- rect(g, 12, 13, 3, 3, "#15101c");
231
- rect(g, 17, 13, 3, 3, "#15101c");
232
- row(g, 12, 12, 14, F); row(g, 12, 17, 19, F);
233
- row(g, 14, 15, 16, F);
234
- px(g, 12, 13, "#3ad4e0"); px(g, 17, 13, "#3ad4e0");
235
- },
236
-
237
- aviator(g, p) {
238
- // Teardrop shape, gold
239
- const F = "#c9a040";
240
- // Left lens
241
- row(g, 13, 12, 14, F);
242
- px(g, 12, 14, F); px(g, 14, 14, F);
243
- px(g, 13, 15, F); px(g, 14, 15, F);
244
- // Right lens
245
- row(g, 13, 17, 19, F);
246
- px(g, 17, 14, F); px(g, 19, 14, F);
247
- px(g, 17, 15, F); px(g, 18, 15, F);
248
- // Bridge
249
- px(g, 15, 13, F); px(g, 16, 13, F);
250
- // Sheen
251
- px(g, 13, 13, "#ffd870"); px(g, 18, 13, "#ffd870");
252
- },
253
-
254
- visor(g, p) {
255
- // Cyber horizontal slit
256
- rect(g, 11, 13, 10, 3, "#15101c");
257
- row(g, 14, 12, 19, "#3ad4e0");
258
- px(g, 13, 14, "#aaf0f8"); px(g, 18, 14, "#aaf0f8");
259
- px(g, 11, 13, "#3a3445"); px(g, 20, 15, "#3a3445");
260
- },
261
-
262
- pince(g, p) {
263
- // Pince-nez: small lenses on nose, no temples, with chain
264
- px(g, 13, 14, "#5a5a5a"); px(g, 13, 13, "#5a5a5a");
265
- px(g, 14, 14, "#5a5a5a");
266
- px(g, 17, 14, "#5a5a5a"); px(g, 18, 13, "#5a5a5a");
267
- px(g, 18, 14, "#5a5a5a");
268
- px(g, 15, 14, "#5a5a5a"); px(g, 16, 14, "#5a5a5a");
269
- // Small chain to side
270
- px(g, 11, 15, "#5a5a5a");
271
- px(g, 11, 16, "#5a5a5a");
272
- },
273
-
274
- "3d"(g, p) {
275
- // Red + blue 3D glasses
276
- rect(g, 12, 13, 3, 3, "#c8281a");
277
- rect(g, 17, 13, 3, 3, "#3a78c8");
278
- row(g, 12, 12, 14, "#0a0512"); row(g, 12, 17, 19, "#0a0512");
279
- row(g, 14, 15, 16, "#0a0512");
280
- px(g, 12, 13, "#f25a3a"); px(g, 17, 13, "#7ba8e8");
281
- },
282
- };
283
-
284
- // ════════════════════════════════════════════════════════
285
- // MUSTACHES / BEARDS (rows 15-16, careful w/ belt at 17)
286
- // ════════════════════════════════════════════════════════
287
-
288
- const MUSTACHES = {
289
- clean(g, p) {},
290
-
291
- handlebar(g, p) {
292
- const c = "#2a1408";
293
- row(g, 16, 13, 18, c);
294
- px(g, 14, 15, c); px(g, 17, 15, c);
295
- px(g, 12, 15, c); px(g, 11, 15, c);
296
- px(g, 19, 15, c); px(g, 20, 15, c);
297
- px(g, 15, 16, "#5a3018"); px(g, 16, 16, "#5a3018");
298
- },
299
-
300
- walrus(g, p) {
301
- const c = "#2a1408", cd = "#15080a";
302
- row(g, 15, 12, 19, c);
303
- row(g, 16, 11, 20, c);
304
- px(g, 11, 17, c); px(g, 20, 17, c);
305
- row(g, 16, 14, 17, cd);
306
- },
307
-
308
- pencil(g, p) {
309
- row(g, 15, 14, 17, "#2a1408");
310
- },
311
-
312
- chevron(g, p) {
313
- const c = "#2a1408", cd = "#15080a";
314
- row(g, 15, 13, 18, c);
315
- row(g, 16, 13, 18, c);
316
- px(g, 13, 16, cd); px(g, 18, 16, cd);
317
- },
318
-
319
- horseshoe(g, p) {
320
- const c = "#2a1408";
321
- row(g, 15, 13, 18, c);
322
- row(g, 16, 13, 18, c);
323
- px(g, 13, 17, c); px(g, 18, 17, c);
324
- },
325
-
326
- imperial(g, p) {
327
- // Big curls way up at the ends
328
- const c = "#2a1408";
329
- row(g, 16, 14, 17, c);
330
- // Curls up to row 14 / 13
331
- px(g, 13, 15, c); px(g, 12, 14, c); px(g, 11, 13, c); px(g, 11, 14, c);
332
- px(g, 18, 15, c); px(g, 19, 14, c); px(g, 20, 13, c); px(g, 20, 14, c);
333
- },
334
-
335
- soulPatch(g, p) {
336
- // Small tuft just under lip
337
- const c = "#2a1408";
338
- row(g, 16, 15, 16, c);
339
- px(g, 15, 15, c);
340
- },
341
-
342
- goatee(g, p) {
343
- // Chin + jaw line
344
- const c = "#2a1408";
345
- row(g, 16, 14, 17, c);
346
- px(g, 13, 16, c); px(g, 18, 16, c);
347
- // Sides going down to body bottom edge
348
- colp(g, 13, 16, 19, c);
349
- colp(g, 18, 16, 19, c);
350
- row(g, 19, 14, 17, c);
351
- },
352
-
353
- fullBeard(g, p) {
354
- // Covers lower face cols 11-20, rows 15-19
355
- const c = "#2a1408", cd = "#15080a";
356
- row(g, 15, 12, 19, c);
357
- row(g, 16, 11, 20, c);
358
- // Sideburns
359
- colp(g, 11, 15, 18, c);
360
- colp(g, 20, 15, 18, c);
361
- // Chin extension
362
- row(g, 18, 13, 18, c);
363
- row(g, 19, 14, 17, c);
364
- // Mustache shadow
365
- row(g, 16, 14, 17, cd);
366
- },
367
- };
368
-
369
- // ════════════════════════════════════════════════════════
370
- // PROPS (held in front of body, rows 17-21 area)
371
- // Drawn LAST so they appear on top.
372
- // ════════════════════════════════════════════════════════
373
-
374
- const PROPS = {
375
- none(g, p) {},
376
-
377
- coffee(g, p) {
378
- // Mug w/ handle, in front of belt
379
- rect(g, 13, 18, 4, 4, "#2a1a14"); // mug body
380
- rect(g, 13, 18, 4, 1, "#5a3a2a"); // top rim
381
- // Coffee surface (steam-y dark brown)
382
- px(g, 14, 18, "#1a0a05"); px(g, 15, 18, "#1a0a05");
383
- // Handle
384
- px(g, 17, 19, "#2a1a14"); px(g, 17, 20, "#2a1a14");
385
- // Steam
386
- px(g, 14, 17, "#dad5c8"); px(g, 15, 16, "#dad5c8");
387
- // Highlight
388
- px(g, 13, 19, "#5a3a2a");
389
- },
390
-
391
- notebook(g, p) {
392
- // Open notebook / clipboard
393
- rect(g, 12, 18, 8, 4, "#f1dfc4"); // paper
394
- // Lines on paper
395
- row(g, 19, 13, 18, "#7a5a3a");
396
- row(g, 20, 13, 18, "#7a5a3a");
397
- // Clip on top
398
- rect(g, 14, 18, 4, 1, "#5a5a5a");
399
- px(g, 15, 17, "#5a5a5a"); px(g, 16, 17, "#5a5a5a");
400
- // Edge shadow
401
- colp(g, 19, 18, 21, "#c9b58a");
402
- },
403
-
404
- magnifier(g, p) {
405
- // Circle lens + handle
406
- // Lens ring
407
- row(g, 17, 13, 15, "#5a3a18");
408
- row(g, 19, 13, 15, "#5a3a18");
409
- px(g, 12, 18, "#5a3a18"); px(g, 16, 18, "#5a3a18");
410
- // Lens glass
411
- px(g, 13, 18, "#aaf0f8"); px(g, 14, 18, "#aaf0f8"); px(g, 15, 18, "#aaf0f8");
412
- px(g, 13, 17, "#ffffff"); // sparkle
413
- // Handle (going down-right)
414
- px(g, 16, 19, "#5a3a18"); px(g, 17, 20, "#5a3a18"); px(g, 18, 21, "#5a3a18");
415
- },
416
-
417
- lightbulb(g, p) {
418
- // Bulb above body w/ glow
419
- // Glow
420
- px(g, 16, 16, "#fff5b0");
421
- px(g, 14, 17, "#fff5b0"); px(g, 18, 17, "#fff5b0");
422
- // Bulb
423
- rect(g, 15, 18, 3, 3, "#ffe070");
424
- px(g, 14, 19, "#ffe070"); px(g, 18, 19, "#ffe070");
425
- // Bulb highlight
426
- px(g, 15, 18, "#ffffff");
427
- // Base / screw cap
428
- row(g, 21, 15, 17, "#5a5a5a");
429
- },
430
-
431
- gavel(g, p) {
432
- // Wooden gavel
433
- // Hammer head (horizontal block)
434
- rect(g, 12, 18, 4, 2, "#7a5a3a");
435
- px(g, 12, 18, "#a87a4a"); px(g, 13, 18, "#a87a4a");
436
- px(g, 15, 19, "#3a2a18");
437
- // Handle (diagonal)
438
- px(g, 16, 20, "#5a3a18"); px(g, 17, 21, "#5a3a18");
439
- // Strike plate small block underneath
440
- px(g, 14, 21, "#3a2a18"); px(g, 15, 21, "#3a2a18");
441
- },
442
-
443
- scroll(g, p) {
444
- // Rolled paper held diagonally
445
- rect(g, 13, 18, 6, 3, "#f1dfc4"); // paper
446
- row(g, 19, 14, 17, "#a87a4a"); // text line
447
- // Roll ends (darker)
448
- colp(g, 13, 18, 20, "#c9b58a");
449
- colp(g, 18, 18, 20, "#c9b58a");
450
- // Cap end caps (rounded curls)
451
- px(g, 12, 18, "#a87a4a"); px(g, 12, 20, "#a87a4a");
452
- px(g, 19, 18, "#a87a4a"); px(g, 19, 20, "#a87a4a");
453
- },
454
-
455
- pen(g, p) {
456
- // Diagonal pen
457
- px(g, 12, 21, "#15101c");
458
- px(g, 13, 20, "#15101c");
459
- px(g, 14, 19, "#15101c");
460
- px(g, 15, 18, "#3a3a44"); // body
461
- px(g, 16, 17, "#3a3a44");
462
- px(g, 17, 16, "#cfa040"); // gold cap end
463
- px(g, 18, 16, "#cfa040");
464
- // Tip ink dot
465
- px(g, 11, 21, "#15101c");
466
- },
467
-
468
- lantern(g, p) {
469
- // Box w/ handle
470
- rect(g, 13, 18, 4, 4, "#3a3a44"); // frame
471
- rect(g, 14, 19, 2, 2, "#ffe070"); // glow
472
- px(g, 14, 19, "#ffffff");
473
- // Top handle
474
- row(g, 17, 14, 15, "#5a5a5a");
475
- px(g, 14, 18, "#5a5a5a"); px(g, 15, 18, "#5a5a5a");
476
- // Bottom shadow
477
- row(g, 22, 13, 16, "#15101c");
478
- },
479
- };
480
-
481
- // ════════════════════════════════════════════════════════
482
- // PLACEHOLDER (grey BRICK + "?")
483
- // ════════════════════════════════════════════════════════
484
-
485
- function drawPlaceholder(g) {
486
- const p = { name: "placeholder", base: "#5a5a5a", hi: "#7a7a7a", sh: "#3a3a3a", deep: "#1a1a1a", eye: "#15151a", belt: "#2a2a2a", buckle: "#a0a0a0" };
487
- drawBody(g, p);
488
- // "?" centered on face area (rows 12-15)
489
- const q = ["XXXX", "X X", " XX", " X ", " ", " X "];
490
- const ox = 13, oy = 11;
491
- for (let dy = 0; dy < q.length; dy++)
492
- for (let dx = 0; dx < q[dy].length; dx++)
493
- if (q[dy][dx] === "X") px(g, ox + dx, oy + dy, "#fafafa");
494
- }
495
-
496
- // ════════════════════════════════════════════════════════
497
- // GENERATE
498
- // ════════════════════════════════════════════════════════
499
-
500
- const GLASSES_NAMES = Object.keys(GLASSES);
501
- const MUSTACHE_NAMES = Object.keys(MUSTACHES);
502
- const EXPRESSION_NAMES = Object.keys(EXPRESSIONS);
503
- const PROP_NAMES = Object.keys(PROPS);
504
-
505
- function generate(seed, opts) {
506
- const o = opts || {};
507
- const grid = makeGrid();
508
-
509
- if (o.placeholder) {
510
- drawPlaceholder(grid);
511
- return svgFromGrid(grid);
512
- }
513
-
514
- // CLASSIC: locked terracotta + no accessories (chair)
515
- if (o.variant === "classic") {
516
- const pal = BODY_COLORS[0]; // terracotta
517
- drawBody(grid, pal);
518
- EXPRESSIONS.default(grid, pal);
519
- return svgFromGrid(grid);
520
- }
521
-
522
- const rng = makeRng(seed);
523
-
524
- // Body color — bias slightly toward terracotta + warm earthy tones
525
- const bodyWeights = [4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3];
526
- const palette = weighted(rng, BODY_COLORS, bodyWeights);
527
-
528
- // Glasses — slight bias toward none / common shapes
529
- const glassWeights = [4, 2, 2, 1, 2, 1, 1, 1, 1];
530
- const glassName = weighted(rng, GLASSES_NAMES, glassWeights);
531
-
532
- // Mustache — most have none
533
- const stachWeights = [5, 2, 2, 2, 2, 1, 1, 1, 2, 1];
534
- const stachName = weighted(rng, MUSTACHE_NAMES, stachWeights);
535
-
536
- // Expression — default common, others rare
537
- const exprWeights = [5, 2, 1, 1, 1, 1, 2];
538
- const exprName = weighted(rng, EXPRESSION_NAMES, exprWeights);
539
-
540
- // Prop — most have none
541
- const propWeights = [10, 1, 1, 1, 1, 1, 1, 1, 1];
542
- const propName = weighted(rng, PROP_NAMES, propWeights);
543
-
544
- // ── Compose · order matters ───────────────────────────
545
- drawBody(grid, palette);
546
-
547
- // Expression (eyes + brows) — drawn before glasses so glasses overlay
548
- EXPRESSIONS[exprName](grid, palette);
549
-
550
- // Glasses sit on top of eyes
551
- GLASSES[glassName](grid, palette);
552
-
553
- // Mustache below eyes
554
- MUSTACHES[stachName](grid, palette);
555
-
556
- // Prop drawn very last — held in front of body
557
- PROPS[propName](grid, palette);
558
-
559
- return svgFromGrid(grid);
560
- }
561
-
562
- // ════════════════════════════════════════════════════════
563
- // SVG SERIALIZATION
564
- // ════════════════════════════════════════════════════════
565
-
566
- // Per-avatar viewBox: scan the drawn pixels for their actual bounding
567
- // box, then emit a square viewBox centered on that content with 1-cell
568
- // padding. This auto-centers every avatar regardless of hat presence
569
- // (CLASSIC chair fills its frame, wizard-hatted seats fit hat + body
570
- // without the body being shoved against the bottom edge).
571
- function svgFromGrid(grid) {
572
- let minX = GRID, maxX = -1, minY = GRID, maxY = -1;
573
- let rects = "";
574
- for (let y = 0; y < GRID; y++) {
575
- for (let x = 0; x < GRID; x++) {
576
- const c = grid[y][x];
577
- if (!c) continue;
578
- rects += `<rect x="${x * PX}" y="${y * PX}" width="${PX}" height="${PX}" fill="${c}"/>`;
579
- if (x < minX) minX = x;
580
- if (x > maxX) maxX = x;
581
- if (y < minY) minY = y;
582
- if (y > maxY) maxY = y;
583
- }
584
- }
585
- if (maxX < 0) {
586
- return `<svg viewBox="0 0 ${SIZE} ${SIZE}" xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges"/>`;
587
- }
588
- const w = maxX - minX + 1;
589
- const h = maxY - minY + 1;
590
- const sz = Math.max(w, h) + 4; // +4 cells = 2 cells padding each side
591
- const cx = (minX + maxX + 1) / 2; // content center in cell coords
592
- const cy = (minY + maxY + 1) / 2;
593
- const vbX = (cx - sz / 2) * PX;
594
- const vbY = (cy - sz / 2) * PX;
595
- const vbSize = sz * PX;
596
- return `<svg viewBox="${vbX} ${vbY} ${vbSize} ${vbSize}" xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges" preserveAspectRatio="xMidYMid meet">${rects}</svg>`;
597
- }
598
-
599
- function generateDataUrl(seed, opts) {
600
- return "data:image/svg+xml;utf8," + encodeURIComponent(generate(seed, opts));
601
- }
602
-
603
- function randomSeed() {
604
- return Math.random().toString(36).slice(2, 12);
605
- }
606
-
607
- function attach({ frame, button, onSeed, initialSeed }) {
608
- if (!frame || !button) return;
609
- let seed = initialSeed || null;
610
- function paintFrame(nextSeed, opts) {
611
- seed = nextSeed;
612
- frame.innerHTML = generate(nextSeed, opts);
613
- if (typeof onSeed === "function") onSeed(seed);
614
- }
615
- if (seed) paintFrame(seed);
616
- else paintFrame("__placeholder__", { placeholder: true });
617
- button.addEventListener("click", (e) => {
618
- e.preventDefault();
619
- paintFrame(randomSeed());
620
- });
621
- }
622
-
623
- window.AvatarSkill = {
624
- generate,
625
- generateDataUrl,
626
- randomSeed,
627
- attach,
628
- };
629
- })();