@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,100 @@
|
|
|
1
|
+
// THE HAIR — the humanoid's only, and the part that turns a head into a
|
|
2
|
+
// character. The shapes are all in `ghair.js`; this file is the dice.
|
|
3
|
+
//
|
|
4
|
+
// The DEFAULT IS BALD, and that is deliberate rather than lazy: a cast
|
|
5
|
+
// style table is exclusive, so a species that says nothing about hair
|
|
6
|
+
// gets none, and only the humanoid casts real styles. A bear with a
|
|
7
|
+
// ponytail is one line away and it is not one anybody wanted.
|
|
8
|
+
import { buildHair, HAIR_STYLES } from '../ghair.js';
|
|
9
|
+
import { HAIR_IDS, HAIR_WEIGHTS } from '../gpalette.js';
|
|
10
|
+
|
|
11
|
+
export { HAIR_STYLES };
|
|
12
|
+
|
|
13
|
+
export const Hair = {
|
|
14
|
+
id: 'hair', label: 'hair', order: 2,
|
|
15
|
+
|
|
16
|
+
gen: (rng, C) => ({
|
|
17
|
+
// bald unless a species asks — see the header
|
|
18
|
+
style: C.pick(rng, 'style', [['bald', 100]]),
|
|
19
|
+
// hair has its own colour table, never the body's palette
|
|
20
|
+
color: C.pick(rng, 'color', HAIR_WEIGHTS),
|
|
21
|
+
// how far the clumps stand off the head. WIDE, because flat-and-
|
|
22
|
+
// sleek against big-and-soft is most of the difference between two
|
|
23
|
+
// heads wearing the same cut.
|
|
24
|
+
vol: C.range(rng, 'vol', .7, 1.5),
|
|
25
|
+
// how many clumps: fewer and chunkier, or more and finer
|
|
26
|
+
density: C.range(rng, 'density', .8, 1.25),
|
|
27
|
+
// how far each clump wraps past its own slice — the overlap
|
|
28
|
+
width: C.range(rng, 'width', .9, 1.15),
|
|
29
|
+
// how unequal the tips are. This is what stops a hem reading as a
|
|
30
|
+
// line somebody ruled across the head.
|
|
31
|
+
jag: C.range(rng, 'jag', .6, 1.5),
|
|
32
|
+
wave: C.range(rng, 'wave', .7, 1.35),
|
|
33
|
+
part: C.range(rng, 'part', .7, 1.3),
|
|
34
|
+
// how hard an upswept style lifts. Only the styles that ask.
|
|
35
|
+
pomp: C.range(rng, 'pomp', .75, 1.35),
|
|
36
|
+
// where this head's whorl sits, nudged off the style's own spot —
|
|
37
|
+
// two heads with the same cut still comb differently
|
|
38
|
+
whorl: C.range(rng, 'whorl', -.35, .35),
|
|
39
|
+
// how long and how present the point strands are — the momiage in
|
|
40
|
+
// front of the ears, the loose fringe wisps, the flyaways
|
|
41
|
+
wisps: C.range(rng, 'wisps', .75, 1.3),
|
|
42
|
+
// ONE seed for every per-clump variation on the head. It has to be
|
|
43
|
+
// a rolled parameter rather than live randomness: hair is rebuilt
|
|
44
|
+
// on every boil frame, and anything re-rolled per frame shimmers.
|
|
45
|
+
seed: rng.r(0, 6.283),
|
|
46
|
+
// the tied-up styles
|
|
47
|
+
// Just BEHIND the ear — about a right angle off the nose. Tied
|
|
48
|
+
// further round than that the tail hangs behind the head and the
|
|
49
|
+
// whole silhouette is lost from the front, which is the only angle
|
|
50
|
+
// the sheet ever sees; tied forward of it, it covers the cheek.
|
|
51
|
+
tailAng: C.range(rng, 'tailAng', 1.45, 1.85), // radians off the face
|
|
52
|
+
tailY: C.range(rng, 'tailY', .5, .78), // how high it is tied
|
|
53
|
+
tailLen: C.range(rng, 'tailLen', .8, 1.9), // × head height
|
|
54
|
+
bunAng: C.range(rng, 'bunAng', .7, 1.15),
|
|
55
|
+
bunR: C.range(rng, 'bunR', .17, .26),
|
|
56
|
+
// THE AHOGE, and it is worth a knob of its own: one strand off the
|
|
57
|
+
// crown reads as a whole personality, so it is dealt often but not
|
|
58
|
+
// to everyone.
|
|
59
|
+
ahoge: C.chance(rng, 'ahoge', .22),
|
|
60
|
+
ahogeAng: rng.r(-.5, .5),
|
|
61
|
+
ahogeDir: rng.chance(.5) ? 1 : -1,
|
|
62
|
+
}),
|
|
63
|
+
|
|
64
|
+
meta: () => ({
|
|
65
|
+
style: { label: 'cut', pick: HAIR_STYLES },
|
|
66
|
+
color: { label: 'colour', pick: HAIR_IDS },
|
|
67
|
+
vol: { label: 'volume', range: [.5, 2] },
|
|
68
|
+
wave: { label: 'wave', range: [0, 2] },
|
|
69
|
+
part: { label: 'parting', range: [0, 2] },
|
|
70
|
+
density: { label: 'clumps', range: [.6, 1.6] },
|
|
71
|
+
width: { label: 'clump width', range: [.7, 1.4] },
|
|
72
|
+
jag: { label: 'ragged tips', range: [0, 2] },
|
|
73
|
+
pomp: { label: 'sweep up', range: [0, 2] },
|
|
74
|
+
whorl: { label: 'whorl', range: [-1, 1] },
|
|
75
|
+
wisps: { label: 'wisps', range: [.3, 2] },
|
|
76
|
+
seed: { label: 'shuffle', range: [0, 6.28] },
|
|
77
|
+
tailY: { label: 'tied at', range: [.2, .8] },
|
|
78
|
+
tailLen: { label: 'tail length', range: [.4, 2.4] },
|
|
79
|
+
tailAng: { label: 'tails apart', range: [1.2, 2.6] },
|
|
80
|
+
bunR: { label: 'bun size', range: [.1, .35] },
|
|
81
|
+
ahoge: { label: 'ahoge', bool: true },
|
|
82
|
+
}),
|
|
83
|
+
|
|
84
|
+
build(add, P, L) {
|
|
85
|
+
for (const piece of buildHair(P, L)) {
|
|
86
|
+
// a bun is a ball, and the body's own solid primitive already
|
|
87
|
+
// makes those — no reason for `ghair.js` to loft a sphere
|
|
88
|
+
if (piece.ball) {
|
|
89
|
+
add({ type: 'solid', id: piece.id, color: L.hair, cast: true, finish: 'hair',
|
|
90
|
+
rx: piece.ball.r, ry: piece.ball.r * .92, rz: piece.ball.r,
|
|
91
|
+
pos: [piece.ball.pos[0], piece.ball.pos[1] + L.cy, piece.ball.pos[2]] });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// hair is a real volume standing off the head, not a feature lying
|
|
95
|
+
// flush on it, so unlike every other face part it CASTS
|
|
96
|
+
add({ type: 'mesh', id: piece.id, mesh: piece.mesh,
|
|
97
|
+
pos: [0, L.cy, 0], color: L.hair, cast: true, finish: 'hair' });
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// HEADWEAR — the last of the extras and the loudest, because unlike a
|
|
2
|
+
// mark or a pair of glasses a hat changes the SILHOUETTE, which is the
|
|
3
|
+
// only thing that survives at sheet scale.
|
|
4
|
+
//
|
|
5
|
+
// Its relationship with the HAIR splits the catalogue in two, and
|
|
6
|
+
// getting that wrong is what a first pass does:
|
|
7
|
+
//
|
|
8
|
+
// HUG a beanie or a cap is PULLED ON, and a real one compresses
|
|
9
|
+
// the hair under it — it wants the head's own radius plus a
|
|
10
|
+
// little, and the hair poking out below the rim is the read.
|
|
11
|
+
// Sized to clear a big soft cut instead, it stands a whole
|
|
12
|
+
// hair's volume off the skull at every height, including
|
|
13
|
+
// down at the ears where there is no hair at all, and comes
|
|
14
|
+
// out as a bowl balanced on the head.
|
|
15
|
+
// PERCH a headband, a bow, a flower, a crown — these sit ON TOP of
|
|
16
|
+
// whatever is up there and genuinely need `L.hairTop`, which
|
|
17
|
+
// the hair publishes via `hairOuter`. Same edge as the eyes
|
|
18
|
+
// publishing `eyeProud` for the spectacles.
|
|
19
|
+
//
|
|
20
|
+
// The vocabulary is deliberately three shapes of thing:
|
|
21
|
+
//
|
|
22
|
+
// DOME a sphere cut short (`dome`) — a beanie, a cap's crown
|
|
23
|
+
// BAND a sphere ZONE, cut at both ends (`domeFrom`) — a brim, a
|
|
24
|
+
// cuff, a headband. This is what `domeFrom` was added for.
|
|
25
|
+
// PLATE the flat stuff — a bow, a flower, a crown's points
|
|
26
|
+
//
|
|
27
|
+
// Colour comes off the character's own palette (`warm`, `lite`, `ink`) rather
|
|
28
|
+
// than a new table. A hat in an unrelated colour is a different
|
|
29
|
+
// object; a hat in the family's colours is an accessory.
|
|
30
|
+
const STYLE = {
|
|
31
|
+
none: null,
|
|
32
|
+
// pulled down over the crown, with a rolled cuff at the rim
|
|
33
|
+
beanie: { dome: .42, cuff: [.38, .47], hug: 1.09, bare: true },
|
|
34
|
+
// a thin zone lying round the head: the headband
|
|
35
|
+
band: { band: [.26, .33], hug: 1.05, bare: true },
|
|
36
|
+
// points standing round the crown
|
|
37
|
+
crown: { points: 6, grow: 1.03 },
|
|
38
|
+
// one bow on the side of the head
|
|
39
|
+
bow: { bow: true, grow: 1.02 },
|
|
40
|
+
// and one flower, same place, quieter
|
|
41
|
+
flower: { bloom: true, grow: 1.02 },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const HAT_STYLES = Object.keys(STYLE);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Does this hat go on a BARE head?
|
|
48
|
+
*
|
|
49
|
+
* A beanie and a headband are pulled ON, and there is no arrangement of
|
|
50
|
+
* sizes that lets one share a skull with a big soft haircut: sized to
|
|
51
|
+
* clear the hair it is a bowl balanced on the head, sized to the skull
|
|
52
|
+
* it vanishes underneath, and squashing the hair under its rim — which
|
|
53
|
+
* was built, and worked — still leaves the two fighting for the same
|
|
54
|
+
* few millimetres at every seed. So they are simply worn on a bare
|
|
55
|
+
* head, which is a real look and needs no negotiation at all.
|
|
56
|
+
*
|
|
57
|
+
* `ghair.js` reads this off the LAYOUT and builds nothing. The hats
|
|
58
|
+
* that PERCH (a bow, a flower, a crown) sit on top of the hair as
|
|
59
|
+
* before and are not listed here.
|
|
60
|
+
*/
|
|
61
|
+
export function hatBare(P) {
|
|
62
|
+
return !!STYLE[P.hat?.style]?.bare;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const Hat = {
|
|
66
|
+
// after the hair, whose outer radius it needs
|
|
67
|
+
id: 'hat', label: 'hat', order: 3,
|
|
68
|
+
|
|
69
|
+
// Rare. A hat is a strong statement and the sheet can carry a few.
|
|
70
|
+
gen: (rng, C) => ({
|
|
71
|
+
// NOBODY wears a hat unless a species asks — see the header. A bear
|
|
72
|
+
// in a beanie is one line away and it is not one anybody wanted.
|
|
73
|
+
style: C.pick(rng, 'style', [['none', 100]]),
|
|
74
|
+
// WHICH accessory colour. The layout turns this into a hex that is
|
|
75
|
+
// guaranteed to stand clear of the skin and the hair.
|
|
76
|
+
accIx: rng.ri(0, 9),
|
|
77
|
+
size: C.range(rng, 'size', .94, 1.12),
|
|
78
|
+
// which side a bow or a flower sits on, and how far round
|
|
79
|
+
side: rng.chance(.5) ? 1 : -1,
|
|
80
|
+
lean: C.range(rng, 'lean', .7, 1.15),
|
|
81
|
+
}),
|
|
82
|
+
|
|
83
|
+
meta: () => ({
|
|
84
|
+
style: { label: 'style', pick: HAT_STYLES },
|
|
85
|
+
accIx: { label: 'colour', range: [0, 9] },
|
|
86
|
+
size: { label: 'size', range: [.8, 1.3] },
|
|
87
|
+
lean: { label: 'placement', range: [.4, 1.5] },
|
|
88
|
+
}),
|
|
89
|
+
|
|
90
|
+
build(add, P, L) {
|
|
91
|
+
const T = P.hat;
|
|
92
|
+
const st = STYLE[T.style];
|
|
93
|
+
if (!st) return;
|
|
94
|
+
const col = L.acc;
|
|
95
|
+
// An accessory is NOT made of the character: left to inherit the shell's
|
|
96
|
+
// finish a beanie on a humanoid is poured in SKIN. `acc` is the
|
|
97
|
+
// soft plastic they all wear, and its sheen is self-coloured —
|
|
98
|
+
// rubber's white one washed a brick red out to pale pink.
|
|
99
|
+
const fin = 'acc';
|
|
100
|
+
// HUG the head and let the hair squash under it, or PERCH on top
|
|
101
|
+
// of whatever the hair is doing — see the header
|
|
102
|
+
const k = (st.hug ?? (L.hairTop * st.grow)) * T.size;
|
|
103
|
+
const rx = L.rx * k, ry = L.ry * k, rz = L.rz * k;
|
|
104
|
+
const solid = (id, extra) => add({
|
|
105
|
+
type: 'solid', id, rx, ry, rz, exp: L.exp, cast: true, finish: fin,
|
|
106
|
+
pos: [0, L.cy, 0], color: col, ...extra });
|
|
107
|
+
|
|
108
|
+
if (st.dome) {
|
|
109
|
+
solid('hat', { dome: st.dome });
|
|
110
|
+
// the CUFF: a zone, slightly fatter than the crown it rolls
|
|
111
|
+
// under, which is the whole read of a knitted brim
|
|
112
|
+
if (st.cuff)
|
|
113
|
+
add({ type: 'solid', id: 'hatCuff', exp: L.exp, cast: true,
|
|
114
|
+
rx: rx * 1.045, ry: ry * 1.03, rz: rz * 1.045,
|
|
115
|
+
dome: st.cuff[1], domeFrom: st.cuff[0], finish: fin,
|
|
116
|
+
pos: [0, L.cy, 0], color: col });
|
|
117
|
+
}
|
|
118
|
+
if (st.band)
|
|
119
|
+
add({ type: 'solid', id: 'hat', exp: L.exp, cast: true,
|
|
120
|
+
rx, ry, rz, dome: st.band[1], domeFrom: st.band[0], finish: fin,
|
|
121
|
+
pos: [0, L.cy, 0], color: col });
|
|
122
|
+
|
|
123
|
+
// THE CROWN — points standing round the top on `L.top`, the same
|
|
124
|
+
// anchor the ears use
|
|
125
|
+
if (st.points) {
|
|
126
|
+
for (let i = 0; i < st.points; i++) {
|
|
127
|
+
const t = (i / (st.points - 1) - .5) * 1.5;
|
|
128
|
+
const a = L.top(t);
|
|
129
|
+
const u = L.s * .16 * T.size;
|
|
130
|
+
add({ type: 'plate', id: 'hatPoint' + i, outline: 'tri', flip: true,
|
|
131
|
+
w: u * .5, h: u, p: a.p, n: [a.n[0] * .5, a.n[1] * .5, 1],
|
|
132
|
+
roll: -t * .5, d: u * .35, bevel: u * .16,
|
|
133
|
+
offset: [0, u * .35], color: col, cast: true, finish: fin });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// A BOW or a FLOWER, off to one side of the crown — the two extras
|
|
138
|
+
// that are placed rather than worn
|
|
139
|
+
if (st.bow || st.bloom) {
|
|
140
|
+
const a = L.top(T.side * T.lean * .62);
|
|
141
|
+
const u = L.s * (st.bow ? .2 : .17) * T.size;
|
|
142
|
+
const n = [a.n[0] * .6, a.n[1] * .6, .8];
|
|
143
|
+
if (st.bloom) {
|
|
144
|
+
add({ type: 'plate', id: 'hatBloom', outline: 'flower', petals: 5, amp: .34,
|
|
145
|
+
w: u, h: u, p: a.p, n, d: u * .3, bevel: u * .14,
|
|
146
|
+
proud: u * .1, color: col, cast: true, finish: fin });
|
|
147
|
+
add({ type: 'plate', id: 'hatBloomEye', outline: 'ellipse',
|
|
148
|
+
w: u * .3, h: u * .3, p: a.p, n, d: u * .2, bevel: u * .09,
|
|
149
|
+
proud: u * .34, color: L.sclera, finish: fin });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// two loops and a knot: the cheapest bow that reads as one
|
|
153
|
+
for (const s of [-1, 1])
|
|
154
|
+
add({ type: 'plate', id: 'hatBow' + (s < 0 ? 'L' : 'R'), outline: 'tri',
|
|
155
|
+
w: u * .5, h: u * .8, p: a.p, n, roll: s * Math.PI / 2,
|
|
156
|
+
offset: [s * u * .62, 0], d: u * .3, bevel: u * .13,
|
|
157
|
+
proud: u * .08, color: col, cast: true, finish: fin });
|
|
158
|
+
add({ type: 'plate', id: 'hatKnot', outline: 'ellipse',
|
|
159
|
+
w: u * .26, h: u * .26, p: a.p, n,
|
|
160
|
+
d: u * .3, bevel: u * .13, proud: u * .2, color: col, finish: fin });
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The registry. Order is build order, and the only hard requirement is
|
|
2
|
+
// that `body` runs first — the layout it establishes is what every
|
|
3
|
+
// face part places against.
|
|
4
|
+
//
|
|
5
|
+
// Adding a face part = one file here + one line below. It never
|
|
6
|
+
// touches the rig, the shapes it can use are in `gshape.js`, and it
|
|
7
|
+
// places itself through `L.at`.
|
|
8
|
+
import { Body } from './body.js';
|
|
9
|
+
import { Frame } from './frame.js';
|
|
10
|
+
import { Crest } from './crest.js';
|
|
11
|
+
import { Hair } from './hair.js';
|
|
12
|
+
import { Eyes } from './eyes.js';
|
|
13
|
+
import { Brows } from './brows.js';
|
|
14
|
+
import { Nose } from './nose.js';
|
|
15
|
+
import { Mouth } from './mouth.js';
|
|
16
|
+
import { Blush } from './blush.js';
|
|
17
|
+
// THE EXTRAS. Everything above is what a character IS; these are what it is
|
|
18
|
+
// WEARING or what has happened to it, and all three are dealt rare on
|
|
19
|
+
// purpose — an accessory on every character is not a character, it is the
|
|
20
|
+
// house style.
|
|
21
|
+
import { Specs } from './specs.js';
|
|
22
|
+
import { Hat } from './hat.js';
|
|
23
|
+
import { Mark } from './mark.js';
|
|
24
|
+
|
|
25
|
+
// There WAS a `muzzle` part here — a second storey on the lower face
|
|
26
|
+
// in patch, lump and snout. All three are gone. The flat patch read as
|
|
27
|
+
// a white block behind the mouth, and the two solid ones pushed the
|
|
28
|
+
// face out into a snout that fought the head's own silhouette. What
|
|
29
|
+
// carries a species turned out to be the EARS and the eye patches, not
|
|
30
|
+
// a second lump of geometry.
|
|
31
|
+
export const GPARTS = [Body, Frame, Crest, Hair, Hat, Eyes, Brows, Specs, Nose, Mouth,
|
|
32
|
+
Blush, Mark];
|
|
33
|
+
export const GPART_BY_ID = Object.fromEntries(GPARTS.map(p => [p.id, p]));
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// MARKS — the small stuff painted or stuck on a face: a scar, a
|
|
2
|
+
// plaster, freckles, a beauty spot, ink tears. The drawn generator has
|
|
3
|
+
// had these since the beginning (`src/parts/extras.js`) and they do the
|
|
4
|
+
// same job here: they are the difference between a mascot and a
|
|
5
|
+
// character, and they cost almost nothing.
|
|
6
|
+
//
|
|
7
|
+
// EVERY ONE IS ASYMMETRIC OR SCATTERED, on purpose. A mark mirrored
|
|
8
|
+
// neatly onto both cheeks stops reading as something that HAPPENED to
|
|
9
|
+
// this character and starts reading as decoration that came out of the mould
|
|
10
|
+
// — which is the one thing a scar must not do. So a scar takes a side,
|
|
11
|
+
// freckles are a scatter, and only `tears` is a pair (it is a face
|
|
12
|
+
// doing something, not a mark).
|
|
13
|
+
//
|
|
14
|
+
// Marks lie FLAT: barely proud, no bevel to speak of. They are the one
|
|
15
|
+
// family in the lab that is painted rather than moulded, and a mark
|
|
16
|
+
// with a fat rounded rim reads as a lump of vinyl stuck on a cheek.
|
|
17
|
+
const STYLE = {
|
|
18
|
+
none: null,
|
|
19
|
+
// one stroke across a brow, and a second short one crossing it
|
|
20
|
+
scar: { kind: 'scar', ticks: 1 },
|
|
21
|
+
// the stitched version: a stroke with rungs
|
|
22
|
+
sutures: { kind: 'scar', ticks: 4 },
|
|
23
|
+
// a plaster: a pad with a lighter middle
|
|
24
|
+
plaster: { kind: 'plaster' },
|
|
25
|
+
// a scatter across the nose and both cheeks
|
|
26
|
+
freckles: { kind: 'dots', n: 7, size: .09, spread: 1 },
|
|
27
|
+
// one small dot, high on a cheek
|
|
28
|
+
spot: { kind: 'dots', n: 1, size: .13, spread: 0 },
|
|
29
|
+
// two ink streaks running down from the eyes — the drawn rig's
|
|
30
|
+
// black tears, and the only mark here that is a pair
|
|
31
|
+
tears: { kind: 'tears' },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const MARK_STYLES = Object.keys(STYLE);
|
|
35
|
+
|
|
36
|
+
export const Mark = {
|
|
37
|
+
// LAST. It goes over everything, including the blush.
|
|
38
|
+
id: 'mark', label: 'marks', order: 6,
|
|
39
|
+
|
|
40
|
+
gen: (rng, C) => ({
|
|
41
|
+
style: C.pick(rng, 'style', [['none', 82], ['freckles', 5], ['scar', 3],
|
|
42
|
+
['plaster', 3], ['spot', 3], ['sutures', 2],
|
|
43
|
+
['tears', 2]]),
|
|
44
|
+
side: rng.chance(.5) ? 1 : -1,
|
|
45
|
+
size: C.range(rng, 'size', .85, 1.2),
|
|
46
|
+
roll: rng.r(-.5, .5),
|
|
47
|
+
// a scar over the brow reads as a fight; the same scar on the
|
|
48
|
+
// cheek reads as a scratch. Worth having both.
|
|
49
|
+
high: rng.chance(.55),
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
meta: () => ({
|
|
53
|
+
style: { label: 'style', pick: MARK_STYLES },
|
|
54
|
+
size: { label: 'size', range: [.5, 1.8] },
|
|
55
|
+
roll: { label: 'tilt', range: [-1.2, 1.2] },
|
|
56
|
+
high: { label: 'over the brow', bool: true },
|
|
57
|
+
}),
|
|
58
|
+
|
|
59
|
+
build(add, P, L) {
|
|
60
|
+
const M = P.mark;
|
|
61
|
+
const st = STYLE[M.style];
|
|
62
|
+
if (!st) return;
|
|
63
|
+
const r = L.eyeR * M.size;
|
|
64
|
+
const sd = M.side;
|
|
65
|
+
// a plaster is not made of skin either — see `acc` in gmedia.js
|
|
66
|
+
const fin = 'acc';
|
|
67
|
+
|
|
68
|
+
// ---- SCAR / SUTURES ------------------------------------------------
|
|
69
|
+
if (st.kind === 'scar') {
|
|
70
|
+
const ay = M.high ? L.eyeY + .34 : L.eyeY - .3;
|
|
71
|
+
const a = L.onFace(sd * (L.eyeX + .06), ay, { halfW: r * .5 });
|
|
72
|
+
const len = r * .58, tube = r * .055;
|
|
73
|
+
add({ type: 'plate', id: 'markScar', outline: 'band', curve: 'line',
|
|
74
|
+
w: len, h: tube, tube,
|
|
75
|
+
p: a.p, n: a.n, roll: M.roll + sd * .5,
|
|
76
|
+
d: tube * 1.2, bevel: tube * .5, proud: tube * .3, color: L.ink });
|
|
77
|
+
// the rungs. One is a crossed scar; four is a stitched seam.
|
|
78
|
+
for (let i = 0; i < st.ticks; i++) {
|
|
79
|
+
const t = st.ticks === 1 ? 0 : (i / (st.ticks - 1) - .5) * 1.3;
|
|
80
|
+
add({ type: 'plate', id: 'markTick' + i, outline: 'band', curve: 'line',
|
|
81
|
+
w: tube * 3.2, h: tube * .8, tube: tube * .8,
|
|
82
|
+
p: a.p, n: a.n, roll: M.roll + sd * .5 + Math.PI / 2,
|
|
83
|
+
offset: [t * len * .78, 0],
|
|
84
|
+
d: tube, bevel: tube * .4, proud: tube * .3, color: L.ink });
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---- PLASTER -------------------------------------------------------
|
|
90
|
+
if (st.kind === 'plaster') {
|
|
91
|
+
const a = L.onFace(sd * (L.eyeX + .1), L.eyeY - .34, { halfW: r * .62 });
|
|
92
|
+
const w = r * .62, h = r * .26;
|
|
93
|
+
add({ type: 'plate', id: 'markPlaster', outline: 'rect',
|
|
94
|
+
w, h, r: h * .5,
|
|
95
|
+
p: a.p, n: a.n, roll: M.roll + sd * .38,
|
|
96
|
+
d: r * .07, bevel: r * .035, proud: r * .03, color: L.sclera, finish: fin });
|
|
97
|
+
// the pad: the darker middle every plaster has
|
|
98
|
+
add({ type: 'plate', id: 'markPad', outline: 'rect',
|
|
99
|
+
w: w * .38, h: h * .62, r: h * .2,
|
|
100
|
+
p: a.p, n: a.n, roll: M.roll + sd * .38,
|
|
101
|
+
d: r * .05, bevel: r * .02, proud: r * .055, color: L.warm, finish: fin });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- DOTS: freckles, or one beauty spot ----------------------------
|
|
106
|
+
if (st.kind === 'dots') {
|
|
107
|
+
const rr = r * st.size;
|
|
108
|
+
for (let i = 0; i < st.n; i++) {
|
|
109
|
+
// A FIXED scatter, not a rolled one: marks are rebuilt every
|
|
110
|
+
// boil frame, so anything random here would crawl across the
|
|
111
|
+
// face. Two harmonics give a spread that does not read as a row.
|
|
112
|
+
const u = st.n === 1 ? 0 : i / (st.n - 1) * 2 - 1;
|
|
113
|
+
const ax = st.spread
|
|
114
|
+
? u * (L.eyeX + .34) + Math.sin(i * 2.4) * .06
|
|
115
|
+
: sd * (L.eyeX + .3);
|
|
116
|
+
const ay = st.spread
|
|
117
|
+
? L.eyeY - .3 + Math.sin(i * 3.9) * .1
|
|
118
|
+
: L.eyeY - .16;
|
|
119
|
+
const a = L.onFace(ax, ay, { halfW: rr });
|
|
120
|
+
add({ type: 'plate', id: 'markDot' + i, outline: 'ellipse',
|
|
121
|
+
w: rr, h: rr, p: a.p, n: a.n,
|
|
122
|
+
d: rr * .5, bevel: rr * .25, proud: rr * .1,
|
|
123
|
+
color: st.n === 1 ? L.ink : L.warm });
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---- TEARS ---------------------------------------------------------
|
|
129
|
+
// straight down from under each eye, and long: a short one reads as
|
|
130
|
+
// a smudge rather than as something running
|
|
131
|
+
for (const [id, side] of [['markTearL', -1], ['markTearR', 1]]) {
|
|
132
|
+
const a = L.onFace(side * L.eyeX, L.eyeY - .34, { halfW: r * .09 });
|
|
133
|
+
add({ type: 'plate', id, outline: 'band', curve: 'line',
|
|
134
|
+
w: r * .5, h: r * .085, tube: r * .085,
|
|
135
|
+
p: a.p, n: a.n, roll: Math.PI / 2,
|
|
136
|
+
d: r * .06, bevel: r * .03, proud: r * .03, color: L.ink });
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// THE MOUTH — two families now, and the split is the whole reference:
|
|
2
|
+
//
|
|
3
|
+
// LINES — a stroke along a centreline (smile, cat, zig…). Small,
|
|
4
|
+
// ink, the pastel-egg register. These are what we had.
|
|
5
|
+
//
|
|
6
|
+
// MAWS — a CONSTRUCTED mouth: an ink outline with a dark maroon
|
|
7
|
+
// interior behind it, and optionally a white strip of teeth hanging
|
|
8
|
+
// from the top lip and a warm tongue rising from the bottom. This is
|
|
9
|
+
// the Ferriz read — the shark grin, the zombie teeth, the huge
|
|
10
|
+
// open holler — and it is allowed to be ENORMOUS: half the face and
|
|
11
|
+
// more. The earlier small-and-black rule was for the line family,
|
|
12
|
+
// and the maws are exactly the exception it was waiting for.
|
|
13
|
+
//
|
|
14
|
+
// A maw is stacked plates, nothing else — the same trick as a pupil
|
|
15
|
+
// on a white. Interior parked behind the outline, furniture parked in
|
|
16
|
+
// front of the interior.
|
|
17
|
+
const STYLE = {
|
|
18
|
+
none: null,
|
|
19
|
+
// -- lines: small, ink -------------------------------------------
|
|
20
|
+
smile: { line: true, curve: 'arc', sag: .42, wf: 1, tf: .15 },
|
|
21
|
+
frown: { line: true, curve: 'arc', sag: -.4, wf: .9, tf: .15 },
|
|
22
|
+
flat: { line: true, curve: 'line', sag: 0, wf: .8, tf: .14 },
|
|
23
|
+
cat: { line: true, curve: 'wave', sag: .34, wf: 1.05, tf: .14 },
|
|
24
|
+
zig: { line: true, curve: 'zig', sag: .34, wf: 1, tf: .12 },
|
|
25
|
+
// -- maws: big, built --------------------------------------------
|
|
26
|
+
// mul is the licence to go big: it scales the whole construction
|
|
27
|
+
// `mul` is the licence to go big — but it was written when the eye
|
|
28
|
+
// unit was half this size, and everything here scales off that unit.
|
|
29
|
+
// At 2.3 a maw came out spanning the whole face.
|
|
30
|
+
open: { maw: true, wf: 1, hf: .78, mul: 1.42 },
|
|
31
|
+
holler: { maw: true, wf: .8, hf: 1.05, mul: 1.45 }, // taller than wide
|
|
32
|
+
grin: { maw: true, wf: 1.3, hf: .52, mul: 1.5, allTeeth: true },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const MOUTH_STYLES = Object.keys(STYLE);
|
|
36
|
+
|
|
37
|
+
/** how far the mouth reaches above its own centre, in units of
|
|
38
|
+
* `L.eyeR`. The layout asks, the same way it asks `eyeReach`, so a
|
|
39
|
+
* maw two eyes tall still gets pushed clear of the eyes. */
|
|
40
|
+
export function mouthReach(P) {
|
|
41
|
+
const st = STYLE[P.mouth.style];
|
|
42
|
+
if (!st) return 0;
|
|
43
|
+
return (st.line ? st.tf * 1.4 : st.hf) * (st.mul ?? 1) * P.mouth.size;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** and sideways: the mouth's half-width in units of `L.eyeR`. A maw on
|
|
47
|
+
* a narrow chin is the layout's problem in BOTH axes. */
|
|
48
|
+
export function mouthSpan(P) {
|
|
49
|
+
const st = STYLE[P.mouth.style];
|
|
50
|
+
if (!st) return 0;
|
|
51
|
+
return (st.wf ?? 1) * (st.mul ?? 1) * P.mouth.size;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const Mouth = {
|
|
55
|
+
id: 'mouth', label: 'mouth', order: 5,
|
|
56
|
+
|
|
57
|
+
gen: (rng, C) => ({
|
|
58
|
+
style: C.pick(rng, 'style', [['open', 18], ['smile', 16], ['grin', 12], ['cat', 12],
|
|
59
|
+
['flat', 10], ['holler', 9], ['zig', 7], ['frown', 6],
|
|
60
|
+
['none', 10]]),
|
|
61
|
+
// close under the eyes: the face reads as one band of features,
|
|
62
|
+
// not a pair of eyes with a mouth adrift somewhere below them
|
|
63
|
+
y: C.range(rng, 'y', -.2, -.02), // a wish — the layout may push it down
|
|
64
|
+
size: C.range(rng, 'size', .5, .78), // × eye size, × the style's own mul
|
|
65
|
+
proud: C.range(rng, 'proud', .08, .2),
|
|
66
|
+
teeth: C.chance(rng, 'teeth', .6), // a maw's white strip
|
|
67
|
+
tongue: C.chance(rng, 'tongue', .45), // and its warm tongue
|
|
68
|
+
}),
|
|
69
|
+
|
|
70
|
+
meta: () => ({
|
|
71
|
+
style: { label: 'style', pick: MOUTH_STYLES },
|
|
72
|
+
y: { label: 'height', range: [-.8, .1] },
|
|
73
|
+
size: { label: 'size', range: [.4, 2] },
|
|
74
|
+
proud: { label: 'relief', range: [0, .5] },
|
|
75
|
+
teeth: { label: 'teeth', bool: true },
|
|
76
|
+
tongue: { label: 'tongue', bool: true },
|
|
77
|
+
}),
|
|
78
|
+
|
|
79
|
+
build(add, P, L) {
|
|
80
|
+
const M = P.mouth;
|
|
81
|
+
const st = STYLE[M.style];
|
|
82
|
+
if (!st) return;
|
|
83
|
+
const r = L.eyeR * M.size * (st.mul ?? 1) * L.mouthFit;
|
|
84
|
+
const a = L.at(0, L.mouthY);
|
|
85
|
+
const base = r * M.proud * (st.line ? 1 : .4);
|
|
86
|
+
|
|
87
|
+
if (st.line) {
|
|
88
|
+
add({ type: 'plate', id: 'mouth', outline: 'band',
|
|
89
|
+
curve: st.curve, sag: st.sag * r, tube: r * st.tf,
|
|
90
|
+
w: r * st.wf, h: r * st.tf,
|
|
91
|
+
p: a.p, n: a.n,
|
|
92
|
+
d: r * .22, bevel: r * st.tf * .5,
|
|
93
|
+
proud: base, color: L.ink });
|
|
94
|
+
// ONE white fang hanging off the line, point down and a little
|
|
95
|
+
// off-centre — the single tooth is a whole personality
|
|
96
|
+
if (M.teeth && M.tongue) {
|
|
97
|
+
add({ type: 'plate', id: 'mouthFang', outline: 'tri',
|
|
98
|
+
w: r * .16, h: r * .22,
|
|
99
|
+
p: a.p, n: a.n,
|
|
100
|
+
d: r * .18, bevel: r * .05,
|
|
101
|
+
proud: base + r * .05, offset: [r * st.wf * .38, -r * .2],
|
|
102
|
+
color: L.sclera });
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---- a maw ------------------------------------------------------
|
|
108
|
+
const w = r * st.wf, h = r * st.hf, d = r * .16;
|
|
109
|
+
const corner = Math.min(w, h) * .8;
|
|
110
|
+
|
|
111
|
+
// the outline: the drawn lip line around everything
|
|
112
|
+
add({ type: 'plate', id: 'mouth', outline: 'rect',
|
|
113
|
+
w, h, r: corner,
|
|
114
|
+
p: a.p, n: a.n, d, bevel: d * .5,
|
|
115
|
+
proud: base, color: L.ink });
|
|
116
|
+
|
|
117
|
+
// the interior, behind the lip line
|
|
118
|
+
add({ type: 'plate', id: 'mouthMaw', outline: 'rect',
|
|
119
|
+
w: w * .84, h: h * .8, r: corner * .8,
|
|
120
|
+
p: a.p, n: a.n, d: d * .8, bevel: d * .3,
|
|
121
|
+
proud: base + d * .5, color: st.allTeeth ? L.sclera : L.maw });
|
|
122
|
+
|
|
123
|
+
if (st.allTeeth) {
|
|
124
|
+
// the grin: the interior IS the teeth, split by ink bars
|
|
125
|
+
const bars = w > h * 1.8 ? 3 : 2;
|
|
126
|
+
for (let i = 0; i < bars; i++) {
|
|
127
|
+
const fx = (i + 1) / (bars + 1) * 2 - 1;
|
|
128
|
+
add({ type: 'plate', id: `mouthBar${i}`, outline: 'rect',
|
|
129
|
+
w: w * .035, h: h * .68, r: w * .03,
|
|
130
|
+
p: a.p, n: a.n, d: d * .5, bevel: d * .12,
|
|
131
|
+
proud: base + d * .95, offset: [fx * w * .8, 0],
|
|
132
|
+
color: L.ink });
|
|
133
|
+
}
|
|
134
|
+
// and one horizontal split, upper teeth from lower
|
|
135
|
+
add({ type: 'plate', id: 'mouthSplit', outline: 'rect',
|
|
136
|
+
w: w * .8, h: h * .05, r: h * .04,
|
|
137
|
+
p: a.p, n: a.n, d: d * .5, bevel: d * .12,
|
|
138
|
+
proud: base + d * .95, offset: [0, -h * .06],
|
|
139
|
+
color: L.ink });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (M.teeth) {
|
|
144
|
+
// Hanging FROM the top lip, and SPLIT. One white slab in a dark
|
|
145
|
+
// mouth reads as a sticking plaster however it is placed — what
|
|
146
|
+
// makes it teeth is the divisions, which is exactly why `grin`
|
|
147
|
+
// was the best-reading mouth on the sheet. Same trick here.
|
|
148
|
+
const tw = w * .62, th = h * .3, ty = h * .8 - th;
|
|
149
|
+
add({ type: 'plate', id: 'mouthTeeth', outline: 'rect',
|
|
150
|
+
w: tw, h: th, r: th * .16,
|
|
151
|
+
p: a.p, n: a.n, d: d * .6, bevel: d * .15,
|
|
152
|
+
proud: base + d * .95, offset: [0, ty],
|
|
153
|
+
color: L.sclera });
|
|
154
|
+
for (let i = 0; i < 3; i++) {
|
|
155
|
+
add({ type: 'plate', id: `mouthTooth${i}`, outline: 'rect',
|
|
156
|
+
w: tw * .035, h: th * .82, r: tw * .03,
|
|
157
|
+
p: a.p, n: a.n, d: d * .4, bevel: d * .1,
|
|
158
|
+
proud: base + d * 1.15, offset: [(i - 1) * tw * .46, ty],
|
|
159
|
+
color: L.ink });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (M.tongue) {
|
|
163
|
+
add({ type: 'plate', id: 'mouthTongue', outline: 'ellipse',
|
|
164
|
+
w: w * .42, h: h * .42,
|
|
165
|
+
p: a.p, n: a.n, d: d * .8, bevel: d * .3,
|
|
166
|
+
proud: base + d * .8, offset: [0, -h * .52],
|
|
167
|
+
color: L.warm });
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// THE NOSE. Small, central, and the one feature that is sometimes a
|
|
2
|
+
// real lump rather than a plate: a button nose wants to be a ball,
|
|
3
|
+
// because half of what sells it is the highlight sliding around a
|
|
4
|
+
// curve the flat features do not have.
|
|
5
|
+
const STYLE = {
|
|
6
|
+
none: null,
|
|
7
|
+
dot: { type: 'plate', outline: 'ellipse', wf: .3, hf: .3 },
|
|
8
|
+
button: { type: 'solid', wf: .42, hf: .42, sink: .45 },
|
|
9
|
+
snout: { type: 'plate', outline: 'rect', wf: .8, hf: .38 },
|
|
10
|
+
beak: { type: 'plate', outline: 'tri', wf: .46, hf: .42, flip: true },
|
|
11
|
+
cat: { type: 'plate', outline: 'tri', wf: .42, hf: .3, flip: true },
|
|
12
|
+
heart: { type: 'plate', outline: 'heart', wf: .38, hf: .34 },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const NOSE_STYLES = Object.keys(STYLE);
|
|
16
|
+
|
|
17
|
+
export const Nose = {
|
|
18
|
+
id: 'nose', label: 'nose', order: 3,
|
|
19
|
+
|
|
20
|
+
// Most characters have NO nose. Two eyes and a mouth is the whole face in
|
|
21
|
+
// the reference, and a nose on every one of them turns a sheet of
|
|
22
|
+
// characters into a sheet of the same character.
|
|
23
|
+
gen: (rng, C) => ({
|
|
24
|
+
style: C.pick(rng, 'style', [['none', 91], ['dot', 3], ['button', 2], ['snout', 1],
|
|
25
|
+
['cat', 1], ['beak', 1], ['heart', 1]]),
|
|
26
|
+
bias: C.range(rng, 'bias', -.04, .04), // off the midpoint the layout publishes
|
|
27
|
+
size: C.range(rng, 'size', .42, .66), // × eye size
|
|
28
|
+
warm: C.chance(rng, 'warm', .35), // in the palette's warm colour, not ink
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
meta: () => ({
|
|
32
|
+
style: { label: 'style', pick: NOSE_STYLES },
|
|
33
|
+
bias: { label: 'height', range: [-.2, .2] },
|
|
34
|
+
size: { label: 'size', range: [.3, 1.8] },
|
|
35
|
+
warm: { label: 'warm tone', bool: true },
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
build(add, P, L) {
|
|
39
|
+
const N = P.nose;
|
|
40
|
+
const st = STYLE[N.style];
|
|
41
|
+
if (!st) return;
|
|
42
|
+
// the layout's midpoint between eyes and mouth: a nose cannot
|
|
43
|
+
// collide with a mouth however either of them is dragged
|
|
44
|
+
const r = L.eyeR * N.size;
|
|
45
|
+
const a = L.at(0, L.noseY + N.bias);
|
|
46
|
+
const color = N.warm ? L.warm : L.ink;
|
|
47
|
+
|
|
48
|
+
const base = 0;
|
|
49
|
+
|
|
50
|
+
if (st.type === 'solid') {
|
|
51
|
+
// sunk most of the way in, so it reads as a bump on the face and
|
|
52
|
+
// not a bead balanced on it
|
|
53
|
+
add({ type: 'solid', id: 'nose', rx: r * st.wf, ry: r * st.hf, rz: r * st.wf,
|
|
54
|
+
p: a.p, n: a.n, proud: base - r * st.wf * st.sink, color });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
add({ type: 'plate', id: 'nose', outline: st.outline, flip: st.flip,
|
|
58
|
+
w: r * st.wf, h: r * st.hf,
|
|
59
|
+
p: a.p, n: a.n,
|
|
60
|
+
d: r * .3, bevel: Math.min(r * .18, r * st.hf * .45),
|
|
61
|
+
proud: base + r * .12, color });
|
|
62
|
+
},
|
|
63
|
+
};
|