motion.json is the input contract for
@pixel-point/aval-compiler. Project schema 1.0 describes one logical
animation, its source media, encoding targets, and optional state graph. The
compiler creates one AVAL wire-1.1 file for every requested codec plus a
build.json report.
The schema is exact: every top-level field is required, and unknown fields,
missing fields, duplicate IDs, invalid references, and project versions other
than 1.0 are rejected. Source paths are resolved relative to the directory
containing motion.json.
This project expects motion.mov to be a progressive, square-pixel, unrotated
1920×1080 video at a constant 30 fps with at least 120 frames. It compiles
frames 0 through 119 as a looping VP9 AVAL.
{
"projectVersion": "1.0",
"alpha": "auto",
"canvas": {
"width": 1920,
"height": 1080,
"fit": "contain",
"pixelAspect": [1, 1],
"colorSpace": "srgb"
},
"frameRate": { "numerator": 30, "denominator": 1 },
"sources": [
{
"id": "motion",
"type": "video",
"path": "motion.mov",
"timing": { "mode": "exact" }
}
],
"encodings": [
{
"codec": "vp9",
"deadline": "best",
"cpuUsed": 0,
"threads": 8,
"renditions": [
{ "id": "motion.1x", "width": 1920, "height": "auto", "crf": 40 }
]
}
],
"units": [
{
"id": "idle.body",
"kind": "body",
"source": "motion",
"range": [0, 120],
"playback": "loop",
"ports": []
}
],
"initialState": "idle",
"states": [{ "id": "idle", "bodyUnit": "idle.body" }],
"edges": [],
"bindings": []
}Compile it without installing the compiler into the project:
npx @pixel-point/aval-compiler compile motion.json --out dist/motionThe result contains dist/motion/vp9.avl and
dist/motion/build.json. Add more unique codec entries to encodings to
publish a multi-codec bundle.
All eleven fields are required, even when edges or bindings is empty.
| Field | Type | Accepted value or purpose |
|---|---|---|
projectVersion |
string | Exactly "1.0". |
alpha |
string | "auto", "opaque", or "packed". |
canvas |
object | Logical source canvas and aspect information. |
frameRate |
object | Exact project frame-rate fraction. |
sources |
array | 1–32 video or PNG-sequence sources. |
encodings |
array | 1–4 unique codec entries. |
units |
array | 1–96 independently decodable authored ranges. |
initialState |
identifier | ID of the state selected at startup. |
states |
array | 1–32 named graph states. |
edges |
array | 0–64 state transitions. |
bindings |
array | 0–32 element-event to graph-event mappings. |
Every ID and event name must match:
^[a-z][a-z0-9._-]{0,63}$
IDs therefore start with a lowercase ASCII letter and contain at most 64 lowercase letters, digits, dots, underscores, or hyphens.
Source paths must be confined relative local paths. Absolute paths, URLs,
Windows drive paths, empty path segments, . segments, and .. segments are
rejected. Moving a project directory with its media therefore preserves its
path relationships.
{
"width": 1920,
"height": 1080,
"fit": "contain",
"pixelAspect": [1, 1],
"colorSpace": "srgb"
}| Field | Accepted values |
|---|---|
width, height |
Positive integers no greater than 4294967295. |
fit |
"contain", "cover", "fill", or "none". |
pixelAspect |
A reduced positive [numerator, denominator]; each value is at most 10,000. Use [1, 1] for square pixels. |
colorSpace |
Exactly "srgb". |
Source media must preserve the canvas aspect ratio and be at least as large as the canvas; the compiler does not upscale it. Renditions may be smaller than the canvas while preserving the same aspect ratio.
{ "numerator": 30000, "denominator": 1001 }Both values are positive integers. denominator is at most 1001, the fraction
must be reduced, and the result must not exceed 60 fps. Common examples are
{ "numerator": 24, "denominator": 1 },
{ "numerator": 30, "denominator": 1 }, and
{ "numerator": 30000, "denominator": 1001 }.
| Value | Behavior |
|---|---|
"auto" |
Audits every referenced canonical source frame and selects opaque or packed-alpha output. |
"opaque" |
Requires every referenced canonical pixel to be opaque; compilation fails when transparency is found. |
"packed" |
Always emits packed-alpha video. Fully opaque input is allowed but produces a warning. |
Packed alpha works across AV1, VP9, H.265, and H.264 by storing color and alpha in one decoded video surface. Use a source that actually carries alpha, such as ProRes 4444 or RGBA PNGs.
Each source ID must be unique, every source must be referenced by at least one unit, and one project may contain at most 32 sources.
{
"id": "motion",
"type": "video",
"path": "media/motion.mov",
"timing": { "mode": "exact" }
}| Field | Accepted values |
|---|---|
id |
Unique identifier. |
type |
Exactly "video". |
path |
Confined relative path ending in .mov, .mp4, or .m4v. |
timing.mode |
"exact" or "normalize-hold". |
Use exact when the source is constant-frame-rate and already matches the
project frame rate. It fails rather than silently retiming mismatched or
variable-rate media. Use normalize-hold explicitly to map source timestamps
onto the project grid by holding the most recent source frame; duplicated and
dropped frame indexes are recorded as build warnings.
For files such as frames/frame-0000.png through
frames/frame-0119.png:
{
"id": "motion",
"type": "png-sequence",
"directory": "frames",
"prefix": "frame-",
"digits": 4,
"suffix": ".png",
"firstNumber": 0,
"frameCount": 120
}| Field | Accepted values |
|---|---|
directory |
Confined relative directory path. |
prefix |
Non-empty plain filename prefix without /, \\, or %. |
digits |
Integer from 1 through 12. |
suffix |
Exactly ".png". |
firstNumber |
Nonnegative integer. |
frameCount |
Positive integer. |
The last frame number must fit within the declared digit width. PNG sequences
use the project frameRate directly and are appropriate for lossless RGBA
source frames.
encodings contains one to four entries. codec is unique within the array
and accepts "av1", "vp9", "h265", or "h264". Array order controls
compiler and build.json output order only; browser preference remains AV1 →
VP9 → H.265 → H.264.
Every encoding owns one to four renditions:
{
"id": "motion.1x",
"width": 1920,
"height": "auto",
"crf": 40
}| Rendition field | Accepted values |
|---|---|
id |
Unique identifier within this encoding. |
width, height |
Positive integer no larger than the corresponding canvas dimension, or "auto" for exactly one dimension. |
crf |
H.264: 1..51; H.265: 0..51; VP9/AV1: 0..63. Lower values generally preserve more detail and produce larger files. |
Both dimensions cannot be "auto". Two explicit dimensions must preserve
the canvas aspect ratio. When one dimension is "auto", the compiler derives
a positive even value that preserves that ratio.
{
"codec": "h264",
"preset": "veryslow",
"renditions": [
{ "id": "motion.1x", "width": 1920, "height": "auto", "crf": 20 }
]
}preset is required. It accepts ultrafast, superfast, veryfast,
faster, fast, medium, slow, slower, veryslow, or placebo.
H.264 has no project-level threads field.
{
"codec": "h265",
"preset": "veryslow",
"threads": 8,
"renditions": [
{ "id": "motion.1x", "width": 1920, "height": "auto", "crf": 30 }
]
}threads is required and accepts 1..64. preset accepts the same values as
H.264 and may be omitted; authored projects default it to veryslow.
{
"codec": "vp9",
"deadline": "best",
"cpuUsed": 0,
"threads": 8,
"renditions": [
{ "id": "motion.1x", "width": 1920, "height": "auto", "crf": 40 }
]
}cpuUsed is required and accepts -8..8; lower values spend more work on
compression. threads is required and accepts 1..64. deadline accepts
best, good, or realtime and may be omitted; authored projects default it
to best.
{
"codec": "av1",
"bitDepth": 10,
"cpuUsed": 0,
"tiles": { "columns": 4, "rows": 2 },
"rowMt": true,
"threads": 32,
"renditions": [
{ "id": "motion.1x", "width": 1920, "height": "auto", "crf": 15 }
]
}| Field | Accepted values |
|---|---|
bitDepth |
8 or 10. |
cpuUsed |
Integer 0..8; 0 is the slowest, quality-focused mode. |
tiles.columns, tiles.rows |
Power-of-two integer from 1 through 64; their product must not exceed 64. |
rowMt |
Boolean enabling or disabling row multithreading. |
threads |
Integer 1..64. |
Set top-level alpha to "auto" to pack alpha only when transparency is
found, or to "packed" to require packed-alpha output. The two codecs can be
requested together:
{
"alpha": "packed",
"encodings": [
{
"codec": "vp9",
"deadline": "best",
"cpuUsed": 0,
"threads": 8,
"renditions": [
{ "id": "motion.4k", "width": 3840, "height": "auto", "crf": 40 }
]
},
{
"codec": "h265",
"preset": "veryslow",
"threads": 8,
"renditions": [
{ "id": "motion.4k", "width": 3840, "height": "auto", "crf": 30 }
]
}
]
}This is a fragment: keep the other required top-level project fields from the minimal example. The canvas must be at least as large as the rendition.
Compression fields are closed structured controls. Arbitrary FFmpeg arguments,
audio, filters, MP4/WebM tags, and container options such as faststart are not
accepted in motion.json because AVAL stores validated elementary video units.
A unit selects a non-empty half-open source range [start, end). For example,
[0, 120] includes frames 0 through 119. Units are independently decodable;
prediction does not cross unit boundaries.
All unit variants require id, kind, source, and range.
| Kind | Additional fields | Purpose |
|---|---|---|
body |
playback, ports |
Resident state animation. |
bridge |
none | Locked transition animation. |
reversible |
residency |
Forward/reverse transition retained around both endpoints. |
one-shot |
none | Optional startup animation for the initial state. |
{
"id": "idle.body",
"kind": "body",
"source": "motion",
"range": [0, 120],
"playback": "loop",
"ports": [
{ "id": "default", "entryFrame": 0, "portalFrames": [0, 30, 60, 90] }
]
}playback accepts "loop" or "finite". A body has 0–16 unique ports.
entryFrame is exactly 0; portalFrames is a non-empty array of unique frame
offsets within the body range. The compiler sorts those offsets canonically.
{
"id": "idle.engaged.bridge",
"kind": "bridge",
"source": "motion",
"range": [120, 150]
}Change kind to "one-shot" for a unit used as the optional initial animation
of the initial state.
{
"id": "engage.shift",
"kind": "reversible",
"source": "motion",
"range": [120, 150],
"residency": {
"endpoints": [
{ "state": "idle", "port": "default", "frames": 6 },
{ "state": "engaged", "port": "default", "frames": 6 }
]
}
}endpoints contains exactly two distinct state/port pairs. frames is an
integer from 6 through 12. A reversible unit must be referenced by exactly two
edges; every other unit must be referenced exactly once.
For a single loop, use one state and leave edges and bindings empty. For
interactive motion, states reference body units and edges describe legal
routes between them.
{
"id": "idle",
"bodyUnit": "idle.body"
}Every state requires a unique id and a bodyUnit that references a body
unit. A state may also contain "initialUnit": "intro"; it must reference a
one-shot unit and is allowed only on the project selected by initialState.
Every edge requires id, from, to, start, and continuity. trigger is
optional. Non-cut edges may also contain transition.
{
"id": "idle.engaged",
"from": "idle",
"to": "engaged",
"trigger": { "type": "event", "name": "control.engage" },
"start": {
"type": "portal",
"sourcePort": "default",
"targetPort": "default",
"maxWaitFrames": 12
},
"transition": {
"kind": "locked",
"unit": "idle.engaged.bridge"
},
"continuity": "exact-authored"
}| Shape | Meaning |
|---|---|
{ "type": "event", "name": "control.engage" } |
Route when the named event is sent. |
{ "type": "completion" } |
Route when the current finite body completes. |
An omitted trigger creates an edge that can still be selected explicitly by the graph API.
| Type | Required fields |
|---|---|
portal |
sourcePort, targetPort, and nonnegative maxWaitFrames. |
finish |
targetPort and nonnegative maxWaitFrames. |
cut |
targetPort and maxWaitFrames: 1. The edge also requires continuity: "cut" and targetRunwayFrames from 6 through 12. |
Portal and finish edges use continuity: "exact-authored" or
"exact-reverse". A cut edge cannot contain a transition.
| Kind | Required fields |
|---|---|
locked |
unit, referencing a bridge unit. |
reversible |
unit, referencing a reversible unit; direction is "forward" or "reverse"; optional reverseOf identifies the paired edge. |
The graph validator additionally checks port consistency, reverse-pair consistency, transition unit kinds, and the legal combination of trigger, start, transition, and continuity policies.
A binding turns an element-owned interaction or lifecycle signal into an event trigger used by an edge:
{
"source": "pointer.enter",
"event": "control.engage"
}source accepts exactly:
activateengagement.offengagement.onfocus.infocus.outhiddenpointer.enterpointer.leavevisible
Each binding source may appear at most once, and event must match the name of
an event trigger used by an edge.
See states and triggers and preparing video and authoring states for complete interactive examples. The checked-in idle/engaged project demonstrates paired reversible edges and bindings.
The scoped package exposes the avl executable directly through npx:
npx @pixel-point/aval-compiler compile motion.json --out dist/motion
npx @pixel-point/aval-compiler inspect dist/motion/vp9.avl
npx @pixel-point/aval-compiler validate dist/motion/vp9.avl
npx @pixel-point/aval-compiler dev motion.json --out dist/motion --openCompilation requires caller-installed FFmpeg and FFprobe with the encoders
requested by the project. Use --ffmpeg <absolute-path> and
--ffprobe <absolute-path> to select explicit binaries, --force to replace an
existing bundle directory atomically, --json for machine-readable CLI output,
and --media-timeout-ms <positive-integer> only when the build environment
needs a wall-clock limit for each media operation.
See the compiler guide for output structure and operational details.