@chatwoot/captain-avatar

中文文档
0.2.0发布于 2026/11/03MITTypeScriptES Module零依赖近 30 天下载 4425

An animated 3D avatar of Chatwoot's Captain, with an LED-matrix face that shows what the agent is doing. WebGL2, no runtime dependencies. A plain TypeScript API and a Vue component.

原始使用文档
文档目录17 节
  1. Install
  2. Use it
  3. Plain TypeScript
  4. Vue
  5. States
  6. Showing that the user is typing
  7. Options
  8. Themes
  9. Entrance
  10. Events
  11. Browser support
  12. Performance
  13. Load it lazily
  14. Custom states
  15. Development
  16. License
  17. Credits

@chatwoot/captain-avatar

An animated 3D avatar of Chatwoot's Captain. Its LED-matrix face and the light on its headset mic show what the agent is doing: thinking, searching, listening, speaking and more.

It renders with WebGL2 and has no runtime dependencies. You can use it from plain TypeScript or as a Vue 3 component.

Install

npm install @chatwoot/captain-avatar

Vue is an optional peer dependency. You need Vue 3.3 or later only if you use the component.

Use it

Plain TypeScript

import { createCaptainAvatar, thinking, speaking } from '@chatwoot/captain-avatar';

const avatar = createCaptainAvatar(document.querySelector('#captain')!, {
  states: [thinking, speaking],
  state: 'thinking',
  size: 96,
});

avatar.update({ state: 'speaking' });
avatar.destroy();

createCaptainAvatar turns the element into a size × size box and draws the avatar inside it. Hops, the mic's glow and the floor shadow draw past the box edges without taking part in layout. If a parent has overflow: hidden, it clips them.

Vue

<script setup>
import { ref } from 'vue';
import { CaptainAvatar } from '@chatwoot/captain-avatar/vue';
import { thinking, speaking } from '@chatwoot/captain-avatar';

const states = [thinking, speaking];
const status = ref('thinking');
const onReady = () => console.log('Captain is on screen');
</script>

<template>
  <CaptainAvatar :states="states" :state="status" :size="96" @ready="onReady" />
</template>

The component takes the same options as props. It emits the events listed below as ready, unsupported, enter-start, enter-end, state-change and poke. Through a template ref, it exposes poke() and enter().

States

Pass the states you use. idle is always included. Importing only what you use keeps the rest out of your bundle. To include everything, pass allStates.

StateFaceMotionMic light
idlePill eyes that blink and glance, as in the logoLooks around, hops now and thenSoft white
thinkingEyes with a scan line sweeping down through them in time with the mic, drifting in a slow circle. Now and then one squints or both blink slowlyHead tilted up, switching sidesAmber pulse
searchingDarting eyes, a beam sweeping the screenHead sweeps side to sideCyan flash
listeningAlert eyes over a live equaliserHead cocked, small nodsSteady green
typingEyes that read along as the user types, and a blinking caretHead cocked, a small nod per keystrokeViolet, flashes per keystroke
speakingA mouth that moves with the wordsBobs as it talksFlickers with speech
workingEyes looking down, a progress barBusy bobbing, a hop with a spin now and thenBlue blink
happyArched eyes and a smileBouncy hopsPink
errorRed X eyes and a zigzag mouthShakes its headRed blink
sleepingClosed eyes and rising z'sHead drooped, slow breathingOff

If you pass happy, a click also shows the happy face for a moment.

Showing that the user is typing

Use typing while the user types, and call avatar.keyStroke() from the input's input event:

const avatar = createCaptainAvatar(el, { states: [typing], state: 'typing' });
input.addEventListener('input', () => avatar.keyStroke());

Each call moves the eyes' gaze on, flashes the mic light and nods the head. After 8 keystrokes the gaze sweeps back to the start, like a wrapped line. After about 2 seconds without a keystroke, the eyes return to the centre. In Vue, call keyStroke() on the component's template ref.

Options

OptionDefaultWhat it does
state'idle'The name of the state to show. An unknown name falls back to idle.
statesnoneThe states the avatar can show, in addition to idle.
size160The box the avatar rests in, in px.
themeclassicThe colour set.
speed1Multiplier on every animation's speed.
pausedfalseHolds the state's still pose.
interactivetrueFollows the pointer, and hops and spins on a click.
floorShadowtrueDraws the soft shadow under the head.
seedrandomA number from 0 to 1 that offsets the blinks and glances.
entrancefalsePlays the entrance once the avatar is ready.
orbitfalseLets you drag the head round to look at it from any side. It stays where you leave it, and a click turns it back. On touch screens, the avatar takes over touch gestures.
labelCaptain, <state>The accessible label.

update(options) changes any of these. A key you leave out keeps its value. A key set to undefined goes back to its default.

Below 64 px, the avatar draws low detail on its own. It drops the grain and the LED grid, and each LED fills its cell so the face stays readable.

Themes

The four themes are classic, midnight, snow and grape. Each is an exported object:

import { createCaptainAvatar, midnight } from '@chatwoot/captain-avatar';

createCaptainAvatar(el, { theme: midnight });

