aaavatars 3.0.1 → 4.0.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/README.md +49 -75
- package/dist/Avatar.d.ts +3 -3
- package/dist/Avatar.js +5 -5
- package/dist/AvatarGroup.js +1 -1
- package/dist/styles.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# aaavatars
|
|
2
2
|
|
|
3
|
-
Deterministic 2D character avatars for AI agents. Same name, same character.
|
|
3
|
+
Deterministic 2D character avatars for AI agents. Same name, same character. The eyes show what the agent is doing, how it feels, where it looks and what just happened.
|
|
4
4
|
|
|
5
5
|
[Live demo](https://rsmdt.github.io/aaavatars/)
|
|
6
6
|
|
|
@@ -11,11 +11,11 @@ npm install aaavatars
|
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
```tsx
|
|
14
|
-
import { Avatar,
|
|
14
|
+
import { Avatar, type AvatarHandle } from "aaavatars";
|
|
15
15
|
|
|
16
16
|
const avatar = useRef<AvatarHandle>(null);
|
|
17
17
|
|
|
18
|
-
<Avatar ref={avatar} name="Chief of Staff"
|
|
18
|
+
<Avatar ref={avatar} name="Chief of Staff" frame={0.5} blink loop activity="coding" />;
|
|
19
19
|
|
|
20
20
|
await avatar.current?.play("success");
|
|
21
21
|
```
|
|
@@ -25,75 +25,57 @@ await avatar.current?.play("success");
|
|
|
25
25
|
| Prop | Values |
|
|
26
26
|
|---|---|
|
|
27
27
|
| `name` | seed string |
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
28
|
+
| `activity` / `expression` / `gaze` | see [Layers](#layers) |
|
|
29
|
+
| `frame` | background from `0` (square) to `0.5` (circle) |
|
|
30
|
+
| `blink` / `loop` | blinking and the looping animations |
|
|
31
|
+
| `interactive` | click, double-click, drag-to-squish and press-and-hold reactions |
|
|
32
32
|
| `shape` | `circle`, `squircle`, `pill`, `hex`, `triangle`, `drop`, `cloud`, `blob` |
|
|
33
33
|
| `eyes` | `round`, `dot`, `tall`, `wide` |
|
|
34
34
|
| `mouth` | `none`, `smile`, `flat`, `open`, or `renderMouth={(slot) => <svg element />}` |
|
|
35
35
|
| `accessory` | `none`, `antenna`, `sprout`, `hat`, `glasses`, `bowtie` |
|
|
36
36
|
| `content` | `face` or `initial` |
|
|
37
37
|
| `colors` / `body` / `background` | palette to pick from, or explicit hex colours |
|
|
38
|
-
| `fill`
|
|
38
|
+
| `fill` | `solid` or `gradient` |
|
|
39
|
+
| `shading` | soft-clay depth from `0` to `1` |
|
|
39
40
|
| `src` | image URL, falls back to the character when it fails to load |
|
|
40
41
|
| `badge` | node pinned to the bottom-right corner |
|
|
41
|
-
| `label` | tooltip and accessible description
|
|
42
|
-
| `
|
|
43
|
-
| `interactive` | click, double-click, drag-to-squish and press-and-hold reactions |
|
|
44
|
-
| `onChange` | called with the event when one starts playing, from `play()` or an interactive reaction, and with `null` when it ends |
|
|
42
|
+
| `label` | tooltip and accessible description |
|
|
43
|
+
| `onChange` | called with an event when it starts playing, and `null` when it ends |
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
With `shading` above `0`, the head is shaded like soft clay: a darker rim, a sheen that rolls with the gaze, and a soft contact shadow that stays put while the body bobs.
|
|
45
|
+
`frame`, `blink`, `loop`, `interactive`, `mouth`, `accessory` and `shading` are off until you set them. Unset `shape`, `eyes` and colours come from the name.
|
|
49
46
|
|
|
50
47
|
## Layers
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
|
55
|
-
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
| Gaze | where is it looking? | until changed | `gaze` prop, defaults per activity |
|
|
59
|
-
| Event | what just happened? | about 1 to 3 seconds, then back to rest | `play(event)` |
|
|
60
|
-
|
|
61
|
-
The eyes carry every layer, so a face without a mouth still reads. Each eye has a top and bottom lid, and its outline can morph into a glyph.
|
|
62
|
-
|
|
63
|
-
When two layers want the same part of the face, the more specific one wins:
|
|
64
|
-
|
|
65
|
-
1. An event owns the whole face while it plays. Glyph eyes morph back to round eyes for it, except `error`, which morphs them into `x x`, and return when it ends.
|
|
66
|
-
2. A non-`neutral` expression owns the eyes, the blinking and, when a mouth is set, the mouth.
|
|
67
|
-
3. The activity owns the body motion, the accessory motion and, while the expression is `neutral`, the eye pose and the mouth.
|
|
68
|
-
4. The gaze moves where the eyes point. It never changes their shape.
|
|
49
|
+
| Layer | Answers | Set with |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| Activity | what is it doing? | `activity` |
|
|
52
|
+
| Expression | how does it feel? | `expression` |
|
|
53
|
+
| Gaze | where is it looking? | `gaze`, defaults per activity |
|
|
54
|
+
| Event | what just happened? | `play(event)`, lasts 1 to 3 seconds |
|
|
69
55
|
|
|
70
|
-
|
|
56
|
+
Each layer changes without touching the others. While an event plays it owns the face. Otherwise a non-`neutral` expression owns the eyes, the activity owns the body, and the gaze only moves where the eyes point.
|
|
71
57
|
|
|
72
58
|
### Activity
|
|
73
59
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
| Activity | Body | Eyes, with a `neutral` expression | Default gaze |
|
|
60
|
+
| Activity | Body | Eyes, while `neutral` | Gaze |
|
|
77
61
|
|---|---|---|---|
|
|
78
62
|
| `idle` | slow drift | | `follow` |
|
|
79
63
|
| `thinking` | sway | | `ponder` |
|
|
80
64
|
| `working` | quick bob | | `scan` |
|
|
81
65
|
| `reading` | still | | `read` |
|
|
82
|
-
| `searching` | leans in | dart
|
|
83
|
-
| `reviewing` | head dips
|
|
84
|
-
| `coding` | bob |
|
|
66
|
+
| `searching` | leans in | dart and hold | `ahead` |
|
|
67
|
+
| `reviewing` | head dips | narrow, look down and up | `ahead` |
|
|
68
|
+
| `coding` | bob | a `>_` prompt with a blinking cursor | `ahead` |
|
|
85
69
|
| `writing` | slow bob | | `read` |
|
|
86
|
-
| `waiting` | still |
|
|
87
|
-
| `asking` | head tilted |
|
|
88
|
-
| `listening` | nod |
|
|
70
|
+
| `waiting` | still | circle around each other | `ahead` |
|
|
71
|
+
| `asking` | head tilted | one eye wider | `ahead` |
|
|
72
|
+
| `listening` | nod | one tall blinking cursor | `ahead` |
|
|
89
73
|
| `speaking` | bob, mouth talks | | `follow` |
|
|
90
|
-
| `stuck` | tilted | sweep
|
|
91
|
-
| `sleeping` | breathe, sunk | droop
|
|
74
|
+
| `stuck` | tilted | sweep, then `x x` | `ahead` |
|
|
75
|
+
| `sleeping` | breathe, sunk | droop and close, whatever the expression | `ahead` |
|
|
92
76
|
|
|
93
77
|
### Expression
|
|
94
78
|
|
|
95
|
-
An expression is a mood that can last for minutes, shown through the lids and, when set, the mouth. A feeling that only makes sense for a moment, like surprise, is an event instead, so no value belongs to both layers.
|
|
96
|
-
|
|
97
79
|
| Expression | Eyes |
|
|
98
80
|
|---|---|
|
|
99
81
|
| `neutral` | the activity's eyes |
|
|
@@ -102,46 +84,36 @@ An expression is a mood that can last for minutes, shown through the lids and, w
|
|
|
102
84
|
| `angry` | top lids down, tilted inward |
|
|
103
85
|
| `confused` | one lid lowered |
|
|
104
86
|
| `curious` | both eyes slightly wider |
|
|
105
|
-
| `tired` | heavy top lids
|
|
106
|
-
| `love` |
|
|
87
|
+
| `tired` | heavy top lids |
|
|
88
|
+
| `love` | one beating heart |
|
|
107
89
|
|
|
108
90
|
### Gaze
|
|
109
91
|
|
|
110
|
-
A gaze is a pattern of where to look. It works with any activity and any eye shape, and only moves the eyes. A loop that also needs the lids or the head, like `reviewing` or `waiting`, belongs to the activity instead.
|
|
111
|
-
|
|
112
92
|
| Gaze | Eyes |
|
|
113
93
|
|---|---|
|
|
114
|
-
| `follow` | follow
|
|
115
|
-
| `wander` | glance around
|
|
116
|
-
| `ponder` | look up-left, then up-right
|
|
117
|
-
| `scan` | sweep
|
|
118
|
-
| `read` | read line by line
|
|
94
|
+
| `follow` | follow the pointer anywhere on the page |
|
|
95
|
+
| `wander` | glance around, ignoring the pointer |
|
|
96
|
+
| `ponder` | look up-left, then up-right |
|
|
97
|
+
| `scan` | sweep right, jump back |
|
|
98
|
+
| `read` | read line by line |
|
|
119
99
|
| `ahead` | look straight out |
|
|
120
|
-
| `down` | look down at the work
|
|
121
|
-
| `away` | look off to the side
|
|
100
|
+
| `down` | look down at the work |
|
|
101
|
+
| `away` | look off to the side |
|
|
122
102
|
|
|
123
103
|
### Event
|
|
124
104
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
An event does not change the `activity`, `expression` or `gaze` props. `EVENT_LAYERS[event]` names the expression and gaze it shows while it plays, for example `success` shows `happy` eyes looking `ahead`. Pair it with `onChange` to mirror the face in your own UI.
|
|
105
|
+
`play(event)` resolves when the reaction ends, or immediately under `prefers-reduced-motion`. It does not change the props. `EVENT_LAYERS[event]` names the expression and gaze it shows, for example `success` shows `happy` eyes looking `ahead`.
|
|
128
106
|
|
|
129
107
|
| Event | Reaction |
|
|
130
108
|
|---|---|
|
|
131
|
-
| `acknowledge` | a double nod
|
|
132
|
-
| `handoff` |
|
|
133
|
-
| `success` | happy eyes with a
|
|
134
|
-
| `warning` | a wince
|
|
135
|
-
| `error` |
|
|
136
|
-
| `surprised` | a squint, then wide eyes
|
|
137
|
-
| `sigh` |
|
|
138
|
-
| `yawn` |
|
|
139
|
-
|
|
140
|
-
Under `prefers-reduced-motion`, or while the page is hidden, `play()` resolves immediately.
|
|
141
|
-
|
|
142
|
-
### Rendering
|
|
143
|
-
|
|
144
|
-
Each name blinks, glances and loops at its own tempo and phase, in CSS only. The `follow` gaze uses one shared pointer listener, updated once per frame. Offscreen avatars pause their loops. The glyphs (`>_`, `x x`, the cursor) are the eyes themselves, morphing their outline: smoothly in Chrome and Firefox, as an instant swap in Safari, which does not animate the SVG `d` property.
|
|
109
|
+
| `acknowledge` | a double nod |
|
|
110
|
+
| `handoff` | lean to the side and back |
|
|
111
|
+
| `success` | happy eyes with a bounce |
|
|
112
|
+
| `warning` | a wince |
|
|
113
|
+
| `error` | `x x` eyes, then sink and recover |
|
|
114
|
+
| `surprised` | a squint, then wide eyes |
|
|
115
|
+
| `sigh` | breathe in, then slump |
|
|
116
|
+
| `yawn` | stretch tall with closed eyes |
|
|
145
117
|
|
|
146
118
|
## AvatarGroup
|
|
147
119
|
|
|
@@ -159,8 +131,10 @@ const svg = renderAvatarSvg({ name: "Research", size: 128 });
|
|
|
159
131
|
|
|
160
132
|
Requires `react-dom`.
|
|
161
133
|
|
|
134
|
+
## Development
|
|
135
|
+
|
|
162
136
|
```sh
|
|
163
|
-
npm run dev
|
|
137
|
+
npm run dev
|
|
164
138
|
npm test
|
|
165
139
|
npm run build
|
|
166
140
|
```
|
package/dist/Avatar.d.ts
CHANGED
|
@@ -29,11 +29,11 @@ export interface AvatarProps {
|
|
|
29
29
|
expression?: Expression;
|
|
30
30
|
eyes?: Eyes;
|
|
31
31
|
fill?: Fill;
|
|
32
|
-
frame?:
|
|
32
|
+
frame?: number;
|
|
33
33
|
gaze?: Gaze;
|
|
34
34
|
interactive?: boolean;
|
|
35
35
|
label?: string;
|
|
36
|
-
|
|
36
|
+
loop?: boolean;
|
|
37
37
|
mouth?: Mouth;
|
|
38
38
|
name: string;
|
|
39
39
|
onChange?: (event: AvatarEvent | null) => void;
|
|
@@ -43,5 +43,5 @@ export interface AvatarProps {
|
|
|
43
43
|
size?: number;
|
|
44
44
|
src?: string;
|
|
45
45
|
}
|
|
46
|
-
export declare function frameRadius(frame:
|
|
46
|
+
export declare function frameRadius(frame: number | undefined): string | undefined;
|
|
47
47
|
export declare const Avatar: import("react").ForwardRefExoticComponent<AvatarProps & import("react").RefAttributes<AvatarHandle>>;
|
package/dist/Avatar.js
CHANGED
|
@@ -114,11 +114,11 @@ function clampFrame(frame) {
|
|
|
114
114
|
return Math.min(Math.max(frame, 0), 0.5);
|
|
115
115
|
}
|
|
116
116
|
export function frameRadius(frame) {
|
|
117
|
-
if (frame ===
|
|
117
|
+
if (frame === undefined)
|
|
118
118
|
return undefined;
|
|
119
|
-
return `${clampFrame(frame
|
|
119
|
+
return `${clampFrame(frame) * 100}%`;
|
|
120
120
|
}
|
|
121
|
-
export const Avatar = forwardRef(function Avatar({ accessory, activity = "idle", background, badge, blink =
|
|
121
|
+
export const Avatar = forwardRef(function Avatar({ accessory, activity = "idle", background, badge, blink = false, body, className, colors, content = "face", expression = "neutral", eyes, fill = "solid", frame, gaze, interactive = false, label, loop = false, mouth, name, onChange, renderMouth, shading = 0, shape, size = 64, src, }, ref) {
|
|
122
122
|
const uid = `aaa-${useId().replace(/[^a-zA-Z0-9-_]/g, "")}`;
|
|
123
123
|
const svgRef = useRef(null);
|
|
124
124
|
const bodyRef = useRef(null);
|
|
@@ -135,7 +135,7 @@ export const Avatar = forwardRef(function Avatar({ accessory, activity = "idle",
|
|
|
135
135
|
useEffect(() => setImageFailed(false), [src]);
|
|
136
136
|
const spec = useMemo(() => createSpec(name, { accessory, background, body, colors, eyes, mouth, shape }), [name, accessory, background, body, colors, eyes, mouth, shape]);
|
|
137
137
|
const geometry = SHAPE_GEOMETRY[spec.shape];
|
|
138
|
-
const framed = frame !==
|
|
138
|
+
const framed = frame !== undefined;
|
|
139
139
|
const radius = framed ? clampFrame(frame) * 100 : 0;
|
|
140
140
|
const showImage = Boolean(src) && !imageFailed;
|
|
141
141
|
const hasMouth = content === "face" && (Boolean(renderMouth) || spec.mouth !== "none");
|
|
@@ -282,7 +282,7 @@ export const Avatar = forwardRef(function Avatar({ accessory, activity = "idle",
|
|
|
282
282
|
}
|
|
283
283
|
void play(event.detail > 1 ? "surprised" : "acknowledge", true);
|
|
284
284
|
}
|
|
285
|
-
const svg = (_jsxs("svg", { ref: svgRef, "aria-label": label ? `${name}, ${label}` : name, className: className && !badge ? `aaa ${className}` : "aaa", "data-activity": activity, "data-blink": blink ? undefined : "off", "data-expression": shownExpression, "data-gaze": resolvedGaze, "data-interactive": interactive ? "" : undefined, "data-
|
|
285
|
+
const svg = (_jsxs("svg", { ref: svgRef, "aria-label": label ? `${name}, ${label}` : name, className: className && !badge ? `aaa ${className}` : "aaa", "data-activity": activity, "data-blink": blink ? undefined : "off", "data-expression": shownExpression, "data-gaze": resolvedGaze, "data-interactive": interactive ? "" : undefined, "data-loop": loop ? undefined : "off", height: size, overflow: framed ? undefined : "visible", role: "img", style: {
|
|
286
286
|
"--aaa-gap": geometry.eyeGap,
|
|
287
287
|
"--aaa-phase": spec.phase,
|
|
288
288
|
"--aaa-tempo": spec.tempo,
|
package/dist/AvatarGroup.js
CHANGED
|
@@ -12,7 +12,7 @@ export function AvatarGroup({ className, max = 4, members, overlap = 0.3, ringCo
|
|
|
12
12
|
const offset = -Math.round(size * overlap);
|
|
13
13
|
return (_jsxs("div", { "aria-label": label, className: className, role: "group", style: { display: "inline-flex" }, children: [visible.map((member, index) => (_jsx("span", { style: {
|
|
14
14
|
borderRadius: frameRadius(member.frame),
|
|
15
|
-
boxShadow: member.frame ===
|
|
15
|
+
boxShadow: member.frame === undefined ? undefined : `0 0 0 2px ${ringColor}`,
|
|
16
16
|
display: "inline-flex",
|
|
17
17
|
marginLeft: index === 0 ? 0 : offset,
|
|
18
18
|
zIndex: visible.length - index,
|
package/dist/styles.js
CHANGED
|
@@ -141,7 +141,7 @@ export const AVATAR_CSS = `
|
|
|
141
141
|
.aaa[data-reacting] .aaa-look{--aaa-look-x:0px;--aaa-look-y:0px;--aaa-look-sx:1}
|
|
142
142
|
.aaa[data-reacting] .aaa-head{--aaa-head-x:0px;--aaa-head-y:0px;--aaa-head-r:0deg}
|
|
143
143
|
.aaa[data-blink=off] .aaa-blink{animation:none}
|
|
144
|
-
.aaa[data-
|
|
144
|
+
.aaa[data-loop=off] *:not(.aaa-blink){animation:none!important}
|
|
145
145
|
.aaa[data-offscreen] *{animation-play-state:paused!important}
|
|
146
146
|
${blinks([3, 21, 47, 66, 88], 0.6)}
|
|
147
147
|
${glances("aaa-saccade", [
|