Description:
Moodstone is a React Native avatar component that represents AI agent activity through animated facial expressions.
Procedural shapes, two eyes, and changing light form the avatar; nine moods and seven morphing silhouettes control its appearance and motion.
Change the mood as an agent thinks, streams, finishes, or fails. PNG snapshots and static or animated SVG exports extend the avatar beyond the native app.
Preview
How To Use It
Install Moodstone and dependencies:
npx expo install @shopify/react-native-skia react-native-reanimated react-native-worklets
npm install moodstone
Use it in your app
import { Moodstone } from "moodstone";
export function AgentAvatar() {
return <Moodstone color="pine" mood="thinking" cut="hexagon" size={48} />;
}Map Agent Activity to Moods
Change mood when the agent’s activity changes. A new mood restarts its animation loop. Shape and color remain independent choices.
| Mood | Activity |
|---|---|
idle | Ready for input |
observing | Reading context or awaiting a tool result |
thinking | Searching or making a short tool call |
processing | Reasoning for an extended period |
working | Streaming output |
done | Finished successfully |
failed | An operation failed |
invalid | Input was rejected |
inactive | Offline or paused |
Shape, Color, and Playback
| Prop | Default | Role |
|---|---|---|
mood | idle | Activity state from the mood table |
cut | circle | Silhouette preset: circle, squircle, square, diamond, hexagon, badge, and burst. |
color | pine | One of 12 palette colors or a hex string |
eyeColor | white | White, black, or a hex color |
size | 64 | Canvas size in dp |
seed | DEFAULT_SEED | Three numbers for light start angle, drift, and orbit |
animated | true | Set false for the mood’s still key pose |
paused | false | Freeze the current loop |
phase | Unset | Still-frame position or loop start offset from 0 to 1 |
morphDuration | 460 | Shape-morph duration in milliseconds; 0 snaps |
tune | Unset | Cut-specific shape parameters |
shape | Unset | Partial ShapeParams for a custom silhouette; mutually exclusive with tune |
style | Unset | React Native View style on the canvas |
ref | Unset | MoodstoneHandle for snapshots, restarts, and resolved specifications |
Use
animated={false}for a static avatar in long lists. Keep a seed stable for an avatar’s lighting identity, or userandomSeed()when creating a new avatar.
useAvatarSpec(props)builds a resolved specification from component props when no rendered avatar ref is available.
Tune a Silhouette
tune accepts parameters specific to the cut. Hexagons expose sides, round, and rot; badges and bursts expose lobes, depth, and sharp.
<Moodstone
cut="hexagon"
tune={{ sides: 5 }}
color="#4f8f79"
mood="working"
size={64}
/>Capture a PNG
Use the ref handle to render an offscreen snapshot. snapshot accepts size (default 1024 pixels), optional background (transparent when omitted), and optional loop phase. It returns a Skia image or null; encode the image as a PNG string.
| Ref method | Purpose |
|---|---|
snapshot(options) | Render an offscreen Skia image |
restart() | Replay the current mood |
getSpec() | Return the resolved avatar specification |
import { useRef } from "react";
import { Button, View } from "react-native";
import { Moodstone, type MoodstoneHandle } from "moodstone";
export function ExportableAvatar() {
const avatar = useRef<MoodstoneHandle>(null);
function capture() {
const image = avatar.current?.snapshot({
size: 1024,
background: "#0e1113",
});
const png = image?.encodeToBase64();
if (png) console.log(png);
}
return (
<View>
<Moodstone ref={avatar} mood="done" color="sky" />
<Button title="Capture avatar" onPress={capture} />
</View>
);
}Generate Static or Animated SVG
The animated SVG uses SMIL. For tuned or custom shapes, populate the specification’s shape and run it through resolveSpec to fit the eyes.
import {
DEFAULT_SEED,
PALETTE,
renderAvatarSvg,
renderAnimatedAvatarSvg,
stillFrame,
type AvatarSpec,
} from "moodstone";
const spec: AvatarSpec = {
seed: DEFAULT_SEED,
color: PALETTE.pine,
cut: "circle",
mood: "thinking",
eyeColor: "#FFFFFF",
};
const stillSvg = renderAvatarSvg(spec, stillFrame(spec), { size: 240 });
const animatedSvg = renderAnimatedAvatarSvg(spec, { size: 240, fps: 24 });