privateboard 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/dist/cli.js +10502 -0
- package/dist/cli.js.map +1 -0
- package/package.json +63 -0
- package/public/adjourn-overlay.css +253 -0
- package/public/agent-overlay.css +444 -0
- package/public/agent-overlay.js +604 -0
- package/public/agent-profile.css +3230 -0
- package/public/agent-profile.js +3329 -0
- package/public/app.js +6629 -0
- package/public/auto-hide-scroll.js +90 -0
- package/public/avatar-skill.js +793 -0
- package/public/avatars/chair.svg +98 -0
- package/public/avatars/first-principles.svg +122 -0
- package/public/avatars/long-horizon.svg +147 -0
- package/public/avatars/open_ai.png +0 -0
- package/public/avatars/phenomenologist.svg +130 -0
- package/public/avatars/socrates.svg +187 -0
- package/public/avatars/user-empathy.svg +117 -0
- package/public/avatars/value-investor.svg +117 -0
- package/public/favicon.svg +10 -0
- package/public/fonts/agent-Italic.woff2 +0 -0
- package/public/fonts/human-sans.woff2 +0 -0
- package/public/icons.css +103 -0
- package/public/models-cache.js +57 -0
- package/public/new-agent.css +1359 -0
- package/public/new-agent.js +675 -0
- package/public/onboarding.css +628 -0
- package/public/onboarding.js +782 -0
- package/public/prototype-dashboard.html +7596 -0
- package/public/report/spines/a16z-thesis.css +1055 -0
- package/public/report/spines/anthropic-essay.css +556 -0
- package/public/report/spines/boardroom-dark.css +1082 -0
- package/public/report/spines/gartner-note.css +538 -0
- package/public/report/spines/mckinsey-deck.css +523 -0
- package/public/report/spines/openai-paper.css +516 -0
- package/public/report.html +1417 -0
- package/public/room-settings.css +895 -0
- package/public/room-settings.js +1039 -0
- package/public/themes.css +338 -0
- package/public/user-settings.css +1236 -0
- package/public/user-settings.js +1291 -0
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
2
|
+
AVATAR SKILL · 48×56 chibi pixel-art SVG generator
|
|
3
|
+
─────────────────────────────────────────────────────────────
|
|
4
|
+
Head-only chibi sprites in the same hand-crafted style as the
|
|
5
|
+
seeded directors (avatars/socrates.svg, first-principles.svg, etc.):
|
|
6
|
+
· rounded face (chamfered top + chin)
|
|
7
|
+
· multi-tone skin shading (highlight / mid / shadow / outline)
|
|
8
|
+
· varied hair styles + colours, glasses, eyebrows, mouth
|
|
9
|
+
· neck stub at the bottom (no body / clothes)
|
|
10
|
+
viewBox is `0 -4 48 56` so the head sits with breathing room
|
|
11
|
+
at the top and the neck stub fades into the canvas bottom.
|
|
12
|
+
|
|
13
|
+
Public API · window.AvatarSkill:
|
|
14
|
+
generate(seed?, opts?) → SVG markup string
|
|
15
|
+
generateDataUrl(seed?, opts?) → "data:image/svg+xml;…"
|
|
16
|
+
randomSeed() → fresh seed string
|
|
17
|
+
attach({ frame, button, onSeed }) → wire a UI in one line.
|
|
18
|
+
|
|
19
|
+
opts.placeholder = true → neutral grey silhouette w/ "?".
|
|
20
|
+
═══════════════════════════════════════════════════════════════ */
|
|
21
|
+
|
|
22
|
+
(function () {
|
|
23
|
+
if (window.AvatarSkill) return;
|
|
24
|
+
|
|
25
|
+
// ── Constants ────────────────────────────────────────────
|
|
26
|
+
const W = 48;
|
|
27
|
+
const H = 56;
|
|
28
|
+
// The visible Y range is -4..52. Internally we operate on 0..H
|
|
29
|
+
// and serialize with viewBox="0 -4 48 56".
|
|
30
|
+
|
|
31
|
+
// ── Palettes (multi-tone) ────────────────────────────────
|
|
32
|
+
const SKIN = [
|
|
33
|
+
{ hl: "#FFE5C5", mid: "#F2B585", shadow: "#D69570", deep: "#A8704A", outline: "#5A3520" }, // peach
|
|
34
|
+
{ hl: "#FFCFA2", mid: "#E8B084", shadow: "#D69970", deep: "#B58050", outline: "#6E4830" }, // warm tan
|
|
35
|
+
{ hl: "#EFD0AC", mid: "#D6A878", shadow: "#B58858", deep: "#6E4830", outline: "#3F2818" }, // olive (chair)
|
|
36
|
+
{ hl: "#E5BFA0", mid: "#C68863", shadow: "#A86B47", deep: "#6F4528", outline: "#2A1A12" }, // medium
|
|
37
|
+
{ hl: "#C68863", mid: "#A86B47", shadow: "#8B5A3C", deep: "#5A3520", outline: "#1A0A05" }, // bronze
|
|
38
|
+
{ hl: "#A86B47", mid: "#8B5A3C", shadow: "#6F4528", deep: "#3A2014", outline: "#1A0A05" }, // deep
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Each hair colour ships highlight / mid / shadow / outline.
|
|
42
|
+
const HAIR = [
|
|
43
|
+
{ name: "black-blue", hl: "#3A5A9C", mid: "#1F2A4A", shadow: "#0A0A1A", outline: "#0A0A1A", brow: "#0A0A1A" },
|
|
44
|
+
{ name: "dark-brown", hl: "#5C3A22", mid: "#3A2418", shadow: "#1F1006", outline: "#0E0501", brow: "#1F1006" },
|
|
45
|
+
{ name: "chestnut", hl: "#A05A2C", mid: "#7B4F2A", shadow: "#5C3A22", outline: "#2A1A0A", brow: "#5C3A22" },
|
|
46
|
+
{ name: "auburn", hl: "#F5904A", mid: "#E26336", shadow: "#A8482A", outline: "#5A1808", brow: "#A8482A" },
|
|
47
|
+
{ name: "blonde", hl: "#FFE072", mid: "#F2C037", shadow: "#C99826", outline: "#7A5A14", brow: "#A07820" },
|
|
48
|
+
{ name: "silver", hl: "#FFFFFF", mid: "#C8C5BE", shadow: "#9C9890", outline: "#3A3A3A", brow: "#6E665F" },
|
|
49
|
+
{ name: "white", hl: "#FFFFFF", mid: "#F0EDE5", shadow: "#DAD5C8", outline: "#6E665F", brow: "#DAD5C8" },
|
|
50
|
+
{ name: "red", hl: "#FF6A38", mid: "#D44820", shadow: "#A8281A", outline: "#5A1808", brow: "#A8281A" },
|
|
51
|
+
{ name: "plum", hl: "#A88BD1", mid: "#7B5AA8", shadow: "#5A3D8F", outline: "#2A1A4A", brow: "#5A3D8F" },
|
|
52
|
+
{ name: "navy", hl: "#3E78C8", mid: "#2D3E66", shadow: "#1A2540", outline: "#0A1228", brow: "#1A2540" },
|
|
53
|
+
{ name: "ginger", hl: "#F5A05A", mid: "#C46A2C", shadow: "#8E4818", outline: "#3F1F08", brow: "#8E4818" },
|
|
54
|
+
{ name: "mint", hl: "#A8E5C8", mid: "#5DBE9A", shadow: "#2F7A60", outline: "#0E3A28", brow: "#2F7A60" },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// Eye iris colors (shown when chance hits)
|
|
58
|
+
const IRIS = [
|
|
59
|
+
"#3E78C8", // sky blue
|
|
60
|
+
"#7BA0E8", // pale blue
|
|
61
|
+
"#5A8A4D", // green
|
|
62
|
+
"#7A5A3A", // hazel
|
|
63
|
+
"#C99826", // amber
|
|
64
|
+
"#6A9B97", // cyan
|
|
65
|
+
"#5C3A22", // brown
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// Glasses · 4 distinct shapes + 5 frame colors
|
|
69
|
+
const GLASSES_FRAMES = [
|
|
70
|
+
{ rim: "#1A1A1A", hi: "#3A3A3A", name: "black" },
|
|
71
|
+
{ rim: "#2C4AAB", hi: "#7BA0E8", name: "royal-blue" },
|
|
72
|
+
{ rim: "#A87818", hi: "#F2C037", name: "gold" },
|
|
73
|
+
{ rim: "#7A4838", hi: "#A86B47", name: "tortoise" },
|
|
74
|
+
{ rim: "#9C9890", hi: "#E0DDD3", name: "silver" },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// Beanie palettes (multi-tone)
|
|
78
|
+
const BEANIES = [
|
|
79
|
+
{ name: "red", hl: "#F5604A", mid: "#E03020", shadow: "#A82820", outline: "#5A0E08" },
|
|
80
|
+
{ name: "yellow", hl: "#FFE560", mid: "#FFC830", shadow: "#C98818", outline: "#7A4F0A" },
|
|
81
|
+
{ name: "navy", hl: "#3E78C8", mid: "#2D3E66", shadow: "#1A2540", outline: "#0A1228" },
|
|
82
|
+
{ name: "forest", hl: "#5DBE3F", mid: "#2F7A24", shadow: "#1A4818", outline: "#0E2A0E" },
|
|
83
|
+
{ name: "plum", hl: "#A88BD1", mid: "#7B5AA8", shadow: "#5A3D8F", outline: "#2A1A4A" },
|
|
84
|
+
{ name: "charcoal",hl: "#9C948C", mid: "#5A5A5A", shadow: "#3A3A3A", outline: "#1A1A1A" },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
// ── Pixel grid + RNG ─────────────────────────────────────
|
|
88
|
+
function makeGrid() {
|
|
89
|
+
const g = new Array(H);
|
|
90
|
+
for (let y = 0; y < H; y++) g[y] = new Array(W).fill(null);
|
|
91
|
+
return g;
|
|
92
|
+
}
|
|
93
|
+
function px(grid, x, y, color) {
|
|
94
|
+
if (x < 0 || x >= W || y < 0 || y >= H) return;
|
|
95
|
+
grid[y][x] = color;
|
|
96
|
+
}
|
|
97
|
+
function fillRect(grid, x, y, w, h, color) {
|
|
98
|
+
for (let dy = 0; dy < h; dy++) {
|
|
99
|
+
for (let dx = 0; dx < w; dx++) px(grid, x + dx, y + dy, color);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function makeRng(seed) {
|
|
104
|
+
let s = 0;
|
|
105
|
+
const str = String(seed || "default");
|
|
106
|
+
for (let i = 0; i < str.length; i++) s = (s * 31 + str.charCodeAt(i)) >>> 0;
|
|
107
|
+
return function () {
|
|
108
|
+
s = (s * 1664525 + 1013904223) >>> 0;
|
|
109
|
+
return s / 0xFFFFFFFF;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function pick(rng, arr) { return arr[Math.floor(rng() * arr.length)]; }
|
|
113
|
+
function chance(rng, p) { return rng() < p; }
|
|
114
|
+
function weighted(rng, items, weights) {
|
|
115
|
+
let total = 0;
|
|
116
|
+
for (const w of weights) total += w;
|
|
117
|
+
let r = rng() * total;
|
|
118
|
+
for (let i = 0; i < items.length; i++) {
|
|
119
|
+
r -= weights[i];
|
|
120
|
+
if (r <= 0) return items[i];
|
|
121
|
+
}
|
|
122
|
+
return items[items.length - 1];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── ROUNDED HEAD · skin fill + outline ───────────────────
|
|
126
|
+
// Shape:
|
|
127
|
+
// y=10: 22w (top chamfer) x=13..34
|
|
128
|
+
// y=11: 26w x=11..36
|
|
129
|
+
// y=12: 30w x=9..38
|
|
130
|
+
// y=13..29: full 30w main x=9..38
|
|
131
|
+
// y=30: 30w
|
|
132
|
+
// y=31: 26w (chin chamfer)
|
|
133
|
+
// y=32: 22w (chin tip)
|
|
134
|
+
function drawHead(grid, skin) {
|
|
135
|
+
// outline silhouette
|
|
136
|
+
fillRect(grid, 13, 10, 22, 1, skin.outline);
|
|
137
|
+
fillRect(grid, 11, 11, 2, 1, skin.outline);
|
|
138
|
+
fillRect(grid, 35, 11, 2, 1, skin.outline);
|
|
139
|
+
fillRect(grid, 9, 12, 2, 1, skin.outline);
|
|
140
|
+
fillRect(grid, 37, 12, 2, 1, skin.outline);
|
|
141
|
+
fillRect(grid, 8, 13, 1, 17, skin.outline);
|
|
142
|
+
fillRect(grid, 39, 13, 1, 17, skin.outline);
|
|
143
|
+
fillRect(grid, 9, 30, 2, 1, skin.outline);
|
|
144
|
+
fillRect(grid, 37, 30, 2, 1, skin.outline);
|
|
145
|
+
fillRect(grid, 11, 31, 2, 1, skin.outline);
|
|
146
|
+
fillRect(grid, 35, 31, 2, 1, skin.outline);
|
|
147
|
+
fillRect(grid, 13, 32, 22, 1, skin.outline);
|
|
148
|
+
// skin fill (rounded interior)
|
|
149
|
+
fillRect(grid, 13, 10, 22, 1, skin.mid);
|
|
150
|
+
fillRect(grid, 11, 11, 26, 1, skin.mid);
|
|
151
|
+
fillRect(grid, 9, 12, 30, 1, skin.mid);
|
|
152
|
+
fillRect(grid, 9, 13, 30, 17, skin.mid);
|
|
153
|
+
fillRect(grid, 9, 30, 30, 1, skin.mid);
|
|
154
|
+
fillRect(grid, 11, 31, 26, 1, skin.mid);
|
|
155
|
+
fillRect(grid, 13, 32, 22, 1, skin.mid);
|
|
156
|
+
// forehead highlight
|
|
157
|
+
fillRect(grid, 13, 11, 22, 2, skin.hl);
|
|
158
|
+
fillRect(grid, 14, 12, 20, 1, skin.hl);
|
|
159
|
+
// temple shadow
|
|
160
|
+
fillRect(grid, 9, 18, 2, 9, skin.deep);
|
|
161
|
+
fillRect(grid, 37, 18, 2, 9, skin.deep);
|
|
162
|
+
// cheekbone highlight
|
|
163
|
+
fillRect(grid, 11, 22, 3, 3, skin.hl);
|
|
164
|
+
fillRect(grid, 34, 22, 3, 3, skin.hl);
|
|
165
|
+
// jaw shadow
|
|
166
|
+
fillRect(grid, 11, 27, 26, 2, skin.shadow);
|
|
167
|
+
fillRect(grid, 13, 29, 22, 2, skin.deep);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── HAIR STYLES ──────────────────────────────────────────
|
|
171
|
+
function drawShortHair(grid, hair) {
|
|
172
|
+
fillRect(grid, 14, 3, 20, 1, hair.outline);
|
|
173
|
+
fillRect(grid, 11, 4, 26, 2, hair.shadow);
|
|
174
|
+
fillRect(grid, 9, 6, 30, 2, hair.shadow);
|
|
175
|
+
fillRect(grid, 9, 8, 30, 2, hair.mid);
|
|
176
|
+
// sideburns (rounded around head edge)
|
|
177
|
+
fillRect(grid, 9, 10, 2, 3, hair.shadow);
|
|
178
|
+
fillRect(grid, 37, 10, 2, 3, hair.shadow);
|
|
179
|
+
fillRect(grid, 11, 10, 2, 2, hair.shadow);
|
|
180
|
+
fillRect(grid, 35, 10, 2, 2, hair.shadow);
|
|
181
|
+
// sheen highlights
|
|
182
|
+
fillRect(grid, 14, 5, 6, 1, hair.hl);
|
|
183
|
+
fillRect(grid, 24, 5, 6, 1, hair.hl);
|
|
184
|
+
fillRect(grid, 11, 7, 5, 1, hair.hl);
|
|
185
|
+
fillRect(grid, 28, 7, 5, 1, hair.hl);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function drawSidePartHair(grid, hair) {
|
|
189
|
+
fillRect(grid, 12, 3, 24, 1, hair.outline);
|
|
190
|
+
fillRect(grid, 10, 4, 28, 1, hair.shadow);
|
|
191
|
+
fillRect(grid, 9, 5, 30, 1, hair.shadow);
|
|
192
|
+
fillRect(grid, 9, 6, 30, 2, hair.mid);
|
|
193
|
+
fillRect(grid, 9, 8, 30, 2, hair.mid);
|
|
194
|
+
fillRect(grid, 9, 10, 2, 3, hair.shadow);
|
|
195
|
+
fillRect(grid, 37, 10, 2, 3, hair.shadow);
|
|
196
|
+
fillRect(grid, 11, 10, 2, 2, hair.shadow);
|
|
197
|
+
fillRect(grid, 35, 10, 2, 2, hair.shadow);
|
|
198
|
+
// side parting line (clear strip)
|
|
199
|
+
fillRect(grid, 16, 5, 1, 5, hair.hl);
|
|
200
|
+
fillRect(grid, 17, 5, 6, 1, hair.hl);
|
|
201
|
+
fillRect(grid, 25, 5, 5, 1, hair.hl);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function drawLongHair(grid, hair) {
|
|
205
|
+
// top mass
|
|
206
|
+
fillRect(grid, 11, 2, 26, 1, hair.shadow);
|
|
207
|
+
fillRect(grid, 9, 3, 30, 1, hair.mid);
|
|
208
|
+
fillRect(grid, 8, 4, 32, 2, hair.mid);
|
|
209
|
+
fillRect(grid, 7, 6, 34, 3, hair.mid);
|
|
210
|
+
fillRect(grid, 7, 9, 34, 2, hair.shadow);
|
|
211
|
+
// side hair flowing past head
|
|
212
|
+
fillRect(grid, 6, 11, 3, 20, hair.mid);
|
|
213
|
+
fillRect(grid, 39, 11, 3, 20, hair.mid);
|
|
214
|
+
fillRect(grid, 5, 14, 2, 17, hair.shadow);
|
|
215
|
+
fillRect(grid, 41, 14, 2, 17, hair.shadow);
|
|
216
|
+
// outline
|
|
217
|
+
fillRect(grid, 4, 17, 1, 14, hair.outline);
|
|
218
|
+
fillRect(grid, 43, 17, 1, 14, hair.outline);
|
|
219
|
+
fillRect(grid, 11, 2, 1, 9, hair.outline);
|
|
220
|
+
fillRect(grid, 36, 2, 1, 9, hair.outline);
|
|
221
|
+
// highlights
|
|
222
|
+
fillRect(grid, 13, 4, 4, 1, hair.hl);
|
|
223
|
+
fillRect(grid, 22, 4, 5, 1, hair.hl);
|
|
224
|
+
fillRect(grid, 32, 4, 3, 1, hair.hl);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function drawCurlyHair(grid, hair) {
|
|
228
|
+
// top curl bumps (5 separate clusters)
|
|
229
|
+
fillRect(grid, 11, 2, 3, 1, hair.outline);
|
|
230
|
+
fillRect(grid, 16, 2, 3, 1, hair.outline);
|
|
231
|
+
fillRect(grid, 22, 2, 3, 1, hair.outline);
|
|
232
|
+
fillRect(grid, 28, 2, 3, 1, hair.outline);
|
|
233
|
+
fillRect(grid, 33, 2, 3, 1, hair.outline);
|
|
234
|
+
fillRect(grid, 9, 3, 30, 1, hair.shadow);
|
|
235
|
+
fillRect(grid, 8, 4, 32, 2, hair.shadow);
|
|
236
|
+
fillRect(grid, 7, 6, 34, 3, hair.mid);
|
|
237
|
+
fillRect(grid, 7, 9, 34, 2, hair.hl);
|
|
238
|
+
// side curls flowing past head
|
|
239
|
+
fillRect(grid, 6, 11, 3, 18, hair.mid);
|
|
240
|
+
fillRect(grid, 39, 11, 3, 18, hair.mid);
|
|
241
|
+
fillRect(grid, 5, 14, 2, 14, hair.hl);
|
|
242
|
+
fillRect(grid, 41, 14, 2, 14, hair.hl);
|
|
243
|
+
// curl outline
|
|
244
|
+
fillRect(grid, 4, 14, 1, 14, hair.outline);
|
|
245
|
+
fillRect(grid, 43, 14, 1, 14, hair.outline);
|
|
246
|
+
fillRect(grid, 5, 28, 2, 3, hair.outline);
|
|
247
|
+
fillRect(grid, 41, 28, 2, 3, hair.outline);
|
|
248
|
+
// texture bumps on sides
|
|
249
|
+
px(grid, 5, 17, hair.hl);
|
|
250
|
+
px(grid, 5, 19, hair.hl);
|
|
251
|
+
px(grid, 5, 22, hair.hl);
|
|
252
|
+
px(grid, 5, 24, hair.hl);
|
|
253
|
+
px(grid, 42, 17, hair.hl);
|
|
254
|
+
px(grid, 42, 19, hair.hl);
|
|
255
|
+
px(grid, 42, 22, hair.hl);
|
|
256
|
+
px(grid, 42, 24, hair.hl);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function drawAfroHair(grid, hair) {
|
|
260
|
+
// big round puffy mass
|
|
261
|
+
fillRect(grid, 13, 1, 22, 1, hair.outline);
|
|
262
|
+
fillRect(grid, 11, 2, 26, 2, hair.shadow);
|
|
263
|
+
fillRect(grid, 8, 4, 32, 2, hair.shadow);
|
|
264
|
+
fillRect(grid, 6, 6, 36, 3, hair.mid);
|
|
265
|
+
fillRect(grid, 6, 9, 36, 2, hair.mid);
|
|
266
|
+
fillRect(grid, 5, 11, 4, 6, hair.mid);
|
|
267
|
+
fillRect(grid, 39, 11, 4, 6, hair.mid);
|
|
268
|
+
// bumps for texture
|
|
269
|
+
fillRect(grid, 4, 13, 1, 4, hair.outline);
|
|
270
|
+
fillRect(grid, 43, 13, 1, 4, hair.outline);
|
|
271
|
+
fillRect(grid, 5, 17, 4, 1, hair.outline);
|
|
272
|
+
fillRect(grid, 39, 17, 4, 1, hair.outline);
|
|
273
|
+
// highlights (catching light at the top)
|
|
274
|
+
fillRect(grid, 14, 3, 4, 1, hair.hl);
|
|
275
|
+
fillRect(grid, 22, 3, 4, 1, hair.hl);
|
|
276
|
+
fillRect(grid, 30, 3, 4, 1, hair.hl);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function drawBunHair(grid, hair) {
|
|
280
|
+
// base hair around head
|
|
281
|
+
fillRect(grid, 12, 4, 24, 1, hair.outline);
|
|
282
|
+
fillRect(grid, 10, 5, 28, 1, hair.shadow);
|
|
283
|
+
fillRect(grid, 9, 6, 30, 2, hair.mid);
|
|
284
|
+
fillRect(grid, 9, 8, 30, 2, hair.mid);
|
|
285
|
+
fillRect(grid, 9, 10, 2, 3, hair.shadow);
|
|
286
|
+
fillRect(grid, 37, 10, 2, 3, hair.shadow);
|
|
287
|
+
fillRect(grid, 11, 10, 2, 2, hair.shadow);
|
|
288
|
+
fillRect(grid, 35, 10, 2, 2, hair.shadow);
|
|
289
|
+
// bun on top
|
|
290
|
+
fillRect(grid, 19, 0, 10, 1, hair.outline);
|
|
291
|
+
fillRect(grid, 17, 1, 14, 2, hair.shadow);
|
|
292
|
+
fillRect(grid, 18, 1, 12, 1, hair.mid);
|
|
293
|
+
fillRect(grid, 18, 3, 12, 1, hair.shadow);
|
|
294
|
+
fillRect(grid, 20, 1, 4, 1, hair.hl);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Bald = nothing drawn (head shows through)
|
|
298
|
+
function drawBaldHair(_grid, _hair) {}
|
|
299
|
+
|
|
300
|
+
const HAIR_STYLES = {
|
|
301
|
+
short: drawShortHair,
|
|
302
|
+
sidepart: drawSidePartHair,
|
|
303
|
+
long: drawLongHair,
|
|
304
|
+
curly: drawCurlyHair,
|
|
305
|
+
afro: drawAfroHair,
|
|
306
|
+
bun: drawBunHair,
|
|
307
|
+
bald: drawBaldHair,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// ── ACCESSORIES ──────────────────────────────────────────
|
|
311
|
+
function drawBeanie(grid, beanie) {
|
|
312
|
+
fillRect(grid, 14, 5, 20, 1, beanie.outline);
|
|
313
|
+
fillRect(grid, 11, 6, 26, 1, beanie.shadow);
|
|
314
|
+
fillRect(grid, 9, 7, 30, 1, beanie.shadow);
|
|
315
|
+
fillRect(grid, 9, 8, 30, 3, beanie.mid);
|
|
316
|
+
fillRect(grid, 9, 11, 30, 2, beanie.hl);
|
|
317
|
+
fillRect(grid, 9, 13, 30, 3, beanie.shadow);
|
|
318
|
+
// ribbing bars
|
|
319
|
+
for (const x of [11, 14, 17, 20, 23, 26, 29, 32, 35]) {
|
|
320
|
+
fillRect(grid, x, 13, 1, 3, beanie.outline);
|
|
321
|
+
}
|
|
322
|
+
fillRect(grid, 9, 6, 1, 10, beanie.outline);
|
|
323
|
+
fillRect(grid, 38, 6, 1, 10, beanie.outline);
|
|
324
|
+
fillRect(grid, 13, 9, 4, 1, beanie.hl);
|
|
325
|
+
fillRect(grid, 22, 9, 4, 1, beanie.hl);
|
|
326
|
+
// pom-pom
|
|
327
|
+
fillRect(grid, 22, 1, 4, 1, "#8E8675");
|
|
328
|
+
fillRect(grid, 21, 2, 6, 2, "#FFFFFF");
|
|
329
|
+
fillRect(grid, 22, 4, 4, 1, "#E5E2D8");
|
|
330
|
+
px(grid, 20, 3, "#B8B0A0");
|
|
331
|
+
px(grid, 27, 3, "#B8B0A0");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function drawLaurel(grid) {
|
|
335
|
+
// top leaves
|
|
336
|
+
fillRect(grid, 22, 2, 4, 1, "#1A4818");
|
|
337
|
+
fillRect(grid, 22, 3, 4, 1, "#2F7A24");
|
|
338
|
+
fillRect(grid, 23, 4, 2, 1, "#5DBE3F");
|
|
339
|
+
fillRect(grid, 16, 3, 3, 1, "#1A4818");
|
|
340
|
+
fillRect(grid, 15, 4, 4, 1, "#2F7A24");
|
|
341
|
+
fillRect(grid, 16, 5, 3, 1, "#4DA53D");
|
|
342
|
+
fillRect(grid, 29, 3, 3, 1, "#1A4818");
|
|
343
|
+
fillRect(grid, 29, 4, 4, 1, "#2F7A24");
|
|
344
|
+
fillRect(grid, 29, 5, 3, 1, "#4DA53D");
|
|
345
|
+
fillRect(grid, 9, 5, 3, 1, "#1A4818");
|
|
346
|
+
fillRect(grid, 9, 6, 4, 1, "#2F7A24");
|
|
347
|
+
fillRect(grid, 10, 7, 3, 1, "#4DA53D");
|
|
348
|
+
fillRect(grid, 36, 5, 3, 1, "#1A4818");
|
|
349
|
+
fillRect(grid, 35, 6, 4, 1, "#2F7A24");
|
|
350
|
+
fillRect(grid, 35, 7, 3, 1, "#4DA53D");
|
|
351
|
+
// circlet band
|
|
352
|
+
fillRect(grid, 8, 8, 32, 1, "#0E2A0E");
|
|
353
|
+
fillRect(grid, 7, 9, 34, 1, "#1A4818");
|
|
354
|
+
fillRect(grid, 7, 10, 34, 2, "#2F7A24");
|
|
355
|
+
fillRect(grid, 7, 12, 34, 1, "#4DA53D");
|
|
356
|
+
fillRect(grid, 9, 11, 3, 1, "#5DBE3F");
|
|
357
|
+
fillRect(grid, 14, 11, 3, 1, "#7CD850");
|
|
358
|
+
fillRect(grid, 20, 11, 3, 1, "#5DBE3F");
|
|
359
|
+
fillRect(grid, 26, 11, 3, 1, "#7CD850");
|
|
360
|
+
fillRect(grid, 32, 11, 3, 1, "#5DBE3F");
|
|
361
|
+
// berries
|
|
362
|
+
for (const bx of [13, 22, 31]) {
|
|
363
|
+
fillRect(grid, bx, 9, 2, 2, "#7A5A14");
|
|
364
|
+
fillRect(grid, bx, 9, 2, 1, "#F2C037");
|
|
365
|
+
px(grid, bx, 9, "#FFE072");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function drawHeadband(grid, color) {
|
|
370
|
+
fillRect(grid, 9, 10, 30, 2, color);
|
|
371
|
+
fillRect(grid, 9, 10, 30, 1, "#FFFFFF");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function drawHat(grid, hair) {
|
|
375
|
+
// top brim
|
|
376
|
+
fillRect(grid, 11, 4, 26, 1, hair.outline);
|
|
377
|
+
fillRect(grid, 9, 5, 30, 1, hair.shadow);
|
|
378
|
+
// hat body
|
|
379
|
+
fillRect(grid, 12, 6, 24, 5, hair.mid);
|
|
380
|
+
fillRect(grid, 12, 6, 24, 1, hair.hl);
|
|
381
|
+
// hat band
|
|
382
|
+
fillRect(grid, 12, 9, 24, 1, hair.outline);
|
|
383
|
+
// brim wide
|
|
384
|
+
fillRect(grid, 6, 11, 36, 1, hair.outline);
|
|
385
|
+
fillRect(grid, 7, 12, 34, 1, hair.shadow);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function drawHood(grid, color) {
|
|
389
|
+
fillRect(grid, 9, 4, 30, 1, color);
|
|
390
|
+
fillRect(grid, 7, 5, 34, 2, color);
|
|
391
|
+
fillRect(grid, 6, 7, 36, 4, color);
|
|
392
|
+
fillRect(grid, 5, 11, 4, 18, color);
|
|
393
|
+
fillRect(grid, 39, 11, 4, 18, color);
|
|
394
|
+
// outline
|
|
395
|
+
fillRect(grid, 5, 4, 1, 25, "#1A1A1A");
|
|
396
|
+
fillRect(grid, 42, 4, 1, 25, "#1A1A1A");
|
|
397
|
+
// inner shadow
|
|
398
|
+
fillRect(grid, 9, 10, 2, 18, "#000000");
|
|
399
|
+
fillRect(grid, 37, 10, 2, 18, "#000000");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── EYEBROWS ─────────────────────────────────────────────
|
|
403
|
+
function drawEyebrows(grid, hair, kind) {
|
|
404
|
+
const c = hair.brow || hair.shadow;
|
|
405
|
+
if (kind === "bushy") {
|
|
406
|
+
fillRect(grid, 13, 19, 9, 1, c);
|
|
407
|
+
fillRect(grid, 26, 19, 9, 1, c);
|
|
408
|
+
fillRect(grid, 13, 20, 9, 1, hair.shadow);
|
|
409
|
+
fillRect(grid, 26, 20, 9, 1, hair.shadow);
|
|
410
|
+
// straggler hairs
|
|
411
|
+
px(grid, 14, 18, hair.shadow);
|
|
412
|
+
px(grid, 20, 18, hair.shadow);
|
|
413
|
+
px(grid, 28, 18, hair.shadow);
|
|
414
|
+
px(grid, 33, 18, hair.shadow);
|
|
415
|
+
} else if (kind === "sharp") {
|
|
416
|
+
fillRect(grid, 13, 19, 9, 1, c);
|
|
417
|
+
fillRect(grid, 26, 19, 9, 1, c);
|
|
418
|
+
fillRect(grid, 20, 20, 2, 1, c);
|
|
419
|
+
fillRect(grid, 26, 20, 2, 1, c);
|
|
420
|
+
} else if (kind === "raised") {
|
|
421
|
+
fillRect(grid, 13, 19, 7, 1, c);
|
|
422
|
+
fillRect(grid, 28, 19, 7, 1, c);
|
|
423
|
+
fillRect(grid, 28, 18, 3, 1, c); // right brow lifted
|
|
424
|
+
} else {
|
|
425
|
+
// "soft" default
|
|
426
|
+
fillRect(grid, 13, 19, 7, 1, c);
|
|
427
|
+
fillRect(grid, 28, 19, 7, 1, c);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── EYES ─────────────────────────────────────────────────
|
|
432
|
+
function drawEyes(grid, irisColor, hasLashes) {
|
|
433
|
+
// sclera
|
|
434
|
+
fillRect(grid, 14, 21, 6, 3, "#FFFFFF");
|
|
435
|
+
fillRect(grid, 28, 21, 6, 3, "#FFFFFF");
|
|
436
|
+
// pupils
|
|
437
|
+
fillRect(grid, 15, 21, 4, 3, "#1F1F1F");
|
|
438
|
+
fillRect(grid, 29, 21, 4, 3, "#1F1F1F");
|
|
439
|
+
// iris (smaller circle inside pupil)
|
|
440
|
+
if (irisColor) {
|
|
441
|
+
fillRect(grid, 15, 21, 2, 2, irisColor);
|
|
442
|
+
fillRect(grid, 29, 21, 2, 2, irisColor);
|
|
443
|
+
}
|
|
444
|
+
// catchlight
|
|
445
|
+
px(grid, 15, 21, "#FFFFFF");
|
|
446
|
+
px(grid, 29, 21, "#FFFFFF");
|
|
447
|
+
// upper lid line
|
|
448
|
+
fillRect(grid, 14, 20, 6, 1, "#5A3520");
|
|
449
|
+
fillRect(grid, 28, 20, 6, 1, "#5A3520");
|
|
450
|
+
// lashes
|
|
451
|
+
if (hasLashes) {
|
|
452
|
+
px(grid, 13, 20, "#1F1F1F");
|
|
453
|
+
px(grid, 19, 20, "#1F1F1F");
|
|
454
|
+
px(grid, 27, 20, "#1F1F1F");
|
|
455
|
+
px(grid, 33, 20, "#1F1F1F");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ── GLASSES ──────────────────────────────────────────────
|
|
460
|
+
function drawSquareGlasses(grid, frame) {
|
|
461
|
+
// left lens outline
|
|
462
|
+
fillRect(grid, 11, 18, 11, 1, frame.rim);
|
|
463
|
+
fillRect(grid, 11, 24, 11, 1, frame.rim);
|
|
464
|
+
fillRect(grid, 11, 19, 1, 5, frame.rim);
|
|
465
|
+
fillRect(grid, 21, 19, 1, 5, frame.rim);
|
|
466
|
+
// right lens
|
|
467
|
+
fillRect(grid, 26, 18, 11, 1, frame.rim);
|
|
468
|
+
fillRect(grid, 26, 24, 11, 1, frame.rim);
|
|
469
|
+
fillRect(grid, 26, 19, 1, 5, frame.rim);
|
|
470
|
+
fillRect(grid, 36, 19, 1, 5, frame.rim);
|
|
471
|
+
// bridge
|
|
472
|
+
fillRect(grid, 22, 20, 4, 1, frame.rim);
|
|
473
|
+
// temple arms
|
|
474
|
+
fillRect(grid, 9, 20, 2, 1, frame.rim);
|
|
475
|
+
fillRect(grid, 37, 20, 2, 1, frame.rim);
|
|
476
|
+
// lens highlights
|
|
477
|
+
fillRect(grid, 13, 19, 3, 1, frame.hi);
|
|
478
|
+
fillRect(grid, 28, 19, 3, 1, frame.hi);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function drawRoundGlasses(grid, frame) {
|
|
482
|
+
// big round Lennon-style
|
|
483
|
+
// left lens (circular outline)
|
|
484
|
+
fillRect(grid, 13, 20, 6, 1, frame.rim);
|
|
485
|
+
fillRect(grid, 13, 24, 6, 1, frame.rim);
|
|
486
|
+
fillRect(grid, 12, 21, 1, 3, frame.rim);
|
|
487
|
+
fillRect(grid, 19, 21, 1, 3, frame.rim);
|
|
488
|
+
// right lens
|
|
489
|
+
fillRect(grid, 28, 20, 6, 1, frame.rim);
|
|
490
|
+
fillRect(grid, 28, 24, 6, 1, frame.rim);
|
|
491
|
+
fillRect(grid, 27, 21, 1, 3, frame.rim);
|
|
492
|
+
fillRect(grid, 34, 21, 1, 3, frame.rim);
|
|
493
|
+
// bridge
|
|
494
|
+
fillRect(grid, 20, 22, 7, 1, frame.rim);
|
|
495
|
+
// temple arms
|
|
496
|
+
fillRect(grid, 9, 22, 3, 1, frame.rim);
|
|
497
|
+
fillRect(grid, 35, 22, 3, 1, frame.rim);
|
|
498
|
+
// lens shine
|
|
499
|
+
px(grid, 14, 21, frame.hi);
|
|
500
|
+
px(grid, 29, 21, frame.hi);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function drawWireRimGlasses(grid, frame) {
|
|
504
|
+
// thin gold half-moon scholar style
|
|
505
|
+
fillRect(grid, 14, 18, 6, 1, frame.rim);
|
|
506
|
+
fillRect(grid, 14, 22, 6, 1, frame.rim);
|
|
507
|
+
fillRect(grid, 13, 19, 1, 3, frame.rim);
|
|
508
|
+
fillRect(grid, 20, 19, 1, 3, frame.rim);
|
|
509
|
+
fillRect(grid, 28, 18, 6, 1, frame.rim);
|
|
510
|
+
fillRect(grid, 28, 22, 6, 1, frame.rim);
|
|
511
|
+
fillRect(grid, 27, 19, 1, 3, frame.rim);
|
|
512
|
+
fillRect(grid, 34, 19, 1, 3, frame.rim);
|
|
513
|
+
fillRect(grid, 21, 20, 6, 1, frame.rim);
|
|
514
|
+
fillRect(grid, 11, 20, 2, 1, frame.rim);
|
|
515
|
+
fillRect(grid, 35, 20, 2, 1, frame.rim);
|
|
516
|
+
// shine
|
|
517
|
+
fillRect(grid, 15, 18, 2, 1, frame.hi);
|
|
518
|
+
fillRect(grid, 29, 18, 2, 1, frame.hi);
|
|
519
|
+
px(grid, 14, 19, frame.hi);
|
|
520
|
+
px(grid, 28, 19, frame.hi);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function drawTortoiseGlasses(grid, frame) {
|
|
524
|
+
// rectangular thick frames
|
|
525
|
+
fillRect(grid, 11, 18, 11, 1, "#2A1A12");
|
|
526
|
+
fillRect(grid, 11, 23, 11, 1, "#2A1A12");
|
|
527
|
+
fillRect(grid, 11, 19, 1, 4, "#2A1A12");
|
|
528
|
+
fillRect(grid, 21, 19, 1, 4, "#2A1A12");
|
|
529
|
+
fillRect(grid, 26, 18, 11, 1, "#2A1A12");
|
|
530
|
+
fillRect(grid, 26, 23, 11, 1, "#2A1A12");
|
|
531
|
+
fillRect(grid, 26, 19, 1, 4, "#2A1A12");
|
|
532
|
+
fillRect(grid, 36, 19, 1, 4, "#2A1A12");
|
|
533
|
+
fillRect(grid, 12, 18, 9, 1, frame.rim);
|
|
534
|
+
fillRect(grid, 27, 18, 9, 1, frame.rim);
|
|
535
|
+
fillRect(grid, 12, 19, 1, 4, frame.rim);
|
|
536
|
+
fillRect(grid, 20, 19, 1, 4, frame.rim);
|
|
537
|
+
fillRect(grid, 27, 19, 1, 4, frame.rim);
|
|
538
|
+
fillRect(grid, 35, 19, 1, 4, frame.rim);
|
|
539
|
+
fillRect(grid, 22, 20, 4, 1, frame.rim);
|
|
540
|
+
fillRect(grid, 9, 20, 2, 1, "#2A1A12");
|
|
541
|
+
fillRect(grid, 37, 20, 2, 1, "#2A1A12");
|
|
542
|
+
fillRect(grid, 14, 19, 2, 1, frame.hi);
|
|
543
|
+
fillRect(grid, 29, 19, 2, 1, frame.hi);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ── NOSE ─────────────────────────────────────────────────
|
|
547
|
+
function drawNose(grid, skin) {
|
|
548
|
+
fillRect(grid, 22, 20, 4, 6, skin.mid);
|
|
549
|
+
fillRect(grid, 22, 20, 1, 6, skin.hl);
|
|
550
|
+
fillRect(grid, 22, 20, 1, 3, skin.hl);
|
|
551
|
+
fillRect(grid, 25, 22, 1, 4, skin.shadow);
|
|
552
|
+
fillRect(grid, 22, 26, 4, 1, skin.deep);
|
|
553
|
+
px(grid, 21, 26, skin.deep);
|
|
554
|
+
px(grid, 26, 26, skin.deep);
|
|
555
|
+
px(grid, 23, 26, skin.outline);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ── MOUTH ────────────────────────────────────────────────
|
|
559
|
+
function drawMouth(grid, kind) {
|
|
560
|
+
if (kind === "smile") {
|
|
561
|
+
px(grid, 14, 27, "#A85040");
|
|
562
|
+
px(grid, 33, 27, "#A85040");
|
|
563
|
+
fillRect(grid, 15, 28, 18, 1, "#A85040");
|
|
564
|
+
fillRect(grid, 16, 29, 16, 1, "#D67F5C");
|
|
565
|
+
fillRect(grid, 22, 28, 4, 1, "#FFFFFF"); // tooth shine
|
|
566
|
+
} else if (kind === "frown") {
|
|
567
|
+
fillRect(grid, 18, 28, 12, 1, "#5A3520");
|
|
568
|
+
px(grid, 17, 29, "#5A3520");
|
|
569
|
+
px(grid, 30, 29, "#5A3520");
|
|
570
|
+
} else if (kind === "smirk") {
|
|
571
|
+
fillRect(grid, 17, 28, 14, 1, "#7A4838");
|
|
572
|
+
fillRect(grid, 29, 29, 3, 1, "#7A4838");
|
|
573
|
+
fillRect(grid, 18, 29, 13, 1, "#D67F5C");
|
|
574
|
+
} else {
|
|
575
|
+
// neutral
|
|
576
|
+
fillRect(grid, 18, 28, 12, 1, "#5A4838");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ── BEARDS ───────────────────────────────────────────────
|
|
581
|
+
function drawBeard(grid, kind) {
|
|
582
|
+
if (kind === "white-full") {
|
|
583
|
+
// mustache layer
|
|
584
|
+
fillRect(grid, 13, 27, 9, 1, "#8E8675");
|
|
585
|
+
fillRect(grid, 26, 27, 9, 1, "#8E8675");
|
|
586
|
+
fillRect(grid, 12, 28, 11, 1, "#DAD5C8");
|
|
587
|
+
fillRect(grid, 25, 28, 11, 1, "#DAD5C8");
|
|
588
|
+
fillRect(grid, 11, 29, 13, 1, "#FFFFFF");
|
|
589
|
+
fillRect(grid, 24, 29, 13, 1, "#FFFFFF");
|
|
590
|
+
// beard cascading
|
|
591
|
+
fillRect(grid, 9, 30, 1, 14, "#4D4538");
|
|
592
|
+
fillRect(grid, 38, 30, 1, 14, "#4D4538");
|
|
593
|
+
fillRect(grid, 11, 44, 2, 1, "#4D4538");
|
|
594
|
+
fillRect(grid, 35, 44, 2, 1, "#4D4538");
|
|
595
|
+
fillRect(grid, 13, 45, 22, 1, "#4D4538");
|
|
596
|
+
fillRect(grid, 10, 30, 28, 14, "#F0EDE5");
|
|
597
|
+
fillRect(grid, 13, 45, 22, 1, "#F0EDE5");
|
|
598
|
+
fillRect(grid, 11, 30, 26, 2, "#FFFFFF");
|
|
599
|
+
fillRect(grid, 10, 38, 28, 2, "#DAD5C8");
|
|
600
|
+
fillRect(grid, 10, 40, 28, 2, "#B8B0A0");
|
|
601
|
+
fillRect(grid, 10, 42, 28, 2, "#8E8675");
|
|
602
|
+
// strands
|
|
603
|
+
for (const sx of [13, 17, 20, 24, 27, 31, 34]) {
|
|
604
|
+
fillRect(grid, sx, 32, 1, 12, sx % 7 === 0 ? "#DAD5C8" : "#B8B0A0");
|
|
605
|
+
}
|
|
606
|
+
fillRect(grid, 15, 31, 1, 3, "#FFFFFF");
|
|
607
|
+
fillRect(grid, 22, 31, 1, 3, "#FFFFFF");
|
|
608
|
+
fillRect(grid, 29, 31, 1, 3, "#FFFFFF");
|
|
609
|
+
} else if (kind === "stubble") {
|
|
610
|
+
// scattered shadow pixels along jaw
|
|
611
|
+
for (let x = 12; x < 36; x += 2) {
|
|
612
|
+
if ((x % 3) !== 0) px(grid, x, 27, "#5A3520");
|
|
613
|
+
if ((x % 5) === 0) px(grid, x, 29, "#5A3520");
|
|
614
|
+
}
|
|
615
|
+
fillRect(grid, 12, 30, 24, 1, "#5A3520");
|
|
616
|
+
} else if (kind === "goatee") {
|
|
617
|
+
fillRect(grid, 19, 29, 10, 1, "#3A2418");
|
|
618
|
+
fillRect(grid, 20, 30, 8, 2, "#3A2418");
|
|
619
|
+
fillRect(grid, 21, 32, 6, 1, "#3A2418");
|
|
620
|
+
}
|
|
621
|
+
// null = no beard
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ── NECK STUB ────────────────────────────────────────────
|
|
625
|
+
function drawNeck(grid, skin) {
|
|
626
|
+
fillRect(grid, 19, 33, 10, 6, skin.mid);
|
|
627
|
+
fillRect(grid, 19, 33, 10, 1, skin.deep);
|
|
628
|
+
fillRect(grid, 19, 33, 1, 6, skin.deep);
|
|
629
|
+
fillRect(grid, 28, 33, 1, 6, skin.deep);
|
|
630
|
+
fillRect(grid, 20, 34, 8, 1, skin.hl);
|
|
631
|
+
// collar shadow at bottom (suggesting clothing off-frame)
|
|
632
|
+
for (let x = 14; x < 34; x++) px(grid, x, 39, skin.deep);
|
|
633
|
+
fillRect(grid, 13, 41, 22, 3, skin.outline);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ── PLACEHOLDER (?) ──────────────────────────────────────
|
|
637
|
+
function drawPlaceholder(grid) {
|
|
638
|
+
const placeholderSkin = { hl: "#6E6C63", mid: "#5E5C53", shadow: "#48463F", deep: "#38362F", outline: "#1F1E18" };
|
|
639
|
+
drawHead(grid, placeholderSkin);
|
|
640
|
+
drawNeck(grid, placeholderSkin);
|
|
641
|
+
// "?" centered on the face
|
|
642
|
+
const q = [
|
|
643
|
+
" XXXX ",
|
|
644
|
+
" X X ",
|
|
645
|
+
" X ",
|
|
646
|
+
" XX ",
|
|
647
|
+
" X ",
|
|
648
|
+
" ",
|
|
649
|
+
" X ",
|
|
650
|
+
];
|
|
651
|
+
const ox = 20, oy = 17;
|
|
652
|
+
for (let dy = 0; dy < q.length; dy++) {
|
|
653
|
+
for (let dx = 0; dx < q[dy].length; dx++) {
|
|
654
|
+
if (q[dy][dx] === "X") px(grid, ox + dx, oy + dy, "#1A1A18");
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ── MAIN GENERATE ────────────────────────────────────────
|
|
660
|
+
function generate(seed, opts) {
|
|
661
|
+
const placeholder = !!(opts && opts.placeholder);
|
|
662
|
+
const grid = makeGrid();
|
|
663
|
+
|
|
664
|
+
if (placeholder) {
|
|
665
|
+
drawPlaceholder(grid);
|
|
666
|
+
return svgFromGrid(grid);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const rng = makeRng(seed);
|
|
670
|
+
|
|
671
|
+
// ── Pick features deterministically ────────────────
|
|
672
|
+
const skin = pick(rng, SKIN);
|
|
673
|
+
const hair = pick(rng, HAIR);
|
|
674
|
+
|
|
675
|
+
// Hair style — bald rare, beanie/laurel/hat are accessories that
|
|
676
|
+
// OVERRIDE hair, so they're picked separately.
|
|
677
|
+
const hairStyles = ["short", "sidepart", "long", "curly", "afro", "bun"];
|
|
678
|
+
const hairWeights = [4, 4, 2, 3, 1, 1];
|
|
679
|
+
let hairStyle = weighted(rng, hairStyles, hairWeights);
|
|
680
|
+
if (chance(rng, 0.05)) hairStyle = "bald";
|
|
681
|
+
|
|
682
|
+
// Accessory roll · most agents have nothing on top.
|
|
683
|
+
const accessoryRoll = rng();
|
|
684
|
+
let accessory = null;
|
|
685
|
+
if (accessoryRoll < 0.10) accessory = "beanie";
|
|
686
|
+
else if (accessoryRoll < 0.16) accessory = "laurel";
|
|
687
|
+
else if (accessoryRoll < 0.22) accessory = "headband";
|
|
688
|
+
else if (accessoryRoll < 0.27) accessory = "hat";
|
|
689
|
+
else if (accessoryRoll < 0.30) accessory = "hood";
|
|
690
|
+
|
|
691
|
+
// Eyewear — varied, slight bias toward having glasses.
|
|
692
|
+
const eyewearRoll = rng();
|
|
693
|
+
let eyewear = null;
|
|
694
|
+
if (eyewearRoll < 0.20) eyewear = "square";
|
|
695
|
+
else if (eyewearRoll < 0.36) eyewear = "round";
|
|
696
|
+
else if (eyewearRoll < 0.48) eyewear = "wire";
|
|
697
|
+
else if (eyewearRoll < 0.58) eyewear = "tortoise";
|
|
698
|
+
const glassesFrame = pick(rng, GLASSES_FRAMES);
|
|
699
|
+
|
|
700
|
+
// Eyebrow style
|
|
701
|
+
const browKinds = ["soft", "sharp", "bushy", "raised"];
|
|
702
|
+
const browWeights = [4, 3, 2, 2];
|
|
703
|
+
const brow = weighted(rng, browKinds, browWeights);
|
|
704
|
+
|
|
705
|
+
// Mouth
|
|
706
|
+
const mouth = weighted(rng, ["smile", "smile", "neutral", "frown", "smirk"], [3, 2, 3, 2, 2]);
|
|
707
|
+
|
|
708
|
+
// Beard (only for some agents — bias to none)
|
|
709
|
+
const beardKinds = [null, null, null, null, "stubble", "goatee", "white-full"];
|
|
710
|
+
const beard = pick(rng, beardKinds);
|
|
711
|
+
const mouthCovered = beard === "white-full";
|
|
712
|
+
|
|
713
|
+
// Iris color
|
|
714
|
+
const irisColor = chance(rng, 0.55) ? pick(rng, IRIS) : null;
|
|
715
|
+
const hasLashes = chance(rng, 0.30);
|
|
716
|
+
|
|
717
|
+
// ── Compose ───────────────────────────────────────
|
|
718
|
+
drawHead(grid, skin);
|
|
719
|
+
|
|
720
|
+
// Hair drawn first (so the head shape covers the face area)
|
|
721
|
+
if (accessory === "beanie") {
|
|
722
|
+
drawBeanie(grid, pick(rng, BEANIES));
|
|
723
|
+
} else if (accessory === "hat") {
|
|
724
|
+
drawHat(grid, hair);
|
|
725
|
+
} else if (accessory === "hood") {
|
|
726
|
+
drawHood(grid, hair);
|
|
727
|
+
} else {
|
|
728
|
+
const drawHair = HAIR_STYLES[hairStyle] || HAIR_STYLES.short;
|
|
729
|
+
drawHair(grid, hair);
|
|
730
|
+
if (accessory === "laurel") drawLaurel(grid);
|
|
731
|
+
if (accessory === "headband") drawHeadband(grid, "#" + ((Math.floor(rng() * 0xFFFFFF)).toString(16)).padStart(6, "0"));
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
drawEyebrows(grid, hair, brow);
|
|
735
|
+
|
|
736
|
+
if (eyewear === "square") drawSquareGlasses(grid, glassesFrame);
|
|
737
|
+
else if (eyewear === "round") drawRoundGlasses(grid, glassesFrame);
|
|
738
|
+
else if (eyewear === "wire") drawWireRimGlasses(grid, glassesFrame);
|
|
739
|
+
else if (eyewear === "tortoise") drawTortoiseGlasses(grid, glassesFrame);
|
|
740
|
+
|
|
741
|
+
drawEyes(grid, irisColor, hasLashes);
|
|
742
|
+
drawNose(grid, skin);
|
|
743
|
+
if (!mouthCovered) drawMouth(grid, mouth);
|
|
744
|
+
drawBeard(grid, beard);
|
|
745
|
+
drawNeck(grid, skin);
|
|
746
|
+
|
|
747
|
+
return svgFromGrid(grid);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function svgFromGrid(grid) {
|
|
751
|
+
let rects = "";
|
|
752
|
+
for (let y = 0; y < H; y++) {
|
|
753
|
+
for (let x = 0; x < W; x++) {
|
|
754
|
+
const c = grid[y][x];
|
|
755
|
+
if (c) rects += `<rect x="${x}" y="${y}" width="1" height="1" fill="${c}"/>`;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return `<svg viewBox="0 -4 48 56" xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges" preserveAspectRatio="xMidYMid meet">${rects}</svg>`;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function generateDataUrl(seed, opts) {
|
|
762
|
+
return "data:image/svg+xml;utf8," + encodeURIComponent(generate(seed, opts));
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function randomSeed() {
|
|
766
|
+
return Math.random().toString(36).slice(2, 12);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** Wire a UI in one line:
|
|
770
|
+
* AvatarSkill.attach({ frame, button, onSeed }) */
|
|
771
|
+
function attach({ frame, button, onSeed, initialSeed }) {
|
|
772
|
+
if (!frame || !button) return;
|
|
773
|
+
let seed = initialSeed || null;
|
|
774
|
+
function paintFrame(nextSeed, opts) {
|
|
775
|
+
seed = nextSeed;
|
|
776
|
+
frame.innerHTML = generate(nextSeed, opts);
|
|
777
|
+
if (typeof onSeed === "function") onSeed(seed);
|
|
778
|
+
}
|
|
779
|
+
if (seed) paintFrame(seed);
|
|
780
|
+
else paintFrame("__placeholder__", { placeholder: true });
|
|
781
|
+
button.addEventListener("click", (e) => {
|
|
782
|
+
e.preventDefault();
|
|
783
|
+
paintFrame(randomSeed());
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
window.AvatarSkill = {
|
|
788
|
+
generate,
|
|
789
|
+
generateDataUrl,
|
|
790
|
+
randomSeed,
|
|
791
|
+
attach,
|
|
792
|
+
};
|
|
793
|
+
})();
|