Made for AI
A Stew Engine animation is plain JSON with a TypeScript type for every field. A person or an AI can read one, write one and check it.
Plain JSON
A clip is the JSON Stew Factory saves: a duration and a list of tracks. The page that plays it adds what a saved clip doesn't hold: pivots, and the paths and boxes some tracks need. Stew Engine reads it as it is. Fields it doesn't know are ignored, so the editor's own, like a keyframe's id, ride along.
Edit the clip below. Once you stop typing, the scene plays your data. The Try buttons make a few common changes for you.
Targets on this stage: ball, star and box. The ball and the box are groups: moves, fill and opacity show on them. The star is a path, so stroke, strokeWidth and morph show on it too. Try giving the box a track of its own.
import { compile, createPlayer, svgDrawer, type StewDoc } from "@stew/engine";
// the playground's own check, field by field, by the types below
import { readShape } from "./clipShape";
// the text, as data: text that isn't JSON throws here
const data: unknown = JSON.parse(text);
// the shape is yours to check: data of the wrong shape can make
// compile throw. This names the first field that doesn't fit.
const shape = readShape(data);
if ("problem" in shape) throw new Error(`${shape.problem.at || "the clip"} ${shape.problem.says}`);
const doc: StewDoc = shape.doc;
// read once: what it can't play is left out and noted in skipped
const program = compile(doc);
for (const note of program.skipped) console.warn(note);
// each target is written to the elements carrying its uid
const drawer = svgDrawer(program, uid =>
[...svg.querySelectorAll(`[data-stew-uid="${CSS.escape(uid)}"]`)]);
const player = createPlayer(program, drawer, { repeat: -1 });
player.play();Checking it
A clip goes through three checks on its way in, and each one catches something different.
JSON.parse(text)- Text that isn't JSON: a missing comma, a stray bracket. It throws, so catch it.
the shape- A field that's missing or of the wrong type, by the types below. Data of the wrong shape can make
compilethrow, so this check is yours to make. The playground's check names the first field that doesn't fit. compile(doc)- What the engine can't play: a property it doesn't play, a keyframe it can't read, a
motiontrack with no path, ananchortrack with no box. Each is left out with a note inprogram.skipped, and the rest still plays. An ease it doesn't know playspower1.inOut, with a note too. Fields it doesn't know are ignored, without a note.
The types
Every field above has a TypeScript type. These are printed from the engine's own source file, format.ts, with their comments.
- StewDoc
- StewPoint
- StewBox
- StewPath
- StewTrack
- StewKeyframe
- DEFAULT_EASE
// One clip: how long it is and what moves in it.
export interface StewDoc {
// the format version; absent = 1
version?: 1;
// seconds: the clip's length (one lap when it loops); Infinity = it
// never ends (a shape's loop actions: each track keeps its own cycle)
duration: number;
// repeat when it ends; absent = true, as in Stew
loop?: boolean;
tracks: StewTrack[];
// where each target turns, scales and skews about, in the user units of
// the node its transforms are written to. The host measures these (or
// bakes them into an export). Absent = that node's origin (0, 0).
pivots?: Record<string, StewPoint>;
// each target's own box, for an `anchor` track: its keyframes ("0 1" =
// the box's left bottom) say where it turns about from that moment on,
// in place of its pivot.
boxes?: Record<string, StewBox>;
// the path each target's `motion` track follows (Stew keeps it on the
// element, not in the clip, so the host hands it over).
paths?: Record<string, StewPath>;
}
export interface StewPoint { x: number; y: number }
// A target's own box, and the matrix (a, b, c, d, e, f) from its units into
// the node its transforms are written to (Stew: through its resting pose
// onto its animation layer; absent = the same units).
export interface StewBox {
x: number;
y: number;
width: number;
height: number;
matrix?: [number, number, number, number, number, number];
}
// A path to travel. A motion track's value is how far along it the target
// is: 0 its start, 1 its end, by distance. The target's `anchor` rides the
// path; with `rotate` it also turns to face the way it goes.
export interface StewPath {
// SVG path data, in the space it was drawn in (Stew: the artboard's)
d: string;
// turn to face the way it travels
rotate?: boolean;
// degrees added to that turn: which way the art itself faces
rotateOffset?: number;
// the point that rides the path, in the travelling node's own units;
// absent = its origin
anchor?: StewPoint;
// the matrix (a, b, c, d, e, f) from the space the travelling node moves
// in into the path's space - the host measures it; absent = the same
// space. Its turn is taken off the facing, so the target faces the way
// it goes as seen.
frame?: [number, number, number, number, number, number];
}
// One property of one target, keyed over time.
export interface StewTrack {
// which target: the host's name for it (Stew's data-stew-uid)
targetUid: string;
// what it animates: "x", "rotate", "fill"...
property: string;
// where it is at moments (any order: the engine sorts them)
keyframes: StewKeyframe[];
// seconds: the keys start over every `cycle` seconds, forever, whatever
// the clip's length (a loop action's own lap); absent = they don't
cycle?: number;
}
export interface StewKeyframe {
// seconds
time: number;
value: number | string;
// the ease of the stretch LEAVING this keyframe, toward the next one: a
// name ("power2.out", "back.out(1.7)") or "cubic-bezier(x1, y1, x2, y2)".
// Absent = DEFAULT_EASE.
easing?: string;
}
// The ease a keyframe with none leaves along: Stew's default.
export const DEFAULT_EASE = "power1.inOut";StewDoc, StewTrack, StewKeyframe- The clip, one property of one target, and one moment of it.
StewPoint, StewBox, StewPath- A pivot, a target's own box for
anchortracks, and a path formotiontracks. Program- A compiled clip: flat lists of numbers, and
skipped, what was left out and why. Pose- Everything one frame shows, in numbers.
Player, PlayerOptions, PlayerEvent- The player, its
clockandrepeatoptions, and the events it tells its listeners. Drawer, Bind, Channel- What draws a pose on the SVG, and how it finds the elements each target's channel is written to.
Stage, Saved- The artwork several players share, and how it looked before the engine first wrote to it.
Clock, ManualClock- Where time comes from: the page's frames, or a clock that moves only when told.
EaseFn- An ease as a function: progress in, how far along out.
Curves, Subpath, PathTable- A path in all-curve form, and a path measured for distances along it.
Chaser- A number that glides to where it's told.
SpringChaser- A number on a spring (
createSpring): told somewhere new, it keeps its speed. CursorParams, CursorTarget- Follow mouse and Run away on a shape.
DragParams, DragTarget- Drag on a shape.
NodeAction, NodeBendTarget, NodeTemplate- Pointer actions on single nodes of a path.
StartTrigger- What starts a clip:
load,click,hover,invieworscrollscrub. RuleTrigger, RuleEnd, RuleParams, RuleVerbs- A clip that plays on an event: what fires it, what a hold does when it ends, its settings, and what it does to its player.
Writing one
An AI writes a clip the way a person does: it fills in these types. There is no second format to learn: the engine plays the same shape Stew Factory saves its clips in.
Keep it small, name targets the way your SVG does, and read program.skipped after compile. A few things are easy to miss.
targetUidstringrequired- Your name for a target.
bindhands back the elements it's written to: on this page, every element whosedata-stew-uidmatches. A name no element carries is drawn nowhere, without a note. x, y- Moves from where the element is drawn: 0 is where it already is. The engine writes that element's whole
transform, so give it none of its own (wrap the art in a<g>). rotate, scale, scaleX, scaleY, skewX, skewY- Turn, stretch and skew about the target's pivot, angles in degrees. With no entry in
pivots, the pivot is the element's own (0, 0), not its middle. valuenumber | stringrequired- A number; a color as hex,
rgb(),hsl()or a name (none: no fill or stroke; notvar(...)orcurrentColor); path data, formorphandwarp; two box fractions like"0 1", foranchor; a gradient as JSON text, forfillGradient. easingstringdefault "power1.inOut"- The ease of the stretch leaving this keyframe, toward the next one: a name like
power2.outorback.out(1.7), orcubic-bezier(x1, y1, x2, y2). The last keyframe's is never used. keyframesStewKeyframe[]required- In any order: the engine sorts them. Before the first one a track holds its first value, after the last one its last (unless it has a
cycle: then they start over). durationnumberrequired- Seconds. A clip is never shorter than its last keyframe.
program.skippedstring[]- Read it after
compile: what was left out, and why.