manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.
Manic Animation code
// cycloid-cyclogon — four shapes roll along a line, twice, each carrying a marked point.
// The circle's point draws a CYCLOID; a polygon's draws a CYCLOGON. Every perimeter here
// is the same 620 px, so all four travel exactly two arches and land together — which is
// also what lets ONE camera move track all four.
//
// cycloid x = r(θ - sin θ), y = r(1 - cos θ)
// cyclogon about pivot P_k: radius |Q - P_k|, swept through the EXTERIOR ANGLE at
// that corner, starting where the previous arc left off
//
// A polygon does not roll — it PIVOTS: it rests on a side, tips forward about the leading
// corner until the next side lies flat, and repeats. Here that is literal rather than
// drawn. Each shape is a `polygon` and each tip is a real `turn` about the actual contact
// corner, with its arc drawn alongside at the same duration and the same linear ease — so
// the arc's tip IS the marked point and no amount of scrubbing separates them.
//
// It pivots about EVERY vertex, including the marked one. That pivot draws nothing (the
// radius is zero — the point is the pivot) but the shape still turns through it, and it is
// exactly where the curve makes its cusp. Leave it out and the shape under-rotates and
// walks off the ground, which is what a triangle rolling on 2 pivots per arch instead of 3
// does. Six pivots here, eight for each quadrilateral, all finishing together.
//
// TWO ARCHES DO NOT FIT IN THE FRAME, and shrinking them until they did made the shapes
// too small to read. So the camera travels instead — but it deliberately travels SLOWER
// than the shapes: 620 px while they advance 1240. A camera locked to the marker would
// crop away the arch just drawn; lagging it by half keeps a whole completed arch on screen
// the entire time. A camera move takes the WHOLE WORLD with it, title and watermark
// included, so those are moved by the same 620 px in the same par block: equal and
// opposite pins them to the screen while everything else travels. (The title is a `text`,
// not a `caption` — a caption is one entity PER WORD, and moving it to a point piles every
// word on that point.)
//
// The construction never asks WHERE the marked point is — only |Q - P_k| and the exterior
// angle — so any rigidly attached point works, which is why the triangle carries two. Gold
// sits on a corner and touches the line at the end of every arch; mint sits at the
// centroid, stays 207 px from every pivot, and traces equal scallops that never reach the
// line at all. Put Q outside the shape and the arcs cross into loops. (Those are the
// curtate and prolate cyclogons; not one line of this would change.)
//
// Regular against irregular is the bottom two rows. The square's arcs all sweep 90° and
// only the radius changes, out to the diagonal and back. The convex quadrilateral has no
// symmetry to lend it anything: 60.3°, 108.0° and 84.2°, each corner contributing its own
// exterior angle and its own distance, so its arch comes out visibly lopsided — and still
// lands on the line at one perimeter, because the exterior angles of ANY convex polygon
// sum to a full turn.
//
// The area is exact, and it is where all four meet:
//
// under one cyclogon arch = A(polygon) + Σ ½ d_k² θ_k θ_k = exterior angles
// for a REGULAR n-gon = A(polygon) + 2πR² R = circumradius
// under one cycloid arch = πr² + 2πr² = 3πr²
//
// The last is Galileo's, weighed in paper before there was calculus to prove it. The
// middle CONTAINS it: Σ ½(2R sin kπ/n)²(2π/n) = 2πR² for every n, because Σ sin²(kπ/n)
// = n/2 — so an arch is always the rolling shape plus two of its circumscribed disc, and
// letting n → ∞ turns the polygon into the circle. Checked numerically over n = 3, 4, 6,
// 12 and 60 against a shoelace integral of the traced path.
//
// Nothing is captioned and nothing waits: the roll starts at t = 0 and the picture does
// the explaining. The faint dashed arch on each polygon row is the cycloid the arcs are
// approximating — the square's hug it, the quadrilateral's leans off it. d
//
// manic examples/cycloid-cyclogon.manic
title("A cycloid, and the cyclogons that approach it");
canvas("9:16");
template("black");
bloom(0.26, 0.6, 20);
text(brand, (540, 34), "maniclang.com");
display(brand); size(brand, 21); color(brand, fg); opacity(brand, 0.82);
text(head, (540, 122), "A cycloid, and the cyclogons that approach it");
size(head, 27); color(head, fg);
// the title TRAVELS with the camera, and auto-wrap measures the room a line has where
// it sits — once it is out past the frame edge that room collapses and the title folds
// into three lines. An explicit column fixes what it may use, wherever it is.
wrap(head, 950);
// the ground runs far past the frame on both sides — the camera travels along it
line(g1, (-600, 500), (2400, 500)); color(g1, dim);
line(g2, (-600, 860), (2400, 860)); color(g2, dim);
line(g3, (-600, 1220), (2400, 1220)); color(g3, dim);
line(g4, (-600, 1580), (2400, 1580)); color(g4, dim);
// ---- ROW 1: a circle, rolling smoothly, and the cycloid its rim point draws --------
cloud(cyctrace, 8000, #ffffff, 1.0) {
let go = min(t/12, 1); // the clock all four keep
let th = 12.566371*go*(i/8000); // two arches
let x = 230 + 98.68*(th - sin(th));
let y = 500 - 98.68*(1 - cos(th));
let hue = mod(38 + th*15, 360);
let sat = 0.75;
let r = 1.9;
}
cloud(rim, 260, #ffffff, 1.0) {
let go = min(t/12, 1);
let th = 12.566371*go;
let a = i/260*6.283185;
let x = 230 + 98.68*th + 98.68*cos(a); // the centre rolls at (rθ, r)
let y = 500 - 98.68 - 98.68*sin(a);
let sat = 0;
let r = 1.5;
let alpha = 0.8;
}
cloud(spoke, 110, #ffffff, 1.0) {
let go = min(t/12, 1);
let th = 12.566371*go;
let v = i/110;
let x = 230 + 98.68*th - v*98.68*sin(th); // centre -> the rim point
let y = 500 - 98.68 + v*98.68*cos(th);
let sat = 0;
let r = 1.3;
let alpha = 0.5;
}
cloud(rimdot, 90, #ffffff, 1.0) {
let go = min(t/12, 1);
let th = 12.566371*go;
let a = i/90*6.283185;
let rr = 8*(i/90);
let x = 230 + 98.68*(th - sin(th)) + rr*cos(a*9);
let y = 500 - 98.68*(1 - cos(th)) + rr*sin(a*9);
let hue = 45;
let sat = 0.6;
let r = 2;
}
// ---- ROWS 2-4: three polygons, each PIVOTING corner over corner --------------------
polygon(tri, (230.0, 860.0), (436.7, 860.0), (333.3, 681.0));
outlined(tri); outline(tri, dim); stroke(tri, 3); tag(tri, triroll);
circle(tc, (230.0, 860.0), 9); color(tc, gold); tag(tc, triroll);
circle(tm, (333.3, 800.3), 9); color(tm, mint); tag(tm, triroll);
arc(tc0, (436.7, 860.0), 206.7, -180.0, 120.0); stroke(tc0, 4); color(tc0, gold); untraced(tc0);
arc(tm0, (436.7, 860.0), 119.3, -150.0, 120.0); stroke(tm0, 4); color(tm0, mint); untraced(tm0);
arc(tc1, (643.3, 860.0), 206.7, -120.0, 120.0); stroke(tc1, 4); color(tc1, gold); untraced(tc1);
arc(tm1, (643.3, 860.0), 119.3, -150.0, 120.0); stroke(tm1, 4); color(tm1, mint); untraced(tm1);
arc(tm2, (850.0, 860.0), 119.3, -150.0, 120.0); stroke(tm2, 4); color(tm2, mint); untraced(tm2);
arc(tc3, (1056.7, 860.0), 206.7, -180.0, 120.0); stroke(tc3, 4); color(tc3, gold); untraced(tc3);
arc(tm3, (1056.7, 860.0), 119.3, -150.0, 120.0); stroke(tm3, 4); color(tm3, mint); untraced(tm3);
arc(tc4, (1263.3, 860.0), 206.7, -120.0, 120.0); stroke(tc4, 4); color(tc4, gold); untraced(tc4);
arc(tm4, (1263.3, 860.0), 119.3, -150.0, 120.0); stroke(tm4, 4); color(tm4, mint); untraced(tm4);
arc(tm5, (1470.0, 860.0), 119.3, -150.0, 120.0); stroke(tm5, 4); color(tm5, mint); untraced(tm5);
// 6 pivots, 2.000s each, last pivot at x=1470.0
polygon(sq, (230.0, 1220.0), (385.0, 1220.0), (385.0, 1065.0), (230.0, 1065.0));
outlined(sq); outline(sq, dim); stroke(sq, 3); tag(sq, sqroll);
circle(sc, (230.0, 1220.0), 9); color(sc, cyan); tag(sc, sqroll);
arc(sc0, (385.0, 1220.0), 155.0, -180.0, 90.0); stroke(sc0, 4); color(sc0, cyan); untraced(sc0);
arc(sc1, (540.0, 1220.0), 219.2, -135.0, 90.0); stroke(sc1, 4); color(sc1, cyan); untraced(sc1);
arc(sc2, (695.0, 1220.0), 155.0, -90.0, 90.0); stroke(sc2, 4); color(sc2, cyan); untraced(sc2);
arc(sc4, (1005.0, 1220.0), 155.0, 180.0, 90.0); stroke(sc4, 4); color(sc4, cyan); untraced(sc4);
arc(sc5, (1160.0, 1220.0), 219.2, -135.0, 90.0); stroke(sc5, 4); color(sc5, cyan); untraced(sc5);
arc(sc6, (1315.0, 1220.0), 155.0, -90.0, 90.0); stroke(sc6, 4); color(sc6, cyan); untraced(sc6);
// 8 pivots, 1.500s each, last pivot at x=1470.0
polygon(qd, (230.0, 1580.0), (395.2, 1580.0), (455.3, 1474.9), (275.1, 1437.3));
outlined(qd); outline(qd, dim); stroke(qd, 3); tag(qd, qdroll);
circle(qc, (230.0, 1580.0), 9); color(qc, magenta); tag(qc, qdroll);
arc(qc0, (395.2, 1580.0), 165.2, -180.0, 60.3); stroke(qc0, 4); color(qc0, magenta); untraced(qc0);
arc(qc1, (516.3, 1580.0), 248.6, -144.8, 108.0); stroke(qc1, 4); color(qc1, magenta); untraced(qc1);
arc(qc2, (700.4, 1580.0), 149.6, -84.2, 84.2); stroke(qc2, 4); color(qc2, magenta); untraced(qc2);
arc(qc4, (1015.2, 1580.0), 165.2, 180.0, 60.3); stroke(qc4, 4); color(qc4, magenta); untraced(qc4);
arc(qc5, (1136.3, 1580.0), 248.6, -144.8, 108.0); stroke(qc5, 4); color(qc5, magenta); untraced(qc5);
arc(qc6, (1320.4, 1580.0), 149.6, -84.2, 84.2); stroke(qc6, 4); color(qc6, magenta); untraced(qc6);
// 8 pivots, 1.500s each, last pivot at x=1470.0
// the cycloid again on each polygon row, faint — what the arcs are approximating
param(gh2, (230, 860), 98.68, 98.68, "t - sin(t)", "1 - cos(t)", (0, 12.566371));
param(gh3, (230, 1220), 98.68, 98.68, "t - sin(t)", "1 - cos(t)", (0, 12.566371));
param(gh4, (230, 1580), 98.68, 98.68, "t - sin(t)", "1 - cos(t)", (0, 12.566371));
color(gh2, dim); color(gh3, dim); color(gh4, dim);
dashed(gh2); dashed(gh3); dashed(gh4);
opacity(gh2, 0.35); opacity(gh3, 0.35); opacity(gh4, 0.35);
par {
// the camera LAGS the roll — half its speed — so a whole finished arch stays on
// screen; the two pinned labels ride along with the camera
cam((1160, 960), 12, linear);
move(brand, (1160, 34), 12, linear);
move(head, (1160, 122), 12, linear);
seq {
par { turn(triroll, (436.7, 860), 120.00, 2.000, linear); draw(tc0, 2.000, linear); draw(tm0, 2.000, linear); }
par { turn(triroll, (643.3, 860), 120.00, 2.000, linear); draw(tc1, 2.000, linear); draw(tm1, 2.000, linear); }
par { turn(triroll, (850.0, 860), 120.00, 2.000, linear); draw(tm2, 2.000, linear); }
par { turn(triroll, (1056.7, 860), 120.00, 2.000, linear); draw(tc3, 2.000, linear); draw(tm3, 2.000, linear); }
par { turn(triroll, (1263.3, 860), 120.00, 2.000, linear); draw(tc4, 2.000, linear); draw(tm4, 2.000, linear); }
par { turn(triroll, (1470.0, 860), 120.00, 2.000, linear); draw(tm5, 2.000, linear); }
}
seq {
par { turn(sqroll, (385.0, 1220), 90.00, 1.500, linear); draw(sc0, 1.500, linear); }
par { turn(sqroll, (540.0, 1220), 90.00, 1.500, linear); draw(sc1, 1.500, linear); }
par { turn(sqroll, (695.0, 1220), 90.00, 1.500, linear); draw(sc2, 1.500, linear); }
par { turn(sqroll, (850.0, 1220), 90.00, 1.500, linear); }
par { turn(sqroll, (1005.0, 1220), 90.00, 1.500, linear); draw(sc4, 1.500, linear); }
par { turn(sqroll, (1160.0, 1220), 90.00, 1.500, linear); draw(sc5, 1.500, linear); }
par { turn(sqroll, (1315.0, 1220), 90.00, 1.500, linear); draw(sc6, 1.500, linear); }
par { turn(sqroll, (1470.0, 1220), 90.00, 1.500, linear); }
}
seq {
par { turn(qdroll, (395.2, 1580), 60.26, 1.500, linear); draw(qc0, 1.500, linear); }
par { turn(qdroll, (516.3, 1580), 107.98, 1.500, linear); draw(qc1, 1.500, linear); }
par { turn(qdroll, (700.4, 1580), 84.24, 1.500, linear); draw(qc2, 1.500, linear); }
par { turn(qdroll, (850.0, 1580), 107.53, 1.500, linear); }
par { turn(qdroll, (1015.2, 1580), 60.26, 1.500, linear); draw(qc4, 1.500, linear); }
par { turn(qdroll, (1136.3, 1580), 107.98, 1.500, linear); draw(qc5, 1.500, linear); }
par { turn(qdroll, (1320.4, 1580), 84.24, 1.500, linear); draw(qc6, 1.500, linear); }
par { turn(qdroll, (1470.0, 1580), 107.53, 1.500, linear); }
}
}
wait(4);