@pieai/swimmer-avatar-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +23 -0
  3. package/README.md +125 -0
  4. package/UPSTREAM.md +47 -0
  5. package/dist/avatar.d.ts +2 -0
  6. package/dist/avatar.js +69 -0
  7. package/dist/avatar.js.map +1 -0
  8. package/dist/index.d.ts +3 -0
  9. package/dist/index.js +3 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/materials.d.ts +20 -0
  12. package/dist/materials.js +61 -0
  13. package/dist/materials.js.map +1 -0
  14. package/dist/react-three-fiber.d.ts +16 -0
  15. package/dist/react-three-fiber.js +52 -0
  16. package/dist/react-three-fiber.js.map +1 -0
  17. package/dist/recipe.d.ts +18 -0
  18. package/dist/recipe.js +118 -0
  19. package/dist/recipe.js.map +1 -0
  20. package/dist/types.d.ts +62 -0
  21. package/dist/types.js +2 -0
  22. package/dist/types.js.map +1 -0
  23. package/package.json +102 -0
  24. package/upstream-lock.json +38 -0
  25. package/vendor/kindergrimm/LICENSE +24 -0
  26. package/vendor/kindergrimm/src/gloss/catmullClark.js +129 -0
  27. package/vendor/kindergrimm/src/gloss/gface.d.ts +25 -0
  28. package/vendor/kindergrimm/src/gloss/gface.js +226 -0
  29. package/vendor/kindergrimm/src/gloss/gform.js +201 -0
  30. package/vendor/kindergrimm/src/gloss/ghair.js +534 -0
  31. package/vendor/kindergrimm/src/gloss/glayout.js +365 -0
  32. package/vendor/kindergrimm/src/gloss/gmedia.d.ts +28 -0
  33. package/vendor/kindergrimm/src/gloss/gmedia.js +470 -0
  34. package/vendor/kindergrimm/src/gloss/gpalette.d.ts +10 -0
  35. package/vendor/kindergrimm/src/gloss/gpalette.js +205 -0
  36. package/vendor/kindergrimm/src/gloss/gparts/blush.js +57 -0
  37. package/vendor/kindergrimm/src/gloss/gparts/body.js +47 -0
  38. package/vendor/kindergrimm/src/gloss/gparts/brows.js +55 -0
  39. package/vendor/kindergrimm/src/gloss/gparts/crest.js +88 -0
  40. package/vendor/kindergrimm/src/gloss/gparts/eyes.js +357 -0
  41. package/vendor/kindergrimm/src/gloss/gparts/frame.js +225 -0
  42. package/vendor/kindergrimm/src/gloss/gparts/hair.js +100 -0
  43. package/vendor/kindergrimm/src/gloss/gparts/hat.js +163 -0
  44. package/vendor/kindergrimm/src/gloss/gparts/index.js +33 -0
  45. package/vendor/kindergrimm/src/gloss/gparts/mark.js +139 -0
  46. package/vendor/kindergrimm/src/gloss/gparts/mouth.js +170 -0
  47. package/vendor/kindergrimm/src/gloss/gparts/nose.js +63 -0
  48. package/vendor/kindergrimm/src/gloss/gparts/specs.js +124 -0
  49. package/vendor/kindergrimm/src/gloss/grig.d.ts +41 -0
  50. package/vendor/kindergrimm/src/gloss/grig.js +286 -0
  51. package/vendor/kindergrimm/src/gloss/gshape.js +421 -0
  52. package/vendor/kindergrimm/src/gloss/gspecies.d.ts +10 -0
  53. package/vendor/kindergrimm/src/gloss/gspecies.js +338 -0
  54. package/vendor/kindergrimm/src/gloss/gtexture.js +406 -0
  55. package/vendor/kindergrimm/src/rng.d.ts +11 -0
  56. package/vendor/kindergrimm/src/rng.js +27 -0
