npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bot-avatars

v0.1.1

Published

Animated bot avatars for React — eighteen glossy 3D shapes with living faces that turn, hop and flip, three states (idle, working, sleeping), 2D canvas, no WebGL

Readme

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, rounded extrusion: no WebGL, no runtime dependencies.

Live demo · Repository · Report an issue

Install

npm install bot-avatars

Quick start

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:

<BotAvatar type="clover" />    {/* sky */}
<BotAvatar type="flower" />    {/* pink */}
<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" />   {/* orange nut */}
<BotAvatar type="cat" />       {/* butter, round head, two ears */}
<BotAvatar type="cloud" />     {/* pale sky, five puffs */}
<BotAvatar type="pill" />      {/* indigo, a wide capsule */}
<BotAvatar type="pebble" />    {/* peach, a wide flat stone */}
<BotAvatar type="puddle" />    {/* salmon, a taller lumpy blob */}

Faces

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

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

States

<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

<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, more vivid than the palette */}
<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.

Shading

plastic (the default) 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. 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:

<BotAvatar
  shadow={1.4}      // 0–2, strength of the shadow side (default 0.35)
  highlight={0.6}   // 0–2, strength of the lit side (default 1.3)
  light={315}       // degrees clockwise from the top (default 265, from the left)
  depth={1.5}       // 0.2–2, thickness shown when the head turns (default 0.65)
  rim={1.5}         // 0–2, Fresnel strength in plastic, rim width in crisp (default 0.5)
  spread={0.7}      // 0.4–2.5, width of the highlight in plastic, reach of the soft shading in smooth (default 1.55)
/>

Other props

<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:

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',
  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 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.

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:

<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:

<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.

<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.

{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.
  • The body is thirteen copies of its outline stacked through the depth with a pillow profile, projected with the head's yaw and pitch and lit from the upper left — a rounded solid that turns and flips, in plain 2D canvas fills. Cheap enough for a whole roster at once.
  • Server rendering works: the canvas paints on the client, after mount.

License

MIT © Jakub Antalik