# Face actuation

The rig behind the animatronic face: ten servo channels, nine blendable expressions, per-servo stroke against travel, and the ellipsoid maths that puts a feature on a skull.

> 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/face-actuation.json
```

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

## Notes

- A channel is a servo. Ten of them: browInner, browOuter, lidUpper, lidLower, cheek and lipCorner on each side, plus noseWrinkle, lipPress, lipPucker and jaw on the centreline.
- lidUpper is the one lid channel that runs both ways — a lid retracts past open, which is what makes surprise and fear read as wide-eyed rather than merely un-blinked.
- Blink takes the larger of itself and the expression's own lid rather than summing, because a lid cannot close twice; speech takes the larger jaw and slackens the lips, because a pressed mouth is not also speaking.
- Out-of-travel channels still return complete values with withinLimits false, so a UI draws the fault instead of handling an exception.

## Usage

```tsx
import { blendFace, faceShape, solveFace, onFace, ellipsoidOutline } from "@/lib/robocn/face"

const solution = solveFace({ expression: "doubt", intensity: 0.7, speech: 0.4 })
solution.left.browOuter   // the left brow's servo
solution.actuators        // id, value, stroke, travel, withinLimits
solution.withinLimits     // false when any servo ran out of stroke

// Expressions mix channel by channel, which is what an ease between them is.
const halfway = blendFace(faceShape("neutral"), faceShape("joy"), 0.5)
```

## API

| name | type | default | description |
| --- | --- | --- | --- |
| `solveFace` | `(input?: FaceInput, geometry?: HeadGeometry) => FaceSolution` | — | Expression scaled by intensity, then blink and speech added on top, then explicit channels last. Every channel is clamped, and each one reports its servo stroke. |
| `faceShape` | `(expression: FaceExpression) => FaceChannels` | — | The full-intensity channel vector for one of the nine expressions. Six channels are paired, so doubt can raise one brow and level the other. |
| `blendFace` | `(a: FaceChannels, b: FaceChannels, t: number) => FaceChannels` | — | Channel-by-channel mix. An ease between expressions is this, not a cross-fade of two drawings. |
| `onFace` | `(x: number, y: number, radii: Vec3, outset?: number) => Vec3` | — | Solves the ellipsoid for z, so a feature placed on the front elevation lands on the skull. Outside the silhouette it lands on the equator rather than returning NaN. |
| `ellipsoidOutline` | `(radii: Vec3, pose: HeadPose, camera: RobotCamera, center?: Vec3) => EllipseOutline` | — | The exact silhouette of an ellipsoid under an orthographic camera: cx, cy, the two semi-axes and the tilt of the major one. |
| `rotateHead` | `(point: Vec3, pose: HeadPose) => Vec3` | — | Neck rotation in degrees, applied roll, then pitch, then yaw. |
| `defaultHeadGeometry` | `HeadGeometry` | — | Skull half-axes, servo gain in world units per unit of channel, and the stroke the servos have. |

## Source

- `src/lib/robocn/face.ts`