To make your own, pass an object with name, color (the shell), trimColor (the ear cups), headsetColor (the band, cushions and boom) and screenColor (the LEDs). Each colour is a #rgb, #rrggbb or rgb() string.

Entrance

With entrance: true, or when you call avatar.enter(), Captain puts on the headset. The head pops in with its screen off. Then the headset drops on, the cups close and the mic boom swings down. The mic flickers on, then the screen switches on. If you passed happy, the screen opens on the happy face. It takes about 1.3 seconds.

Events

avatar.on(event, listener) returns a function that stops listening.

EventFires whenDetail
readyThe shaders are compiled and the first frame is on screen.none
unsupportedThe graphics context was lost and didn't come back within 5 seconds. The avatar stops rendering and releases the element.none
enterstartThe entrance starts.none
enterendThe entrance finishes. If the avatar is still, it fires straight after enterstart.none
statechangeupdate changed the state.{ from, to }
pokeThe avatar was clicked while interactive.none

Browser support

The avatar needs WebGL2 running on real graphics hardware. It asks for its context with failIfMajorPerformanceCaveat: true, so a browser that would render WebGL on the CPU counts as unsupported.

Where it isn't supported, createCaptainAvatar leaves the element untouched and renders nothing. The handle it returns has supported: false, and its methods do nothing. The Vue component renders nothing and emits unsupported.

To avoid downloading the avatar where it can't render, run the same check in your app before you load it:

function supportsCaptainAvatar() {
  try {
    const gl = document.createElement('canvas').getContext('webgl2', {
      failIfMajorPerformanceCaveat: true,
    });
    gl?.getExtension('WEBGL_lose_context')?.loseContext();
    return !!gl;
  } catch {
    return false;
  }
}

Performance

Each frame, the CPU runs the motion, writes under a kilobyte of LED values and makes up to three draw calls. The GPU draws everything else from shaders.

The frame rate follows what the avatar is doing:

  • While something moves: a hop, the entrance, a face change, the pointer or an active state runs at the display's full rate.
  • Calm idle: capped at 60 fps, so a still avatar doesn't keep a 120 Hz display busy.
  • Calm sleeping: capped at 30 fps.
  • Off screen, paused or in a hidden tab: nothing renders.

With prefers-reduced-motion: reduce, the avatar draws the state's still pose and doesn't animate.

Load it lazily

Bundlers split a dynamic import() into its own chunk. Load the avatar that way so it stays out of your main bundle:

import { defineAsyncComponent } from 'vue';

const CaptainAvatar = defineAsyncComponent(() =>
  import('@chatwoot/captain-avatar/vue').then((m) => m.CaptainAvatar),
);

Import everything else from the package, such as states and themes, inside the same lazily loaded code. A normal import anywhere else pulls the shared code back into the main bundle.

Custom states

A state is a plain object, so you can write your own. It has:

  • name: the name you pass as state
  • face(grid, params): draws the face on a 24 × 18 LED area each frame
  • light: the LED colour (screen), the mic colour (mic) and the mic brightness over time (pulse)
  • motion(context): optional, sets the head's target pose each step
  • rest: optional, the still pose for reduced motion and paused
  • followsPointer: optional, turns the head toward the pointer
  • calmFps: optional, caps the frame rate while the state is calm

The package exports the drawing helpers the built-in faces use, including eyes, pill, rect, sprite and put. The files in src/states/ are working examples.

Development

bun install
bun run dev        # the playground
bun run build      # dist/, with the shaders minified
bun run typecheck

The playground has live mini avatars for every state, theme, size and speed controls, an event log, and a debug panel with frame stats and a pose hold.

License

MIT. See LICENSE.

Credits

The head's design, lighting model, motion and LED faces started as a port of robot-heads by Fayaz Ahmed, under the MIT License. Its license is in LICENSE.robot-heads.

版本与发布记录

@chatwoot/captain-avatar 共发布 3 个版本,首次发布于 2026/11/03;过去 12 个月发布了 3 个版本。

版本发布日期距今
0.2.0latest2026/11/036 天前
0.1.02026/11/036 天前
0.0.0-stage2026/11/036 天前

关于 @chatwoot/captain-avatar 的常见问题

@chatwoot/captain-avatar 是什么?

An animated 3D avatar of Chatwoot's Captain, with an LED-matrix face that shows what the agent is doing. WebGL2, no runtime dependencies. A plain TypeScript API and a Vue component.

如何安装 @chatwoot/captain-avatar?

在项目目录运行 npm install @chatwoot/captain-avatar,或使用 pnpm add / yarn add / bun add。

@chatwoot/captain-avatar 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@chatwoot/captain-avatar 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@chatwoot/captain-avatar 可以商用吗?

@chatwoot/captain-avatar 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@chatwoot/captain-avatar 的下载量有多少?

根据 npm 官方下载统计,@chatwoot/captain-avatar 在 2026/10/10 – 2026/11/08 这 30 天内被下载 4,425 次,日均约 148 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@chatwoot/captain-avatar 还在维护吗?

最新版本 0.2.0 发布于 2026/11/03(6 天前);至今共发布 3 个版本,其中过去 12 个月发布了 3 个;首次发布于 2026/11/03。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。