Scenes¶
A scene is JSON. The Python API builds the same models, so everything on this page applies to both. buttery
schema prints the full JSON schema, generated from the pydantic models with Scene.model_json_schema().
{
"duration": 5, "fps": 60, "size": [1920, 1080], "background": "#111111", "view_width": 8,
"objects": [
{"id": "dot", "type": "circle", "r": 0.3, "fill": "coral",
"x": {"op": "tween", "keys": [[0, -3], [1.5, 3]], "ease": "out_cubic"},
"y": "0.2 * sin(6*t)"},
{"id": "ring", "type": "circle", "fill": null, "stroke": "white", "x": "dot.x", "r": "dot.r + 0.5"},
{"id": "label", "type": "text", "content": "Hello", "y": -1.6, "size": 0.4, "fill": "#dddddd"}
]
}
Only duration is required. The defaults are 60 fps, 1920×1080, a near-black background, and a view 8 units
wide.
Coordinates¶
World units, origin at the center, y up. The frame is view_width units wide, so with the default of 8,
x runs from -4 to 4 and at 16:9 y runs from -2.25 to 2.25. size and view_width are the only link between
world units and pixels, and they live on the scene, not on the objects. Angles are degrees, counter-clockwise.
Colors are hex (#ff7f50) or CSS names (coral).
Primitives¶
Every object needs a unique id (global, including inside groups) and accepts opacity. Properties marked
static are plain values; every other property is animatable and accepts an expression.
| type | properties |
|---|---|
| circle | x y r fill stroke stroke_width |
| rect | x y w h corner_radius rotation fill stroke stroke_width (x, y is the center) |
| line | x1 y1 x2 y2 stroke stroke_width |
| text | x y size fill · static: content font align max_width line_height. Newlines in content start new lines; max_width wraps at words. |
| code | x y size fill · static: content font char_width line_height theme spans. A monospace block with its top-left at (x, y) on a fixed grid. |
| span | only inside code.spans: fill background opacity · static selectors: line token nth chars |
| group | x y rotation scale children. Children are transformed with the group; their ids are still global. |
fill and stroke accept a color, null (not drawn), or a tween between colors.
Expressions¶
Any numeric property accepts:
| form | example |
|---|---|
| a number | 1.5 |
| time | "t" |
| a reference to another object's property | "dot.x" |
| a shorthand string | "0.2 * sin(6*t)", "dot.r + 0.5" |
| an op node | {"op": "mul", "args": [0.2, {"op": "sin", "args": [{"op": "mul", "args": [6, "t"]}]}]} |
| a tween | {"op": "tween", "keys": [[0, -3], [1.5, 3]], "ease": "out_cubic"} |
Shorthand strings are parsed into the same op tree, never evaled. The parser knows pi and tau.
Ops: add sub mul div neg sin cos abs min max clamp smoothstep noise.
clamp(x, lo, hi)andsmoothstep(edge0, edge1, x)follow the GLSL conventions.noise(x)is smooth 1D noise in roughly -1..1; a second argument,noise(x, 3), picks a different channel so two objects do not wobble in sync.
Tween: {"op": "tween", "keys": [[t, v], ...], "ease": "..."}. Keys are sorted by time. The value holds
at the first key before it and at the last key after it, so a tween is also a way to say "hold, then go".
Color properties tween between colors: "keys": [[1, "#4a90d9"], [1.5, "coral"]].
Eases: linear in_quad out_quad in_out_quad out_cubic in_out_cubic spring.
References form a dependency graph across objects. A ring can follow a dot with "x": "dot.x", and an edge
can join two nodes with "x1": "a.x", "y1": "a.y", "x2": "b.x", "y2": "b.y". Only numeric properties can be
referenced, and cycles are rejected at validation.
Code¶
A code object is a monospace block anchored at its top-left (x, y); size is the font size. Characters
sit on a fixed grid, column c of row r at (x + c*char_width*size, y - r*line_height*size) (defaults 0.6
and 1.4), so layout never depends on the font. Use spaces, not tabs. theme: "default" syntax-highlights
Python; a mapping from token kind to color (keyword, name, builtin, string, number, comment) is a
custom theme.
Parts of the snippet are selected with spans, each with an id and animatable fill, background, and
opacity:
{"id": "fn", "type": "code", "size": 0.34, "theme": "default", "content": "def f(x):\n return x + 1",
"spans": [
{"id": "kw", "token": "return", "fill": "coral"},
{"id": "arg", "token": "x", "nth": -1, "background": "#2b2b3d"},
{"id": "l1", "line": 1, "opacity": {"op": "tween", "keys": [[0.5, 0], [1, 1]]}},
{"id": "call", "line": 2, "chars": [11, 16], "fill": "#e0a54a"}
]}
line: n(1-based) selects a whole line.token: "x"selects the nth Python token with exactly that text (nthfrom 0, negative from the end). Addlineto search only that line. Onlytokenneeds the content to be valid Python.chars: [a, b]is a half-open range of offsets into the snippet, or of columns whenlineis given.- Span
opacitymultiplies the block's,fillreplaces it, andbackgroundpaints the cells. Later spans win where fills overlap. buttery stateshows each span's resolvedstartandend, so you can check what a selector picked.
Validation¶
Two layers, both run by buttery validate and by loading a scene in Python:
- Structural, from pydantic: unknown fields are rejected, op arity is checked, colors must parse, ids must be well formed.
- Semantic: ids are unique across the whole scene, references resolve to numeric animatable properties,
there are no dependency cycles, and tween keys match the property kind (numbers for numeric properties,
colors for
fillandstroke).
Errors are objects, not stack traces:
{"ok": false, "errors": [
{"path": "objects[1].x", "message": "reference 'dott.x': no object with id 'dott'",
"object": "ring", "property": "x", "t": null}
]}
Evaluation and rendering¶
- Each expression is compiled to a closure once.
state(t)walks the objects in dependency order and never mutates the scene, so every frame is independent. - Motion blur samples
samplessub-frames spread acrossshutter× the frame interval and averages them. The default, 8 samples at 0.5, is a 180° shutter. - Frames render in parallel on every core and stream to ffmpeg for
.mp4and.mov, or to a directory as numbered PNGs.
Recipes¶
Values are numeric properties unless noted.
- Fade in at 0.5 s over 0.4 s:
"opacity": {"op": "tween", "keys": [[0.5, 0], [0.9, 1]], "ease": "out_quad"} - Enter from the left, settle with overshoot:
"x": {"op": "tween", "keys": [[0.2, -5], [1.0, -1.5]], "ease": "spring"} - Stagger: the same tween on several objects, keys shifted by 0.15 s each.
- Hold then go:
"keys": [[0, 0], [1.2, 0], [2, 3]]. - Multi-stage path:
"keys": [[0, -3], [1, 0], [2, 0], [3, 3]](the flat segment holds). - Bounce (continuous):
"y": "-0.5 + 0.9 * abs(sin(4*t))" - Squash on landing (rect):
"h": "0.8 - 0.3 * smoothstep(0.9, 1.0, abs(cos(4*t)))" - Pulse:
"r": "0.3 + 0.05 * sin(tau * t)" - Wobble:
"rotation": "8 * noise(2*t)"; decorrelate a second object withnoise(2*t, 3). - Orbit: put children on a
groupand set"rotation": "90*t"on the group. - Follow:
"x": "ball.x", or lag it:"x": "ball.x - 0.3". - Color change:
"fill": {"op": "tween", "keys": [[1, "#4a90d9"], [1.5, "coral"]], "ease": "in_out_quad"} - Realistic bounce: sum parabola windows that are zero outside their interval, each arc
4*h*u*(1-u)withu = clamp((t - start)/dur, 0, 1), and decayhanddurper bounce. Seeexamples/squash_bounce.py.
Taste¶
- Ease almost everything.
out_cubicfor arrivals,in_out_cubicfor point-to-point,springsparingly. - Leave air: start motion at 0.3 to 0.5 s, finish 0.5 s before the end.
- Motion blur is on by default. Fast crosses of the frame in under 0.5 s read well because of it.
- Stroke widths around 0.03 to 0.06 units; text size 0.35 to 0.5 for labels, up to 0.8 for titles.
- A dark background (
#0e0e11,#111111) with one warm accent (coral) and one cool (#4a90d9) is a safe palette. - Keep ids short and meaningful (
ball,shadow,title); errors quote them.