# Animatronic face

An expressive humanoid head where every feature is a servo: paired brows, lids, cheeks and lip corners, a hinged jaw, and nine expressions that blend rather than swap.

> For the complete index, see [llms.txt](https://robocn.dev/llms.txt). A Markdown version of any page is available by appending `.md` to its URL or by sending an `Accept: text/markdown` header.

## Install

```bash
bunx --bun shadcn@latest add https://robocn.dev/r/animatronic-face.json
```

Registry item: `animatronic-face` · [`https://robocn.dev/r/animatronic-face.json`](https://robocn.dev/r/animatronic-face.json)

## Notes

- Nothing in the drawing branches on an expression name. Every expression resolves to the same ten-channel vector — six of them paired left and right — and the face reads channels, which is why intensity, blink and speech compose instead of one winning.
- The skull is an ellipsoid and its silhouette is projected exactly: composing the camera, the neck rotation and the radii gives a 2×3 matrix whose shape matrix eigen-decomposes into one ellipse. Four cameras, no per-angle artwork.
- Features are curves drawn in the face's own chart and pushed onto that surface, so the brow wraps the temple and the far eye turns away by itself. A patch whose normal points away from the camera fades out — which is why the face is gone in plan view, looking at the crown.
- The jaw is a hinge on a real axis through the ear servos, and the lower lip rides the jaw plate, so the mouth opens because the mechanism moved rather than because a second mouth was drawn.
- showActuators draws one rod per servo and paints it in the accent colour when it runs out of stroke. The component clamps its own inputs, so the only way to see a fault is to tighten geometry.travel.
- Gaze is illustrated, not solved — the eyes are discs on the surface, not a solved eyeball in a socket. Everything else the rig reports is a channel value the drawing is bound to.

## Usage

```tsx
import { AnimatronicFace } from "@/components/ui/animatronic-face"

<AnimatronicFace behavior="converse" />

// An expression is a blend, so intensity is a real dial, not a fade.
<AnimatronicFace expression="doubt" intensity={0.6} showActuators />

// Or drive a servo yourself; it wins over the expression.
<AnimatronicFace expression="joy" channels={{ jaw: 0.4, left: { browOuter: -0.8 } }} />
```

## Props

| name | type | default | description |
| --- | --- | --- | --- |
| `view` | `"plan" | "front" | "profile" | "iso"` | `"front"` | Where the camera stands. One head, four projections: straight down, straight on, side elevation, or three-quarter from above. |
| `expression` | `"neutral" | "joy" | "surprise" | "sorrow" | "anger" | "fear" | "disgust" | "doubt" | "sleep"` | — | Which expression the rig drives toward. Omit and the behavior picks one. |
| `intensity` | `number` | `1` | How far it drives there, clamped to 0–1. The whole channel vector scales, so half a smile is a different face rather than a faded one. |
| `behavior` | `"idle" | "converse" | "listen" | "emote" | "static"` | `"idle"` | What the head does with anything you have not supplied: breathe and glance about, talk, attend to you, or walk the whole expression set. |
| `speed` | `number` | `0.3` | Cycles per second. |
| `animate` | `boolean` | `true` | Off parks the machine at phase and stops rendering. A reduced-motion preference does the same. |
| `paused` | `boolean` | `false` | Freeze where it stands. |
| `phase` | `number` | `0` | Seconds of offset, so a row of machines breaks step. |
| `blink` | `number` | — | Lid closure over the expression, clamped to 0–1. Omit and it blinks on an irregular cycle of its own. |
| `speech` | `number` | — | Speech level, clamped to 0–1: opens the jaw and slackens the lips on top of whatever the face is holding. |
| `yaw / pitch / roll` | `number` | — | Neck angles in degrees, clamped to ±34, ±28, ±26. Omit and the head turns toward the pointer. |
| `look` | `Vec2 | null` | `null` | Pupil aim in −1..1 on both axes. Set it to drive the gaze; leave it null to track the pointer. |
| `track` | `boolean` | `true` | Follow the pointer anywhere on the page while look is null. |
| `interactive` | `boolean` | `true` | Turn the head toward the pointer, and react when clicked — a start, a blink, and a warming toward pleased. |
| `onReact` | `() => void` | — | Fired on the click that starts a reaction. |
| `channels` | `Partial<FaceChannels>` | — | Drive individual servos: jaw, lipPress, lipPucker, noseWrinkle, and a left / right object each carrying browInner, browOuter, lidUpper, lidLower, cheek and lipCorner. These win over the expression. |
| `showActuators` | `boolean` | `false` | Draw the sixteen push-rods from the frame ring to the parts they drive. |
| `showNeck` | `boolean` | `true` | Neck column and shoulder plate under the head. |
| `showGround` | `boolean` | `true` | Contact shadow. |
| `geometry` | `Partial<HeadGeometry>` | — | Override the skull half-axes, the servo gain, or the stroke the servos have. |
| `label` | `string` | — | Caption under the head. |
| `variant` | `"solid" | "outline" | "blueprint" | "wire"` | `"solid"` | How the machine is painted. Geometry never changes between variants. |
| `size` | `"xs" | "sm" | "md" | "lg" | "xl" | number` | `"md"` | Rendered width in pixels, or a step on the scale. |
| `color` | `string` | `var(--robot-shell)` | Body panels — the colour the machine reads as. |
| `accent` | `string` | `var(--robot-accent)` | Status colour: tip light, live tool, readouts. |
| `metal` | `string` | `var(--robot-metal)` | Bare machined parts: collars, bolts, tool bodies. |
| `dark` | `string` | `var(--robot-dark)` | Cast joints, base, shadow side. |
| `palette` | `Partial<RobotPalette>` | — | Override any subset of roles at once, including glow and grid. |

## Source

- `src/components/ui/animatronic-face.tsx`
