@voqalize/avatar 0.2.1 → 0.3.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 -661
- package/README.md +144 -663
- package/client/Avatar.tsx +57 -0
- package/client/AvatarClient.ts +523 -0
- package/client/arjun.ts +24 -0
- package/client/createAvatar.ts +139 -0
- package/client/createCanvasAvatar.ts +72 -0
- package/client/index.ts +39 -0
- package/client/internal.ts +80 -0
- package/client/interviewer-female.ts +4 -0
- package/client/interviewer-male.ts +4 -0
- package/client/ishita.ts +24 -0
- package/client/kabir.ts +24 -0
- package/client/meera.ts +24 -0
- package/client/naina.ts +24 -0
- package/client/professional-female-a.ts +4 -0
- package/client/professional-female-b.ts +4 -0
- package/client/professional-male-a.ts +4 -0
- package/client/professional-male-b.ts +4 -0
- package/client/react.ts +13 -0
- package/client/types.ts +146 -0
- package/client/useAvatar.ts +74 -0
- package/client/vikram.ts +24 -0
- package/dist/Avatar.d.ts +44 -0
- package/dist/Avatar.d.ts.map +1 -0
- package/{client/dist → dist}/Avatar.js +2 -2
- package/dist/Avatar.js.map +1 -0
- package/dist/AvatarClient.d.ts +196 -0
- package/dist/AvatarClient.d.ts.map +1 -0
- package/dist/AvatarClient.js +450 -0
- package/dist/AvatarClient.js.map +1 -0
- package/dist/arjun.d.ts +6 -0
- package/dist/arjun.d.ts.map +1 -0
- package/dist/arjun.js +18 -0
- package/dist/arjun.js.map +1 -0
- package/dist/createAvatar.d.ts +99 -0
- package/dist/createAvatar.d.ts.map +1 -0
- package/dist/createAvatar.js +75 -0
- package/dist/createAvatar.js.map +1 -0
- package/dist/createCanvasAvatar.d.ts +22 -0
- package/dist/createCanvasAvatar.d.ts.map +1 -0
- package/dist/createCanvasAvatar.js +47 -0
- package/dist/createCanvasAvatar.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +30 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +33 -0
- package/dist/internal.js.map +1 -0
- package/dist/interviewer-female.d.ts +4 -0
- package/dist/interviewer-female.d.ts.map +1 -0
- package/dist/interviewer-female.js +3 -0
- package/dist/interviewer-female.js.map +1 -0
- package/dist/interviewer-male.d.ts +4 -0
- package/dist/interviewer-male.d.ts.map +1 -0
- package/dist/interviewer-male.js +3 -0
- package/dist/interviewer-male.js.map +1 -0
- package/dist/ishita.d.ts +6 -0
- package/dist/ishita.d.ts.map +1 -0
- package/dist/ishita.js +18 -0
- package/dist/ishita.js.map +1 -0
- package/dist/kabir.d.ts +6 -0
- package/dist/kabir.d.ts.map +1 -0
- package/dist/kabir.js +18 -0
- package/dist/kabir.js.map +1 -0
- package/dist/meera.d.ts +6 -0
- package/dist/meera.d.ts.map +1 -0
- package/dist/meera.js +18 -0
- package/dist/meera.js.map +1 -0
- package/dist/naina.d.ts +6 -0
- package/dist/naina.d.ts.map +1 -0
- package/dist/naina.js +18 -0
- package/dist/naina.js.map +1 -0
- package/dist/professional-female-a.d.ts +4 -0
- package/dist/professional-female-a.d.ts.map +1 -0
- package/dist/professional-female-a.js +3 -0
- package/dist/professional-female-a.js.map +1 -0
- package/dist/professional-female-b.d.ts +4 -0
- package/dist/professional-female-b.d.ts.map +1 -0
- package/dist/professional-female-b.js +3 -0
- package/dist/professional-female-b.js.map +1 -0
- package/dist/professional-male-a.d.ts +4 -0
- package/dist/professional-male-a.d.ts.map +1 -0
- package/dist/professional-male-a.js +3 -0
- package/dist/professional-male-a.js.map +1 -0
- package/dist/professional-male-b.d.ts +4 -0
- package/dist/professional-male-b.d.ts.map +1 -0
- package/dist/professional-male-b.js +3 -0
- package/dist/professional-male-b.js.map +1 -0
- package/dist/react.d.ts +13 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +13 -0
- package/dist/react.js.map +1 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +93 -0
- package/dist/types.js.map +1 -0
- package/dist/useAvatar.d.ts +39 -0
- package/dist/useAvatar.d.ts.map +1 -0
- package/dist/useAvatar.js +35 -0
- package/dist/useAvatar.js.map +1 -0
- package/dist/vikram.d.ts +6 -0
- package/dist/vikram.d.ts.map +1 -0
- package/dist/vikram.js +18 -0
- package/dist/vikram.js.map +1 -0
- package/package.json +81 -24
- package/src/avatar.d.ts +161 -120
- package/src/avatar.js +234 -146
- package/src/behavior.d.ts +26 -0
- package/src/behavior.js +102 -0
- package/src/camera.js +29 -0
- package/src/canvas/author/parts/eye.mjs +722 -0
- package/src/canvas/author/parts/hand.mjs +1156 -0
- package/src/canvas/author/parts/mouth.mjs +741 -0
- package/src/canvas/author/parts/nose.mjs +100 -0
- package/src/canvas/author/parts/skin-detail.mjs +67 -0
- package/src/canvas/author/path.mjs +283 -0
- package/src/canvas/author/rig.mjs +405 -0
- package/src/canvas/avatars/round/face.d.mts +3 -0
- package/src/canvas/avatars/round/face.mjs +1307 -0
- package/src/canvas/create-rig.d.ts +15 -0
- package/src/canvas/create-rig.js +100 -0
- package/src/canvas/data/img/professional-female-a-hair-back.webp +0 -0
- package/src/canvas/data/img/professional-female-a-hair-front.webp +0 -0
- package/src/canvas/data/img/professional-female-a-top-body.webp +0 -0
- package/src/canvas/data/img/professional-female-b-hair-back.webp +0 -0
- package/src/canvas/data/img/professional-female-b-hair-front.webp +0 -0
- package/src/canvas/data/img/professional-female-b-top-body.webp +0 -0
- package/src/canvas/data/img/professional-male-a-hair-back.webp +0 -0
- package/src/canvas/data/img/professional-male-a-hair-front.webp +0 -0
- package/src/canvas/data/img/professional-male-a-top-body.webp +0 -0
- package/src/canvas/data/img/professional-male-b-hair-back.webp +0 -0
- package/src/canvas/data/img/professional-male-b-hair-front.webp +0 -0
- package/src/canvas/data/img/professional-male-b-top-body.webp +0 -0
- package/src/canvas/data/img/round-m3-hair-back.webp +0 -0
- package/src/canvas/data/img/round-m3-hair-front.webp +0 -0
- package/src/canvas/data/img/round-m3-top-body.webp +0 -0
- package/src/canvas/data/img/round-w1-hair-back.webp +0 -0
- package/src/canvas/data/img/round-w1-hair-front.webp +0 -0
- package/src/canvas/data/img/round-w1-top-body.webp +0 -0
- package/src/canvas/data/interviewer-female.rig.json +1 -0
- package/src/canvas/data/interviewer-male.rig.json +1 -0
- package/src/canvas/data/professional-female-a.rig.json +1 -0
- package/src/canvas/data/professional-female-b.rig.json +1 -0
- package/src/canvas/data/professional-male-a.rig.json +1 -0
- package/src/canvas/data/professional-male-b.rig.json +1 -0
- package/src/canvas/src/live.js +508 -0
- package/src/canvas/src/render2d.js +218 -0
- package/src/canvas/src/rig.js +297 -0
- package/src/canvas/src/vocab.js +96 -0
- package/src/clips.js +18 -7
- package/src/conformance.js +119 -0
- package/src/emotions.js +4 -3
- package/src/face-core.js +27 -1
- package/src/face-myna.d.ts +7 -0
- package/src/face-myna.js +192 -131
- package/src/face-peep-control-plane.js +167 -0
- package/src/face-peep.d.ts +7 -0
- package/src/face-peep.js +274 -116
- package/src/face-wren.d.ts +7 -0
- package/src/face-wren.js +17 -19
- package/src/faces.d.ts +12 -0
- package/src/faces.js +53 -0
- package/src/gaze.js +119 -4
- package/src/hand.js +120 -91
- package/src/idle.js +18 -101
- package/src/interjections.js +156 -31
- package/src/params.js +8 -4
- package/src/perform.js +7 -9
- package/src/rig.d.ts +24 -0
- package/src/rig.js +32 -0
- package/src/speech-timing.js +23 -0
- package/src/visemes.js +28 -12
- package/client/dist/Avatar.d.ts +0 -27
- package/client/dist/Avatar.d.ts.map +0 -1
- package/client/dist/Avatar.js.map +0 -1
- package/client/dist/AvatarClient.d.ts +0 -139
- package/client/dist/AvatarClient.d.ts.map +0 -1
- package/client/dist/AvatarClient.js +0 -223
- package/client/dist/AvatarClient.js.map +0 -1
- package/client/dist/index.d.ts +0 -22
- package/client/dist/index.d.ts.map +0 -1
- package/client/dist/index.js +0 -22
- package/client/dist/index.js.map +0 -1
- package/client/dist/types.d.ts +0 -86
- package/client/dist/types.d.ts.map +0 -1
- package/client/dist/types.js +0 -31
- package/client/dist/types.js.map +0 -1
- package/client/dist/useAvatar.d.ts +0 -42
- package/client/dist/useAvatar.d.ts.map +0 -1
- package/client/dist/useAvatar.js +0 -51
- package/client/dist/useAvatar.js.map +0 -1
- package/client/src/Avatar.tsx +0 -33
- package/client/src/AvatarClient.ts +0 -270
- package/client/src/index.ts +0 -22
- package/client/src/types.ts +0 -104
- package/client/src/useAvatar.ts +0 -85
- package/docs/contract-avatar.md +0 -371
- package/docs/contract-protocol.md +0 -461
package/src/face-myna.js
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The third line-art character, authored against a user-supplied reference
|
|
5
5
|
* asset (a raster illustration): a young contemporary Indian woman — wavy
|
|
6
|
-
* shoulder-length dark hair with paper highlight slashes,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* shoulder-length dark hair with paper highlight slashes, orange hoop
|
|
7
|
+
* earrings, a white crew tee under an open overshirt. Hand-authored geometry
|
|
8
|
+
* with the reference on screen; nothing is traced. Same idiom as peep and
|
|
9
|
+
* wren — no strokes anywhere, every mark a filled taper — and the same
|
|
10
|
+
* 30-channel contract underneath.
|
|
11
11
|
*
|
|
12
12
|
* What is deliberately different from the siblings, and why:
|
|
13
13
|
* - THE HAIR IS A HORSESHOE, NOT A CLOUD. One closed loop: wavy outer
|
|
@@ -20,10 +20,9 @@
|
|
|
20
20
|
* head-layer underlay (same loop at head parallax) backfills the ±3-unit
|
|
21
21
|
* slide between the two layers under yaw — same insurance as peep's, doing
|
|
22
22
|
* one extra job here.
|
|
23
|
-
* - THE
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* panels and everything overshoots the frame.
|
|
23
|
+
* - THE OVERSHIRT IS TWO PAPER PANELS over a paper tee, distinguished only by
|
|
24
|
+
* ink outline — no fill colour on the garment. The tee still runs
|
|
25
|
+
* edge-to-edge under both panels and everything overshoots the frame.
|
|
27
26
|
* - THE MOUTH HAS LIPS. The proven aperture-contour model, but the ring's
|
|
28
27
|
* width profile carries a cupid's-bow dip at the top centre and a fuller
|
|
29
28
|
* lower lip. (An under-lip shadow mark existed through v7; it was deleted —
|
|
@@ -44,59 +43,74 @@ import {
|
|
|
44
43
|
f, createFaceShell, faceApi, poseTransforms, pairedTeeth,
|
|
45
44
|
} from './face-core.js';
|
|
46
45
|
import { taper, taperRing, region } from './line-art.js';
|
|
46
|
+
import { viewBoxForHead } from './camera.js';
|
|
47
47
|
|
|
48
48
|
export const THEME = {
|
|
49
|
-
// Sampled from the reference card: a warmer, creamier ground than peep's.
|
|
50
|
-
bg0: '#f2ead9',
|
|
51
|
-
bg1: '#e6d9c2',
|
|
52
49
|
ink: '#191919',
|
|
53
50
|
paper: '#ffffff',
|
|
54
|
-
// The
|
|
55
|
-
//
|
|
51
|
+
// The hoop earrings' orange. ~9% duskier than the asset's sample: at tile
|
|
52
|
+
// size the sampled value out-shouted the face.
|
|
56
53
|
accent: '#dc8432',
|
|
57
54
|
mouthIn: '#191919',
|
|
58
55
|
teeth: '#ffffff',
|
|
59
56
|
tongue: '#8d7f79',
|
|
60
57
|
};
|
|
61
58
|
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
59
|
+
// --- reshape ------------------------------------------------------------
|
|
60
|
+
// Same move as peep, for the same reason: built next to the Canvas2D
|
|
61
|
+
// professional identities, this head read tall and narrow — 285 units wide in
|
|
62
|
+
// a 907-unit 4:3 frame — where the canvas avatars were calibrated at call-tile
|
|
63
|
+
// size from the start. One static, non-animated transform on the head cluster
|
|
64
|
+
// only (skull, hair, features — never neck or body), pivoted at CHIN_Y so the
|
|
65
|
+
// chin's own silhouette point does not move and the jaw-to-collar join needs
|
|
66
|
+
// no rework. Every landmark constant below still describes the original art
|
|
67
|
+
// space. See face-peep.js for the full derivation, including why "broader"
|
|
68
|
+
// here means a tighter 4:3 crop rather than a wider aspect.
|
|
69
|
+
const CX = 380;
|
|
70
|
+
const CHIN_Y = 572;
|
|
71
|
+
const RESHAPE_SX = 1.10;
|
|
72
|
+
const RESHAPE_SY = 0.88;
|
|
73
|
+
const RESHAPE = `translate(${CX} ${CHIN_Y}) scale(${RESHAPE_SX} ${RESHAPE_SY}) translate(${-CX} ${-CHIN_Y})`;
|
|
74
|
+
|
|
75
|
+
// The outer wave of hair, rather than HEAD_TOP under it, is the crown a viewer
|
|
76
|
+
// measures. The chin is the lowest point of the resting jaw silhouette.
|
|
77
|
+
// crownY is the reshaped crown: CHIN_Y + (96 − CHIN_Y) × RESHAPE_SY, worked out
|
|
78
|
+
// by hand because the camera reads landmarks, not the transform above it.
|
|
79
|
+
const FRAME = { centerX: CX, crownY: 153, chinY: CHIN_Y };
|
|
80
|
+
const VB = viewBoxForHead(FRAME);
|
|
65
81
|
|
|
66
82
|
export const META = {
|
|
67
83
|
viewBox: { x: VB.x, y: VB.y, w: VB.w, h: VB.h },
|
|
68
|
-
|
|
84
|
+
// The original crop (298, 436, 164, 100) with its corners carried through
|
|
85
|
+
// RESHAPE — a camera change does not move a path, but the reshape is
|
|
86
|
+
// geometry rather than camera, so the inspection crop has to follow it.
|
|
87
|
+
mouthCrop: { x: 290, y: 452, w: 180, h: 88 },
|
|
69
88
|
};
|
|
70
89
|
|
|
71
90
|
// --- landmarks --------------------------------------------------------------
|
|
72
|
-
|
|
91
|
+
// These are the NATIVE art space and RESHAPE never enters them: it is a
|
|
92
|
+
// wrapper on the rendered group, so `apply()` and every number below still
|
|
93
|
+
// mean what they meant before it existed.
|
|
73
94
|
const HEAD_TOP = 150;
|
|
74
95
|
|
|
75
|
-
|
|
96
|
+
// rx widened from 15 — the bean was very nearly round, and cutting an almond
|
|
97
|
+
// aperture from it without an absurd lid line needed the same headroom peep's
|
|
98
|
+
// eye needed. See the eyes section.
|
|
99
|
+
const EYE = { y: 386, dx: 56, rx: 17, ry: 16 };
|
|
76
100
|
const MOUTH = { cx: CX, cy: 492 };
|
|
77
101
|
const MOUTH_APERTURE = 36;
|
|
78
102
|
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// same way here. Lens centres sit 4 units outboard of the eyes so the bridge
|
|
85
|
-
// keeps the reference's narrow 18-unit gap without moving the pupils.
|
|
86
|
-
const LENS = { dx: 60, hw: 51, hh: 40, r: 30 };
|
|
87
|
-
|
|
88
|
-
// Brows: bold, long and clearly arched, with real air between them and the
|
|
89
|
-
// frames — the reference's brows are its strongest single mark. The rest
|
|
90
|
-
// shape is a gentle taper-arc: v7's outer third dropped 17 units in 15 and
|
|
91
|
-
// over a resting smile the kink composited into a smirk that curdled across
|
|
92
|
-
// a long session. The kink is still available — as a browAngle POSE.
|
|
103
|
+
// Brows: bold, long and clearly arched, with real air between them — the
|
|
104
|
+
// reference's brows are its strongest single mark. The rest shape is a
|
|
105
|
+
// gentle taper-arc: v7's outer third dropped 17 units in 15 and over a
|
|
106
|
+
// resting smile the kink composited into a smirk that curdled across a long
|
|
107
|
+
// session. The kink is still available — as a browAngle POSE.
|
|
93
108
|
const BROW_L = [[CX - 22, 340], [CX - 40, 327], [CX - 60, 320], [CX - 76, 319],
|
|
94
109
|
[CX - 88, 322], [CX - 97, 327], [CX - 103, 332]];
|
|
95
110
|
// Near-mirrored: the right arch rides 1 unit higher (personality, small
|
|
96
111
|
// enough to survive the mirror-flip levelness check, which is judged on the
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
// drawn lean.
|
|
112
|
+
// eye lines). Anything bigger read as a baked-in head tilt — the mixer's
|
|
113
|
+
// headRoll adds on top of the art and nothing can subtract a drawn lean.
|
|
100
114
|
const BROW_R = [[CX + 22, 340], [CX + 40, 326], [CX + 60, 319], [CX + 76, 318],
|
|
101
115
|
[CX + 88, 321], [CX + 97, 327], [CX + 103, 332]];
|
|
102
116
|
|
|
@@ -122,7 +136,11 @@ const HEAD = [
|
|
|
122
136
|
];
|
|
123
137
|
// Lighter than wren's ring by ~20% — the reference line is confident but not
|
|
124
138
|
// woodcut. Heavy low on the jaw, nothing at the crown (under hair).
|
|
125
|
-
|
|
139
|
+
//
|
|
140
|
+
// Scaled to ~0.65 of the original profile ([3, 6, 9.5, 12, 13, 11.5, 9, 6, 4,
|
|
141
|
+
// 3]) when RESHAPE landed: the same taper shape, but a line rather than a rope
|
|
142
|
+
// once the head is drawn broader and the frame tighter around it.
|
|
143
|
+
const HEAD_W = [2, 4, 6, 8, 8.5, 7.5, 6, 4, 2.5, 2];
|
|
126
144
|
|
|
127
145
|
// Neck: the shared truncated-cone construction, slim; fused into the head
|
|
128
146
|
// layer and overshot past every neckline for the standard parallax reasons.
|
|
@@ -229,44 +247,8 @@ function circlePts(cx, cy, r) {
|
|
|
229
247
|
const HOOP_L = taperRing(circlePts(236, 488, 12), [4.5, 5, 4.5]);
|
|
230
248
|
const HOOP_R = taperRing(circlePts(524, 488, 12), [4.5, 5, 4.5]);
|
|
231
249
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
//
|
|
235
|
-
// Wider than tall, corners genuinely rounded, thin ring. Temples run outward
|
|
236
|
-
// and vanish under the hair's inner edge. Drawn after the eyes so the frame
|
|
237
|
-
// passes over the bean at gaze extremes rather than under it.
|
|
238
|
-
// ---------------------------------------------------------------------------
|
|
239
|
-
function roundRectPts(cx, cy, hw, hh, r) {
|
|
240
|
-
const k = r * K;
|
|
241
|
-
const L = cx - hw, R = cx + hw, T = cy - hh, B = cy + hh;
|
|
242
|
-
return [
|
|
243
|
-
[R, cy],
|
|
244
|
-
[R, cy + (hh - r) / 2], [R, B - r - k * 0.2], [R, B - r],
|
|
245
|
-
[R, B - r + k], [R - r + k, B], [R - r, B],
|
|
246
|
-
[R - r - (hw - r) / 2, B], [L + r + (hw - r) / 2, B], [L + r, B],
|
|
247
|
-
[L + r - k, B], [L, B - r + k], [L, B - r],
|
|
248
|
-
[L, B - r - k * 0.2], [L, cy + (hh - r) / 2], [L, cy],
|
|
249
|
-
[L, cy - (hh - r) / 2], [L, T + r + k * 0.2], [L, T + r],
|
|
250
|
-
[L, T + r - k], [L + r - k, T], [L + r, T],
|
|
251
|
-
[L + r + (hw - r) / 2, T], [R - r - (hw - r) / 2, T], [R - r, T],
|
|
252
|
-
[R - r + k, T], [R, T + r - k], [R, T + r],
|
|
253
|
-
[R, T + r + k * 0.2], [R, cy - (hh - r) / 2], [R, cy],
|
|
254
|
-
];
|
|
255
|
-
}
|
|
256
|
-
const LENS_L = roundRectPts(CX - LENS.dx, EYE.y, LENS.hw, LENS.hh, LENS.r);
|
|
257
|
-
const LENS_R = roundRectPts(CX + LENS.dx, EYE.y, LENS.hw, LENS.hh, LENS.r);
|
|
258
|
-
const RING_W = [3, 3.4, 3, 3.4, 3];
|
|
259
|
-
// Bridge at the upper third and temples off the top outer corners, sweeping
|
|
260
|
-
// up toward the ears — both as drawn in the reference.
|
|
261
|
-
const BRIDGE = [[CX - 9, 371], [CX - 3, 366], [CX + 3, 366], [CX + 9, 371]];
|
|
262
|
-
// Attach on the top-outer corner arc (the rounded corner pulls the rim in to
|
|
263
|
-
// ~x±108 at this height), sweeping up to vanish under the hair's inner edge.
|
|
264
|
-
const TEMPLE_L = [[CX - 108, 364], [CX - 124, 357], [CX - 141, 352], [CX - 158, 350]];
|
|
265
|
-
const TEMPLE_R = [[CX + 108, 364], [CX + 124, 357], [CX + 141, 352], [CX + 158, 350]];
|
|
266
|
-
|
|
267
|
-
/** The nose: a tiny soft hook, tip only — the glasses own the bridge. It sits
|
|
268
|
-
* lower than v6 (the reference's nose mark tops out at ~55% of eye→mouth,
|
|
269
|
-
* not 39%), which is also what gives the taller lenses their clearance. */
|
|
250
|
+
/** The nose: a tiny soft hook, tip only. It sits lower than v6 (the
|
|
251
|
+
* reference's nose mark tops out at ~55% of eye→mouth, not 39%). */
|
|
270
252
|
const NOSE_D = taper(
|
|
271
253
|
[[CX + 3, 439], [CX - 2, 448], [CX - 6, 456], [CX - 4, 462],
|
|
272
254
|
[CX + 1, 465], [CX + 7, 463], [CX + 10, 458]],
|
|
@@ -274,12 +256,13 @@ const NOSE_D = taper(
|
|
|
274
256
|
);
|
|
275
257
|
|
|
276
258
|
// ---------------------------------------------------------------------------
|
|
277
|
-
// Static art: torso — white crew tee under an open
|
|
259
|
+
// Static art: torso — a white crew tee under an open overshirt, both paper,
|
|
260
|
+
// distinguished only by ink outline.
|
|
278
261
|
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
262
|
+
// The tee is edge-to-edge under everything; the two overshirt panels sit on
|
|
263
|
+
// top, their inner edges making the open V. Both panels keep the house
|
|
264
|
+
// two-run shoulder: outer arm edge, hard acromion turn, near-horizontal
|
|
265
|
+
// trapezius shelf. Everything overshoots the frame.
|
|
283
266
|
// ---------------------------------------------------------------------------
|
|
284
267
|
const TEE = [
|
|
285
268
|
[288, 960],
|
|
@@ -373,7 +356,7 @@ const POSE = {
|
|
|
373
356
|
// parallax slides like a wig.
|
|
374
357
|
yawPx: 26, pitchPx: 16,
|
|
375
358
|
pivot: PIVOT,
|
|
376
|
-
breathSwell: 0.
|
|
359
|
+
breathSwell: 0.008, swellPivot: { x: CX, y: 950 },
|
|
377
360
|
turnPx: 16,
|
|
378
361
|
layers: LAYERS, parallax: PARALLAX,
|
|
379
362
|
torsoLayers: ['body'],
|
|
@@ -428,8 +411,8 @@ function mouthGeometry(p) {
|
|
|
428
411
|
// Corner travel 28 (was 23): the tile swallows less. Press straightens the
|
|
429
412
|
// band by pulling the corner lift toward zero.
|
|
430
413
|
const cLift = 1 - 0.85 * press;
|
|
431
|
-
const yL = cy - (
|
|
432
|
-
const yR = cy - (
|
|
414
|
+
const yL = cy - (1.5 + p.mouthCornerL * 28) * cLift;
|
|
415
|
+
const yR = cy - (1.5 + p.mouthCornerR * 28) * cLift;
|
|
433
416
|
|
|
434
417
|
const apTop = cy - h * 0.25;
|
|
435
418
|
let apBot = cy + h * 0.75;
|
|
@@ -480,8 +463,12 @@ function teethPath(m, amt, lower) {
|
|
|
480
463
|
);
|
|
481
464
|
}
|
|
482
465
|
|
|
483
|
-
// Eyes: the
|
|
484
|
-
//
|
|
466
|
+
// Eyes: the lid-line-plus-iris model, ported from face-peep.js. It reads
|
|
467
|
+
// better against the "big and dark pupils" brief than the old whole-bean
|
|
468
|
+
// translation did, not worse — the reference's pupils get a real iris that
|
|
469
|
+
// carries the gaze while the lash and lid line hold still. See face-peep.js
|
|
470
|
+
// for the two wrong passes (paper-on-ink, then a too-small pupil that read as
|
|
471
|
+
// a startled target) that are not recoverable from the numbers alone.
|
|
485
472
|
|
|
486
473
|
/** Top-lid height for a given lid value — shared by bean and lash so they can
|
|
487
474
|
* never disagree. The mapping is a 0.6-power, not linear: the mixer's rest
|
|
@@ -495,26 +482,70 @@ function lidTopY(cy, lid) {
|
|
|
495
482
|
return lerp(cy - EYE.ry * 1.05, cy - EYE.ry * 0.42, Math.pow(clamp(lid), 0.6));
|
|
496
483
|
}
|
|
497
484
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
485
|
+
// A cubic whose two controls share a y reaches only 3/4 of the way to it.
|
|
486
|
+
// Everything below works in DRAWN extents and divides that back out, so an
|
|
487
|
+
// inset of n units is n units on screen — see face-peep.js for why the inset
|
|
488
|
+
// below cannot be proportional instead.
|
|
489
|
+
const BULGE = 0.75;
|
|
490
|
+
|
|
491
|
+
/** Two cubics between (cx±rx, cyMid), bulging to yTop and yBot; tilt about cy. */
|
|
492
|
+
function lensPath(cx, cy, g, tiltDeg) {
|
|
493
|
+
if (!g) return '';
|
|
505
494
|
const a = (tiltDeg * Math.PI) / 180;
|
|
506
495
|
const ca = Math.cos(a), sa = Math.sin(a);
|
|
507
496
|
const R = (x, y) => {
|
|
508
497
|
const dx = x - cx, dy = y - cy;
|
|
509
498
|
return `${f(cx + dx * ca - dy * sa)} ${f(cy + dx * sa + dy * ca)}`;
|
|
510
499
|
};
|
|
500
|
+
const ctlTop = g.cyMid + (g.yTop - g.cyMid) / BULGE;
|
|
501
|
+
const ctlBot = g.cyMid + (g.yBot - g.cyMid) / BULGE;
|
|
511
502
|
return (
|
|
512
|
-
`M${R(cx - rx,
|
|
513
|
-
`C${R(cx - rx * 0.5,
|
|
514
|
-
`C${R(cx + rx * 0.5,
|
|
503
|
+
`M${R(cx - g.rx, g.cyMid)}` +
|
|
504
|
+
`C${R(cx - g.rx * 0.5, ctlTop)} ${R(cx + g.rx * 0.5, ctlTop)} ${R(cx + g.rx, g.cyMid)}` +
|
|
505
|
+
`C${R(cx + g.rx * 0.5, ctlBot)} ${R(cx - g.rx * 0.5, ctlBot)} ${R(cx - g.rx, g.cyMid)}Z`
|
|
515
506
|
);
|
|
516
507
|
}
|
|
517
508
|
|
|
509
|
+
/** The lid silhouette, in drawn extents. Same power-curve lid mapping and
|
|
510
|
+
* squint gain (.95: the smile-squint is most of what separates warm from
|
|
511
|
+
* neutral at tile size) as before the eye was opened out. */
|
|
512
|
+
function eyeGeom(cy, lid, squint) {
|
|
513
|
+
const L = clamp(lid);
|
|
514
|
+
const ctlTop = lidTopY(cy, lid);
|
|
515
|
+
const ctlBot = lerp(cy + EYE.ry * 1.05, cy - EYE.ry * 0.05, L) - clamp(squint) * EYE.ry * 0.95;
|
|
516
|
+
return {
|
|
517
|
+
cyMid: cy,
|
|
518
|
+
rx: EYE.rx,
|
|
519
|
+
yTop: cy + BULGE * (ctlTop - cy),
|
|
520
|
+
yBot: cy + BULGE * (ctlBot - cy),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// The lid line, as constant distances — a proportional inset inverts the
|
|
525
|
+
// moment a closing lid carries the bean's lower edge above the eye centre and
|
|
526
|
+
// leaks paper out through a shut eye.
|
|
527
|
+
const LASH_X = 3.0;
|
|
528
|
+
const LASH_TOP = 5.5;
|
|
529
|
+
const LASH_BOT = 4.5;
|
|
530
|
+
|
|
531
|
+
/** The paper almond inside the lid line, or null once the lid has shut on it. */
|
|
532
|
+
function apertureGeom(g) {
|
|
533
|
+
const yTop = g.yTop + LASH_TOP;
|
|
534
|
+
const yBot = g.yBot - LASH_BOT;
|
|
535
|
+
const rx = g.rx - LASH_X;
|
|
536
|
+
if (yBot - yTop < 0.5 || rx < 0.5) return null;
|
|
537
|
+
return { cyMid: (yTop + yBot) / 2, rx, yTop, yBot };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Sized to fill the resting aperture's height rather than float inside it —
|
|
541
|
+
// this is what "big and dark pupils" means now that the eye has a real iris
|
|
542
|
+
// instead of a translating bean.
|
|
543
|
+
const IRIS_R = 8.0;
|
|
544
|
+
|
|
545
|
+
// Travel, native units: one corner's worth of paper. At either extreme one
|
|
546
|
+
// sliver closes and the other doubles, which is the whole gaze signal.
|
|
547
|
+
const IRIS_TRAVEL = { x: 4.5, y: 2.2 };
|
|
548
|
+
|
|
518
549
|
/**
|
|
519
550
|
* The upper lash line: lies ON the bean's top edge and ends in a small
|
|
520
551
|
* outward flick at the outer corner. The body samples the bean's own top
|
|
@@ -547,12 +578,9 @@ function lashPath(cx, cy, lid, dir) {
|
|
|
547
578
|
// - Raises land at 20 (v7: 16) — the minimal-face lesson says small deltas
|
|
548
579
|
// vanish, and at a 130 px tile the emotion poses collapsed below this.
|
|
549
580
|
// - Descent is capped at 8.5 and weighted toward the inner half (corrugator
|
|
550
|
-
// pull)
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
// and brows-down — concentration — silently dies. The inner weighting also
|
|
554
|
-
// keeps the outer tail off the frame's corner arc. What travel can't carry,
|
|
555
|
-
// thickness does: a knitted brow bulks up 30% at full descent.
|
|
581
|
+
// pull), so brows-down reads as concentration rather than a merged smear.
|
|
582
|
+
// What travel can't carry, thickness does: a knitted brow bulks up 30% at
|
|
583
|
+
// full descent.
|
|
556
584
|
function browPath(pts, raise, angle, inner) {
|
|
557
585
|
const n = pts.length - 1;
|
|
558
586
|
const up = Math.max(0, raise);
|
|
@@ -579,59 +607,63 @@ function markup(id, t) {
|
|
|
579
607
|
<svg id="${id}" viewBox="${VB.x} ${VB.y} ${VB.w} ${VB.h}" xmlns="http://www.w3.org/2000/svg"
|
|
580
608
|
preserveAspectRatio="xMidYMid meet" style="display:block;width:100%;height:100%">
|
|
581
609
|
<defs>
|
|
582
|
-
<radialGradient id="${id}-gBg" cx="50%" cy="36%" r="76%">
|
|
583
|
-
<stop offset="0%" stop-color="${t.bg0}"/><stop offset="100%" stop-color="${t.bg1}"/>
|
|
584
|
-
</radialGradient>
|
|
585
610
|
<clipPath id="${id}-clipMouth"><path id="${id}-clipMouthP" d=""/></clipPath>
|
|
611
|
+
<clipPath id="${id}-clipEyeL"><path id="${id}-clipEyeLP" d=""/></clipPath>
|
|
612
|
+
<clipPath id="${id}-clipEyeR"><path id="${id}-clipEyeRP" d=""/></clipPath>
|
|
586
613
|
</defs>
|
|
587
614
|
|
|
588
|
-
<rect x="${VB.x}" y="${VB.y}" width="${VB.w}" height="${VB.h}" fill="url(#${id}-gBg)"/>
|
|
589
|
-
|
|
590
615
|
<!-- head and neck; hair underlay at head parallax is the anti-sliver
|
|
591
|
-
insurance AND the backfill behind the hair-layer ears (see header).
|
|
616
|
+
insurance AND the backfill behind the hair-layer ears (see header).
|
|
617
|
+
|
|
618
|
+
RESHAPE wraps the art INSIDE the parallax group, never the group itself:
|
|
619
|
+
poseTransforms writes the group's own transform every frame and would
|
|
620
|
+
overwrite it. Same for features and hair below. The neck stays outside
|
|
621
|
+
it — the reshape is pivoted on the chin so that join still lands. -->
|
|
592
622
|
<g id="${id}-head">
|
|
593
623
|
<path d="${NECK_FILL}" fill="${t.paper}"/>
|
|
594
624
|
${ink(taper(NECK_L, [3, 7, 5]))}
|
|
595
625
|
${ink(taper(NECK_R, [3, 7, 5]))}
|
|
596
|
-
<
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
626
|
+
<g transform="${RESHAPE}">
|
|
627
|
+
<path d="${HEAD_FILL}" fill="${t.paper}"/>
|
|
628
|
+
${ink(HEAD_RING)}
|
|
629
|
+
${ink(taper(JAW_UNDER, [2, 5, 2]))}
|
|
630
|
+
<path d="${HAIR_D}" fill="${t.ink}"/>
|
|
631
|
+
</g>
|
|
600
632
|
</g>
|
|
601
633
|
|
|
602
634
|
<!-- tee, then the two overshirt panels and their tailoring -->
|
|
603
635
|
<g id="${id}-body">
|
|
604
636
|
${paper(region(TEE))}
|
|
605
637
|
${ink(taper(TEE_NECK, [3, 5.5, 3]))}
|
|
606
|
-
<path d="${region(PANEL_L)}" fill="${t.
|
|
607
|
-
<path d="${region(PANEL_R)}" fill="${t.
|
|
638
|
+
<path d="${region(PANEL_L)}" fill="${t.paper}"/>
|
|
639
|
+
<path d="${region(PANEL_R)}" fill="${t.paper}"/>
|
|
608
640
|
${ink(taper(PANEL_L_OUT, PANEL_W))}
|
|
609
641
|
${ink(taper(PANEL_R_OUT, PANEL_W))}
|
|
610
642
|
${ink(taper(PANEL_L_IN, [6, 7, 5]))}
|
|
611
643
|
${ink(taper(PANEL_R_IN, [6, 7, 5]))}
|
|
612
|
-
<path d="${region(COLLAR_L)}" fill="${t.
|
|
613
|
-
<path d="${region(COLLAR_R)}" fill="${t.
|
|
644
|
+
<path d="${region(COLLAR_L)}" fill="${t.paper}"/>
|
|
645
|
+
<path d="${region(COLLAR_R)}" fill="${t.paper}"/>
|
|
614
646
|
${ink(taperRing(COLLAR_L, [3, 4.5, 3], 6))}
|
|
615
647
|
${ink(taperRing(COLLAR_R, [3, 4.5, 3], 6))}
|
|
616
648
|
${CREASES.map((c) => ink(taper(c.p, c.w))).join('\n ')}
|
|
617
649
|
</g>
|
|
618
650
|
|
|
619
|
-
<!-- features: brows, eyes+lashes, nose,
|
|
651
|
+
<!-- features: brows, eyes+lashes, nose, mouth -->
|
|
620
652
|
<g id="${id}-features">
|
|
653
|
+
<g transform="${RESHAPE}">
|
|
621
654
|
<path id="${id}-browL" fill="${t.ink}"/>
|
|
622
655
|
<path id="${id}-browR" fill="${t.ink}"/>
|
|
623
656
|
<g id="${id}-eyes">
|
|
624
657
|
<path id="${id}-eyeL" fill="${t.ink}"/>
|
|
625
658
|
<path id="${id}-eyeR" fill="${t.ink}"/>
|
|
659
|
+
<path id="${id}-apertureL" fill="${t.paper}"/>
|
|
660
|
+
<path id="${id}-apertureR" fill="${t.paper}"/>
|
|
661
|
+
<g clip-path="url(#${id}-clipEyeL)"><circle id="${id}-irisL" fill="${t.ink}"/></g>
|
|
662
|
+
<g clip-path="url(#${id}-clipEyeR)"><circle id="${id}-irisR" fill="${t.ink}"/></g>
|
|
626
663
|
<path id="${id}-lashL" fill="${t.ink}"/>
|
|
627
664
|
<path id="${id}-lashR" fill="${t.ink}"/>
|
|
628
665
|
</g>
|
|
629
666
|
<path d="${NOSE_D}" fill="${t.ink}"/>
|
|
630
|
-
${ink(taperRing(LENS_L, RING_W, 6))}
|
|
631
|
-
${ink(taperRing(LENS_R, RING_W, 6))}
|
|
632
|
-
${ink(taper(BRIDGE, [3, 4, 3]))}
|
|
633
|
-
${ink(taper(TEMPLE_L, [3, 3.5, 4]))}
|
|
634
|
-
${ink(taper(TEMPLE_R, [3, 3.5, 4]))}
|
|
635
667
|
<g id="${id}-mouth">
|
|
636
668
|
<path id="${id}-mouthIn" fill="${t.mouthIn}"/>
|
|
637
669
|
<g clip-path="url(#${id}-clipMouth)">
|
|
@@ -641,10 +673,12 @@ function markup(id, t) {
|
|
|
641
673
|
</g>
|
|
642
674
|
<path id="${id}-lips" fill="${t.ink}"/>
|
|
643
675
|
</g>
|
|
676
|
+
</g>
|
|
644
677
|
</g>
|
|
645
678
|
|
|
646
679
|
<!-- hair: the mass, its highlights, then ears and hoops on top -->
|
|
647
680
|
<g id="${id}-hair">
|
|
681
|
+
<g transform="${RESHAPE}">
|
|
648
682
|
<path d="${HAIR_D}" fill="${t.ink}"/>
|
|
649
683
|
${SHINE.map((s) => paper(taper(s.p, s.w, 6))).join('\n ')}
|
|
650
684
|
<path d="${region(EAR_L)}" fill="${t.paper}"/>
|
|
@@ -655,6 +689,7 @@ function markup(id, t) {
|
|
|
655
689
|
${ink(taper(EAR_R_IN, [2, 4, 2]))}
|
|
656
690
|
<path d="${HOOP_L}" fill="${t.accent}"/>
|
|
657
691
|
<path d="${HOOP_R}" fill="${t.accent}"/>
|
|
692
|
+
</g>
|
|
658
693
|
</g>
|
|
659
694
|
</svg>`;
|
|
660
695
|
}
|
|
@@ -673,7 +708,10 @@ export function createFace(mount, theme = {}) {
|
|
|
673
708
|
const el = {
|
|
674
709
|
head: $('head'), body: $('body'), features: $('features'), hair: $('hair'),
|
|
675
710
|
browL: $('browL'), browR: $('browR'),
|
|
676
|
-
|
|
711
|
+
eyeL: $('eyeL'), eyeR: $('eyeR'),
|
|
712
|
+
clipEyeL: $('clipEyeLP'), clipEyeR: $('clipEyeRP'),
|
|
713
|
+
apertureL: $('apertureL'), apertureR: $('apertureR'),
|
|
714
|
+
irisL: $('irisL'), irisR: $('irisR'),
|
|
677
715
|
lashL: $('lashL'), lashR: $('lashR'),
|
|
678
716
|
mouthIn: $('mouthIn'), lips: $('lips'),
|
|
679
717
|
clipMouth: $('clipMouthP'),
|
|
@@ -683,19 +721,35 @@ export function createFace(mount, theme = {}) {
|
|
|
683
721
|
function apply(p) {
|
|
684
722
|
poseTransforms(p, set, el, POSE);
|
|
685
723
|
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
//
|
|
689
|
-
// Past these the pupil starts reading pressed against the frame.
|
|
690
|
-
set(el.eyes, 'transform', `translate(${f(p.pupilX * 14)} ${f(p.pupilY * 10)})`);
|
|
691
|
-
|
|
724
|
+
// The lid line holds still and the iris carries the gaze — see the eyes
|
|
725
|
+
// section. The lid follows vertical gaze downward only: looking up
|
|
726
|
+
// genuinely does widen the aperture, so there is nothing to add there.
|
|
692
727
|
const lidFollow = Math.max(0, p.pupilY) * 0.22;
|
|
693
728
|
const lidL = p.lidL + lidFollow;
|
|
694
729
|
const lidR = p.lidR + lidFollow;
|
|
695
730
|
// Both beans exactly on EYE.y: a ±1 stagger here read as head tilt at
|
|
696
731
|
// rest, and rest must be level (see BROW_R).
|
|
697
|
-
|
|
698
|
-
|
|
732
|
+
const eye = (cx, cy, lid, squint, tilt, aperture, iris, clip) => {
|
|
733
|
+
const g = eyeGeom(cy, lid, squint);
|
|
734
|
+
const ap = apertureGeom(g);
|
|
735
|
+
const apD = lensPath(cx, cy, ap, tilt);
|
|
736
|
+
set(el[aperture], 'd', apD);
|
|
737
|
+
// The iris is clipped to the APERTURE, not to the lid line, so it is
|
|
738
|
+
// cropped by the same edge the paper is — a shut lid, which has no
|
|
739
|
+
// aperture at all, takes the iris with it and needs no opacity logic.
|
|
740
|
+
set(el[clip], 'd', apD);
|
|
741
|
+
set(el[iris], 'cx', f(cx + p.pupilX * IRIS_TRAVEL.x));
|
|
742
|
+
// Anchored to the aperture's midline rather than the eye's, so a
|
|
743
|
+
// half-closed eye keeps the iris centred in the slit instead of showing
|
|
744
|
+
// a band of paper above it.
|
|
745
|
+
set(el[iris], 'cy', f((ap ? ap.cyMid : cy) + p.pupilY * IRIS_TRAVEL.y));
|
|
746
|
+
set(el[iris], 'r', f(IRIS_R));
|
|
747
|
+
return lensPath(cx, cy, g, tilt);
|
|
748
|
+
};
|
|
749
|
+
set(el.eyeL, 'd', eye(CX - EYE.dx, EYE.y, lidL, p.squintL, -3,
|
|
750
|
+
'apertureL', 'irisL', 'clipEyeL'));
|
|
751
|
+
set(el.eyeR, 'd', eye(CX + EYE.dx, EYE.y, lidR, p.squintR, 3,
|
|
752
|
+
'apertureR', 'irisR', 'clipEyeR'));
|
|
699
753
|
set(el.lashL, 'd', lashPath(CX - EYE.dx, EYE.y, lidL, -1));
|
|
700
754
|
set(el.lashR, 'd', lashPath(CX + EYE.dx, EYE.y, lidR, 1));
|
|
701
755
|
|
|
@@ -723,3 +777,10 @@ export function createFace(mount, theme = {}) {
|
|
|
723
777
|
|
|
724
778
|
return faceApi(mount, svg, apply, t);
|
|
725
779
|
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* This face as a **Face** record — `{ create, meta }`, the shape
|
|
783
|
+
* `createAvatar({ face })` takes. Importing it costs this drawing and nothing
|
|
784
|
+
* else; `src/faces.js` is the all-three table, for tooling.
|
|
785
|
+
*/
|
|
786
|
+
export const myna = { create: createFace, meta: META };
|