an.stage.serialize#
JSON contract between the Python compiler and the (future) JS runtime.
These Pydantic models describe exactly the JSON shape the PixiJS runtime
consumes — nothing more. compile_shot produces these objects; the runtime
reads them. The schema is deliberately separate from the cutout-internal
Python evaluation types (clip.Clip, channel.Channel) so the Python side
can evolve internally without breaking the runtime contract.
This file used to carry a sketched Spine-flavoured swap vocabulary
(SkinJSON, RigJSON, CutoutSceneJSON.rigs, SlotJSON/
NodeJSON.slots, current_attachment) that nothing populated and nothing
read — deleted in an#86 so the real swap mechanism (VisualJSON’s
per-node asset maps driven by step channels) is the only one the contract
describes. A few declared-but-unwired scalars remain (PlacedClipJSON.blend_in
/ blend_out — recorded, never applied; VisualJSON.texture_id): the same
debt class at smaller scale, kept only because they are field-shaped
placeholders rather than a parallel vocabulary for a capability that shipped
elsewhere. Do not add more; a new field needs its producer and its consumer in
the same change.
>>> j = CutoutSceneJSON(
... meta={"fps": 30, "width": 1920, "height": 1080, "duration": 5.0},
... scene=NodeJSON(name="root"),
... animations={},
... timeline=TimelineJSON(duration=5.0, tracks=[]),
... assets=AssetsJSON(textures={}, audio={}),
... )
>>> from_dict(to_dict(j)) == j
True
Module Attributes
The visual kinds |
Functions
|
Rebuild a scene from a plain-dict representation. |
|
Dump a scene to a plain-dict representation (no None pruning). |
Classes
|
A named, reusable animation clip. |
|
A single asset (texture / audio file). |
|
How one scene entity's store reference actually resolved at compile time. |
|
Map of asset id → AssetJSON, split by kind. |
|
One animated property of one target. |
|
Top-level cutout scene JSON — the JS runtime's input contract. |
|
Per-shot metadata. |
|
Single keyframe in an animation channel. |
|
One node in the scene tree. |
|
A stroked path's drawing instruction (an#160), carried on a |
|
An animation placed on a track at a specific time. |
|
Top-level timeline: total duration + tracks. |
|
A sequence of placed clips with optional target-prefix metadata. |
|
Local transform of a scene-graph node (authoring form). |
|
A copy of a node's own visual, drawn BEHIND it in the same container (an#163 gap 5: the outline and the paper-gap shadow). |
|
Drawable content attached to a node. |
- class an.stage.serialize.AnimationClipJSON(**data)[source]#
Bases:
_JSONModelA named, reusable animation clip.
- class an.stage.serialize.AssetJSON(**data)[source]#
Bases:
_JSONModelA single asset (texture / audio file).
- class an.stage.serialize.AssetResolutionJSON(**data)[source]#
Bases:
_JSONModelHow one scene entity’s store reference actually resolved at compile time.
The IR declares every drawable entity as
store+ref. What the compiler builds from that pair is not recoverable from the scene tree afterwards: a character whose descriptor is missing and a character that never had one produce byte-identical procedural rigs. That ambiguity is an#33 — three CI runners once agreed perfectly about a picture that was not the picture, and the agreement read as a clean positive result.So the compiler records what it did, per entity, in the artifact the browser actually loads.
fallbackis the load-bearing bit: True means the declared ref supplied nothing and a stand-in was drawn in its place.>>> AssetResolutionJSON( ... id="maya", kind="character", store="characters", ref="maya-v1", ... resolved="descriptor", fallback=False, ... ).fallback False
- class an.stage.serialize.AssetsJSON(**data)[source]#
Bases:
_JSONModelMap of asset id → AssetJSON, split by kind.
- an.stage.serialize.BUILTIN_VISUAL_KINDS: tuple[str, ...] = ('sprite', 'rect', 'ellipse', 'svg_sprite', 'path')#
The visual kinds
runtime.jsdraws itself. Any other kind must be registered by a genre’s runtime script (an#247), or it draws as a rect.
- class an.stage.serialize.ChannelJSON(**data)[source]#
Bases:
_JSONModelOne animated property of one target.
- class an.stage.serialize.CutoutSceneJSON(**data)[source]#
Bases:
_JSONModelTop-level cutout scene JSON — the JS runtime’s input contract.
Versioned so the runtime can refuse incompatible inputs.
- asset_resolution: list[AssetResolutionJSON]#
One entry per drawable entity, in scene order — see
AssetResolutionJSON. Inert to the runtime; read by the bench harness and the golden-corpus bless to assert WHICH render path ran.
- model_config: ClassVar[ConfigDict] = {'extra': 'allow', 'populate_by_name': True}#
Configuration for the model, should be a dictionary conforming to [
ConfigDict][pydantic.config.ConfigDict].
- overlay: NodeJSON | None#
a second top-level container the runtime centres on the canvas and never indexes, so no channel — the camera’s
root.pivot/root.scaleincluded — can reach it. Its children are indexed by their own paths exactly likescene’s, so an overlay title’s words tween like anything else.Nonewhen the shot has no overlay, and then omitted from the serialized document.- Type:
The camera-immune layer (an#155)
- class an.stage.serialize.CutoutSceneMetaJSON(**data)[source]#
Bases:
_JSONModelPer-shot metadata.
- blink_phases: dict[str, float]#
Per-entity blink phase in [0, 1), a pure function of the entity NAME (an#88). Stamped by the compiler — which now emits blinks as channels — and inert to the runtime. It is recorded because renaming a corpus character silently re-phases every blink and moves every pixel metric; a stamped phase turns that into a visible diff instead of an unexplained metric shift. (The runtime’s determinism probe used to carry this; the fact moved with the mechanism.)
- entity_spaces: dict[str, str]#
Per entity, the property space its kind declares, where that is NOT the kernel default (
stage.node) – so evaluating this document with no explicit space evaluates each target in its entity’s space, asan validateand the compiler already do (an#245; read byan.timing.timeline.timeline_from_compiled()). **Serialized only when non-empty**: no shipped kind declares another space, so no document and no contract hash moves until a genre registers one.
- extensions: dict[str, Any]#
What genre compile passes add to the document’s meta, by key (an#247): the generic slot a pass writes through
CompileState.meta_extensionsrather than a field the stage would have to know by name. Also where a genre’s explicit replacement of a stage pass is recorded (replaced_compile_passes). Serialized only when non-empty.
- fonts: dict[str, str]#
entity id ->
"<family> <style> [(embedded)] sha256:<digest>"— identity by the font’s BYTES, since a family name is not one. Provenance, inert to the runtime (the glyphs are already SVG). Serialized only when non-empty.- Type:
Per text block, the face that set it (an#155)
- gaze_seeds: dict[str, int]#
Per-entity saccade seed (an#99), a pure function of the entity NAME like
blink_phases; stamped for the rigs that have pupils and serialized only when non-empty — a pre-Wave-6 rig has no pupils and its compiled document, the bench’s scene contract, must not move.
- model_config: ClassVar[ConfigDict] = {'extra': 'allow', 'populate_by_name': True}#
Configuration for the model, should be a dictionary conforming to [
ConfigDict][pydantic.config.ConfigDict].
- spaces: dict[str, dict[str, Any]]#
The definition of each space
entity_spacesnames, in the timing contract’skinds.jsonform (PropertySpace.to_json), so the document is self-describing:runtime.jshas no registry and evaluates a declared entity’s targets by THESE field kinds and write groups (an#287), andan.timing.timeline.timeline_from_compiled()reads the same definitions back. Serialized only when non-empty (it is empty exactly whenentity_spacesis).
- step_hz: float | None#
The stepped-timing policy the shot’s tweens were compiled under (an#89);
None= smooth. Serialized only when set: the compiled document is the bench’s scene contract (scene_contract_sha256), so anullhere would move every committed row’s hash for a knob nobody turned. Inert to the runtime; read by the ledger.
- style_pack: str | None#
The name of the
StylePackthis scene was compiled under, orNone. Recorded so a rendered document says which art direction produced it — the pack’s COLOURS are already resolved into the nodes, so this is provenance rather than an instruction. Omitted from the serialized document when unset (see the wrap serializer below).
- class an.stage.serialize.KeyframeJSON(**data)[source]#
Bases:
_JSONModelSingle keyframe in an animation channel.
- class an.stage.serialize.NodeJSON(**data)[source]#
Bases:
_JSONModelOne node in the scene tree.
- model_config: ClassVar[ConfigDict] = {'extra': 'allow', 'populate_by_name': True}#
Configuration for the model, should be a dictionary conforming to [
ConfigDict][pydantic.config.ConfigDict].
- paint_order: list[str] | None#
The order its descendants’ visuals are PAINTED in, when it is not the tree’s (an#430): node paths relative to this node, back to front. Set on a rig’s entity node whose nested chain interleaves with an unrelated part (an upper arm 1, a cord 2, a forearm 3); the runtime keeps each visual in its transform tree and paints it from this list.
- scope: str | None#
as the children of the node named
scopein the same parent ("": as the parent’s own children).None= under this node’s own path, the rule before an#343. An environment’s foreground container carriesscope=<env>, so every plane is<env>/<plane>wherever the environment is cut.an.stage.treeis the Python statement of the rule.- Type:
Where this node’s CHILDREN are indexed (an#343)
- class an.stage.serialize.PathJSON(**data)[source]#
Bases:
_JSONModelA stroked path’s drawing instruction (an#160), carried on a
pathvisual.pointsis always a POLYLINE — the compiler flattens cubic Béziers (an.stage.path_geometry.flatten_curve), so the runtime knows one geometry.trim_start/trim_endare the values shown before any channel touches the node; channels on those two properties move them.head_length == 0means no arrowhead. What the runtime draws from this is specified byan.stage.path_geometry.path_geometry.- closed: bool#
closedjoins the stroke at the first point (the polyline already returns there);fill(#rrggbb) is drawn under the stroke atfill_alphaand is not trimmed. Omitted from the wire when off, so no existing path document’s hash moves.- Type:
A closed shape and its fill (an#161)
- dash: float#
dash > 0is on.dash_offsetis the value shown before a channel touches it. These are wire fields of path visuals only, so they cannot move a non-path document’s hash.- Type:
Dash pattern (an#161)
- model_config: ClassVar[ConfigDict] = {'extra': 'allow', 'populate_by_name': True}#
Configuration for the model, should be a dictionary conforming to [
ConfigDict][pydantic.config.ConfigDict].
- class an.stage.serialize.PlacedClipJSON(**data)[source]#
Bases:
_JSONModelAn animation placed on a track at a specific time.
- class an.stage.serialize.TimelineJSON(**data)[source]#
Bases:
_JSONModelTop-level timeline: total duration + tracks.
- class an.stage.serialize.TrackJSON(**data)[source]#
Bases:
_JSONModelA sequence of placed clips with optional target-prefix metadata.
- class an.stage.serialize.TransformJSON(**data)[source]#
Bases:
_JSONModelLocal transform of a scene-graph node (authoring form).
alphais not geometry, but it lives here for the same reason the rest does: this is the per-node property bag the runtime applies, and it is animatable through the same channel machinery.It is set on the node’s container, so it cascades — fading a character fades every part of it. Note that this is per-part compositing, not a flattened group fade: where two parts of the same character overlap, the seam is visible mid-fade. That is the standard behaviour of a 2D scene graph and the right default; a true group fade needs the subtree rendered to a texture first, which costs a render pass per node per frame.
**This class’s field defaults are the single source of truth for a property’s rest value** — see
compile.py’s_PROPERTY_REST_VALUES, which is derived from them rather than restated.
- class an.stage.serialize.UnderlayJSON(**data)[source]#
Bases:
_JSONModelA copy of a node’s own visual, drawn BEHIND it in the same container (an#163 gap 5: the outline and the paper-gap shadow).
Being in the node’s container is the whole design: the copy takes every transform the node takes — tweens,
play, the camera — with no channel of its own, and a swap on the node re-textures its copies (runtime.jsapplySwap). One copy is drawn per entry ofoffsets(in the node’s own frame), each grown bygrowpixels: a rect/ellipse is redrawn with its geometry grown, an SVG sprite is scaled about its art’s centre so its box grows bygrowon every side.coloris the fill of a redrawn shape and thetint(a multiply) of a sprite copy. Compiled byan.stage.surface; never authored.
- class an.stage.serialize.VisualJSON(**data)[source]#
Bases:
_JSONModelDrawable content attached to a node.
kind="svg_sprite"is the Phase 11b path: the runtime instantiates aPIXI.Spritefrom a pre-loaded SVG texture identified byasset_id.asset_setscarries this node’s swap vocabulary —{set_name: {KEY: asset_id}}, the compiler’s per-slot projection of the descriptor’sasset_setsonto the slot this visual draws (an#87). A channel whose property names one of these sets swaps the sprite’s texture by key;visemeis just the conventional set name lip-sync uses. Same field name asCharacterDescriptor.asset_setson purpose: one vocabulary, two layers (descriptor = declared, wire = resolved to texture aliases). Replaces the mouth-onlyviseme_assets.width/heightare the box the art is fitted into, not the size it is forced to. Underfit="contain"the art keeps its own aspect ratio and may leave slack on one axis; that slack is the correct rendering, not a bug.- asset_geometry: dict[str, dict[str, float]] | None#
Per-texture geometry for a swap key drawn differently from the built one (an#211):
{asset_id: {"width", "height", "anchor_x", "anchor_y", "x", "y"}}— the box the key is fitted into, its anchor, and its offset from the node (scene pixels). A swap used to carry the texture only, so every key drew in the DEFAULT attachment’s box: a closed mouth on a thin canvas squashed every open mouth to a fraction of a pixel. Only keys whose geometry differs are listed;None= every key shares it.
- blend: Literal['add', 'multiply'] | None#
"add"for a glow,"multiply"for the paper grain. PixiJS 7 does both in the blend equation — no filter, no render texture.None= normal.- Type:
The engine’s native blend mode for this visual (an#163)
- fit: Literal['stretch', 'contain']#
How the art is fitted to
width/height."contain"scales uniformly so the art keeps the shape it was drawn with — the invariant of an#74."stretch"sizes each axis independently, which is what every sprite did before that issue and what distortedarm_lby 3.929x on the repo’s own art.Additive with a
"stretch"default so no stored scene changes meaning; the compiler emits"contain"for every sprite it builds.
- kind: str#
One of
BUILTIN_VISUAL_KINDS, or a kind a genre’s runtime script registers (window.anRegisterVisual, an#247). A string on the wire, as it always was; the open set is what letscutanship the mouth and eye.
- model_config: ClassVar[ConfigDict] = {'extra': 'allow', 'populate_by_name': True}#
Configuration for the model, should be a dictionary conforming to [
ConfigDict][pydantic.config.ConfigDict].
- underlays: list[UnderlayJSON] | None#
Copies drawn behind this visual, back to front (an#163) — the outline and the paper-gap shadow. Only
rect,ellipseandsvg_spritetake them; the runtime refuses any other kind.