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
12 eye styles are supported: oval, round, rectangle, dot, almond, crescent, star, heart, cat, cross, diamond, semicircle.
| 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 |
almond | Tapered almond | Elegant, sleek faces |
crescent | Crescent/moon curve | Mystical, dreamy |
star | Star shape | Playful, sparkly |
heart | Heart shape | Affectionate, cute |
cat | Vertical slit | Feline, mysterious |
cross | Cross/plus | Medical, utility |
diamond | Diamond/rhombus | Angular, gem-like |
semicircle | Half-circle | Sleepy, relaxed |
"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": { "shape": "circle", // circle, slit, star, heart, diamond, cross, ring, flower, spiral, none "color": "#0A0A0A", "size": 0.35, "gazeStrength": 0.6 }, "specular": { "shape": "circle", // circle, star, crescent, dual, line, cross, ring, none "size": 0.25, "lookFollow": 0.3 }, "eyelash": { "style": "none" // none, simple, thick, wing, bottom, full, spider }, "eyelid": { "cover": 0.15, "color": "#1a1a2a" } }
Per-Eye Overrides (Heterochromia)
Override style, pupil, or color independently per eye:
"eyes": { "style": "oval", "left": { "style": "star", "pupil": { "shape": "heart" } }, "right": { "style": "crescent", "pupil": { "shape": "slit" } } }
narrow, square, pixel) are rejected. The renderer supports all 12 styles listed above.Mouth
10 mouth shapes: curve, cat, slit, zigzag, pixel, circle, fang, smirk, wave, none.
"mouth": { "shape": "curve", // curve, cat, slit, zigzag, pixel, circle, fang, smirk, wave, none "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.
Nose
6 nose styles: none (default), dot, line, triangle, L, button.
"nose": { "style": "dot", "size": 0.02, "verticalPosition": 0.04 }
Head Layer
12 head shapes: fullscreen, circle, rounded, oval, squircle, hexagon, diamond, egg, pill, shield, cloud, octagon.
"head": { "shape": "circle", // any of the 12 head shapes "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. All other shapes 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.
Decorations
10 face decoration types: freckles, tears, sweat, scar, stripes, sparkles, bandaid, hearts, stars, lines.
"decorations": [ { "type": "freckles", "opacity": 0.6 }, { "type": "sparkles", "opacity": 0.8 } ]
Decorations are layered on top of the face and can be combined freely.
Accessories
"accessories": [ { "id": "antenna-left", "type": "antenna", "layer": "back", // "back" | "mid" | "front" | "overlay" "anchor": { "x": -0.12, "y": -0.18 }, "symmetry": "mirrorX", // 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": { "states": { "idle": "#4FC3F7", "thinking": "#CE93D8", "speaking": "#4FC3F7", "listening": "#81C784", "working": "#90CAF9" // ... all 11 states }, "emotions": { "happy": "#FFD54F", "sad": "#7986CB" // ... override per emotion }, "head": { "fill": "#1a1a28", "stroke": "#2a2a3f" }, "body": { "fill": "#1a1a28", "stroke": "#2a2a3f" }, "emotionColorBlend": 0.5 }
Use palette.emotionColorBlend (0-1) to control how much emotion colors override state colors. 0 = state only, 1 = full override.
Dual-Color System
Each visual feature supports independent fill and stroke colors:
"palette": { "eyes": { "fill": "#4FC3F7", "stroke": "#2196F3" }, "mouth": { "fill": "#FF8A65", "stroke": "#E64A19" }, "brows": { "fill": "#90A4AE", "stroke": "#607D8B" }, "nose": { "fill": "#B0BEC5", "stroke": "#78909C" } }
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 the 12 supported styles (oval, round, rectangle, dot, almond, crescent, star, heart, cat, cross, diamond, semicircle)
- 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 built-in face manifest
- Adjust controls (eyes, mouth, head, colors, body, brows, decorations, accessories, 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 use compressed #packz= URL fragments.
Publishing to the Gallery
Click the Publish button in the builder to submit your pack to the community gallery. A modal prompts for a name, description, and tags. If you're logged in with GitHub, your verified username is attached as the author. Submissions are rate-limited to 10 per hour.
Published packs appear on the gallery page and can be loaded by other users via ?gallery={id} or ?community={id} query params in the builder URL.
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.
Programmatic Generation
Instead of hand-authoring JSON, you can generate face packs programmatically using the renderer's built-in generator:
import { generateFromArchetype, generateFromDescription, generateFromPersonality, interpolatePacks, ARCHETYPES } from "@openface/renderer";
From an Archetype
7 built-in archetype templates (friendly, serious, cute, edgy, minimal, retro, organic). Each produces a complete pack with coherent geometry, palette, and personality:
// Pick an archetype, optionally add variation (0-1) const cute = ARCHETYPES.find(a => a.name === "cute"); const pack = generateFromArchetype(cute, 0.3);
From a Description
Pass a name and natural language description. The generator maps keywords to personality traits and selects an appropriate archetype:
const pack = generateFromDescription( "Pixel", "A calm, minimal assistant with a retro terminal aesthetic" );
From Personality Traits
Specify the 5 personality dimensions directly:
const pack = generateFromPersonality({ energy: 0.8, expressiveness: 0.9, warmth: 0.7, stability: 0.4, playfulness: 0.85 }, "Bouncy");
Pack Interpolation
Blend two existing packs together to create hybrids:
const hybrid = interpolatePacks(packA, packB, 0.5);
FaceDefinition objects — save as .face.json, pass to the renderer, or use with the config API on oface.io.Resources
- Face Pack Builder — visual editor with publish-to-gallery
- Community Gallery — browse and submit face packs
- Protocol Spec — state messages and face definitions
- API Reference — endpoints including oface.io
- Face Schema (JSON)
- Built-in packs (16 examples)