Your illustration, in motion

Mesh Avatar
Studio

Turn one illustration into an animated avatar.

Create and adjust it in Studio, then bring it to your app with the mesh-avatar runtime.

A local editor. An avatar you can embed.

Live preview Miko

Enable JavaScript to try the interactive preview.

Avatar controls

Move your pointer and she turns to face it.

Try talking — this demo has no audio.

Running here with mesh-avatar.

What it does

Blink, speak, and follow a pose.

Build the avatar in Studio. Use the same motion and expressions in your own app with the runtime.

Blink and breathe

Automatic blinking, breathing, and idle motion keep the avatar moving between interactions.

Lip sync

Drive the mouth from voice levels, or preview vowel shapes and kana text in the editor.

Head turns and tilts

Control the head and gaze with pose parameters. Review the movement in the editor’s live preview.

Hair and accessory sway

Physics adds follow-through to hair strands and pendulum movement to accessories such as tassels.

Expressions and drawings

Switch smiles, surprise, and winks. Add drawn eyes and mouths for blinking and speech.

Lighting presets

Position a light over the avatar, adjust its shading, and save setups to switch between in the Live page.

Make it in Studio

From your illustration to an avatar file.

A coding agent prepares the rig and layers. You review the result and adjust the parts in a local editor.

  1. Give your agent an illustration

    Open Claude Code or Codex in the repository and ask it to follow the agent guide. A front-facing PNG with a transparent background works best.

    The Studio editor with the Miko illustration, rig handles, and an animated preview.
    Open the project your agent prepares.
  2. Review the motion

    Open the project in Studio. Use Pose test to turn the head and close the eyes, then Lip sync to check the mouth shapes.

    The editor’s Lip sync panel with vowel buttons and the avatar’s mouth open in the preview.
    Check poses and speech before exporting.
  3. Adjust by dragging handles

    Select a part and move its dots on the image. The preview follows your edits. If an outline changes the cut-out layers, choose Save and rebuild layers.

    A zoomed eye outline with editable points selected on the illustration.
    Place the rig where it belongs on your drawing.
  4. Add drawn variants for richer expressions

    Request drawn eyes and mouths from Drawn variants to give blinks, lip sync, and smiles more character. When it looks right, choose Export .mavatar to pack the rig, built layers, and drawings into one file.

    The Drawn variants panel with a request for mouth drawings ready to copy to a coding agent.
    Add drawn variants before exporting the avatar.

Start locally with npm install and npm run dev. Follow the setup guide

Use it live

Move with your face. Bring it to your stream.

Studio’s Live page sends your avatar’s motion to a separate stream view that you can add to OBS.

Track your face or use your voice

Start the camera and calibrate, then adjust mirroring, sensitivity, and smoothing. The camera is optional: microphone lip sync can drive the mouth while idle motion continues.

The Live page with face tracking, microphone, expression, background, and lighting controls beside Miko.
Camera video and microphone audio stay on your machine.

Add an OBS Browser Source

Choose a background, copy the OBS URL, and add it as a Browser Source. Use transparency in OBS or a green background for chroma key in other capture software.

The stream view showing Miko against a solid green chroma key background without editor controls.
A separate view for capture, with only the avatar.

Both views use the local development server. Keep the Live page open in its own window while streaming. Streaming guide

Embed with mesh-avatar

An avatar in your app’s canvas.

Load the file you exported from Studio. The runtime starts animating it, and your app controls its pose, expressions, and voice level.

  • WebGL
  • ES modules
  • TypeScript types
  • No React dependency

Install the runtime

npm install mesh-avatar

Place your .mavatar file in your app’s public assets. Add the canvas to your HTML and run the JavaScript from your app’s module entry.

Idle motion and blinking start automatically. For speech, your app plays the audio and sends its loudness to the runtime.

Runtime API and examples

Add a canvas HTML

<canvas id="avatar" width="720" height="960"></canvas>

Load and control it JavaScript

import { loadMeshAvatar } from 'mesh-avatar';

const canvas = document.querySelector('#avatar');
const avatar = await loadMeshAvatar(
  canvas, '/avatars/character.mavatar'
);

// Pick an expression or set a pose.
avatar.setExpression('smile');
avatar.setParameters({ angleX: 10, gazeX: 0.25 });

// While your app plays audio, send its loudness (0–1).
avatar.setSpeaking(true);
avatar.setVoiceLevel(0.6);

// When the line finishes, return to idle.
avatar.setVoiceLevel(0);
avatar.setSpeaking(false);
avatar.setExpression('neutral');
avatar.setParameters({});

// Call avatar.destroy() when removing the canvas.

How this preview works JavaScript

import { loadMeshAvatar } from 'mesh-avatar';

const canvas = document.querySelector('#avatar');
const avatar = await loadMeshAvatar(canvas, 'miko.mavatar', { padTop: -0.04 });

// Expression buttons switch the face.
for (const button of document.querySelectorAll('[data-expression]')) {
  button.addEventListener('click', () => {
    avatar.setExpression(button.dataset.expression);
  });
}

// Talk toggles speaking; this demo sends a silent loudness curve instead of audio.
let speaking = false;
document.querySelector('#talk').addEventListener('click', () => {
  speaking = !speaking;
  avatar.setSpeaking(speaking);
  if (!speaking) avatar.setVoiceLevel(0);
});

// Turn the head and eyes toward the pointer (x and y run from -1 to 1).
const hero = document.querySelector('.hero');
hero.addEventListener('pointermove', (event) => {
  const rect = hero.getBoundingClientRect();
  const x = (event.clientX - rect.left) / rect.width * 2 - 1;
  const y = 1 - (event.clientY - rect.top) / rect.height * 2;
  avatar.setParameters({ angleX: x * 12, angleY: y * 8, gazeX: x * 0.7, gazeY: y * 0.55 });
});
hero.addEventListener('pointerleave', () => avatar.setParameters({}));

// While talking, send a loudness value (0–1) every frame.
function frame(time) {
  if (speaking) avatar.setVoiceLevel(Math.abs(Math.sin(time / 90)));
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

Simplified from this page’s script. Full source on GitHub