Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Shapes, points and colors

Points

point(x, y) makes a point, which also serves as a vector. Points add, subtract, negate, and multiply or divide by a number. Read the parts with .x and .y.

let p = point(1.0, 2.0) + RIGHT * 3.0;   // point(4, 2)
let mid = (p + ORIGIN) / 2.0;
print(mid.y);                             // 1.0

Colors

FunctionExample
Named constantsWHITE, BLACK, BLUE, RED, GREEN, YELLOW, GREY, ORANGE, PINK, PURPLE, TRANSPARENT
rgb(r, g, b)rgb(1.0, 0.5, 0.0), each 0–1
hex(rgb)hex(0xf4d345)
with_alpha(c, a)BLUE.with_alpha(0.5)

Shapes

All shapes are centered on the origin unless they take points.

FunctionShape
circle(radius)Circle
square(side)Square
rectangle(width, height)Rectangle
ellipse(width, height)Ellipse
arc(radius, start, sweep)Arc, sweep radians counter-clockwise from start
line(a, b)Line from point a to point b
dot(p)Small filled white dot at p
polygon([p1, p2, ...])Closed polygon through the points
dashed_line(a, b)Dashed line from a to b, dashes about 0.05 long
dashed_line(a, b, dash_length)The same, dashes about dash_length long
function_graph(f, x0, x1, segments)Graph of y = f(x) for x in x0..x1, in scene units
let wave = function_graph(|x| x.sin(), -PI, PI, 32);

union(a, b), intersection(a, b), difference(a, b) (a without b) and exclusion(a, b) (one or the other, not both) make a new shape from the areas two shapes cover, in the default style.

let lens = intersection(circle(1.0).shift(LEFT * 0.5), circle(1.0).shift(RIGHT * 0.5));

Styling

Style methods return a new shape, so they chain.

MethodEffect
.fill(color)Fill color
.fill(color, opacity)Fill color and opacity, 0–1
.fill_opacity(opacity)Fill opacity, keeping the color or gradient
.stroke(color, width)Outline color and width
.stroke_opacity(opacity)Outline opacity, keeping the color or gradient
.fill_gradient([c1, c2, ...])Fill with a linear gradient from left to right
.fill_gradient([c1, c2, ...], dir)The same, across the shape along dir, e.g. UP
.stroke_gradient([c1, c2, ...])Outline with a linear gradient, left to right; dir as for fills
.line_cap(cap)Ends of open outlines: "round" (default), "butt" or "square"
.line_join(join)Corners: "round" (default), "miter" or "bevel"
.dashed()The outline cut into 15 dashes, each half its period
.dashed(n) / .dashed(n, ratio)n dashes, each ratio (0–1) of its period
.shader(wgsl)Paint the fill with a WGSL shader; see Shader fills
.z_index(n)Draw order; higher draws on top
.rotate(angle)Rotate by angle radians
let s = scene.add(square(2.0).fill(BLUE, 0.5).stroke(WHITE, 0.04).rotate(PI / 4.0));
let warm = scene.add(circle(1.0).fill_gradient([YELLOW, ORANGE, RED], UP));
let ring = scene.add(circle(1.5).stroke(WHITE, 0.1).stroke_gradient([BLUE, PINK]).dashed(24));

.fill and .stroke replace a gradient with a solid color, so set a gradient after them. A gradient spans the shape’s box when it is set and then moves, scales and rotates with the shape; transform blends one gradient into another, or into a solid color. Fading changes its opacity like a solid color’s.

Dashes are geometry: a dashed shape is many short open pieces, so create draws it dash by dash, and a fill of a dashed shape fills each piece on its own. An open outline starts and ends on a dash; a closed one repeats evenly around.

Texts take .fill(color, opacity) and .fill_gradient(...) too, the gradient running across the whole text. To color a group or a text item by item instead, use .set_color_by_gradient([c1, c2, ...]): each shape or glyph, first to last, gets the next color along the stops, keeping its fill and outline opacities.

let title = scene.add(text("fastanim").fill_gradient([BLUE, GREEN]));
let dots = scene.add(group([dot(LEFT), dot(ORIGIN), dot(RIGHT)]).set_color_by_gradient([RED, BLUE]));

Positioning

These methods work on shapes, texts, axes and groups alike.

MethodEffect
.shift(v)Move by vector v
.move_to(p)Move the center to p
.scale(k)Scale about the center
.to_edge(dir)Move against a frame edge, e.g. UP
.next_to(other, dir)Place beside other (a shape, text, group or point, not a mobject handle), DEFAULT_BUFF apart
.next_to(other, dir, buff)The same, buff apart
.align_to(other, dir)Line up the dir edge with other’s
.scale_to_fit_height(h)Scale about the center to be h tall
.scale_to_fit_width(w)Scale about the center to be w wide
.center()The bounding-box center, as a point
.width(), .height()The bounding-box size
let box = square(1.0);
let label = text("box").next_to(box, DOWN);

Images