@@ -0,0 +1,534 @@
1
+ // ---------------------------------------------------------------
2
+ // THE HAIR — the humanoid's, and the fourth kind of thing this lab
3
+ // builds: ONE MOLDED MASS with the clumps carved into it, plus a
4
+ // handful of STRANDS growing from points on the head.
5
+ //
6
+ // That decomposition is not invented — it is how the figures in the
7
+ // reference are actually molded. A vinyl figure's hair is a back piece
8
+ // (the mass), a front piece (the fringe), and separate accent strands:
9
+ // the momiage hanging in front of each ear, loose wisps over the
10
+ // forehead, a flyaway or two at the crown. The mass gives volume, and
11
+ // the strands are what read as STYLED — a molded mass alone always
12
+ // looks like it came out of a mold.
13
+ //
14
+ // Two dead ends are still worth keeping: a single smooth shell was a
15
+ // helmet, and separate tiled clumps gapped onto skin, caught the studio
16
+ // as glassy streaks, and had no volume. One thick closed shell, carved:
17
+ //
18
+ // volume the outer surface stands well off the head; the inner one
19
+ // hugs it, and the fold between them at the hem is the thick
20
+ // rounded rim every molded haircut has
21
+ // grooves narrow notches in the outer radius at clump boundaries
22
+ // scallop the hem drops to a point under each clump
23
+ //
24
+ // And it GROWS FROM A WHORL, not from the top pole. The first carving
25
+ // used plain azimuth, so every groove was a meridian and they all met
26
+ // at the geometric top — a pumpkin, not a haircut. Hair radiates from
27
+ // a whorl set BACK off the crown (or pulled to the front hairline for
28
+ // the upswept cuts), so every groove is a great-circle ray out of a
29
+ // placeable whorl point: the fringe's grooves run forward-and-down
30
+ // over the forehead, the back's run down the nape, the way combed hair
31
+ // lies. The strands follow the same field, which is what keeps a wisp
32
+ // and the groove under it agreeing about which way this head is combed.
33
+ //
34
+ // The hem — front/side/back, by azimuth — is still the whole haircut.
35
+ // One generator covers the sphere AND the cube: both are the same
36
+ // superellipsoid and `surfT` carries the exponent.
37
+ //
38
+ // This file never touches three.js: it hands back `{verts, faces}`.
39
+ // ---------------------------------------------------------------
40
+ import { subdivideN } from './catmullClark.js';
41
+ import { surfT } from './gshape.js';
42
+
43
+ const lerp = (a, b, t) => a + (b - a) * t;
44
+ const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
45
+ const smooth = t => t * t * (3 - 2 * t);
46
+ const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
47
+ const cross = (a, b) => [a[1] * b[2] - a[2] * b[1],
48
+ a[2] * b[0] - a[0] * b[2],
49
+ a[0] * b[1] - a[1] * b[0]];
50
+ const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
51
+ const norm = a => { const l = Math.hypot(a[0], a[1], a[2]) || 1;
52
+ return [a[0] / l, a[1] / l, a[2] / l]; };
53
+
54
+ // ---- the styles -------------------------------------------------------
55
+ // `front` / `side` / `back` are where the mass ends, in body height: +1
56
+ // the crown, 0 the middle of the head, −1 the bottom, below −1 hanging.
57
+ // THE ORDERING IS THE READ: fringe high, temples lower, nape lowest —
58
+ // level all round is a cap, level nose-to-ear cuts the head on a line.
59
+ //
60
+ // vol how fat the mass is, off the head's radius
61
+ // n how many clumps are carved into it
62
+ // jag how far the hem drops to a tip under each clump
63
+ // part runs the fringe diagonally; also swings the whorl aside
64
+ // curtain opens the fringe over the nose, forehead showing
65
+ // pomp sweeps the front UP — displacement, not a hem number
66
+ // open holds the face clear further round, for the long cuts
67
+ // whorlAz/whorlY where this cut is combed from, if not the default
68
+ // back-of-crown
69
+ const STYLE = {
70
+ bald: null,
71
+
72
+ // --- swept UP, off the forehead --------------------------------------
73
+ // upswept styles grow from the FRONT hairline — the whorl sits low
74
+ // over the brow and the flow runs up and back over the crown
75
+ quiff: { front: .54, side: .10, back: -.40, vol: .17, n: 12, jag: .10, pomp: .34,
76
+ whorlAz: 0, whorlY: .48 },
77
+ swept: { front: .46, side: -.04, back: -.50, vol: .18, n: 13, jag: .12,
78
+ pomp: .22, part: .30 },
79
+ crop: { front: .48, side: .18, back: -.30, vol: .12, n: 14, jag: .06, pomp: .10 },
80
+ spiky: { front: .40, side: .06, back: -.42, vol: .19, n: 11, jag: .26, pomp: .16,
81
+ whorlY: .95 },
82
+
83
+ // --- short, with a fringe DOWN ---------------------------------------
84
+ bowl: { front: .06, side: -.26, back: -.58, vol: .20, n: 16, jag: .05 },
85
+ pixie: { front: .24, side: -.08, back: -.52, vol: .18, n: 14, jag: .18, part: .26 },
86
+ side: { front: .12, side: -.18, back: -.56, vol: .20, n: 14, jag: .12, part: .36 },
87
+ curtain:{ front: .08, side: -.30, back: -.56, vol: .19, n: 14, jag: .10, curtain: .38 },
88
+ curly: { front: .20, side: -.20, back: -.50, vol: .27, n: 12, jag: .12, wave: .05 },
89
+
90
+ // --- MID: the media melena -------------------------------------------
91
+ bob: { front: .14, side: -.95, back: -1.15, vol: .19, n: 16, jag: .09, open: .34 },
92
+ midi: { front: .16, side: -1.35, back: -1.55, vol: .19, n: 16, jag: .10, open: .36 },
93
+ layers: { front: .22, side: -1.28, back: -1.48, vol: .22, n: 13, jag: .34, open: .36,
94
+ curtain: .24 },
95
+ wavy: { front: .12, side: -1.50, back: -1.70, vol: .21, n: 16, jag: .14,
96
+ wave: .05, open: .34 },
97
+
98
+ // --- long -------------------------------------------------------------
99
+ long: { front: .16, side: -1.95, back: -2.25, vol: .18, n: 16, jag: .10, open: .38 },
100
+ hime: { front: .02, side: -2.10, back: -2.40, vol: .18, n: 18, jag: .04, open: .14,
101
+ locks: 2.2 },
102
+
103
+ // --- tied up ----------------------------------------------------------
104
+ pony: { front: .16, side: -.06, back: -.46, vol: .17, n: 14, jag: .09,
105
+ part: .22, tails: 1 },
106
+ twin: { front: .14, side: -.10, back: -.50, vol: .17, n: 14, jag: .09, tails: 2 },
107
+ buns: { front: .14, side: -.14, back: -.50, vol: .17, n: 14, jag: .09,
108
+ curtain: .22, buns: 2 },
109
+ };
110
+
111
+ export const HAIR_STYLES = Object.keys(STYLE);
112
+ export const isLongHair = id => (STYLE[id]?.side ?? 1) < -1;
113
+
114
+ /**
115
+ * How far the hair's outer surface stands off the head, as an
116
+ * inflation factor. HATS need it: a beanie sized to a bare skull sinks
117
+ * into a big soft cut and comes out as a painted band. Same edge as
118
+ * `eyeReach` — the part publishes a fact about itself rather than the
119
+ * hat reading this style table.
120
+ */
121
+ export function hairOuter(P) {
122
+ const st = STYLE[P.hair?.style];
123
+ if (!st) return 1;
124
+ return 1 + st.vol * (P.hair.vol ?? 1) + (st.pomp ?? 0) * (P.hair.pomp ?? 1) * .35;
125
+ }
126
+
127
+ // where the face arc holds and where it lets go, in radians off the
128
+ // nose. Driven by the ANGLE, not by `cos(ang)`: a cosine starts closing
129
+ // the moment it leaves the nose and the hair swallows the temples.
130
+ const FACE_HOLD = .95, FACE_END = 1.55;
131
+ const BACK_START = 1.95, BACK_END = 2.6;
132
+ // below this the hair has left the head: it keeps the horizontal it had
133
+ // here and simply descends, which is what falling hair does.
134
+ const FALL = -.45;
135
+
136
+ const A = 72; // azimuth samples — the grooves live here
137
+ const ROWS = 8; // rows hem → crown, each surface
138
+ const SUBDIV = 1;
139
+
140
+ /** the base hem: where the mass ends at this azimuth. The haircut. */
141
+ function hemAt(st, ang, H) {
142
+ const a = Math.abs(Math.atan2(Math.sin(ang), Math.cos(ang)));
143
+ const o = st.open ?? 0;
144
+ const hold = FACE_HOLD + o, end = FACE_END + o;
145
+ const bs = Math.max(BACK_START, end + .1);
146
+ const fw = 1 - smooth(clamp((a - hold) / (end - hold), 0, 1));
147
+ const bw = smooth(clamp((a - bs) / (BACK_END - bs), 0, 1));
148
+ let v = st.side + (st.front - st.side) * fw + (st.back - st.side) * bw;
149
+ // a PARTING runs the fringe diagonally: long over one brow, high at
150
+ // the opposite temple
151
+ if (st.part) v += st.part * Math.sin(ang) * fw * H.part;
152
+ // a CURTAIN raises it in the middle and drops it at both temples, so
153
+ // the forehead shows between two falling sides
154
+ if (st.curtain) {
155
+ const c = Math.max(0, Math.cos(ang * 1.5));
156
+ v += st.curtain * H.part * c * c * fw;
157
+ }
158
+ return v;
159
+ }
160
+
161
+ /** deterministic per-strand jitter. From an index and one seed, never
162
+ * from `rng`: hair is rebuilt on every boil frame and anything rolled
163
+ * per-frame would shimmer. */
164
+ const jitter = (i, seed) => Math.sin(i * 12.9898 + seed) * .5 + .5;
165
+
166
+ /**
167
+ * (azimuth, height) → a point on or off the body. `infl` 1 is the skin.
168
+ * Below `FALL` the hair has left the head and only descends.
169
+ */
170
+ function pointer(shape) {
171
+ const { rx, ry, rz, exp } = shape;
172
+ const on = (az, y, infl) => {
173
+ const el = Math.asin(clamp(y, -1, 1)) * .999;
174
+ const c = Math.cos(el);
175
+ const d = [Math.sin(az) * c, Math.sin(el), Math.cos(az) * c];
176
+ const t = surfT(d[0], d[1], d[2], rx, ry, rz, exp) * infl;
177
+ return [d[0] * t, d[1] * t, d[2] * t];
178
+ };
179
+ return (az, y, infl) => {
180
+ if (y >= FALL) return on(az, y, infl);
181
+ const b = on(az, FALL, infl);
182
+ return [b[0], b[1] + (y - FALL) * ry, b[2]];
183
+ };
184
+ }
185
+
186
+ /**
187
+ * THE FLOW FIELD — where this head is combed from.
188
+ *
189
+ * The whorl sits at the back of the crown by default, nudged per head,
190
+ * swung round for parted styles and pulled to the front hairline for
191
+ * the upswept ones. `flow(az, y)` gives a surface point's coordinates
192
+ * in that field: `phi`, which ray out of the whorl it lies on (the
193
+ * groove coordinate), and `theta`, how far from the whorl it is.
194
+ * `ray(phi0, s)` walks back out: the point at angle `s` along one ray —
195
+ * which is exactly the path a strand of combed hair takes, so the
196
+ * wisps march along it.
197
+ */
198
+ function makeFlow(st, H) {
199
+ const wAz = (st.whorlAz ?? Math.PI) + H.whorl
200
+ - (st.part ?? 0) * H.part * 1.6;
201
+ const wY = clamp(st.whorlY ?? .74, -.9, .98);
202
+ const wEl = Math.asin(wY);
203
+ const W = [Math.sin(wAz) * Math.cos(wEl), wY, Math.cos(wAz) * Math.cos(wEl)];
204
+ // tangent frame seeded from the face direction, so phi = 0 always
205
+ // points down the front of the head
206
+ let e1 = [0 - W[0] * W[2], 0 - W[1] * W[2], 1 - W[2] * W[2]];
207
+ if (Math.hypot(e1[0], e1[1], e1[2]) < 1e-4) e1 = [1, 0, 0];
208
+ e1 = norm(e1);
209
+ const e2 = cross(W, e1);
210
+
211
+ function flow(az, y) {
212
+ const el = Math.asin(clamp(Math.max(y, FALL), -1, 1));
213
+ const c = Math.cos(el);
214
+ const d = [Math.sin(az) * c, Math.sin(el), Math.cos(az) * c];
215
+ const dw = dot(d, W);
216
+ const v = [d[0] - dw * W[0], d[1] - dw * W[1], d[2] - dw * W[2]];
217
+ const l = Math.hypot(v[0], v[1], v[2]);
218
+ if (l < 1e-5) return { phi: 0, theta: 0 };
219
+ return { phi: Math.atan2(dot(v, e2) / l, dot(v, e1) / l),
220
+ theta: Math.acos(clamp(dw, -1, 1)) };
221
+ }
222
+
223
+ function ray(phi0, s) {
224
+ const u = [e1[0] * Math.cos(phi0) + e2[0] * Math.sin(phi0),
225
+ e1[1] * Math.cos(phi0) + e2[1] * Math.sin(phi0),
226
+ e1[2] * Math.cos(phi0) + e2[2] * Math.sin(phi0)];
227
+ const d = [W[0] * Math.cos(s) + u[0] * Math.sin(s),
228
+ W[1] * Math.cos(s) + u[1] * Math.sin(s),
229
+ W[2] * Math.cos(s) + u[2] * Math.sin(s)];
230
+ return { az: Math.atan2(d[0], d[2]), y: d[1] };
231
+ }
232
+
233
+ return { flow, ray };
234
+ }
235
+
236
+ /**
237
+ * THE MASS. One closed shell with torus topology: the outer surface
238
+ * climbs hem → crown, folds over, and the inner surface comes back down
239
+ * hugging the head — no caps, no seams, no bare skin, and the fold at
240
+ * the hem is the thick rounded rim of a molded piece.
241
+ */
242
+ function hairMass(st, pt, H, shape, F) {
243
+ const vol = st.vol * H.vol;
244
+ const lift = (st.pomp ?? 0) * H.pomp;
245
+ const n = Math.max(8, Math.round(st.n * H.density));
246
+
247
+ // the clump boundaries of the FLOW, jittered so the carving never
248
+ // tiles into a repeat — the eye reads a repeat before the hair
249
+ const bounds = [];
250
+ for (let k = 0; k < n; k++)
251
+ bounds.push((k + .5 + (jitter(k, H.seed) - .5) * .5) / n * Math.PI * 2);
252
+
253
+ function clumpAt(phi) {
254
+ const a = ((phi % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2);
255
+ for (let k = 0; k < n; k++) {
256
+ const lo = bounds[k], hi = bounds[(k + 1) % n] + (k === n - 1 ? Math.PI * 2 : 0);
257
+ if (a >= lo && a < hi) return { k, p: (a - lo) / (hi - lo) };
258
+ }
259
+ return { k: 0, p: .5 };
260
+ }
261
+
262
+ // the GROOVE: a notch at each clump boundary of the flow, fading near
263
+ // the whorl (a whorl is a point, not a star of notches) and near the
264
+ // antipode, where the rays reconverge
265
+ const GW = .16;
266
+ function carve(az, y) {
267
+ const { phi, theta } = F.flow(az, y);
268
+ const { p } = clumpAt(phi);
269
+ const d = Math.min(p, 1 - p) / GW;
270
+ const notch = d < 1 ? smooth(1 - d) : 0;
271
+ const fade = smooth(clamp(theta / .5, 0, 1))
272
+ * smooth(clamp((Math.PI - theta) / .5, 0, 1));
273
+ // this scales the hair's THICKNESS, never the total radius. As a
274
+ // factor on the whole inflation a deep notch dived inside the head
275
+ // and opened a black gap in the fringe — a groove is carved into
276
+ // the hair, and it must bottom out at the hair's own floor.
277
+ return 1 - .52 * notch * fade;
278
+ }
279
+
280
+ // the SCALLOP: the tip pattern follows the flow too, so the scallop
281
+ // and the grooves above it always agree about the combing
282
+ function hemCol(az) {
283
+ const base = hemAt(st, az, H);
284
+ const { k, p } = clumpAt(F.flow(az, base).phi);
285
+ const deep = (st.jag ?? .1) * H.jag * (.45 + .55 * jitter(k + 31, H.seed));
286
+ return base - deep * Math.pow(Math.sin(Math.PI * p), 1.6);
287
+ }
288
+
289
+ const verts = [], rings = [];
290
+ const ring = f => {
291
+ const r = [];
292
+ for (let i = 0; i < A; i++) {
293
+ const az = i / A * Math.PI * 2;
294
+ verts.push(f(az));
295
+ r.push(verts.length - 1);
296
+ }
297
+ rings.push(r);
298
+ };
299
+
300
+ for (let j = 0; j < ROWS; j++) {
301
+ const t = j / (ROWS - 1);
302
+ ring(az => {
303
+ const hem = hemCol(az);
304
+ const y = lerp(hem, 1.0, smooth(t) * .85 + t * .15);
305
+ const swell = .72 + .28 * Math.sin(Math.min(1, .15 + t) * Math.PI * .8);
306
+ const wob = st.wave ? st.wave * H.wave * Math.sin(az * 3 + t * 9) : 0;
307
+ const p = pt(az, y, 1.012 + vol * swell * carve(az, y) + wob);
308
+ // THE POMPADOUR: a radial push cannot make an upswept fringe — it
309
+ // just makes a fatter helmet — so this displaces the outer surface
310
+ // UP and FORWARD over the front only, peaking between hairline and
311
+ // crown and returning to zero at both: the outer surface meets the
312
+ // inner one at the crown, and a lift carried into that fold would
313
+ // tear the mass open.
314
+ if (lift) {
315
+ const fz = Math.max(0, Math.cos(az));
316
+ const w = Math.sin(t * Math.PI) * fz * fz;
317
+ return [p[0], p[1] + lift * shape.ry * w, p[2] + lift * shape.rz * .40 * w];
318
+ }
319
+ return p;
320
+ });
321
+ }
322
+ for (let j = 0; j < ROWS; j++) {
323
+ const t = j / (ROWS - 1);
324
+ ring(az => pt(az, lerp(1.0, hemCol(az) + .05, smooth(t)), 1.015));
325
+ }
326
+
327
+ const faces = [], R = rings.length;
328
+ for (let j = 0; j < R; j++) {
329
+ const r0 = rings[j], r1 = rings[(j + 1) % R];
330
+ for (let i = 0; i < A; i++) {
331
+ const i2 = (i + 1) % A;
332
+ faces.push([r0[i], r0[i2], r1[i2], r1[i]]);
333
+ }
334
+ }
335
+ return subdivideN({ verts, faces }, SUBDIV);
336
+ }
337
+
338
+ /** several meshes as one — every wisp on a head arrives as ONE mesh,
339
+ * because eight wisps as eight meshes is 280 draw calls on the sheet. */
340
+ function merge(list) {
341
+ const verts = [], faces = [];
342
+ for (const m of list) {
343
+ const off = verts.length;
344
+ for (const v of m.verts) verts.push(v);
345
+ for (const f of m.faces) faces.push(f.map(i => i + off));
346
+ }
347
+ return { verts, faces };
348
+ }
349
+
350
+ /** A TUBE along a path — rings carried by PARALLEL TRANSPORT rather
351
+ * than rebuilt from a fixed up-vector, which would spin where the
352
+ * path turns vertical and pinch it into an hourglass. */
353
+ function strand(path, radii, sides = 8, subdiv = 2) {
354
+ const verts = [], rings = [];
355
+ let nrm = null;
356
+ for (let i = 0; i < path.length; i++) {
357
+ const a = path[Math.max(0, i - 1)], b = path[Math.min(path.length - 1, i + 1)];
358
+ const t = norm(sub(b, a));
359
+ if (!nrm) nrm = cross(Math.abs(t[1]) < .9 ? [0, 1, 0] : [1, 0, 0], t);
360
+ else { const d = dot(nrm, t); nrm = [nrm[0] - t[0] * d, nrm[1] - t[1] * d, nrm[2] - t[2] * d]; }
361
+ nrm = norm(nrm);
362
+ const bi = cross(t, nrm), ring = [];
363
+ for (let k = 0; k < sides; k++) {
364
+ const a2 = k / sides * Math.PI * 2;
365
+ const c = Math.cos(a2) * radii[i], s2 = Math.sin(a2) * radii[i];
366
+ verts.push([path[i][0] + nrm[0] * c + bi[0] * s2,
367
+ path[i][1] + nrm[1] * c + bi[1] * s2,
368
+ path[i][2] + nrm[2] * c + bi[2] * s2]);
369
+ ring.push(verts.length - 1);
370
+ }
371
+ rings.push(ring);
372
+ }
373
+ const faces = [];
374
+ for (let i = 0; i < rings.length - 1; i++)
375
+ for (let k = 0; k < sides; k++) {
376
+ const k2 = (k + 1) % sides;
377
+ faces.push([rings[i][k], rings[i][k2], rings[i + 1][k2], rings[i + 1][k]]);
378
+ }
379
+ faces.push(rings[rings.length - 1].slice());
380
+ faces.push(rings[0].slice().reverse());
381
+ return subdivideN({ verts, faces }, subdiv);
382
+ }
383
+
384
+ // ---- THE WISPS --------------------------------------------------------
385
+ // The point strands. Three families, and each one is a real part of how
386
+ // the reference figures are molded:
387
+ //
388
+ // MOMIAGE the pair hanging in front of the ears. They hang, so they
389
+ // do not follow the flow — they run down the surface and
390
+ // keep falling past the mass hem, longer than it, which is
391
+ // what makes them read as separate strands and not as two
392
+ // more clumps.
393
+ // FRINGE loose strands over the forehead, marching along the flow
394
+ // ray through their root — down for a fringe, up-and-back
395
+ // for the upswept cuts, automatically, because those styles
396
+ // moved the whorl to the front hairline.
397
+ //
398
+ // Every root starts INSIDE the mass (a strand growing from a point must
399
+ // never float off it) and every tip eases just proud of it.
400
+
401
+ function wispSet(st, pt, H, F, shape, massTop) {
402
+ const ry = shape.ry;
403
+ const out = [];
404
+ const W = H.wisps;
405
+
406
+ // MOMIAGE — only where the ear region is actually EXPOSED. On a bob
407
+ // or a melena the mass itself already falls past the ear, and a
408
+ // separate lock riding outside it just reads as a ridge stuck onto
409
+ // the haircut. `st.locks` overrides: the hime cut IS its face locks.
410
+ const lockLen = (st.locks ?? 1) * W;
411
+ for (const sgn of [-1, 1]) {
412
+ const az = sgn * (1.38 + .12 * jitter(sgn + 3, H.seed));
413
+ const hem = hemAt(st, az, H);
414
+ if (hem < -.9 && !st.locks) continue;
415
+ const end = hem - (.25 + .3 * jitter(sgn + 9, H.seed)) * lockLen;
416
+ const N = 7, path = [], radii = [];
417
+ for (let i = 0; i < N; i++) {
418
+ const t = i / (N - 1);
419
+ const y = lerp(.25, end, smooth(t) * .8 + t * .2);
420
+ // buried at the root, HUGGING the mass — a lock that stands off
421
+ // it reads as an antenna — and easing barely proud at the tip
422
+ const infl = lerp(massTop - .06, massTop + .012, smooth(Math.min(1, t * 2)))
423
+ + .03 * t * t;
424
+ path.push(pt(az, y, infl));
425
+ radii.push(ry * .068 * (1 - .8 * t * t) + ry * .004);
426
+ }
427
+ out.push(strand(path, radii, 6, 1));
428
+ }
429
+
430
+ // FRINGE wisps — strands that HANG PAST the fringe's hem, close to
431
+ // the skin, the stray bangs every reference figure has. They were
432
+ // flow-marched first, riding on top of the fringe, and read as
433
+ // antennae however short they got: a wisp crossing the outer surface
434
+ // at an angle is a stick, wherever it points. Hanging is a direction
435
+ // that cannot be misread. Root buried under the mass rim, and by the
436
+ // time it clears the hem it sits just off the SKIN — below the hem
437
+ // there is no mass to hug.
438
+ const nf = 2 + (jitter(17, H.seed) > .45 ? 1 : 0);
439
+ for (let k = 0; k < nf; k++) {
440
+ const az0 = (nf === 1 ? 0 : (k / (nf - 1) - .5)) * .9
441
+ + (jitter(k + 23, H.seed) - .5) * .25;
442
+ const hem = hemAt(st, az0, H);
443
+ if (hem < -.85) continue; // no forehead hem to hang past
444
+ const drop = (.14 + .12 * jitter(k + 41, H.seed)) * W;
445
+ const N = 6, path = [], radii = [];
446
+ for (let i = 0; i < N; i++) {
447
+ const t = i / (N - 1);
448
+ const y = lerp(hem + .16, hem - drop, smooth(t) * .7 + t * .3);
449
+ const infl = lerp(massTop - .05, 1.055, smooth(Math.min(1, t * 1.6)));
450
+ path.push(pt(az0 + .04 * Math.sin(t * 3 + k), y, infl));
451
+ radii.push(ry * .034 * (1 - .72 * t * t) + ry * .003);
452
+ }
453
+ out.push(strand(path, radii, 6, 1));
454
+ }
455
+
456
+ // There was a third family here — FLYAWAYS, one or two strands
457
+ // marching off the crown near the whorl. Removed after three tuning
458
+ // rounds: a tube crossing the crown's convex silhouette reads as a
459
+ // stick at ANY length or lift, and the reference figures have none —
460
+ // their crowns are smooth molded vinyl, and the one strand standing
461
+ // up there is the AHOGE, which is already a deliberate feature with
462
+ // its own dice. A wisp the tuning cannot save is a wisp the design
463
+ // does not want.
464
+
465
+ return merge(out);
466
+ }
467
+
468
+ /** a tail hanging from an anchor: OUT first, then down — standing clear
469
+ * of the head is the entire silhouette of a tied-up style. Its reach
470
+ * scales with the HEAD, never with the hair's length. */
471
+ function tailAt(pt, ry, az, y, len, thick, flick) {
472
+ const root = pt(az, y, 1.08);
473
+ const out = norm([root[0], 0, root[2]]);
474
+ const reach = ry * .58, N = 7, path = [], radii = [];
475
+ for (let i = 0; i < N; i++) {
476
+ const t = i / (N - 1);
477
+ const outward = (.30 + Math.sin(t * 1.45) * .80) * (1 - t * .22) + flick * t * t;
478
+ path.push([root[0] + out[0] * outward * reach,
479
+ root[1] - len * smooth(t),
480
+ root[2] + out[2] * outward * reach - len * .10 * t]);
481
+ radii.push(thick * (1 - .62 * t * t) * (t < .12 ? .8 + t * 1.7 : 1));
482
+ }
483
+ return strand(path, radii);
484
+ }
485
+
486
+ /** THE AHOGE — one strand off the crown, curling over. Eight vertices,
487
+ * and the single most chibi thing on a head. */
488
+ function ahogeAt(pt, ry, H) {
489
+ const root = pt(H.ahogeAng, .93, 1.06), N = 7, path = [], radii = [];
490
+ for (let i = 0; i < N; i++) {
491
+ const t = i / (N - 1);
492
+ path.push([root[0] + Math.sin(t * 2.6) * ry * .34 * H.ahogeDir,
493
+ root[1] + ry * .62 * Math.sin(t * 1.5),
494
+ root[2] + ry * .20 * Math.sin(t * 2.1)]);
495
+ radii.push(ry * .062 * (1 - .78 * t));
496
+ }
497
+ return strand(path, radii, 6, 2);
498
+ }
499
+
500
+ /**
501
+ * P.hair + the layout → a list of `{ id, mesh }` and `{ id, ball }`.
502
+ * The PART turns these into specs; this file has no opinion about
503
+ * materials, colours or three.js.
504
+ */
505
+ export function buildHair(P, L) {
506
+ const H = P.hair;
507
+ const st = STYLE[H.style];
508
+ // a beanie or a headband is worn BARE — see `hatBare` in hat.js
509
+ if (!st || !L.shape || L.hatBare) return [];
510
+ const pt = pointer(L.shape);
511
+ const ry = L.shape.ry;
512
+ const vol = st.vol * H.vol;
513
+ const F = makeFlow(st, H);
514
+
515
+ const out = [{ id: 'hair', mesh: hairMass(st, pt, H, L.shape, F) }];
516
+ out.push({ id: 'hairWisps', mesh: wispSet(st, pt, H, F, L.shape, 1 + vol) });
517
+
518
+ if (st.tails === 2) {
519
+ for (const s of [-1, 1])
520
+ out.push({ id: 'hairTail' + (s < 0 ? 'L' : 'R'),
521
+ mesh: tailAt(pt, ry, s * H.tailAng, H.tailY,
522
+ ry * H.tailLen, ry * .34, s * .12) });
523
+ } else if (st.tails === 1) {
524
+ out.push({ id: 'hairTail',
525
+ mesh: tailAt(pt, ry, Math.PI, H.tailY, ry * H.tailLen * 1.15, ry * .40, 0) });
526
+ }
527
+ if (st.buns) {
528
+ for (const s of [-1, 1])
529
+ out.push({ id: 'hairBun' + (s < 0 ? 'L' : 'R'),
530
+ ball: { r: ry * H.bunR, pos: pt(s * H.bunAng, .62, 1 + vol) } });
531
+ }
532
+ if (H.ahoge) out.push({ id: 'hairAhoge', mesh: ahogeAt(pt, ry, H) });
533
+ return out;
534
+ }