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

react-ai-pet

v0.4.4

Published

Customisable animated pet avatar for React: shapes, colours, expressions, jelly physics and cursor-following eyes. A ray-marched WebGL body, zero dependencies.

Readme

react-ai-pet

A small, dependency-free animated pet avatar for React: a handful of props, and a ray-marched WebGL body with real refraction and lighting.

Live demo, docs and playground: https://ai-pet.yajis.dev

  • 12 body shapes — circle, blob, squircle, capsule, triangle, hexagon, cloud, drop, star, poop (a soft-serve swirl), mochi (a rice cake: straight sides that round under into a soft base), daifuku (a squashed ball, gently rounded underneath) — that morph smoothly into each other
  • 17 expressions — neutral, happy, excited, laughing, surprised, curious, confused, dizzy, sad, angry, scared, sleepy, suspicious, wink, shy, proud, unimpressed — that blend into each other
  • Any colour (any CSS colour string), eye colour too, and eyeShape="dot" for small round bead eyes instead of the tall capsules. eyeShine adds a catchlight, blush soft pink cheeks, and eyes sets their size, spacing and height. Add color2 for a two-tone body that blends from one colour to the other, and iridescence for a thin-film rainbow sheen that shifts with the viewing angle and shimmers as the body moves
  • WebGL renderer — the body is a real 3D solid rendered by ray marching a signed distance field: lit like a small studio (a warm key light, a cool fill, sky above and the page's own colour bouncing up from below), GGX highlights per material, self-shadowing and contact occlusion, Fresnel reflections, refraction into a translucent jelly with Beer-Lambert absorption, eyes embedded inside it, a soft sky-tinted contact shadow, and a filmic tone map. Matte bodies get a clean clay look. Zero dependencies, one fragment shader.
  • Four materialsmatte (opaque clay), jelly (translucent), frost (milky and frosted: soft highlights, no see-through, and it glows where it is thin) and glass (clear and rigid: higher refractive index, sharp highlights, a second reflection off the far surface that lights up the edges). Every material is bouncy by default: a spring-based squash, wobble and lean plays when the avatar is poked, changes shape, or the pointer darts around (bouncy={false} turns it off).
  • Follows the cursor — the whole body turns towards the pointer (a real rotation of the body) and the eyes move a little with it; the eyes live on a sphere and foreshorten as they near the edge
  • Reactions — performances you can fire: alert (!), question (?), thinking (…), ponder (aurora ribbons swirling while it thinks), orbit (glowing orbs on atomic orbits while the body spins round twice), love (hearts), sparkle (stars), magic (a spiral of sparkles), celebrate (confetti), charge (winds up, then rings and sparks burst out), eyeBeam (whole eyes flicker to charge, then fire broad yellow-to-gold beams with a bright white core), shocked (freezes in comic shock with blue stress lines), idea (a light bulb pops up top right and flicks on), melt (flattens into a sleepy puddle), giggle (soft chuckles and a small hop), nuzzle (leans toward the cursor with a floating heart), nod, shake, sleepy (zzz), gloom (a private rain cloud), fume (steaming with anger), sweat (a cold sweat: darting eyes and a bead running down the cheek). The body hops, tilts or shakes, the face changes for a moment, and the effects are drawn in an SVG layer over the canvas. thinking, ponder, orbit, gloom, sweat and sleepy can be held
  • Idle life: gaze drift, breathing, a deterministic blink schedule
  • Honours prefers-reduced-motion
  • SSR-safe, no Date.now() or randomness in the frame: the engine is a function of time

Install

npm i react-ai-pet

Use

import { PetAvatar } from 'react-ai-pet'

<PetAvatar shape="blob" color="#3b82f6" expression="happy" material="jelly" size={240} />
<PetAvatar shape="drop" color="#1fb6c1" expression="surprised" material="glass" />
<PetAvatar shape="blob" color="#22d3ee" color2="#7c3aed" iridescence={0.6} material="glass" />

Props

| prop | type | default | | |---|---|---|---| | shape | ShapeId | 'circle' | body shape | | color | string | '#0b0b10' | any CSS colour | | color2 | string | | second colour: the body blends from color (top-left) to color2 (bottom-right) | | iridescence | number | 0 | thin-film rainbow sheen at grazing angles, 0 to 1 | | glow | number | 0 | the body lights itself in its own colour, 0 to 1, whatever the lamps or the page do; it lights the ground under it too | | halo | number \| { strength?, color?, size? } | none | a soft glow on the page behind the body: a number is the strength 0 to 1 in the body's colour; the object also sets the colour and how far it reaches (in multiples of the frame, default 1) | | eyeColor | string | '#ffffff' | | | eyeShape | 'capsule' \| 'dot' | 'capsule' | dot is a pair of small round beads; every expression still applies | | eyeShine | number | 0 | a small catchlight high in each eye, 0 to 1 | | eyeGlow | number | 0 | the eyes light up in their own colour and cast it on the body around them, 0 to 1 | | blush | number \| { strength?, color? } | none | soft cheeks under the eyes: a number is the strength 0 to 1 in a rosy pink; the object also sets the colour | | eyes | { size?, spacing?, height? } | | size and spacing as multiples of the default (1), height -1 (low) to 1 (high); changes ease in, expressions apply on top | | expression | ExpressionId | 'neutral' | | | material | 'matte' \| 'jelly' \| 'glass' \| 'frost' | 'matte' | body material; jelly (boolean) is a shorthand for material="jelly" | | bouncy | boolean | true | spring physics (squash, wobble, lean) on pokes, shape changes and fast pointer moves; false for a stiff body | | translucency | number | jelly and frost 0.5, glass 0.7 | how much light passes through a jelly, frost or glass body, 0 (opaque) to 1 (clear) | | inner | InnerPattern \| { pattern, color?, color2?, strength?, scale?, motion?, hueShift?, detail? } | none | what a jelly, glass or frost body holds inside, lit from within and seen through the surface. Patterns: orb (a few large soft veils of colour drifting through the dark), nebula (two colours of cloud with stars behind), swirl (smoke folding slowly), marble (fumed glass, a few thin luminous sheets folded and layered in the dark, faint face-on and bright along their edges, with glitter caught in the folds), sparkle (twinkling flecks), fireflies (motes wandering and blinking); color defaults to the body's hue at full brightness, color2 is the swirl's gaps, the nebula's other cloud, the orb's second wisp and the marble's second sheet, whose third lies between (default the first colour), strength 0 to 1 (0.7), scale a multiple of the default size (1), motion 0 to 1 (0.3), hueShift makes the second colour instead of color2: the first turned this many degrees round the hue wheel (180 the complement), default 0; detail 0 to 1 (0.5) is how finely the marble's sheets are drawn, thinner and crisper for more GPU | | backdrop | string | | a solid colour rendered behind the avatar for the jelly to refract; omit to let the page show through | | lighting | preset name or Lighting | 'studio' | up to four lights (direction, colour, intensity, size, softness, shadow), ambient, roughness, reflection, shadow strength / softness / tint, contact darkening, exposure. Presets: studio, noon, dramatic, soft, flat (quiet and even, no shadow) | | background | string | detected | the page colour behind the avatar, used to light and composite translucent bodies; read from the DOM by default, pass a CSS colour to override | | followCursor | boolean | true | eyes follow the mouse or pen anywhere in the window; a finger on a touch screen is not followed | | followReach | number | 1.6 | distance (in avatar widths) at which the gaze saturates | | size | number \| string | 240 | px, or any CSS length. The WebGL canvas is drawn 1.5× wider than this (centred, pointer-transparent) so the ground shadow is not cut off; keep overflow: hidden off the parent | | paused | boolean | false | freeze the animation. An idle avatar also stops painting on its own while it is scrolled out of view (reactions, entrances and exits still play), so a page full of avatars only pays for the ones on screen | | reducedMotion | boolean | OS setting | | | pokeOnClick | boolean | true | tap the body to bounce; the empty frame around it is click-through to the page beneath, and anything in front of the body that takes pointer events gets the tap instead | | onPoke | () => void | | | | enter | 'pop' \| 'roll' \| 'rollBack' \| 'drop' \| 'fade' \| 'summon' \| 'walk' | | entrance, played on mount and whenever visible turns true; summon draws a magic circle on the ground and materialises the body in a column of light | | exit | same | enter | exit, played when visible turns false | | visible | boolean | true | show / hide, animated when enter / exit are set | | onEnterEnd, onExitEnd | () => void | | | | stage | RefObject<Element> \| 'viewport' | | what entrances and exits travel from and to: a container's edge or the viewport's; default just past the avatar's own box | | reaction | ReactionId \| null | | plays a reaction whenever it changes to an id | | onReactionEnd | (id) => void | | | | effectColor | string | currentColor | colour of reaction marks and dots | | effectStyle | 'glass' \| 'solid' | 'glass' | marks and dots as translucent frosted glass, or flat fills | | speech | string \| Speech \| null | | a speech bubble beside the body, see Speech | | onSpeechEnd | () => void | | fires when a timed speech has been taken down | | gauge | number \| Gauge \| null | | a 0–100 progress gauge, poured into the body or ringed around it in light, see Gauge | | className, style, title | | | passed to the wrapper element (title labels the canvas) |

The body is drawn on a WebGL2 canvas; where WebGL2 is unavailable the frame stays empty, and the reactions, speech bubble and gauge ring (HTML and SVG overlays) still show. The canvas is transparent and premultiplied, so it composites onto any page background. The renderer reads the page colour behind it (or takes the background prop) and adapts: on a dark page the studio's ambient light goes away so glass shows as rims and highlights rather than a haze, the transmitted colour is composited against the real background, and the ground shadow becomes the faint light a translucent body lets through.

Lighting

<PetAvatar lighting="dramatic" />
<PetAvatar
  lighting={{
    lights: [
      { direction: [-0.6, 0.8, 0.5], color: '#fff1dd', intensity: 1.2, size: 0.2, shadow: true },
      { direction: [0.8, 0.2, 0.4], color: '#cfe4ff', intensity: 0.4, size: 0.6 }
    ],
    ambient: 0.8,
    roughness: 0.3,
    shadow: { strength: 1, softness: 0.3, tint: '#101828' },
    exposure: 1.1
  }}
/>

Directions point towards the light (x right, y up, z towards the viewer). A light's size widens its highlight and softens the shadows it casts; softness is how soft the lamp's edge is where it reflects in the body (0 a crisp soft box, as a glass ball shows it, 1 a hazy glow; left out, matte and jelly are hazy and glass is crisp); shadow: true makes it cast the ground shadow and self-shadowing (the first light does by default). Anything you leave out keeps the studio default, so { ambient: 0.5 } alone just dims the room.

Entrances and exits

const [open, setOpen] = useState(true)
<PetAvatar enter="pop" exit="roll" visible={open} onExitEnd={() => console.log('gone')} />

pop grows in with a boing and shrinks away, roll rolls in from the left and out to the right (rollBack the other way), drop falls in and lands with a squash then jumps away, walk takes small swaying steps in from the left and out to the right. fade is the quiet one. With enter set the avatar mounts hidden and plays the entrance; ref.current.enter(id?) / exit(id?) play them on demand (measuring the stage for you). Pass stage={containerRef} (or "viewport") and the roll and drop travel from that container's edge instead of the avatar's own box: the whole element is moved with a CSS transform, so nothing is clipped on the way.

Reactions

const ref = useRef<PetAvatarHandle>(null)
<PetAvatar ref={ref} />
ref.current?.avatar.react('alert')                          // one shot
ref.current?.avatar.react('ponder', { duration: Infinity })   // hold …
ref.current?.avatar.stopReaction()                          // … until you say so

Or declaratively: <PetAvatar reaction="love" onReactionEnd={() => setReaction(null)} /> plays whenever the prop changes to a new id. Marks and dots use the layer's CSS color (effectColor prop, default currentColor), so they follow your page's text colour; hearts and stars have their own colours.

Reactions are defined in src/core/reactions.ts as pure functions of the time since they started, returning a body pose (squash, offset, tilt, turn), a temporary expression and gaze, and a list of effects to draw. Adding one is adding an entry to that table.

Speech

<PetAvatar speech="Hi there! 👋" />
<PetAvatar speech={{ text: 'Over here', placement: 'left', offset: { y: -10 }, typing: 60 }} />
ref.current?.say('Done!', { duration: 3000 })   // a one-off line, then back to the prop
ref.current?.say(null)                          // take it down

The bubble is plain HTML positioned against the avatar's box, with a tail pointing at the body, so it may hang out over the page (keep overflow: hidden off the parents). It pops in, types its text (typing characters per second, 0 for all at once) and pops out when cleared or when duration runs out. placement is one of top, top-left, top-right, left, right, bottom, bottom-left, bottom-right (default top-right); offset nudges it in pixels; maxWidth, fontSize, background, color, className and style restyle it. The bubble rides on the body: it sways with the idle drift, bounces with a poke and turns with the body towards the pointer; follow: false keeps it still beside the frame. The bubble (and the gauge's ring) waits until the avatar is on stage: it appears once an entrance has finished and goes away as an exit starts, so a timed line set during an entrance starts its clock when it is shown.

