@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.
- package/LICENSE +21 -0
- package/NOTICE.md +23 -0
- package/README.md +125 -0
- package/UPSTREAM.md +47 -0
- package/dist/avatar.d.ts +2 -0
- package/dist/avatar.js +69 -0
- package/dist/avatar.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/materials.d.ts +20 -0
- package/dist/materials.js +61 -0
- package/dist/materials.js.map +1 -0
- package/dist/react-three-fiber.d.ts +16 -0
- package/dist/react-three-fiber.js +52 -0
- package/dist/react-three-fiber.js.map +1 -0
- package/dist/recipe.d.ts +18 -0
- package/dist/recipe.js +118 -0
- package/dist/recipe.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +102 -0
- package/upstream-lock.json +38 -0
- package/vendor/kindergrimm/LICENSE +24 -0
- package/vendor/kindergrimm/src/gloss/catmullClark.js +129 -0
- package/vendor/kindergrimm/src/gloss/gface.d.ts +25 -0
- package/vendor/kindergrimm/src/gloss/gface.js +226 -0
- package/vendor/kindergrimm/src/gloss/gform.js +201 -0
- package/vendor/kindergrimm/src/gloss/ghair.js +534 -0
- package/vendor/kindergrimm/src/gloss/glayout.js +365 -0
- package/vendor/kindergrimm/src/gloss/gmedia.d.ts +28 -0
- package/vendor/kindergrimm/src/gloss/gmedia.js +470 -0
- package/vendor/kindergrimm/src/gloss/gpalette.d.ts +10 -0
- package/vendor/kindergrimm/src/gloss/gpalette.js +205 -0
- package/vendor/kindergrimm/src/gloss/gparts/blush.js +57 -0
- package/vendor/kindergrimm/src/gloss/gparts/body.js +47 -0
- package/vendor/kindergrimm/src/gloss/gparts/brows.js +55 -0
- package/vendor/kindergrimm/src/gloss/gparts/crest.js +88 -0
- package/vendor/kindergrimm/src/gloss/gparts/eyes.js +357 -0
- package/vendor/kindergrimm/src/gloss/gparts/frame.js +225 -0
- package/vendor/kindergrimm/src/gloss/gparts/hair.js +100 -0
- package/vendor/kindergrimm/src/gloss/gparts/hat.js +163 -0
- package/vendor/kindergrimm/src/gloss/gparts/index.js +33 -0
- package/vendor/kindergrimm/src/gloss/gparts/mark.js +139 -0
- package/vendor/kindergrimm/src/gloss/gparts/mouth.js +170 -0
- package/vendor/kindergrimm/src/gloss/gparts/nose.js +63 -0
- package/vendor/kindergrimm/src/gloss/gparts/specs.js +124 -0
- package/vendor/kindergrimm/src/gloss/grig.d.ts +41 -0
- package/vendor/kindergrimm/src/gloss/grig.js +286 -0
- package/vendor/kindergrimm/src/gloss/gshape.js +421 -0
- package/vendor/kindergrimm/src/gloss/gspecies.d.ts +10 -0
- package/vendor/kindergrimm/src/gloss/gspecies.js +338 -0
- package/vendor/kindergrimm/src/gloss/gtexture.js +406 -0
- package/vendor/kindergrimm/src/rng.d.ts +11 -0
- 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
|
+
}
|