A rule profile is a named set of rule values an owner assigns to a tenant. Every validate and publish of a face lens or a screen effect also checks it against the profiles that apply to it, and the editor shows the same issues live. Profiles are data: the platform defines the rules below; which values a tenant uses is configuration, never code.
Implementation: packages/profiles (@ingevora-mirage/profiles). Designs: docs/superpowers/specs/2026-09-29-mirage-editor-rule-profiles-design.md (face lenses) and 2026-10-01-mirage-screen-effect-rules-design.md (screen effects). Numbers in brackets refer to rendering-v1.md, E-numbers to effect-rendering-v1.md.
packages/profiles
@ingevora-mirage/profiles
docs/superpowers/specs/2026-09-29-mirage-editor-rule-profiles-design.md
2026-10-01-mirage-screen-effect-rules-design.md
rendering-v1.md
effect-rendering-v1.md
Face lenses and screen effects are separate products with their own manifests. Each rule applies to one product: face lenses are checked against keepOutZones (§2, §3), screen effects against the flashing limit and the screen-coverage cap (§5). A face lens ignores the screen-effect rules, and a screen effect ignores keepOutZones.
keepOutZones
A strict JSON object (ruleProfileSchema); unknown keys are rejected. Every rule is optional, and an absent rule is not checked. Values are INTEGER enums.
ruleProfileSchema
1
2
placement.mode
maxFlashesPerSecond
screenCoverage
{ maxShare, maxMs }
maxShare
maxMs
{ "keepOutZones": [1, 2], "maxFlashesPerSecond": 3, "screenCoverage": { "maxShare": 0.6, "maxMs": 500 } }
Frame overlays (type 3), colour looks (type 1) and full-frame sprites (type 4, placement.mode 2) are exempt from keep-out: they are full-frame by design, and v1 checks geometry, not pixels (a transparent-centre frame is legitimate).
Keep-out is checked statically on one reference face (referenceFace()), in a 1000 × 1000 frame (REFERENCE_FRAME):
referenceFace()
1000 × 1000
REFERENCE_FRAME
M = (500, 500)
W = 400
±200 / 2.2 = ±90.909…
M
packages/render/test/helpers.ts
W = 44
400 / 44
0.10 W
0.30 ×
+109.09
+409.09
+227.27
0.36 W = 144
Zones follow the face's own landmarks (keepOutZones(face, frame), ZONE_GEOMETRY), so the same definition draws correctly on a live face:
keepOutZones(face, frame)
ZONE_GEOMETRY
u
v
W
(500, 727.27)
(86.4, 40)
(590.91, 500)
(409.09, 500)
48
keepOutZones returns image-pixel shapes — { id, kind: 'ellipse', center, radii, rotationDeg } or { id, kind: 'circle', center, radius, rotationDeg }, rotationDeg = the face roll — in the order mouth, the subject's left eye, the subject's right eye.
{ id, kind: 'ellipse', center, radii, rotationDeg }
{ id, kind: 'circle', center, radius, rotationDeg }
rotationDeg
Overlap.
followRoll
offset
CLIP_SAMPLE_MS
durationMs
placeOnFace(faceTransformAt(…))
Profile issues join the format's { path, code, message } issue list (ManifestIssue), after the format's own. Paths are JSON Pointers into the manifest. Messages are English and name the profile.
{ path, code, message }
ManifestIssue
profile.keep_out
/layers/<i>
i
/animations/<i>
profile.flashing
profile.screen_coverage
profile.asset_unreadable
/layers/<i>/asset
Example messages: Rule profile "Studio One": layer "mask" overlaps the mouth and eyes keep-out zones on the reference face. · Rule profile "Studio One": clip "drop" moves layer "badge" into the mouth keep-out zone at 240 ms on the reference face. · for particles, … clip "rise" moves the particle spawn point of layer "sparks" into the eyes keep-out zone at 610 ms ….
Rule profile "Studio One": layer "mask" overlaps the mouth and eyes keep-out zones on the reference face.
Rule profile "Studio One": clip "drop" moves layer "badge" into the mouth keep-out zone at 240 ms on the reference face.
… clip "rise" moves the particle spawn point of layer "sparks" into the eyes keep-out zone at 610 ms …
Screen effects (times in seconds to two decimals, measured shares to a tenth of a percent): Rule profile "Studio One": 6 flashes within one second from 0.40 s (flash motions "strobe", "boom"); at most 3 are allowed. · Rule profile "Studio One": the effect paints 82.4 % of the screen for 1.20 s from 0.30 s (layers "sky", "badge"); at most 60 % for 500 ms is allowed. — a run that reaches the end reads … for 1.20 s from 0.30 s, to the end … · Rule profile "Studio One": the image "a_00000000.png" could not be decoded, so screen coverage was not checked. One flash, motion or layer reads in the singular (1 flash, flash motion "strobe", layer "sky"); a limit of 0 reads no flash is allowed. A screen effect's flashing issue comes first, then its coverage issues.
Rule profile "Studio One": 6 flashes within one second from 0.40 s (flash motions "strobe", "boom"); at most 3 are allowed.
Rule profile "Studio One": the effect paints 82.4 % of the screen for 1.20 s from 0.30 s (layers "sky", "badge"); at most 60 % for 500 ms is allowed.
… for 1.20 s from 0.30 s, to the end …
Rule profile "Studio One": the image "a_00000000.png" could not be decoded, so screen coverage was not checked.
1 flash
flash motion "strobe"
layer "sky"
no flash is allowed
The same for both products:
tenantId
checkProfile(manifest, assets, profile) checks one profile against a face lens, and checkEffectProfile(manifest, assets, profile) against a screen effect (§5); the caller (API, editor) runs it for every profile that applies. Both expect a schema-valid manifest; assets maps each asset name to its pixel size (sprite sheets: the whole sheet) — for a screen effect, with its decoded alpha when the coverage cap is checked (§5).
checkProfile(manifest, assets, profile)
checkEffectProfile(manifest, assets, profile)
assets
The flashing limit and the screen-coverage cap are worked out by playing the effect through the render package's own effect instance (effect-rendering-v1), so they see exactly what the SDKs draw: triggers, sprite frames, clips, particles, shake and pulse.
The play-through.
REFERENCE_SCREEN
S
EFFECT_STEP_MS
effect.started
Flashes (maxFlashesPerSecond).
alpha(t − 1) ≥ alpha(t) < alpha(t + 1)
1 / frequencyHz
run motion
intensity
[s, s + 1000)
s
Coverage (screenCoverage).
COVERAGE_SAMPLE_MS
COVERAGE_GRID_PT
(u, v)
(src.x + ⌊u · src.width⌋, src.y + ⌊v · src.height⌋)
a = opacity × alpha / 255
1 − Π(1 − a)
PAINTED_ALPHA
n
n × 10
{ "maxShare": 0.6, "maxMs": 500 }
a > 0
Pixels. The coverage cap needs each asset's alpha. decodePngAlpha decodes a PNG to one byte of alpha per pixel, row-major, expanded as E4.4 says: a palette entry takes its tRNS alpha (entries past the chunk's are opaque); grey and RGB pixels are opaque except those equal to the tRNS key colour; 16-bit samples keep their high byte. An interlaced PNG below 8 bits per sample cannot be decoded — export it without interlacing. The cap is checked only once every asset a layer uses has its alpha; an image that could not be decoded is reported (profile.asset_unreadable) and the cap is not checked further. The flashing limit needs no pixels.
decodePngAlpha
tRNS
Cost. At most 10 000 renderer steps and 1 000 sampled frames; a sample drawing the same quads as the one before it (static content) is not composited again. Deterministic: the API and the editor run the same code on the same bytes, so they report the same issues.