@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
|
@@ -0,0 +1,1156 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// author/parts/hand.mjs — the SECONDARY GESTURE hand: one drawing, placed by
|
|
3
|
+
// four numbers, that rises from under the bottom edge of the frame, holds
|
|
4
|
+
// beside the face and drops back out.
|
|
5
|
+
//
|
|
6
|
+
// It is a PART in the sense ./README.md means, with one difference worth
|
|
7
|
+
// saying out loud: every other part in this directory is a piece of a FACE and
|
|
8
|
+
// is driven by the control vector's face channels. This one is not on the face
|
|
9
|
+
// at all. It is the nearest object in the frame, it lives in FRAME space, and
|
|
10
|
+
// the only thing that reaches it is a single `HandFrame`.
|
|
11
|
+
//
|
|
12
|
+
// CONSTRUCTION (build time, once per persona)
|
|
13
|
+
// makeHand({ P, PALETTE, solid, frame, pen, group })
|
|
14
|
+
// `frame` is the four numbers below, `PALETTE` supplies the skin rungs, so
|
|
15
|
+
// the hand is the same character's hand as the face above it.
|
|
16
|
+
//
|
|
17
|
+
// DRIVING part.draws(h, env) -> the thirteen draws (twenty-six with a
|
|
18
|
+
// `pen`: every silhouette gains a line), in paint order
|
|
19
|
+
// REST handRest() -> the `hand:` block of the control vector; also
|
|
20
|
+
// `part.rest`. At rest there is no gesture: every draw is at alpha
|
|
21
|
+
// 0 AND parked below the frame, which is two independent reasons
|
|
22
|
+
// for a hand not to be on screen and exactly one more than a rig
|
|
23
|
+
// with a wardrobe, a pose blend and a live evaluator deserves.
|
|
24
|
+
//
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// THE DRIVER BLOCK. voqalize's `HandFrame`, verbatim, all three fields:
|
|
27
|
+
//
|
|
28
|
+
// field rest meaning
|
|
29
|
+
// gesture null 'greet' | 'farewell' | 'approve' | 'wait', or null
|
|
30
|
+
// / anything unknown, which is "no hand"
|
|
31
|
+
// progress 0 0..1 through that gesture. The MIXER owns the
|
|
32
|
+
// clock: it computes `(now - start) / dur` and we
|
|
33
|
+
// never see a millisecond. `GESTURES[g].dur` is here
|
|
34
|
+
// for documentation and for a host that wants to
|
|
35
|
+
// drive the sweep at the upstream tempo.
|
|
36
|
+
// side 'right' 'right' puts the hand on the VIEWER's right (the
|
|
37
|
+
// character gesturing with its left hand), 'left' on
|
|
38
|
+
// the viewer's left. A persistent per-avatar setting
|
|
39
|
+
// upstream, not something a gesture chooses.
|
|
40
|
+
//
|
|
41
|
+
// Both ends of that are honest: the thumb splays AWAY from the body on either
|
|
42
|
+
// side, because the drawing is mirrored rather than translated.
|
|
43
|
+
//
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// THE FOUR NUMBERS, and why they are the caller's and not ours.
|
|
46
|
+
//
|
|
47
|
+
// cx design x of the frame's centre
|
|
48
|
+
// bottom design y of the frame's BOTTOM EDGE — the visible one, the
|
|
49
|
+
// one `meta.artboard` + `meta.align` crop to, not the
|
|
50
|
+
// artboard's
|
|
51
|
+
// reach design units per ART unit: the hand's size
|
|
52
|
+
// outboardLimit how far from `cx` ink may travel before it leaves the
|
|
53
|
+
// frame through a SIDE edge
|
|
54
|
+
//
|
|
55
|
+
// A part cannot derive those. `bottom` is the camera window's, which is
|
|
56
|
+
// metadata an avatar owns (avatars/round/face.mjs, CAMERA). The head height is
|
|
57
|
+
// the avatar's own landmark measurement, so a wider camera does not make the
|
|
58
|
+
// hand a different size. The avatar hands both in, and this file's geometry is
|
|
59
|
+
// in ART units throughout — one multiply by `reach` on the way out.
|
|
60
|
+
//
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// WHERE THE DRAWING COMES FROM. Not from upstream's point tables, and that is
|
|
63
|
+
// the second cut of this file rather than the first.
|
|
64
|
+
//
|
|
65
|
+
// The first cut copied voqalize's `hand.js` outlines verbatim, on the argument
|
|
66
|
+
// that the same character has to wave in both renderers. What that argument
|
|
67
|
+
// misses is that upstream draws every shape with a black ink contour, and a
|
|
68
|
+
// contour carries an enormous amount of read: three lumps and a stub are a
|
|
69
|
+
// hand once a line goes round them. Round has no outlines. The same three
|
|
70
|
+
// lumps, filled flat, are a mitten — which is exactly what came back from the
|
|
71
|
+
// first 1x judging, in those words.
|
|
72
|
+
//
|
|
73
|
+
// So the shapes here are MEASURED, the same way `parts/mouth` and `parts/eye`
|
|
74
|
+
// are measured: `parts/sheet.py --kind hand` renders one 1536x1024 reference
|
|
75
|
+
// sheet of three hands in a flat, front-lit, outline-free vocabulary, and
|
|
76
|
+
// `parts/ref/hand/v1-medium.png` is traced into silhouette profiles — column
|
|
77
|
+
// tops for the fingertips, row run-lengths for the finger boxes and the gaps,
|
|
78
|
+
// the narrowest row of the lower third for the wrist, the crease and shade
|
|
79
|
+
// components for the interior marks. Every number below is that trace, in ART
|
|
80
|
+
// units, at
|
|
81
|
+
//
|
|
82
|
+
// 1 art unit = 1 reference-sheet pixel / 6.667 (PX_TO_ART, below)
|
|
83
|
+
//
|
|
84
|
+
// with the origin at the WRIST CENTRE of that cell and y NEGATIVE UP.
|
|
85
|
+
//
|
|
86
|
+
// The trace also settled the finger proportions, and settled them the way a
|
|
87
|
+
// hand is actually read: not "each finger is 92% of the middle one's LENGTH",
|
|
88
|
+
// which depends on where you decide the knuckle is, but "each fingertip stands
|
|
89
|
+
// at 92% of the middle fingertip's HEIGHT ABOVE THE WRIST", which is what the
|
|
90
|
+
// silhouette shows. Measured: index .924, middle 1, ring .944, little .806.
|
|
91
|
+
//
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// HOW BIG, and the one number in this file that is a compromise rather than a
|
|
94
|
+
// measurement.
|
|
95
|
+
//
|
|
96
|
+
// The size is set against peep's hand — the same gesture, in the same demo, at
|
|
97
|
+
// the same 1x toggle — because that is the hand a viewer has already seen, and
|
|
98
|
+
// because that toggle sizes the two frames so BOTH HEADS measure 229 px across
|
|
99
|
+
// (adapter/demo.html, ONE_X), which is the only thing that makes a pixel over
|
|
100
|
+
// there comparable with a pixel over here.
|
|
101
|
+
//
|
|
102
|
+
// Measured rather than guessed: peep's hand group rasterised on its own,
|
|
103
|
+
// alpha-boxed, sampled every 80 ms through greet. Through the hold it is
|
|
104
|
+
// 130-132 wide by 80-88 tall; at the top of the hold, 131 x 85 CSS px.
|
|
105
|
+
//
|
|
106
|
+
// Those two numbers cannot both be matched, and the reason is anatomy rather
|
|
107
|
+
// than tuning: peep's hand is a squat caricature, 1.54 wide for every 1 tall
|
|
108
|
+
// of what the frame lets you see. A hand with five fingers on it is 0.67 wide
|
|
109
|
+
// for every 1 tall — the trace says 430 x 644 — and no arrangement of five
|
|
110
|
+
// fingers says otherwise. Matching the WIDTH would make ours half as tall
|
|
111
|
+
// again as peep's whole hand; matching the HEIGHT would make ours two thirds
|
|
112
|
+
// as wide, and at 1x a finger would be eight pixels across, which is where the
|
|
113
|
+
// mitten came from.
|
|
114
|
+
//
|
|
115
|
+
// What is matched instead is the AREA of that bounding box — the ink on
|
|
116
|
+
// screen, which is what "the same size hand" means to an eye, and which is
|
|
117
|
+
// also the measure by which the first cut of this file was called 1.6x too
|
|
118
|
+
// big. Ours comes out 99 x 112 against peep's 131 x 85: the same ink to within
|
|
119
|
+
// half a percent, 24% narrower, 32% taller. That height is 1.18 of round's
|
|
120
|
+
// chin-to-eyes distance (95 px at 1x) and 0.50 of its head — a hand, not a
|
|
121
|
+
// head, which is the rule the size was asked to obey.
|
|
122
|
+
//
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// THE FOUR RULES the drawing obeys, which are upstream's and which are the
|
|
125
|
+
// whole reason a hand with no arm reads as a hand with an arm:
|
|
126
|
+
//
|
|
127
|
+
// 1. THE WRIST NEVER ENTERS THE FRAME. Every silhouette runs to art y +300
|
|
128
|
+
// and is cut by the bottom edge; the wrist line itself is always at least
|
|
129
|
+
// ~24/576 of the frame width BELOW it. A lid drawn across the wrist is a
|
|
130
|
+
// severed hand, and the eye reads a severed hand as a sticker.
|
|
131
|
+
// The NARROWING is visible even though the narrowest point is not: each
|
|
132
|
+
// `hold` below is solved so the crop falls 16-19 art units above the
|
|
133
|
+
// wrist (hold / sc, the four of them 19.0 / 18.5 / 16.0 / 17.0), where the
|
|
134
|
+
// heel has already lost a quarter of its width and the plane painted
|
|
135
|
+
// across it has already turned it under. What continues below the crop is
|
|
136
|
+
// the forearm.
|
|
137
|
+
// 2. THE HAND IS BIG, but measured against peep and not against the frame —
|
|
138
|
+
// see the section above. A correctly-scaled hand reads as a prop.
|
|
139
|
+
// 3. WHAT RISES PAST THE FIST IS ONLY EVER A DIGIT, and nothing reaches the
|
|
140
|
+
// chin. The stations came down when the drawing came down in size (a
|
|
141
|
+
// smaller hand cannot hold its fingertips at the jaw AND keep its wrist
|
|
142
|
+
// out of frame — the two constraints meet only at a hand this file no
|
|
143
|
+
// longer draws), so the four holds land at .725 / .728 / .683 / .820 of
|
|
144
|
+
// the way from the bottom edge to the chin, and the only marks that clear
|
|
145
|
+
// the fist are the thumb of `approve` and the index of `wait`.
|
|
146
|
+
// 4. ONLY THE BOTTOM EDGE CUTS THE HAND. `outboardLimit` is checked against
|
|
147
|
+
// the shape's own width AND its rotation — a hand this tall swinging 16
|
|
148
|
+
// degrees about a wrist below the frame throws its corner a long way
|
|
149
|
+
// sideways, and the swing counts against the budget. `checkFraming()`
|
|
150
|
+
// below is that check, run by the avatar's build.
|
|
151
|
+
//
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// WHAT WE DO NOT DO, all of it deliberate:
|
|
154
|
+
//
|
|
155
|
+
// * no arm above the forearm stub, no shoulder — see rule 1. The arm is
|
|
156
|
+
// implied by the crop; drawing it would put a limb on a character that has
|
|
157
|
+
// none.
|
|
158
|
+
// * no idle hand. The hand exists for the length of one gesture and is not
|
|
159
|
+
// on screen otherwise. There is no resting-hand pose to blend to.
|
|
160
|
+
// * no clock. `progress` crosses the seam; the mixer owns start, duration,
|
|
161
|
+
// interrupt and queue, exactly as upstream does.
|
|
162
|
+
// * no per-gesture side. `side` is the avatar's, set once.
|
|
163
|
+
// * no fingernails, no knuckle wrinkles, no palm lines beyond one. The
|
|
164
|
+
// reference sheet was asked for none of them either: at 94 px across, the
|
|
165
|
+
// marks that survive are the ones separating MASSES, and the rest reads as
|
|
166
|
+
// dirt on the lens.
|
|
167
|
+
//
|
|
168
|
+
// THE ROUND IDIOM. Flat skin planes off the persona's own palette — `face` for
|
|
169
|
+
// the lit mass (the hand is nearer the light than the neck behind it), an
|
|
170
|
+
// OPAQUE rung below it for the planes turning away (see `paints()` for why it
|
|
171
|
+
// may not be `PALETTE.shade`), `crease` for the interior marks. No outline:
|
|
172
|
+
// `pen` is the hook the ink idiom takes (see below), and it is off unless
|
|
173
|
+
// somebody asks for it. Which is why the SEPARATIONS here are tonal and not linear: a
|
|
174
|
+
// thumb tucked at the side of a fist in the same flat skin as the fist is part
|
|
175
|
+
// of the fist, so `point`'s folded thumb is drawn a rung darker — on the true
|
|
176
|
+
// side of the form as well, since a thumb crossing a fist IS a plane of it
|
|
177
|
+
// turning away. `approve`'s thumb is the exception and for the same reason
|
|
178
|
+
// read the other way: it stands UP, clear of the fist against the background,
|
|
179
|
+
// in the same light as everything else, and a darker one read as a bruise.
|
|
180
|
+
//
|
|
181
|
+
// THE INK IDIOM, which is `pen` and which is the same drawing with a line round
|
|
182
|
+
// it. Ink's own numbers, not this file's: `PALETTE.ink` (rgb 25,35,51) at
|
|
183
|
+
// W_SIL = 4.0 artboard units on every silhouette — the mass, the digits, the
|
|
184
|
+
// raised thumb, the folded one — and W_FINE = 2.4 on the interior marks, which
|
|
185
|
+
// are the same two weights avatars/ink/face.mjs gives the head's outline and
|
|
186
|
+
// its jaw accent. A stroke width is a constant of a draw (`rig.js` copies it
|
|
187
|
+
// from base to out and never blends it), so it does NOT shrink with the
|
|
188
|
+
// gesture's depth `sc`: a pen has one nib and a hand pushed at the camera is
|
|
189
|
+
// still drawn with it.
|
|
190
|
+
//
|
|
191
|
+
// What changes besides the outline, all of it because a line is not a tone:
|
|
192
|
+
// * the interior marks are STROKED runs rather than filled tapered rings, so
|
|
193
|
+
// an outlined palette is never asked for a `crease` rung (ink has none);
|
|
194
|
+
// * the JOINT marks go, because the distal segment's own contour draws that
|
|
195
|
+
// line already;
|
|
196
|
+
// * the folded thumb of `point` moves from under the fist to on top of it, as
|
|
197
|
+
// a line. Under it, it is invisible in both idioms — it clears the fist's
|
|
198
|
+
// edge by under 1.5 art units — and round says "folded thumb" with the one
|
|
199
|
+
// thing a flat style has, a darker rung. A pen says it with a contour.
|
|
200
|
+
//
|
|
201
|
+
// THE FIXED-OPCODE GUARANTEE. Every run in here has a length fixed at module
|
|
202
|
+
// load; the per-frame work is an affine over points and then `spline`. The
|
|
203
|
+
// three shapes have three different topologies, so all thirteen draws — 26
|
|
204
|
+
// under a pen — are emitted every frame and the gesture chooses which are at
|
|
205
|
+
// alpha 1. Nine of the thirteen (eighteen of the 26) are always at zero, which
|
|
206
|
+
// costs the renderer that many comparisons and buys a display list that never
|
|
207
|
+
// changes shape.
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
import { spline, openSpline, strip, contours, circle } from '../path.mjs';
|
|
211
|
+
import { clamp, drawPusher } from '../rig.mjs';
|
|
212
|
+
|
|
213
|
+
// --- the front door --------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
// Design units per ART unit at this character's 664-unit skull. Art units are
|
|
216
|
+
// the reference sheet's pixels over 6.667 (PX_TO_ART), so this number and that
|
|
217
|
+
// one together are the whole of "how big is the hand"; this is the half that
|
|
218
|
+
// stays put when the drawing is re-traced.
|
|
219
|
+
export const REACH_AT_664_HEAD = 6.455555555555555;
|
|
220
|
+
|
|
221
|
+
// Rule 4's margin, in the same 576-wide frame units.
|
|
222
|
+
export const SIDE_MARGIN = 8;
|
|
223
|
+
|
|
224
|
+
// Rule 1, in ART units: how far below the frame's bottom edge the parked hand
|
|
225
|
+
// sits. It has to clear the tallest shape at its own depth scale — `wait`'s
|
|
226
|
+
// index finger, 93.75 * 0.75 = 70.3 — with room for the exit's rotation.
|
|
227
|
+
export const HIDE = 80;
|
|
228
|
+
|
|
229
|
+
// The reference sheet is 1536 x 1024 for three cells, so a cell is 512 wide and
|
|
230
|
+
// the hand in it is 430 x 644 px. This is the only place those pixels become
|
|
231
|
+
// art units, and the divisor is chosen so the placed hand's bounding box
|
|
232
|
+
// matches peep's in AREA — see "HOW BIG" above. Nothing else knows the sheet
|
|
233
|
+
// exists; the tables below are already converted.
|
|
234
|
+
export const PX_TO_ART = 1 / 6.667;
|
|
235
|
+
|
|
236
|
+
// Mark widths are quoted in upstream's SVG units, where the drawing was tuned;
|
|
237
|
+
// this converts them to art units so they scale with `reach` like everything
|
|
238
|
+
// else. Halved against the first cut, because the drawing they decorate is
|
|
239
|
+
// half the width it was, and a crease that keeps its absolute weight while the
|
|
240
|
+
// form under it halves stops being a crease and becomes a scar.
|
|
241
|
+
const MARK_K = 1 / 4.80;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The four placement numbers, from the avatar's own camera window.
|
|
245
|
+
* @param {{x:number,y:number,w:number,h:number}} win the VISIBLE rectangle in
|
|
246
|
+
* design space — what `meta.artboard` + `meta.align` crop to.
|
|
247
|
+
* @param {number} headHeight crown-to-chin height in the same design space.
|
|
248
|
+
*/
|
|
249
|
+
export function handFrameOf(win, headHeight) {
|
|
250
|
+
const k = headHeight / 664;
|
|
251
|
+
return {
|
|
252
|
+
cx: win.x + win.w / 2,
|
|
253
|
+
bottom: win.y + win.h,
|
|
254
|
+
reach: REACH_AT_664_HEAD * k,
|
|
255
|
+
outboardLimit: win.w / 2 - SIDE_MARGIN * (win.w / 576),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The `hand:` block of a control vector. */
|
|
260
|
+
export function handRest() {
|
|
261
|
+
return { hand: { gesture: null, progress: 0, side: 'right' } };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// TWO GENERATORS, both run once at module load.
|
|
266
|
+
//
|
|
267
|
+
// They exist because a finger written out as twelve literal points is twelve
|
|
268
|
+
// numbers nobody can check against a measurement, and there are nine fingers
|
|
269
|
+
// and thumb segments in this file. Written as an axis and two widths, every one
|
|
270
|
+
// of them can be read straight off the trace — and the point COUNT is a
|
|
271
|
+
// constant, which is the whole of the fixed-opcode guarantee.
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
// A tapered digit with a rounded tip and a base meant to be buried in the mass
|
|
275
|
+
// it grows from. 12 points, always.
|
|
276
|
+
//
|
|
277
|
+
// base/tip the axis, in art units
|
|
278
|
+
// wb/wt width ACROSS the digit at the base and at the tip
|
|
279
|
+
//
|
|
280
|
+
// The cap is a half-circle of radius wt/2 about the tip, sampled at 0, 45 and
|
|
281
|
+
// 90 degrees each side, which is what makes a fingertip round rather than
|
|
282
|
+
// chamfered; the base runs 15% of the length PAST the base point so the join
|
|
283
|
+
// into the palm has no shoulder to catch the eye.
|
|
284
|
+
const digit = (base, tip, wb, wt) => {
|
|
285
|
+
const dx = tip[0] - base[0], dy = tip[1] - base[1];
|
|
286
|
+
const L = Math.hypot(dx, dy) || 1;
|
|
287
|
+
const ux = dx / L, uy = dy / L; // along the digit, toward the tip
|
|
288
|
+
const nx = -uy, ny = ux; // across it
|
|
289
|
+
const at = (t, s) => [base[0] + ux * L * t + nx * s, base[1] + uy * L * t + ny * s];
|
|
290
|
+
const w = (t) => (wb + (wt - wb) * t) / 2;
|
|
291
|
+
const r = (wt / 2) / L; // the cap, as a fraction of the axis
|
|
292
|
+
const W = w(1 - r), c = 0.7071;
|
|
293
|
+
return [
|
|
294
|
+
at(0, -w(0)), at(0.35, -w(0.35)), at(0.7, -w(0.7)), at(1 - r, -W),
|
|
295
|
+
at(1 - r + r * c, -W * c), at(1, 0), at(1 - r + r * c, W * c), at(1 - r, W),
|
|
296
|
+
at(0.7, w(0.7)), at(0.35, w(0.35)), at(0, w(0)), at(-0.15, 0),
|
|
297
|
+
];
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// Force a run clockwise in the renderer's y-down space. Several contours of one
|
|
301
|
+
// path are filled `nonzero`, so two runs that disagree about which way round
|
|
302
|
+
// they go punch a hole where they overlap instead of merging — which is fine
|
|
303
|
+
// until a thumb crosses a palm. Cheap, once, at load.
|
|
304
|
+
const cw = (pts) => {
|
|
305
|
+
let a = 0;
|
|
306
|
+
for (let i = 0; i < pts.length; i++) {
|
|
307
|
+
const p = pts[i], q = pts[(i + 1) % pts.length];
|
|
308
|
+
a += p[0] * q[1] - q[0] * p[1];
|
|
309
|
+
}
|
|
310
|
+
return a < 0 ? pts.slice().reverse() : pts;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// THE SHAPES, in ART units, y NEGATIVE UP, origin at the WRIST CENTRE.
|
|
315
|
+
//
|
|
316
|
+
// PALM the palmar view. A palm mass with the fingers rising out of it as
|
|
317
|
+
// four SEPARATE runs, because the gaps between spread fingers are
|
|
318
|
+
// BACKGROUND and not marks — that is the single biggest thing the trace
|
|
319
|
+
// changed, and the reason five fingers can be counted at 1x. Plus a
|
|
320
|
+
// two-segment thumb set low on the radial side, the thenar ball as a
|
|
321
|
+
// shade blob, and one crease across the palm.
|
|
322
|
+
// FIST the thumbs-up. Four curled fingers as a stack of bands separated by
|
|
323
|
+
// three creases (the trace found them horizontal, which is the emoji's
|
|
324
|
+
// own idiom and the reason a fist reads as a fist at any size), and a
|
|
325
|
+
// two-segment thumb standing out of the top on the radial side, drawn
|
|
326
|
+
// UNDER the fist so its base is swallowed.
|
|
327
|
+
// POINT the SAME fist — literally the same point table — with the index
|
|
328
|
+
// extended in two segments with a bend at the base knuckle, and the
|
|
329
|
+
// thumb folded down the near side as a darker band.
|
|
330
|
+
//
|
|
331
|
+
// The last points before and after the two +300 tails are the wrist; the tails
|
|
332
|
+
// are the forearm running off the bottom of the frame. Rule 1 lives there.
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
// -- PALM -------------------------------------------------------------------
|
|
336
|
+
// Left edge -19.4 at the index base, the heel narrowing to a 13.5 half-width
|
|
337
|
+
// wrist; right edge bulging to 22 at the little finger. Trace: 366 px across
|
|
338
|
+
// the palm alone, 430 px including the thumb, 644 px tip to wrist.
|
|
339
|
+
const PALM_MASS = cw([
|
|
340
|
+
[-19.4, -53.5], [-19.6, -46], [-19.2, -38], [-18.3, -30], [-16.6, -18],
|
|
341
|
+
[-14.8, -6], [-13.5, 0], [-14.6, 20], [-15.5, 300],
|
|
342
|
+
[15.5, 300], [14.6, 20], [13.5, 0],
|
|
343
|
+
[15.4, -7], [18.0, -16], [20.2, -28], [21.6, -40], [22.0, -50], [21.6, -54],
|
|
344
|
+
[12, -54.5], [0, -55], [-10, -54.5],
|
|
345
|
+
]);
|
|
346
|
+
|
|
347
|
+
// base x, tip x, tip y, base width, tip width. The base y is the same for all
|
|
348
|
+
// four — 7.5 art below the palm's top edge, so no finger has a visible
|
|
349
|
+
// shoulder — and the tips splay outward, which is what "slightly spread" is.
|
|
350
|
+
const FINGERS = [
|
|
351
|
+
[-15.0, -18.5, -89.10, 8.8, 7.0], // index .924 of the middle's height
|
|
352
|
+
[-4.0, -5.5, -96.45, 8.6, 7.2], // middle 1
|
|
353
|
+
[7.2, 7.6, -91.05, 8.8, 7.0], // ring .944
|
|
354
|
+
[17.8, 20.8, -77.70, 7.4, 5.9], // little .806
|
|
355
|
+
].map(([bx, tx, ty, wb, wt]) => cw(digit([bx, -46], [tx, ty], wb, wt)));
|
|
356
|
+
|
|
357
|
+
// The thumb, two segments about an interphalangeal joint at (-29.5, -39.5). It
|
|
358
|
+
// is set 27 art units below the finger bases and swung 55 degrees off them,
|
|
359
|
+
// which is the trace's own angle and is what stops it reading as a fifth
|
|
360
|
+
// finger somebody shortened.
|
|
361
|
+
const PALM_THUMB = [
|
|
362
|
+
cw(digit([-15, -19], [-29.5, -39.5], 13.5, 10.5)),
|
|
363
|
+
cw(digit([-28, -37], [-37.5, -51.5], 10.5, 8.0)),
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
// The thenar ball: the one place a palm is not flat. Not a region of the
|
|
367
|
+
// silhouette but a bulge INSIDE it, and not a circle either — it is the muscle
|
|
368
|
+
// that works the thumb and it lies ALONG the thumb, so it is built with the
|
|
369
|
+
// same rounded-capped helper the digits are and pointed the same way. It runs
|
|
370
|
+
// past the palm's edge at its upper end on purpose: the thumb is painted after
|
|
371
|
+
// it and takes that end back.
|
|
372
|
+
const PALM_THENAR = cw(digit([-8.5, -12], [-17.5, -31], 16, 13));
|
|
373
|
+
|
|
374
|
+
// The distal crease, higher on the radial side, running out to the ulnar edge.
|
|
375
|
+
const PALM_CREASE = [[-10.5, -39.4], [-3, -36.6], [5, -33.6], [12.4, -31.8]];
|
|
376
|
+
// The thumb's joint, across its axis.
|
|
377
|
+
const PALM_THUMB_JOINT = [[-32.7, -36.9], [-29, -39.5], [-25.3, -42.1]];
|
|
378
|
+
|
|
379
|
+
// -- FIST -------------------------------------------------------------------
|
|
380
|
+
// 36 art across against the palm's 41, and 55 tall to the top of the knuckles.
|
|
381
|
+
const FIST_MASS = cw([
|
|
382
|
+
[-14.5, -52], [-16.6, -43.5], [-16.4, -32.5], [-15.8, -21], [-14.9, -13.5],
|
|
383
|
+
[-13.4, -6], [-12.7, 0], [-14.0, 20], [-15.0, 300],
|
|
384
|
+
[15.0, 300], [14.0, 20], [12.7, 0],
|
|
385
|
+
[14.6, -6], [17.6, -13.5], [19.1, -21], [19.6, -32.5], [19.6, -43.5],
|
|
386
|
+
// The knuckle row. Four rises with three shallow dips between them, 1.5 art
|
|
387
|
+
// deep — about two pixels at 1x, which is nothing there and is exactly what
|
|
388
|
+
// stops the top edge reading as a dome at 3x.
|
|
389
|
+
[17.9, -51], [15.0, -53.6], [11.8, -52.4], [8.6, -55.6], [4.6, -54.2],
|
|
390
|
+
[0.6, -56.4], [-3.4, -54.9], [-7.4, -55.2], [-11.0, -53.6],
|
|
391
|
+
]);
|
|
392
|
+
|
|
393
|
+
// The raised thumb, and the two numbers that matter are the ones that make it
|
|
394
|
+
// NOT the pointing index. First cut had it 25 art above the knuckles, 8.8 wide,
|
|
395
|
+
// rising from the middle of the fist — which is the description of an index
|
|
396
|
+
// finger, and at 1x `approve` and `wait` were the same picture. A thumb is
|
|
397
|
+
// SHORT and THICK and it comes off the SIDE: 21 art of clearance against the
|
|
398
|
+
// index's 38, 12.5 wide against 8, and its base is over the fist's thumb flank
|
|
399
|
+
// (negative x, the same side `point` folds its thumb down) rather than the
|
|
400
|
+
// centre. The tip leans a little further out than the base, which is the last
|
|
401
|
+
// of it: a thumb's axis is not the forearm's.
|
|
402
|
+
const FIST_THUMB = [
|
|
403
|
+
cw(digit([-11.5, -44], [-13.2, -63], 15.5, 12.8)),
|
|
404
|
+
cw(digit([-13.0, -60], [-14.6, -76], 12.6, 11.0)),
|
|
405
|
+
];
|
|
406
|
+
const FIST_THUMB_JOINT = [[-19.2, -62.6], [-13.1, -61.8], [-7.0, -61.0]];
|
|
407
|
+
|
|
408
|
+
// The three creases between the four curled fingers. Traced at y -43.0, -31.8
|
|
409
|
+
// and -20.7, spanning x -7.9 to +15.8 — they stop well short of the silhouette
|
|
410
|
+
// at both ends, which is what keeps them creases and not cuts.
|
|
411
|
+
const FIST_BANDS = [
|
|
412
|
+
[[-7.5, -43.6], [0, -43.1], [8, -42.7], [15.6, -42.6]],
|
|
413
|
+
[[-7.5, -32.4], [0, -31.9], [8, -31.5], [15.6, -31.4]],
|
|
414
|
+
[[-7.0, -21.3], [0, -20.8], [8, -20.4], [15.0, -20.3]],
|
|
415
|
+
];
|
|
416
|
+
|
|
417
|
+
// -- POINT ------------------------------------------------------------------
|
|
418
|
+
// The index, in two segments with a bend at the base knuckle: the proximal
|
|
419
|
+
// leans out, the distal comes back. Trace: tip at -93.75, 9.3 wide at the base.
|
|
420
|
+
const POINT_INDEX = [
|
|
421
|
+
cw(digit([5.2, -48], [3.4, -70], 9.6, 8.0)),
|
|
422
|
+
cw(digit([3.5, -68], [2.4, -93.75], 8.0, 6.6)),
|
|
423
|
+
];
|
|
424
|
+
const POINT_INDEX_JOINT = [[-0.4, -69.4], [3.5, -69.2], [7.4, -69.0]];
|
|
425
|
+
// The thumb, folded down the near side of the fist.
|
|
426
|
+
const POINT_THUMB = cw(digit([-9.5, -26], [-10.5, -50], 10.5, 9.5));
|
|
427
|
+
// The same three creases, started clear of the folded thumb.
|
|
428
|
+
const POINT_BANDS = FIST_BANDS.map((r) => r.map(([x, y], i) => [i === 0 ? -4 : x, y]));
|
|
429
|
+
|
|
430
|
+
// Mark width profiles, in upstream's units, sampled along the run 0..1.
|
|
431
|
+
const W_CREASE = [2, 8, 2];
|
|
432
|
+
const W_JOINT = [3, 7, 3];
|
|
433
|
+
const W_BAND = [2.5, 6, 6, 2.5];
|
|
434
|
+
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
// THE KNUCKLE BANDS, AGAIN, FOR A PEN — and why the pen cannot have the flat
|
|
437
|
+
// idiom's geometry here when it takes the flat idiom's geometry everywhere else.
|
|
438
|
+
//
|
|
439
|
+
// The three runs above are the trace's, and they are right for a TONE: round
|
|
440
|
+
// fills them with a translucent crease rung, edge to edge across the fist, and
|
|
441
|
+
// a tone that runs out at the silhouette has no ends to notice. Painted in the
|
|
442
|
+
// pen's near-black at a constant weight they are three parallel rules across a
|
|
443
|
+
// rounded form, they meet the silhouette line at both ends, and what a viewer
|
|
444
|
+
// counts at 1x is lines and not fingers — "three ruled lines" was the 1x note
|
|
445
|
+
// and it is the right note. Two of the three run within five art units of the
|
|
446
|
+
// frame's own bottom cut, which is the second-crop-line problem `lowerRegion`
|
|
447
|
+
// spends a paragraph on, arriving from the other direction.
|
|
448
|
+
//
|
|
449
|
+
// So the pen gets its own bands, and it is the only place in this file where an
|
|
450
|
+
// idiom changes a MEASUREMENT rather than a paint. Four changes, all of them
|
|
451
|
+
// the same idea — a crease is an EVENT on a form, not a division of it:
|
|
452
|
+
//
|
|
453
|
+
// * SHORTENED to the middle of the traced run, so a band starts and ends over
|
|
454
|
+
// bare skin and never touches the outline it would otherwise cut;
|
|
455
|
+
// * STAGGERED, longest at the index/middle end of the stack and shortest at
|
|
456
|
+
// the row nearest the wrist, so three marks are not one ruled grid;
|
|
457
|
+
// * BOWED toward the fingertips, which are DOWN the page in a curled fist —
|
|
458
|
+
// the knuckle row is this shape's top edge — so each band sags where the
|
|
459
|
+
// finger's own mass is thickest;
|
|
460
|
+
// * TAPERED to nothing at both ends by the ribbon below, which is the other
|
|
461
|
+
// half of "no ends to notice": round's marks always had that and a stroked
|
|
462
|
+
// run, whose `lineWidth` is one scalar, could not.
|
|
463
|
+
//
|
|
464
|
+
// And `wait` drops the row nearest the wrist entirely. It sits under four art
|
|
465
|
+
// units above the crop on that gesture — a horizontal a viewer reads against
|
|
466
|
+
// the frame edge rather than against the hand — and `wait` is the gesture with
|
|
467
|
+
// an index finger of its own to carry the read.
|
|
468
|
+
//
|
|
469
|
+
// The shortened band no longer reaches the fist's thumb flank, which retires
|
|
470
|
+
// `POINT_BANDS` for the pen: the whole of that table was "the same three
|
|
471
|
+
// creases, started clear of the folded thumb", and a band that stops at x -0.6
|
|
472
|
+
// was never near it. The pen therefore takes FIST's bands on both shapes.
|
|
473
|
+
const PEN_BAND_N = 5; // points per band
|
|
474
|
+
const PEN_BAND_KEEP = [0.70, 0.60, 0.50]; // fraction of the traced run kept
|
|
475
|
+
const PEN_BAND_BOW = 1.1; // art units of sag at mid-span
|
|
476
|
+
// Width along a band and along the palm crease, as fractions of the pen's mark
|
|
477
|
+
// width. Both end at 0: the ring closes to a point, which is the taper.
|
|
478
|
+
const A_BAND = [0, 0.88, 1, 0.88, 0];
|
|
479
|
+
const A_CREASE = [0, 1, 1, 0];
|
|
480
|
+
// A ribbon that peaks at the nib's own width carries LESS ink than a stroke of
|
|
481
|
+
// that width, because it is only that wide in the middle. The gain buys the
|
|
482
|
+
// difference back at the one place it is read.
|
|
483
|
+
const PEN_MARK_GAIN = 1.2;
|
|
484
|
+
// …and the alpha, which is per SHAPE because the two kinds of interior mark in
|
|
485
|
+
// this drawing are not the same kind of mark. See `penMarkA` in SHAPE.
|
|
486
|
+
const A_ONE = 1; // one accent on a form: full ink, like `jawInk`
|
|
487
|
+
const A_MANY = 0.62; // a repeated mark inside a silhouette: read after it
|
|
488
|
+
|
|
489
|
+
// The point at arc-length fraction t along an open polyline. The bands are
|
|
490
|
+
// four points of a near-straight run, so "the middle 58%" has to be measured
|
|
491
|
+
// along the run and not counted in points.
|
|
492
|
+
const alongRun = (pts, t) => {
|
|
493
|
+
const seg = [];
|
|
494
|
+
let total = 0;
|
|
495
|
+
for (let i = 1; i < pts.length; i++) {
|
|
496
|
+
const d = Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]);
|
|
497
|
+
seg.push(d);
|
|
498
|
+
total += d;
|
|
499
|
+
}
|
|
500
|
+
let want = clamp(t, 0, 1) * total;
|
|
501
|
+
for (let i = 0; i < seg.length; i++) {
|
|
502
|
+
if (want <= seg[i] || i === seg.length - 1) {
|
|
503
|
+
const u = seg[i] ? clamp(want / seg[i], 0, 1) : 0;
|
|
504
|
+
return [pts[i][0] + (pts[i + 1][0] - pts[i][0]) * u,
|
|
505
|
+
pts[i][1] + (pts[i + 1][1] - pts[i][1]) * u];
|
|
506
|
+
}
|
|
507
|
+
want -= seg[i];
|
|
508
|
+
}
|
|
509
|
+
return pts[pts.length - 1].slice();
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const penBand = (run, keep, bow) => Array.from({ length: PEN_BAND_N }, (_, i) => {
|
|
513
|
+
const u = i / (PEN_BAND_N - 1);
|
|
514
|
+
const [x, y] = alongRun(run, 0.5 - keep / 2 + keep * u);
|
|
515
|
+
return [x, y + bow * Math.sin(Math.PI * u)];
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
const PEN_BANDS = FIST_BANDS.map((r, i) => penBand(r, PEN_BAND_KEEP[i], PEN_BAND_BOW));
|
|
519
|
+
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
// Three more build-time helpers, for the interior marks and the shade plane.
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
|
|
524
|
+
// Sample a width profile at t in 0..1.
|
|
525
|
+
const profile = (ws) => (t) => {
|
|
526
|
+
if (ws.length === 1) return ws[0];
|
|
527
|
+
const u = clamp(t, 0, 1) * (ws.length - 1);
|
|
528
|
+
const i = Math.min(Math.floor(u), ws.length - 2);
|
|
529
|
+
return ws[i] + (ws[i + 1] - ws[i]) * (u - i);
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
// Offset an OPEN run along its own normal. `bulge` in ../path.mjs offsets in y
|
|
533
|
+
// only, which is right for a brow and wrong for a thumb joint that runs at 50
|
|
534
|
+
// degrees; this is the same idea with the normal taken from the neighbours.
|
|
535
|
+
// Fixed-opcode: n points in, n points out.
|
|
536
|
+
const offsetRun = (pts, wAt) => {
|
|
537
|
+
const n = pts.length;
|
|
538
|
+
return pts.map(([x, y], i) => {
|
|
539
|
+
const p = pts[Math.max(0, i - 1)], q = pts[Math.min(n - 1, i + 1)];
|
|
540
|
+
const dx = q[0] - p[0], dy = q[1] - p[1], l = Math.hypot(dx, dy) || 1;
|
|
541
|
+
const w = wAt(n === 1 ? 0 : i / (n - 1));
|
|
542
|
+
return [x - (dy / l) * w, y + (dx / l) * w];
|
|
543
|
+
});
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
// A tapered interior mark, as the closed ring `strip` wants: the run pushed off
|
|
547
|
+
// both ways by half the profile. Widths arrive in ART units.
|
|
548
|
+
const ribbon = (pts, ws) => {
|
|
549
|
+
const half = profile(ws.map((w) => w / 2));
|
|
550
|
+
return cw(strip(offsetRun(pts, half), offsetRun(pts, (t) => -half(t))));
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
// The same, for the flat idiom's marks, whose widths are quoted in upstream's
|
|
554
|
+
// units because that is where they were tuned.
|
|
555
|
+
const mark = (pts, ws) => ribbon(pts, ws.map((w) => w * MARK_K));
|
|
556
|
+
|
|
557
|
+
// The part of a closed silhouette below a line, as its own closed run: the
|
|
558
|
+
// silhouette's own points from where it crosses the line, round through the two
|
|
559
|
+
// tails, back to where it crosses again, closed by a straight chord.
|
|
560
|
+
//
|
|
561
|
+
// This is the shade plane, and the line is TILTED rather than horizontal. Two
|
|
562
|
+
// separate reasons, and the second one was learnt at 1x:
|
|
563
|
+
//
|
|
564
|
+
// * `side` mirrors the drawing in x, so a plane down one flank would be on
|
|
565
|
+
// the shadow side for one hand and on the lit side for the other. A plane
|
|
566
|
+
// across the BOTTOM is the same plane either way, and it is also the true
|
|
567
|
+
// one: the heel of a hand held up in front of a face is the part turning
|
|
568
|
+
// away from a light that is above it.
|
|
569
|
+
// * but a straight horizontal edge, a dozen art units above the frame cut and
|
|
570
|
+
// PARALLEL to it, is a second crop line. At 1x the first cut of this drawing
|
|
571
|
+
// read as a hand resting on a shelf — two horizontals across a rounded form,
|
|
572
|
+
// and the eye takes the pair for an object and its edge. `tilt` is how many
|
|
573
|
+
// art units the line falls across a half-width; at 5 over 22 it is about 12
|
|
574
|
+
// degrees off the crop, which is enough that the two lines stop rhyming and
|
|
575
|
+
// the band reads as the heel turning under.
|
|
576
|
+
//
|
|
577
|
+
// Fixed-opcode: the two crossings and the chord's interior points are a fixed
|
|
578
|
+
// count, and which silhouette points are below a CONSTANT line is fixed too.
|
|
579
|
+
// `inset` is the one number that is not geometry: a closed Catmull-Rom rounds
|
|
580
|
+
// the two corners where the chord meets the silhouette, and it rounds them
|
|
581
|
+
// OUTWARD, which puts shade on the background beside the hand. Pulling the two
|
|
582
|
+
// crossings toward each other along the chord moves those corners back inside
|
|
583
|
+
// by more than the rounding takes them out. There is no clipping anywhere in
|
|
584
|
+
// this pipeline, so this is the only place to fix it.
|
|
585
|
+
const HALF_W = 22;
|
|
586
|
+
const lowerRegion = (pts, ys, { inset = 2, mids = 3, tilt = 5 } = {}) => {
|
|
587
|
+
const n = pts.length, at = (i) => pts[((i % n) + n) % n];
|
|
588
|
+
const lineY = (x) => ys + (tilt * x) / HALF_W;
|
|
589
|
+
const below = (p) => p[1] > lineY(p[0]);
|
|
590
|
+
const cross = (a, b) => {
|
|
591
|
+
const fa = a[1] - lineY(a[0]), fb = b[1] - lineY(b[0]);
|
|
592
|
+
const t = fa / (fa - fb);
|
|
593
|
+
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
|
594
|
+
};
|
|
595
|
+
// Seed on a point that IS below the line before walking out from it. Index 0
|
|
596
|
+
// is not it: every silhouette here starts at the TOP of the shape, so seeding
|
|
597
|
+
// at 0 made both `cross` calls interpolate between two points on the same
|
|
598
|
+
// side of the line, `t` came out far outside 0..1, and the chord shot off as
|
|
599
|
+
// a hairline sliver across the shoulder. The deepest point is always below.
|
|
600
|
+
let seed = 0;
|
|
601
|
+
for (let i = 1; i < n; i++) if (pts[i][1] > pts[seed][1]) seed = i;
|
|
602
|
+
let i0 = seed; while (below(at(i0 - 1))) i0--;
|
|
603
|
+
let i1 = seed; while (below(at(i1 + 1))) i1++;
|
|
604
|
+
const a0 = cross(at(i0 - 1), at(i0)), a1 = cross(at(i1), at(i1 + 1));
|
|
605
|
+
const dx = Math.sign(a1[0] - a0[0]) * inset;
|
|
606
|
+
const start = [a0[0] + dx, lineY(a0[0] + dx)], end = [a1[0] - dx, lineY(a1[0] - dx)];
|
|
607
|
+
const run = [start];
|
|
608
|
+
for (let i = i0; i <= i1; i++) run.push(at(i).slice());
|
|
609
|
+
run.push(end);
|
|
610
|
+
for (let j = 1; j <= mids; j++) {
|
|
611
|
+
const t = j / (mids + 1);
|
|
612
|
+
run.push([end[0] + (start[0] - end[0]) * t, end[1] + (start[1] - end[1]) * t]);
|
|
613
|
+
}
|
|
614
|
+
return cw(run);
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
// ---------------------------------------------------------------------------
|
|
618
|
+
// The three shapes, as lists of runs BY PAINT. Runs that share a paint and do
|
|
619
|
+
// not need anything between them share a draw, which is how a drawing with
|
|
620
|
+
// nine digits in it still costs thirteen draws — and 26 with a pen, where a
|
|
621
|
+
// run that overlaps another can no longer share one (`splitAt`, below).
|
|
622
|
+
//
|
|
623
|
+
// under painted first, a rung darker, so the mass swallows its base
|
|
624
|
+
// lit the face rung: [ [mass], [digits] ]
|
|
625
|
+
// shade the form plane, plus any bulge inside the outline
|
|
626
|
+
// marks [run, width profile] pairs, all of them one crease-coloured draw
|
|
627
|
+
//
|
|
628
|
+
// The form line of each shape — the `lowerRegion` cut — is chosen against that
|
|
629
|
+
// gesture's own hold, so the visible band is the lower third of what is above
|
|
630
|
+
// the frame edge and its bottom runs off frame. A shade that ENDS at the crop
|
|
631
|
+
// draws a second edge there and the crop stops reading as a crop.
|
|
632
|
+
// ---------------------------------------------------------------------------
|
|
633
|
+
|
|
634
|
+
// Where a group of runs stops being disjoint, and why only a PEN cares.
|
|
635
|
+
//
|
|
636
|
+
// Every run in a `lit` or `under` group shares one flat fill, so a flat idiom
|
|
637
|
+
// can put all of them in one draw: overlapping fills of one colour have no
|
|
638
|
+
// visible edge. An OUTLINED idiom cannot. The two segments of a thumb (or of
|
|
639
|
+
// `wait`'s index) overlap at the joint by construction — each one's base runs
|
|
640
|
+
// 15% past its base point so the join has no shoulder — and two contours over
|
|
641
|
+
// one fill is two curves crossing where a hand has one crease. So the pen
|
|
642
|
+
// splits such a group at the given index and paints the far half after the near
|
|
643
|
+
// half's line, which is the same hidden-line removal the rest of this order is:
|
|
644
|
+
// the distal segment's fill covers the proximal one's cap, and what is left is
|
|
645
|
+
// one line across the digit.
|
|
646
|
+
//
|
|
647
|
+
// `null` is "one layer", which is what round asks for and what keeps its bytes.
|
|
648
|
+
const splitAt = (runs, i) => (i == null ? [runs] : [runs.slice(0, i), runs.slice(i)]);
|
|
649
|
+
|
|
650
|
+
// ...and the other half of the same problem, which `splitAt` cannot reach: a
|
|
651
|
+
// digit does not only overlap the digit beside it, it grows OUT OF THE MASS.
|
|
652
|
+
// Every base in this file is buried on purpose — `digit()` runs the base 15%
|
|
653
|
+
// past its base point "so the join into the palm has no shoulder" — and a flat
|
|
654
|
+
// idiom never sees it, because the buried end is the mass's own colour. A pen
|
|
655
|
+
// draws it: the four fingers came out as four capsules with rounded bottoms
|
|
656
|
+
// lying ON the palm, and `approve`'s thumb as a lobe with the fist's edge line
|
|
657
|
+
// ruled straight through it. Both are the same false line, on either side of
|
|
658
|
+
// the same boundary.
|
|
659
|
+
//
|
|
660
|
+
// So the pen strokes only the part of a run that is OUTSIDE the mass, and the
|
|
661
|
+
// fills do the rest of the work in this order:
|
|
662
|
+
//
|
|
663
|
+
// mass fill -> shade -> MASS LINE -> digit fill -> DIGIT ARC
|
|
664
|
+
//
|
|
665
|
+
// The digit's fill covers the mass's line where the two overlap; the digit's
|
|
666
|
+
// arc starts and ends exactly where the mass's line reappears, because both
|
|
667
|
+
// endpoints are ON the mass's contour. What comes out is the union outline,
|
|
668
|
+
// with no clipping anywhere and no boolean geometry — two draws in the right
|
|
669
|
+
// order and a run cut at two points, once, at module load.
|
|
670
|
+
//
|
|
671
|
+
// Even-odd crossing test, and the first hit along a segment. Both are plain
|
|
672
|
+
// O(n*m) on tables of at most 26 points, run once for nine runs.
|
|
673
|
+
const inPoly = (p, poly) => {
|
|
674
|
+
let hit = false;
|
|
675
|
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
676
|
+
const [xi, yi] = poly[i], [xj, yj] = poly[j];
|
|
677
|
+
if ((yi > p[1]) !== (yj > p[1])
|
|
678
|
+
&& p[0] < ((xj - xi) * (p[1] - yi)) / (yj - yi) + xi) hit = !hit;
|
|
679
|
+
}
|
|
680
|
+
return hit;
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const segHit = (a, b, poly) => {
|
|
684
|
+
const rx = b[0] - a[0], ry = b[1] - a[1];
|
|
685
|
+
let best = null;
|
|
686
|
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
687
|
+
const c = poly[j], d = poly[i];
|
|
688
|
+
const sx = d[0] - c[0], sy = d[1] - c[1];
|
|
689
|
+
const den = rx * sy - ry * sx;
|
|
690
|
+
if (!den) continue;
|
|
691
|
+
const t = ((c[0] - a[0]) * sy - (c[1] - a[1]) * sx) / den;
|
|
692
|
+
const u = ((c[0] - a[0]) * ry - (c[1] - a[1]) * rx) / den;
|
|
693
|
+
if (t >= 0 && t <= 1 && u >= 0 && u <= 1 && (best === null || t < best)) best = t;
|
|
694
|
+
}
|
|
695
|
+
return best === null ? null : [a[0] + rx * best, a[1] + ry * best];
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
// The arc of a closed run that lies outside `mass`, as an OPEN run with its two
|
|
699
|
+
// ends interpolated onto the mass's boundary — or `null` for "nothing to cut",
|
|
700
|
+
// which is both "wholly outside" (a distal segment, whose own base cap IS the
|
|
701
|
+
// joint line and must stay) and "wholly inside" (`point`'s folded thumb, which
|
|
702
|
+
// is a line laid on the fist on purpose). A run that leaves and re-enters more
|
|
703
|
+
// than once is refused the same way, because none of these do and a silent
|
|
704
|
+
// wrong answer is worse than the line it was meant to remove.
|
|
705
|
+
//
|
|
706
|
+
// Fixed-opcode: the tables are constants, so which vertices are outside is a
|
|
707
|
+
// constant and so is the point count.
|
|
708
|
+
const carve = (run, mass) => {
|
|
709
|
+
const n = run.length;
|
|
710
|
+
const out = run.map((p) => !inPoly(p, mass));
|
|
711
|
+
const k = out.filter(Boolean).length;
|
|
712
|
+
if (k === 0 || k === n) return null;
|
|
713
|
+
let blocks = 0, i0 = -1;
|
|
714
|
+
for (let i = 0; i < n; i++) if (out[i] && !out[(i + n - 1) % n]) { blocks++; i0 = i; }
|
|
715
|
+
if (blocks !== 1) return null;
|
|
716
|
+
let i1 = i0;
|
|
717
|
+
while (out[(i1 + 1) % n]) i1++;
|
|
718
|
+
const A = segHit(run[i0], run[(i0 + n - 1) % n], mass);
|
|
719
|
+
const B = segHit(run[i1 % n], run[(i1 + 1) % n], mass);
|
|
720
|
+
const arc = A ? [A] : [];
|
|
721
|
+
for (let i = i0; i <= i1; i++) arc.push(run[i % n]);
|
|
722
|
+
if (B) arc.push(B);
|
|
723
|
+
return arc;
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
const SHAPE = {
|
|
727
|
+
PALM: {
|
|
728
|
+
under: [],
|
|
729
|
+
lit: [[PALM_MASS], [...FINGERS, ...PALM_THUMB]],
|
|
730
|
+
shade: [lowerRegion(PALM_MASS, -32), PALM_THENAR],
|
|
731
|
+
marks: [[PALM_CREASE, W_CREASE], [PALM_THUMB_JOINT, W_JOINT, 'joint']],
|
|
732
|
+
// Pen only: the same crease, the joint dropped (the distal segment's own
|
|
733
|
+
// contour is that line), and the widths solved on the way out. See below.
|
|
734
|
+
penMarks: [[PALM_CREASE, A_CREASE]], penMarkA: A_ONE,
|
|
735
|
+
// Pen only: where the runs of a group stop being disjoint. See `splitAt`.
|
|
736
|
+
split: { 1: 5 }, // the four fingers and the thumb's root, then its tip
|
|
737
|
+
},
|
|
738
|
+
FIST: {
|
|
739
|
+
// The raised thumb is the whole of this gesture and it is in the SAME light
|
|
740
|
+
// as the fist: `underLit`. It is still painted first so the mass swallows
|
|
741
|
+
// its base — under, but not darker.
|
|
742
|
+
under: FIST_THUMB, underLit: true,
|
|
743
|
+
lit: [[FIST_MASS]],
|
|
744
|
+
shade: [lowerRegion(FIST_MASS, -28)],
|
|
745
|
+
marks: [[FIST_THUMB_JOINT, W_JOINT, 'joint'], ...FIST_BANDS.map((b) => [b, W_BAND])],
|
|
746
|
+
penMarks: PEN_BANDS.map((b) => [b, A_BAND]), penMarkA: A_MANY,
|
|
747
|
+
split: { under: 1 },
|
|
748
|
+
},
|
|
749
|
+
POINT: {
|
|
750
|
+
// This thumb is tucked at the side of the fist rather than raised, so here
|
|
751
|
+
// the darker rung is the point: it is the part of the hand turned away.
|
|
752
|
+
under: [POINT_THUMB], underLit: false,
|
|
753
|
+
lit: [[FIST_MASS], POINT_INDEX],
|
|
754
|
+
shade: [lowerRegion(FIST_MASS, -28)],
|
|
755
|
+
marks: [[POINT_INDEX_JOINT, W_JOINT, 'joint'], ...POINT_BANDS.map((b) => [b, W_BAND])],
|
|
756
|
+
// Two bands, not three: the row nearest the wrist goes on this gesture.
|
|
757
|
+
penMarks: PEN_BANDS.slice(0, 2).map((b) => [b, A_BAND]), penMarkA: A_MANY,
|
|
758
|
+
split: { 1: 1 },
|
|
759
|
+
},
|
|
760
|
+
};
|
|
761
|
+
for (const s of Object.values(SHAPE)) {
|
|
762
|
+
s.markRuns = s.marks.map(([p, w]) => mark(p, w));
|
|
763
|
+
// every point the shape owns, for checkFraming
|
|
764
|
+
s.hull = [...s.under, ...s.lit.flat(), s.shade[0]].flat();
|
|
765
|
+
// Pen only, and computed here rather than in `draws` because it is geometry
|
|
766
|
+
// and geometry in this file is a constant: the arc of every digit and every
|
|
767
|
+
// thumb segment that is not swallowed by the mass it grows from. `null` per
|
|
768
|
+
// run means "stroke the closed run" — see `carve`.
|
|
769
|
+
const mass = s.lit[0][0];
|
|
770
|
+
s.penUnder = s.under.map((r) => (s.underLit ? carve(r, mass) : null));
|
|
771
|
+
s.penLit = s.lit.map((grp, i) => (i === 0 ? grp.map(() => null) : grp.map((r) => carve(r, mass))));
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ---------------------------------------------------------------------------
|
|
775
|
+
// THE GESTURES. Upstream's four timelines, reparametrised.
|
|
776
|
+
//
|
|
777
|
+
// Upstream keys in MILLISECONDS against its own duration, and its `dy` keys
|
|
778
|
+
// are absolute distances below a bottom edge that is 200 units under peep's
|
|
779
|
+
// chin. Neither survives a change of framing, so both were re-expressed:
|
|
780
|
+
//
|
|
781
|
+
// * the KEY is `progress`, the fraction of the gesture the mixer sends. So
|
|
782
|
+
// `dur` is documentation and the shape of the motion is duration-free.
|
|
783
|
+
// * `out` is in ART units — a multiple of the hand's own size — so the hand
|
|
784
|
+
// sits the same distance from the head whatever the frame is.
|
|
785
|
+
// * `dy` is a TRAVEL FRACTION `up`, from HIDE toward the gesture's `hold`:
|
|
786
|
+
// `dy = HIDE - up * (HIDE - hold)`. That keeps everything the curve was
|
|
787
|
+
// carrying — the ballistic placement (55-74% of the rise inside the first
|
|
788
|
+
// 12% of the gesture), the ~2% overshoot and settle, the breathing hold,
|
|
789
|
+
// the steeper exit — and lets `hold` be solved for OUR frame.
|
|
790
|
+
//
|
|
791
|
+
// `hold` is in art units and is solved from the CROP rather than from a
|
|
792
|
+
// station: `hold = crop * sc`, where `crop` is how far above the wrist the
|
|
793
|
+
// frame's bottom edge falls. Sixteen to eighteen art units up is where the heel
|
|
794
|
+
// has already lost a fifth of its width, so the narrowing shows and the wrist
|
|
795
|
+
// does not (rule 1). What comes out, as a fraction of the distance from the
|
|
796
|
+
// bottom edge to the chin (464.5 design units on round), is rule 3:
|
|
797
|
+
//
|
|
798
|
+
// greet 0.725 fingertips at the neck, a clear head-height below the chin
|
|
799
|
+
// farewell 0.728 the same, a hair higher, because it waves longer
|
|
800
|
+
// approve 0.728 the fist at the neck, and the THUMB alone above it
|
|
801
|
+
// wait 0.820 the index highest of the four, and still short of the chin
|
|
802
|
+
//
|
|
803
|
+
// Three of those four are the same number, and that is the floor talking: at
|
|
804
|
+
// this size rule 1 binds before rule 3 does, so `hold` is not free to place
|
|
805
|
+
// greet lower than approve any more. The only gesture with room above it is
|
|
806
|
+
// `wait`, and it is the one that is supposed to have room.
|
|
807
|
+
//
|
|
808
|
+
// `sc` is depth: how far the hand is pushed toward the lens. A thumbs-up is
|
|
809
|
+
// pushed at the camera and a wave is thrown out to the side, which is why
|
|
810
|
+
// `approve` is the biggest and `greet` the smallest. Upstream's four ratios,
|
|
811
|
+
// all four scaled by 0.96 — the last 4% of the area match against peep.
|
|
812
|
+
//
|
|
813
|
+
// `rot` is in degrees and is scale-free, so it is upstream's, unchanged. The
|
|
814
|
+
// swings are what makes a wave a wave: three for greet, four for farewell,
|
|
815
|
+
// and a near-still hold for the other two.
|
|
816
|
+
// ---------------------------------------------------------------------------
|
|
817
|
+
|
|
818
|
+
export const GESTURES = {
|
|
819
|
+
greet: {
|
|
820
|
+
dur: 1250, shape: 'PALM', sc: 0.67, hold: 12.73, swings: 3,
|
|
821
|
+
out: [[0, 0], [0.12, 30.833], [0.24, 47.5], [0.80, 47.5], [1, 12.5]],
|
|
822
|
+
up: [[0, 0], [0.12, 0.6399], [0.24, 1.0230], [0.312, 0.9847], [0.56, 1.0038],
|
|
823
|
+
[0.80, 0.9885], [0.896, 0.8391], [1, 0]],
|
|
824
|
+
rot: [[0, -3], [0.12, 2], [0.248, 16], [0.38, -2], [0.512, 16], [0.644, -1],
|
|
825
|
+
[0.776, 12], [0.80, 8], [1, -3]],
|
|
826
|
+
},
|
|
827
|
+
farewell: {
|
|
828
|
+
dur: 1550, shape: 'PALM', sc: 0.67, hold: 12.40, swings: 4,
|
|
829
|
+
out: [[0, 0], [0.10968, 31.667], [0.20645, 48.333], [0.83871, 48.333], [1, 13.333]],
|
|
830
|
+
up: [[0, 0], [0.10323, 0.6505], [0.20645, 1.0250], [0.26452, 0.9834],
|
|
831
|
+
[0.45161, 1.0061], [0.64516, 0.9871], [0.83871, 0.9985], [0.91613, 0.8434], [1, 0]],
|
|
832
|
+
rot: [[0, -3], [0.10968, 2], [0.21290, 16], [0.32903, -2], [0.44516, 16],
|
|
833
|
+
[0.56129, -2], [0.67742, 16], [0.79355, -1], [0.83871, 8], [1, -3]],
|
|
834
|
+
},
|
|
835
|
+
approve: {
|
|
836
|
+
dur: 1300, shape: 'FIST', sc: 0.79, hold: 12.64, swings: 0,
|
|
837
|
+
out: [[0, 0], [0.12308, 22.5], [0.23077, 35], [0.80769, 35], [1, 13.333]],
|
|
838
|
+
up: [[0, 0], [0.12308, 0.7421], [0.23077, 1.0204], [0.30, 0.9870],
|
|
839
|
+
[0.53846, 1.0019], [0.80769, 0.9907], [0.89231, 0.8423], [1, 0]],
|
|
840
|
+
rot: [[0, -6], [0.12308, -2], [0.23077, 3], [0.30769, 0], [0.53846, 1.5],
|
|
841
|
+
[0.76923, 0], [0.80769, 0], [1, -8]],
|
|
842
|
+
},
|
|
843
|
+
wait: {
|
|
844
|
+
dur: 1700, shape: 'POINT', sc: 0.75, hold: 12.75, swings: 0,
|
|
845
|
+
out: [[0, 0], [0.10, 25], [0.18824, 40], [0.82353, 40], [1, 14.167]],
|
|
846
|
+
up: [[0, 0], [0.10, 0.6257], [0.18824, 1.0277], [0.24118, 0.9860],
|
|
847
|
+
[0.41176, 1.0011], [0.64706, 0.9879], [0.82353, 0.9973], [0.89412, 0.8305], [1, 0]],
|
|
848
|
+
rot: [[0, -7], [0.10, -2], [0.18824, 2], [0.24706, 0], [0.47059, 1],
|
|
849
|
+
[0.70588, -0.5], [0.82353, 0], [1, -9]],
|
|
850
|
+
},
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
// The depth each SHAPE is drawn at, which is a property of the shape and not of
|
|
854
|
+
// the gesture only because the two waves happen to agree: greet and farewell
|
|
855
|
+
// are both PALM at 0.67, approve is FIST, wait is POINT. The pen needs it — a
|
|
856
|
+
// filled ribbon scales with `sc` where a stroke's `lineWidth` does not, and the
|
|
857
|
+
// mark has to come out the same weight on screen either way — so the agreement
|
|
858
|
+
// is asserted here rather than assumed at the call site. A fifth gesture that
|
|
859
|
+
// reused a shape at a new depth would fail the build, which is the right place
|
|
860
|
+
// to find out.
|
|
861
|
+
const SHAPE_SC = {};
|
|
862
|
+
for (const [name, g] of Object.entries(GESTURES)) {
|
|
863
|
+
if (SHAPE_SC[g.shape] != null && SHAPE_SC[g.shape] !== g.sc) {
|
|
864
|
+
throw new Error(`hand: ${name} draws ${g.shape} at depth ${g.sc}, not ${SHAPE_SC[g.shape]}`);
|
|
865
|
+
}
|
|
866
|
+
SHAPE_SC[g.shape] = g.sc;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Smoothstep between keys, upstream's easing. Linear between two keys of a
|
|
870
|
+
// ballistic curve is what an arc-less rig looks like: the hand arrives, stops
|
|
871
|
+
// dead, and reads as a sprite being placed.
|
|
872
|
+
const smoothstep = (t) => t * t * (3 - 2 * t);
|
|
873
|
+
|
|
874
|
+
export function sample(keys, t) {
|
|
875
|
+
if (t <= keys[0][0]) return keys[0][1];
|
|
876
|
+
const last = keys[keys.length - 1];
|
|
877
|
+
if (t >= last[0]) return last[1];
|
|
878
|
+
for (let i = 1; i < keys.length; i++) {
|
|
879
|
+
if (t <= keys[i][0]) {
|
|
880
|
+
const [t0, v0] = keys[i - 1], [t1, v1] = keys[i];
|
|
881
|
+
return v0 + (v1 - v0) * smoothstep((t - t0) / (t1 - t0));
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return last[1];
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// ---------------------------------------------------------------------------
|
|
888
|
+
// makeHand
|
|
889
|
+
// ---------------------------------------------------------------------------
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* @param {object} o
|
|
893
|
+
* @param {object} o.P the avatar's parameter block (unused geometry-
|
|
894
|
+
* side; taken so the part has the same front door as the others and so a
|
|
895
|
+
* future idiom can reach the character's own proportions)
|
|
896
|
+
* @param {object} o.PALETTE needs `face` and an opaque plane a rung below it
|
|
897
|
+
* (`deep`, or `neck`, or last resort `shade`), plus `crease` — but only when
|
|
898
|
+
* there is no `pen`: a pen draws every interior mark as a LINE, so an
|
|
899
|
+
* outlined style is never asked for a crease rung it does not have. (ink's
|
|
900
|
+
* palette has none: `makePalette` there ends at the translucent overlays.)
|
|
901
|
+
* @param {Function} o.solid the paint registry's `solid`
|
|
902
|
+
* @param {object} o.frame { cx, bottom, reach, outboardLimit }
|
|
903
|
+
* @param {?object} o.pen the ink idiom's outline:
|
|
904
|
+
* `{ paint, w, mark, cap, join }`. `w` is the silhouette's stroke width and
|
|
905
|
+
* `mark` the interior marks', both in ARTBOARD units and both construction
|
|
906
|
+
* constants — `rig.js` copies a stroke from base to out and never blends it,
|
|
907
|
+
* so a pen width can never be a channel and a pen line never tapers. Off by
|
|
908
|
+
* default: round has no outlines and its bytes do not move when this is null.
|
|
909
|
+
* @param {string} o.group the draw group. NOT the head group and NOT the
|
|
910
|
+
* body group: the hand is in frame space and neither the head matrix nor the
|
|
911
|
+
* breath rise may reach it (src/live.js reads `meta.live.hand.slots`).
|
|
912
|
+
*/
|
|
913
|
+
export function makeHand({ P, PALETTE, solid, frame, pen = null, group = 'hand' }) {
|
|
914
|
+
const { cx, bottom, reach } = frame;
|
|
915
|
+
// The paints are resolved on the FIRST draw and not here, which is a
|
|
916
|
+
// one-line rule with a byte behind it: a paint registry hands out indices in
|
|
917
|
+
// the order it is first asked, so a part that registers at CONSTRUCTION time
|
|
918
|
+
// puts its colours in front of the face's and renumbers every draw in the
|
|
919
|
+
// rig. This part reuses three paints the face has already asked for by the
|
|
920
|
+
// time the hand is drawn, so asking late costs nothing and asking early
|
|
921
|
+
// costs a rebuild of every file. Three Map lookups on the first frame.
|
|
922
|
+
//
|
|
923
|
+
// THREE TONES, and the middle one is not `PALETTE.shade`. That entry is the
|
|
924
|
+
// face's side plane and it is TRANSLUCENT on purpose (round: 17% warm brown),
|
|
925
|
+
// which works there because there is always skin under it. Nothing is under
|
|
926
|
+
// the hand: it is the frontmost thing in the frame, over the shirt and over
|
|
927
|
+
// the background. Painted at 17% the heel plane vanished against skin and the
|
|
928
|
+
// raised thumb came out a green ghost over the tee — measured (91,111,94),
|
|
929
|
+
// which is neither a hand colour nor a shirt colour. So the plane takes an
|
|
930
|
+
// OPAQUE rung a step below the lit one; `neck` already is exactly that (one
|
|
931
|
+
// skin rung down, opaque) and costs no new paint. The crease may stay
|
|
932
|
+
// translucent: every mark is inside the silhouette with skin beneath it.
|
|
933
|
+
let paint = null;
|
|
934
|
+
const paints = () => (paint || (paint = {
|
|
935
|
+
face: solid(PALETTE.face),
|
|
936
|
+
deep: solid(PALETTE.deep || PALETTE.neck || PALETTE.shade),
|
|
937
|
+
crease: pen ? solid(pen.paint) : solid(PALETTE.crease),
|
|
938
|
+
pen: pen ? solid(pen.paint) : 0,
|
|
939
|
+
}));
|
|
940
|
+
// The one stroke descriptor, built once. `stroke.paint` is deliberately not
|
|
941
|
+
// among its keys — `render2d.js` strokes with the DRAW's paint and
|
|
942
|
+
// `validate.mjs` refuses a rig that carries a field nothing reads.
|
|
943
|
+
//
|
|
944
|
+
// There is no second nib for the marks any more, and that is the whole of the
|
|
945
|
+
// taper: a stroke's `lineWidth` is ONE SCALAR for the length of the run, so a
|
|
946
|
+
// stroked mark ends in a blunt round cap and cannot do anything else. The pen
|
|
947
|
+
// draws its marks as filled ribbons instead — the same tapered ring round
|
|
948
|
+
// fills, in the pen's own colour — which costs no draw (the marks were
|
|
949
|
+
// already one draw per shape) and buys ends that go to a point.
|
|
950
|
+
const nib = (w) => ({ w, cap: pen.cap || 'round', join: pen.join || 'round' });
|
|
951
|
+
const PEN_SIL = pen ? nib(pen.w) : null;
|
|
952
|
+
|
|
953
|
+
// …and the ribbons themselves, once, here rather than at module load, because
|
|
954
|
+
// their width is the only geometry in this file that is NOT in art units:
|
|
955
|
+
// `pen.mark` is a distance on the artboard, the way a `lineWidth` is, and it
|
|
956
|
+
// has to come out that thick on screen whatever the frame and whatever the
|
|
957
|
+
// gesture's depth. Solving `art = mark / (reach * sc)` is what holds the mark
|
|
958
|
+
// to one nib the way a stroke did — see SHAPE_SC — and it is the reason a
|
|
959
|
+
// ribbon can replace a stroke without the drawing getting heavier when the
|
|
960
|
+
// hand is pushed at the lens.
|
|
961
|
+
const penMarks = {};
|
|
962
|
+
if (pen) {
|
|
963
|
+
const wm = pen.mark || pen.w;
|
|
964
|
+
for (const [key, s] of Object.entries(SHAPE)) {
|
|
965
|
+
const w = (wm * PEN_MARK_GAIN) / (reach * SHAPE_SC[key]);
|
|
966
|
+
penMarks[key] = s.penMarks.map(([run, prof]) => ribbon(run, prof.map((k) => k * w)));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// The affine, once per run per frame: rotate about the wrist, scale by depth
|
|
971
|
+
// (and MIRROR in x by `dir`, which is what makes one drawing two hands), then
|
|
972
|
+
// translate to the placement. `reach` is folded into the scale so the tables
|
|
973
|
+
// above stay in art units.
|
|
974
|
+
const place = (pts, x, y, rot, sc, dir) => {
|
|
975
|
+
const r = (rot * Math.PI) / 180, cs = Math.cos(r), sn = Math.sin(r);
|
|
976
|
+
const kx = -dir * sc * reach, ky = sc * reach;
|
|
977
|
+
return pts.map(([px, py]) => [
|
|
978
|
+
x + kx * (px * cs - py * sn),
|
|
979
|
+
y + ky * (px * sn + py * cs),
|
|
980
|
+
]);
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
// Where a shape sits at `t` through its gesture, or parked.
|
|
984
|
+
const at = (g, t, dir) => (g
|
|
985
|
+
? {
|
|
986
|
+
x: cx + dir * sample(g.out, t) * reach,
|
|
987
|
+
y: bottom + (HIDE - sample(g.up, t) * (HIDE - g.hold)) * reach,
|
|
988
|
+
rot: sample(g.rot, t), sc: g.sc, dir,
|
|
989
|
+
}
|
|
990
|
+
: { x: cx, y: bottom + HIDE * reach, rot: 0, sc: 1, dir });
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* @param {?{gesture:?string, progress:number, side:string}} h a HandFrame
|
|
994
|
+
* @param {object} env unused; the part convention's fourth argument
|
|
995
|
+
*/
|
|
996
|
+
function draws(h, env = {}) {
|
|
997
|
+
const out = [];
|
|
998
|
+
const push = drawPusher(out);
|
|
999
|
+
const C = paints();
|
|
1000
|
+
|
|
1001
|
+
const g = h && GESTURES[h.gesture] ? GESTURES[h.gesture] : null;
|
|
1002
|
+
const t = g ? clamp(typeof h.progress === 'number' ? h.progress : 0, 0, 1) : 0;
|
|
1003
|
+
const dir = h && h.side === 'left' ? -1 : 1;
|
|
1004
|
+
const p = at(g, t, dir);
|
|
1005
|
+
const T = (pts) => spline(place(pts, p.x, p.y, p.rot, p.sc, p.dir), 1);
|
|
1006
|
+
const many = (runs) => contours(...runs.map(T));
|
|
1007
|
+
// The same, with two free ends: what a pen's interior marks are, and what a
|
|
1008
|
+
// carved silhouette arc is, where a flat idiom fills a tapered ring or
|
|
1009
|
+
// buries the end in the mass instead.
|
|
1010
|
+
const TO = (pts) => openSpline(place(pts, p.x, p.y, p.rot, p.sc, p.dir), 1);
|
|
1011
|
+
// A pen's layer: runs paired with their carved arc (`null` = stroke the
|
|
1012
|
+
// closed run), split where the group stops being disjoint. The fill is
|
|
1013
|
+
// always the whole run; only the LINE is cut.
|
|
1014
|
+
const layers = (runs, arcs, k) => splitAt(runs.map((r, i) => [r, arcs[i]]), k);
|
|
1015
|
+
const fillOf = (L) => many(L.map(([r]) => r));
|
|
1016
|
+
const inkOf = (L) => contours(...L.map(([r, c]) => (c ? TO(c) : T(r))));
|
|
1017
|
+
|
|
1018
|
+
// All three shape sets, every frame, with alpha choosing between them —
|
|
1019
|
+
// three topologies cannot be one interpolatable draw and pretending they
|
|
1020
|
+
// can is how a display list stops being interpolatable.
|
|
1021
|
+
for (const [key, s] of Object.entries(SHAPE)) {
|
|
1022
|
+
const a = g && g.shape === key ? 1 : 0;
|
|
1023
|
+
const n = key.toLowerCase();
|
|
1024
|
+
// ---- THE FLAT IDIOM ------------------------------------------------
|
|
1025
|
+
// Thirteen draws, and round's bytes are this branch: nothing below it
|
|
1026
|
+
// may be reachable with `pen` null.
|
|
1027
|
+
//
|
|
1028
|
+
// UNDER first: `approve`'s thumb and `point`'s folded thumb go beneath
|
|
1029
|
+
// the mass they belong to, so the mass swallows their base and they
|
|
1030
|
+
// emerge exactly as far as they should. Then MASS, the planes, and the
|
|
1031
|
+
// DIGITS on top of them. That order is not arbitrary and it is not
|
|
1032
|
+
// back-to-front either: the thenar ball is a bulge of the palm and the
|
|
1033
|
+
// thumb lies over it, so a shade drawn after the digits painted that ball
|
|
1034
|
+
// on top of its own thumb. Everything the shade draw contains belongs to
|
|
1035
|
+
// the mass; nothing in it belongs in front of a finger.
|
|
1036
|
+
if (!pen) {
|
|
1037
|
+
if (s.under.length) {
|
|
1038
|
+
push(`hand/${n}Thumb`, group, many(s.under), s.underLit ? C.face : C.deep, a);
|
|
1039
|
+
}
|
|
1040
|
+
push(`hand/${n}`, group, many(s.lit[0]), C.face, a);
|
|
1041
|
+
push(`hand/${n}Shade`, group, many(s.shade), C.deep, a);
|
|
1042
|
+
for (let i = 1; i < s.lit.length; i++) {
|
|
1043
|
+
push(`hand/${n}Digits`, group, many(s.lit[i]), C.face, a);
|
|
1044
|
+
}
|
|
1045
|
+
push(`hand/${n}Marks`, group, many(s.markRuns), C.crease, a);
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// ---- THE INK IDIOM -------------------------------------------------
|
|
1050
|
+
// Twenty-six draws, in one order that is nothing but hidden-line removal
|
|
1051
|
+
// in a pipeline with no clipping: every line is drawn, and then whatever
|
|
1052
|
+
// covers it is drawn over it.
|
|
1053
|
+
const split = s.split || {};
|
|
1054
|
+
// A FOLDED thumb is a PLANE, not a silhouette — `point`'s clears the
|
|
1055
|
+
// fist's edge by under 1.5 art units, so it is invisible in any idiom.
|
|
1056
|
+
// Its tone goes under the mass; its line goes over it, below.
|
|
1057
|
+
if (s.under.length && !s.underLit) {
|
|
1058
|
+
push(`hand/${n}Thumb`, group, many(s.under), C.deep, a);
|
|
1059
|
+
}
|
|
1060
|
+
push(`hand/${n}`, group, many(s.lit[0]), C.face, a);
|
|
1061
|
+
push(`hand/${n}Shade`, group, many(s.shade), C.deep, a);
|
|
1062
|
+
// The mass's line goes AFTER its shade plane — which would otherwise eat
|
|
1063
|
+
// the inner half of it, the reason ink's face draws `faceInk` after its
|
|
1064
|
+
// own shadow band — and BEFORE everything that grows out of the mass.
|
|
1065
|
+
push(`hand/${n}Ink`, group, many(s.lit[0]), C.pen, a, { stroke: PEN_SIL });
|
|
1066
|
+
// …and here is the folded thumb: a LINE laid over the fist, because in an
|
|
1067
|
+
// outlined idiom a fold is a line. Round says the same thing with a tone,
|
|
1068
|
+
// which is the only way a flat style can say it.
|
|
1069
|
+
if (s.under.length && !s.underLit) {
|
|
1070
|
+
push(`hand/${n}ThumbInk`, group, many(s.under), C.pen, a, { stroke: PEN_SIL });
|
|
1071
|
+
}
|
|
1072
|
+
// A RAISED thumb IS part of the silhouette. Its fill comes after the
|
|
1073
|
+
// mass's line and cuts that line out of itself; its own line is only the
|
|
1074
|
+
// arc outside the fist (`carve`), so the two read as one contour instead
|
|
1075
|
+
// of a lobe with a chord ruled through it.
|
|
1076
|
+
if (s.under.length && s.underLit) {
|
|
1077
|
+
layers(s.under, s.penUnder, split.under).forEach((L, j) => {
|
|
1078
|
+
const slot = `hand/${n}Thumb${j ? j + 1 : ''}`;
|
|
1079
|
+
push(slot, group, fillOf(L), C.face, a);
|
|
1080
|
+
push(`${slot}Ink`, group, inkOf(L), C.pen, a, { stroke: PEN_SIL });
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
// The digits, the same way: fill cuts the mass's top edge out from under
|
|
1084
|
+
// the fingers growing through it, and the carved arc picks that edge back
|
|
1085
|
+
// up at both ends.
|
|
1086
|
+
for (let i = 1; i < s.lit.length; i++) {
|
|
1087
|
+
layers(s.lit[i], s.penLit[i], split[i]).forEach((L, j) => {
|
|
1088
|
+
const slot = `hand/${n}Digits${j ? j + 1 : ''}`;
|
|
1089
|
+
push(slot, group, fillOf(L), C.face, a);
|
|
1090
|
+
push(`${slot}Ink`, group, inkOf(L), C.pen, a, { stroke: PEN_SIL });
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
// THE MARKS, and this is the one draw where the pen does NOT take the flat
|
|
1094
|
+
// idiom's geometry: `penMarks` above, shortened, staggered, bowed and one
|
|
1095
|
+
// band lighter on `wait`. The JOINTS are gone from both — the distal
|
|
1096
|
+
// segment's own contour already draws a line across the digit exactly
|
|
1097
|
+
// there, and two of them is the double line this drawing spends its whole
|
|
1098
|
+
// paint order avoiding.
|
|
1099
|
+
//
|
|
1100
|
+
// Alpha, per shape, and it is ink's own tiering rather than a fudge.
|
|
1101
|
+
// `avatars/ink/face.mjs` draws an EDGE and a lone accent at full ink
|
|
1102
|
+
// (`faceInk`, `jawInk`, `noseInk`, `chinInk`) and a mark that REPEATS
|
|
1103
|
+
// inside a feature translucent — 0.58 for the eyelid crease, 0.42 for the
|
|
1104
|
+
// tooth separators, whose own comment is "gaps, not a barcode". The palm
|
|
1105
|
+
// crease is the first kind: one accent on a bare palm, and it is the mark
|
|
1106
|
+
// that says the palm is a palm. The knuckle bands are the second: three
|
|
1107
|
+
// of them at full ink out-weigh the fist they are drawn inside, which is
|
|
1108
|
+
// what a barcode is, in a hand.
|
|
1109
|
+
push(`hand/${n}Marks`, group, many(penMarks[key]), C.pen, a * s.penMarkA);
|
|
1110
|
+
}
|
|
1111
|
+
return out;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
return { rest: handRest(), draws, frame, place, at };
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// ---------------------------------------------------------------------------
|
|
1118
|
+
// checkFraming — rule 4, and rule 1, as an assertion the build runs.
|
|
1119
|
+
//
|
|
1120
|
+
// It walks every gesture at 2% steps, transforms every point the shape owns
|
|
1121
|
+
// (rotation included: the swing is most of the budget), and reports the worst
|
|
1122
|
+
// outboard excursion and the smallest wrist drop. It is exported rather than
|
|
1123
|
+
// run here because a part does not know what frame it is going to be given.
|
|
1124
|
+
// ---------------------------------------------------------------------------
|
|
1125
|
+
|
|
1126
|
+
export function checkFraming(frame) {
|
|
1127
|
+
const { reach, outboardLimit } = frame;
|
|
1128
|
+
const rows = [];
|
|
1129
|
+
for (const [name, g] of Object.entries(GESTURES)) {
|
|
1130
|
+
const pts = SHAPE[g.shape].hull;
|
|
1131
|
+
let worst = 0, drop = Infinity, top = -Infinity;
|
|
1132
|
+
for (let i = 0; i <= 50; i++) {
|
|
1133
|
+
const t = i / 50;
|
|
1134
|
+
const ox = sample(g.out, t) * reach;
|
|
1135
|
+
const dy = (HIDE - sample(g.up, t) * (HIDE - g.hold)) * reach;
|
|
1136
|
+
const r = (sample(g.rot, t) * Math.PI) / 180, cs = Math.cos(r), sn = Math.sin(r);
|
|
1137
|
+
for (const [px, py] of pts) {
|
|
1138
|
+
// the forearm tails: below the frame by construction, and a 16-degree
|
|
1139
|
+
// swing about a point 300 art units away would report an excursion the
|
|
1140
|
+
// viewer never sees.
|
|
1141
|
+
if (py > 100) continue;
|
|
1142
|
+
const x = ox + g.sc * reach * (px * cs - py * sn);
|
|
1143
|
+
const y = dy + g.sc * reach * (px * sn + py * cs);
|
|
1144
|
+
worst = Math.max(worst, Math.abs(x));
|
|
1145
|
+
top = Math.max(top, -y);
|
|
1146
|
+
}
|
|
1147
|
+
drop = Math.min(drop, dy);
|
|
1148
|
+
}
|
|
1149
|
+
rows.push({ gesture: name, outboard: worst, limit: outboardLimit, wristDrop: drop, rise: top });
|
|
1150
|
+
if (worst > outboardLimit) {
|
|
1151
|
+
throw new Error(`hand: ${name} pushes ink ${worst.toFixed(0)} from centre, past ${outboardLimit.toFixed(0)}`);
|
|
1152
|
+
}
|
|
1153
|
+
if (drop <= 0) throw new Error(`hand: ${name} brings the wrist into the frame`);
|
|
1154
|
+
}
|
|
1155
|
+
return rows;
|
|
1156
|
+
}
|