Gauge

<PetAvatar gauge={progress} />                                        // liquid fills the body
<PetAvatar gauge={{ value: progress, style: 'ring', color: '#ff7ae6' }} />   // a ring of light around it

value runs 0–100 and the drawn level eases after it, sloshing while it moves. 'fill' (default) pours a liquid into the body: a real volume inside the ray-marched body, absorbing and scattering light in a jelly or glass and painted with a meniscus on a matte one. 'ring' closes a glowing ring of light clockwise around the body, with a spark at its head, pulsing once complete; label: true prints the percentage under it.

Imperative control

const ref = useRef<PetAvatarHandle>(null)
<PetAvatar ref={ref} />
ref.current?.avatar.poke()
ref.current?.avatar.setLook({ x: 0.5, y: -0.2 }) // -1..1 each axis, null to release
ref.current?.say('Hello', { duration: 2000 })      // a speech bubble
ref.current?.element // the <canvas>

Without React

The engine has no framework dependency: createAvatar gives you an object you tick with a clock, and createGlRenderer draws a frame onto any WebGL2 canvas.

import { createAvatar } from 'react-ai-pet/core'
import { createGlRenderer } from 'react-ai-pet'

const avatar = createAvatar({ shape: 'star', expression: 'excited' })
const renderer = createGlRenderer(canvas) // null where WebGL2 is unavailable
renderer?.resize(canvas.width, canvas.height)
const tick = (now: number) => {
  renderer?.render(avatar.update(now), { color: '#f6c945', eyeColor: '#ffffff', material: 'jelly', translucency: 0.3 })
  requestAnimationFrame(tick)
}
requestAnimationFrame(tick)

