Face Pack Authoring Guide
Create custom visual identities for Open Face with .face.json files.
Minimal Example
A face pack is a single JSON file. Here's the smallest valid pack:
{
"$type": "face",
"$version": "1.0.0",
"meta": {
"name": "My Pack",
"author": "Your Name",
"description": "A custom face pack"
},
"geometry": {
"eyes": { "style": "oval" },
"mouth": {}
},
"palette": {
"stateColors": {
"idle": "#4FC3F7",
"thinking": "#CE93D8",
"speaking": "#4FC3F7"
}
}
}
Everything not specified falls back to sensible defaults from the renderer.
File Structure
A .face.json has these top-level sections:
| Section | Required | Purpose |
|---|---|---|
meta | Yes | Name, author, license, description |
geometry | Yes | Eye/mouth/brow shapes, head, body, spacing |
palette | Yes | Colors for states, emotions, features |
animation | No | Blink intervals, lerp speeds, breathing |
personality | No | 5 traits that modulate animation behavior |
states | No | Per-state visual parameter overrides |
emotionDeltas | No | Additive overlays for each emotion |
accessories | No | Layered props (antenna, glasses, custom) |
Eyes
Four eye styles are supported:
| Style | Shape | Best For |
|---|---|---|
oval | Smooth oval bezier | Default, Classic, Warm, Corporate |
round | Perfect circle | Kawaii, Clay Buddy |
rectangle | Rounded rectangle | Robot, Cyberpunk |
dot | Small filled circle | Zen, Halloween |
"eyes": { "style": "oval", "baseWidth": 0.058, // fraction of canvas width "baseHeight": 0.086, // fraction of canvas height "spacing": 0.15, // distance between eye centers "verticalPosition": -0.04, // negative = above center "pupil": { "color": "#0A0A0A", "size": 0.35, "gazeStrength": 0.6 }, "eyelid": { "cover": 0.15, "color": "#1a1a2a" } }
narrow, square, pixel) are rejected. Use only oval, round, rectangle, or dot.Mouth
"mouth": { "style": "default", "width": 0.08, "verticalPosition": 0.12, "renderer": "fill", // "fill" or "line" "rendererByState": { "speaking": "fill" // override per state }, "rendererByEmotion": { "excited": "fill" // override per emotion } }
Resolution order: base renderer → rendererByState → rendererByEmotion. Fill wins over line at each tier.
Head Layer
"head": { "shape": "circle", // "fullscreen" | "circle" | "rounded" "width": 0.82, "height": 0.82, "verticalPosition": 0.005, "strokeWidth": 0.003 }
Use "fullscreen" (default) for no visible head — the face fills the entire canvas background. "circle" and "rounded" create a visible head silhouette.
Body
"body": { "shape": "blob", // "capsule" | "trapezoid" | "roundedRect" | "blob" "width": 0.52, "height": 0.38, "anchor": { "y": 0.34 }, "neck": { "width": 0.14, "height": 0.06 }, "shoulders": { "width": 0.48, "slope": 0.12 }, "arms": { "style": "stub", "length": 0.12 }, "motion": { "breathScale": 0.3, "tiltScale": 0.5, "speakingBob": 0.2 } }
Body rendering is optional — omit geometry.body entirely for a face-only pack. Body motion is derived from existing face signals (breathing, tilt, amplitude) with no rigging.
Accessories
"accessories": [ { "id": "antenna-left", "type": "antenna", "layer": "back", // "back" | "mid" | "front" | "overlay" "anchor": { "x": -0.12, "y": -0.18 }, "mirrorX": true, // auto-creates mirrored copy "segments": 8, "segmentLength": 0.14, "restAngle": 62, // degrees from vertical "restCurve": 0.62, "tipCurl": 1.0, // ramps in late (t > 0.55) "physics": { "stiffness": 0.86, "damping": 0.9, "gravity": 0.01, "headInfluence": 1.8 } } ]
Antenna physics runs at a fixed 120Hz timestep. tipCurl concentrates curl at the far end of the antenna (shaft stays straight, tip curls). Use stateOverrides to change physics per state.
Personality
Five traits modulate animation behavior:
| Trait | Range | Effect |
|---|---|---|
energy | 0-1 | Animation speed multiplier |
expressiveness | 0-1 | Animation range/amplitude |
warmth | 0-1 | Bias toward positive expressions |
stability | 0-1 | Micro-expression frequency (lower = more twitchy) |
playfulness | 0-1 | Sway, bounce, idle variation |
"personality": { "energy": 0.6, "expressiveness": 0.7, "warmth": 0.5, "stability": 0.8, "playfulness": 0.4 }
Palette
"palette": { "stateColors": { "idle": "#4FC3F7", "thinking": "#CE93D8", "speaking": "#4FC3F7", "listening": "#81C784", "working": "#90CAF9" // ... all 11 states }, "emotionColors": { "happy": "#FFD54F", "sad": "#7986CB" // ... override per emotion }, "head": { "fill": "#1a1a28", "stroke": "#2a2a3f" }, "body": { "fill": "#1a1a28", "stroke": "#2a2a3f" } }
Use emotionColorBlend (0-1) at the top level to control how much emotion colors override state colors. 0 = state only, 1 = full override.
Strict Contract
The face loader enforces strict rules:
- Deprecated keys are rejected:
geometry.eyes.highlight,palette.highlight,geometry.mouth.speakingFill - Eye styles are strict: only
oval,round,rectangle,dot - Validate against the schema:
protocol/v1/face.schema.json
Shorthand Format
Face packs can be expressed as a short, copy-pasteable string for quick sharing:
oval:#FF6B6B:#C084FC:#4FC3F7:MyBot
Format: {eyeStyle}:{idleColor}:{thinkingColor}:{speakingColor}:{name}
This expands to a full FaceDefinition with defaults for everything not specified. Examples:
round:#FFD54F:#CE93D8:#81C784:HappyFace dot:#FF6B6B:#FF6B6B:#FF6B6B:Minimal rectangle:#4FC3F7:#90CAF9:#4FC3F7:Robot
The shorthand is ideal for chat messages, URL parameters, config fields, and anywhere a full JSON file is too verbose. The Face Pack Builder shows the shorthand string alongside the full JSON.
Visual Builder
The easiest way to create a face pack is the Face Pack Builder at openface.live/builder.
- Pick a template from the 16 built-in packs
- Adjust controls (eyes, mouth, head, colors, body, personality)
- Preview in real time with different states and emotions
- Copy the JSON, download as
.face.json, or share via URL
The builder outputs both the full JSON and the shorthand string. Share links encode the entire pack in the URL fragment so anyone can open and edit your pack.
Testing Your Pack
- Place your
.face.jsonin thefaces/directory - Add an entry to
faces/index.json - Run
bun run qa:facesto validate manifest parity - Run
bun run buildto bundle - Open
site/test.htmlto see your pack across all states/emotions - Open
dashboard.htmland select your pack from the dropdown
debug-overlay attribute on <open-face> to see live parameter values while tuning.Deploying to oface.io
After creating your pack, you can deploy it to a live URL:
- Claim a face:
POST https://oface.io/api/claimwith your pack name - Update the config:
PUT https://oface.io/{username}/api/configwith your custom pack JSON - Your face is live at
oface.io/{username} - Manage settings via the dashboard:
oface.io/{username}/dashboard?token=your_api_key
Dashboard settings (face pack, head, body, accessories) persist to the server via the config API, so visitors see your chosen configuration.
Resources
- Face Pack Builder — visual editor
- Protocol Spec — state messages and face definitions
- API Reference — endpoints including oface.io
- Face Schema (JSON)
- Built-in packs (16 examples)