Sprite binaries
A sprite asset-generation run draws through a drawing binary on its PATH. That
binary is the only channel for making a mark. Two binaries share one drawing
implementation, built from crates/draw:
drawdraws a single sprite: one 32-bit RGBA canvas.draw-sheetdraws a sprite sheet: one separate image per frame. It isdrawplus a required--frame <index>on every drawing operation, and it adds keyframe animation of layers.
Each binary has its own asset-generation
run-container image: draw in
test-cabinet-sprite (asset_kind = "sprite") and draw-sheet in
test-cabinet-sprite-sheet (asset_kind = "sprite-sheet"), so a sprite
asset-generation run carries only the tool its asset_kind names. Both
full-stack images carry both binaries, because
one full-stack run produces every asset its game needs. After the run, core
regenerates each frame from its recorded log through the same library and
compares the result to the model’s preview. See
Evaluation.
Drawing operations
Section titled “Drawing operations”A case seeds no operations schema. The drawing vocabulary is the binary’s own
--help, and the brief tells the model to read it:
draw --help # every operationdraw fill-rect --help # one operation's exact flagsEach operation is a subcommand with flags:
draw fill-rect --x 28 --y 28 --width 8 --height 1 --color "#ff4ec7"draw fill-circle --cx 20 --cy 16 --r 8 --color "#c46bff"draw mirror-horizontal --axis-x 32The operations are fill-background, set-pixel, fill-rect, stroke-rect,
line, fill-circle, stroke-circle, flood-fill, and mirror-horizontal.
Coordinates are signed, so a shape may be placed partially off-canvas and the
off-canvas portion is clipped. Sizes and radii are unsigned. Colors are
#rrggbb or #rrggbbaa. An operation replaces the pixels it touches, so the
recorded log regenerates to an exact, order-only image.
Every drawing operation also accepts --layer <name>, which redirects it onto a
registered layer instead of the canvas itself.
Layers
Section titled “Layers”An operation paints straight onto the canvas by default, where it is indistinguishable from everything already drawn. A layer is a separate, independently positioned surface, painted once and then placed, so the pieces of a sprite stay separable.
draw register-layer --name ball --x 10 --y 6 --width 12 --height 12draw fill-circle --layer ball --cx 6 --cy 6 --r 5 --color "#c46bff"A layer carries its own extent. --width/--height give that extent and
--x/--y place its top-left corner on the canvas. A small layer paints only
where it sits, and anything falling outside the canvas is clipped. Coordinates
in an operation carrying --layer are layer-local, so --cx 6 --cy 6 above is
the centre of the 12×12 layer rather than of the canvas.
Layers composite on top of the canvas log: first the operations drawn directly,
then each layer in --z order, ties broken by registration order. A layer
composites source-over, so its transparent pixels let what is underneath show
through and a partly transparent color blends. That is what makes layers
stackable.
Registration also sets the layer’s resting transform, each property of which is animatable:
| Flag | Meaning | Default |
|---|---|---|
--x, --y | top-left corner on the canvas | required |
--width, --height | the layer’s own extent | required |
--z | composite order, low to high | 0 |
--opacity | 0 (invisible) to 255 (opaque) | 255 |
--rotation | whole degrees clockwise, about the layer’s centre | 0 |
--scale-x, --scale-y | percent, 100 = actual size | 100 |
Rotation and scale resample nearest-neighbour about the layer’s centre, which keeps the result crisp and stair-steps at angles that are not multiples of 90°. Every value is an integer and every transform runs in fixed point, so a regenerated image is bit-identical to the preview the model was shown.
Three more subcommands keep a layer editable while the model iterates:
draw list-layers # every layer, its transform, and its op countdraw clear-layer --name ball # discard its drawing ops, keep it registereddraw remove-layer --name ball # remove the layer entirelyAnimating layers
Section titled “Animating layers”draw-sheet adds keyframes on any layer transform property, so a shape painted
once moves across the sheet’s frames without being redrawn per frame. This is
the tool for motion that is awkward to hand-place frame by frame, such as an
arc, an overshoot, or a spin.
draw-sheet animate-layer --layer ball --property x --frame 0 --value 2draw-sheet animate-layer --layer ball --property x --frame 11 --value 50 --interp linear--property is one of x, y, opacity, rotation, scale-x, scale-y,
and --value is that property’s integer value at --frame. A property with no
keyframes stays at the value register-layer gave it. Before the first keyframe
it holds the first value, and after the last it holds the last.
clear-keyframes --layer <name> [--property <p>] drops keyframes and returns
those properties to their resting values.
Each keyframe’s --interp sets how the curve leaves it, using the same F-curve
vocabulary as the voxel tools:
constantholds the value until the next key, which is the right choice for snapping between poses.lineardraws a straight line to it.bezier, the default, draws a smooth curve shaped by tangent handles.--handle-out <dframes,dvalue>on this key and--handle-in <dframes,dvalue>on the next are each an offset from their own key. Omit them for a smooth auto tangent.ease-instarts slow and accelerates into the next key,ease-outstarts fast and decelerates, andease-in-outeases both ends.
Curved paths
Section titled “Curved paths”A path is curved when x and y are shaped differently. Animating both
linearly produces a straight line, which is the usual reason hand-built motion
looks robotic. To throw a ball in an arc, let x travel at a constant rate
while y decelerates up and accelerates back down:
# x: steady left-to-right across the whole sheetdraw-sheet animate-layer --layer ball --property x --frame 0 --value 2 --interp lineardraw-sheet animate-layer --layer ball --property x --frame 11 --value 50
# y: rises to a peak at frame 5, slowing as it goes, then falls away fasterdraw-sheet animate-layer --layer ball --property y --frame 0 --value 40 --interp ease-outdraw-sheet animate-layer --layer ball --property y --frame 5 --value 8 --interp ease-indraw-sheet animate-layer --layer ball --property y --frame 11 --value 40A slow rotation track over the same span makes the ball tumble as it flies.
Layer content
Section titled “Layer content”A layer’s painted content is shared by every frame: it is painted once and the
keyframes place it. Put on a layer whatever moves without changing shape, and
draw directly into a frame’s own log whatever changes shape between frames. A
bouncing ball is a layer; a character’s leg mid-stride is per-frame art. Most
sheets want both, and a layer operation needs no --frame precisely because it
applies to all of them.
draw-sheet register-layer --name ball --x 2 --y 40 --width 12 --height 12draw-sheet fill-circle --layer ball --cx 6 --cy 6 --r 5 --color "#c46bff" # no --framedraw-sheet fill-rect --frame 3 --x 0 --y 60 --width 64 --height 4 --color "#222"Layers, their content, and their keyframes live in a sheet-wide layers.json
alongside the per-frame action logs. It is seeded empty and, like the logs, it
is authoritative: the reviewed image is regenerated from both together.
Recording and preview
Section titled “Recording and preview”Each operation appends itself to the run’s action log and re-renders the preview
from the whole log, so the recorded log is the single source of truth and the
preview reflects it. The orchestrator seeds a draw.config.json next to the
workspace giving the canvas size, background, and the log, preview, and layer
paths, so an operation needs no canvas flags. A model reads the preview between
calls to judge its progress.
draw init # write an empty log and a blank preview (a run starts pre-seeded)draw render --actions <log> --out <png> --width <w> --height <h> # regenerate a logrender reproduces the finished image: the action log with every layer
composited over it, which is what the preview shows and what the run is
scored on. It reads the seeded
layers.json on its own.
Two flags narrow that, for checking one piece of the work:
draw render --actions <log> --out <png> --width <w> --height <h> \ --only-layer head # composite only this layer (repeatable)draw render ... --no-layers # the log alone, with nothing compositedLive preview
Section titled “Live preview”A run driven by a driver is watched, and the model’s drawing is streamed to the viewer in real time so a person sees the sprite take shape operation by operation.
The intermediate frames live inside the run container and the binary’s stdout is
mediated by the harness, so the orchestrator opens a TCP listener on the run
host and adds a live block to the seeded draw.config.json:
{ "width": 64, "height": 64, "background": "transparent", "actions": "actions.json", "preview": "canvas.png", "layers": "layers.json", "live": { // the run host, reachable from the container as host.docker.internal "endpoint": "host.docker.internal:54123", "token": "…", // an opaque per-run token echoed with each frame },}After each operation the binary connects back and streams the freshly rendered
frame: a one-line JSON header ({ token, frame, operation, operationCount, length }) followed by the frame’s raw PNG bytes. The container is given a route
to the host with --add-host host.docker.internal:host-gateway, which both
Docker and Podman resolve to a host-reachable address. The listener validates
the token, decodes the frame, and relays it to the viewer over the run’s
existing live channel. For a sprite sheet each frame carries its own index, so
the viewer shows the most-recently-drawn frame and the status of every frame at
once.
Streaming is best-effort. It is absent for an unwatched run, which seeds no
live block, a drawing operation succeeds whether or not the listener responds,
and the frames are never recorded. The recorded action log remains the run’s
authoritative output, and the reviewed image is always
regenerated from it.
Sprite sheets: one file per frame
Section titled “Sprite sheets: one file per frame”A sprite sheet’s frames are separate files rather than regions of one image.
draw-sheet adds a required --frame <index> selecting which frame an
operation draws into. That frame has its own action log and its own preview,
both {frame} templates the case declares, for example
frames/{frame}.actions.json and frames/{frame}.png. Coordinates are within
the frame.
draw-sheet --help # same operations, plus --framedraw-sheet fill-circle --frame 0 --cx 20 --cy 16 --r 8 --color "#c46bff"draw-sheet init # initialize every declared framedraw-sheet render --actions <log> --out <png> --width 32 --height 32 --frame 4draw-sheet render takes --frame so it knows which frame to resolve the
layers’ keyframes at. It picks up the layers themselves on its own, as draw
does.
The seeded draw.config.json lists the declared frame indices and the {frame}
templates, so draw-sheet init initializes every frame and each operation
resolves its frame’s files. Which frames exist and the animation sequences are
declared in the case’s [sheet] table. See
Sprite cases.
layers.json is the one artifact that is not per-frame. Layers and their
keyframes are sheet-wide, which is what lets one painted layer move across
frames. A layer operation therefore takes no --frame, and changing a layer
re-renders every frame’s preview at once.