Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 3 min read

I gave my AI chat a face in two lines of HTML (and built the 3D engine behind it)

Every AI chat I've built ends up with the same thing on screen while the model thinks: three grey dots. I always wanted something there that actually reacts. A little character that thinks while the request is in flight,

I gave my AI chat a face in two lines of HTML (and built the 3D engine behind it)

Every AI chat I've built ends up with the same thing on screen while the model thinks: three grey dots. I always wanted something there that actually reacts. A little character that thinks while the request is in flight, talks while the reply streams, and celebrates when it's done.

Lottie can't react to live state. Rive is great, but it's a whole design tool and runtime. Three.js plus a GLB model is hundreds of KB before you've animated anything.

So I spent the weekend building my own. It's called Wobnimo.

πŸ‘‰ Play with it live, it's way more fun than reading about it.

What you get

Two lines in any HTML page. No build step, no config:

<script type="module" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/wobnimo.js"></script>

<wobnimo-mascot character="mochi" state="thinking"></wobnimo-mascot>

That's a real-time 3D character on your page, lit, blinking and breathing.

Using React and the Vercel AI SDK? It's one prop. The mascot thinks, speaks, celebrates and shows errors on its own:

import { Mascot } from 'wobnimo/react';

const { status } = useChat();

<Mascot character="mochi" status={status} />

There are ten characters (a jelly, a ghost, an octopus, a robot duck, a baby dino…) and every one of them plays ten states: idle, listening, thinking, speaking, working, celebrating, error, surprised, sad and sleeping.

One mascot is about 25 KB gzipped, with zero dependencies.

How it works: no models, just math

This is the part I had the most fun with.

There are no meshes and no textures. Every character is built from signed distance functions (SDFs): tiny formulas that tell you how far any point in space is from a shape. A sphere is one line of math. So is a capsule or a rounded cone.

The trick is that you can blend SDFs smoothly. Put a sphere on top of a squashed ellipsoid with a soft blend and you get a jelly drop, with no seam where they meet.

A character is just data. Here's a trimmed piece of Mochi:

export const mochi: CharacterSpec = {
  id: 'mochi',
  materials: {
    jelly: { color: '#3fcf9f', sss: 0.55, gloss: 0.9 },
  },
  bones: [
    { name: 'body', pivot: [0, 0, 0] },
    { name: 'sprout', parent: 'body', pivot: [0, 0.83, -0.02],
      jiggle: { axis: [0, 1, 0], length: 0.16, freq: 2.2, damping: 0.09 } },
  ],
  parts: [
    { bone: 'body', shape: 'sphere', at: [0, 0.41, 0], radius: 0.42, material: 'jelly' },
    { bone: 'body', shape: 'ellipsoid', at: [0, 0.17, 0], radii: [0.5, 0.17, 0.47],
      blend: 0.24, material: 'jelly' },
    // …
  ],
  acting: {
    // the sprout spins like a propeller while it thinks
    thinking: { sprout: { rx: -0.15, ry: [0, 0, 0, 0, 7] } },
    // and wilts when it's sad
    sad: { sprout: { rx: 1.1, rz: 0.5 } },
  },
};

From there, the pipeline is:

  1. Compile. The spec gets compiled into a single GLSL fragment shader, one per character. Parts become SDF calls, bones become transforms fed in as uniforms.
  2. Raymarch. In WebGL2, each pixel shoots a ray into the scene and steps forward by the distance the SDF reports until it hits the surface. Lighting, rim light and a fake subsurface glow (that's why the jelly looks like jelly) all happen in the same shader.
  3. Animate with springs. Nothing is a canned clip. Every bone is driven by springs, so moving between states gives you squash, overshoot and wobble for free. The sprout on Mochi's head is a jiggle bone that lags behind the body.
  4. Share one context. Browsers cap how many WebGL contexts a page can have, so all mascots on a page render through one shared context. Mascots off screen go to sleep, and prefers-reduced-motion is respected.

Because characters are data, recolouring is just swapping a value at runtime, with no rebuild:

mascot.setPalette({ jelly: '#ff5d8f' });

Your agent can set it up for you

I built this with AI agents in mind, so it should be installable by one.

On the playground, pick a character and a mood, hit Copy prompt, and paste it into Claude Code, Cursor or whatever you use. Your agent installs it and wires the mascot to your app's real events. You spend about ten seconds.

The package also ships an AGENTS.md, and the full API lives at llms.txt.

Try it

npm i wobnimo

It's MIT licensed. Next on my list are a WebGPU renderer, a Rust + WebAssembly core and a visual editor for making your own characters.

I'd genuinely love to know which character you'd put in your app, and what you'd want it to do. And if it made you smile, a star on GitHub helps a lot. πŸ™

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.