# bot-avatars

Animated bot avatars for React. Eighteen simple 3D shapes with living faces — round eyes that blink and glance, an optional mouth — and three states an agent can be in: idle, working and sleeping. Each one turns its head, looks around, hops and flips, and every state change is a cross-animation. Vector shapes drawn on a 2D canvas as a lit, inflated body in plush faux fur (or glossy plastic): no WebGL, no runtime dependencies.

[Live demo](https://libraries.dev/bots) · [Repository](https://github.com/Jakubantalik/Libraries.dev) · [Report an issue](https://github.com/Jakubantalik/Libraries.dev/issues)

## Install

```bash
npm install bot-avatars
```

## Quick start

```tsx
import { BotAvatar } from 'bot-avatars';

function Agent({ busy }: { busy: boolean }) {
  return <BotAvatar type="clover" state={busy ? 'working' : 'default'} />;
}
```

## Types

Eighteen bodies, each with its own colour:

```tsx
<BotAvatar type="clover" />    {/* sky */}
<BotAvatar type="flower" />    {/* green */}
<BotAvatar type="triangle" />  {/* magenta */}
<BotAvatar type="square" />    {/* sky */}
<BotAvatar type="blob" />      {/* green */}
<BotAvatar type="ghost" />     {/* white */}
<BotAvatar type="circle" />    {/* lavender */}
<BotAvatar type="drop" />      {/* teal */}
<BotAvatar type="star" />      {/* yellow */}
<BotAvatar type="droid" />     {/* pale, antenna ball, round ears */}
<BotAvatar type="mech" />      {/* slate, wide head, two antennae */}
<BotAvatar type="alien" />     {/* lime, wide brow, narrow chin */}
<BotAvatar type="hexagon" />   {/* coral nut */}
<BotAvatar type="cat" />       {/* apricot, round head, two ears */}
<BotAvatar type="cloud" />     {/* pale sky, five puffs */}
<BotAvatar type="pill" />      {/* indigo, a wide capsule */}
<BotAvatar type="pebble" />    {/* sage, a wide flat stone */}
<BotAvatar type="puddle" />    {/* pink, a taller lumpy blob */}
```

### Your own shape

`path` takes SVG path data for the body outline, in a 100×100 box centred on (50, 50), in place of the type's own. It is inflated, lit and animated like any stock shape. The type still sets where the face sits, the default colour and the label, so keep the outline solid round the face.

```tsx
const heart = 'M50 88C50 88 12 64 12 38C12 24 23 14 35 14C42 14 47 18 50 23C53 18 58 14 65 14C77 14 88 24 88 38C88 64 50 88 50 88Z';

<BotAvatar type="circle" path={heart} color="#ff5c8a" />
```

The stock outlines are exported as `botAvatarShapes`, a good place to start from.

## Faces

The eyes alone by default. `face="mouth"` adds a small mouth that changes with the state:

```tsx
<BotAvatar type="clover" face="mouth" />
```

## States

```tsx
<BotAvatar state="default" />   {/* idle: turns to look around, blinks, a jump with a full turn now and then */}
<BotAvatar state="working" />   {/* busy: hopping — every third hop a spin — with a wide smile and the odd laugh */}
<BotAvatar state="sleeping" />  {/* head down, lids shut, slow breaths and the odd nod */}
```

Every state is a resting pose plus its own motion. The pose is a small rig — yaw, pitch, roll, position, squash, eyes — and a state switch eases from the old targets to the new ones on a timed curve (soft start, soft finish), so it never snaps or creeps. With `prefers-reduced-motion: reduce` (or `paused`) the still pose of the state is drawn instead.

## Colour

```tsx
<BotAvatar type="blob" color="#ff5c8a" />          {/* any body colour */}
<BotAvatar type="clover" color="#111" />            {/* the ink turns light on a dark body */}
<BotAvatar type="star" ink="#4D7CFF" />             {/* or pick the ink yourself */}
<BotAvatar type="drop" brightness={1.25} />          {/* lighter body; below 1 darker */}
<BotAvatar type="drop" saturation={0.7} />           {/* duller body; the default is 1.5 (or the type's own), more vivid than the palette */}
<BotAvatar type="clover" saturation={2.2} />         {/* past 1.5 (up to 2.5) more vivid: richer colour, richer shade, coloured light */}
<BotAvatar type="clover" paused pose={{ yaw: 0.8, pitch: -0.2 }} />  {/* held turned round, as a 3D viewer would */}
<BotAvatar type="clover" shading="plastic" />       {/* glossy plastic instead of the default plush fur */}
<BotAvatar type="square" shading="crisp" />         {/* a lit rim with a clean edge */}
<BotAvatar type="square" shading="smooth" />        {/* soft shadow and highlight, no edge */}
<BotAvatar type="square" shading="flat" />          {/* keep the depth, drop the lighting */}
```

The palette is exported as `botAvatarPalette` (type → colour), with `botAvatarPresets` carrying each type's face and label (and, for a few, their own `brightness` and `saturation`).

## Shading

`fabric` is the default, a plush fur (below); `plastic` is a real material, shaded per pixel: the outline is baked once into a pillow height field (lobes become domes, rays become tubes, the cusps between them fall into shadow), and every frame a lit sphere is evaluated for the head's pose — wrap-around diffuse, a tight hot spot and a broad sheen, a Fresnel rim under a sky, a window reflection, saturated shadows — and mapped onto it. `fabric` dresses the same form in a plush faux fur, like a toy's, under a strong studio light: a key from the upper left, low and from round the side, with a clear terminator and a real shade side (the colour deepened, never grey, with a saturated band where the key turns into shade), a dim fill, the floor's shade under the body, and a back light from behind the toy that the fibres carry through to a bright rim over the top and the side away from the key, the edge's hairs glowing. Single fibres catch the key as highlights along the locks where it grazes them. `backLight` sets where the light behind comes from, `lightFront` how far round to the front the key sits, `shine` the fibres' highlights, `backSoftness` how soft the back light is (crisp hairs to an even glow), `sheen` the smooth satin light over the edge (0 for a matte pile), and `rim` the back light's strength. The fur is a volume, not a texture: tapered locks lie on the curved body, each a lit mound with a shaded side and a contact shadow under the tip of the one above, made of fine strands with dark roots and lighter tips that cast thin shadows into the pile, gathered into tufts with darker partings between them; it covers the whole body as one continuous pile, round the sides and the back as it turns, its gaps a deep shade of the body's own colour. The silhouette is a soft halo: a haze of fine hairs lit toward their tips, broken into tufts, laid round the whole turned body, so a side view or a flip is as fuzzy as the front. Fabric has its own default light (`shadow` 1.15, `highlight` 1.45, `light` 295, `rim` 0.6, `spread` 1.6, `lightFront` 32, `shine` 0, `sheen` 0, `backSoftness` 1).

Both are drawn on an inflated body: thickest in the middle and rounding off to nothing at the outline all round, like a cushion or a plush toy, so a turn shows a round side rather than the wall of an extruded shape, smooth all the way round to a side view. `roundness` (0–1, default 1) bends that profile toward a slab with soft edges. `crisp` lights a rim round the front with a clean edge, `smooth` lays a soft shadow and highlight over the whole form, `flat` keeps only the depth. The light itself is adjustable:

```tsx
<BotAvatar
  shadow={1.4}      // 0–2, strength of the shadow side (default 1.15 in fabric, 0.35 in the others)
  highlight={0.6}   // 0–2, strength of the lit side (default 1.45 in fabric, 1.3 in the others)
  light={265}       // degrees clockwise from the top (default 295 in fabric, 300 in the others: the upper left)
  depth={1.5}       // 0.2–2, thickness shown when the head turns (default 0.65)
  roundness={0.5}   // 0–1, plastic and fabric: a cushion at 1, toward a slab at 0 (default 1)
  rim={1.5}         // 0–2, strength of the back light in fabric, of the Fresnel rim in plastic, rim width in crisp (default 0.6 in fabric, 0.5 in the others)
  spread={0.7}      // 0.4–2.5, how far the light wraps round in fabric, width of the highlight in plastic, reach of the soft shading in smooth (default 1.6 in fabric, 1.55 in the others)
/>
```

The plush pile has a style of its own:

```tsx
<BotAvatar
  furLength={2}     // 0.3–2.5, length of the pile (default 1)
  furDensity={1}    // 0.3–2, how many fibres (default 1.6)
  furFuzz={0.5}     // 0–1, the haze of hairs at the silhouette (default 0.9)
  furClumps={0.7}   // 0–1, how much the fibres gather into tufts (default 0.4)
  furCurl={0.5}     // 0–1, straight to wavy (default 0.7)
  furGravity={0.4}  // 0–1, how much it hangs: combed down from the top, hanging off the sides (default 0.9)
/>
```

Each style is baked once per shape on idle time and shared by every avatar that wears it.

## Wear

A hat, glasses, headphones and a bow tie, each a small 3D object built from lit surfaces and placed on the body's own shape — the hat seated where the head is wide enough to hold it, the headphones on its widest row with the band resting on the head, the glasses over the eyes, the bow tie under the face — and turned with the head, so it tips, turns and flips with the body. They are lit from the body's light and cast a soft shadow where they sit. Each surface is built once per shape and size and painted a strip at a time with the light carried along it, so a hat is a few dozen canvas fills — about 0.2–0.8 ms a frame at 120 px.

```tsx
<BotAvatar type="clover" hat="beret" />
<BotAvatar type="triangle" glasses="round" bowTie />
<BotAvatar type="flower" headphones accessoryColor="#f4efe6" />
```

- `hat`: `none` (default), `beret` (pleated felt with a leather binding and a stalk), `beanie` (knit stitch by stitch, with a ribbed cuff and a yarn pompom), `party` (a striped paper cone with a foil trim and tinsel) or `crown` (gold, with ridged points, pearls, cut stones, a velvet cap and an orb and cross)
- the headphones are over-ear: leather cushions, brushed metal rings and yokes, a stitched padded band; the glasses are acetate with rounded rims and reflecting lenses; the bow tie is satin, or felt on a plush body
- `glasses`: `none` (default), `round`, `square` or `shades`
- `headphones`, `bowTie`: booleans
- `accessoryColor`: the hat's, the headphones' and the bow tie's colour, a soft black by default (the crown stays gold)

## Other props

```tsx
<BotAvatar
  type="star"
  size={40}            // px, or any CSS length; default 64
  speed={1.5}          // multiplier on every animation
  paused={false}       // freeze on the current frame
  seed={0.3}           // 0–1: offsets the blink and glance loops; auto by default
  interactive={false}  // no pointer following, no hop on click
  theme="light"        // the surface the avatar sits on; auto by default
  aria-label="Talent scout, working"  // overrides the per-state default
/>
```

All other `<canvas>` props (`className`, `style`, `onClick`, `data-*`, …) pass through. The canvas draws a little larger than its box and pulls itself back with negative margins, so a hop or a flip is never clipped while the layout stays exactly `size` square.

## Drawing it yourself

The rig and the renderer are exported for custom uses — a filmstrip, a sprite sheet, another canvas:

```ts
import { BotAvatarSim, drawBotAvatarFrame, botAvatarShapes, botAvatarPresets, autoInk, BOT_AVATAR_OVERSCAN } from 'bot-avatars';

const sim = new BotAvatarSim(0.5, 'working');
sim.update(1 / 60); // advance a frame
drawBotAvatarFrame(ctx, 64, sim.pose, {
  path: new Path2D(botAvatarShapes.clover),
  ...botAvatarPresets.clover, // face, faceX, faceY, faceScale, color
  ink: autoInk(botAvatarPresets.clover.color),
  shading: 'plastic',
  typeKey: 'clover', // names the outline for the material caches, the key warmBotAvatarPlastic takes
  dpr: devicePixelRatio, // the scale the context is set to (optional; else read from the context)
  sides: 'auto', // plastic's side slices: 'vector' fills, or 'sprite' blits (what WebKit gets by default)
}); // on a canvas 64 * BOT_AVATAR_OVERSCAN * devicePixelRatio px square; the body's centre sits BOT_AVATAR_RISE * 64 below its middle
```

In `plastic` and `fabric` the first frame of a new type bakes its form (a few ms, done on idle time when an animation loop is running; the smooth look stands in until then). `warmBotAvatarPlastic(type, path, devicePx)` bakes ahead of time; `warmBotAvatarPlastic(type, path, devicePx, depth, true)` also queues `fabric`'s pile, in small steps on idle time.

## The idle look

Idle, the head looks to a corner, stays a few seconds and swings across to the opposite one. `turn` scales how far it goes to the side:

```tsx
<BotAvatar turn={1.4} />   {/* 0–2; 1 as the library has it, 0 faces forward */}
```

## The jump

Now and then in the idle state, and on every click, the avatar jumps and turns right round. Its numbers are props:

```tsx
<BotAvatar
  jumpHeight={32}    // body units; the body is 100 tall (default 26)
  jumpTime={0.8}     // seconds in the air (0.68)
  jumpStretch={1.3}  // stretch in the air, 0–2 (1)
  jumpSquash={1.4}   // squash on the ground, before take-off and on landing, 0–2 (1.15)
  jumpSquashTime={0.3}     // seconds the landing squash takes (0.37)
  jumpSquashEase="bouncy"  // sharp | pulse | soft | bouncy (pulse)
  jumpGroundTime={0.2}     // seconds held at the deepest squash on the ground (0.11; 0 for none)
  jumpGroundEase="bouncy"  // how the weight settles through it: sharp | pulse | soft | bouncy (pulse)
  jumpRiseTime={0.5}       // seconds from the deepest squash back to shape (0.33)
  jumpRiseEase="bouncy"    // how it rises: sharp | pulse | soft | bouncy (pulse)
  jumpClickSquashTime={0.8}  // seconds a click's jump takes for its crouch and landing squash (0.24)
  jumpSpin={2}       // whole turns in the air, 0–2 (1)
  jumpLean={10}      // degrees of lean into it (6)
  jumpEvery={5}      // seconds between idle jumps, give or take 40 %; 0 for none (8)
  jumpLand={-0.08}   // when the landing squash begins: seconds before (negative) or after touch-down (0, at contact)
/>
```

## Pointer play

By default an avatar's eyes and head follow a pointer that comes within a few head widths, and a click makes it hop and turn right round, in any state. Turn it off with `interactive={false}`; your own `onClick` still runs either way.

## The whirl

Off by default. With `whirl` set, a spin — the idle jump, a click, the working spin hop — draws a puff of motion round the body: one tapered trail on a tilted ring, made of the body's own material as a translucent plastic tube, lit from the same light, passing behind the body on the far side and over the face on the near side, where it casts a soft shadow. It is only there while the turn is under way, and it is already moving when it appears.

```tsx
<BotAvatar
  whirl={1}           // 0–2 strength; off by default
  whirlSize={1.1}     // 0.6–1.6 ring size
  whirlWidth={0.8}    // 0.4–2 trail thickness
  whirlLength={1.3}   // 0.4–1.6 trail length round the ring
  whirlTilt={1.4}     // 0.5–1.8, how open the ring is seen
/>
```

## A row of them

Each instance seeds its own blink timing from its React id, so a roster never blinks in unison. Pass the same `seed` to two avatars to make them move in step.

```tsx
{agents.map((a) => (
  <BotAvatar key={a.id} type={a.avatar} state={a.busy ? 'working' : 'default'} size={32} />
))}
```

## Accessibility & performance

- `role="img"` with a per-state `aria-label` ("Clover bot, working") out of the box.
- `prefers-reduced-motion: reduce` keeps the pose and drops the motion.
- One shared animation frame loop for every avatar on the page; each one pauses when scrolled offscreen or when the tab is hidden. Device-pixel-ratio capped at 2.
- In `crisp`, `smooth` and `flat` the body is seventeen copies of its outline stacked through the depth with a pillow profile; in `plastic` and `fabric` it is slices through an inflated height field, each in the side's own light, with the lit front laid over them — a rounded solid that turns and flips, in plain 2D canvas fills, about 1 ms of main thread a frame (fabric a little more: its fur is baked once per shape on idle time and laid over the whole body in one pass). Cheap enough for a whole roster at once.
- Server rendering works: the canvas paints on the client, after mount.

## License

MIT © Jakub Antalik