image(path) loads a PNG or JPEG file (relative to the script’s directory), and image(rows) builds one from pixels: an array of rows, top row first, each pixel a grey value or [r, g, b] or [r, g, b, a], all from 0 to 255. Like manim’s ImageMobject, an image is sized so that one 1080 pixels tall would fill the frame’s height; scale_to_fit_height sets it directly.

An image is a shape: a rectangle with the pixels drawn inside, and no outline. It moves, scales, rotates, fades and groups like any other shape, and .stroke(color, width) gives it a border. create fades it in rather than drawing it. .pixelated() keeps pixels sharp when scaled up, instead of smoothing them.

let photo = scene.add(image("photo.png").scale_to_fit_height(4.0));
let ramp = [];
for i in 0..256 { ramp.push(i); }
let rows = [];
for row in 0..16 { rows.push(ramp); }
let bar = scene.add(image(rows).pixelated().next_to(scene.get(photo), DOWN));
scene.play(rotate(group([photo, bar]), PI / 8.0));

transform between two images moves one onto the other, and switches pixels at the end; use fade_out and fade_in to cross-fade. Image files can’t be loaded in the browser playground yet, but images built from pixels can.

Lottie animations

lottie(path) loads a Lottie file: the JSON that After Effects’ Bodymovin plugin, LottieFiles and many design tools export. It’s sized like an image (a composition 1080 pixels tall fills the frame’s height) and positioned like a group: .scale_to_fit_height, .next_to, .rotate and the rest place its whole frame. .at_frame(f) picks the frame it shows before it plays, the first by default, and .duration is its length in seconds.

Added, it is a group with one shape per fill and stroke in the file, which every group animation (fade_in, shift, scale, indicate, …) works on. play_lottie(m) plays it through once at its own frame rate. Its options are start and end frames (playing backwards if end comes first) and loops, and .run_time speeds it up or slows it down:

let anim = scene.add(lottie("bounce.json").scale_to_fit_height(5.0));
scene.play(fade_in(anim));
scene.play(play_lottie(anim, #{loops: 2}));
scene.play([shift(anim, LEFT * 3.0), play_lottie(anim, #{end: 30.0}).run_time(2.0)]);

play_lottie follows the group, so animations that move, fade or reorder it can play alongside it. They must come before play_lottie in the list, or the Lottie’s own frames overwrite them.

Lottie features drawn: shape, solid, null and precomposition layers, with parenting, time stretch and time remapping; groups, paths, rectangles, ellipses, stars and polygons; fills, strokes and trim paths; eased, held and spatial keyframes. Gradients are drawn in the average of their colours, and rounded star corners are drawn sharp. Masks, mattes, repeaters, dashes, line caps and joins, image layers and text layers are not drawn yet. Like image files, Lottie files can’t be loaded in the browser playground yet.

Groups

group([...]) puts shapes, texts, axes and other groups together, like manim’s VGroup. A group positions as one, and .arrange(dir) (or .arrange(dir, buff)) lays its items out in a row, keeping the group’s center.

let row = group([square(1.0), circle(0.5), text("x")]).arrange(RIGHT).to_edge(UP);
let g = scene.add(row);
scene.play(fade_in(g));
scene.play(rotate(g, PI));      // turns about the group's center
scene.play(indicate(g[1]));     // members by index; g[-1] is the last

scene.add(group) returns a group mobject with one member per item, in order. Every animation that takes a mobject takes a group too, except those that need one shape (transform, update, apply_function, always, scene.get) or one text (transform_diff). g.len() is the number of members.

group([a, b]) of mobjects already added makes a group mobject from them, to play one animation on all of them:

scene.play(fade_out(group([title, g])));

Axes

axes(x_range, y_range) makes manim-style axes, 12 by 6 units. Ranges are [min, max] or [min, max, step]; axes(x_range, y_range, width, height) sets the size.

fit_axes(f, x_range) (or fit_axes(f, x_range, width, height)) picks the y range for you: it samples f across x_range and rounds out to about 8 ticks of 1, 2 or 5 × 10ⁿ. manim has no equivalent.

MethodReturns
ax.c2p(x, y)Graph coordinates to a scene point
ax.p2c(p)Scene point to graph coordinates
ax.plot(f)Shape: graph of f across the whole x range
ax.plot(f, x0, x1)Shape: graph of f in graph coordinates
ax.plot(f, x0, x1, segments)The same in segments cubics instead of 64, for wiggly graphs
ax.vertical_line(p)Shape: line from the x-axis up to p
ax.numbers()Text: tick numbers
ax.labels(x, y)Text: axis labels, as Typst math
ax.shape()Shape: the axes themselves, with arrow tips (filled: recolor with .stroke(c, w).fill(c))

scene.add(ax) adds the axes as one shape.

let ax = axes([-3, 3], [-1.5, 1.5, 0.5]);
scene.play(create(scene.add(ax)));
scene.add(ax.numbers());
let graph = scene.add(ax.plot(|x| x.sin(), -3.0, 3.0).stroke(YELLOW, 0.05));
scene.play(create(graph));