A still is canvas.toDataURL() after one render().

How it works

  • Shapes are radial profiles. Every body is r(θ) sampled at 96 angles, so morphing between any two shapes is a per-sample lerp of radii. Polygons, unions of circles and parametric curves are turned into profiles by ray casting and then smoothed to round the corners. All shapes are normalised to the same area so they carry the same visual weight.
  • Eyes sit on a sphere. Each eye has a longitude and latitude; the gaze rotates the sphere and the eye is projected orthographically, squashing as it turns away. The projected point is then scaled by the body's real radius in that direction so the face fits non-circular shapes.
  • Expressions are eye specs: width, height, roundness, tilt, and two lids (in body colour, clipped to the eye) whose curvature ranges from a tight smiling arch to a flat line.
  • Lids are cut out of the eye in the shader rather than painted over it, so a translucent body shows through a half-closed eye.
  • Jelly motion is three damped springs: squash/stretch, a third-harmonic outline wobble, and a lean towards the pointer. Impulses come from pokes, shape and expression changes, and fast gaze movement.
  • The WebGL renderer ray-marches an SDF. The 2D profile is inflated like a balloon: the CPU solves a Poisson pressure field inside the outline (src/gl/inflate.ts, a 64×64 grid re-solved only when the profile changes) and the shader reads it as a texture, so the body is an exact ellipsoid for a circle, thinner in narrow lobes, and free of creases along dents in the outline; near the silhouette the radial ratio rho / r(θ) takes over, which is exact there. The face is a flat 2D drawing projected onto it (printed on a matte body, seen just under the surface of a jelly one), and each pixel marches a ray into the scene. Jelly pixels refract into the body, march again to the exit, and attenuate by the path length. The ground is invisible except for its soft shadow. Silhouettes are anti-aliased from the closest miss distance.

