A screen effect plays in an overlay the host app places above its own screens — no camera,
no face. It is a product of its own: this manifest is independent of the face lens manifest
(manifest-v1.md), with its own enums, schema, fixtures and rendering rules
(effect-rendering-v1.md). Both manifests ship in the git tag
format-v1.0.0.
#Shape
json
{
"formatVersion": 1,
"effectId": "<uuid>",
"version": 1,
"minEngineVersion": 1,
"durationMs": 2500,
"layers": [],
"triggers": [],
"animations": [],
"assets": {}
}
durationMs — required, integer 100–10 000: the effect always ends here.
layers — 1–8, drawn in array order (first = back). triggers — at most 32.
animations — optional, at most 16 keyframe clips (see Animations); absent = none.
assets — <name>_<first 8 hex of sha256>.png → { sha256, bytes }. Every object is strict:
an unknown key is rejected.
#Enums
| Enum |
Values |
layer type |
1 particles · 2 full-screen sprite · 3 motion · 4 image · 5 sprite |
fit (type 2) |
1 contain · 2 cover |
sheet.endBehavior (types 2, 5) |
1 hold last frame · 2 hide (the layer's visible becomes false; a later show/play sprite shows it again from frame 0) |
motion (type 3) |
1 shake · 2 flash · 3 pulse |
event source |
1 lifecycle · 2 host |
action type |
1 show · 2 hide · 3 play sprite (→ 2, 5) · 4 emit particles (→ 1, needs count) · 5 run motion (→ 3) · 6 play animation (names a clip: { "type": 6, "animation": "<clip id>" }) |
animation property |
1 x · 2 y · 3 scale · 4 rotation · 5 opacity · 6 zoom |
animation easing |
1 linear · 2 ease in · 3 ease out · 4 ease in-out · 5 hold · 6 back · 7 bounce |
animation endBehavior |
1 hold last values · 2 return to base |
Lifecycle names: effect.started, effect.ended. Host names: any ^[a-z][a-z0-9_.-]{0,63}$ —
the platform defines the mechanism, the customer the vocabulary. Triggers have no threshold.
#Layers
Every layer has id (^[a-z][a-z0-9_-]{0,31}$, unique) and visible.
| Type |
Fields |
| 1 particles |
asset; origin {x, y} (fractions of the overlay, 0–1); emitter { ratePerSecond 0–300, lifetimeMs 100–10 000, speed 0–2, directionDeg −180–180, spreadDeg 0–360, sizeScale 0.01–1, gravity −2–2 }; burstCount 0–300; maxParticles 1–300 |
| 2 full-screen sprite |
asset; sheet { frameWidth, frameHeight 1–1024, frameCount 1–256, fps 1–60, loop, endBehavior }; fit; startMs 0–10 000 |
| 3 motion |
motion; intensity 0–1; startMs 0–10 000; durationMs 50–10 000; frequencyHz 0.5–30 |
| 4 image |
asset; position {x, y} (the image's centre, fractions of the overlay); scale (> 0, ≤ 2: width as a multiple of the overlay's short side); rotationDeg −180–180; opacity 0–1 |
| 5 sprite |
asset; sheet; position; scale; rotationDeg; opacity; startMs 0–10 000 |
Lengths (particle speed, gravity, sizeScale, image and sprite scale) are multiples of
S = min(overlay width, overlay height).
#Animations
A clip animates one layer over its own time; the rendering rules are
effect-rendering-v1.md §E10.
json
{
"id": "bounce-in", "layer": "badge",
"durationMs": 800, "loop": false, "autoplay": true, "startMs": 0, "endBehavior": 1,
"tracks": [
{ "property": 3, "keys": [{ "atMs": 0, "value": 0.1, "easing": 6 }, { "atMs": 800, "value": 0.5, "easing": 1 }] }
]
}
id — same pattern as a layer id, unique among clips. layer — the layer it animates.
durationMs 50–10 000; loop; autoplay (the clip starts itself at startMs); startMs
optional, 0–10 000 (absent = 0); endBehavior required (no effect when loop is true).
tracks — 0–6, at most one per property. keys — 1–32 per track, atMs 0–durationMs,
strictly increasing; a key's easing shapes the segment to the next key (the last key's easing
is unused but required).
- Values are absolute: a playing clip's track replaces the property's resting value. One clip
plays per layer at a time; action 6 (re)starts a clip and implies show of its layer.
Properties each layer type may animate, and each property's key range:
| Property |
Image 4, sprite 5 |
Particles 1 |
Full-screen sprite 2 |
Key range |
| 1 x |
position.x |
origin.x |
— |
0–1 |
| 2 y |
position.y |
origin.y |
— |
0–1 |
| 3 scale |
scale |
— |
— |
> 0, ≤ 2 |
| 4 rotation |
rotationDeg |
— |
— |
−720…720 |
| 5 opacity |
opacity |
— |
opacity (resting value 1) |
0–1 |
| 6 zoom |
— |
— |
zoom (resting value 1) |
0.1–4 |
Motion layers (3) animate nothing. Tables in code: EFFECT_ANIMATABLE_PROPERTIES,
EFFECT_ANIMATION_VALUE_RANGE, MAX_ANIMATIONS (16), MAX_TRACKS_PER_ANIMATION (6),
MAX_KEYS_PER_TRACK (32). Every animation feature is engine level 1.
#Files
- JSON Schema (generated):
packages/format/schema/effect.v1.schema.json
- Fixtures:
packages/format/fixtures/effect/valid/ (must pass) and
packages/format/fixtures/effect/invalid/ — the same invalid-fixture format as the lens
manifest (description, base = a file in fixtures/effect/valid/ without .json, an RFC 6902
patch of add/remove/replace, then exactly one of expectedIssues or
rejectedBySchema: true).
- Valid fixtures cover every layer type:
confetti.json (screen particles 1, motion 3),
background-sprite.json (full-screen sprite 2) and trophy.json (placed sprite 5, placed image 4);
animated.json uses every animation property and easing, an autoplay, a looping and an
action-started clip, and action 6.
#Validation = schema + rules
Semantic rules run only on a schema-valid manifest. Only the path + code of a semantic issue
are normative; a schema failure only has to be rejected.
| Code |
Path |
Condition |
layer.duplicate_id |
/layers/i/id |
A layer id repeats an earlier one |
asset.missing |
/layers/i/asset |
A layer's asset is not a key of assets |
asset.unused |
/assets/<name> |
An assets key no layer uses |
package.too_large |
/assets |
Σ assets[*].bytes > 3 MiB (3 145 728) |
particles.too_many |
/layers |
Σ maxParticles over type 1 layers > 300 |
timing.exceeds_duration |
/layers/i/durationMs |
Type 3: startMs + durationMs > durationMs of the effect (ending exactly at the end is allowed) |
timing.exceeds_duration |
/layers/i/startMs |
Types 2 and 5: startMs ≥ durationMs of the effect |
trigger.unknown_event |
/triggers/i/event/name |
Source 1 with a name other than effect.started / effect.ended |
trigger.unknown_layer |
/triggers/i/actions/j/layer |
The action's layer matches no layer id (suppresses action_mismatch for that action; action 6 has no layer) |
trigger.unknown_animation |
/triggers/i/actions/j/animation |
Action 6 names no clip |
animation.duplicate_id |
/animations/i/id |
A clip id repeats an earlier one |
animation.unknown_layer |
/animations/i/layer |
No layer has that id |
animation.property_not_animatable |
/animations/i/tracks/j/property |
The layer's type may not animate the property (not reported when the layer is unknown) |
animation.duplicate_property |
/animations/i/tracks/j/property |
A property repeats within one clip |
animation.keys_out_of_order |
/animations/i/tracks/j/keys/k/atMs |
atMs ≤ the previous key's |
animation.key_outside_clip |
/animations/i/tracks/j/keys/k/atMs |
atMs > the clip's durationMs |
animation.value_out_of_range |
/animations/i/tracks/j/keys/k/value |
The value is outside the property's key range |
animation.autoplay_conflict |
/animations/i/autoplay |
A second autoplay clip on the same layer |
timing.exceeds_duration |
/animations/i/startMs |
An autoplay clip whose startMs (absent = 0) ≥ durationMs of the effect |
trigger.action_mismatch |
/triggers/i/actions/j |
The target layer's type is not one the action allows (see the action enum) |
engine.too_low |
/minEngineVersion |
minEngineVersion < the highest engine level of any layer type or event source used. Unreachable in v1 (all level 1); no fixture. |
Decode-time check, run by the API at validate and publish: asset.sheet_too_small — for a type 2
or type 5 layer, frameCount must not exceed floor(width / frameWidth) × floor(height / frameHeight) of the asset's actual pixel size.
#Versioning
formatVersion changes only on a breaking change to this document. Adding a layer type or an
event source is not breaking: it raises that feature's engine level
(EFFECT_ENGINE_LEVEL_BY_LAYER_TYPE, EFFECT_ENGINE_LEVEL_BY_EVENT_SOURCE in constants.ts),
and a renderer compares minEngineVersion with its own engine version before it validates
strictly. Until the first effect is published and the SDKs' 1.0.0 ship, every v1 change is part
of the tag format-v1.0.0 (the tag is moved and the SDKs re-pin it).