Over the past few weeks I built a small interface experiment called Dot, a minimal SVG avatar that can express emotional states through geometry, respond to your voice in real time, and track your cursor.
It’s not an assistant. It’s not a chatbot. It’s a face.
Live demo · Source · npm package
Why a Face?
Most AI interfaces are text boxes. You type, you wait, you read. This works, but it ignores something fundamental: humans evolved to read faces, not loading spinners.
We extract intention and emotional state from incredibly small visual signals: a slight eyebrow raise, a half-second delay before a response, the direction of a gaze. These cognitive systems are deeply embedded and absurdly fast. Text interfaces ignore all of them.
Dot started as a question: what if the interface was the face?
Not a face rendered on top of a chat window. Not an avatar next to a message bubble. The face itself as the entire interaction surface, where every emotional state, every audio reaction, every cursor movement maps directly to geometry.
The Emotion Engine
The core of Dot is a 5-axis emotion model:
joy · sadness · surprise · anger · curiosity
Each axis is a float from 0 to 1. States blend: you can have joy: 0.6 and curiosity: 0.8 simultaneously, producing a face that looks interested and pleased. The axes aren’t mutually exclusive; they’re dimensions in an emotion space.
The interpolation runs at 60fps via requestAnimationFrame. Every frame, the current emotion moves 8% toward the target state. Separately, a “base emotion” drifts 1.5% toward the current state, creating a natural inertia where the face settles slowly after a stimulus ends rather than snapping back.
// Every frame
const next = lerp(current, target, 0.08);
const newBase = lerp(base, next, 0.015);This two-layer system means the face always feels like it’s arriving somewhere rather than being placed somewhere. The distinction sounds small but it’s the difference between mechanical and organic.
Three Faces, Three Personalities
I built three visual variants to test how geometry affects perceived personality:



| Variant | Geometry | Animation | Personality |
|---|---|---|---|
| minimal | Soft ellipse eyes, smooth curves | Sinusoidal easing | Calm, approachable |
| tron | Sharp rectangular eyes, hard edges | Stepped/quantized motion (CRT-like) | Digital, precise |
| analogue | Stroked ellipses + pencil turbulence filter | Organic wobble, line boil | Hand-drawn, warm |
The interesting finding: changing only the eye shape and animation easing completely shifts how people describe the avatar’s personality. Same emotion values, same code, different geometry, different character.
This reinforced something I kept coming back to throughout the project: shape is personality.
Voice Reactivity
Dot listens to your microphone and reacts in real time using the Web Audio API. But rather than treating audio as a single volume number, I split the input into five frequency bands:
| Band | Frequency | What it drives |
|---|---|---|
| Bass | 60–250 Hz | Body breathing / scale pulse |
| Low-mid | 250–500 Hz | Mouth opening |
| Mid | 500–2 kHz | Mouth width variation |
| High-mid | 2–4 kHz | Eye micro-pulse |
| Presence | 4–8 kHz | Random glance triggers |
This means different voices produce different reactions. A deep voice drives more body movement. A bright voice triggers more eye activity. The face doesn’t just respond to volume. It responds to timbre.
Cursor as Emotion Input
The entire viewport is an interaction field. Moving your cursor maps to emotional state:
- Up → joy
- Down → sadness
- Horizontal extremes → anger, curiosity
- Distance from center → intensity
There’s a dead zone in the center (18% radius) so the face doesn’t twitch from minor mouse movements. Beyond that, a smooth clamping function scales intensity. The pointer contribution blends 60/40 with the base emotion, so it inflects the resting state rather than overriding it.
On mobile, swiping left/right cycles through emotion presets. The drag uses a 60px threshold so casual touches don’t trigger transitions.
Autonomous Behaviors
These run with zero configuration. The face is alive from the moment it renders:
- Blinking: randomized 2.5–6s intervals, variant-specific animation
- Breathing: subtle scale oscillation, tempo varies by variant
- Glancing: micro eye movements every 4–12s
- Idle mouth: gentle resting wave on the mouth curve
- Pointer repulsion: face leans subtly away when the cursor gets very close
- Pencil boil: animated SVG turbulence filter on the analogue variant
The combination means Dot never feels static. Even with no input, the face breathes, blinks, and glances around. This turned out to be more important than any single feature: presence requires continuous subtle motion.
Extracting the npm Package
Midway through development I realized the face system was separable from the app. So I extracted it into a standalone React component:
npm i @xoboid/avatar gsapimport { Avatar } from "@xoboid/avatar";
import "@xoboid/avatar/avatar.css";
<Avatar
emotion={{ joy: 0.8, sadness: 0, surprise: 0.2, anger: 0, curiosity: 0.3 }}
variant="minimal"
color="#FF6B6B"
interactive
/>;That gives you a living face with blinking, breathing, idle glances, pointer tracking, and boop reactions. Peer dependencies are just React 18+ and GSAP 3.12+. Ships as ESM + CJS with full TypeScript declarations.
The idea is that anyone building an AI product, a game, a dashboard, or anything that could benefit from an expressive presence can drop this in without rebuilding the animation system from scratch.
Full docs and API reference on npm →
Technical Decisions
A few choices worth noting:
Why GSAP instead of CSS animations or Framer Motion?
The face has 12+ SVG attributes updating in sync every animation frame: eye radii, pupil positions, mouth curves, container scale, rotation, glow intensity. CSS transitions are declarative (you describe the end state) and spring libraries are too. GSAP is imperative: you control the exact value of each attribute per frame via shared timelines. For a face where everything needs to move together with precise timing, the imperative model is significantly easier to reason about.
Why SVG instead of Canvas or WebGL?
Accessibility and composability. SVG elements are part of the DOM, so they inherit CSS variables (currentColor, custom properties), respond to media queries, and work with existing React tooling. The face is styled with one accent color via CSS variables: change the variable, everything updates atomically. Try that with Canvas.
Why five emotion axes instead of discrete states?
Discrete states (happy, sad, angry) create jarring transitions. Continuous axes allow blending: a face can be 60% curious and 30% joyful simultaneously, which reads as “interested and pleased.” This is closer to how human emotion actually works, and it makes the interpolation system trivially simple.
What I Learned
Presence matters more than complexity. A circle with two eyes and motion is enough to communicate a wide range of states. Humans are extraordinarily good at reading intent from minimal visual information. When complexity disappears, the remaining signals get stronger.
Emotion is already an interaction layer; most interfaces just ignore it. Loading spinners communicate impatience. Lag communicates friction. Instant responses communicate confidence. These emotional signals exist whether you design for them or not. Dot makes emotional state the primary interface instead of an accident.
Shape is personality. Changing only eye geometry and animation easing completely shifts how people perceive the avatar. Same code, same emotion values, different character. This suggests that interaction design for AI companions will be less about features and more about carefully chosen visual and temporal details.
The smallest possible interface can be the most expressive. Dot is a circle, two eyes, and motion. That’s it. Yet it can express curiosity, joy, confusion, surprise, and anger, legibly, at a glance. Minimalism here isn’t aesthetic restraint. It’s communication efficiency.
An Algorithmic Face for AI
What I actually want to build is bigger than Dot. AI has a voice now. It can talk, listen, and answer in real time. What it doesn’t have is a face, and a face is how humans have always read intent.
I don’t mean a photorealistic avatar. Those land in the uncanny valley and cost a fortune to render. I mean an algorithmic face: pure geometry and motion, driven directly by the model’s state. When the model is uncertain, the face hesitates. When it’s confident, the face settles. When it’s working, the face thinks. The same five axes that follow your cursor today could just as easily be driven by token-level confidence, latency, or the sentiment of what’s being said.
That’s the direction. A small, fast, expressive presence that any AI product can drop in the way you’d drop in a loading spinner today, except this one actually communicates something.
Dot is the first sketch of that idea, and it’s honestly still early. The component needs work: the API surface is rough, the variants are a starting point rather than a system, and driving emotion from real model state instead of cursor position is the hard, unsolved part. But the core bet feels right. Give AI a face and the interaction stops feeling like typing into a box and starts feeling like talking to something that’s actually there.
What’s Next
The emotion engine and face system are stable. Current areas of exploration:
- Emotion inference from speech content (not just audio level), connecting to LLM sentiment analysis
- More face variants with distinct geometric personalities
- Configurable interpolation speeds per emotion axis
- A gallery of community-built variants
Links
- Live demo: dot-0.vercel.app
- Source: github.com/x0bd/proto-0
- npm package: @xoboid/avatar
- Me: xoboid.com
Dot is just a dot. But sometimes a small signal is enough to begin a conversation.