Develop

pnpm install
pnpm dev        # demo on http://localhost:5180 — Docs (this reference, with a copy-as-Markdown button) at /docs; Playground (stage left, controls right; stacked on narrow screens) from the app bar; Board (every shape × expression) at /playground#board; light / dark toggle top right
pnpm test
pnpm build      # library to dist/

The demo is a Vite app in demo/ built with shadcn/ui on Tailwind v4. Add UI components with the CLI, never by hand: pnpm dlx shadcn@latest add <component> (config in components.json, components land in demo/components/ui). Sliders are the smoothui Scrubber, added the same way from its shadcn registry: pnpm dlx shadcn@latest add https://smoothui.dev/r/scrubber.json (lands in demo/components/smoothui/scrubber; the CLI may drop it under src/components, move it into demo/). The demo's dependencies are devDependencies; the library itself has none.

Deploy the demo to Cloudflare Workers

The GitHub Actions workflow in .github/workflows/deploy.yml deploys the demo on every push to main, including merged pull requests. It runs typechecking, tests, and the Vite build before publishing dist-demo/ with Workers Static Assets. You can also run the workflow manually from the Actions tab on main.

In the repository's Settings → Secrets and variables → Actions, add these repository secrets:

| Secret | Value | |---|---| | CLOUDFLARE_API_TOKEN | A Cloudflare API token created using the Edit Cloudflare Workers template, scoped to the deployment account | | CLOUDFLARE_ACCOUNT_ID | The Cloudflare account ID for that account |

See Cloudflare's GitHub Actions setup guide for token creation and account ID instructions. Ensure the account has a workers.dev subdomain configured in Cloudflare.

The Worker name is ai-pet (change name in wrangler.jsonc if needed). The deployment URL will be https://ai-pet.<your-subdomain>.workers.dev; Wrangler prints the actual URL in the Actions deploy log. No custom domain is required.

To validate or deploy locally:

pnpm build:demo
pnpm exec wrangler deploy --dry-run  # validate without publishing
pnpm exec wrangler login            # authenticate for local deployment
pnpm deploy                        # build and publish the demo

License

MIT