FX Spec
The design file format — one JSON file that describes a visual, its lifecycle states, bindings and performance rules, and renders identically on every platform.
An FX Spec is a small JSON file that describes a visual completely: the object and pattern, colour, materials, lifecycle states, data bindings and low-power rules. Every platform resolves it with the same engine, so one file renders the same on the web, iOS, Android and React Native. Designers can hand it over, and apps can ship it, load it from a server, or build it in code.
{
"fxSpec": "1.8",
"name": "Voice orb",
"description": "One shape for the whole conversation: the pattern stays `glowing` and the agent's state changes how it behaves -- speed, density, glow, `ink` and which voice drives it.",
"object": "orb",
"pattern": "glowing",
"size": 64,
"ink": 1,
"color": {
"value": "#6E56CF",
"mix": 0.92
},
"params": {
"hueSpread": 30,
"hueSpeed": 0,
"surfaceScale": 1.2,
"surfaceSpeed": 0.1,
"nodeCount": 220,
"nodeSize": 1.15,
"saturation": 0.5
},
"materials": {
"glow": {
"strength": 0.25,
"radius": 3
}
},
"bindings": {
"muted": {
"input": "micMuted"
}
},
"states": {
"initializing": {
"speed": 0.8,
"params": {
"nodeCount": 160,
"nodeSize": 1,
"surfaceSpeed": 0.6,
"saturation": 0.4
},
"ink": 0.7
},
"idle": {
"speed": 0.6,
"materials": {
"pulse": {
"strength": 0.6,
"period": 4.5,
"scale": 0.04,
"opacity": 0.25
}
},
"ink": 0.8
},
"listening": {
"speed": 0.9,
"params": {
"nodeCount": 260,
"nodeSize": 1.3,
"surfaceSpeed": 0.28,
"saturation": 0.6
},
"materials": {
"glow": {
"strength": 0.3
}
},
"bindings": {
"audioLevel": {
"input": "micLevel",
"curve": "easeOut"
}
}
},
"thinking": {
"speed": 1.05,
"params": {
"nodeCount": 340,
"nodeSize": 1.05,
"surfaceScale": 3,
"surfaceSpeed": 0.55,
"saturation": 0.8,
"hueSpeed": 0
},
"materials": {
"glow": {
"strength": 0.28
},
"pulse": {
"strength": 0.6,
"period": 1.8,
"scale": 0.015,
"opacity": 0.45
}
}
},
"speaking": {
"speed": 1.15,
"params": {
"nodeCount": 260,
"nodeSize": 1.5,
"surfaceScale": 1.5,
"surfaceSpeed": 0.5,
"saturation": 0.62
},
"materials": {
"glow": {
"strength": 0.45
}
},
"bindings": {
"audioLevel": {
"input": "agentVolume",
"inputRange": [
0,
0.8
]
}
}
}
}
}
Structure
fxSpec(required): the version,"1.14".object(required) andpattern(required): what to draw. See the catalog.size(20 · 32 · 64) andspeed: see Speed & size.ink(1.8): how present the visual is,0to1(default1). It fades everything, halos included, and a state can patch it — an assistant rests below full ink when idle and comes to full ink when it listens.color,gradient: see Colour & theme.materials:glow,noise,pulse. See Materials.params: the pattern's own props (the catalog's style props), e.g."ringCount": 3.bindings: map your app's inputs onto values. See Bindings.states: a patch per lifecycle state. See States vs patterns.transitions(1.9): how a change between two states animates. See Transitions below.rules(1.9): pick a state from the app's inputs ("10,000 steps →goalReached"). See Rules.accessibility(1.9): the view's spoken name and the words per state. See Accessibility.performance:maxFps, and what to shed under low power.name,description,$schema: for people and editors.
Point $schema at https://sinua.dev/schema/fx-spec-1.json and your editor completes and checks the file as you type. A character recipe inside it is checked too.
Versioning
fxSpec is "1.14", written as major.minor (so 1.10 comes after 1.9). A file declares the version it was written for, and a runtime reads every file of its major version: keys it doesn't know become warnings, and a different major is refused. Declaring a version older than the keys you use is an error (transitions in a "1.8" file, say), so write "1.14" and use whatever this page documents. An older file stays valid and renders the same, with the exceptions from before 1.0: object: "edge" (0.1.0-beta.8), and in the 1.14 cleanup (0.1.0-beta.9) the liquid, particles and holographic materials, the concluding pattern and the wardrobe, named palettes and eye styles. A file that uses a removed section gets an error and isn't drawn, and the message says what to delete. After 1.0, a removal only comes in a major version (FX Spec 2.0), after a minor of deprecation warnings.
1.14: the 0.1.0-beta.9 cleanup
1.14 adds no key. It's the version the 0.1.0-beta.9 cleanup ships under, so a "1.14" file is read strictly (its unknown keys are errors).
- Removed, an error at every version: the liquid, particles and holographic materials, the concluding pattern, and the wardrobe, named palettes and eye styles.
- One value change that reaches every version:
glowingzooms in less between voice states (thinking and speaking come closer to listening), so it no longer seems to jump at large sizes.
1.13: the richer look and silhouettes
palette(character) also takes roles (primary,secondary,accent) and adarkvariant. See Roles and the dark variant.paramson a character takegrain(0–1) andshading(0/1). Under low power, grain and the soft shading turn off.silhouette(orb,pattern: "silhouette"):"human","helmet"or your own{ path, eyes, mouth }. See Silhouette.- Voice states without
states: in a 1.13 file, a voice state the file has no entry for takes the built-in voice behaviour, so a character file with nostatesstill listens, thinks and speaks. - Recipe limits: 96 parts (was 48) and 4,096 path points in all.
- Removed in 0.1.0-beta.9:
cosmetics,wardrobe, named palettes ("palette": "sunset",theme) andparams.eyeStyleare errors in any version. - 1.8–1.12 files resolve exactly as before.
1.12: character recipes
1.12 lets a file carry its own character: the new key recipe holds a character recipe, the same format as the built-in characters. No code and no Sinua release; the file is the character. Make your own character walks through one, and the recipe reference lists every key.
{
"$schema": "https://sinua.dev/schema/fx-spec-1.json",
"fxSpec": "1.12",
"object": "character",
"pattern": "pip",
"recipe": { "recipe": 1, "id": "pip", "profile": "chirp", "palette": { … }, "rig": { … }, "parts": [ … ], "burst": { … } },
"params": { "seed": 5, "flutterGain": 0.3 }
}recipeneeds"fxSpec": "1.12"andobject: "character". Thepattern(and any state's) names the recipe'sid, which can't be a built-in character's. Its mistakes point into it:/recipe/parts/2/segments: expected a whole number from 3 to 128.- Limits, so a file from outside can't make the engine slow: at most 64 KB, 48 parts, lists of 32, numbers within ±1000. Over a limit is an error, never trimmed; a recipe that would be heavy to draw is a warning.
- Shapes from a drawing: any shape in a recipe may be an SVG path,
"shape": { "path": "M50 74 L56 22 … Z" }, with the commandsM L H V C S Q T Z(no arcs) in the 200-unit box; later subpaths are holes. - Voice states: a recipe moves like the shared character, or like a built-in with
"profile": "chirp". The file's ownstatesgo on top. expression(any character):"happy","surprised","thoughtful","sad","sleepy"or"none", in the base and instates. A state change blends one into the next.palette(any character):{ "<slot>": "<colour>" }, hex or DTCG, in the base and instates. See Palette.- 1.8–1.11 files resolve exactly as before.
Removed: the edge object
object: "edge" (the screen-edge glow, SinuaEdge, <sinua-edge>) was removed in 0.1.0-beta.8. A file with it fails at every version, with an error at /object. There's no replacement: draw a screen-edge glow in your app. It was the first removal before 1.0 (see Versioning).
Removed: three materials
The liquid, particles and holographic materials were removed in 0.1.0-beta.9 (FX Spec 1.14). A file that uses materials.liquid, materials.particles or materials.holographic, or names liquid or particles in performance.lowPower.disable, is rejected at every version, with an error naming the section. Delete the section; there's no replacement. Under low power the host now turns off glow only.
Removed: the concluding pattern
Also in 0.1.0-beta.9: the orb's concluding pattern. A file whose pattern (or a state's) is "concluding" is rejected at every version; pick another pattern.
Removed: the wardrobe, named palettes and eye styles
Also in 0.1.0-beta.9: the character wardrobe and cosmetics (cosmetics, wardrobe, a view's loadout, catalog packs), the built-in named palettes ("palette": "sunset", palette.theme) and the eye styles (params.eyeStyle). A file that uses one is rejected at every version, with an error naming the key; in a recipe, slots, tags, cosmetics, a part's role and the eye fields style, iris and sclera are errors too. Delete them; there's no replacement. Palettes by slot or role, with a dark variant, stay.
1.11: characters
1.11 adds one object, character: a face for a voice assistant. Its voice states are built in, so a file only says who it is:
{
"fxSpec": "1.11",
"name": "Buzzy",
"description": "A character for a voice assistant (FX Spec 1.11, the `character` object). Like the one-shape showcase, the file only says who it is: the voice-state profile ships Buzzy's state language -- listening leans in and looks at you while sound arcs light at its ears, thinking looks up and away with a blink at the turn and thinking dots, speaking smiles and its mouth is a voice line that swings with the agent's level. `hue` turns its shell (here a teal Buzzy); its eyes and amber accents stay. A `color` or `gradient` section is an error on a character: they would repaint its eyes and screen.",
"object": "character",
"pattern": "buzzy",
"size": 64,
"params": {
"hue": 190,
"seed": 3
},
"bindings": {
"muted": {
"input": "micMuted"
}
},
"states": {
"initializing": {},
"idle": {},
"listening": {
"bindings": {
"audioLevel": {
"input": "micLevel"
}
}
},
"thinking": {},
"speaking": {
"bindings": {
"audioLevel": {
"input": "agentVolume",
"inputRange": [0, 0.8]
}
}
}
},
"accessibility": {
"name": "Buzzy"
}
}
object: "character"needs"fxSpec": "1.11". An older file gets an error at/object.- No
colororgradienton a character. They would repaint its eyes and screen, so the file gets an error that points atparams.hue.hueturns the character's own colour, andmouth,accessories,look,turnandseedare its other options. - The pose values are
paramstoo. The values the states set (gazeX,lid,eyeSmile, …) can go inparamsor in a state's patch, to tune how a state looks. stateAgeis a new runtime input: the seconds since the view's state changed. Every view sets it (a character blinks with it as your turn ends), and it can't go inparams.- Older files (1.8 to 1.10) resolve exactly as before.
1.10: calmer voice states
1.10 adds no keys. It made the built-in voice states calmer: listening, thinking and speaking stopped adding particles (see How the states look). The particles material itself was removed in 0.1.0-beta.9, so no version draws them any more.
Transitions
State changes animate, 0.6 s easeInOut unless the file says otherwise. transitions sets the duration and curve per pair of states:
{
"fxSpec": "1.9",
"name": "Voice orb (with transitions)",
"object": "orb",
"pattern": "glowing",
"size": 64,
"states": {
"idle": {},
"listening": {},
"thinking": {},
"speaking": {}
},
"transitions": {
"default": { "duration": 0.6, "curve": "easeInOut" },
"idle->listening": { "duration": 0.25, "curve": "easeOut" },
"*->idle": { "duration": 0.9 }
}
}
- Keys:
default,"from->to","from->*"and"*->to", with the names fromstates. The most specific entry wins, field by field: the exact pair, thenfrom->*, then*->to, thendefault. - Fields:
durationin seconds,0to10(0cuts), andcurve:linear,ease,easeIn,easeOutoreaseInOut. - What moves: when both states keep the pattern, the values interpolate and the shape flows; a change of pattern cross-fades (the orb lattice trio, glowing, calibrating and progressing, morphs point by point). The file sets how long and how; the engine picks the technique.
- A view's
crossFadeoption, when you set it, overrides every change's duration (0still cuts).
Resolving a spec yourself
Views resolve specs for you. To inspect what the engine will draw, for tooling or tests, resolve one directly:
import { resolveFxSpec } from "@sinua/core";
import spec from "../spec/voice-orb.fxspec.json";
// Resolve once to see what the engine will draw for a lifecycle state.
const r = resolveFxSpec(JSON.stringify(spec), {
state: "listening",
inputs: { micLevel: 0.4, micMuted: 0 },
});
if (!r.ok) console.error(r.diagnostics);
console.log(r.state, r.overrides); // "listening" pattern, with audioLevel bound to micLevel
Every key
Top level
| Key | Type | Description |
|---|---|---|
$schema | string | |
fxSpec | string | "major.minor", 1.8 or later (the runtime's floor; 1.0-1.7 are rejected). The runtime reads up to 1.14 strictly; a newer 1.x file resolves, with its unknown keys reported as warnings. |
name | string | |
description | string | |
object | ||
pattern | string | The visual pattern, e.g. working, metering, tracking, scanning, generating. Must belong to object's family. |
size | ||
speed | number | Multiplier on the preset's tuned speed; frameFromFxSpec applies elapsed * presetSpeed * speed. |
ink | number | FX Spec 1.8: the whole visual's opacity, 0 to 1. 1 draws it as the pattern defines it; lower fades everything, halos included. A state patches it (a voice assistant's idle typically sits below 1). |
palette | object | FX Spec 1.12, object: character only: repaint some of the character's colours (its recipe's palette names, e.g. shell), as hex or DTCG colours. A slot's Light/Dark tones follow it; the rest stays as drawn. A state patches it. 1.13: role names (primary, secondary, accent, through the recipe's roles) and a dark variant the views pick in a dark theme. Named palettes (a string, or theme) were removed in 0.1.0-beta.9 and are an error. |
expression | FX Spec 1.12, object: character only: the character's expression. It shapes the eyes and the resting mouth; the voice state keeps the gaze, the turn and the talking or thinking mouth. A state patches it; null there removes it. | |
color | A color (shorthand for { value, mix: 1 }) or the full section. | |
gradient | object | see gradient |
materials | object | |
params | object | Per-state engine opts (docs/parameters.md). Runtime inputs (pointer*, audio*, history*, interrupt*, muted*, voiceStateCode, peakN) and section-owned keys (color*, gradient*, glow*, noise*, pulse*) are rejected here. FX Spec 1.7: progress / segment may be arrays. |
bindings | object | see bindings |
states | object | Lifecycle key -> design entry, merged over the top-level design (RFC 7396: objects merge, null deletes, arrays replace). Keys are free-form; LiveKit's AgentState names (initializing, idle, listening, thinking, speaking) are the convention. A state not listed renders the top-level design. |
performance | object | see performance |
transitions | object | see transitions |
rules | object | see rules |
accessibility | object | see accessibility |
recipe | object | 1.12: a character recipe (the format of spec/characters/*.json) for object: character; pattern equal to its id draws it. Every field: spec/character-recipe-1.schema.json and docs/character-recipe.md. |
silhouette | FX Spec 1.13, pattern: silhouette: which head and shoulders the dots fill. A built-in by name, or a file's own outline in a 200 × 200 box (start and end the path below the shoulders: the bottom stays open and fades) with its landmarks. File-wide. See docs/fx-spec.md. |
A states entry
| Key | Type | Description |
|---|---|---|
pattern | string | The pattern for this lifecycle key; defaults to the top-level pattern. |
speed | number | |
ink | number | FX Spec 1.8: this state's opacity; inherits the top-level ink when absent. |
palette | FX Spec 1.12: this state's palette override (a character); merged over the top-level palette. 1.13: also role names and dark. | |
expression | FX Spec 1.12: this state's expression (a character); inherits the top-level expression, null removes it. | |
color | object | null | |
gradient | object | null | |
materials | object | null | |
params | object | null | |
bindings | object | null |
A binding
| Key | Type | Description |
|---|---|---|
input | string | The app's input name (e.g. micLevel, steps, heartRate); the engine never interprets it. Missing at runtime = binding inactive. |
inputRange | array | |
outputRange | array | Default: the target's range, except the per-ring progress[0]…progress[3], which default to [0, 1] (the goal = one lap); for extra laps (up to 3) pass a multi-stop outputRange, e.g. [0, 1, 3]. |
curve |
Bindable targets: progress, quality, accuracy, audioLevel, muted, progress[0], progress[1], progress[2], progress[3], glow.strength, noise.strength, gradient.strength, pulse.strength, color.mix.
Deprecated aliases (FX Spec 1.6 and earlier; still accepted, with a warning in a 1.7 file): state → pattern, progress0 → progress[0], progress1 → progress[1], progress2 → progress[2], progress3 → progress[3], glowStrength → glow.strength, noiseStrength → noise.strength, gradientStrength → gradient.strength, pulseStrength → pulse.strength, colorMix → color.mix. See the rename map.
color
| Key | Type | Range | Description |
|---|---|---|---|
value | object | see color | |
mix | number | 0–1 | |
lightness | number | -1–1 | |
mode | enum | ink · fixed |
gradient
| Key | Type | Range | Description |
|---|---|---|---|
stops | array | ||
angle | number | -360–720 | |
strength | number | 0–1 | |
saturation | number | 0–1 | |
mid | number | 0.05–0.95 | |
path | enum | short · long |
materials.glow
| Key | Type | Range | Description |
|---|---|---|---|
strength | number | 0–1 | |
radius | number | 1–8 | |
layers | number | 1–8 | |
tint | number | 0–1 | |
hue | number | 0–360 | |
mode | enum | stacked · blur | stacked = concentric copies (every renderer); blur = one Gaussian-blurred halo per element (paint-contract blur). Low power with disable: ["blur"] falls back to stacked. |
blend | enum | normal · additive | How blur-mode halos composite: additive = Canvas lighter / SwiftUI plusLighter / Compose BlendMode.Plus. |
materials.noise
| Key | Type | Range | Description |
|---|---|---|---|
strength | number | 0–1 | |
amplitude | number | 0–1 | |
scale | number | 0–64 | |
speed | number | 0–16 | |
seed | number |
materials.pulse
| Key | Type | Range | Description |
|---|---|---|---|
strength | number | 0–1 | |
period | number | 0.05–60 | |
opacity | number | 0–1 | |
scale | number | 0–1 | |
phase | number |
performance
| Key | Type | Range | Description |
|---|---|---|---|
maxFps | number | 1–120 | Frame-rate cap the host should honour. Absent = the host's default. |
lowPower | object |