Ship

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.

0.0 / 2.0 s
Try

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.

It plays. Nothing left out.
play.ts
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 compile throw, 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 motion track with no path, an anchor track with no box. Each is left out with a note in program.skipped, and the rest still plays. An ease it doesn't know plays power1.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.

Read from the engine's format.ts
  • StewDoc
  • StewPoint
  • StewBox
  • StewPath
  • StewTrack
  • StewKeyframe
  • DEFAULT_EASE
format.ts
// 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";
Every type the engine exports
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 anchor tracks, and a path for motion tracks.
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 clock and repeat options, 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, inview or scrollscrub.
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. bind hands back the elements it's written to: on this page, every element whose data-stew-uid matches. 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; not var(...) or currentColor); path data, for morph and warp; two box fractions like "0 1", for anchor; a gradient as JSON text, for fillGradient.
easingstringdefault "power1.inOut"
The ease of the stretch leaving this keyframe, toward the next one: a name like power2.out or back.out(1.7), or cubic-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.
NextExports