let-them-talk 3.5.0 → 3.6.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/office/face.js ADDED
@@ -0,0 +1,258 @@
1
+ import * as THREE from 'three';
2
+
3
+ export function buildFaceSprite(eyeStyle, mouthStyle, sleeping) {
4
+ var size = 256;
5
+ var canvas = document.createElement('canvas');
6
+ canvas.width = size; canvas.height = size;
7
+ var ctx = canvas.getContext('2d');
8
+ var cx = size / 2, cy = size / 2;
9
+ ctx.clearRect(0, 0, size, size);
10
+
11
+ var eyeY = cy - 12;
12
+ var eyeSpacing = 28;
13
+
14
+ if (sleeping) {
15
+ ctx.strokeStyle = '#2a2a3e';
16
+ ctx.lineWidth = 3;
17
+ ctx.lineCap = 'round';
18
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing, eyeY, 10, 0.1 * Math.PI, 0.9 * Math.PI); ctx.stroke();
19
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing, eyeY, 10, 0.1 * Math.PI, 0.9 * Math.PI); ctx.stroke();
20
+ ctx.strokeStyle = '#c0846b';
21
+ ctx.lineWidth = 2;
22
+ ctx.beginPath(); ctx.arc(cx, cy + 28, 4, 0, Math.PI * 2); ctx.stroke();
23
+ } else {
24
+ // Eyebrows (styles that draw their own: surprised, angry, happy, confident)
25
+ var customBrows = { surprised: 1, angry: 1, happy: 1, confident: 1 };
26
+ if (!customBrows[eyeStyle]) {
27
+ ctx.strokeStyle = '#4a4a5e';
28
+ ctx.lineWidth = 2.5;
29
+ ctx.lineCap = 'round';
30
+ ctx.beginPath(); ctx.moveTo(cx - eyeSpacing - 8, eyeY - 16); ctx.quadraticCurveTo(cx - eyeSpacing, eyeY - 20, cx - eyeSpacing + 8, eyeY - 16); ctx.stroke();
31
+ ctx.beginPath(); ctx.moveTo(cx + eyeSpacing - 8, eyeY - 16); ctx.quadraticCurveTo(cx + eyeSpacing, eyeY - 20, cx + eyeSpacing + 8, eyeY - 16); ctx.stroke();
32
+ }
33
+
34
+ // Eyes
35
+ switch (eyeStyle) {
36
+ case 'dots':
37
+ ctx.fillStyle = '#ffffff';
38
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing, eyeY, 9, 0, Math.PI * 2); ctx.fill();
39
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing, eyeY, 9, 0, Math.PI * 2); ctx.fill();
40
+ ctx.fillStyle = '#1a1a2e';
41
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing, eyeY + 1, 6, 0, Math.PI * 2); ctx.fill();
42
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing, eyeY + 1, 6, 0, Math.PI * 2); ctx.fill();
43
+ ctx.fillStyle = '#ffffff';
44
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing + 2, eyeY - 2, 2.5, 0, Math.PI * 2); ctx.fill();
45
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing + 2, eyeY - 2, 2.5, 0, Math.PI * 2); ctx.fill();
46
+ break;
47
+ case 'anime':
48
+ ctx.fillStyle = '#ffffff';
49
+ ctx.beginPath(); ctx.ellipse(cx - eyeSpacing, eyeY, 11, 13, 0, 0, Math.PI * 2); ctx.fill();
50
+ ctx.beginPath(); ctx.ellipse(cx + eyeSpacing, eyeY, 11, 13, 0, 0, Math.PI * 2); ctx.fill();
51
+ ctx.fillStyle = '#3b82f6';
52
+ ctx.beginPath(); ctx.ellipse(cx - eyeSpacing, eyeY + 1, 8, 10, 0, 0, Math.PI * 2); ctx.fill();
53
+ ctx.beginPath(); ctx.ellipse(cx + eyeSpacing, eyeY + 1, 8, 10, 0, 0, Math.PI * 2); ctx.fill();
54
+ ctx.fillStyle = '#1a1a2e';
55
+ ctx.beginPath(); ctx.ellipse(cx - eyeSpacing, eyeY + 2, 5, 7, 0, 0, Math.PI * 2); ctx.fill();
56
+ ctx.beginPath(); ctx.ellipse(cx + eyeSpacing, eyeY + 2, 5, 7, 0, 0, Math.PI * 2); ctx.fill();
57
+ ctx.fillStyle = '#ffffff';
58
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing + 3, eyeY - 4, 3.5, 0, Math.PI * 2); ctx.fill();
59
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing + 3, eyeY - 4, 3.5, 0, Math.PI * 2); ctx.fill();
60
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing - 2, eyeY + 4, 2, 0, Math.PI * 2); ctx.fill();
61
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing - 2, eyeY + 4, 2, 0, Math.PI * 2); ctx.fill();
62
+ break;
63
+ case 'glasses':
64
+ ctx.fillStyle = '#ffffff';
65
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing, eyeY, 8, 0, Math.PI * 2); ctx.fill();
66
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing, eyeY, 8, 0, Math.PI * 2); ctx.fill();
67
+ ctx.fillStyle = '#1a1a2e';
68
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing, eyeY + 1, 5, 0, Math.PI * 2); ctx.fill();
69
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing, eyeY + 1, 5, 0, Math.PI * 2); ctx.fill();
70
+ ctx.fillStyle = '#ffffff';
71
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing + 2, eyeY - 2, 2, 0, Math.PI * 2); ctx.fill();
72
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing + 2, eyeY - 2, 2, 0, Math.PI * 2); ctx.fill();
73
+ ctx.strokeStyle = '#555';
74
+ ctx.lineWidth = 2.5;
75
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing, eyeY, 14, 0, Math.PI * 2); ctx.stroke();
76
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing, eyeY, 14, 0, Math.PI * 2); ctx.stroke();
77
+ ctx.beginPath(); ctx.moveTo(cx - eyeSpacing + 14, eyeY); ctx.lineTo(cx + eyeSpacing - 14, eyeY); ctx.stroke();
78
+ ctx.lineWidth = 2;
79
+ ctx.beginPath(); ctx.moveTo(cx - eyeSpacing - 14, eyeY); ctx.lineTo(cx - eyeSpacing - 20, eyeY - 2); ctx.stroke();
80
+ ctx.beginPath(); ctx.moveTo(cx + eyeSpacing + 14, eyeY); ctx.lineTo(cx + eyeSpacing + 20, eyeY - 2); ctx.stroke();
81
+ break;
82
+ case 'sleepy':
83
+ ctx.fillStyle = '#ffffff';
84
+ ctx.beginPath(); ctx.ellipse(cx - eyeSpacing, eyeY + 2, 9, 5, 0, 0, Math.PI * 2); ctx.fill();
85
+ ctx.beginPath(); ctx.ellipse(cx + eyeSpacing, eyeY + 2, 9, 5, 0, 0, Math.PI * 2); ctx.fill();
86
+ ctx.fillStyle = '#1a1a2e';
87
+ ctx.beginPath(); ctx.ellipse(cx - eyeSpacing, eyeY + 3, 5, 4, 0, 0, Math.PI * 2); ctx.fill();
88
+ ctx.beginPath(); ctx.ellipse(cx + eyeSpacing, eyeY + 3, 5, 4, 0, 0, Math.PI * 2); ctx.fill();
89
+ ctx.fillStyle = 'rgba(0,0,0,0)';
90
+ ctx.strokeStyle = '#4a4a5e';
91
+ ctx.lineWidth = 3;
92
+ ctx.beginPath(); ctx.moveTo(cx - eyeSpacing - 10, eyeY - 2); ctx.lineTo(cx - eyeSpacing + 10, eyeY); ctx.stroke();
93
+ ctx.beginPath(); ctx.moveTo(cx + eyeSpacing - 10, eyeY); ctx.lineTo(cx + eyeSpacing + 10, eyeY - 2); ctx.stroke();
94
+ break;
95
+ case 'surprised':
96
+ // Wide open round eyes with tiny pupils
97
+ ctx.fillStyle = '#ffffff';
98
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing, eyeY, 12, 0, Math.PI * 2); ctx.fill();
99
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing, eyeY, 12, 0, Math.PI * 2); ctx.fill();
100
+ ctx.fillStyle = '#1a1a2e';
101
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing, eyeY, 4, 0, Math.PI * 2); ctx.fill();
102
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing, eyeY, 4, 0, Math.PI * 2); ctx.fill();
103
+ ctx.fillStyle = '#ffffff';
104
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing + 2, eyeY - 3, 2, 0, Math.PI * 2); ctx.fill();
105
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing + 2, eyeY - 3, 2, 0, Math.PI * 2); ctx.fill();
106
+ // Raised eyebrows (override default)
107
+ ctx.strokeStyle = '#4a4a5e'; ctx.lineWidth = 2.5; ctx.lineCap = 'round';
108
+ ctx.beginPath(); ctx.moveTo(cx - eyeSpacing - 10, eyeY - 22); ctx.quadraticCurveTo(cx - eyeSpacing, eyeY - 28, cx - eyeSpacing + 10, eyeY - 22); ctx.stroke();
109
+ ctx.beginPath(); ctx.moveTo(cx + eyeSpacing - 10, eyeY - 22); ctx.quadraticCurveTo(cx + eyeSpacing, eyeY - 28, cx + eyeSpacing + 10, eyeY - 22); ctx.stroke();
110
+ break;
111
+ case 'angry':
112
+ // Angry slanted eyes with V-shaped eyebrows
113
+ ctx.fillStyle = '#ffffff';
114
+ ctx.beginPath(); ctx.ellipse(cx - eyeSpacing, eyeY, 10, 8, 0, 0, Math.PI * 2); ctx.fill();
115
+ ctx.beginPath(); ctx.ellipse(cx + eyeSpacing, eyeY, 10, 8, 0, 0, Math.PI * 2); ctx.fill();
116
+ ctx.fillStyle = '#1a1a2e';
117
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing, eyeY + 1, 5, 0, Math.PI * 2); ctx.fill();
118
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing, eyeY + 1, 5, 0, Math.PI * 2); ctx.fill();
119
+ ctx.fillStyle = '#ffffff';
120
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing + 2, eyeY - 2, 2, 0, Math.PI * 2); ctx.fill();
121
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing + 2, eyeY - 2, 2, 0, Math.PI * 2); ctx.fill();
122
+ // Angry V eyebrows
123
+ ctx.strokeStyle = '#3a3a4e'; ctx.lineWidth = 3; ctx.lineCap = 'round';
124
+ ctx.beginPath(); ctx.moveTo(cx - eyeSpacing - 10, eyeY - 12); ctx.lineTo(cx - eyeSpacing + 6, eyeY - 20); ctx.stroke();
125
+ ctx.beginPath(); ctx.moveTo(cx + eyeSpacing + 10, eyeY - 12); ctx.lineTo(cx + eyeSpacing - 6, eyeY - 20); ctx.stroke();
126
+ break;
127
+ case 'happy':
128
+ // Closed happy eyes (upside down U shapes) with sparkle
129
+ ctx.strokeStyle = '#1a1a2e'; ctx.lineWidth = 3; ctx.lineCap = 'round';
130
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing, eyeY + 3, 8, Math.PI, 2 * Math.PI); ctx.stroke();
131
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing, eyeY + 3, 8, Math.PI, 2 * Math.PI); ctx.stroke();
132
+ // Sparkle marks
133
+ ctx.strokeStyle = '#ffcc00'; ctx.lineWidth = 2;
134
+ ctx.beginPath(); ctx.moveTo(cx - eyeSpacing + 14, eyeY - 8); ctx.lineTo(cx - eyeSpacing + 18, eyeY - 12); ctx.stroke();
135
+ ctx.beginPath(); ctx.moveTo(cx - eyeSpacing + 16, eyeY - 6); ctx.lineTo(cx - eyeSpacing + 16, eyeY - 14); ctx.stroke();
136
+ break;
137
+ case 'wink':
138
+ // Left eye normal, right eye winking (horizontal line)
139
+ ctx.fillStyle = '#ffffff';
140
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing, eyeY, 9, 0, Math.PI * 2); ctx.fill();
141
+ ctx.fillStyle = '#1a1a2e';
142
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing, eyeY + 1, 6, 0, Math.PI * 2); ctx.fill();
143
+ ctx.fillStyle = '#ffffff';
144
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing + 2, eyeY - 2, 2.5, 0, Math.PI * 2); ctx.fill();
145
+ // Right eye: wink arc
146
+ ctx.strokeStyle = '#1a1a2e'; ctx.lineWidth = 3; ctx.lineCap = 'round';
147
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing, eyeY + 2, 8, 0.1 * Math.PI, 0.9 * Math.PI); ctx.stroke();
148
+ break;
149
+ case 'confident':
150
+ // Slightly narrowed determined eyes
151
+ ctx.fillStyle = '#ffffff';
152
+ ctx.beginPath(); ctx.ellipse(cx - eyeSpacing, eyeY + 1, 10, 7, 0, 0, Math.PI * 2); ctx.fill();
153
+ ctx.beginPath(); ctx.ellipse(cx + eyeSpacing, eyeY + 1, 10, 7, 0, 0, Math.PI * 2); ctx.fill();
154
+ ctx.fillStyle = '#1a1a2e';
155
+ ctx.beginPath(); ctx.ellipse(cx - eyeSpacing, eyeY + 2, 6, 5, 0, 0, Math.PI * 2); ctx.fill();
156
+ ctx.beginPath(); ctx.ellipse(cx + eyeSpacing, eyeY + 2, 6, 5, 0, 0, Math.PI * 2); ctx.fill();
157
+ ctx.fillStyle = '#ffffff';
158
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing + 2, eyeY - 1, 2.5, 0, Math.PI * 2); ctx.fill();
159
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing + 2, eyeY - 1, 2.5, 0, Math.PI * 2); ctx.fill();
160
+ // Flat confident eyebrows
161
+ ctx.strokeStyle = '#3a3a4e'; ctx.lineWidth = 3; ctx.lineCap = 'round';
162
+ ctx.beginPath(); ctx.moveTo(cx - eyeSpacing - 9, eyeY - 16); ctx.lineTo(cx - eyeSpacing + 9, eyeY - 17); ctx.stroke();
163
+ ctx.beginPath(); ctx.moveTo(cx + eyeSpacing - 9, eyeY - 17); ctx.lineTo(cx + eyeSpacing + 9, eyeY - 16); ctx.stroke();
164
+ break;
165
+ case 'tired':
166
+ // Droopy half-lidded eyes with bags
167
+ ctx.fillStyle = '#ffffff';
168
+ ctx.beginPath(); ctx.ellipse(cx - eyeSpacing, eyeY + 3, 9, 5, 0, 0, Math.PI * 2); ctx.fill();
169
+ ctx.beginPath(); ctx.ellipse(cx + eyeSpacing, eyeY + 3, 9, 5, 0, 0, Math.PI * 2); ctx.fill();
170
+ ctx.fillStyle = '#1a1a2e';
171
+ ctx.beginPath(); ctx.ellipse(cx - eyeSpacing, eyeY + 4, 5, 3, 0, 0, Math.PI * 2); ctx.fill();
172
+ ctx.beginPath(); ctx.ellipse(cx + eyeSpacing, eyeY + 4, 5, 3, 0, 0, Math.PI * 2); ctx.fill();
173
+ // Heavy eyelids
174
+ ctx.fillStyle = 'rgba(200, 170, 150, 0.5)';
175
+ ctx.beginPath(); ctx.ellipse(cx - eyeSpacing, eyeY - 1, 10, 6, 0, Math.PI, 2 * Math.PI); ctx.fill();
176
+ ctx.beginPath(); ctx.ellipse(cx + eyeSpacing, eyeY - 1, 10, 6, 0, Math.PI, 2 * Math.PI); ctx.fill();
177
+ // Under-eye bags
178
+ ctx.strokeStyle = 'rgba(150, 120, 120, 0.3)'; ctx.lineWidth = 1.5;
179
+ ctx.beginPath(); ctx.arc(cx - eyeSpacing, eyeY + 12, 8, 0, Math.PI); ctx.stroke();
180
+ ctx.beginPath(); ctx.arc(cx + eyeSpacing, eyeY + 12, 8, 0, Math.PI); ctx.stroke();
181
+ break;
182
+ }
183
+
184
+ // Nose
185
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.08)';
186
+ ctx.beginPath(); ctx.ellipse(cx, cy + 10, 4, 3, 0, 0, Math.PI * 2); ctx.fill();
187
+
188
+ // Blush
189
+ ctx.fillStyle = 'rgba(255, 130, 130, 0.15)';
190
+ ctx.beginPath(); ctx.ellipse(cx - eyeSpacing - 4, eyeY + 16, 10, 6, 0, 0, Math.PI * 2); ctx.fill();
191
+ ctx.beginPath(); ctx.ellipse(cx + eyeSpacing + 4, eyeY + 16, 10, 6, 0, 0, Math.PI * 2); ctx.fill();
192
+
193
+ // Mouth
194
+ switch (mouthStyle) {
195
+ case 'smile':
196
+ ctx.strokeStyle = '#c0846b';
197
+ ctx.lineWidth = 2.5;
198
+ ctx.lineCap = 'round';
199
+ ctx.beginPath(); ctx.arc(cx, cy + 24, 8, 0.15 * Math.PI, 0.85 * Math.PI); ctx.stroke();
200
+ break;
201
+ case 'neutral':
202
+ ctx.strokeStyle = '#c0846b';
203
+ ctx.lineWidth = 2.5;
204
+ ctx.lineCap = 'round';
205
+ ctx.beginPath(); ctx.moveTo(cx - 6, cy + 28); ctx.lineTo(cx + 6, cy + 28); ctx.stroke();
206
+ break;
207
+ case 'open':
208
+ ctx.fillStyle = '#8b4c3a';
209
+ ctx.beginPath(); ctx.ellipse(cx, cy + 26, 6, 5, 0, 0, Math.PI * 2); ctx.fill();
210
+ ctx.fillStyle = '#d4736a';
211
+ ctx.beginPath(); ctx.ellipse(cx, cy + 29, 4, 3, 0, 0, Math.PI); ctx.fill();
212
+ break;
213
+ case 'grin':
214
+ // Wide grin showing teeth
215
+ ctx.fillStyle = '#8b4c3a';
216
+ ctx.beginPath(); ctx.ellipse(cx, cy + 26, 10, 6, 0, 0, Math.PI * 2); ctx.fill();
217
+ ctx.fillStyle = '#ffffff';
218
+ ctx.beginPath(); ctx.ellipse(cx, cy + 25, 8, 4, 0, 0, Math.PI); ctx.fill();
219
+ ctx.strokeStyle = '#c0846b'; ctx.lineWidth = 1.5; ctx.lineCap = 'round';
220
+ ctx.beginPath(); ctx.arc(cx, cy + 24, 10, 0.1 * Math.PI, 0.9 * Math.PI); ctx.stroke();
221
+ break;
222
+ case 'frown':
223
+ // Downturned mouth
224
+ ctx.strokeStyle = '#c0846b'; ctx.lineWidth = 2.5; ctx.lineCap = 'round';
225
+ ctx.beginPath(); ctx.arc(cx, cy + 34, 8, 1.15 * Math.PI, 1.85 * Math.PI); ctx.stroke();
226
+ break;
227
+ case 'smirk':
228
+ // One-sided smirk
229
+ ctx.strokeStyle = '#c0846b'; ctx.lineWidth = 2.5; ctx.lineCap = 'round';
230
+ ctx.beginPath(); ctx.moveTo(cx - 6, cy + 28); ctx.quadraticCurveTo(cx + 2, cy + 28, cx + 8, cy + 24); ctx.stroke();
231
+ break;
232
+ case 'tongue':
233
+ // Playful tongue sticking out
234
+ ctx.strokeStyle = '#c0846b'; ctx.lineWidth = 2.5; ctx.lineCap = 'round';
235
+ ctx.beginPath(); ctx.arc(cx, cy + 24, 8, 0.15 * Math.PI, 0.85 * Math.PI); ctx.stroke();
236
+ ctx.fillStyle = '#e8837c';
237
+ ctx.beginPath(); ctx.ellipse(cx, cy + 33, 4, 5, 0, 0, Math.PI * 2); ctx.fill();
238
+ ctx.fillStyle = '#d4736a';
239
+ ctx.beginPath(); ctx.ellipse(cx, cy + 34, 3, 3, 0, 0, Math.PI); ctx.fill();
240
+ break;
241
+ case 'whistle':
242
+ // Small O-shaped mouth
243
+ ctx.fillStyle = '#8b4c3a';
244
+ ctx.beginPath(); ctx.arc(cx, cy + 28, 4, 0, Math.PI * 2); ctx.fill();
245
+ ctx.fillStyle = '#6b3c2a';
246
+ ctx.beginPath(); ctx.arc(cx, cy + 28, 2.5, 0, Math.PI * 2); ctx.fill();
247
+ break;
248
+ }
249
+ }
250
+
251
+ var tex = new THREE.CanvasTexture(canvas);
252
+ tex.needsUpdate = true;
253
+ var faceMat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, depthWrite: false });
254
+ var faceMesh = new THREE.Mesh(new THREE.PlaneGeometry(0.38, 0.38), faceMat);
255
+ faceMesh.userData.canvas = canvas;
256
+ faceMesh.userData.texture = tex;
257
+ return faceMesh;
258
+ }
package/office/hair.js ADDED
@@ -0,0 +1,183 @@
1
+ import * as THREE from 'three';
2
+
3
+ export function buildHair(style, colorHex) {
4
+ var group = new THREE.Group();
5
+ var mat = new THREE.MeshStandardMaterial({ color: colorHex, roughness: 0.8 });
6
+ switch (style) {
7
+ case 'short': {
8
+ var geo = new THREE.SphereGeometry(0.26, 16, 12, 0, Math.PI * 2, 0, Math.PI / 2);
9
+ var hair = new THREE.Mesh(geo, mat);
10
+ hair.position.y = 0.02; hair.castShadow = true;
11
+ group.add(hair);
12
+ break;
13
+ }
14
+ case 'spiky': {
15
+ for (var i = 0; i < 6; i++) {
16
+ var angle = (i / 6) * Math.PI * 2;
17
+ var spike = new THREE.Mesh(new THREE.ConeGeometry(0.06, 0.2, 6), mat);
18
+ spike.position.set(Math.cos(angle) * 0.18, 0.2, Math.sin(angle) * 0.18);
19
+ spike.rotation.x = Math.sin(angle) * 0.4;
20
+ spike.rotation.z = -Math.cos(angle) * 0.4;
21
+ spike.castShadow = true;
22
+ group.add(spike);
23
+ }
24
+ var topSpike = new THREE.Mesh(new THREE.ConeGeometry(0.07, 0.25, 6), mat);
25
+ topSpike.position.y = 0.3; topSpike.castShadow = true;
26
+ group.add(topSpike);
27
+ break;
28
+ }
29
+ case 'long': {
30
+ var capGeo = new THREE.SphereGeometry(0.27, 16, 12, 0, Math.PI * 2, 0, Math.PI / 2);
31
+ var cap = new THREE.Mesh(capGeo, mat);
32
+ cap.position.y = 0.02; cap.castShadow = true;
33
+ group.add(cap);
34
+ [-0.22, 0.22].forEach(function(x) {
35
+ var panel = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.35, 0.12), mat);
36
+ panel.position.set(x, -0.1, 0);
37
+ panel.castShadow = true;
38
+ group.add(panel);
39
+ });
40
+ break;
41
+ }
42
+ case 'ponytail': {
43
+ var capGeo2 = new THREE.SphereGeometry(0.26, 16, 12, 0, Math.PI * 2, 0, Math.PI / 2);
44
+ var cap2 = new THREE.Mesh(capGeo2, mat);
45
+ cap2.position.y = 0.02; cap2.castShadow = true;
46
+ group.add(cap2);
47
+ var ptGeo = new THREE.CapsuleGeometry(0.06, 0.2, 4, 8);
48
+ var pt = new THREE.Mesh(ptGeo, mat);
49
+ pt.position.set(0, 0.05, -0.25);
50
+ pt.rotation.x = 0.4; pt.castShadow = true;
51
+ group.add(pt);
52
+ break;
53
+ }
54
+ case 'bob': {
55
+ var capGeo3 = new THREE.SphereGeometry(0.28, 16, 12);
56
+ var cap3 = new THREE.Mesh(capGeo3, mat);
57
+ cap3.position.y = 0.02;
58
+ cap3.scale.set(1, 0.7, 1);
59
+ cap3.castShadow = true;
60
+ group.add(cap3);
61
+ break;
62
+ }
63
+ case 'curly': {
64
+ // Clustered spheres for curly volume
65
+ var curlPositions = [
66
+ [0, 0.18, 0.08], [0.15, 0.15, 0.05], [-0.15, 0.15, 0.05],
67
+ [0.1, 0.22, 0], [-0.1, 0.22, 0], [0, 0.26, -0.02],
68
+ [0.18, 0.08, -0.05], [-0.18, 0.08, -0.05],
69
+ [0.12, 0.05, -0.15], [-0.12, 0.05, -0.15], [0, 0.1, -0.18],
70
+ [0.08, 0.2, -0.1], [-0.08, 0.2, -0.1],
71
+ ];
72
+ curlPositions.forEach(function(p) {
73
+ var curl = new THREE.Mesh(new THREE.SphereGeometry(0.07 + Math.random() * 0.03, 8, 6), mat);
74
+ curl.position.set(p[0], p[1], p[2]);
75
+ curl.castShadow = true;
76
+ group.add(curl);
77
+ });
78
+ break;
79
+ }
80
+ case 'afro': {
81
+ // Large round afro — big sphere with smaller detail spheres
82
+ var afroMain = new THREE.Mesh(new THREE.SphereGeometry(0.36, 16, 14), mat);
83
+ afroMain.position.y = 0.1;
84
+ afroMain.scale.set(1, 0.85, 1);
85
+ afroMain.castShadow = true;
86
+ group.add(afroMain);
87
+ // Texture bumps around the perimeter
88
+ for (var ai = 0; ai < 10; ai++) {
89
+ var aa = (ai / 10) * Math.PI * 2;
90
+ var bump = new THREE.Mesh(new THREE.SphereGeometry(0.06, 6, 5), mat);
91
+ bump.position.set(Math.cos(aa) * 0.32, 0.1 + Math.sin(aa * 0.5) * 0.08, Math.sin(aa) * 0.32);
92
+ bump.castShadow = true;
93
+ group.add(bump);
94
+ }
95
+ break;
96
+ }
97
+ case 'bun': {
98
+ // Smooth cap + bun on top
99
+ var bunCap = new THREE.Mesh(new THREE.SphereGeometry(0.26, 16, 12, 0, Math.PI * 2, 0, Math.PI / 2), mat);
100
+ bunCap.position.y = 0.02; bunCap.castShadow = true;
101
+ group.add(bunCap);
102
+ var bunBall = new THREE.Mesh(new THREE.SphereGeometry(0.1, 12, 10), mat);
103
+ bunBall.position.set(0, 0.28, -0.06);
104
+ bunBall.castShadow = true;
105
+ group.add(bunBall);
106
+ // Hair band around bun
107
+ var bandMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.6 });
108
+ var band = new THREE.Mesh(new THREE.TorusGeometry(0.1, 0.012, 6, 16), bandMat);
109
+ band.position.set(0, 0.28, -0.06);
110
+ band.rotation.x = Math.PI / 6;
111
+ group.add(band);
112
+ break;
113
+ }
114
+ case 'braids': {
115
+ // Cap + two braided strands (capsule chains) hanging down sides
116
+ var braidCap = new THREE.Mesh(new THREE.SphereGeometry(0.26, 16, 12, 0, Math.PI * 2, 0, Math.PI / 2), mat);
117
+ braidCap.position.y = 0.02; braidCap.castShadow = true;
118
+ group.add(braidCap);
119
+ [-0.2, 0.2].forEach(function(sx) {
120
+ for (var bi = 0; bi < 5; bi++) {
121
+ var seg = new THREE.Mesh(new THREE.SphereGeometry(0.04, 6, 5), mat);
122
+ var zigzag = (bi % 2 === 0 ? 0.02 : -0.02);
123
+ seg.position.set(sx + zigzag, -0.05 - bi * 0.07, 0);
124
+ seg.castShadow = true;
125
+ group.add(seg);
126
+ }
127
+ // Braid tie at bottom
128
+ var tieMat = new THREE.MeshStandardMaterial({ color: 0xf97316, roughness: 0.5 });
129
+ var tie = new THREE.Mesh(new THREE.SphereGeometry(0.025, 6, 5), tieMat);
130
+ tie.position.set(sx, -0.05 - 5 * 0.07, 0);
131
+ group.add(tie);
132
+ });
133
+ break;
134
+ }
135
+ case 'mohawk': {
136
+ // Central ridge of fin-like shapes along the top
137
+ for (var mi = 0; mi < 7; mi++) {
138
+ var mz = 0.15 - mi * 0.05;
139
+ var mh = 0.14 + Math.sin(mi / 6 * Math.PI) * 0.1;
140
+ var fin = new THREE.Mesh(new THREE.BoxGeometry(0.04, mh, 0.04), mat);
141
+ fin.position.set(0, 0.2 + mh / 2, mz);
142
+ fin.castShadow = true;
143
+ group.add(fin);
144
+ }
145
+ // Shaved sides (darker material, thin caps)
146
+ var sideMat = new THREE.MeshStandardMaterial({ color: colorHex, roughness: 0.9 });
147
+ sideMat.color.multiplyScalar(0.4);
148
+ [-1, 1].forEach(function(side) {
149
+ var sideHair = new THREE.Mesh(new THREE.SphereGeometry(0.255, 12, 8, 0, Math.PI, 0, Math.PI / 2.5), sideMat);
150
+ sideHair.position.y = 0.01;
151
+ sideHair.rotation.y = side > 0 ? 0 : Math.PI;
152
+ sideHair.castShadow = true;
153
+ group.add(sideHair);
154
+ });
155
+ break;
156
+ }
157
+ case 'wavy': {
158
+ // Flowing wavy hair — cap + wavy side panels using sine-displaced boxes
159
+ var wavyCap = new THREE.Mesh(new THREE.SphereGeometry(0.27, 16, 12, 0, Math.PI * 2, 0, Math.PI / 2), mat);
160
+ wavyCap.position.y = 0.02; wavyCap.castShadow = true;
161
+ group.add(wavyCap);
162
+ [-0.2, 0.2].forEach(function(wx) {
163
+ for (var wi = 0; wi < 6; wi++) {
164
+ var waveX = wx + Math.sin(wi * 1.2) * 0.04;
165
+ var seg2 = new THREE.Mesh(new THREE.BoxGeometry(0.07, 0.05, 0.1), mat);
166
+ seg2.position.set(waveX, -0.02 - wi * 0.055, -0.02);
167
+ seg2.rotation.z = Math.sin(wi * 1.2) * 0.15;
168
+ seg2.castShadow = true;
169
+ group.add(seg2);
170
+ }
171
+ });
172
+ // Back flow
173
+ for (var bwi = 0; bwi < 4; bwi++) {
174
+ var backSeg = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.05, 0.06), mat);
175
+ backSeg.position.set(0, -0.02 - bwi * 0.06, -0.22 - bwi * 0.02);
176
+ backSeg.castShadow = true;
177
+ group.add(backSeg);
178
+ }
179
+ break;
180
+ }
181
+ }
182
+ return group;
183
+ }