plushies 0.1.0-alpha.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.
@@ -0,0 +1,2289 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
6
+ var __toCommonJS = (from) => {
7
+ var entry = __moduleCache.get(from), desc;
8
+ if (entry)
9
+ return entry;
10
+ entry = __defProp({}, "__esModule", { value: true });
11
+ if (from && typeof from === "object" || typeof from === "function")
12
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
13
+ get: () => from[key],
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ }));
16
+ __moduleCache.set(from, entry);
17
+ return entry;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+
29
+ // src/index.ts
30
+ var exports_src = {};
31
+ __export(exports_src, {
32
+ resetEnvironment: () => resetEnvironment,
33
+ plushieOutline: () => plushieOutline,
34
+ fabricGrain: () => fabricGrain,
35
+ fabricFur: () => fabricFur,
36
+ createPlushie: () => createPlushie,
37
+ PLUSHIE_PINS: () => PLUSHIE_PINS,
38
+ PLUSHIE_NECKS: () => PLUSHIE_NECKS,
39
+ PLUSHIE_MOUTHS: () => PLUSHIE_MOUTHS,
40
+ PLUSHIE_MOUSTACHES: () => PLUSHIE_MOUSTACHES,
41
+ PLUSHIE_KINDS: () => PLUSHIE_KINDS,
42
+ PLUSHIE_HATS: () => PLUSHIE_HATS,
43
+ PLUSHIE_GLASSES: () => PLUSHIE_GLASSES,
44
+ PLUSHIE_FINISHES: () => PLUSHIE_FINISHES,
45
+ PLUSHIE_FABRICS: () => PLUSHIE_FABRICS,
46
+ PLUSHIE_EYES: () => PLUSHIE_EYES,
47
+ MAX_FUR_SHELLS: () => MAX_FUR_SHELLS,
48
+ DEFAULT_POSE: () => DEFAULT_POSE,
49
+ DEFAULT_OPTIONS: () => DEFAULT_OPTIONS,
50
+ ACCESSORY_COLORS: () => ACCESSORY_COLORS
51
+ });
52
+ module.exports = __toCommonJS(exports_src);
53
+ function toRgb(three, color) {
54
+ if (typeof color !== "string") {
55
+ const rgb2 = [color[0], color[1], color[2]].map((v) => Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 0);
56
+ return rgb2;
57
+ }
58
+ const cached = COLORS.get(color);
59
+ if (cached)
60
+ return cached;
61
+ const rgb = parseColor(three, color) ?? unknownColor(three, color);
62
+ if (COLORS.size > 256)
63
+ COLORS.clear();
64
+ COLORS.set(color, rgb);
65
+ return rgb;
66
+ }
67
+ var COLORS = new Map;
68
+ function toHex(three, color) {
69
+ if (color === undefined)
70
+ return;
71
+ if (/^#[0-9a-f]{6}$/i.test(color))
72
+ return color;
73
+ return "#" + toRgb(three, color).map((v) => Math.round(v * 255).toString(16).padStart(2, "0")).join("");
74
+ }
75
+ function unknownColor(three, color) {
76
+ console.warn(`plushies: unknown colour ${JSON.stringify(color)}, using ${DEFAULT_COLOR}`);
77
+ return parseColor(three, DEFAULT_COLOR);
78
+ }
79
+ var DEFAULT_COLOR = "#f2b33d";
80
+ var NUM = "[+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:e[+-]?\\d+)?";
81
+ var IS_NUMBER = new RegExp(`^${NUM}$`);
82
+ var IS_PERCENT = new RegExp(`^${NUM}%$`);
83
+ var IS_HUE = new RegExp(`^${NUM}(?:deg|grad|rad|turn)?$`);
84
+ function cssHue(token) {
85
+ const v = parseFloat(token);
86
+ if (token.endsWith("turn"))
87
+ return v * 360;
88
+ if (token.endsWith("grad"))
89
+ return v * 0.9;
90
+ if (token.endsWith("rad"))
91
+ return v * 180 / Math.PI;
92
+ return v;
93
+ }
94
+ function parseRgbHsl(three, name, body) {
95
+ const legacy = body.includes(",");
96
+ let parts;
97
+ let alpha;
98
+ if (legacy) {
99
+ parts = body.split(",").map((p) => p.trim());
100
+ if (parts.length === 4)
101
+ alpha = parts.pop();
102
+ } else {
103
+ const [main, a, extra] = body.split("/").map((p) => p.trim());
104
+ if (extra !== undefined || a === "")
105
+ return null;
106
+ parts = main.split(/\s+/).filter(Boolean);
107
+ alpha = a;
108
+ }
109
+ if (parts.length !== 3)
110
+ return null;
111
+ if (alpha !== undefined && !IS_NUMBER.test(alpha) && !IS_PERCENT.test(alpha))
112
+ return null;
113
+ let rgb;
114
+ if (name.startsWith("rgb")) {
115
+ const pct = parts.map((p) => IS_PERCENT.test(p));
116
+ if (parts.some((p, i) => !pct[i] && !IS_NUMBER.test(p)))
117
+ return null;
118
+ if (legacy && pct.some((v) => v !== pct[0]))
119
+ return null;
120
+ rgb = parts.map((p, i) => pct[i] ? parseFloat(p) / 100 : parseFloat(p) / 255);
121
+ } else {
122
+ const [hue, ...sl] = parts;
123
+ if (!IS_HUE.test(hue))
124
+ return null;
125
+ if (sl.some((p) => !IS_PERCENT.test(p) && (legacy || !IS_NUMBER.test(p))))
126
+ return null;
127
+ const [s, l] = sl.map((p) => Math.max(0, Math.min(1, parseFloat(p) / 100)));
128
+ const h = (cssHue(hue) % 360 + 360) % 360 / 360;
129
+ const out = { r: 0, g: 0, b: 0 };
130
+ new three.Color().setHSL(h, s, l, three.SRGBColorSpace).getRGB(out, three.SRGBColorSpace);
131
+ rgb = [out.r, out.g, out.b];
132
+ }
133
+ if (rgb.some((v) => !Number.isFinite(v)))
134
+ return null;
135
+ return rgb.map((v) => Math.max(0, Math.min(1, v)));
136
+ }
137
+ function parseColor(three, input) {
138
+ const color = input.trim().toLowerCase();
139
+ const hex = /^#([0-9a-f]{3,8})$/.exec(color);
140
+ if (hex && (hex[1].length === 3 || hex[1].length === 4)) {
141
+ const h = hex[1];
142
+ return [0, 1, 2].map((i) => parseInt(h[i] + h[i], 16) / 255);
143
+ }
144
+ if (hex && (hex[1].length === 6 || hex[1].length === 8)) {
145
+ const h = hex[1];
146
+ return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
147
+ }
148
+ if (hex)
149
+ return null;
150
+ const fn = /^(rgba?|hsla?)\(\s*([^()]*)\)$/.exec(color);
151
+ if (fn)
152
+ return parseRgbHsl(three, fn[1], fn[2]) ?? browserColor(color);
153
+ if (/^[a-z]+$/.test(color) && color in three.Color.NAMES) {
154
+ return parseColor(three, "#" + three.Color.NAMES[color].toString(16).padStart(6, "0"));
155
+ }
156
+ return browserColor(color);
157
+ }
158
+ var probe;
159
+ function browserColor(color) {
160
+ if (probe === undefined) {
161
+ try {
162
+ const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(1, 1) : typeof document !== "undefined" ? Object.assign(document.createElement("canvas"), { width: 1, height: 1 }) : null;
163
+ probe = canvas?.getContext("2d", { willReadFrequently: true }) ?? null;
164
+ } catch {
165
+ probe = null;
166
+ }
167
+ }
168
+ if (!probe || typeof CSS === "undefined" || !CSS.supports("color", color))
169
+ return null;
170
+ probe.clearRect(0, 0, 1, 1);
171
+ probe.fillStyle = "#000";
172
+ probe.fillStyle = color;
173
+ probe.fillRect(0, 0, 1, 1);
174
+ const [r, g, b] = probe.getImageData(0, 0, 1, 1).data;
175
+ return [r, g, b].map((v) => v / 255);
176
+ }
177
+ function mulberry32(seed) {
178
+ let a = seed >>> 0;
179
+ return () => {
180
+ a = a + 1831565813 >>> 0;
181
+ let t = a;
182
+ t = Math.imul(t ^ t >>> 15, t | 1);
183
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
184
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
185
+ };
186
+ }
187
+ function signedArea(poly) {
188
+ let a = 0;
189
+ for (let i = 0;i < poly.length; i++) {
190
+ const [x0, y0] = poly[i];
191
+ const [x1, y1] = poly[(i + 1) % poly.length];
192
+ a += x0 * y1 - x1 * y0;
193
+ }
194
+ return a / 2;
195
+ }
196
+ function centroid(poly) {
197
+ let cx = 0;
198
+ let cy = 0;
199
+ let a = 0;
200
+ for (let i = 0;i < poly.length; i++) {
201
+ const [x0, y0] = poly[i];
202
+ const [x1, y1] = poly[(i + 1) % poly.length];
203
+ const cross = x0 * y1 - x1 * y0;
204
+ a += cross;
205
+ cx += (x0 + x1) * cross;
206
+ cy += (y0 + y1) * cross;
207
+ }
208
+ return [cx / (3 * a), cy / (3 * a)];
209
+ }
210
+ function roundedPolygon(vertices, radius, steps = 14) {
211
+ const out = [];
212
+ const n = vertices.length;
213
+ for (let i = 0;i < n; i++) {
214
+ const p = vertices[i];
215
+ const prev = vertices[(i + n - 1) % n];
216
+ const next = vertices[(i + 1) % n];
217
+ const la = Math.hypot(prev[0] - p[0], prev[1] - p[1]);
218
+ const lb = Math.hypot(next[0] - p[0], next[1] - p[1]);
219
+ const a = [(prev[0] - p[0]) / la, (prev[1] - p[1]) / la];
220
+ const b = [(next[0] - p[0]) / lb, (next[1] - p[1]) / lb];
221
+ const angle = Math.acos(Math.max(-1, Math.min(1, a[0] * b[0] + a[1] * b[1])));
222
+ const t = Math.min(radius / Math.tan(angle / 2), la * 0.48, lb * 0.48);
223
+ const r = t * Math.tan(angle / 2);
224
+ const bis = [a[0] + b[0], a[1] + b[1]];
225
+ const bl = Math.hypot(bis[0], bis[1]) || 1;
226
+ const dist = r / Math.sin(angle / 2);
227
+ const c = [p[0] + bis[0] / bl * dist, p[1] + bis[1] / bl * dist];
228
+ const t1 = [p[0] + a[0] * t, p[1] + a[1] * t];
229
+ const t2 = [p[0] + b[0] * t, p[1] + b[1] * t];
230
+ const s = Math.atan2(t1[1] - c[1], t1[0] - c[0]);
231
+ let e = Math.atan2(t2[1] - c[1], t2[0] - c[0]);
232
+ let d = e - s;
233
+ while (d > Math.PI)
234
+ d -= Math.PI * 2;
235
+ while (d < -Math.PI)
236
+ d += Math.PI * 2;
237
+ e = s + d;
238
+ for (let k = 0;k <= steps; k++) {
239
+ const th = s + d * k / steps;
240
+ out.push([c[0] + Math.cos(th) * r, c[1] + Math.sin(th) * r]);
241
+ }
242
+ }
243
+ return out;
244
+ }
245
+ function smoothClosed(poly, iterations) {
246
+ let pts = poly;
247
+ for (let it = 0;it < iterations; it++) {
248
+ pts = pts.map((p, i) => {
249
+ const a = pts[(i + pts.length - 1) % pts.length];
250
+ const b = pts[(i + 1) % pts.length];
251
+ return [p[0] * 0.5 + (a[0] + b[0]) * 0.25, p[1] * 0.5 + (a[1] + b[1]) * 0.25];
252
+ });
253
+ }
254
+ return pts;
255
+ }
256
+ function resampleClosed(poly, n) {
257
+ const lengths = [0];
258
+ for (let i = 0;i < poly.length; i++) {
259
+ const a = poly[i];
260
+ const b = poly[(i + 1) % poly.length];
261
+ lengths.push(lengths[i] + Math.hypot(b[0] - a[0], b[1] - a[1]));
262
+ }
263
+ const total = lengths[poly.length];
264
+ const out = [];
265
+ let seg = 0;
266
+ for (let k = 0;k < n; k++) {
267
+ const target = k / n * total;
268
+ while (lengths[seg + 1] < target)
269
+ seg++;
270
+ const a = poly[seg];
271
+ const b = poly[(seg + 1) % poly.length];
272
+ const f = (target - lengths[seg]) / Math.max(0.000000001, lengths[seg + 1] - lengths[seg]);
273
+ out.push([a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f]);
274
+ }
275
+ return out;
276
+ }
277
+ function polar(n, r) {
278
+ return Array.from({ length: n }, (_, i) => {
279
+ const th = i / n * Math.PI * 2;
280
+ const rr = r(th);
281
+ return [Math.cos(th) * rr, Math.sin(th) * rr];
282
+ });
283
+ }
284
+ function regular(n, radius, phase) {
285
+ return Array.from({ length: n }, (_, i) => {
286
+ const th = phase + i / n * Math.PI * 2;
287
+ return [Math.cos(th) * radius, Math.sin(th) * radius];
288
+ });
289
+ }
290
+ function silhouette(kind, o) {
291
+ const round = Math.max(0, Math.min(1, o.roundness));
292
+ let poly;
293
+ switch (kind) {
294
+ case "square": {
295
+ const sq = [[-1, -1], [1, -1], [1, 1], [-1, 1]];
296
+ poly = roundedPolygon(sq, 0.12 + 0.62 * round);
297
+ break;
298
+ }
299
+ case "triangle":
300
+ case "polygon": {
301
+ const n = kind === "triangle" ? 3 : Math.max(3, Math.round(o.sides ?? 6));
302
+ const inradius = Math.cos(Math.PI / n);
303
+ poly = roundedPolygon(regular(n, 1, Math.PI / 2), inradius * (0.12 + 0.72 * round));
304
+ break;
305
+ }
306
+ case "star": {
307
+ const n = Math.max(3, Math.round(o.sides ?? 5));
308
+ const inner = Math.max(0.2, Math.min(0.9, o.starInner));
309
+ const pts = [];
310
+ for (let i = 0;i < n * 2; i++) {
311
+ const th = Math.PI / 2 + i / (n * 2) * Math.PI * 2;
312
+ const rr = i % 2 === 0 ? 1 : inner;
313
+ pts.push([Math.cos(th) * rr, Math.sin(th) * rr]);
314
+ }
315
+ poly = roundedPolygon(pts, 0.06 + 0.3 * round);
316
+ break;
317
+ }
318
+ case "heart": {
319
+ const raw = Array.from({ length: 240 }, (_, i) => {
320
+ const t = i / 240 * Math.PI * 2;
321
+ return [
322
+ 16 * Math.sin(t) ** 3,
323
+ 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)
324
+ ];
325
+ });
326
+ poly = smoothClosed(raw, Math.round(20 + 180 * round));
327
+ break;
328
+ }
329
+ case "pill":
330
+ poly = roundedPolygon([[-0.62, -1], [0.62, -1], [0.62, 1], [-0.62, 1]], 0.61);
331
+ break;
332
+ case "cloud": {
333
+ const circles = [
334
+ [-0.62, -0.2, 0.42],
335
+ [-0.25, 0.18, 0.5],
336
+ [0.3, 0.22, 0.47],
337
+ [0.68, -0.12, 0.4],
338
+ [0.05, -0.28, 0.46]
339
+ ];
340
+ const raw = polar(360, (th) => {
341
+ const ux = Math.cos(th);
342
+ const uy = Math.sin(th);
343
+ let best = 0;
344
+ for (const [cx, cy, r] of circles) {
345
+ const b = ux * cx + uy * cy;
346
+ const disc = b * b - (cx * cx + cy * cy) + r * r;
347
+ if (disc >= 0)
348
+ best = Math.max(best, b + Math.sqrt(disc));
349
+ }
350
+ return best;
351
+ });
352
+ poly = smoothClosed(raw, Math.round(10 + 60 * round));
353
+ break;
354
+ }
355
+ case "blob": {
356
+ const rand = mulberry32(o.seed * 9973 + 17);
357
+ const waves = [2, 3, 4, 5].map((k) => ({
358
+ k,
359
+ amp: 0.2 / k * (0.5 + rand()),
360
+ phase: rand() * Math.PI * 2
361
+ }));
362
+ poly = polar(240, (th) => waves.reduce((r, w) => r + w.amp * Math.cos(w.k * th + w.phase), 1));
363
+ break;
364
+ }
365
+ case "egg":
366
+ poly = polar(240, () => 1).map(([x, y]) => [x * 0.8 * (1 - 0.14 * y), y]);
367
+ break;
368
+ case "drop": {
369
+ const raw = Array.from({ length: 240 }, (_, i) => {
370
+ const t = i / 240 * Math.PI * 2;
371
+ return [Math.sin(t) * Math.pow(Math.sin(t / 2), 1.3), Math.cos(t)];
372
+ });
373
+ poly = smoothClosed(raw, Math.round(4 + 60 * round));
374
+ break;
375
+ }
376
+ case "ghost": {
377
+ const raw = [];
378
+ for (let i = 0;i <= 60; i++) {
379
+ const th = i / 60 * Math.PI;
380
+ raw.push([Math.cos(th), 0.15 + Math.sin(th)]);
381
+ }
382
+ for (let i = 1;i <= 20; i++)
383
+ raw.push([-1, 0.15 - i / 20 * 1.05]);
384
+ for (let i = 0;i <= 90; i++) {
385
+ const x = -1 + i / 90 * 2;
386
+ raw.push([x, -0.9 - 0.14 * Math.abs(Math.sin((x + 1) / 2 * Math.PI * 3))]);
387
+ }
388
+ for (let i = 0;i < 20; i++)
389
+ raw.push([1, -0.9 + i / 20 * 1.05]);
390
+ poly = smoothClosed(raw, Math.round(8 + 30 * round));
391
+ break;
392
+ }
393
+ case "bean":
394
+ poly = polar(240, () => 1).map(([x, y]) => [x, 0.62 * y - 0.26 * Math.pow(Math.max(0, y), 6)]);
395
+ break;
396
+ case "flower": {
397
+ const petals = Math.max(3, Math.round(o.sides ?? 6));
398
+ poly = polar(360, (th) => 0.6 + 0.4 * Math.abs(Math.cos(petals * (th - Math.PI / 2) / 2)));
399
+ poly = smoothClosed(poly, Math.round(6 + 50 * round));
400
+ break;
401
+ }
402
+ case "diamond":
403
+ poly = roundedPolygon([[0, 1], [-0.78, 0], [0, -1], [0.78, 0]], 0.08 + 0.3 * round);
404
+ break;
405
+ case "squircle": {
406
+ const n = 2.2 + (1 - round) * 5;
407
+ poly = polar(240, (th) => Math.pow(Math.pow(Math.abs(Math.cos(th)), n) + Math.pow(Math.abs(Math.sin(th)), n), -1 / n));
408
+ break;
409
+ }
410
+ case "arch": {
411
+ const raw = [[-0.8, -1], [0.8, -1]];
412
+ for (let i = 0;i <= 40; i++) {
413
+ const th = i / 40 * Math.PI;
414
+ raw.push([Math.cos(th) * 0.8, 0.2 + Math.sin(th) * 0.8]);
415
+ }
416
+ poly = roundedPolygon(raw, 0.1 + 0.3 * round, 10);
417
+ break;
418
+ }
419
+ case "circle":
420
+ default:
421
+ poly = polar(240, () => 1);
422
+ }
423
+ if (signedArea(poly) < 0)
424
+ poly = poly.slice().reverse();
425
+ poly = resampleClosed(poly, 150);
426
+ let minX = Infinity;
427
+ let maxX = -Infinity;
428
+ let minY = Infinity;
429
+ let maxY = -Infinity;
430
+ for (const [x, y] of poly) {
431
+ minX = Math.min(minX, x);
432
+ maxX = Math.max(maxX, x);
433
+ minY = Math.min(minY, y);
434
+ maxY = Math.max(maxY, y);
435
+ }
436
+ const s = 2 / Math.max(maxX - minX, maxY - minY);
437
+ const mx = (minX + maxX) / 2;
438
+ const my = (minY + maxY) / 2;
439
+ return poly.map(([x, y]) => [(x - mx) * s, (y - my) * s]);
440
+ }
441
+ function distanceToOutline(x, y, poly) {
442
+ let best = Infinity;
443
+ for (let i = 0;i < poly.length; i++) {
444
+ const [ax, ay] = poly[i];
445
+ const [bx, by] = poly[(i + 1) % poly.length];
446
+ const dx = bx - ax;
447
+ const dy = by - ay;
448
+ const t = Math.max(0, Math.min(1, ((x - ax) * dx + (y - ay) * dy) / (dx * dx + dy * dy)));
449
+ best = Math.min(best, Math.hypot(ax + dx * t - x, ay + dy * t - y));
450
+ }
451
+ return best;
452
+ }
453
+ function inflate(three, outline, thickness) {
454
+ const N = outline.length;
455
+ const K = 18;
456
+ const center = centroid(outline);
457
+ const [cx, cy] = center;
458
+ let dmax = 0;
459
+ for (let i = 0;i < 64; i++) {
460
+ const th = i / 64 * Math.PI * 2;
461
+ const x = cx + Math.cos(th) * 0.15;
462
+ const y = cy + Math.sin(th) * 0.15;
463
+ dmax = Math.max(dmax, distanceToOutline(x, y, outline));
464
+ }
465
+ dmax = Math.max(dmax, distanceToOutline(cx, cy, outline));
466
+ const H = thickness;
467
+ const D = Math.min(dmax, H * 1.25);
468
+ const profile = (d) => {
469
+ const u = Math.min(1, Math.max(0, d / D));
470
+ return H * (0.84 * Math.sqrt(u * (2 - u)) + 0.16 * Math.min(1, d / dmax));
471
+ };
472
+ const ringS = (k) => 1 - Math.pow(1 - k / K, 1.8);
473
+ const xy = [[cx, cy]];
474
+ for (let k = 1;k <= K; k++) {
475
+ const s = ringS(k);
476
+ for (let i = 0;i < N; i++) {
477
+ xy.push([cx + (outline[i][0] - cx) * s, cy + (outline[i][1] - cy) * s]);
478
+ }
479
+ }
480
+ const idx = (k, i) => k === 0 ? 0 : 1 + (k - 1) * N + (i % N + N) % N;
481
+ let h = xy.map(([x, y], v) => v >= 1 + (K - 1) * N ? 0 : profile(distanceToOutline(x, y, outline)));
482
+ for (let it = 0;it < 6; it++) {
483
+ const next = h.slice();
484
+ let ring1 = 0;
485
+ for (let i = 0;i < N; i++)
486
+ ring1 += h[idx(1, i)];
487
+ next[0] = h[0] * 0.5 + ring1 / N * 0.5;
488
+ for (let k = 1;k < K; k++) {
489
+ for (let i = 0;i < N; i++) {
490
+ const v = idx(k, i);
491
+ const avg = (h[idx(k, i - 1)] + h[idx(k, i + 1)] + h[idx(k - 1, i)] + h[idx(k + 1, i)]) / 4;
492
+ next[v] = h[v] * 0.5 + avg * 0.5;
493
+ }
494
+ }
495
+ h = next;
496
+ }
497
+ const V = xy.length;
498
+ const backStart = V;
499
+ const backCount = 1 + (K - 1) * N;
500
+ const positions = new Float32Array((V + backCount) * 3);
501
+ for (let v = 0;v < V; v++) {
502
+ positions.set([xy[v][0], xy[v][1], h[v]], v * 3);
503
+ }
504
+ for (let v = 0;v < backCount; v++) {
505
+ positions.set([xy[v][0], xy[v][1], -h[v] * 0.9], (backStart + v) * 3);
506
+ }
507
+ const backIdx = (k, i) => k === K ? idx(K, i) : backStart + idx(k, i);
508
+ const index = [];
509
+ for (let i = 0;i < N; i++) {
510
+ index.push(0, idx(1, i), idx(1, i + 1));
511
+ index.push(backStart, backIdx(1, i + 1), backIdx(1, i));
512
+ for (let k = 1;k < K; k++) {
513
+ const a = idx(k, i);
514
+ const b = idx(k, i + 1);
515
+ const c = idx(k + 1, i + 1);
516
+ const d = idx(k + 1, i);
517
+ index.push(a, d, c, a, c, b);
518
+ const ba = backIdx(k, i);
519
+ const bb = backIdx(k, i + 1);
520
+ const bc = backIdx(k + 1, i + 1);
521
+ const bd = backIdx(k + 1, i);
522
+ index.push(ba, bc, bd, ba, bb, bc);
523
+ }
524
+ }
525
+ const geometry = new three.BufferGeometry;
526
+ geometry.setAttribute("position", new three.BufferAttribute(positions, 3));
527
+ geometry.setIndex(index);
528
+ geometry.computeVertexNormals();
529
+ return {
530
+ outline,
531
+ center,
532
+ heightAt: (x, y) => profile(distanceToOutline(x, y, outline)),
533
+ geometry
534
+ };
535
+ }
536
+ var FUR_SHELLS = 16;
537
+ function furShells(three, base, shells = FUR_SHELLS) {
538
+ const pos = base.getAttribute("position");
539
+ const nor = base.getAttribute("normal");
540
+ const baseIndex = base.getIndex();
541
+ const count = pos.count;
542
+ const layers = shells + 1;
543
+ const positions = new Float32Array(count * layers * 3);
544
+ const normals = new Float32Array(count * layers * 3);
545
+ const shell = new Float32Array(count * layers);
546
+ for (let l = 0;l < layers; l++) {
547
+ positions.set(pos.array, l * count * 3);
548
+ normals.set(nor.array, l * count * 3);
549
+ shell.fill(l, l * count, (l + 1) * count);
550
+ }
551
+ const src = baseIndex ? Array.from(baseIndex.array) : Array.from({ length: count }, (_, i) => i);
552
+ const index = new Uint32Array(src.length * layers);
553
+ for (let l = 0;l < layers; l++) {
554
+ for (let i = 0;i < src.length; i++)
555
+ index[l * src.length + i] = src[i] + l * count;
556
+ }
557
+ const g = new three.BufferGeometry;
558
+ g.setAttribute("position", new three.BufferAttribute(positions, 3));
559
+ g.setAttribute("normal", new three.BufferAttribute(normals, 3));
560
+ g.setAttribute("shell", new three.BufferAttribute(shell, 1));
561
+ g.setIndex(new three.BufferAttribute(index, 1));
562
+ g.userData.indexPerLayer = src.length;
563
+ return g;
564
+ }
565
+ var RIG = {
566
+ sky: ["#fffaf2", 0.5],
567
+ ground: ["#6e655d", 0.5],
568
+ key: { dir: [-300, 420, 520], color: "#fff4e4", power: 0.78 },
569
+ rim: { dir: [380, 260, -420], color: "#ffffff", power: 0.8 },
570
+ fill: { dir: [420, -120, 400], color: "#e8eeff", power: 0.2 }
571
+ };
572
+ var FUR_VERTEX = `
573
+ attribute float shell;
574
+ uniform float uFur;
575
+ uniform float uLayers;
576
+ uniform vec3 uGravity;
577
+ uniform float uRoot;
578
+ uniform vec3 uColor;
579
+ uniform vec3 uSheen;
580
+ uniform vec3 uSky;
581
+ uniform vec3 uGround;
582
+ uniform vec3 uKeyDir;
583
+ uniform vec3 uKeyColor;
584
+ uniform vec3 uRimDir;
585
+ uniform vec3 uRimColor;
586
+ uniform vec3 uFillDir;
587
+ uniform vec3 uFillColor;
588
+ uniform float uSheenAmt;
589
+ uniform float uMottle;
590
+ uniform float uBias;
591
+ varying float vShell;
592
+ varying vec3 vBase;
593
+ varying vec3 vBaseNormal;
594
+ varying vec3 vLit;
595
+ float mottleHash(vec3 p) {
596
+ p = fract(p * 0.1031);
597
+ p += dot(p, p.zyx + 31.32);
598
+ return fract((p.x + p.y) * p.z);
599
+ }
600
+ // Smooth 3D value noise — only evaluated per vertex, so it is cheap.
601
+ float mottleNoise(vec3 p) {
602
+ vec3 i = floor(p);
603
+ vec3 f = fract(p);
604
+ f = f * f * (3.0 - 2.0 * f);
605
+ return mix(
606
+ mix(mix(mottleHash(i), mottleHash(i + vec3(1, 0, 0)), f.x),
607
+ mix(mottleHash(i + vec3(0, 1, 0)), mottleHash(i + vec3(1, 1, 0)), f.x), f.y),
608
+ mix(mix(mottleHash(i + vec3(0, 0, 1)), mottleHash(i + vec3(1, 0, 1)), f.x),
609
+ mix(mottleHash(i + vec3(0, 1, 1)), mottleHash(i + vec3(1, 1, 1)), f.x), f.y),
610
+ f.z);
611
+ }
612
+ void main() {
613
+ vShell = min(1.0, shell / uLayers);
614
+ vec3 n = normalize(normal);
615
+ vBase = position;
616
+ vBaseNormal = n;
617
+ vec3 p = position + n * uFur * vShell + uGravity * uFur * vShell * vShell;
618
+ vec4 mv = modelViewMatrix * vec4(p, 1.0);
619
+ vec4 shaded = mv;
620
+ // Slide along the view ray: same pixel on screen, nearer depth.
621
+ mv.xyz += normalize(-mv.xyz) * uBias;
622
+ gl_Position = projectionMatrix * mv;
623
+ mv = shaded;
624
+
625
+ // Shading in view space (normalMatrix stays right under squash).
626
+ vec3 N = normalize(normalMatrix * n);
627
+ vec3 V = normalize(-mv.xyz);
628
+ vec3 up = normalize((viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz);
629
+ vec3 K = normalize((viewMatrix * vec4(uKeyDir, 0.0)).xyz);
630
+ vec3 R = normalize((viewMatrix * vec4(uRimDir, 0.0)).xyz);
631
+ vec3 F = normalize((viewMatrix * vec4(uFillDir, 0.0)).xyz);
632
+ vec3 ambient = mix(uGround, uSky, dot(N, up) * 0.5 + 0.5);
633
+ // Wrapped diffuse: light bleeds round the terminator the way it does
634
+ // through fibres, instead of a hard plastic falloff.
635
+ float key = max(0.0, (dot(N, K) + 0.4) / 1.4);
636
+ float rim = max(0.0, dot(N, R));
637
+ float fill = max(0.0, dot(N, F));
638
+ float grazing = pow(1.0 - max(0.0, dot(N, V)), 2.0);
639
+ float blotch = 1.0;
640
+ if (uMottle > 0.0) {
641
+ blotch = 1.0 + uMottle * (mottleNoise(position * 5.0) + 0.5 * mottleNoise(position * 13.0) - 0.75) * 1.6;
642
+ }
643
+ vec3 lit = uColor * blotch * (ambient + uKeyColor * key + uFillColor * fill)
644
+ + uSheenAmt * (uSheen * grazing * (0.35 + 0.5 * key)
645
+ + uRimColor * uSheen * rim * (0.35 + grazing));
646
+ // Roots sit in the shade of the strands around them.
647
+ vLit = lit * mix(uRoot, 1.08, pow(vShell, 0.7));
648
+ }
649
+ `;
650
+ var FUR_FRAGMENT = `
651
+ uniform float uFur;
652
+ uniform float uStrand;
653
+ varying float vShell;
654
+ varying vec3 vBase;
655
+ varying vec3 vBaseNormal;
656
+ varying vec3 vLit;
657
+ float plushHash(vec3 p) {
658
+ p = fract(p * 0.1031);
659
+ p += dot(p, p.zyx + 31.32);
660
+ return fract((p.x + p.y) * p.z);
661
+ }
662
+ // One jittered strand per lattice cell: (coverage of this pixel, strand height).
663
+ // The strand is a cylinder along the surface normal, so its radius is measured
664
+ // in the tangent plane — measured in 3D, every place where the surface crosses
665
+ // a cell boundary would read as "between strands" and draw a bald contour line.
666
+ // Its edge is blurred over one pixel (px = cells per pixel), not cut.
667
+ vec2 plushStrand(vec3 q, float t, vec3 n, float px) {
668
+ vec3 c = floor(q);
669
+ vec3 f = fract(q) - 0.5;
670
+ f -= (vec3(plushHash(c + 1.7), plushHash(c + 4.3), plushHash(c + 7.9)) - 0.5) * 0.45;
671
+ f -= n * dot(f, n);
672
+ float h = 0.35 + 0.65 * plushHash(c);
673
+ if (t > h) return vec2(0.0);
674
+ float r = 0.62 * (1.0 - 0.85 * t / h);
675
+ return vec2(clamp((r - length(f)) / px + 0.5, 0.0, 1.0), h);
676
+ }
677
+ void main() {
678
+ vec3 q = vBase / uStrand;
679
+ // Strands narrower than a couple of pixels can't be drawn one by one — they
680
+ // would turn into per-pixel noise that crawls as the plush moves — so the
681
+ // pattern fades into its average: how much of each shell strands cover
682
+ // (fitted to plushStrand) and how tall they are there.
683
+ float px = max(max(length(dFdx(q)), length(dFdy(q))), 1e-4);
684
+ float fine = smoothstep(0.45, 1.1, px);
685
+ float tone;
686
+ float alpha = 1.0;
687
+ if (vShell > 0.0) {
688
+ if (uFur < 1e-4) discard;
689
+ vec3 n = normalize(vBaseNormal);
690
+ vec2 a = plushStrand(q, vShell, n, px);
691
+ vec2 b = plushStrand(q + vec3(0.5, 0.31, 0.77), vShell, n, px);
692
+ vec2 s = a.x > b.x ? a : b;
693
+ alpha = mix(s.x, exp(-pow(vShell / 0.48, 2.1)), fine);
694
+ if (alpha < 0.004) discard;
695
+ tone = 0.9 + 0.2 * mix(s.y, 0.74 + 0.24 * vShell * vShell, fine);
696
+ } else {
697
+ tone = 0.92 + 0.12 * mix(plushHash(floor(q * 1.7)), 0.5, fine);
698
+ }
699
+ gl_FragColor = linearToOutputTexel(vec4(vLit * tone, alpha));
700
+ }
701
+ `;
702
+ function rigColor(three, hex, power) {
703
+ return new three.Color(hex).multiplyScalar(power);
704
+ }
705
+ function furMaterial(three, opts) {
706
+ const dir = (d) => new three.Vector3(d[0], d[1], d[2]).normalize();
707
+ const uniforms = {
708
+ uFur: { value: opts.length },
709
+ uStrand: { value: opts.strand },
710
+ uGravity: { value: new three.Vector3(0, -(opts.gravity ?? 0.35), 0) },
711
+ uRoot: { value: opts.root ?? 0.58 },
712
+ uLayers: { value: opts.layers ?? FUR_SHELLS },
713
+ uColor: { value: new three.Color("#ffffff") },
714
+ uSheen: { value: new three.Color("#ffffff") },
715
+ uSheenAmt: { value: opts.sheen ?? 1 },
716
+ uMottle: { value: opts.mottle ?? 0 },
717
+ uBias: opts.bias ?? { value: 0 },
718
+ uSky: { value: rigColor(three, RIG.sky[0], RIG.sky[1]) },
719
+ uGround: { value: rigColor(three, RIG.ground[0], RIG.ground[1]) },
720
+ uKeyDir: { value: dir(RIG.key.dir) },
721
+ uKeyColor: { value: rigColor(three, RIG.key.color, RIG.key.power) },
722
+ uRimDir: { value: dir(RIG.rim.dir) },
723
+ uRimColor: { value: rigColor(three, RIG.rim.color, RIG.rim.power) },
724
+ uFillDir: { value: dir(RIG.fill.dir) },
725
+ uFillColor: { value: rigColor(three, RIG.fill.color, RIG.fill.power) }
726
+ };
727
+ const material = new three.ShaderMaterial({
728
+ uniforms,
729
+ vertexShader: FUR_VERTEX,
730
+ fragmentShader: FUR_FRAGMENT,
731
+ transparent: true
732
+ });
733
+ return { material, uniforms };
734
+ }
735
+ function setFurColor(fur, color) {
736
+ const [r, g, b] = color;
737
+ fur.uniforms.uColor.value.setRGB(r, g, b, "srgb");
738
+ fur.uniforms.uSheen.value.setRGB(r + (1 - r) * 0.55, g + (1 - g) * 0.55, b + (1 - b) * 0.55, "srgb");
739
+ }
740
+ function canvasTexture(three, size, paint, color = true) {
741
+ const canvas = document.createElement("canvas");
742
+ canvas.width = size;
743
+ canvas.height = size;
744
+ paint(canvas.getContext("2d"), size);
745
+ const texture = new three.CanvasTexture(canvas);
746
+ texture.wrapS = three.RepeatWrapping;
747
+ texture.wrapT = three.RepeatWrapping;
748
+ if (color)
749
+ texture.colorSpace = three.SRGBColorSpace;
750
+ return texture;
751
+ }
752
+ function threadTexture(three) {
753
+ return canvasTexture(three, 64, (ctx, s) => {
754
+ ctx.fillStyle = "#ffffff";
755
+ ctx.fillRect(0, 0, s, s);
756
+ ctx.strokeStyle = "#7d7d7d";
757
+ ctx.lineWidth = 7;
758
+ for (let i = -2;i < 4; i++) {
759
+ ctx.beginPath();
760
+ ctx.moveTo(i * 22, 0);
761
+ ctx.lineTo(i * 22 + s, s);
762
+ ctx.stroke();
763
+ }
764
+ });
765
+ }
766
+ function grainTexture(three, seed) {
767
+ const rand = mulberry32(seed);
768
+ return canvasTexture(three, 128, (ctx, s) => {
769
+ const img = ctx.createImageData(s, s);
770
+ for (let i = 0;i < s * s; i++) {
771
+ const v = 110 + rand() * 145;
772
+ img.data.set([v, v, v, 255], i * 4);
773
+ }
774
+ ctx.putImageData(img, 0, 0);
775
+ }, false);
776
+ }
777
+ function stripeTexture(three, a, b) {
778
+ return canvasTexture(three, 128, (ctx, s) => {
779
+ ctx.fillStyle = a;
780
+ ctx.fillRect(0, 0, s, s);
781
+ ctx.fillStyle = b;
782
+ for (let i = -1;i < 3; i++) {
783
+ ctx.beginPath();
784
+ ctx.moveTo(i * 64, 0);
785
+ ctx.lineTo(i * 64 + 28, 0);
786
+ ctx.lineTo(i * 64 + 28 + s, s);
787
+ ctx.lineTo(i * 64 + s, s);
788
+ ctx.closePath();
789
+ ctx.fill();
790
+ }
791
+ });
792
+ }
793
+ function shadowTexture(three) {
794
+ return canvasTexture(three, 128, (ctx, s) => {
795
+ const g = ctx.createRadialGradient(s / 2, s / 2, 0, s / 2, s / 2, s / 2);
796
+ g.addColorStop(0, "rgba(0,0,0,0.9)");
797
+ g.addColorStop(0.45, "rgba(0,0,0,0.45)");
798
+ g.addColorStop(1, "rgba(0,0,0,0)");
799
+ ctx.fillStyle = g;
800
+ ctx.fillRect(0, 0, s, s);
801
+ });
802
+ }
803
+ var ENV_CACHE = new WeakMap;
804
+ function resetEnvironment(renderer) {
805
+ ENV_CACHE.delete(renderer);
806
+ }
807
+ function studioEnvironment(three, renderer) {
808
+ if (!renderer)
809
+ return null;
810
+ try {
811
+ const cached = ENV_CACHE.get(renderer);
812
+ if (cached)
813
+ return cached;
814
+ const scene = new three.Scene;
815
+ scene.background = new three.Color("#5d5752");
816
+ const panel = (w, h, pos, color, power) => {
817
+ const mesh = new three.Mesh(new three.PlaneGeometry(w, h), new three.MeshBasicMaterial({
818
+ color: new three.Color(color).multiplyScalar(power),
819
+ side: three.DoubleSide
820
+ }));
821
+ mesh.position.set(...pos);
822
+ mesh.lookAt(0, 0, 0);
823
+ scene.add(mesh);
824
+ };
825
+ panel(5, 3.5, [-4, 5, 6], "#fff6ea", 5);
826
+ panel(3, 5, [6, 2, -3], "#ffffff", 3);
827
+ panel(8, 2, [0, -5, 3], "#b8a898", 0.9);
828
+ panel(3, 3, [5, 1, 5], "#eaf0ff", 1.2);
829
+ const pmrem = new three.PMREMGenerator(renderer);
830
+ const env = pmrem.fromScene(scene, 0.03).texture;
831
+ pmrem.dispose();
832
+ ENV_CACHE.set(renderer, env);
833
+ return env;
834
+ } catch {
835
+ return null;
836
+ }
837
+ }
838
+ function finishMaterial(k, color, finish = k.finish) {
839
+ const three = k.three;
840
+ switch (finish) {
841
+ case "gloss":
842
+ return plastic(k, color);
843
+ case "satin":
844
+ return new three.MeshPhysicalMaterial({
845
+ color,
846
+ roughness: 0.48,
847
+ clearcoat: 0.12,
848
+ clearcoatRoughness: 0.5,
849
+ envMap: k.env,
850
+ envMapIntensity: 0.55
851
+ });
852
+ case "matte":
853
+ case "felt":
854
+ return new three.MeshStandardMaterial({
855
+ color,
856
+ roughness: 0.92,
857
+ bumpMap: k.grain,
858
+ bumpScale: 0.6,
859
+ envMap: k.env,
860
+ envMapIntensity: 0.3
861
+ });
862
+ }
863
+ }
864
+ function bead(k, color, sx, sy, sz, finish = k.finish) {
865
+ if (finish === "felt") {
866
+ const geometry = new k.three.SphereGeometry(1, 32, 20);
867
+ geometry.scale(sx, sy, Math.max(sz, 0.012));
868
+ return k.fuzzy(geometry, color, 0.009);
869
+ }
870
+ return blob(k, finishMaterial(k, color, finish), sx, sy, sz);
871
+ }
872
+ function plastic(k, color, opts = {}) {
873
+ return new k.three.MeshPhysicalMaterial({
874
+ color,
875
+ roughness: opts.roughness ?? 0.18,
876
+ metalness: opts.metal ? 1 : 0,
877
+ clearcoat: opts.metal ? 0 : 1,
878
+ clearcoatRoughness: 0.08,
879
+ envMap: k.env,
880
+ envMapIntensity: opts.metal ? 1.3 : 1
881
+ });
882
+ }
883
+ function felt(k, color) {
884
+ return new k.three.MeshPhysicalMaterial({
885
+ color,
886
+ roughness: 0.92,
887
+ sheen: 1,
888
+ sheenRoughness: 0.5,
889
+ sheenColor: new k.three.Color(color).lerp(new k.three.Color("#ffffff"), 0.4),
890
+ bumpMap: k.grain,
891
+ bumpScale: 1.2,
892
+ envMap: k.env,
893
+ envMapIntensity: 0.5
894
+ });
895
+ }
896
+ function threadMaterial(k, color) {
897
+ return new k.three.MeshStandardMaterial({
898
+ color,
899
+ roughness: 0.75,
900
+ map: k.thread,
901
+ bumpMap: k.thread,
902
+ bumpScale: 2,
903
+ envMap: k.env,
904
+ envMapIntensity: 0.6
905
+ });
906
+ }
907
+ function blob(k, material, sx, sy, sz) {
908
+ const mesh = new k.three.Mesh(new k.three.SphereGeometry(1, 40, 28), material);
909
+ mesh.scale.set(sx, sy, sz);
910
+ return mesh;
911
+ }
912
+ function tube(k, points, radius, material, closed = false) {
913
+ const three = k.three;
914
+ if (closed) {
915
+ const [a, b] = [points[0], points[points.length - 1]];
916
+ if (Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]) < 0.000001)
917
+ points = points.slice(0, -1);
918
+ }
919
+ const curve = new three.CatmullRomCurve3(points.map((p) => new three.Vector3(...p)), closed, "centripetal");
920
+ const geometry = new three.TubeGeometry(curve, Math.max(32, points.length * 6), radius, 12, closed);
921
+ const uv = geometry.getAttribute("uv");
922
+ const repeat = curve.getLength() / (radius * 5);
923
+ for (let i = 0;i < uv.count; i++)
924
+ uv.setX(i, uv.getX(i) * repeat);
925
+ const group = new three.Group;
926
+ group.add(new three.Mesh(geometry, material));
927
+ if (!closed) {
928
+ for (const p of [points[0], points[points.length - 1]]) {
929
+ const cap = new three.Mesh(new three.SphereGeometry(radius, 16, 12), material);
930
+ cap.position.set(...p);
931
+ group.add(cap);
932
+ }
933
+ }
934
+ return group;
935
+ }
936
+ function arc(cx, cy, r, from, to, n = 16) {
937
+ return Array.from({ length: n + 1 }, (_, i) => {
938
+ const t = (from + (to - from) * i / n) * Math.PI / 180;
939
+ return [cx + Math.cos(t) * r, cy + Math.sin(t) * r, 0];
940
+ });
941
+ }
942
+ function roundedRectPath(w, h, r) {
943
+ const pts = roundedPolygon([[-w / 2, -h / 2], [w / 2, -h / 2], [w / 2, h / 2], [-w / 2, h / 2]], r, 8);
944
+ return resampleClosed(pts, 48).map(([x, y]) => [x, y, 0]);
945
+ }
946
+ function buildEye(k, style, size, color) {
947
+ const three = k.three;
948
+ const group = new three.Group;
949
+ const lid = new three.Group;
950
+ group.add(lid);
951
+ let pupil = null;
952
+ let range = 0;
953
+ switch (style) {
954
+ case "dot": {
955
+ const r = 0.075 * size;
956
+ pupil = bead(k, color, r, r, r * 0.55);
957
+ range = r * 0.4;
958
+ break;
959
+ }
960
+ case "oval": {
961
+ const r = 0.062 * size;
962
+ pupil = bead(k, color, r * 0.8, r * 1.35, r * 0.55);
963
+ range = r * 0.4;
964
+ break;
965
+ }
966
+ case "googly": {
967
+ const r = 0.145 * size;
968
+ lid.add(bead(k, "#fbfaf6", r, r, r * 0.32));
969
+ const pr = r * 0.46;
970
+ pupil = bead(k, color, pr, pr, pr * 0.3);
971
+ pupil.position.z = r * 0.34;
972
+ range = (r - pr) * 0.85;
973
+ break;
974
+ }
975
+ case "ring": {
976
+ const r = 0.13 * size;
977
+ lid.add(tube(k, arc(0, 0, r, 0, 360, 40), 0.017 * size, threadMaterial(k, color), true));
978
+ const pr = r * 0.34;
979
+ pupil = bead(k, color, pr, pr, pr * 0.55);
980
+ range = r - pr - 0.03 * size;
981
+ break;
982
+ }
983
+ case "happy":
984
+ lid.add(tube(k, arc(0, -0.05 * size, 0.08 * size, 25, 155), 0.017 * size, threadMaterial(k, color)));
985
+ range = 0.012 * size;
986
+ break;
987
+ case "sleepy":
988
+ lid.add(tube(k, arc(0, 0.045 * size, 0.08 * size, 205, 335), 0.017 * size, threadMaterial(k, color)));
989
+ range = 0.012 * size;
990
+ break;
991
+ case "none":
992
+ break;
993
+ }
994
+ if (pupil)
995
+ lid.add(pupil);
996
+ return { group, eye: { lid, pupil: pupil ?? lid, range } };
997
+ }
998
+ function buildMouth(k, style, color) {
999
+ const three = k.three;
1000
+ const group = new three.Group;
1001
+ const stroke = 0.016;
1002
+ switch (style) {
1003
+ case "smile":
1004
+ group.add(tube(k, arc(0, 0.06, 0.1, 215, 325), stroke, threadMaterial(k, color)));
1005
+ break;
1006
+ case "flat":
1007
+ group.add(tube(k, [[-0.07, 0, 0], [0.07, 0, 0]], stroke, threadMaterial(k, color)));
1008
+ break;
1009
+ case "cat":
1010
+ group.add(tube(k, arc(-0.045, 0.02, 0.045, 200, 340, 10), stroke, threadMaterial(k, color)));
1011
+ group.add(tube(k, arc(0.045, 0.02, 0.045, 200, 340, 10), stroke, threadMaterial(k, color)));
1012
+ break;
1013
+ case "open": {
1014
+ group.add(bead(k, color, 0.075, 0.095, 0.03));
1015
+ const tongue = bead(k, "#e36a7a", 0.05, 0.035, 0.014, k.finish === "felt" ? "felt" : "matte");
1016
+ tongue.position.set(0, -0.048, 0.022);
1017
+ group.add(tongue);
1018
+ break;
1019
+ }
1020
+ case "grin": {
1021
+ const shape = new three.Shape;
1022
+ shape.moveTo(-0.11, 0.02);
1023
+ shape.lineTo(0.11, 0.02);
1024
+ shape.absarc(0, 0.02, 0.11, 0, -Math.PI, true);
1025
+ const geo = new three.ExtrudeGeometry(shape, {
1026
+ depth: 0.012,
1027
+ bevelEnabled: true,
1028
+ bevelThickness: 0.012,
1029
+ bevelSize: 0.012,
1030
+ bevelSegments: 5,
1031
+ curveSegments: 28
1032
+ });
1033
+ group.add(k.finish === "felt" ? k.fuzzy(geo, color, 0.009) : new three.Mesh(geo, finishMaterial(k, color)));
1034
+ const tongue = bead(k, "#e36a7a", 0.055, 0.03, 0.01, k.finish === "felt" ? "felt" : "matte");
1035
+ tongue.position.set(0, -0.055, 0.03);
1036
+ group.add(tongue);
1037
+ break;
1038
+ }
1039
+ case "none":
1040
+ return null;
1041
+ }
1042
+ return group;
1043
+ }
1044
+ function buildGlasses(k, style, eyeX, size, color) {
1045
+ if (style === "none")
1046
+ return null;
1047
+ const three = k.three;
1048
+ const group = new three.Group;
1049
+ const frame = finishMaterial(k, color, k.finish === "felt" ? "matte" : k.finish);
1050
+ const t = 0.018;
1051
+ const r = 0.17 * size;
1052
+ const lensPath = style === "square" ? roundedRectPath(r * 2.15, r * 1.65, r * 0.45) : arc(0, 0, r, 0, 360, 48);
1053
+ const sides = style === "monocle" ? [1] : [-1, 1];
1054
+ for (const s of sides) {
1055
+ const lens = tube(k, lensPath, t, frame, true);
1056
+ lens.position.x = s * eyeX;
1057
+ group.add(lens);
1058
+ if (style === "shades") {
1059
+ const glass = new three.Mesh(new three.CircleGeometry(r * 0.97, 48), new three.MeshPhysicalMaterial({
1060
+ color: "#1b1a24",
1061
+ roughness: k.finish === "gloss" ? 0.05 : 0.3,
1062
+ transparent: true,
1063
+ opacity: 0.88,
1064
+ clearcoat: k.finish === "gloss" ? 1 : 0.3,
1065
+ envMap: k.env,
1066
+ envMapIntensity: k.finish === "gloss" ? 1.4 : 0.7
1067
+ }));
1068
+ glass.position.set(s * eyeX, 0, 0.004);
1069
+ group.add(glass);
1070
+ }
1071
+ const edge = style === "square" ? r * 1.07 : r;
1072
+ group.add(tube(k, [[s * (eyeX + edge), 0.02, 0], [s * (eyeX + edge + 0.05), 0.03, -0.06], [s * (eyeX + edge + 0.07), 0.03, -0.14]], t * 0.8, frame));
1073
+ }
1074
+ if (style === "monocle") {
1075
+ group.add(tube(k, [[eyeX, -r, 0], [eyeX + 0.05, -r - 0.12, -0.02], [eyeX + 0.14, -r - 0.2, -0.06]], t * 0.45, plastic(k, "#caa24a", { metal: true, roughness: 0.3 })));
1076
+ } else {
1077
+ const inner = eyeX - (style === "square" ? r * 1.07 : r);
1078
+ group.add(tube(k, arc(0, -0.01, inner, 30, 150, 12).map(([x, y]) => [x, y, 0]), t * 0.85, frame));
1079
+ }
1080
+ return group;
1081
+ }
1082
+ function buildHat(k, style, color, accent, fur) {
1083
+ const three = k.three;
1084
+ const group = new three.Group;
1085
+ switch (style) {
1086
+ case "top": {
1087
+ const c = color ?? "#221f26";
1088
+ const mat = felt(k, c);
1089
+ const crown = new three.Mesh(new three.CylinderGeometry(0.28, 0.3, 0.42, 48), mat);
1090
+ crown.position.y = 0.23;
1091
+ const brim = new three.Mesh(new three.CylinderGeometry(0.5, 0.5, 0.03, 64), mat);
1092
+ brim.position.y = 0.015;
1093
+ const rim = new three.Mesh(new three.TorusGeometry(0.5, 0.022, 12, 64), mat);
1094
+ rim.rotation.x = Math.PI / 2;
1095
+ rim.position.y = 0.02;
1096
+ const band = new three.Mesh(new three.CylinderGeometry(0.305, 0.31, 0.085, 48), felt(k, accent));
1097
+ band.position.y = 0.09;
1098
+ group.add(crown, brim, rim, band);
1099
+ return { group, height: 0.44, crown: 0.3, brim: 0.5 };
1100
+ }
1101
+ case "beanie": {
1102
+ const c = color ?? "#e0533d";
1103
+ const dome = fur(new three.SphereGeometry(0.42, 48, 24, 0, Math.PI * 2, 0, Math.PI / 2), c, 0.03);
1104
+ dome.scale.y = 0.9;
1105
+ dome.position.y = 0.08;
1106
+ const cuff = new three.Mesh(new three.CylinderGeometry(0.44, 0.44, 0.14, 48, 1), felt(k, c));
1107
+ cuff.position.y = 0.07;
1108
+ const pom = fur(new three.SphereGeometry(0.1, 32, 20), "#fbf6ec", 0.07);
1109
+ pom.position.y = 0.08 + 0.42 * 0.9 + 0.06;
1110
+ group.add(dome, cuff, pom);
1111
+ return { group, height: 0.66, crown: 0.44, brim: 0.44 };
1112
+ }
1113
+ case "party": {
1114
+ const a = color ?? accent;
1115
+ const cone = new three.Mesh(new three.ConeGeometry(0.22, 0.55, 48, 1, true), new three.MeshPhysicalMaterial({
1116
+ map: stripeTexture(three, a, "#fdf7ea"),
1117
+ roughness: 0.55,
1118
+ side: three.DoubleSide,
1119
+ clearcoat: 0.3,
1120
+ envMap: k.env,
1121
+ envMapIntensity: 0.7
1122
+ }));
1123
+ cone.position.y = 0.275;
1124
+ const pom = fur(new three.SphereGeometry(0.07, 28, 18), "#fdf7ea", 0.05);
1125
+ pom.position.y = 0.58;
1126
+ group.add(cone, pom);
1127
+ return { group, height: 0.66, crown: 0.22, brim: 0.22 };
1128
+ }
1129
+ case "crown": {
1130
+ const gold = plastic(k, color ?? "#e8b949", { metal: true, roughness: 0.28 });
1131
+ gold.side = three.DoubleSide;
1132
+ const geo = new three.CylinderGeometry(0.3, 0.28, 0.2, 80, 1, true);
1133
+ const p = geo.getAttribute("position");
1134
+ const points = 5;
1135
+ for (let i = 0;i < p.count; i++) {
1136
+ if (p.getY(i) > 0) {
1137
+ const th = Math.atan2(p.getZ(i), p.getX(i));
1138
+ const w = Math.abs(Math.cos(th * points / 2));
1139
+ p.setY(i, 0.1 + 0.17 * Math.pow(w, 3));
1140
+ }
1141
+ }
1142
+ geo.computeVertexNormals();
1143
+ const band = new three.Mesh(geo, gold);
1144
+ band.position.y = 0.1;
1145
+ group.add(band);
1146
+ for (let i = 0;i < points; i++) {
1147
+ const th = i / points * Math.PI * 2;
1148
+ const ball = blob(k, gold, 0.03, 0.03, 0.03);
1149
+ ball.position.set(Math.cos(th) * 0.3, 0.38, Math.sin(th) * 0.3);
1150
+ group.add(ball);
1151
+ }
1152
+ const jewel = blob(k, plastic(k, accent, { roughness: 0.05 }), 0.045, 0.045, 0.025);
1153
+ jewel.position.set(0, 0.12, 0.3);
1154
+ group.add(jewel);
1155
+ return { group, height: 0.42, crown: 0.3, brim: 0.3 };
1156
+ }
1157
+ case "cowboy": {
1158
+ const c = color ?? ACCESSORY_COLORS.hat.cowboy;
1159
+ const mat = felt(k, c);
1160
+ mat.side = three.DoubleSide;
1161
+ const profile = [[0.29, 0], [0.295, 0.1], [0.285, 0.22], [0.26, 0.31], [0.2, 0.355], [0.1, 0.37], [0, 0.365]];
1162
+ const crownGeo = new three.LatheGeometry(profile.map(([x, y]) => new three.Vector2(x, y)), 64);
1163
+ const p = crownGeo.getAttribute("position");
1164
+ for (let i = 0;i < p.count; i++) {
1165
+ const y = p.getY(i);
1166
+ if (y < 0.2)
1167
+ continue;
1168
+ const f = (y - 0.2) / 0.17;
1169
+ p.setY(i, y - 0.075 * f * Math.exp(-((p.getX(i) / 0.08) ** 2)));
1170
+ p.setX(i, p.getX(i) * (1 - 0.08 * f));
1171
+ }
1172
+ crownGeo.computeVertexNormals();
1173
+ const crownMesh = new three.Mesh(crownGeo, mat);
1174
+ const bend = (x, z) => 1.1 * Math.max(0, Math.abs(x) - 0.3) ** 2 - 0.05 * Math.max(0, Math.abs(z) - 0.3);
1175
+ const brimMesh = new three.Mesh(brim(three, 0.27, 0.66, 0.52, 0, bend), mat);
1176
+ const rim = tube(k, brimEdge(0.66, 0.52, 0, bend), 0.016, mat, true);
1177
+ const band = new three.Mesh(new three.CylinderGeometry(0.296, 0.296, 0.055, 64, 1, true), felt(k, accent));
1178
+ band.material.side = three.DoubleSide;
1179
+ band.position.y = 0.045;
1180
+ group.add(crownMesh, brimMesh, rim, band);
1181
+ return { group, height: 0.38, crown: 0.3, brim: 0.66 };
1182
+ }
1183
+ case "cap": {
1184
+ const c = color ?? ACCESSORY_COLORS.hat.cap;
1185
+ const mat = felt(k, c);
1186
+ const crownMesh = new three.Mesh(dome(three, 0.38, 0.78), mat);
1187
+ mat.side = three.DoubleSide;
1188
+ const billBend = (x, z) => -0.4 * Math.max(0, z - 0.28) - 0.25 * x * x * Math.min(1, Math.max(0, z - 0.2) * 4);
1189
+ const bill = new three.Mesh(brim(three, 0.3, 0.34, 0.36, 0.27, billBend), mat);
1190
+ const billRim = tube(k, brimEdge(0.34, 0.36, 0.27, billBend).filter(([, , z]) => z > 0.05), 0.012, mat);
1191
+ const button = bead(k, c, 0.035, 0.022, 0.035, "matte");
1192
+ button.position.y = 0.38 * 0.78;
1193
+ const seams = new three.Group;
1194
+ const thread = threadMaterial(k, new three.Color(c).multiplyScalar(0.7).getStyle());
1195
+ for (const a of [-1, 0, 1]) {
1196
+ const th = a * 0.9;
1197
+ const pts = arc(0, 0, 0.385, 8, 88, 20).map(([u, v]) => [Math.sin(th) * u, v * 0.78, Math.cos(th) * u]);
1198
+ seams.add(tube(k, pts, 0.006, thread));
1199
+ }
1200
+ group.add(crownMesh, bill, billRim, button, seams);
1201
+ group.rotation.y = 0.75;
1202
+ return { group, height: 0.32, crown: 0.38, brim: 0.38 };
1203
+ }
1204
+ case "hardhat":
1205
+ case "fireman": {
1206
+ const fire = style === "fireman";
1207
+ const c = color ?? ACCESSORY_COLORS.hat[style];
1208
+ const mat = plastic(k, c, { roughness: 0.28 });
1209
+ mat.side = three.DoubleSide;
1210
+ const shell = new three.Mesh(dome(three, 0.37, 0.88), mat);
1211
+ const ridge = tube(k, arc(0, 0, 0.37, 30, 150, 24).map(([u, v]) => [0, v * 0.88, u]), 0.028, mat);
1212
+ const bend = fire ? (_x, z) => -0.12 * Math.max(0, -z - 0.3) ** 1.2 : (_x, z) => -0.04 * Math.max(0, z - 0.3);
1213
+ const [rx, rz, oz] = fire ? [0.5, 0.56, -0.12] : [0.45, 0.47, 0.07];
1214
+ const brimMesh = new three.Mesh(brim(three, 0.36, rx, rz, oz, bend), mat);
1215
+ brimMesh.position.y = 0.01;
1216
+ const rim = tube(k, brimEdge(rx, rz, oz, bend), 0.016, mat, true);
1217
+ rim.position.y = 0.01;
1218
+ group.add(shell, ridge, brimMesh, rim);
1219
+ if (fire) {
1220
+ const shield = new three.Shape;
1221
+ shield.moveTo(0.12, 0.04);
1222
+ shield.absarc(0, 0.04, 0.12, 0, Math.PI, false);
1223
+ shield.lineTo(-0.1, -0.08);
1224
+ shield.quadraticCurveTo(0, -0.19, 0.1, -0.08);
1225
+ shield.closePath();
1226
+ const badge = puffy(k, shield, "#e8b949", 0.012, 0.012, "gloss");
1227
+ badge.material.metalness = 1;
1228
+ badge.position.set(0, 0.2, 0.36);
1229
+ badge.rotation.x = -0.45;
1230
+ const middle = blob(k, plastic(k, accent), 0.045, 0.045, 0.02);
1231
+ middle.position.set(0, 0.215, 0.385);
1232
+ middle.rotation.x = -0.45;
1233
+ group.add(badge, middle);
1234
+ }
1235
+ return { group, height: 0.4, crown: 0.37, brim: 0.47 };
1236
+ }
1237
+ case "santa": {
1238
+ const c = color ?? ACCESSORY_COLORS.hat.santa;
1239
+ const h = 0.62;
1240
+ const geo = new three.ConeGeometry(0.33, h, 40, 24, true);
1241
+ geo.translate(0, h / 2, 0);
1242
+ const p = geo.getAttribute("position");
1243
+ for (let i = 0;i < p.count; i++) {
1244
+ const t = Math.max(0, p.getY(i) / h);
1245
+ p.setX(i, p.getX(i) + 0.34 * t ** 2.2);
1246
+ p.setY(i, p.getY(i) - 0.2 * t ** 3);
1247
+ }
1248
+ geo.computeVertexNormals();
1249
+ const cone = fur(geo, c, 0.025);
1250
+ const cuff = fur(new three.TorusGeometry(0.335, 0.075, 18, 56), "#fbf6ec", 0.06);
1251
+ cuff.rotation.x = Math.PI / 2;
1252
+ cuff.position.y = 0.03;
1253
+ const pom = fur(new three.SphereGeometry(0.085, 28, 18), "#fbf6ec", 0.07);
1254
+ pom.position.set(0.34, h - 0.2 + 0.01, 0);
1255
+ group.add(cone, cuff, pom);
1256
+ return { group, height: 0.56, crown: 0.34, brim: 0.41 };
1257
+ }
1258
+ case "graduation": {
1259
+ const c = color ?? ACCESSORY_COLORS.hat.graduation;
1260
+ const mat = felt(k, c);
1261
+ const skull = new three.Mesh(new three.CylinderGeometry(0.3, 0.31, 0.18, 48), mat);
1262
+ skull.position.y = 0.09;
1263
+ const board = new three.Mesh(new three.BoxGeometry(0.8, 0.03, 0.8), mat);
1264
+ board.position.y = 0.195;
1265
+ board.rotation.y = Math.PI / 4;
1266
+ const button = bead(k, accent, 0.035, 0.02, 0.035, "matte");
1267
+ button.position.y = 0.215;
1268
+ const cord = taperedTube(k, [[0, 0.215, 0], [0.15, 0.214, 0.15], [0.27, 0.21, 0.27], [0.3, 0.16, 0.3], [0.31, 0.06, 0.31]], () => 0.009);
1269
+ const cordMat = threadMaterial(k, accent);
1270
+ const tassel = new three.Mesh(new three.CylinderGeometry(0.014, 0.038, 0.12, 16), cordMat);
1271
+ tassel.position.set(0.31, 0, 0.31);
1272
+ group.add(skull, board, button, new three.Mesh(cord, cordMat), tassel);
1273
+ return { group, height: 0.24, crown: 0.31, brim: 0.31 };
1274
+ }
1275
+ case "fez": {
1276
+ const c = color ?? ACCESSORY_COLORS.hat.fez;
1277
+ const mat = felt(k, c);
1278
+ const body = new three.Mesh(new three.CylinderGeometry(0.21, 0.27, 0.34, 48), mat);
1279
+ body.position.y = 0.17;
1280
+ const black = "#1d1b22";
1281
+ const cordMat = threadMaterial(k, black);
1282
+ const button = bead(k, black, 0.03, 0.015, 0.03, "matte");
1283
+ button.position.y = 0.345;
1284
+ const cord = taperedTube(k, [[0, 0.345, 0], [0.12, 0.35, 0.05], [0.21, 0.32, 0.09], [0.24, 0.24, 0.11]], () => 0.008);
1285
+ const tassel = new three.Mesh(new three.CylinderGeometry(0.012, 0.04, 0.13, 16), cordMat);
1286
+ tassel.position.set(0.245, 0.17, 0.115);
1287
+ group.add(body, button, new three.Mesh(cord, cordMat), tassel);
1288
+ return { group, height: 0.36, crown: 0.27, brim: 0.27 };
1289
+ }
1290
+ case "halo": {
1291
+ const c = color ?? ACCESSORY_COLORS.hat.halo;
1292
+ const ring = new three.Mesh(new three.TorusGeometry(0.3, 0.036, 20, 72), new three.MeshStandardMaterial({
1293
+ color: c,
1294
+ emissive: new three.Color(c),
1295
+ emissiveIntensity: 0.55,
1296
+ roughness: 0.35,
1297
+ metalness: 0.2,
1298
+ envMap: k.env,
1299
+ envMapIntensity: 0.8
1300
+ }));
1301
+ ring.rotation.x = Math.PI / 2 - 0.3;
1302
+ ring.position.y = 0.3;
1303
+ group.add(ring);
1304
+ return { group, height: 0.36, crown: 0.3, brim: 0.3 };
1305
+ }
1306
+ case "none":
1307
+ return null;
1308
+ }
1309
+ }
1310
+ function buildBowtie(k, color) {
1311
+ const three = k.three;
1312
+ const shape = new three.Shape;
1313
+ shape.moveTo(0, 0.03);
1314
+ shape.bezierCurveTo(-0.08, 0.06, -0.2, 0.15, -0.26, 0.11);
1315
+ shape.bezierCurveTo(-0.3, 0.06, -0.3, -0.06, -0.26, -0.11);
1316
+ shape.bezierCurveTo(-0.2, -0.15, -0.08, -0.06, 0, -0.03);
1317
+ shape.bezierCurveTo(0.08, -0.06, 0.2, -0.15, 0.26, -0.11);
1318
+ shape.bezierCurveTo(0.3, -0.06, 0.3, 0.06, 0.26, 0.11);
1319
+ shape.bezierCurveTo(0.2, 0.15, 0.08, 0.06, 0, 0.03);
1320
+ const geo = new three.ExtrudeGeometry(shape, {
1321
+ depth: 0.02,
1322
+ bevelEnabled: true,
1323
+ bevelThickness: 0.03,
1324
+ bevelSize: 0.022,
1325
+ bevelSegments: 6,
1326
+ curveSegments: 24
1327
+ });
1328
+ geo.center();
1329
+ const group = new three.Group;
1330
+ if (k.finish === "felt") {
1331
+ group.add(k.fuzzy(geo, color, 0.01));
1332
+ } else {
1333
+ const cloth = k.finish === "gloss" ? new three.MeshPhysicalMaterial({
1334
+ color,
1335
+ roughness: 0.35,
1336
+ sheen: 0.5,
1337
+ sheenColor: new three.Color("#3a3a3a"),
1338
+ clearcoat: 0.8,
1339
+ clearcoatRoughness: 0.25,
1340
+ envMap: k.env,
1341
+ envMapIntensity: 1
1342
+ }) : finishMaterial(k, color);
1343
+ group.add(new three.Mesh(geo, cloth));
1344
+ }
1345
+ const knot = bead(k, color, 0.055, 0.065, 0.05);
1346
+ knot.position.z = 0.02;
1347
+ group.add(knot);
1348
+ return group;
1349
+ }
1350
+ var ACCESSORY_COLORS = {
1351
+ hat: {
1352
+ top: "#221f26",
1353
+ beanie: "#e0533d",
1354
+ crown: "#e8b949",
1355
+ cowboy: "#b07a45",
1356
+ cap: "#2f6fd6",
1357
+ hardhat: "#f5b80f",
1358
+ fireman: "#c8261d",
1359
+ santa: "#d22b2b",
1360
+ graduation: "#1d1b22",
1361
+ fez: "#b3202a",
1362
+ halo: "#ffd659"
1363
+ },
1364
+ neck: { necktie: "#c43c3c" },
1365
+ pin: { flower: "#fdf7ea", bow: "#e8436e", heart: "#e8436e", star: "#f5c518" }
1366
+ };
1367
+ function taperedTube(k, points, radius, segments = 48) {
1368
+ const three = k.three;
1369
+ const curve = new three.CatmullRomCurve3(points.map((p) => new three.Vector3(...p)), false, "centripetal");
1370
+ const radial = 12;
1371
+ const geometry = new three.TubeGeometry(curve, segments, 1, radial, false);
1372
+ const position = geometry.getAttribute("position");
1373
+ const c = new three.Vector3;
1374
+ const v = new three.Vector3;
1375
+ for (let i = 0;i <= segments; i++) {
1376
+ const u = i / segments;
1377
+ curve.getPointAt(u, c);
1378
+ const r = radius(u);
1379
+ for (let j = 0;j <= radial; j++) {
1380
+ const n = i * (radial + 1) + j;
1381
+ v.fromBufferAttribute(position, n).sub(c).multiplyScalar(r).add(c);
1382
+ position.setXYZ(n, v.x, v.y, v.z);
1383
+ }
1384
+ }
1385
+ return geometry;
1386
+ }
1387
+ function puffy(k, shape, color, depth, bevel, finish = k.finish) {
1388
+ const geometry = new k.three.ExtrudeGeometry(shape, {
1389
+ depth,
1390
+ bevelEnabled: true,
1391
+ bevelThickness: bevel,
1392
+ bevelSize: bevel * 0.8,
1393
+ bevelSegments: 5,
1394
+ curveSegments: 24
1395
+ });
1396
+ geometry.center();
1397
+ return finish === "felt" ? k.fuzzy(geometry, color, 0.008) : new k.three.Mesh(geometry, finishMaterial(k, color, finish));
1398
+ }
1399
+ function shapeFrom(three, points) {
1400
+ const shape = new three.Shape;
1401
+ points.forEach(([x, y], i) => i ? shape.lineTo(x, y) : shape.moveTo(x, y));
1402
+ shape.closePath();
1403
+ return shape;
1404
+ }
1405
+ function starShape(three, r, points = 5, inner = 0.48) {
1406
+ const raw = Array.from({ length: points * 2 }, (_, i) => {
1407
+ const th = Math.PI / 2 + i * Math.PI / points;
1408
+ const rr = i % 2 ? r * inner : r;
1409
+ return [Math.cos(th) * rr, Math.sin(th) * rr];
1410
+ });
1411
+ return shapeFrom(three, roundedPolygon(raw, r * 0.08, 4));
1412
+ }
1413
+ function heartShape(three, r) {
1414
+ const pts = Array.from({ length: 64 }, (_, i) => {
1415
+ const t = i / 64 * Math.PI * 2;
1416
+ return [
1417
+ 16 * Math.sin(t) ** 3 / 17 * r,
1418
+ (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)) / 17 * r
1419
+ ];
1420
+ });
1421
+ return shapeFrom(three, pts);
1422
+ }
1423
+ function bodyStrip(three, at, z, rows = 24, cols = 6, dy = () => 0) {
1424
+ const positions = [];
1425
+ const uvs = [];
1426
+ for (let r = 0;r <= rows; r++) {
1427
+ const t = r / rows;
1428
+ const [x, y, w] = at(t);
1429
+ for (let c = 0;c <= cols; c++) {
1430
+ const s = c / cols * 2 - 1;
1431
+ const px = x + s * w;
1432
+ const py = y + dy(s, t);
1433
+ positions.push(px, py, z(px, py));
1434
+ uvs.push(c / cols, 1 - t);
1435
+ }
1436
+ }
1437
+ const index = [];
1438
+ for (let r = 0;r < rows; r++) {
1439
+ for (let c = 0;c < cols; c++) {
1440
+ const a = r * (cols + 1) + c;
1441
+ const b = a + cols + 1;
1442
+ index.push(a, b, a + 1, a + 1, b, b + 1);
1443
+ }
1444
+ }
1445
+ const geometry = new three.BufferGeometry;
1446
+ geometry.setAttribute("position", new three.Float32BufferAttribute(positions, 3));
1447
+ geometry.setAttribute("uv", new three.Float32BufferAttribute(uvs, 2));
1448
+ geometry.setIndex(index);
1449
+ geometry.computeVertexNormals();
1450
+ return geometry;
1451
+ }
1452
+ function dome(three, r, squash) {
1453
+ const geometry = new three.SphereGeometry(r, 48, 20, 0, Math.PI * 2, 0, Math.PI / 2);
1454
+ geometry.scale(1, squash, 1);
1455
+ return geometry;
1456
+ }
1457
+ function brim(three, inner, rx, rz, offsetZ, bend) {
1458
+ const geometry = new three.RingGeometry(inner, 1, 96, 8);
1459
+ const position = geometry.getAttribute("position");
1460
+ for (let i = 0;i < position.count; i++) {
1461
+ const x0 = position.getX(i);
1462
+ const y0 = position.getY(i);
1463
+ const d = Math.hypot(x0, y0);
1464
+ const f = (d - inner) / (1 - inner);
1465
+ const a = Math.atan2(y0, x0);
1466
+ const ex = Math.cos(a) * (inner + (rx - inner) * f);
1467
+ const ez = Math.sin(a) * (inner + (rz - inner) * f) + offsetZ * f;
1468
+ position.setXYZ(i, ex, bend(ex, ez), ez);
1469
+ }
1470
+ geometry.computeVertexNormals();
1471
+ return geometry;
1472
+ }
1473
+ function brimEdge(rx, rz, offsetZ, bend, n = 96) {
1474
+ return Array.from({ length: n }, (_, i) => {
1475
+ const a = i / n * Math.PI * 2;
1476
+ const x = Math.cos(a) * rx;
1477
+ const z = Math.sin(a) * rz + offsetZ;
1478
+ return [x, bend(x, z), z];
1479
+ });
1480
+ }
1481
+ function buildMoustache(k, style, color) {
1482
+ if (style === "none")
1483
+ return null;
1484
+ const three = k.three;
1485
+ const group = new three.Group;
1486
+ const material = finishMaterial(k, color, k.finish === "gloss" ? "satin" : k.finish);
1487
+ const wrap = (geometry) => k.finish === "felt" ? k.fuzzy(geometry, color, 0.008) : new three.Mesh(geometry, material);
1488
+ switch (style) {
1489
+ case "curly": {
1490
+ const half = [
1491
+ [0.05, -0.008, 0.008],
1492
+ [0.1, -0.016, 0.002],
1493
+ [0.142, -0.004, -0.006],
1494
+ [0.16, 0.026, -0.01],
1495
+ [0.148, 0.05, -0.012],
1496
+ [0.124, 0.046, -0.01],
1497
+ [0.12, 0.03, -0.008]
1498
+ ];
1499
+ const path = [
1500
+ ...half.map(([x, y, z]) => [-x, y, z]).reverse(),
1501
+ [0, 0.002, 0.012],
1502
+ ...half
1503
+ ];
1504
+ const radius = (u) => 0.007 + 0.03 * Math.pow(Math.max(0, 1 - Math.abs(2 * u - 1)), 0.9);
1505
+ group.add(wrap(taperedTube(k, path, radius, 96)));
1506
+ break;
1507
+ }
1508
+ case "walrus": {
1509
+ const shape = new three.Shape;
1510
+ shape.moveTo(0, 0.04);
1511
+ shape.bezierCurveTo(-0.09, 0.07, -0.2, 0.04, -0.22, -0.075);
1512
+ shape.bezierCurveTo(-0.15, -0.06, -0.07, -0.055, 0, -0.025);
1513
+ shape.bezierCurveTo(0.07, -0.055, 0.15, -0.06, 0.22, -0.075);
1514
+ shape.bezierCurveTo(0.2, 0.04, 0.09, 0.07, 0, 0.04);
1515
+ const geometry = new three.ExtrudeGeometry(shape, {
1516
+ depth: 0.02,
1517
+ bevelEnabled: true,
1518
+ bevelThickness: 0.022,
1519
+ bevelSize: 0.016,
1520
+ bevelSegments: 6,
1521
+ curveSegments: 24
1522
+ });
1523
+ geometry.center();
1524
+ group.add(k.fuzzy(geometry, color, 0.02));
1525
+ break;
1526
+ }
1527
+ case "pencil":
1528
+ for (const s of [-1, 1]) {
1529
+ group.add(tube(k, [[s * 0.016, 0.004, 0], [s * 0.065, 0.012, 0], [s * 0.125, -0.004, 0]], 0.012, threadMaterial(k, color)));
1530
+ }
1531
+ break;
1532
+ }
1533
+ return group;
1534
+ }
1535
+ function buildNeck(k, style, color, accent, site) {
1536
+ const three = k.three;
1537
+ const group = new three.Group;
1538
+ switch (style) {
1539
+ case "necktie": {
1540
+ const top = site.y;
1541
+ const tip = Math.min(top - 0.24, Math.max(site.floor(0) + 0.04, top - 0.62));
1542
+ const length = top - 0.05 - tip;
1543
+ const blade = bodyStrip(three, (t) => [0, top - 0.05 - t * length, 0.042 + 0.05 * Math.min(1, t * 1.4)], (x, y) => site.front(x, y) + 0.035, 28, 6, (s, t) => -Math.max(0, (t - 0.86) / 0.14) * 0.07 * (1 - Math.abs(s)));
1544
+ const cloth = k.finish === "felt" ? k.fuzzy(blade, color, 0.008) : new three.Mesh(blade, new three.MeshPhysicalMaterial({
1545
+ map: stripeTexture(three, color, accent),
1546
+ roughness: 0.5,
1547
+ sheen: 0.4,
1548
+ clearcoat: k.finish === "gloss" ? 0.6 : 0.1,
1549
+ side: three.DoubleSide,
1550
+ envMap: k.env,
1551
+ envMapIntensity: 0.6
1552
+ }));
1553
+ group.add(cloth);
1554
+ const knot = bead(k, color, 0.055, 0.05, 0.035);
1555
+ knot.position.set(0, top - 0.02, site.front(0, top) + 0.06);
1556
+ group.add(knot);
1557
+ return group;
1558
+ }
1559
+ case "bowtie":
1560
+ case "none":
1561
+ return null;
1562
+ }
1563
+ }
1564
+ function buildPin(k, style, color) {
1565
+ const three = k.three;
1566
+ const group = new three.Group;
1567
+ switch (style) {
1568
+ case "flower":
1569
+ for (let i = 0;i < 5; i++) {
1570
+ const th = i / 5 * Math.PI * 2 + Math.PI / 2;
1571
+ const petal = bead(k, color, 0.06, 0.036, 0.022);
1572
+ petal.position.set(Math.cos(th) * 0.058, Math.sin(th) * 0.058, 0);
1573
+ petal.rotation.z = th;
1574
+ group.add(petal);
1575
+ }
1576
+ {
1577
+ const middle = bead(k, "#f5c518", 0.04, 0.04, 0.03);
1578
+ middle.position.z = 0.016;
1579
+ group.add(middle);
1580
+ }
1581
+ break;
1582
+ case "bow": {
1583
+ const bow = buildBowtie(k, color);
1584
+ bow.scale.setScalar(0.5);
1585
+ group.add(bow);
1586
+ for (const s of [-1, 1]) {
1587
+ const tail = tube(k, [[s * 0.012, -0.02, 0], [s * 0.04, -0.07, -0.005], [s * 0.065, -0.11, -0.01]], 0.016, finishMaterial(k, color, k.finish === "felt" ? "matte" : k.finish));
1588
+ group.add(tail);
1589
+ }
1590
+ break;
1591
+ }
1592
+ case "heart":
1593
+ group.add(puffy(k, heartShape(three, 0.12), color, 0.02, 0.022));
1594
+ break;
1595
+ case "star":
1596
+ group.add(puffy(k, starShape(three, 0.13), color, 0.02, 0.02));
1597
+ break;
1598
+ case "badge": {
1599
+ const rim = new three.Mesh(new three.TorusGeometry(0.105, 0.014, 12, 48), plastic(k, "#c9ccd3", { metal: true, roughness: 0.25 }));
1600
+ const face = new three.Mesh(new three.CylinderGeometry(0.105, 0.105, 0.022, 48), plastic(k, color));
1601
+ face.rotation.x = Math.PI / 2;
1602
+ const star = puffy(k, starShape(three, 0.06), "#fdf7ea", 0.004, 0.006, "gloss");
1603
+ star.position.z = 0.016;
1604
+ group.add(rim, face, star);
1605
+ break;
1606
+ }
1607
+ case "none":
1608
+ return null;
1609
+ }
1610
+ return group;
1611
+ }
1612
+ var FABRICS = {
1613
+ plush: { fur: 0.05, grain: 1, sheen: 1, mottle: 0, root: 0.58, gravity: 0.35 },
1614
+ felt: { fur: 0.016, grain: 0.8, sheen: 0.3, mottle: 0.17, root: 0.8, gravity: 0.08 },
1615
+ velvet: { fur: 0.012, grain: 0.9, sheen: 2.1, mottle: 0, root: 0.45, gravity: 0 },
1616
+ shaggy: { fur: 0.11, grain: 1.7, sheen: 0.8, mottle: 0.06, root: 0.5, gravity: 0.7 },
1617
+ fleece: { fur: 0.06, grain: 2.8, sheen: 0.45, mottle: 0.14, root: 0.55, gravity: -0.2 }
1618
+ };
1619
+ function applyDepthBias(material, bias) {
1620
+ material.onBeforeCompile = (shader) => {
1621
+ shader.uniforms.uBias = bias;
1622
+ shader.vertexShader = shader.vertexShader.replace("#include <common>", `#include <common>
1623
+ uniform float uBias;`).replace("#include <project_vertex>", `#include <project_vertex>
1624
+ mvPosition.xyz += normalize(-mvPosition.xyz) * uBias;
1625
+ gl_Position = projectionMatrix * mvPosition;`);
1626
+ };
1627
+ material.customProgramCacheKey = () => "plushies-depth-bias";
1628
+ }
1629
+ var FACE_LIFT = {
1630
+ triangle: 0.2,
1631
+ heart: 0.14,
1632
+ star: 0.06,
1633
+ cloud: 0.04,
1634
+ ghost: 0.1,
1635
+ drop: -0.02,
1636
+ arch: 0.08,
1637
+ bean: -0.04
1638
+ };
1639
+ function insidePolygon(poly, x, y) {
1640
+ let inside = false;
1641
+ for (let i = 0, j = poly.length - 1;i < poly.length; j = i++) {
1642
+ const [xi, yi] = poly[i];
1643
+ const [xj, yj] = poly[j];
1644
+ if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi)
1645
+ inside = !inside;
1646
+ }
1647
+ return inside;
1648
+ }
1649
+ function reach(poly, x, y, dx, dy, margin = 0) {
1650
+ const l = Math.hypot(dx, dy);
1651
+ let d = 0;
1652
+ while (d < 3) {
1653
+ const px = x + (d + 0.005) * dx / l;
1654
+ const py = y + (d + 0.005) * dy / l;
1655
+ if (!insidePolygon(poly, px, py) || distanceToOutline(px, py, poly) < margin)
1656
+ break;
1657
+ d += 0.005;
1658
+ }
1659
+ return d;
1660
+ }
1661
+ var HAT_TILT = 0.2;
1662
+ function hatTilt(outline) {
1663
+ let top = 0;
1664
+ outline.forEach((p, i) => {
1665
+ if (p[1] + 0.001 * p[0] > outline[top][1] + 0.001 * outline[top][0])
1666
+ top = i;
1667
+ });
1668
+ const n = outline.length;
1669
+ const flat = (i) => outline[i][1] > outline[top][1] - 0.015;
1670
+ let from = top;
1671
+ let to = top;
1672
+ for (let k = 0;k < n && flat((from - 1 + n) % n); k++)
1673
+ from = (from - 1 + n) % n;
1674
+ for (let k = 0;k < n && flat((to + 1) % n); k++)
1675
+ to = (to + 1) % n;
1676
+ top = (from + Math.round(((to - from) % n + n) % n / 2)) % n;
1677
+ const walk = (dir) => {
1678
+ let i = top;
1679
+ let d = 0;
1680
+ while (d < 0.15) {
1681
+ const j = (i + dir + n) % n;
1682
+ d += Math.hypot(outline[j][0] - outline[i][0], outline[j][1] - outline[i][1]);
1683
+ i = j;
1684
+ }
1685
+ return outline[i];
1686
+ };
1687
+ const [a, b] = [walk(-1), walk(1)];
1688
+ let tilt = Math.atan2(b[1] - a[1], b[0] - a[0]) - Math.PI;
1689
+ if (tilt < -Math.PI)
1690
+ tilt += Math.PI * 2;
1691
+ return { tilt: tilt - HAT_TILT, at: outline[top] };
1692
+ }
1693
+ function seatHat(outline, tilt, at, crown, brim2, size) {
1694
+ const right = [Math.cos(tilt), Math.sin(tilt)];
1695
+ const up = [-Math.sin(tilt), Math.cos(tilt)];
1696
+ const c = at[0] * right[0] + at[1] * right[1];
1697
+ let inCrown = -Infinity;
1698
+ let underBrim = -Infinity;
1699
+ for (const [x, y] of outline) {
1700
+ const l = Math.abs(x * right[0] + y * right[1] - c);
1701
+ const h = x * up[0] + y * up[1];
1702
+ if (l <= crown * size)
1703
+ inCrown = Math.max(inCrown, h);
1704
+ else if (l <= brim2 * size)
1705
+ underBrim = Math.max(underBrim, h);
1706
+ }
1707
+ const base = Math.max(inCrown - 0.12 * size, underBrim - 0.02 * size);
1708
+ return [right[0] * c + up[0] * base, right[1] * c + up[1] * base];
1709
+ }
1710
+ function buildRig(three, spec) {
1711
+ const env = studioEnvironment(three, spec.renderer);
1712
+ const strand = 0.0115 * spec.furGrain;
1713
+ const bias = { value: 0 };
1714
+ const furs = [];
1715
+ const fuzzy = (geometry, color, length) => {
1716
+ const fur = furMaterial(three, {
1717
+ length,
1718
+ strand: Math.min(strand, 0.0115) * 0.8,
1719
+ gravity: 0.15,
1720
+ root: 0.7,
1721
+ layers: 10,
1722
+ sheen: 0.6,
1723
+ bias
1724
+ });
1725
+ setFurColor(fur, toRgb(three, color));
1726
+ furs.push(fur);
1727
+ const mesh = new three.Mesh(furShells(three, geometry, 10), fur.material);
1728
+ mesh.renderOrder = 1;
1729
+ return mesh;
1730
+ };
1731
+ const kit = {
1732
+ three,
1733
+ env,
1734
+ thread: threadTexture(three),
1735
+ grain: grainTexture(three, 7),
1736
+ finish: spec.finish,
1737
+ fuzzy
1738
+ };
1739
+ const outline = silhouette(spec.kind, spec.outline);
1740
+ const body = inflate(three, outline, spec.thickness);
1741
+ let minY = Infinity;
1742
+ let maxX = 0;
1743
+ for (const [x, y] of outline) {
1744
+ minY = Math.min(minY, y);
1745
+ maxX = Math.max(maxX, Math.abs(x));
1746
+ }
1747
+ const bottom = minY;
1748
+ const perch = hatTilt(outline);
1749
+ const outer = new three.Group;
1750
+ const pivot = new three.Group;
1751
+ pivot.position.y = bottom;
1752
+ outer.add(pivot);
1753
+ const inner = new three.Group;
1754
+ inner.position.y = -bottom;
1755
+ pivot.add(inner);
1756
+ const bodyFur = furMaterial(three, {
1757
+ length: spec.fabric.fur,
1758
+ strand,
1759
+ sheen: spec.fabric.sheen,
1760
+ mottle: spec.fabric.mottle,
1761
+ root: spec.fabric.root,
1762
+ gravity: spec.fabric.gravity
1763
+ });
1764
+ const bodyMesh = new three.Mesh(furShells(three, body.geometry), bodyFur.material);
1765
+ inner.add(bodyMesh);
1766
+ const floaters = [];
1767
+ const compensate = [];
1768
+ const [cx, cy] = body.center;
1769
+ const eyeY = cy + 0.12 + (FACE_LIFT[spec.kind] ?? 0) + spec.faceY;
1770
+ const eyeX = 0.19 * spec.eyeSpacing * Math.max(0.8, spec.eyeSize);
1771
+ const gap = 0.07;
1772
+ const surface = (x, y) => body.heightAt(x, y) + gap;
1773
+ const face = new three.Group;
1774
+ face.position.set(cx, eyeY, 0);
1775
+ inner.add(face);
1776
+ const place = (object, x, y, lift = 0) => {
1777
+ object.position.set(x - cx, y - eyeY, surface(x, y) + lift);
1778
+ face.add(object);
1779
+ floaters.push({ object, rest: object.position.z, lift: 1 });
1780
+ compensate.push(object);
1781
+ };
1782
+ const eyes = [];
1783
+ if (spec.eyes !== "none") {
1784
+ for (const s of [-1, 1]) {
1785
+ const { group, eye } = buildEye(kit, spec.eyes, spec.eyeSize, spec.featureColor);
1786
+ place(group, cx + s * eyeX, eyeY);
1787
+ eyes.push(eye);
1788
+ }
1789
+ }
1790
+ const mouthY = eyeY - 0.2 - 0.04 * spec.eyeSize;
1791
+ const mouth = buildMouth(kit, spec.mouth, spec.featureColor);
1792
+ if (mouth)
1793
+ place(mouth, cx, mouthY);
1794
+ const moustache = buildMoustache(kit, spec.moustache, spec.moustacheColor);
1795
+ if (moustache) {
1796
+ const above = spec.mouth === "none" ? 0.03 : spec.mouth === "open" ? 0.11 : 0.075;
1797
+ place(moustache, cx, mouthY + above, 0.015);
1798
+ }
1799
+ if (spec.cheeks) {
1800
+ for (const s of [-1, 1]) {
1801
+ const disc = new three.SphereGeometry(1, 32, 16);
1802
+ disc.scale(0.085, 0.055, 0.022);
1803
+ const cheek = fuzzy(disc, spec.cheekColor, 0.014);
1804
+ const x = cx + s * (eyeX + 0.1);
1805
+ place(cheek, x, eyeY - 0.13);
1806
+ }
1807
+ }
1808
+ let glasses = null;
1809
+ if (spec.glasses !== "none") {
1810
+ glasses = buildGlasses(kit, spec.glasses, eyeX, Math.max(0.85, spec.eyeSize), spec.glassesColor);
1811
+ if (glasses) {
1812
+ place(glasses, cx, eyeY, 0.05);
1813
+ face.updateWorldMatrix(true, true);
1814
+ const toFace = face.matrixWorld.clone().invert();
1815
+ const inFace = (object) => new three.Box3().setFromObject(object).applyMatrix4(toFace);
1816
+ const lenses = inFace(glasses);
1817
+ let front = -Infinity;
1818
+ for (const child of face.children) {
1819
+ if (child === glasses)
1820
+ continue;
1821
+ const box = inFace(child);
1822
+ if (box.max.x < lenses.min.x || box.min.x > lenses.max.x)
1823
+ continue;
1824
+ if (box.max.y < lenses.min.y || box.min.y > lenses.max.y)
1825
+ continue;
1826
+ front = Math.max(front, box.max.z);
1827
+ }
1828
+ glasses.position.z += Math.max(0, front + 0.03 - lenses.min.z);
1829
+ floaters[floaters.length - 1].rest = glasses.position.z;
1830
+ }
1831
+ }
1832
+ const worn = [face];
1833
+ if (spec.neck === "bowtie") {
1834
+ const tie = buildBowtie(kit, spec.neckColor);
1835
+ tie.scale.setScalar(0.85);
1836
+ const y = bottom + 0.17;
1837
+ tie.position.set(0, y, surface(0, y) + 0.04);
1838
+ inner.add(tie);
1839
+ worn.push(tie);
1840
+ floaters.push({ object: tie, rest: tie.position.z, lift: 0.6 });
1841
+ } else if (spec.neck !== "none") {
1842
+ const y = Math.max(mouthY - 0.16, bottom + 0.3);
1843
+ const neck = buildNeck(kit, spec.neck, spec.neckColor, spec.accent, {
1844
+ y,
1845
+ floor: (x) => y - reach(outline, x, y, 0, -1, 0.08),
1846
+ front: (x, yy) => body.heightAt(x, yy)
1847
+ });
1848
+ if (neck) {
1849
+ inner.add(neck);
1850
+ worn.push(neck);
1851
+ floaters.push({ object: neck, rest: 0, lift: 0.4 });
1852
+ }
1853
+ }
1854
+ const pin = buildPin(kit, spec.pin, spec.pinColor);
1855
+ if (pin) {
1856
+ const head = spec.pin === "flower" || spec.pin === "bow" || spec.pin === "heart";
1857
+ const angle = (head ? 130 : -35) * Math.PI / 180;
1858
+ const dx = Math.cos(angle);
1859
+ const dy = Math.sin(angle);
1860
+ const r = reach(outline, cx, cy, dx, dy) * (head ? 0.74 : 0.6);
1861
+ const x = cx + dx * r;
1862
+ const y = cy + dy * r;
1863
+ pin.position.set(x, y, surface(x, y) + 0.01);
1864
+ const e = 0.02;
1865
+ const n = new three.Vector3(-(body.heightAt(x + e, y) - body.heightAt(x - e, y)) / (2 * e), -(body.heightAt(x, y + e) - body.heightAt(x, y - e)) / (2 * e), 1).normalize();
1866
+ n.lerp(new three.Vector3(0, 0, 1), 0.4).normalize();
1867
+ pin.quaternion.setFromUnitVectors(new three.Vector3(0, 0, 1), n);
1868
+ pin.rotateZ(head ? 0.35 : -0.2);
1869
+ inner.add(pin);
1870
+ worn.push(pin);
1871
+ compensate.push(pin);
1872
+ floaters.push({ object: pin, rest: pin.position.z, lift: 0.5 });
1873
+ }
1874
+ let hat = null;
1875
+ let hatTop = perch.at[1];
1876
+ let hatHalfWidth = 0;
1877
+ const hatRest = [perch.at[0], perch.at[1]];
1878
+ const hatParts = buildHat(kit, spec.hat, spec.hatColor, spec.accent, fuzzy);
1879
+ if (hatParts) {
1880
+ hat = new three.Group;
1881
+ hat.name = "hat";
1882
+ hat.add(hatParts.group);
1883
+ hat.scale.setScalar(spec.hatSize);
1884
+ [hatRest[0], hatRest[1]] = seatHat(outline, perch.tilt, perch.at, hatParts.crown, hatParts.brim, spec.hatSize);
1885
+ hat.position.set(hatRest[0], hatRest[1], 0);
1886
+ hat.rotation.z = perch.tilt;
1887
+ inner.add(hat);
1888
+ worn.push(hat);
1889
+ hatTop = hatRest[1] + Math.cos(perch.tilt) * (hatParts.height + 0.1) * spec.hatSize;
1890
+ hat.updateMatrixWorld(true);
1891
+ const box = new three.Box3().setFromObject(hat);
1892
+ hatHalfWidth = Math.max(-box.min.x, box.max.x);
1893
+ }
1894
+ const biased = new Set;
1895
+ for (const root of worn) {
1896
+ root?.traverse((child) => {
1897
+ const material = child.material;
1898
+ for (const m of Array.isArray(material) ? material : material ? [material] : []) {
1899
+ if (!(m instanceof three.ShaderMaterial) && !biased.has(m)) {
1900
+ biased.add(m);
1901
+ applyDepthBias(m, bias);
1902
+ }
1903
+ }
1904
+ });
1905
+ }
1906
+ let shadow = null;
1907
+ if (spec.shadow) {
1908
+ shadow = new three.Mesh(new three.PlaneGeometry(1, 1), new three.MeshBasicMaterial({
1909
+ map: shadowTexture(three),
1910
+ color: "#000000",
1911
+ transparent: true,
1912
+ depthWrite: false,
1913
+ opacity: 0.4
1914
+ }));
1915
+ shadow.renderOrder = -1;
1916
+ outer.add(shadow);
1917
+ }
1918
+ const shadowBottom = bottom - 0.14;
1919
+ const fitTop = hatTop + 0.08 + spec.headroom * 2;
1920
+ return {
1921
+ outer,
1922
+ pivot,
1923
+ face,
1924
+ eyes,
1925
+ glasses,
1926
+ hat,
1927
+ hatRest,
1928
+ floaters,
1929
+ shadow,
1930
+ bottom,
1931
+ fitW: 2 * Math.max(maxX + 0.12, hatHalfWidth + 0.04),
1932
+ fitH: fitTop - shadowBottom,
1933
+ fitMid: (fitTop + shadowBottom) / 2,
1934
+ furs,
1935
+ bodyFur,
1936
+ bodyGeometry: bodyMesh.geometry,
1937
+ compensate,
1938
+ bias
1939
+ };
1940
+ }
1941
+ function addLights(three, root) {
1942
+ root.add(new three.HemisphereLight("#fffaf2", "#6e655d", 1.25));
1943
+ const key = new three.DirectionalLight("#fff4e4", 2.3);
1944
+ key.position.set(-300, 420, 520);
1945
+ const rim = new three.DirectionalLight("#ffffff", 2.4);
1946
+ rim.position.set(380, 260, -420);
1947
+ const fill = new three.DirectionalLight("#e8eeff", 0.55);
1948
+ fill.position.set(420, -120, 400);
1949
+ root.add(key, rim, fill);
1950
+ }
1951
+ var DEFAULT_POSE = {
1952
+ width: 600,
1953
+ height: 600,
1954
+ pixelRatio: 1,
1955
+ lookX: 0,
1956
+ lookY: 0,
1957
+ blink: 0,
1958
+ squash: 0,
1959
+ hop: 0,
1960
+ lean: 0,
1961
+ turn: 0,
1962
+ float: 1,
1963
+ color: "#f2b33d"
1964
+ };
1965
+ var DEFAULT_OPTIONS = {
1966
+ kind: "circle",
1967
+ roundness: 0.6,
1968
+ starInner: 0.55,
1969
+ seed: 1,
1970
+ thickness: 0.42,
1971
+ fabric: "plush",
1972
+ finish: "satin",
1973
+ eyes: "dot",
1974
+ eyeSize: 1,
1975
+ eyeSpacing: 1,
1976
+ faceY: 0,
1977
+ mouth: "none",
1978
+ cheeks: false,
1979
+ glasses: "none",
1980
+ moustache: "none",
1981
+ hat: "none",
1982
+ hatSize: 1,
1983
+ neck: "none",
1984
+ pin: "none",
1985
+ featureColor: "#18130f",
1986
+ accentColor: "#5b6cff",
1987
+ cheekColor: "#f08a9b",
1988
+ headroom: 0.22,
1989
+ shadow: true,
1990
+ lights: true
1991
+ };
1992
+ var PLUSHIE_KINDS = [
1993
+ "circle",
1994
+ "square",
1995
+ "triangle",
1996
+ "heart",
1997
+ "star",
1998
+ "pill",
1999
+ "cloud",
2000
+ "polygon",
2001
+ "blob",
2002
+ "egg",
2003
+ "drop",
2004
+ "ghost",
2005
+ "bean",
2006
+ "flower",
2007
+ "diamond",
2008
+ "squircle",
2009
+ "arch"
2010
+ ];
2011
+ var PLUSHIE_FABRICS = ["plush", "felt", "velvet", "shaggy", "fleece"];
2012
+ var PLUSHIE_FINISHES = ["gloss", "satin", "matte", "felt"];
2013
+ var PLUSHIE_EYES = ["dot", "oval", "googly", "ring", "happy", "sleepy", "none"];
2014
+ var PLUSHIE_MOUTHS = ["none", "smile", "grin", "open", "flat", "cat"];
2015
+ var PLUSHIE_GLASSES = ["none", "round", "square", "monocle", "shades"];
2016
+ var PLUSHIE_HATS = [
2017
+ "none",
2018
+ "top",
2019
+ "beanie",
2020
+ "party",
2021
+ "crown",
2022
+ "cowboy",
2023
+ "cap",
2024
+ "hardhat",
2025
+ "fireman",
2026
+ "santa",
2027
+ "graduation",
2028
+ "fez",
2029
+ "halo"
2030
+ ];
2031
+ var PLUSHIE_NECKS = ["none", "bowtie", "necktie"];
2032
+ var PLUSHIE_PINS = ["none", "flower", "bow", "heart", "star", "badge"];
2033
+ var PLUSHIE_MOUSTACHES = ["none", "curly", "walrus", "pencil"];
2034
+ function fabricFur(fabric = "plush") {
2035
+ return (FABRICS[fabric] ?? FABRICS.plush).fur;
2036
+ }
2037
+ function fabricGrain(fabric = "plush") {
2038
+ return (FABRICS[fabric] ?? FABRICS.plush).grain;
2039
+ }
2040
+ function plushieOutline(kind = "circle", options = {}) {
2041
+ const { roundness = 0.6, sides, starInner = 0.55, seed = 1 } = options;
2042
+ return silhouette(kind, { roundness, sides, starInner, seed });
2043
+ }
2044
+ var MAX_FUR_SHELLS = FUR_SHELLS;
2045
+ var warned = new Set;
2046
+ function warnOnce(message) {
2047
+ if (warned.has(message))
2048
+ return;
2049
+ warned.add(message);
2050
+ console.warn(`plushies: ${message}`);
2051
+ }
2052
+ function oneOf(name, value, allowed, fallback) {
2053
+ if (value === undefined)
2054
+ return fallback;
2055
+ if (allowed.includes(value))
2056
+ return value;
2057
+ warnOnce(`unknown ${name} ${JSON.stringify(value)}, using ${JSON.stringify(fallback)}`);
2058
+ return fallback;
2059
+ }
2060
+ function flag(name, value, fallback) {
2061
+ if (value === undefined)
2062
+ return fallback;
2063
+ if (typeof value === "boolean")
2064
+ return value;
2065
+ warnOnce(`${name} must be true or false, got ${JSON.stringify(value)}; using ${fallback}`);
2066
+ return fallback;
2067
+ }
2068
+ function within(name, value, fallback, min, max) {
2069
+ if (value === undefined)
2070
+ return fallback;
2071
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2072
+ warnOnce(`${name} must be a number, got ${String(value)}; using ${fallback}`);
2073
+ return fallback;
2074
+ }
2075
+ if (value < min || value > max) {
2076
+ warnOnce(`${name} ${value} is outside ${min}..${max}; clamping`);
2077
+ return Math.max(min, Math.min(max, value));
2078
+ }
2079
+ return value;
2080
+ }
2081
+ function finitePose(next) {
2082
+ let out = null;
2083
+ for (const key of Object.keys(next)) {
2084
+ const value = next[key];
2085
+ if (value === undefined || typeof value === "number" && !Number.isFinite(value)) {
2086
+ if (value !== undefined)
2087
+ warnOnce(`pose ${key} must be a finite number, got ${String(value)}; ignored`);
2088
+ out ??= { ...next };
2089
+ delete out[key];
2090
+ }
2091
+ }
2092
+ return out ?? next;
2093
+ }
2094
+ function createPlushie(three, options = {}) {
2095
+ const {
2096
+ kind: kindOption,
2097
+ roundness: roundnessOption,
2098
+ sides: sidesOption,
2099
+ starInner: starInnerOption,
2100
+ seed: seedOption,
2101
+ thickness: thicknessOption,
2102
+ fabric: fabricOption,
2103
+ furGrain: furGrainOption,
2104
+ finish: finishOption,
2105
+ eyes: eyesOption,
2106
+ eyeSize: eyeSizeOption,
2107
+ eyeSpacing: eyeSpacingOption,
2108
+ faceY: faceYOption,
2109
+ mouth: mouthOption,
2110
+ cheeks: cheeksOption,
2111
+ glasses: glassesOption,
2112
+ moustache: moustacheOption,
2113
+ hat: hatOption,
2114
+ hatSize: hatSizeOption,
2115
+ bowtie: bowtieOption,
2116
+ neck: neckOption,
2117
+ pin: pinOption,
2118
+ featureColor = "#18130f",
2119
+ moustacheColor,
2120
+ glassesColor,
2121
+ hatColor,
2122
+ neckColor,
2123
+ bowtieColor,
2124
+ pinColor,
2125
+ accentColor = "#5b6cff",
2126
+ cheekColor = "#f08a9b",
2127
+ headroom: headroomOption,
2128
+ shadow: shadowOption,
2129
+ lights: lightsOption,
2130
+ renderer,
2131
+ ...initial
2132
+ } = options;
2133
+ const cheeks = flag("cheeks", cheeksOption, false);
2134
+ const bowtie = flag("bowtie", bowtieOption, false);
2135
+ const shadow = flag("shadow", shadowOption, true);
2136
+ const lights = flag("lights", lightsOption, true);
2137
+ const kind = oneOf("kind", kindOption, PLUSHIE_KINDS, "circle");
2138
+ const roundness = within("roundness", roundnessOption, 0.6, 0, 1);
2139
+ const sides = sidesOption === undefined ? undefined : Math.round(within("sides", sidesOption, 6, 3, 24));
2140
+ const starInner = within("starInner", starInnerOption, 0.55, 0.2, 0.9);
2141
+ const seed = within("seed", seedOption, 1, -1e9, 1e9);
2142
+ const thickness = within("thickness", thicknessOption, 0.42, 0.05, 1.5);
2143
+ const fabric = oneOf("fabric", fabricOption, PLUSHIE_FABRICS, "plush");
2144
+ const furGrain = furGrainOption === undefined ? undefined : within("furGrain", furGrainOption, 1, 0.1, 6);
2145
+ const finish = oneOf("finish", finishOption, PLUSHIE_FINISHES, "satin");
2146
+ const eyes = oneOf("eyes", eyesOption, PLUSHIE_EYES, "dot");
2147
+ const eyeSize = within("eyeSize", eyeSizeOption, 1, 0.1, 4);
2148
+ const eyeSpacing = within("eyeSpacing", eyeSpacingOption, 1, 0, 4);
2149
+ const faceY = within("faceY", faceYOption, 0, -1, 1);
2150
+ const mouth = oneOf("mouth", mouthOption, PLUSHIE_MOUTHS, "none");
2151
+ const glasses = oneOf("glasses", glassesOption, PLUSHIE_GLASSES, "none");
2152
+ const moustache = oneOf("moustache", moustacheOption, PLUSHIE_MOUSTACHES, "none");
2153
+ const hat = oneOf("hat", hatOption, PLUSHIE_HATS, "none");
2154
+ const hatSize = within("hatSize", hatSizeOption, 1, 0.1, 4);
2155
+ const neck = oneOf("neck", neckOption, PLUSHIE_NECKS, bowtie ? "bowtie" : "none");
2156
+ const pin = oneOf("pin", pinOption, PLUSHIE_PINS, "none");
2157
+ const headroom = within("headroom", headroomOption, 0.22, 0, 2);
2158
+ const feature = toHex(three, featureColor);
2159
+ const accent = toHex(three, accentColor);
2160
+ const preset = FABRICS[fabric] ?? FABRICS.plush;
2161
+ const spec = {
2162
+ kind,
2163
+ outline: { roundness, sides, starInner, seed },
2164
+ thickness,
2165
+ furGrain: furGrain ?? preset.grain,
2166
+ fabric: preset,
2167
+ finish,
2168
+ eyes,
2169
+ eyeSize,
2170
+ eyeSpacing,
2171
+ faceY: faceY * 0.5,
2172
+ mouth,
2173
+ cheeks,
2174
+ glasses,
2175
+ hat,
2176
+ hatSize,
2177
+ neck,
2178
+ pin,
2179
+ moustache,
2180
+ featureColor: feature,
2181
+ glassesColor: toHex(three, glassesColor) ?? feature,
2182
+ hatColor: toHex(three, hatColor),
2183
+ neckColor: toHex(three, neckColor ?? bowtieColor) ?? (neck === "bowtie" ? feature : ACCESSORY_COLORS.neck[neck] ?? accent),
2184
+ pinColor: toHex(three, pinColor) ?? ACCESSORY_COLORS.pin[pin] ?? accent,
2185
+ moustacheColor: toHex(three, moustacheColor) ?? feature,
2186
+ accent,
2187
+ cheekColor: toHex(three, cheekColor),
2188
+ headroom,
2189
+ shadow,
2190
+ renderer
2191
+ };
2192
+ const object = new three.Group;
2193
+ object.name = "Plushie";
2194
+ if (lights)
2195
+ addLights(three, object);
2196
+ const rig = buildRig(three, spec);
2197
+ object.add(rig.outer);
2198
+ const pose = { ...DEFAULT_POSE, fur: preset.fur };
2199
+ const start = finitePose(initial);
2200
+ for (const key of Object.keys(start)) {
2201
+ pose[key] = start[key];
2202
+ }
2203
+ applyPose(rig, pose, toRgb(three, pose.color));
2204
+ return {
2205
+ object,
2206
+ pose,
2207
+ set(next) {
2208
+ Object.assign(pose, finitePose(next));
2209
+ applyPose(rig, pose, toRgb(three, pose.color));
2210
+ },
2211
+ dispose() {
2212
+ const env = studioEnvironment(three, renderer);
2213
+ object.traverse((node) => {
2214
+ const mesh = node;
2215
+ mesh.geometry?.dispose();
2216
+ const materials = Array.isArray(mesh.material) ? mesh.material : mesh.material ? [mesh.material] : [];
2217
+ for (const material of materials) {
2218
+ for (const value of Object.values(material)) {
2219
+ if (value instanceof three.Texture && value !== env)
2220
+ value.dispose();
2221
+ }
2222
+ material.dispose();
2223
+ }
2224
+ });
2225
+ object.removeFromParent();
2226
+ }
2227
+ };
2228
+ }
2229
+ function applyPose(rig, p, color) {
2230
+ const k = Math.min(p.width / rig.fitW, p.height / rig.fitH) * 0.96;
2231
+ const hop = p.hop / k;
2232
+ rig.outer.scale.setScalar(k);
2233
+ rig.outer.position.y = -rig.fitMid * k;
2234
+ const sq = Math.max(-0.95, Math.min(0.95, p.squash));
2235
+ const sy = sq >= 0 ? 1 - 0.38 * sq : 1 + 0.32 * -sq;
2236
+ const sx = 1 / Math.sqrt(sy);
2237
+ const { pivot } = rig;
2238
+ pivot.position.y = rig.bottom + hop;
2239
+ pivot.scale.set(sx, sy, sx);
2240
+ pivot.rotation.set(0, p.turn * Math.PI / 180, -p.lean * Math.PI / 180);
2241
+ for (const object of rig.compensate) {
2242
+ object.scale.x = object.scale.x / (object.userData.sx ?? 1);
2243
+ object.scale.y = object.scale.y / (object.userData.sy ?? 1);
2244
+ const cx = 1 + (1 / sx - 1) * 0.5;
2245
+ const cy = 1 + (1 / sy - 1) * 0.5;
2246
+ object.userData.sx = cx;
2247
+ object.userData.sy = cy;
2248
+ object.scale.x *= cx;
2249
+ object.scale.y *= cy;
2250
+ }
2251
+ const float = p.float;
2252
+ for (const f of rig.floaters) {
2253
+ f.object.position.z = f.rest + (float - 1) * 0.05 * f.lift;
2254
+ }
2255
+ if (rig.hat) {
2256
+ const lift = (float - 1) * 0.07 + Math.max(0, -sq) * 0.06;
2257
+ rig.hat.position.y = rig.hatRest[1] + lift;
2258
+ rig.hat.scale.x = rig.hat.scale.z / sx * (1 + (sx - 1) * 0.3);
2259
+ rig.hat.scale.y = rig.hat.scale.z / sy * (1 + (sy - 1) * 0.3);
2260
+ }
2261
+ const lx = Math.max(-1.5, Math.min(1.5, p.lookX));
2262
+ const ly = Math.max(-1.5, Math.min(1.5, p.lookY));
2263
+ rig.face.rotation.set(ly * 0.16, lx * 0.24, 0);
2264
+ const blink = Math.max(0, Math.min(1, p.blink));
2265
+ for (const eye of rig.eyes) {
2266
+ eye.lid.scale.y = 1 - 0.93 * blink;
2267
+ if (eye.pupil) {
2268
+ const pupilBase = eye.pupil.userData.base ?? [eye.pupil.position.x, eye.pupil.position.y];
2269
+ eye.pupil.userData.base = pupilBase;
2270
+ eye.pupil.position.x = pupilBase[0] + lx * eye.range;
2271
+ eye.pupil.position.y = pupilBase[1] - ly * eye.range;
2272
+ }
2273
+ }
2274
+ const fur = Math.max(0, p.fur);
2275
+ rig.bias.value = (fur + 0.1) * k / 100;
2276
+ rig.bodyFur.uniforms.uFur.value = fur;
2277
+ const furPx = fur * k * p.pixelRatio;
2278
+ const layers = Math.max(3, Math.min(FUR_SHELLS, Math.ceil(furPx / 0.8)));
2279
+ rig.bodyFur.uniforms.uLayers.value = layers;
2280
+ rig.bodyGeometry.setDrawRange(0, rig.bodyGeometry.userData.indexPerLayer * (layers + 1));
2281
+ setFurColor(rig.bodyFur, color);
2282
+ if (rig.shadow) {
2283
+ const air = Math.max(0, hop);
2284
+ const spread = 1 / (1 + air * 1.6);
2285
+ rig.shadow.scale.set(rig.fitW * 0.78 * sx * spread, 0.2 * spread, 1);
2286
+ rig.shadow.position.set(0, rig.bottom + 0.005, -0.25);
2287
+ rig.shadow.material.opacity = 0.42 * spread;
2288
+ }
2289
+ }