@voqalize/avatar 0.2.2 → 0.4.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/LICENSE-CC-BY-4.0 +396 -0
- package/README.md +191 -662
- package/assets/README.md +48 -0
- package/assets/tanya.glb +0 -0
- package/assets/tara.glb +0 -0
- package/assets/tushar.glb +0 -0
- package/client/Avatar.tsx +57 -0
- package/client/AvatarClient.ts +818 -0
- package/client/arjun.ts +26 -0
- package/client/createAvatar.ts +177 -0
- package/client/createCanvasAvatar.ts +72 -0
- package/client/index.ts +44 -0
- package/client/internal.ts +108 -0
- package/client/interviewer-female.ts +4 -0
- package/client/interviewer-male.ts +4 -0
- package/client/ishita.ts +26 -0
- package/client/kabir.ts +26 -0
- package/client/meera.ts +26 -0
- package/client/naina.ts +26 -0
- package/client/playout.ts +95 -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/supports.ts +28 -0
- package/client/three/assets.ts +24 -0
- package/client/three/budgets.ts +27 -0
- package/client/three/holds.ts +64 -0
- package/client/three/internal.ts +54 -0
- package/client/three/motion-limits.json +116 -0
- package/client/three/sequences.ts +281 -0
- package/client/three/tanya.ts +69 -0
- package/client/three/tara-rig.ts +1562 -0
- package/client/three/tara.ts +76 -0
- package/client/three/tushar.ts +65 -0
- package/client/types.ts +235 -0
- package/client/useAvatar.ts +74 -0
- package/client/vikram.ts +26 -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 +274 -0
- package/dist/AvatarClient.d.ts.map +1 -0
- package/dist/AvatarClient.js +712 -0
- package/dist/AvatarClient.js.map +1 -0
- package/dist/arjun.d.ts +7 -0
- package/dist/arjun.d.ts.map +1 -0
- package/dist/arjun.js +20 -0
- package/dist/arjun.js.map +1 -0
- package/dist/createAvatar.d.ts +135 -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 +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +34 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +41 -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 +7 -0
- package/dist/ishita.d.ts.map +1 -0
- package/dist/ishita.js +20 -0
- package/dist/ishita.js.map +1 -0
- package/dist/kabir.d.ts +7 -0
- package/dist/kabir.d.ts.map +1 -0
- package/dist/kabir.js +20 -0
- package/dist/kabir.js.map +1 -0
- package/dist/meera.d.ts +7 -0
- package/dist/meera.d.ts.map +1 -0
- package/dist/meera.js +20 -0
- package/dist/meera.js.map +1 -0
- package/dist/naina.d.ts +7 -0
- package/dist/naina.d.ts.map +1 -0
- package/dist/naina.js +20 -0
- package/dist/naina.js.map +1 -0
- package/dist/playout.d.ts +42 -0
- package/dist/playout.d.ts.map +1 -0
- package/dist/playout.js +85 -0
- package/dist/playout.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/supports.d.ts +20 -0
- package/dist/supports.d.ts.map +1 -0
- package/dist/supports.js +26 -0
- package/dist/supports.js.map +1 -0
- package/dist/three/assets.d.ts +24 -0
- package/dist/three/assets.d.ts.map +1 -0
- package/dist/three/assets.js +22 -0
- package/dist/three/assets.js.map +1 -0
- package/dist/three/budgets.d.ts +17 -0
- package/dist/three/budgets.d.ts.map +1 -0
- package/dist/three/budgets.js +21 -0
- package/dist/three/budgets.js.map +1 -0
- package/dist/three/holds.d.ts +33 -0
- package/dist/three/holds.d.ts.map +1 -0
- package/dist/three/holds.js +56 -0
- package/dist/three/holds.js.map +1 -0
- package/dist/three/internal.d.ts +52 -0
- package/dist/three/internal.d.ts.map +1 -0
- package/dist/three/internal.js +49 -0
- package/dist/three/internal.js.map +1 -0
- package/dist/three/motion-limits.json +116 -0
- package/dist/three/sequences.d.ts +71 -0
- package/dist/three/sequences.d.ts.map +1 -0
- package/dist/three/sequences.js +262 -0
- package/dist/three/sequences.js.map +1 -0
- package/dist/three/tanya.d.ts +35 -0
- package/dist/three/tanya.d.ts.map +1 -0
- package/dist/three/tanya.js +51 -0
- package/dist/three/tanya.js.map +1 -0
- package/dist/three/tara-rig.d.ts +403 -0
- package/dist/three/tara-rig.d.ts.map +1 -0
- package/dist/three/tara-rig.js +1500 -0
- package/dist/three/tara-rig.js.map +1 -0
- package/dist/three/tara.d.ts +35 -0
- package/dist/three/tara.d.ts.map +1 -0
- package/dist/three/tara.js +58 -0
- package/dist/three/tara.js.map +1 -0
- package/dist/three/tushar.d.ts +31 -0
- package/dist/three/tushar.d.ts.map +1 -0
- package/dist/three/tushar.js +47 -0
- package/dist/three/tushar.js.map +1 -0
- package/dist/types.d.ts +149 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +140 -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 +7 -0
- package/dist/vikram.d.ts.map +1 -0
- package/dist/vikram.js +20 -0
- package/dist/vikram.js.map +1 -0
- package/package.json +110 -25
- package/src/avatar.d.ts +218 -120
- package/src/avatar.js +950 -213
- package/src/behavior.d.ts +42 -0
- package/src/behavior.js +114 -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 +8 -5
- 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 +434 -58
- package/src/hand.js +120 -91
- package/src/head.js +134 -0
- package/src/idle.js +227 -119
- package/src/interjections.js +200 -31
- package/src/params.js +8 -4
- package/src/perform.js +7 -9
- package/src/prosody.js +647 -0
- package/src/rig.d.ts +24 -0
- package/src/rig.js +32 -0
- package/src/speech-timing.js +23 -0
- package/src/visemes.js +110 -24
- 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/client/src/types.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* types.ts — the avatar wire vocabulary, client side.
|
|
3
|
-
*
|
|
4
|
-
* The binding definition is `docs/contract-protocol.md`; this file is its
|
|
5
|
-
* TypeScript restatement and must not drift from it. The Python half of the
|
|
6
|
-
* same vocabulary is `py/src/voqalize_avatar/messages.py` — the three are
|
|
7
|
-
* maintained together, and a command added to one without the others is
|
|
8
|
-
* incomplete.
|
|
9
|
-
*
|
|
10
|
-
* A server pushes these as RTVI `server-message`s under the envelope
|
|
11
|
-
* `{ type: "avatar", ...cmd-specific fields }`. {@link AvatarCommand} describes
|
|
12
|
-
* what rides inside that envelope; {@link isAvatarMessage} is the envelope
|
|
13
|
-
* itself, and is the only definition of "this message is for the avatar" the
|
|
14
|
-
* client has. There is no protocol version field — see `docs/removed.md`
|
|
15
|
-
* § The `v` field.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/** A viseme cue: `t` is a ms offset into the utterance's clock, `v` is a Rhubarb A–H (or X) letter. */
|
|
19
|
-
export interface AvatarCue {
|
|
20
|
-
t: number;
|
|
21
|
-
v: string;
|
|
22
|
-
i?: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface AvatarStateCmd {
|
|
26
|
-
cmd: "state";
|
|
27
|
-
name: string;
|
|
28
|
-
emotion?: string;
|
|
29
|
-
gaze?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface AvatarInterjectCmd {
|
|
33
|
-
cmd: "interject";
|
|
34
|
-
id: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* A hand gesture — the hand at the frame edge plus its face half. Separate from
|
|
39
|
-
* `interject` on purpose: `interject("WAVE")` is the face alone and always was,
|
|
40
|
-
* so a server that upgrades gets no hand until it asks for one.
|
|
41
|
-
*/
|
|
42
|
-
export interface AvatarGestureCmd {
|
|
43
|
-
cmd: "gesture";
|
|
44
|
-
id: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface AvatarCuesCmd {
|
|
48
|
-
cmd: "cues";
|
|
49
|
-
ctx: string;
|
|
50
|
-
/** Discard queued cues at or after this offset (ms), then append `cues`. */
|
|
51
|
-
from_ms: number;
|
|
52
|
-
cues: AvatarCue[];
|
|
53
|
-
/**
|
|
54
|
-
* True on the one chunk that completes this turn's track: the TTS context is
|
|
55
|
-
* closed, so no further chunk will splice into `ctx`. What a client may
|
|
56
|
-
* assume, exactly — nothing about playout. The audio it describes is still
|
|
57
|
-
* ahead, and `speech stop` remains the end of the turn. It is safe to release
|
|
58
|
-
* per-turn cue state (the splice buffer for `ctx`) once the last cue has
|
|
59
|
-
* played, and safe to stop expecting more.
|
|
60
|
-
*
|
|
61
|
-
* Absent on an interrupted turn, deliberately: a turn that was cut never
|
|
62
|
-
* claims to have completed. Absent chunks are the normal case — the widget's
|
|
63
|
-
* own track already completes on the trailing `X`, so ignoring `final`
|
|
64
|
-
* entirely is a correct implementation.
|
|
65
|
-
*/
|
|
66
|
-
final?: boolean;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface AvatarSpeechCmd {
|
|
70
|
-
cmd: "speech";
|
|
71
|
-
event: "start" | "stop";
|
|
72
|
-
ctx: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface AvatarUserCmd {
|
|
76
|
-
cmd: "user";
|
|
77
|
-
speaking: boolean;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export type AvatarCommand =
|
|
81
|
-
| AvatarStateCmd
|
|
82
|
-
| AvatarInterjectCmd
|
|
83
|
-
| AvatarGestureCmd
|
|
84
|
-
| AvatarCuesCmd
|
|
85
|
-
| AvatarSpeechCmd
|
|
86
|
-
| AvatarUserCmd;
|
|
87
|
-
|
|
88
|
-
/** The full server-message payload: the envelope plus its command. */
|
|
89
|
-
export type AvatarServerMessage = AvatarCommand & { type: "avatar" };
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Is this server-message payload the avatar's? The envelope is the whole
|
|
93
|
-
* answer: `{type:"avatar"}` with a string `cmd`. It used to be a per-deployment
|
|
94
|
-
* `accept` predicate on the client, which meant the library could not state
|
|
95
|
-
* what an avatar message *is* — see `docs/removed.md` § The accept predicate.
|
|
96
|
-
*/
|
|
97
|
-
export function isAvatarMessage(msg: unknown): msg is AvatarServerMessage {
|
|
98
|
-
if (typeof msg !== "object" || msg === null) return false;
|
|
99
|
-
const m = msg as Record<string, unknown>;
|
|
100
|
-
return m.type === AVATAR_MESSAGE_TYPE && typeof m.cmd === "string";
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** The envelope `type` the protocol reserves for avatar traffic. */
|
|
104
|
-
export const AVATAR_MESSAGE_TYPE = "avatar";
|
package/client/src/useAvatar.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useAvatar — mount the widget, wire it to a live session, dispatch its
|
|
3
|
-
* server-messages, and clean up.
|
|
4
|
-
*
|
|
5
|
-
* Internal: `<Avatar>` is the only thing the package exports. It is a separate
|
|
6
|
-
* module anyway because the two lifecycles genuinely differ — mounting the
|
|
7
|
-
* widget happens once (an avatar swap remounts by design; see the note in the
|
|
8
|
-
* effect), while attaching to the pipecat client re-runs whenever the client
|
|
9
|
-
* identity changes, which a session reconnect makes it do.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { useEffect, useRef, useState } from "react";
|
|
13
|
-
import type { PipecatClient } from "@pipecat-ai/client-js";
|
|
14
|
-
import { createAvatar, type AvatarApi } from "../../src/avatar.js";
|
|
15
|
-
import { AvatarClient } from "./AvatarClient.js";
|
|
16
|
-
|
|
17
|
-
export interface UseAvatarOptions {
|
|
18
|
-
/** Which face. Omit for the widget's own default. */
|
|
19
|
-
avatar?: string;
|
|
20
|
-
/** The live `PipecatClient` to dispatch server-messages from, or `null`
|
|
21
|
-
* before connect. `useAvatar` (dis)connects the subscription as this
|
|
22
|
-
* changes; it does not create or own the client. */
|
|
23
|
-
client?: PipecatClient | null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* The mount ref's type, written out rather than named as React's `RefObject`.
|
|
28
|
-
*
|
|
29
|
-
* React 18 and 19 declare that alias with different type arguments — 18's
|
|
30
|
-
* `useRef<T>(null)` yields `RefObject<T>`, 19's yields `RefObject<T | null>` —
|
|
31
|
-
* and because both are the *same alias*, TypeScript compares them by variance
|
|
32
|
-
* and rejects whichever one we didn't pick. An anonymous shape forces a
|
|
33
|
-
* structural comparison instead, which both versions satisfy, and which the
|
|
34
|
-
* `ref` prop accepts on both. This is the only place the 18-vs-19 split shows
|
|
35
|
-
* up in the binding; keep it that way.
|
|
36
|
-
*/
|
|
37
|
-
export type AvatarMountRef = { current: HTMLDivElement | null };
|
|
38
|
-
|
|
39
|
-
export interface UseAvatarHandle {
|
|
40
|
-
/** Attach to the mount element: `<div ref={containerRef} />`. */
|
|
41
|
-
containerRef: AvatarMountRef;
|
|
42
|
-
/** The live widget instance once mounted, else `null`. */
|
|
43
|
-
avatar: AvatarApi | null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function useAvatar(options: UseAvatarOptions = {}): UseAvatarHandle {
|
|
47
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
48
|
-
const [avatar, setAvatar] = useState<AvatarApi | null>(null);
|
|
49
|
-
const avatarClientRef = useRef<AvatarClient | null>(null);
|
|
50
|
-
|
|
51
|
-
// Latest-options ref, so the mount effect (which runs once) still reads the
|
|
52
|
-
// live props without re-subscribing.
|
|
53
|
-
const optionsRef = useRef(options);
|
|
54
|
-
optionsRef.current = options;
|
|
55
|
-
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
const mount = containerRef.current;
|
|
58
|
-
if (!mount) return;
|
|
59
|
-
const instance = createAvatar({ mount, avatar: optionsRef.current.avatar });
|
|
60
|
-
avatarClientRef.current = new AvatarClient(instance);
|
|
61
|
-
setAvatar(instance);
|
|
62
|
-
|
|
63
|
-
return () => {
|
|
64
|
-
instance.destroy();
|
|
65
|
-
avatarClientRef.current = null;
|
|
66
|
-
setAvatar(null);
|
|
67
|
-
};
|
|
68
|
-
// Mount once. `avatar` is read at mount time only — the widget has no
|
|
69
|
-
// hot-swap-avatar API (`createFace` runs once per mount), so changing it
|
|
70
|
-
// re-renders nothing here by design; a caller that needs a different face
|
|
71
|
-
// remounts with a `key` prop (see the component's doc).
|
|
72
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
73
|
-
}, []);
|
|
74
|
-
|
|
75
|
-
useEffect(() => {
|
|
76
|
-
const wrapper = avatarClientRef.current;
|
|
77
|
-
const pipecatClient = options.client;
|
|
78
|
-
if (!wrapper || !pipecatClient) return;
|
|
79
|
-
return wrapper.attach(pipecatClient);
|
|
80
|
-
// Re-subscribe when the widget mounts or the session's client changes.
|
|
81
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
|
-
}, [avatar, options.client]);
|
|
83
|
-
|
|
84
|
-
return { containerRef, avatar };
|
|
85
|
-
}
|
package/docs/contract-avatar.md
DELETED
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
# Contract B — mixer ↔ face (the avatar contract)
|
|
2
|
-
|
|
3
|
-
*Living document. Describes the code as of `src/face*.js` on `main`; the
|
|
4
|
-
[Direction](#direction) section flags what is about to change. The counterpart
|
|
5
|
-
contract — what the server drives — is
|
|
6
|
-
[contract-protocol.md](contract-protocol.md).*
|
|
7
|
-
|
|
8
|
-
An avatar is a module exporting exactly this, and nothing more:
|
|
9
|
-
|
|
10
|
-
```js
|
|
11
|
-
createFace(mount, theme) -> { svg, apply(params), theme, destroy() }
|
|
12
|
-
META = { viewBox: {x, y, w, h}, mouthCrop: {x, y, w, h} }
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
- `mount` — element to render into. `destroy()` empties it.
|
|
16
|
-
- `theme` — optional per-key colour overrides merged over the rig's palette;
|
|
17
|
-
the merged object is returned as `theme`. Theme *keys* are per-avatar
|
|
18
|
-
(peep and wren carry 8 each; the retired rigs shared a ~25-key palette).
|
|
19
|
-
Hosts that paint *around* the widget read them off `api.theme` — see
|
|
20
|
-
Palettes below.
|
|
21
|
-
- `svg` — the live `<svg>` element.
|
|
22
|
-
- `apply(params)` — write one full parameter vector into the DOM. Called every
|
|
23
|
-
animation frame.
|
|
24
|
-
- `META` — the **avatar descriptor**: what a host or tool may know about the
|
|
25
|
-
face without opening it. `viewBox` is the framing (hosts derive aspect from
|
|
26
|
-
it; `createAvatar` exposes it as `api.meta`); `mouthCrop` frames the mouth
|
|
27
|
-
for close inspection (the contact sheet's viseme-detail row). Deliberately
|
|
28
|
-
minimal — a landmark joins META when a second consumer needs it, not before.
|
|
29
|
-
|
|
30
|
-
**A face must be callable standalone.** Three rig-tooling pages call
|
|
31
|
-
`AVATARS[name].create(mount)` directly with no mixer and drive `apply()` from
|
|
32
|
-
a raw vector; a face that only works under `createAvatar` is broken.
|
|
33
|
-
|
|
34
|
-
Nothing above the face knows what a face looks like; nothing in the face knows
|
|
35
|
-
what a call is. If a visual change needs `params.js` touched, reconsider; if
|
|
36
|
-
it needs anything outside `src/face*.js`, it's a bug.
|
|
37
|
-
|
|
38
|
-
## Obligations of `apply(params)`
|
|
39
|
-
|
|
40
|
-
- **Consume, don't smooth.** Values arrive already clamped to `RANGE` and
|
|
41
|
-
already smoothed through per-channel time constants. Add no easing of your
|
|
42
|
-
own.
|
|
43
|
-
- **Idempotent and cheap.** Same vector in, same DOM out; memoize attribute
|
|
44
|
-
writes (every rig shares a `prev`-map `set(node, attr, val)` helper) so
|
|
45
|
-
an unchanged channel costs nothing. ~60 calls/s is the budget.
|
|
46
|
-
- **Never write `viewBox`.** Every pose channel is a transform or a path,
|
|
47
|
-
never the camera. Tooling relies on this to crop safely after `apply()`.
|
|
48
|
-
- **Honour the channel's *semantic*, not its plumbing.** The channel value is
|
|
49
|
-
what an author of `visemes.js`/`emotions.js` — who never sees your rig —
|
|
50
|
-
thinks they are asking for. The standing example: `mouthOpen` denotes the
|
|
51
|
-
*visible aperture*. peep initially mapped it to the gap between lip
|
|
52
|
-
centrelines; the drawn lip band was ~11 units thick, so the mouth stayed
|
|
53
|
-
visibly shut until 0.25 and two of the nine visemes live below that. The fix
|
|
54
|
-
was to solve back from aperture to control points, not to re-tune the
|
|
55
|
-
visemes.
|
|
56
|
-
|
|
57
|
-
## The parameter vector
|
|
58
|
-
|
|
59
|
-
30 float channels (`src/params.js`). Rest is the neutral face; range is the
|
|
60
|
-
post-mix clamp; τ is the smoothing time constant the mixer applies (the face
|
|
61
|
-
never does). Sign conventions are from the *viewer's* perspective.
|
|
62
|
-
|
|
63
|
-
| channel | rest | range | τ (s) | means |
|
|
64
|
-
|---|---|---|---|---|
|
|
65
|
-
| `mouthOpen` | 0.02 | 0..1 | 0.042 | visible vertical aperture |
|
|
66
|
-
| `mouthWidth` | 0.42 | 0..1 | 0.042 | narrow..wide (0.42 neutral) |
|
|
67
|
-
| `mouthRound` | 0.10 | 0..1 | 0.042 | pucker / protrusion |
|
|
68
|
-
| `mouthPress` | 0.15 | 0..1 | 0.042 | lips thinned & pressed |
|
|
69
|
-
| `mouthTuck` | 0 | 0..1 | 0.042 | lower lip under upper teeth (F/V) |
|
|
70
|
-
| `mouthCornerL/R` | 0.10 | −1.4..1.4 | 0.13 | −frown..+smile |
|
|
71
|
-
| `teethUpper` | 0 | 0..1 | 0.042 | upper-teeth reveal |
|
|
72
|
-
| `tongue` | 0 | 0..1 | 0.042 | tongue raised into aperture (L) |
|
|
73
|
-
| `jaw` | 0 | 0..1 | 0.07 | extra chin drop, lags the lips |
|
|
74
|
-
| `lidL/R` | 0.12 | 0..1 | 0.018 | 0 wide open..1 closed; rest grazes the iris |
|
|
75
|
-
| `squintL/R` | 0 | 0..1 | 0.12 | lower lid raised (smile/suspicion) |
|
|
76
|
-
| `pupilX/Y` | 0 / 0.05 | −1..1 | 0.032 | gaze offset, +right / +down |
|
|
77
|
-
| `browRaiseL/R` | 0 | −1..1 | 0.08 | whole-brow lift |
|
|
78
|
-
| `browAngleL/R` | 0 | −1.4..1.4 | 0.08 | outer-end up |
|
|
79
|
-
| `browInnerL/R` | 0 | −1..1 | 0.08 | inner-end lift (AU1, "concern") |
|
|
80
|
-
| `headYaw` | 0 | −1.4..1.4 | 0.16 | + toward viewer's right |
|
|
81
|
-
| `headPitch` | 0 | −1.4..1.4 | 0.16 | + chin down |
|
|
82
|
-
| `headRoll` | 0 | −1.4..1.4 | 0.16 | + tilt toward viewer's right |
|
|
83
|
-
| `breath` | 0 | 0..1 | 0.25 | idle-driven breathing cycle |
|
|
84
|
-
| `shoulderL/R` | 0 | −1..1 | 0.19 | −dropped..+raised |
|
|
85
|
-
| `torsoLean` | 0 | −1..1 | 0.24 | −back..+forward; reads as scale change |
|
|
86
|
-
| `torsoTurn` | 0 | −1..1 | 0.44 | trunk lateral travel, + toward viewer's right |
|
|
87
|
-
|
|
88
|
-
Groups (`GROUPS`): `mouth`, `smile`, `eyes`, `gaze`, `brows`, `head`, `body`
|
|
89
|
-
(breath + torsoLean + torsoTurn), `shoulders` — clips declare group ownership by
|
|
90
|
-
them.
|
|
91
|
-
|
|
92
|
-
`torsoTurn`'s time constant is nearly 3× the head's, and that ratio is load-
|
|
93
|
-
bearing rather than taste: the mixer feeds it the *same* target as `headYaw`
|
|
94
|
-
(scaled by `TRUNK_FOLLOW = 0.45` in `avatar.js`), so a sustained head turn is
|
|
95
|
-
chased by a trunk that leaves late and settles late. Follow-through falls out of
|
|
96
|
-
the smoothing the rig already had; there is no second animation system. Shorten
|
|
97
|
-
it toward 0.16 and head and trunk move as one rigid piece, which is the puppet
|
|
98
|
-
read.
|
|
99
|
-
|
|
100
|
-
A face should consume all 30. One sanctioned exception exists: peep ignores
|
|
101
|
-
`jaw` (its construction has no drawn jaw line to drop — a documented character
|
|
102
|
-
decision, not an oversight).
|
|
103
|
-
|
|
104
|
-
There are deliberately **no arm or hand channels**; see the note in
|
|
105
|
-
`params.js` before considering any.
|
|
106
|
-
|
|
107
|
-
## Invariant vs per-avatar
|
|
108
|
-
|
|
109
|
-
Three rigs were built independently and their `apply()` implementations
|
|
110
|
-
converged on the same eight blocks in the same order — torso lean → shoulders
|
|
111
|
-
→ parallax layer loop → eyes → brows → mouth → teeth → tongue — with the same
|
|
112
|
-
memoizer and the same return shape. That convergence now lives in
|
|
113
|
-
**`src/face-core.js`**, which owns:
|
|
114
|
-
|
|
115
|
-
- the shell: mount, id-scoped selector, the memoized `set(node, attr, val)`;
|
|
116
|
-
- `poseTransforms(p, set, el, POSE)` — lean, shoulders, parallax, driven by a
|
|
117
|
-
per-rig `POSE` spec of named scalars (below);
|
|
118
|
-
- the shared feature fragments a rig opts into where its model matches:
|
|
119
|
-
`pairedTeeth` (peep, wren). Two more — `irisLidEyes` and `browPair` — were
|
|
120
|
-
removed with the rigs that used them; a future rig with sclera and
|
|
121
|
-
endpoint-pair brows should recover them from git history rather than
|
|
122
|
-
re-derive them;
|
|
123
|
-
- the shared constants: lean scale `0.055`, head-roll multipliers ×5.5
|
|
124
|
-
features / ×1.5 torso, shrug/tilt derivation `shrug=(L+R)/2`, `tilt=(R−L)/2`,
|
|
125
|
-
lower-teeth reveal ramp `(open − 0.45) / 0.4`, tongue gate `> 0.02`;
|
|
126
|
-
- `faceApi` — the return shape.
|
|
127
|
-
|
|
128
|
-
What legitimately varies per avatar, and stays in the face module:
|
|
129
|
-
|
|
130
|
-
- **The `POSE`/`EYES` spec values**: `yawPx`/`pitchPx` (parallax travel),
|
|
131
|
-
`pivot`, lean travel and pivot, shrug lift and tilt degrees, `turnPx` (lateral
|
|
132
|
-
trunk travel at `torsoTurn = 1`), the breath model, pupil travel, `lidFollow`
|
|
133
|
-
strength (0.22 on both current rigs), plus a `units` factor (see Art units).
|
|
134
|
-
- **The breath model's numbers**. A rig declares `breathSwell` + `swellPivot`
|
|
135
|
-
and breathes as a *scale about the hem*:
|
|
136
|
-
the shoulder line rises and the chest widens while the bottom of the shirt
|
|
137
|
-
stays put, and the head's matching lift is derived arithmetic
|
|
138
|
-
(`swell × (swellPivot.y − pivot.y)`) rather than a second tuned constant, so
|
|
139
|
-
the two layers cannot drift and the neck cannot telescope. `breathSwell` is
|
|
140
|
-
required. It replaced a rigid vertical slide of the whole shirt, which moved
|
|
141
|
-
*more* pixels and read as *less* alive — a figure translating up and down has
|
|
142
|
-
been nudged, not filled with air.
|
|
143
|
-
- **Structural choices**: layer set and parallax table (the retired rigs ran 7
|
|
144
|
-
layers and 4; peep and wren fuse to 4 — the art decides, not a standard); eye
|
|
145
|
-
model
|
|
146
|
-
(iris + 4 lid paths vs peep's single translated group + regenerated bean);
|
|
147
|
-
brow input (endpoint pair vs drawn point list); mouth output (path strings
|
|
148
|
-
vs peep's contour point list); which optional elements exist (`jaw` shade,
|
|
149
|
-
`subLip`, `seam`, split lip edges, lower teeth). A rig whose model diverges
|
|
150
|
-
keeps its own block instead of the shared fragment — peep's eyes and brows
|
|
151
|
-
are the standing example.
|
|
152
|
-
|
|
153
|
-
Do not chase parity between faces: they are separate drawings, not renderings
|
|
154
|
-
of one drawing. A visual improvement lands in one face and stops there.
|
|
155
|
-
|
|
156
|
-
## Art units
|
|
157
|
-
|
|
158
|
-
Units are per-rig (peep 760×950 cropped to `92 76 576 800`; wren and myna the
|
|
159
|
-
same window at `92 50`; the retired rigs were 320×400 and a native 1024² cropped to
|
|
160
|
-
`179 42 666 832` — note how little the aspect agreed). **Copying a magnitude
|
|
161
|
-
between rigs is silent breakage**: one retired rig's travels were the other's
|
|
162
|
-
numbers with `units: S` (S = 2.67) in its `POSE` spec; peep's torso channels
|
|
163
|
-
were once ported without conversion and the shoulders stopped reading, while
|
|
164
|
-
nothing threw and `sweep()` passed. The trap inside the
|
|
165
|
-
trap: **translations convert, degrees don't** — a rotation is already
|
|
166
|
-
unit-independent, which is why `shrugTiltDeg` never takes the `units` factor.
|
|
167
|
-
|
|
168
|
-
`viewBox` is not a rig constant. Hosts derive aspect from `META.viewBox` (or
|
|
169
|
-
`api.meta.viewBox`); the demos do.
|
|
170
|
-
|
|
171
|
-
## Registration
|
|
172
|
-
|
|
173
|
-
`src/avatar.js` holds the registry — `{ create, meta }` records:
|
|
174
|
-
|
|
175
|
-
```js
|
|
176
|
-
export const AVATARS = {
|
|
177
|
-
peep: { create, meta },
|
|
178
|
-
wren: { create, meta },
|
|
179
|
-
myna: { create, meta },
|
|
180
|
-
};
|
|
181
|
-
export const DEFAULT_AVATAR = 'peep';
|
|
182
|
-
createAvatar({ avatar: 'peep' }) // by name
|
|
183
|
-
createAvatar({ face: myCreateFace }) // any factory, never registered
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
A bare `face:` factory has no descriptor; `createAvatar` then derives
|
|
187
|
-
`meta.viewBox` from the produced svg and `meta.mouthCrop` is absent — registry
|
|
188
|
-
avatars always carry the full META.
|
|
189
|
-
|
|
190
|
-
Palettes: there is no barrel `THEME` export — each face module owns its
|
|
191
|
-
palette, and `api.theme` returns the mounted avatar's. A host needs it
|
|
192
|
-
whenever it paints anything *around* the widget: every rig is drawn portrait,
|
|
193
|
-
so a 16:9 call tile leaves a margin either side of the drawing, and the margin
|
|
194
|
-
has to be filled with the rig's own backdrop or the tile reads as a portrait
|
|
195
|
-
picture hung in a landscape frame. `demo/call.html` does exactly that — tile
|
|
196
|
-
background from `theme.bg0/bg1`, plus a mask feathering the drawing's two
|
|
197
|
-
vertical edges, because peep's white shirt is drawn to run off its own frame
|
|
198
|
-
and otherwise stops in mid-air. Reshaping the art to fit a host's box is the
|
|
199
|
-
wrong fix; the widget does not control the box. peep has
|
|
200
|
-
no dark palette **by decision** (inverting two-value line art recolours the
|
|
201
|
-
hair and ages the character; that is geometry wearing a palette's clothes) —
|
|
202
|
-
its theme keys stay overridable, but do not add a `dark` selector.
|
|
203
|
-
|
|
204
|
-
## The hand — a layer no face draws
|
|
205
|
-
|
|
206
|
-
`src/hand.js` puts a hand into the bottom of the frame for `gesture(id)`
|
|
207
|
-
(protocol side: [contract-protocol.md](contract-protocol.md) § Hand gestures).
|
|
208
|
-
It is deliberately **not** part of this contract's parameter space: it writes a
|
|
209
|
-
transform on its own `<g>` appended over the face's svg, it has no channel in
|
|
210
|
-
`params.js`, and a face that never plays a gesture renders byte-for-byte what
|
|
211
|
-
it rendered before. That is the whole reason it could be added at all — a hand
|
|
212
|
-
channel only one avatar could draw is precisely the mistake CLAUDE.md
|
|
213
|
-
constraint 9 names.
|
|
214
|
-
|
|
215
|
-
**What a face owes it: a `META.viewBox`, and `theme.ink` / `theme.paper`.**
|
|
216
|
-
Nothing else, and no new META field. Placement derives four numbers from the
|
|
217
|
-
window itself — centre `x + w/2`, floor `y + h`, a reach scaled off `w`, and an
|
|
218
|
-
outboard limit of `w/2 − 8` — and every gesture timeline is authored in wrist
|
|
219
|
-
depth *below the floor* rather than absolute `y`, so the same drawing lands
|
|
220
|
-
correctly on windows of different heights. peep's bottom is 876 and wren's and
|
|
221
|
-
myna's is 850; all three place identically.
|
|
222
|
-
|
|
223
|
-
Two framing rules are asserted, not assumed. `checkHandFraming(meta)` throws if
|
|
224
|
-
any keyframe would let the wrist rise into the window (the hand must always be
|
|
225
|
-
*cut* by the bottom edge, never end in a floating stump) or let the hand's
|
|
226
|
-
rotated width cross the window's side (a portrait window pillarboxed in a 16:9
|
|
227
|
-
tile slices anything outboard with a hard vertical line that reads as a
|
|
228
|
-
rendering fault). `sweep()` runs it for every registered avatar, so a new face
|
|
229
|
-
with an unusual window fails the gate rather than the eye.
|
|
230
|
-
|
|
231
|
-
If a character's idiom cannot carry it, mount with `hand: false`; `gesture()`
|
|
232
|
-
then plays the face half alone.
|
|
233
|
-
|
|
234
|
-
## Checklist for a new avatar
|
|
235
|
-
|
|
236
|
-
1. Serve with `python3 serve.py 8777` (never `python3 -m http.server` — its
|
|
237
|
-
caching has burned this project three times).
|
|
238
|
-
2. `demo/rig/contact-sheet.html?face=NAME` — every viseme, emotion, gaze and
|
|
239
|
-
channel extreme. Check the **mouth-detail crop row**, not just full heads:
|
|
240
|
-
two visemes can be numerically distinct and visually identical (`G` vs `B`
|
|
241
|
-
both read as a white strip until `G` was rebuilt as nearly-all-teeth). At
|
|
242
|
-
avatar size a viseme is ~40 px tall; letter collisions are invisible on the
|
|
243
|
-
full-head row. The crop row frames itself from your `META.mouthCrop`.
|
|
244
|
-
3. `demo/rig/torso-check.html?face=NAME` — shoulders × lean × head pose.
|
|
245
|
-
These channels only fail *in combination*; this is where a rig leaks
|
|
246
|
-
background from behind the shirt if it is going to.
|
|
247
|
-
4. `demo/rig/clip-strip.html?clip=NOD_SMALL&face=NAME` — phase relationships
|
|
248
|
-
through the mixer's own smoothing, as a filmstrip.
|
|
249
|
-
5. `demo/rig/rig-check.html` → `await sweep()` — conformance: params finite,
|
|
250
|
-
`|v| ≤ 2`, svg connected, across every state/emotion/gaze/interjection and
|
|
251
|
-
a viseme track, plus `checkHandFraming` against your window and a pass of
|
|
252
|
-
every hand gesture. Sweep also cannot see *looks*; it reaches shoulders/torso
|
|
253
|
-
only through clips, so drive those with a `setOverrides` loop over
|
|
254
|
-
`[-1, 0, 1]` per channel — and look at one hand gesture at peak extension
|
|
255
|
-
(`demo/rig/body-lab.html?face=NAME&gesture=HI&at=0.4`), because figure/ground
|
|
256
|
-
between hand and shirt is a judgement the framing check cannot make.
|
|
257
|
-
6. Auto-traced art has known failure modes to budget for: zero-margin abutting
|
|
258
|
-
contours open seams under parallax; the trace stops at the source crop;
|
|
259
|
-
hard horizontal edges invisible in the source appear under motion.
|
|
260
|
-
7. **The 130 px acceptance pass** — downsample the rest pose, the emotions
|
|
261
|
-
row and the X/A mouth crops to ~130 px and judge *there*. Author at
|
|
262
|
-
close-up, accept at tile size: the mouth must still read as smiling (not
|
|
263
|
-
merely present), the six emotions must be tellable apart, and X vs A must
|
|
264
|
-
differ in *shape*. Run the fixation audit on the rest tile: name the first
|
|
265
|
-
three things you see, in order — the eye/mouth band places no worse than
|
|
266
|
-
second. (Why: [research-perception.md](research-perception.md) §2, §5, §8.)
|
|
267
|
-
8. **Levelness by mirror** — render rest, flip it horizontally, and compare
|
|
268
|
-
the pair; tilt and lopsidedness pop instantly. Judge on the glasses line
|
|
269
|
-
and eye line. Rest must be channel-neutral and dead level: the mixer adds
|
|
270
|
-
roll, sway and glances at runtime, and a baked-in tilt compounds with all
|
|
271
|
-
of them. Drawing asymmetry (fringe, chin off midline) is welcome; *pose*
|
|
272
|
-
asymmetry is a defect.
|
|
273
|
-
9. **Worst-case composites, not rest poses, for clearances** — build the
|
|
274
|
-
extreme combination for every pair of marks that move relative to each
|
|
275
|
-
other (brows-down + squint + pitch against a glasses frame; wide-open
|
|
276
|
-
mouth against any under-lip mark) and verify a hard 3–4 unit gap.
|
|
277
|
-
Near-tangency shimmers under animation. If an accessory and a channel
|
|
278
|
-
collide, the accessory yields. Also render one **mid-blink** frame (lids
|
|
279
|
-
held ~0.5 via rig-check's `pose()`): anything that must ride the lid — a
|
|
280
|
-
lash line — is caught here, not at open or closed.
|
|
281
|
-
10. **Independent design review** — before a face is called done, a fresh-eyes
|
|
282
|
-
reviewer (not the author) critiques it against the *product brief* at
|
|
283
|
-
tile size, organized around the questions in
|
|
284
|
-
[research-perception.md](research-perception.md): fixation hierarchy,
|
|
285
|
-
resting trust/warmth, neoteny calibration, caricature economy, animation
|
|
286
|
-
head-room, silhouette, long-session comfort. The output is prescriptions
|
|
287
|
-
ranked by perceptual payoff ÷ stroke cost, plus a **protect-list** of
|
|
288
|
-
marks confirmed right — which then stops future churn on them.
|
|
289
|
-
|
|
290
|
-
## Adding a new avatar
|
|
291
|
-
|
|
292
|
-
Both halves of the old Direction section landed (`src/face-core.js` and
|
|
293
|
-
`META`), and the recipe has been run end-to-end twice: `wren` as the plumbing
|
|
294
|
-
proof, and `myna` (2026-08-07) as the proof of the *staged* process below —
|
|
295
|
-
which is where the time and the judgement actually went.
|
|
296
|
-
|
|
297
|
-
### The staged process
|
|
298
|
-
|
|
299
|
-
Identity and production quality are different problems; solving them in
|
|
300
|
-
stages, with different acceptance bars, is what kept the myna run converging.
|
|
301
|
-
|
|
302
|
-
- **Stage 0 — identity source.** A stakeholder-supplied reference image is
|
|
303
|
-
the identity spec. Hard lesson (koel, rejected on sight the same day it
|
|
304
|
-
passed every rig check): character identity is judged against a concrete
|
|
305
|
-
image, not against adjectives — a brief-first avatar optimizes the wrong
|
|
306
|
-
target however well it verifies. If there is no reference, get one agreed
|
|
307
|
-
before authoring.
|
|
308
|
-
- **Stage 1 — distill, then match.** Extract from the reference: the
|
|
309
|
-
silhouette, the 3–5 identity marks, and the palette structure (where the
|
|
310
|
-
one accent sits). That distillate — not the pixels — is what gets matched
|
|
311
|
-
(peak shift: exaggerate the distinctive, simplify the generic —
|
|
312
|
-
research-perception.md §5). *Measure* proportions off the bitmap instead of
|
|
313
|
-
eyeballing (lens w:h, feature heights as fractions of face height — myna's
|
|
314
|
-
glasses only landed when measured). Hand-author in the idiom; never
|
|
315
|
-
auto-trace. The bar for this stage is the squint test at full size and
|
|
316
|
-
130 px: same person?
|
|
317
|
-
- **Stage 2 — production calibration.** The reference *stops being the bar*;
|
|
318
|
-
the brief takes over. This is where the science does the work, all of it
|
|
319
|
-
judged on the 130 px surface: mouth ink floor so warmth survives the tile;
|
|
320
|
-
channel→geometry gains raised until shared emotion poses land (categorical
|
|
321
|
-
perception: near-extreme or invisible); resting brow/mouth set for trust,
|
|
322
|
-
not personality (the smirk composite); resting lid graze against stare;
|
|
323
|
-
interiors calmed against nod-flicker; clearance envelopes at worst-case
|
|
324
|
-
composites; shape-not-amplitude viseme separation. Guardrail: **one neoteny
|
|
325
|
-
move per round** (eye size *or* forehead *or* jaw *or* nose), then re-judge
|
|
326
|
-
— two at once is how a professional assistant becomes a mascot. Note where
|
|
327
|
-
production calibration diverges from the reference rather than silently
|
|
328
|
-
splitting the difference.
|
|
329
|
-
- **Stage 3 — independent review** (checklist item 10), then the stakeholder.
|
|
330
|
-
The author does not review their own likeness; anchoring is real. Expect
|
|
331
|
-
the reviewer to find the class of error the author cannot: authored at
|
|
332
|
-
close-up, judged at close-up.
|
|
333
|
-
|
|
334
|
-
### What a face module supplies
|
|
335
|
-
|
|
336
|
-
A new face module supplies:
|
|
337
|
-
|
|
338
|
-
1. **Static art** — the markup function: layer groups, the element table's
|
|
339
|
-
nodes, theme-keyed fills. Hand-authored or cleaned trace; budget for the
|
|
340
|
-
auto-trace failure modes in the checklist if tracing. For a line-art
|
|
341
|
-
character, build every mark with `src/line-art.js` (`taper`, `taperRing`,
|
|
342
|
-
`region` — filled variable-width marks, width profiles over normalized s);
|
|
343
|
-
the width *profiles* are per-character and stay in the face module.
|
|
344
|
-
2. **A `POSE` spec** for `poseTransforms` — the named scalars (travels,
|
|
345
|
-
pivots, bob, tilt degrees), the layer list/parallax table/torso subset, and
|
|
346
|
-
`units`. Start from the rig whose construction is closest and re-derive
|
|
347
|
-
every *travel* in your own units; keep degrees as judgements about your own
|
|
348
|
-
collar/neck geometry, not conversions.
|
|
349
|
-
3. **Feature blocks** — use the face-core fragments where your model matches
|
|
350
|
-
(`irisLidEyes`, `browPair`, `pairedTeeth`); write your own where the
|
|
351
|
-
character disagrees. The mouth is always yours: honour the channel
|
|
352
|
-
semantics in *Obligations* above. peep's bean-eye, point-list-brow and
|
|
353
|
-
contour-mouth generators carried into wren as copies with re-derived
|
|
354
|
-
constants — if a third line-art face repeats that, extract them into
|
|
355
|
-
parameterized factories the way the stroke engine was extracted.
|
|
356
|
-
4. **`META`** — viewBox and mouthCrop.
|
|
357
|
-
5. **A registry entry** — `{ create, meta }` in `src/avatar.js`.
|
|
358
|
-
|
|
359
|
-
What you get for free: the mixer, visemes, emotions, gaze, idle, clips,
|
|
360
|
-
interjections, the frame-edge hand (§ The hand — it needs only your viewBox and
|
|
361
|
-
two theme keys), the pose mechanics, the memoizer, and every host page and rig
|
|
362
|
-
tool — the demos' avatar pickers, contact sheet, torso check, clip strip and
|
|
363
|
-
`sweep()` all enumerate the registry. The wren run measured the split: the
|
|
364
|
-
plumbing steps (2, 4, 5) are mechanical; the art (step 1) and the read of
|
|
365
|
-
every state at tile size (the checklist) are where the judgement — and the
|
|
366
|
-
time — actually goes. Static accessories interact with channels: wren's lens
|
|
367
|
-
rings cap pupil travel, the exact channel DISTRACTED needs most — check your
|
|
368
|
-
accessory against the gaze extremes early, not last.
|
|
369
|
-
|
|
370
|
-
Then run the checklist above, and judge by eye — passing `sweep()` is not
|
|
371
|
-
evidence a face looks right.
|