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:

SectionRequiredPurpose
metaYesName, author, license, description
geometryYesEye/mouth/brow shapes, head, body, spacing
paletteYesColors for states, emotions, features
animationNoBlink intervals, lerp speeds, breathing
personalityNo5 traits that modulate animation behavior
statesNoPer-state visual parameter overrides
emotionDeltasNoAdditive overlays for each emotion
accessoriesNoLayered props (antenna, glasses, custom)

Eyes

Four eye styles are supported:

StyleShapeBest For
ovalSmooth oval bezierDefault, Classic, Warm, Corporate
roundPerfect circleKawaii, Clay Buddy
rectangleRounded rectangleRobot, Cyberpunk
dotSmall filled circleZen, 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"
  }
}
Legacy eye styles (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 rendererrendererByStaterendererByEmotion. 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:

TraitRangeEffect
energy0-1Animation speed multiplier
expressiveness0-1Animation range/amplitude
warmth0-1Bias toward positive expressions
stability0-1Micro-expression frequency (lower = more twitchy)
playfulness0-1Sway, 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.

  1. Pick a template from the 16 built-in packs
  2. Adjust controls (eyes, mouth, head, colors, body, personality)
  3. Preview in real time with different states and emotions
  4. 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

  1. Place your .face.json in the faces/ directory
  2. Add an entry to faces/index.json
  3. Run bun run qa:faces to validate manifest parity
  4. Run bun run build to bundle
  5. Open site/test.html to see your pack across all states/emotions
  6. Open dashboard.html and select your pack from the dropdown
Use the 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:

  1. Claim a face: POST https://oface.io/api/claim with your pack name
  2. Update the config: PUT https://oface.io/{username}/api/config with your custom pack JSON
  3. Your face is live at oface.io/{username}
  4. 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