@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,357 @@
1
+ // THE EYES — the part that decides what the character IS. Every style is one
2
+ // plate per side — plus a border, a pupil and a lid on the white-eyed
3
+ // ones — and every one of them lands through `L.at`, so none of them
4
+ // know what shape they are sitting on.
5
+ //
6
+ // There WAS a third plate: a rim, the same outline a size larger in a
7
+ // darker body tone, standing in for the carved socket of the reference.
8
+ // It was removed. Against saturated vinyl a .74 shade reads as shadow,
9
+ // but these palettes are pale and low-contrast, so it came out as a
10
+ // second COLOUR rather than a darker one — and with no concavity behind
11
+ // it there was no cue saying "recess" either. It read as two stacked
12
+ // shapes, because that is what it was. If the carved look is wanted
13
+ // back it has to be real geometry: depth in the rim and the ink sunk
14
+ // into it, not a colour standing in for a shadow.
15
+
16
+ // outline + the numbers that outline reads.
17
+ // `shut` already a closed lid, so the blink leaves it alone
18
+ // `sclera` a WHITE eye with a dark pupil laid on it — two plates,
19
+ // and the pupil is the one thing on this character that moves
20
+ // independently of the face it is on
21
+ const STYLE = {
22
+ bead: { outline: 'ellipse', wf: .62, hf: .62 },
23
+ oval: { outline: 'ellipse', wf: .78, hf: 1.15 },
24
+ round: { outline: 'ellipse', wf: 1, hf: 1 },
25
+ sparkle: { outline: 'sparkle', wf: 1.14, hf: 1.28, pinch: .22 },
26
+ star: { outline: 'star', wf: 1.2, hf: 1.2 },
27
+ heart: { outline: 'heart', wf: 1, hf: 1 },
28
+ ring: { outline: 'ring', wf: 1.05, hf: 1.05, thick: .36 },
29
+ cross: { outline: 'cross', wf: 1.1, hf: 1.1 },
30
+ sleepy: { outline: 'band', wf: 1.15, hf: .16, curve: 'arc', sag: -.14, shut: true },
31
+ happy: { outline: 'band', wf: 1.15, hf: .16, curve: 'arc', sag: .3, shut: true },
32
+ angry: { outline: 'ellipse', wf: .84, hf: 1, roll: .34 },
33
+
34
+ // --- the drawn eye: a BORDER, a white, and a big pupil ---
35
+ // The border is the doodle's ink line, and it is what the first pass
36
+ // was missing. Note this is NOT the rim that was thrown out: that
37
+ // was a body-coloured ring standing in for a shadow, which read as a
38
+ // second colour. An INK line around a WHITE fill is line art, and it
39
+ // is the thing every eye in the reference sheet has.
40
+ pupil: { outline: 'ellipse', wf: .92, hf: 1.14, sclera: true, pupilF: .62, border: .3 },
41
+ // the same construction with a smaller pupil in a bigger white —
42
+ // reads startled even when it is not looking anywhere
43
+ googly: { outline: 'ellipse', wf: 1.02, hf: 1.06, sclera: true, pupilF: .42, border: .26 },
44
+ // and the square of it, for the cube heads
45
+ box: { outline: 'rect', wf: .74, hf: .84, r: .26, sclera: true, pupilF: .6, border: .28 },
46
+ // BIG, and it gets away with it by growing the way a face has room
47
+ // to grow: a tall rectangle gains all its area in height, so it can
48
+ // be enormous without crowding the centre line the way a wide eye
49
+ // does. Rectangular pupil to match, and because the white is so tall
50
+ // the pupil has a long way to travel in it — this is the most
51
+ // expressive gaze in the catalogue.
52
+ slab: { outline: 'rect', wf: .88, hf: 1.40, r: .3, sclera: true, border: .24,
53
+ pupilF: .68, pupilHF: .44, pupilOutline: 'rect', pupilR: .3 },
54
+ // a straight-sided diamond: the sparkle's own family, walked out to
55
+ // pinch .5 where the quadratic control lands on the chord
56
+ diamond: { outline: 'sparkle', wf: .92, hf: 1.18, pinch: .5 },
57
+ square: { outline: 'rect', wf: .66, hf: .7, r: .28 },
58
+
59
+ // --- the BALL eye: the one eye that is a solid, not a plate ---
60
+ // A white sphere standing proud of the head, an ink bead sunk into
61
+ // its front, and a hemisphere CAP in the body colour lying over the
62
+ // top half — the Rabbid / Muppet construction. The cap is centred on
63
+ // the ball, so the blink is a rotation: it ROLLS forward over the
64
+ // eyeball instead of sliding down a face (`lidRoll`, in gface.js).
65
+ // A SPHERE by decree. An ellipsoid lid cannot
66
+ // roll: turned 120° its short axis faces where the ball's long axis
67
+ // is, and the pupil punches through the shell. Sphere on sphere can
68
+ // never misalign, and the ball being a ball IS the style anyway.
69
+ orb: { orb: true, wf: 1.02, hf: 1.02 },
70
+ flower: { outline: 'flower', wf: 1.06, hf: 1.06, petals: 5, amp: .3 },
71
+ crescent: { outline: 'crescent', wf: .82, hf: 1.12, bite: .55 },
72
+ wobble: { outline: 'wobble', wf: .9, hf: 1 },
73
+ spiral: { outline: 'band', wf: 1, hf: .13, curve: 'spiral' },
74
+ };
75
+
76
+ export const EYE_STYLES = Object.keys(STYLE);
77
+
78
+ /**
79
+ * How far an eye reaches from its own centre, in units of `L.eyeSize`.
80
+ *
81
+ * The LAYOUT needs this: it is what stops a mouth being placed inside
82
+ * a tall eye, and only this file knows how tall a style is. A pure
83
+ * function of the recipe, so the layout can ask before anything is
84
+ * built. Border included — it is part of the drawn shape.
85
+ */
86
+ // The eye's extent has to cover EVERYTHING the part emits, not just
87
+ // the eye shape. A roll counts: a tilted rectangle is wider than its
88
+ // own width.
89
+ function extent(P, alongY) {
90
+ const st = STYLE[P.eyes.style] ?? STYLE.oval;
91
+ const hf = st.hf;
92
+ const bt = st.sclera ? Math.min(st.wf, hf) * st.border : 0;
93
+ const a = (alongY ? hf : st.wf) + bt; // the axis we want
94
+ const b = (alongY ? st.wf : hf) + bt; // and the one that rolls into it
95
+ const rl = Math.abs(st.roll ?? 0);
96
+ return a * Math.cos(rl) + b * Math.sin(rl);
97
+ }
98
+
99
+ /**
100
+ * How far an eye reaches from its own centre, in units of `L.eyeSize`.
101
+ *
102
+ * The LAYOUT needs this: it is what stops a mouth being placed inside
103
+ * a tall eye, and only this file knows how tall a style is. A pure
104
+ * function of the recipe, so the layout can ask before anything is
105
+ * built. The border is included — it is part of the drawn shape.
106
+ */
107
+ export function eyeReach(P) { return extent(P, true); }
108
+
109
+ /** the same, sideways: half-width in units of `L.eyeSize`. The layout
110
+ * uses it to keep an eye on the head and out of its partner. */
111
+ export function eyeSpan(P) { return extent(P, false); }
112
+
113
+ /**
114
+ * How far the eye's FRONT stands off the skin, in units of `L.eyeSize`.
115
+ *
116
+ * Spectacles need it and only this file can know it: an `orb` is a ball
117
+ * standing most of its own radius proud, so a lens placed for a flat
118
+ * plate cuts straight through the eyeball. Same edge as `eyeReach` —
119
+ * the eyes publish a fact about themselves rather than the other part
120
+ * reading their style table.
121
+ */
122
+ export function eyeProud(P) {
123
+ const st = STYLE[P.eyes.style] ?? STYLE.oval;
124
+ // the ball sits centred .32 of its radius INTO the head, so its front
125
+ // crest is .68 of a radius out; the pupil adds a little on top
126
+ if (st.orb) return st.wf * .74;
127
+ return (P.eyes.proud ?? .2) + .12; // plate front, plus its pupil
128
+ }
129
+
130
+ export const Eyes = {
131
+ id: 'eyes', label: 'eyes', order: 1,
132
+
133
+ // Weighted, not uniform: eleven styles dealt evenly gives a sheet
134
+ // where a third of the characters have hearts or stars for eyes, and the
135
+ // odd ones stop being odd. The plain shapes carry the line and the
136
+ // rest are the exceptions.
137
+ gen: (rng, C) => ({
138
+ // reweighted against the Ferriz sheet: it is carried by tiny wide
139
+ // beads, white-and-pupil eyes and lidded ones — sparkles are OUR
140
+ // habit, not his, and they drop to a treat
141
+ style: C.pick(rng, 'style', [['pupil', 17], ['bead', 15], ['slab', 9], ['box', 8],
142
+ ['googly', 8], ['oval', 8], ['sleepy', 6], ['happy', 6],
143
+ ['cross', 5], ['round', 4], ['sparkle', 3], ['orb', 3],
144
+ ['square', 2],
145
+ ['diamond', 2], ['crescent', 1], ['star', 1], ['ring', 1],
146
+ ['heart', 1], ['flower', 1], ['angry', 1], ['spiral', 1],
147
+ ['wobble', 1]]),
148
+ // WHERE THE PUPIL PARKS in its white: +1 is jammed against the top
149
+ // of the eye, −1 against the bottom, 0 dead centre. A pupil resting
150
+ // at the top reads alert and a little unhinged; at the bottom it
151
+ // reads sleepy or sly. Centred is the least interesting of the
152
+ // three, which is why it is not the default.
153
+ pupilY: rng.wpick([[.78, 26], [-.72, 20], [0, 24], [.45, 16], [-.4, 14]]),
154
+ // a heavy lid over the top of the eye, and it comes DOWN when the
155
+ // character blinks. Only the white-and-pupil eyes read it — a lid on a
156
+ // solid black shape is just a thicker black shape.
157
+ lid: C.chance(rng, 'lid', .4),
158
+ // a wink is not a twelfth style, it is any style with one lid down
159
+ wink: C.chance(rng, 'wink', .08),
160
+ // The face lives in the UPPER half — that empty sweep of body under
161
+ // the mouth is most of what makes the reference read as a character
162
+ // rather than a smiley. And these ranges are WIDE on purpose:
163
+ // tiny-eyed and saucer-eyed are the same generator.
164
+ // WIDE-SET and not huge. Two big eyes crowding the centre line is
165
+ // the single thing that makes these read as emoji instead of as
166
+ // characters — the reference sets them far enough apart that there is
167
+ // face between them.
168
+ x: C.range(rng, 'x', .46, .68), // face coordinate, out from the middle
169
+ y: C.range(rng, 'y', .08, .3),
170
+ // BIG. The features fill a Ferriz head — small features huddled in
171
+ // the middle of a big empty ball was the single loudest difference
172
+ // on the first sheet. Everything else sizes off the eyes, so this
173
+ // is also what makes the mouths and muzzles read.
174
+ // The FLOOR is what matters here, not the ceiling: everything else
175
+ // on the face sizes off this, so a low roll shrinks the whole face
176
+ // to a stamp in the middle of a big blank head. At .14 there were
177
+ // several a sheet.
178
+ size: C.range(rng, 'size', .142, .182), // × body radius
179
+ proud: rng.r(.1, .3), // × eye size
180
+ }),
181
+
182
+ meta: () => ({
183
+ style: { label: 'style', pick: EYE_STYLES },
184
+ wink: { label: 'wink', bool: true },
185
+ x: { label: 'apart', range: [.1, .8] },
186
+ y: { label: 'height', range: [-.4, .6] },
187
+ size: { label: 'size', range: [.05, .26] },
188
+ proud: { label: 'relief', range: [0, .7] },
189
+ pupilY: { label: 'pupil parks', range: [-1, 1] },
190
+ lid: { label: 'eyelid', bool: true },
191
+ }),
192
+
193
+ build(add, P, L) {
194
+ const E = P.eyes;
195
+ // `eyeSize`, not `eyeR`: the layout publishes the eyes' own radius
196
+ // (bigger on a cube, whose flat face has room a sphere's curvature
197
+ // never gives) separately from the unit the nose, mouth and blush
198
+ // size off. It has to live there and not here — the layout uses the
199
+ // same number to keep the mouth out from under a tall eye.
200
+ const r = L.eyeSize;
201
+
202
+ for (const [id, side] of [['eyeL', -1], ['eyeR', 1]]) {
203
+ // the winking side gets a closed lid whatever the style is
204
+ const shut = E.wink && side > 0;
205
+ const st = shut ? STYLE.happy : STYLE[E.style];
206
+ const a = L.onFace(side * L.eyeX, L.eyeY);
207
+ // AN EYE'S ASPECT IS THE STYLE'S, FULL STOP. There WAS a rolled
208
+ // `tall` that stretched the height on top of the style's own
209
+ // proportions, and it is gone: a round eye came out an egg, a
210
+ // slab came out a bar, and on the rectangles — whose corner
211
+ // radius is a fraction of the SHORTER half-extent — a bar rounded
212
+ // its ends into a capsule and stopped being the shape it named. A
213
+ // clamp on the stretch only bounded the damage. The size varies,
214
+ // the aspect does not, so `round` is round on every character.
215
+ const w = r * st.wf;
216
+ const h = r * st.hf;
217
+ const d = r * .34;
218
+
219
+ // THE BALL EYE takes its own road: three solids, no plates.
220
+ if (st.orb) {
221
+ const R = w; // the ball's radius across
222
+ // sunk a third in, like the button nose: a bump on the face,
223
+ // not a bead balanced on it
224
+ const ballProud = -R * .32;
225
+ // the whole ball barely stirs — the thing inside it moves
226
+ const ride = [R * .06, R * .05];
227
+ // `shut: true` on the ball and the bead: the blink must not
228
+ // squash them — a deflating eyeball is horrible — the cap
229
+ // closing over them IS the blink.
230
+ add({ type: 'solid', id, rx: R, ry: R, rz: R,
231
+ p: a.p, n: a.n, proud: ballProud, color: L.sclera,
232
+ travel: ride, shut: true });
233
+
234
+ // the ink bead, sunk into the ball's front so it can never
235
+ // float — the same trick as `dab` in the voxel hand. Sunk
236
+ // DEEP: its crown must stay inside the lid's shell, or a shut
237
+ // lid has a black dot punching through it.
238
+ const pr = R * .36;
239
+ const park = (R - pr) * .4 * E.pupilY;
240
+ add({ type: 'solid', id: id + 'Pupil', rx: pr, ry: pr, rz: pr,
241
+ p: a.p, n: a.n, proud: ballProud + R - pr * .7,
242
+ color: L.ink, offset: [0, park], anchorY: park,
243
+ travel: [(R - pr) * .5, (R - pr) * .4 * (1 - Math.abs(E.pupilY))],
244
+ shut: true });
245
+
246
+ // the CAP: a hemisphere-and-a-bit in the body colour, CENTRED
247
+ // ON THE BALL — its own p, proud 0 — so its rim hugs the ball
248
+ // all the way round and the blink can pivot it about the
249
+ // ball's centre. Its axis is NOT the eye's surface normal:
250
+ // that normal tips up with the face, which parks the cap over
251
+ // the whole front. The lid brings its own basis, tipped BACK,
252
+ // so at rest only its front rim shows — a heavy lid over the
253
+ // top third — and the pupil has the rest of the ball to live
254
+ // on. The roll budget is what carries it from there to shut.
255
+ const ballC = [a.p[0] + a.n[0] * ballProud,
256
+ a.p[1] + a.n[1] * ballProud,
257
+ a.p[2] + a.n[2] * ballProud];
258
+ add({ type: 'solid', id: id + 'Lid', dome: .56,
259
+ rx: R * 1.16, ry: R * 1.16, rz: R * 1.16,
260
+ p: ballC, n: [a.n[0] * .35, .95, .85],
261
+ color: L.body, travel: ride, lidRoll: 2.2 });
262
+ continue;
263
+ }
264
+
265
+ const roll = (st.roll ?? 0) * side;
266
+ const base = { type: 'plate', p: a.p, n: a.n, roll,
267
+ pinch: st.pinch, thick: st.thick,
268
+ // The outline wants a corner radius in WORLD units,
269
+ // and a style states it as a fraction — of the
270
+ // SHORTER half-extent, which is the only one that
271
+ // means anything on a tall rectangle. As a fraction
272
+ // of height it would round a slab's corners past
273
+ // its own width and silently give back a capsule.
274
+ r: st.r == null ? undefined : Math.min(w, h) * st.r,
275
+ petals: st.petals, amp: st.amp, bite: st.bite,
276
+ curve: st.curve, sag: st.sag == null ? undefined : st.sag * r };
277
+ const isBand = st.outline === 'band';
278
+
279
+ const proud = r * E.proud;
280
+ // the white barely shifts — an eyeball does not slide around a
281
+ // face, the thing inside it does
282
+ const ride = st.sclera ? [w * .1, h * .08] : null;
283
+
284
+ // There WAS a PATCH here — a field of another pour behind each
285
+ // eye, the panda's black oval. Removed: on a face this size it
286
+ // read as a second thing stuck on the head rather than as part
287
+ // of the eye, and it was the one feature that regularly slid off
288
+ // the silhouette because it was twice the eye across.
289
+
290
+ // THE BORDER: the same outline a size larger in ink, sitting a
291
+ // hair further back, so what shows around the white is a drawn
292
+ // line rather than a gap.
293
+ if (st.sclera) {
294
+ // ONE thickness on every side. Grown as a percentage per axis
295
+ // instead, a tall eye gets a heavy brow-line and hairline
296
+ // sides — a drawn outline is a stroke of constant width.
297
+ const bt = Math.min(w, h) * st.border;
298
+ const bw = w + bt, bh = h + bt;
299
+ add({ ...base, id: id + 'Border', outline: st.outline, w: bw, h: bh,
300
+ r: st.r == null ? undefined : Math.min(bw, bh) * st.r,
301
+ d: d * .8, bevel: Math.min(d * .34, bt * .8),
302
+ proud: proud - d * .16, color: L.ink, travel: ride });
303
+ }
304
+
305
+ add({ ...base, id, outline: st.outline, w, h,
306
+ tube: isBand ? h : undefined,
307
+ d, bevel: Math.min(d * .6, h * .45),
308
+ proud, color: st.sclera ? L.sclera : L.ink,
309
+ travel: ride,
310
+ shut: !!(st.shut || shut) });
311
+
312
+ // THE PUPIL, and it is the only part of this character that moves
313
+ // relative to the face it is on. It carries its own travel
314
+ // budget — how far it may slide before it would leave the white
315
+ // — so `gface.js` can drive it without knowing an eye's shape.
316
+ if (st.sclera && !shut) {
317
+ // A style may set the pupil's height independently of its
318
+ // width: on a tall eye a pupil scaled by one factor fills the
319
+ // whole white and has nowhere left to look.
320
+ const pw = w * st.pupilF, ph = h * (st.pupilHF ?? st.pupilF * .96);
321
+ // the room the pupil has to move in, and where in that room it
322
+ // rests. Whatever it parks against, it keeps the REST of the
323
+ // room to look around in — so a pupil already at the top does
324
+ // not get to travel further up and slide out of its own eye.
325
+ const roomY = h - ph;
326
+ const park = roomY * E.pupilY;
327
+ add({ type: 'plate', id: id + 'Pupil',
328
+ outline: st.pupilOutline ?? 'ellipse',
329
+ r: st.pupilR == null ? undefined : Math.min(pw, ph) * st.pupilR,
330
+ p: a.p, n: a.n,
331
+ w: pw, h: ph, d: d * .55, bevel: d * .26,
332
+ proud: proud + d * .3, color: L.ink,
333
+ offset: [0, park], anchorY: park,
334
+ travel: [(w - pw) * .74, roomY * (1 - Math.abs(E.pupilY)) * .85] });
335
+
336
+ // THE LID: a heavy stroke lying over the top of the eye, and
337
+ // it slides down as the character blinks. It is the clearest thing
338
+ // the separate-mesh face buys — a lid closing over a pupil
339
+ // costs one translate and no geometry at all.
340
+ if (E.lid) {
341
+ add({ type: 'plate', id: id + 'Lid', outline: 'band',
342
+ curve: 'arc', sag: -h * .34, tube: h * .3, h: h * .3,
343
+ p: a.p, n: a.n, w: w * 1.04,
344
+ d: d * .9, bevel: d * .3,
345
+ proud: proud + d * .42, color: L.ink,
346
+ offset: [0, h * .82], travel: ride, lidDrop: h * 1.05 });
347
+ }
348
+ }
349
+
350
+ // There was a painted catchlight here — a small white dot on the
351
+ // upper left of every dark eye. It is gone. The clearcoat already
352
+ // puts a real highlight on these, and a painted one sat on top of
353
+ // it as a second, flatter dot that did not move with the light.
354
+ // Gloss is the studio's job; do not draw it on.
355
+ }
356
+ },
357
+ };
@@ -0,0 +1,225 @@
1
+ // THE FRAME — what a character STANDS ON. Two stances: `none` (the head IS
2
+ // the character, the sheet as it always was) and `biped` (a chibi torso on
3
+ // two stub feet, arms at the sides).
4
+ //
5
+ // Everything here is RAYMAN anatomy: no arms, no legs, no neck. The
6
+ // torso is an upright pill (a superellipsoid taller than it is wide,
7
+ // squared just enough to read as a cylinder), the hands are balls
8
+ // FLOATING beside it, the feet are shoes floating under it, and the
9
+ // air in the gaps is what makes it a character and not a figurine. The head
10
+ // still SINKS into the torso — the one joint that is a socket, not a
11
+ // gap. The head stays the character: the whole frame adds roughly half a
12
+ // head of height, never more, so the face keeps carrying the sheet.
13
+ //
14
+ // The split of labour follows the muzzle rule: this part knows what a
15
+ // frame IS (the proportions, where limbs sit), so `frameLayout` states
16
+ // it — and the LAYOUT calls it, because the head's own height depends
17
+ // on the answer. The part's build() then just stamps what the layout
18
+ // measured. Face parts never learn any of this: they place through
19
+ // `L.at`, which simply sits higher.
20
+ import { subdivideN } from '../catmullClark.js';
21
+
22
+ export const Frame = {
23
+ id: 'frame', label: 'frame', order: 1,
24
+
25
+ // rolled for every character, read only when the recipe's stance is not
26
+ // `none` — same shape as `corner`, which only a cube reads
27
+ gen: (rng, C) => ({
28
+ // SMALL. The head is the character and the frame is a stand it grew —
29
+ // these were a quarter bigger and the characters started reading as
30
+ // characters with heads instead of heads with a stance.
31
+ hips: C.range(rng, 'hips', .23, .31), // torso half-width × r
32
+ squat: C.range(rng, 'squat', .2, .27), // torso half-height × r
33
+ // squared enough that the sides run straight — that is the whole
34
+ // difference between a pill and an egg
35
+ belly: C.range(rng, 'belly', 2.4, 3.2),
36
+ float: C.range(rng, 'float', .05, .1), // the air under the torso, × r
37
+ footR: C.range(rng, 'footR', .13, .17), // shoe half-width × r
38
+ handR: C.range(rng, 'handR', .1, .14), // mitt radius × r
39
+ handY: C.range(rng, 'handY', .35, .6), // where on the torso they hover
40
+ // THE OUTFIT. `dressed` swaps the torso from the body's own pour to
41
+ // cloth; gloves and shoes are always coloured — a bare biped with
42
+ // white gloves and red shoes is the oldest character design there
43
+ // is. The layout resolves the colours (`L.outfit`); this only rolls
44
+ // the dice. Half the creatures go dressed; the humanoid pins 1.
45
+ dressed: C.chance(rng, 'dressed', .5),
46
+ clothIx: rng.ri(0, 9),
47
+ // the screen-printed graphic on the cloth. `none` carries the
48
+ // sheet — a print on every chest is a uniform, not a wardrobe.
49
+ motif: C.pick(rng, 'motif', [['none', 44], ['stripes', 16], ['star', 11],
50
+ ['heart', 10], ['spot', 10], ['zig', 9]]),
51
+ }),
52
+
53
+ meta: () => ({
54
+ hips: { label: 'torso width', range: [.2, .6] },
55
+ squat: { label: 'torso height', range: [.18, .55] },
56
+ belly: { label: 'torso square', range: [2, 4.5] },
57
+ float: { label: 'float gap', range: [.02, .3] },
58
+ footR: { label: 'foot size', range: [.1, .35] },
59
+ handR: { label: 'hand size', range: [.08, .3] },
60
+ handY: { label: 'hand height', range: [0, 1] },
61
+ dressed: { label: 'dressed', bool: true },
62
+ motif: { label: 'print', pick: ['none', 'stripes', 'star', 'heart', 'spot', 'zig'] },
63
+ }),
64
+
65
+ build(add, P, L) {
66
+ const Fr = L.frame, O = L.outfit;
67
+ if (!Fr) return;
68
+ // A DRESSED torso is cloth: the `acc` finish (matte, self-coloured
69
+ // sheen) in the outfit's cloth colour, with the print riding as a
70
+ // baked map. Undressed it is the same POUR as the head — a knitted
71
+ // bear has a knitted belly. Either way it is a real volume in the
72
+ // light, so it casts.
73
+ if (O.dressed) {
74
+ add({ type: 'solid', id: 'torso', frame: true, cast: true,
75
+ finish: 'acc', color: O.cloth,
76
+ print: O.motif !== 'none'
77
+ ? { motif: O.motif, cloth: O.cloth, ink: O.ink } : undefined,
78
+ rx: Fr.torso.rx, ry: Fr.torso.ry, rz: Fr.torso.rz, exp: Fr.torso.exp,
79
+ pos: Fr.torso.pos });
80
+ } else {
81
+ add({ type: 'solid', id: 'torso', frame: true, shell: true, cast: true,
82
+ rx: Fr.torso.rx, ry: Fr.torso.ry, rz: Fr.torso.rz, exp: Fr.torso.exp,
83
+ pos: Fr.torso.pos, color: L.body });
84
+ }
85
+ // gloves and shoes are ALWAYS worn — soft plastic in their own
86
+ // colours, never the character's pour
87
+ for (const limb of Fr.limbs)
88
+ add({ type: 'mesh', id: limb.id, frame: true, cast: true,
89
+ finish: 'acc',
90
+ color: limb.id.startsWith('hand') ? O.glove : O.shoe,
91
+ mesh: limb.mesh, pos: limb.pos });
92
+ },
93
+ };
94
+
95
+ // ---- the extremities, MODELED --------------------------------------
96
+ // A mitten has a palm and a thumb; a shoe has a sole, a toe and a
97
+ // heel. Those are parts you can name, so — same verdict as the rock
98
+ // and the slime — they get a control CAGE through Catmull-Clark, not
99
+ // another ellipsoid. Both cages are a handful of rings, deterministic
100
+ // (no rng: a frame is rebuilt per recipe and must never boil), and
101
+ // authored ONCE as the LEFT one — the right is a mirror.
102
+
103
+ const rotY = (verts, a) => {
104
+ const c = Math.cos(a), s = Math.sin(a);
105
+ return verts.map(([x, y, z]) => [x * c + z * s, y, -x * s + z * c]);
106
+ };
107
+
108
+ /** x → −x; faces rewound so the normals keep facing out. */
109
+ const mirrorX = ({ verts, faces }) => ({
110
+ verts: verts.map(([x, y, z]) => [-x, y, z]),
111
+ faces: faces.map(f => f.slice().reverse()),
112
+ });
113
+
114
+ /** rings of [y, k] around y, radius shaped per azimuth by `rad`,
115
+ * capped both ends — the same construction as gform's cage. */
116
+ function ringCage(sides, levels, rad) {
117
+ const verts = [], rings = [];
118
+ for (const [y, k] of levels) {
119
+ const ring = [];
120
+ for (let i = 0; i < sides; i++) {
121
+ const a = i / sides * Math.PI * 2;
122
+ verts.push(rad(a, k, y));
123
+ ring.push(verts.length - 1);
124
+ }
125
+ rings.push(ring);
126
+ }
127
+ const faces = [];
128
+ for (let j = 0; j < rings.length - 1; j++)
129
+ for (let i = 0; i < sides; i++)
130
+ faces.push([rings[j][i], rings[j][(i + 1) % sides],
131
+ rings[j + 1][(i + 1) % sides], rings[j + 1][i]]);
132
+ faces.push(rings[rings.length - 1].slice());
133
+ faces.push(rings[0].slice().reverse());
134
+ return { verts, faces, rings };
135
+ }
136
+
137
+ /**
138
+ * A MITTEN, thumbless-fingered: a soft palm with one thumb swell,
139
+ * authored as the LEFT hand — the thumb on +x, which is the INNER side
140
+ * when the hand floats at −x. Centred on the origin, radius ≈ `s`.
141
+ */
142
+ function makeMitt(s) {
143
+ const cage = ringCage(8,
144
+ [[-1, .55], [-.45, .95], [.25, .97], [.85, .55]],
145
+ (a, k, y) => [Math.sin(a) * k * s, y * s, Math.cos(a) * k * s]);
146
+ // the thumb is IN THE CAGE, like the rock's lumps: one vertex of the
147
+ // upper ring pushed out and up, its neighbours carrying a little of
148
+ // it, so subdivision softens the spike into a swell
149
+ const up = cage.rings[2]; // the y = .25 ring
150
+ const push = (vi, m, lift) => {
151
+ const v = cage.verts[vi];
152
+ v[0] *= m; v[2] *= m; v[1] += lift * s;
153
+ };
154
+ push(up[2], 1.5, .28); // i=2 is dead on +x
155
+ push(up[1], 1.14, .1);
156
+ push(up[3], 1.14, .1);
157
+ return subdivideN({ verts: cage.verts, faces: cage.faces }, 2);
158
+ }
159
+
160
+ /**
161
+ * A SHOE: flat sole (two near-full rings at the bottom — the rock's
162
+ * sitting-flat trick), a toe fuller and longer than the heel, and the
163
+ * upper sloping back as it rises. Origin at the sole's centre, +z is
164
+ * the toe; half-width ≈ `s`.
165
+ */
166
+ function makeShoe(s) {
167
+ const levels = [[0, .97, 0], [.22, 1, 0], [.62, .8, -.14], [1.05, .42, -.34]];
168
+ const cage = ringCage(10, levels.map(l => [l[0], l[1]]),
169
+ (a, k, y) => {
170
+ const zShift = levels.find(l => l[0] === y)[2];
171
+ const cz = Math.cos(a);
172
+ return [Math.sin(a) * k * s,
173
+ y * s * 1.35,
174
+ (cz * (cz > 0 ? 1.6 : 1.05) * k + zShift) * s];
175
+ });
176
+ return subdivideN({ verts: cage.verts, faces: cage.faces }, 2);
177
+ }
178
+
179
+ /** the built mesh's top, for stacking the torso above the shoes. */
180
+ const meshTop = ({ verts }) => verts.reduce((m, v) => Math.max(m, v[1]), 0);
181
+
182
+ /**
183
+ * The frame, measured. Called by the LAYOUT (before anything else is
184
+ * placed, because the head's height depends on it), returned as plain
185
+ * numbers for build() to stamp.
186
+ *
187
+ * `ry` is the HEAD's half-height: the sink and the proportions are all
188
+ * relative to the head, because the head is the subject — a frame that
189
+ * scaled off its own numbers could grow away from the face it carries.
190
+ */
191
+ export function frameLayout(F, { r, ry }, stance) {
192
+ const sink = ry * .22; // the head's socket overlap
193
+ const footR = F.footR * r;
194
+ const gap = F.float * r; // the Rayman air
195
+
196
+ // one LEFT shoe, splayed a few degrees; every other foot is a turn
197
+ // or a mirror of it
198
+ const shoeL = makeShoe(footR);
199
+ shoeL.verts = rotY(shoeL.verts, .14);
200
+ const shoeR = mirrorX(shoeL);
201
+ const footTop = meshTop(shoeL);
202
+
203
+ // biped: an upright pill, two shoes floating under it, two mitts
204
+ // floating beside it. Nothing touches anything — the gaps are load-
205
+ // bearing, they are what says "character" instead of "figurine".
206
+ const rxT = F.hips * r, ryT = F.squat * r * 1.15, rzT = F.hips * r * .92;
207
+ const tCy = footTop + gap + ryT;
208
+ const handR = F.handR * r;
209
+ const mittL = makeMitt(handR), mittR = mirrorX(mittL);
210
+ const limbs = [
211
+ { id: 'footL', mesh: shoeL, pos: [-rxT * .8, 0, r * .07] },
212
+ { id: 'footR', mesh: shoeR, pos: [rxT * .8, 0, r * .07] },
213
+ ...[[-1, 'handL', mittL], [1, 'handR', mittR]].map(([sx, id, mesh]) => ({
214
+ id, mesh,
215
+ pos: [sx * (rxT + handR + r * .06),
216
+ tCy - ryT + F.handY * ryT * 2, 0],
217
+ })),
218
+ ];
219
+ return {
220
+ stance,
221
+ torso: { rx: rxT, ry: ryT, rz: rzT, exp: F.belly, pos: [0, tCy, 0] },
222
+ limbs,
223
+ headBase: tCy + ryT - sink,
224
+ };
225
+ }