Skip to content

Latest commit

 

History

History
561 lines (458 loc) · 15.8 KB

1.0.md

File metadata and controls

561 lines (458 loc) · 15.8 KB

motion.json format and options

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.

Minimal working project

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/motion

The result contains dist/motion/vp9.avl and dist/motion/build.json. Add more unique codec entries to encodings to publish a multi-codec bundle.

Top-level fields

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.

Identifiers and paths

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.

Canvas, frame rate, and alpha

canvas

{
  "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.

frameRate

{ "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 }.

alpha

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.

Sources

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.

Video source

{
  "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.

PNG-sequence source

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 and renditions

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.

H.264

{
  "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.

H.265

{
  "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.

VP9

{
  "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.

AV1

{
  "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.

VP9 and H.265 with alpha

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.

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.

Body unit and ports

{
  "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.

Bridge and one-shot units

{
  "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.

Reversible unit

{
  "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.

States, edges, and bindings

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.

States

{
  "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.

Edges

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"
}

Triggers

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.

Start policies

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.

Transitions

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.

Bindings

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:

  • activate
  • engagement.off
  • engagement.on
  • focus.in
  • focus.out
  • hidden
  • pointer.enter
  • pointer.leave
  • visible

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.

Validate and compile

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 --open

Compilation 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.