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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # aaavatars
2
2
 
3
- Deterministic 2D character avatars for AI agents. Same name, same character. Its state is four [layers](#layers): `activity`, `expression` and `gaze` drive looping states, expressed through the eyes, and `play()` triggers one-shot reactions.
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, AvatarGroup, type AvatarHandle } from "aaavatars";
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" size={36} activity="coding" label="Writing code" />;
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
- | `frame` | `false` (no background), `0` (square) to `0.5` (circle) |
29
- | `activity` | `idle`, `thinking`, `working`, `reading`, `searching`, `reviewing`, `coding`, `writing`, `waiting`, `asking`, `listening`, `speaking`, `stuck`, `sleeping` |
30
- | `expression` | `neutral`, `happy`, `sad`, `angry`, `confused`, `curious`, `tired`, `love` |
31
- | `gaze` | `follow` (pointer), `ponder`, `scan`, `read`, `wander`, `ahead`, `down`, `away`; defaults per activity |
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` / `shading` | `solid` or `gradient`; soft-clay depth from `0` (flat, the default) to `1` |
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 of the current action |
42
- | `blink` / `motion` | turn blinking or looping off |
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
- Unset `shape`, `eyes` and colours come from the name. `mouth` and `accessory` stay off until you set them.
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
- A character's state is four independent layers. Each answers one question, and each can change without touching the others.
53
-
54
- | Layer | Answers | Lasts | Set with |
55
- |---|---|---|---|
56
- | Activity | what is the agent doing? | until changed | `activity` prop |
57
- | Expression | how does it feel? | until changed | `expression` prop |
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
- `sleeping` is the one exception: it keeps the eyes closed whatever the expression.
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
- An activity is the body language of a task. It is not a combination of an expression and a gaze. It sets the body's motion, picks a default gaze, and, only while the expression is `neutral`, may give the eyes their own pose: a glyph, a lid shape or a loop. It never sets an emotion, so `activity="coding" expression="happy"` is a happy coder, with the happy eyes replacing the `>_` until the expression returns to `neutral`.
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 in short jumps and hold, looking for something | `ahead` |
83
- | `reviewing` | head dips with the eyes | narrow to a slit, look down, up, back, then down once more; a 6s loop | `ahead` |
84
- | `coding` | bob | morph into a sharp `>_` prompt with a blinking cursor | `ahead` |
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 | settle to the centre, then circle around each other twice every 3.2s | `ahead` |
87
- | `asking` | head tilted | still, one eye wider: the person's turn to answer or approve | `ahead` |
88
- | `listening` | nod | slide together into one tall blinking cursor | `ahead` |
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 side to side once, then morph into sharp `x x` crosses | `ahead` |
91
- | `sleeping` | breathe, sunk | droop, catch themselves once, droop again and close. Leaving it wakes them: half-open, wide, back to rest | `ahead` |
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, drooping outward |
106
- | `love` | both eyes tilt together into one beating heart |
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 a mouse or pen anywhere on the page while it moves, then return to glancing 0.9s after it stops |
115
- | `wander` | glance around and hold, ignoring the pointer |
116
- | `ponder` | look up-left, then up-right, then back |
117
- | `scan` | sweep across: slow right, quick jump back |
118
- | `read` | read line by line: three short hops right, jump back and down a line, three lines, then back to the top |
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, drifting a little side to side |
121
- | `away` | look off to the side, with a brief check-in straight ahead |
100
+ | `down` | look down at the work |
101
+ | `away` | look off to the side |
122
102
 
123
103
  ### Event
124
104
 
125
- An event is a one-off reaction. `play(event)` resolves when the animation ends. Each reaction first centres the eyes and settles for half a second, except when triggered by a click or press.
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 of the eyes |
132
- | `handoff` | eyes and body lean to the side, then return |
133
- | `success` | happy eyes with a long bounce |
134
- | `warning` | a wince: the eyes shift aside, heavy lids press down twice, the head dips and rises |
135
- | `error` | the eyes morph into `x x` crosses, then the head and eyes sink, hold, and recover |
136
- | `surprised` | a squint, then wide eyes with a tremble |
137
- | `sigh` | a small breath in, then the lids sink and the head slumps before it recovers |
138
- | `yawn` | the head stretches tall while the eyes close, then a blink awake |
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 # playground
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?: false | number;
32
+ frame?: number;
33
33
  gaze?: Gaze;
34
34
  interactive?: boolean;
35
35
  label?: string;
36
- motion?: boolean;
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: false | number | undefined): string | undefined;
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 === false)
117
+ if (frame === undefined)
118
118
  return undefined;
119
- return `${clampFrame(frame ?? 0.5) * 100}%`;
119
+ return `${clampFrame(frame) * 100}%`;
120
120
  }
121
- export const Avatar = forwardRef(function Avatar({ accessory, activity = "idle", background, badge, blink = true, body, className, colors, content = "face", expression = "neutral", eyes, fill = "solid", frame = 0.5, gaze, interactive = false, label, motion = true, mouth, name, onChange, renderMouth, shading = 0, shape, size = 64, src, }, ref) {
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 !== false;
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-motion": motion ? undefined : "off", height: size, overflow: framed ? undefined : "visible", role: "img", style: {
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,
@@ -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 === false ? undefined : `0 0 0 2px ${ringColor}`,
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-motion=off] *{animation:none!important}
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", [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aaavatars",
3
- "version": "3.0.1",
3
+ "version": "4.0.0",
4
4
  "description": "Deterministic 2D character avatars with a play() animation API",
5
5
  "keywords": [
6
6
  "avatar",