@clayzo/animation/authoring, run it, and it writes the file. The types make malformed documents hard to produce, and validation names the node and field when something is still wrong.
Agents with the authoring skill do all of this. This page is for reading what they produce, or for writing your own.
A complete script
Save asauthor.ts and run with npx tsx author.ts. It writes animations/bouncing-dot.json.
author.ts
The parts of a document
A node is only drawn if it’s reachable from
rootNodeIds. That is how a node used only as a clip path, a matte source or an effect input stays out of the frame.
Time is in ticks
ticksPerSecond is the document’s time base and 240 is conventional. frameRate is what it plays at. Every inTick, outTick and keyframe tick is in ticks. At 240 ticks per second and 30 fps a frame is 8 ticks, so keyframes off that grid land between frames and never display exactly. Author at the frame rate the piece will play at and snap keyframes to it.
Animatables
Every animated property has one of two shapes:interpolation is "hold", "linear", "bezier" or "spring". A bezier keyframe needs easing: { x1, y1, x2, y2 }; a spring keyframe needs spring: { mass, stiffness, damping }. Interpolation describes the segment leaving that keyframe, so the last keyframe should be "hold". A track’s first value holds before its first keyframe. Keyframe ids must be unique; derive them from the node id, the property and the index.
Colours are { r, g, b, a } with channels from 0 to 1, not 0 to 255.
Nodes
Every node has the same base fields:
id, name, visible, inTick, outTick, opacity, blendMode and a full transform with all five of position, anchor, scale, rotation and skew. The transform is position minus anchor, scaled, rotated (rotation.z in degrees, clockwise on screen), then translated. Child transforms compose with their parent’s.
Effects
Aneffect node names its effect and a parameters array, matched by name. Available effects:
blur, drop-shadow, depth-blur, color-matrix, brightness-contrast, glass, refraction, dithering, duotone, pixelation, mesh-gradient and custom-sksl for a shader you write.
Effects chain: an effect’s inputId may be another effect. glass and refraction read what is already drawn beneath them, so they must come later in rootNodeIds than what they refract. number and point parameters need minimum and maximum. A parameter name that doesn’t match is not an error; the effect uses its default. The authoring skill carries the full parameter table for every effect.
Things that bite
- Frames are transparent wherever nothing is drawn. Draw a full-canvas
rectfirst, or exports come out on black. - Node
opacitybelow 1 costs an offscreen layer per frame. Animatestyle.fill.opacityon solid shapes instead, and reserve nodeopacityfor groups that must fade as one. - Overlapping children at partial opacity double up unless the group has
isolation: true. - A composition needs
width,heightanddurationTicks, not onlynodesandrootNodeIds. - Write the file with
writeFileSync. A top-levelawaitfails undertsxin a CommonJS project, with an error that names esbuild rather than your script. - Text
wrapdefaults to wrapping inside the box. Setwrap: "none"on single lines. - A custom shader sees device pixels. Its coordinate and its
pointparameters arrive in device pixels;numberparameters arrive as written. A shader that measures anumberradius against apointcentre only looks right at one render size — a preview, a phone at 3× and an export all differ. Declare apointparameter with value{ x: 1, y: 1 }(it arrives as the render scale) and divide the device-pixel inputs by it, so the shader works in document units everywhere.
Validate and stamp, every time
clayzo review binds its findings to one, and the players and exporter treat two documents with the same revision as the same document. A script that writes a document without stamping it hands downstream tools a revision that doesn’t match the contents.
