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

12 eye styles are supported: oval, round, rectangle, dot, almond, crescent, star, heart, cat, cross, diamond, semicircle.

StyleShapeBest For
ovalSmooth oval bezierDefault, Classic, Warm, Corporate
roundPerfect circleKawaii, Clay Buddy
rectangleRounded rectangleRobot, Cyberpunk
dotSmall filled circleZen, Halloween
almondTapered almondElegant, sleek faces
crescentCrescent/moon curveMystical, dreamy
starStar shapePlayful, sparkly
heartHeart shapeAffectionate, cute
catVertical slitFeline, mysterious
crossCross/plusMedical, utility
diamondDiamond/rhombusAngular, gem-like
semicircleHalf-circleSleepy, 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" } }
}
Legacy eye styles (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 rendererrendererByStaterendererByEmotion. 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:

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": {
  "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.

  1. Pick a template from the built-in face manifest
  2. Adjust controls (eyes, mouth, head, colors, body, brows, decorations, accessories, 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 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

  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.

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);
Generated packs are standard FaceDefinition objects — save as .face.json, pass to the renderer, or use with the config API on oface.io.

Resources