Text & UI
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
text-guide
How text works, demonstrated. The one primitive every scene uses, and the one with the most
behaviour behind it: what a bare label does, what happens to a line too long for where it sits
(it wraps to the room it has — it is never clipped), how align pins an edge, what leading
does to line height, the two different ways to run text downwards (rot turns the line and takes
the letters with it; vertical stacks them upright), why caption exists beside text at all
— one entity per word, so karaoke can time them — and how hue colours a label, or a caption
one word at a time. Each section shows the call beside the result, so the guide is its own worked
example.
// How text works — a guide you can watch
//
// Text is the one primitive every scene uses, and the one with the most behaviour hiding behind it.
// This is that behaviour, demonstrated: what a bare `text` does, what happens to a line too long for
// where it sits, how to pin an edge, how to set line height, the two different ways to run text
// downwards, and why `caption` exists beside `text` at all.
//
// Each section shows the call and the result side by side, so the guide is also its own worked
// example — read the source next to the frame it produces.
title("how text works");
canvas("16:9");
template("black");
text(brand, (640, 26), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
text(head, (640, 74), "How text works");
size(head, 34);
bold(head);
color(head, fg);
// ── 1 · the default ──
text(s1, (640, 150), "1 · a bare label is one line, centred on its point");
size(s1, 20);
color(s1, gold);
hidden(s1);
text(c1, (640, 200), "text(hello, (640, 300), \"one line\", 30)");
size(c1, 19);
color(c1, dim);
hidden(c1);
text(hello, (640, 300), "one line");
size(hello, 30);
color(hello, fg);
hidden(hello);
dot(anchor1, (640, 300), 4);
color(anchor1, coral);
hidden(anchor1);
// ── 2 · too long for its room ──
text(s2, (640, 150), "2 · a line too long for its room wraps — it is never clipped");
size(s2, 20);
color(s2, gold);
hidden(s2);
text(c2, (640, 200), "no wrap set: the renderer uses the room the point actually has");
size(c2, 19);
color(c2, dim);
hidden(c2);
text(longone, (400, 320), "This label sits left of centre, so it can only use twice its distance to the near edge — and that is exactly what it wraps to.");
size(longone, 21);
color(longone, cyan);
hidden(longone);
rect(room, (400, 320), 760, 130);
outlined(room);
outline(room, dim);
stroke(room, 1);
hidden(room);
text(c2b, (640, 470), "wrap(id, 300) chooses a narrower column than the frame allows");
size(c2b, 19);
color(c2b, dim);
hidden(c2b);
text(narrow, (980, 340), "A column you chose yourself, three hundred pixels wide.");
size(narrow, 19);
color(narrow, magenta);
wrap(narrow, 300);
hidden(narrow);
// ── 3 · which edge is pinned ──
text(s3, (640, 150), "3 · align pins an edge to the point");
size(s3, 20);
color(s3, gold);
hidden(s3);
line(rule3, (640, 230), (640, 560));
color(rule3, dim);
stroke(rule3, 1);
untraced(rule3);
text(al, (640, 270), "align(id, left)\nboth lines start here");
size(al, 21);
color(al, cyan);
align(al, left);
hidden(al);
text(ac, (640, 390), "align(id, center)\nthe default");
size(ac, 21);
color(ac, fg);
hidden(ac);
text(ar, (640, 510), "align(id, right)\nboth lines end here");
size(ar, 21);
color(ar, coral);
align(ar, right);
hidden(ar);
// ── 4 · line height ──
text(s4, (640, 150), "4 · leading is line height, as a multiple of the size");
size(s4, 20);
color(s4, gold);
hidden(s4);
text(l1, (300, 380), "leading\n1.0\ntight");
size(l1, 22);
color(l1, cyan);
leading(l1, 1.0);
hidden(l1);
text(l2, (640, 380), "default\n1.4\nprose");
size(l2, 22);
color(l2, fg);
hidden(l2);
text(l3, (980, 380), "leading\n2.2\nairy");
size(l3, 22);
color(l3, magenta);
leading(l3, 2.2);
hidden(l3);
// ── 5 · downwards, two different ways ──
text(s5, (640, 150), "5 · downwards: rotate the line, or stack the letters");
size(s5, 20);
color(s5, gold);
hidden(s5);
text(c5a, (300, 250), "rot(id, -90)");
size(c5a, 19);
color(c5a, dim);
hidden(c5a);
text(rotated, (300, 420), "ROTATED", 34);
color(rotated, cyan);
rot(rotated, -90);
hidden(rotated);
text(c5b, (640, 250), "rot(id, 90)");
size(c5b, 19);
color(c5b, dim);
hidden(c5b);
text(rotated2, (640, 420), "ROTATED", 34);
color(rotated2, teal);
rot(rotated2, 90);
hidden(rotated2);
text(c5c, (980, 250), "vertical(id)");
size(c5c, 19);
color(c5c, dim);
hidden(c5c);
text(stacked, (980, 420), "UPRIGHT", 34);
color(stacked, gold);
vertical(stacked);
hidden(stacked);
text(s5b, (640, 610), "the letters turn with the line, or stay the right way up — that is the whole difference");
size(s5b, 17);
color(s5b, fg);
hidden(s5b);
// ── 6 · caption is text per word ──
text(s6, (640, 150), "6 · caption makes one entity PER WORD, so words can be timed");
size(s6, 20);
color(s6, gold);
hidden(s6);
text(c6, (640, 210), "text(id, …) is one entity · caption(id, …) is {id}.w0, {id}.w1, …");
size(c6, 19);
color(c6, dim);
hidden(c6);
text(oneunit, (640, 320), "one entity, moves and fades as a whole");
size(oneunit, 22);
color(oneunit, cyan);
hidden(oneunit);
caption(perword, "one entity per word so each can be timed", (640, 440), 22, gold);
hidden(perword);
text(s6b, (640, 560), "which is what karaoke and wordpop need — and why a caption cannot wrap");
size(s6b, 17);
color(s6b, fg);
hidden(s6b);
// ── 7 · colour by hue, and per word ──
text(s7, (640, 150), "7 · hue colours text by angle — and a caption word by word");
size(s7, 20);
color(s7, gold);
hidden(s7);
text(c7, (640, 210), "hue(id, degrees, [saturation], [lightness]) · to(id, hue, 320, 2) cycles it");
size(c7, 19);
color(c7, dim);
hidden(c7);
text(h1, (330, 300), "hue(h1, 200)", 26);
hue(h1, 200);
hidden(h1);
text(h2, (950, 300), "hue(h2, 40, 0.9, 0.55)", 26);
hue(h2, 40, 0.9, 0.55);
hidden(h2);
caption(spectrum, "one hue per word across a caption", (640, 420), 26, fg);
for i in 0..6 { hue(spectrum.w{i}, i*54, 0.95, 0.62); }
hidden(spectrum);
text(s7b, (640, 530), "a caption is words, so each word can take its own colour");
size(s7b, 17);
color(s7b, fg);
hidden(s7b);
text(s7c, (640, 585), "gradients are shapes-and-strokes only — text is tinted, not filled");
size(s7c, 18);
color(s7c, dim);
hidden(s7c);
// ═══════════════════════════════════════════════════════════════════
// the run
// ═══════════════════════════════════════════════════════════════════
wait(0.6);
// 1 · the default
par { show(s1, 0.5); show(c1, 0.4); }
par { show(hello, 0.5); show(anchor1, 0.3); }
wait(2.4);
// 2 · overflow
par { fade(s1, 0.4); fade(c1, 0.4); fade(hello, 0.4); fade(anchor1, 0.3); }
par { show(s2, 0.5); show(c2, 0.4); }
par { show(room, 0.5); show(longone, 0.6); }
wait(2.6);
par { show(c2b, 0.4); show(narrow, 0.6); }
wait(2.6);
// 3 · alignment
par { fade(s2, 0.4); fade(c2, 0.4); fade(c2b, 0.4); fade(room, 0.4); fade(longone, 0.4); fade(narrow, 0.4); }
show(s3, 0.5);
draw(rule3, 0.6);
show(al, 0.5);
show(ac, 0.5);
show(ar, 0.5);
wait(2.6);
// 4 · leading
par { fade(s3, 0.4); fade(rule3, 0.4); fade(al, 0.4); fade(ac, 0.4); fade(ar, 0.4); }
show(s4, 0.5);
par { show(l1, 0.5); show(l2, 0.5); show(l3, 0.5); }
wait(2.6);
// 5 · downwards
par { fade(s4, 0.4); fade(l1, 0.4); fade(l2, 0.4); fade(l3, 0.4); }
show(s5, 0.5);
par { show(c5a, 0.4); show(rotated, 0.5); }
par { show(c5b, 0.4); show(rotated2, 0.5); }
par { show(c5c, 0.4); show(stacked, 0.5); }
wait(0.6);
show(s5b, 0.5);
wait(2.6);
// 6 · caption
par {
fade(s5, 0.4); fade(c5a, 0.4); fade(c5b, 0.4); fade(c5c, 0.4);
fade(rotated, 0.4); fade(rotated2, 0.4); fade(stacked, 0.4); fade(s5b, 0.4);
}
par { show(s6, 0.5); show(c6, 0.4); }
par { show(oneunit, 0.5); pulse(oneunit); }
wait(0.8);
show(perword, 0.4);
karaoke(perword, 3.2);
wait(0.4);
show(s6b, 0.5);
wait(2.4);
// 7 · hue
par { fade(s6, 0.4); fade(c6, 0.4); fade(oneunit, 0.4); fade(perword, 0.4); fade(s6b, 0.4); }
par { show(s7, 0.5); show(c7, 0.4); }
par { show(h1, 0.5); show(h2, 0.5); }
wait(0.5);
show(spectrum, 0.5);
wait(0.6);
par { show(s7b, 0.4); show(s7c, 0.4); }
to(h1, hue, 320, 2.4);
wait(2.6);
typewriter
Text revealed character by character.
// Typewriter — text revealed letter by letter (Manim's AddTextLetterByLetter),
// then removed letter by letter (RemoveTextLetterByLetter). `type` animates the
// text's `trace` 0->1 (a fraction of characters shown); `erase` runs it back.
// Declare the text `untraced` so it starts hidden, then `type` reveals it.
//
// manic examples/typewriter.manic
title("Typewriter");
canvas("16:9");
text(head, (cx, 150), "text, one letter at a time");
display(head); color(head, cyan); size(head, 26); hidden(head);
// untraced = trace 0 = no characters shown yet
text(line1, (cx, 320), "the quick brown fox"); color(line1, lime); size(line1, 44); untraced(line1);
text(line2, (cx, 400), "jumps over the lazy dog"); color(line2, cyan); size(line2, 44); untraced(line2);
cursor(line2); // this line types with a trailing cursor
// --- script ---
show(head, 0.5);
type(line1, 1.6); // AddTextLetterByLetter
type(line2, 1.6); // AddTextLetterByLetter, with a cursor
wait(1.0);
section("...and back");
erase(line2, 1.0); // RemoveTextLetterByLetter
erase(line1, 1.0);
wait(0.8);
captions
Karaoke / word-pop caption modes.
// Captions — karaoke word highlighting and TikTok-style word pop-in. `caption`
// lays out a phrase's words in a centred row (as {id}.w0, {id}.w1, ... tagged
// {id}.words); `karaoke` highlights them in sequence; `wordpop` pops them in one
// at a time.
//
// manic examples/captions.manic
// manic examples/captions.manic --record out --fps 60
title("Captions");
canvas("16:9");
text(head, (cx, 110), "word-by-word: karaoke + pop-in");
display(head); color(head, cyan); size(head, 26); hidden(head);
// karaoke: starts dim, words light up in sequence
caption(kara, "follow the bouncing highlight", (cx, 280), 46, dim);
// word-pop: hidden first, then each word pops in
caption(pop, "each word pops right in", (cx, 440), 50, lime);
hidden(pop.words);
// --- script ---
show(head, 0.5);
section("Karaoke");
karaoke(kara, 0.34, cyan);
wait(0.8);
section("Word pop");
wordpop(pop, 0.14);
wait(1.6);
terminal_boot
The neon terminal template booting up.
// Terminal Boot — a fake boot sequence typed out line by line, ending at a live
// prompt with a blinking-style cursor. Shows off the `cursor` modifier, `type`
// typewriter reveal, an author-set `masthead`, and the `terminal` template.
//
// manic examples/terminal_boot.manic
// manic examples/terminal_boot.manic --record out --fps 60
title("manic");
canvas("16:9");
template("terminal");
masthead("manic ~ %", "READY"); // your own header text (no engine branding)
text(l1, (cx, 210), ""); color(l1, lime); size(l1, 24);
text(l2, (cx, 260), ""); color(l2, cyan); size(l2, 24); hidden(l2);
text(l3, (cx, 310), ""); color(l3, cyan); size(l3, 24); hidden(l3);
text(l4, (cx, 360), ""); color(l4, lime); size(l4, 24); hidden(l4);
text(prompt, (cx, 440), ""); color(prompt, fg); size(prompt, 28); display(prompt);
hidden(prompt); cursor(prompt); // only the live prompt gets the cursor
// --- boot log ---
say(l1, "> initializing manic engine", 0.1);
type(l1, 1.0);
show(l2, 0.2);
say(l2, " loaded kits: std math geo algo brand", 0.1);
type(l2, 1.3);
show(l3, 0.2);
say(l3, " timeline: deterministic @ 60fps", 0.1);
type(l3, 1.0);
show(l4, 0.2);
say(l4, " ready.", 0.1);
type(l4, 0.5);
// --- the prompt, awaiting input ---
show(prompt, 0.2);
say(prompt, "manic ~ % render my_idea", 0.1);
type(prompt, 1.2);
wait(1.6);
brace
The curly-brace family.
// Braces — label spans and parts with curly braces, manic's Brace / BraceLabel
// / BraceBetweenPoints. A length is split into two parts a and b; a brace under
// each names it, and a brace over the whole names the sum. Every brace here is
// a BraceBetweenPoints (two points + a depth); bracelabel adds the text.
//
// manic examples/brace.manic
// manic examples/brace.manic --record out --fps 60
title("Braces");
canvas(1280, 720);
text(head, (640, 120), "label a span, or its parts");
display(head); color(head, cyan); size(head, 28); hidden(head);
text(cap, (640, 620), ""); color(cap, dim); size(cap, 24);
// the length, split at x = 680
line(seg, (300, 360), (980, 360)); color(seg, fg); stroke(seg, 3); untraced(seg);
dot(dl, (300, 360)); dot(dm, (680, 360)); dot(dr, (980, 360));
color(dl, magenta); color(dm, lime); color(dr, cyan);
hidden(dl); hidden(dm); hidden(dr);
// braces under the two parts, and over the whole (bulges up: points go R->L)
bracelabel(ba, (300, 392), (680, 392), "a", 30); color(ba, magenta);
bracelabel(bb, (680, 392), (980, 392), "b", 30); color(bb, cyan);
bracelabel(bt, (980, 320), (300, 320), "a + b", 34); color(bt, lime);
hidden(ba); hidden(bb); hidden(bt);
// --- reveal ---
show(head, 0.5);
say(cap, "here is a length");
draw(seg, 0.7);
par { show(dl, 0.3); show(dr, 0.3); }
wait(0.3);
section("Two parts");
say(cap, "split it at a point into parts a and b");
show(dm, 0.3);
show(ba, 0.5);
show(bb, 0.5);
wait(0.5);
section("The whole");
say(cap, "the whole span is a + b");
show(bt, 0.6);
par { pulse(ba.label); pulse(bb.label); pulse(bt.label); }
wait(1.4);
banner
The manic logo / banner reveal.
// The manic banner & watermark (à la ManimBanner). "create" draws the icon
// trio on; "expand" reveals the wordmark; the watermark persists in the corner.
//
// manic examples/banner.manic
// manic examples/banner.manic --record out --fps 60
title("manic");
canvas(1280, 720);
banner(logo, (600, 360), 1.1);
untraced(logo.icon); // icon shapes drawn on
hidden(logo.word); // wordmark revealed on "expand"
// a persistent, screen-fixed watermark, bottom-right
watermark(wm, (1120, 690), "manic // synthwave");
text(cap, (640, 560), ""); color(cap, dim); size(cap, 22);
// --- create: trace the icon trio on (staggered) ---
say(cap, "create");
stagger(0.2) {
draw(logo.dot, 0.6);
draw(logo.sq, 0.6);
draw(logo.tri, 0.6);
}
par { pulse(logo.dot); pulse(logo.sq); pulse(logo.tri); }
wait(0.4);
// --- expand: reveal the wordmark ---
say(cap, "expand");
show(logo.word, 0.6);
wait(1.2);
// --- unwrite: fade the whole banner ---
say(cap, "");
par { fade(logo.icon, 0.5); fade(logo.word, 0.5); }
wait(0.8);
equation
Display-quality LaTeX for fractions, roots, sums, powers and integrals, with semantic colour and template tinting.
// LaTeX math typesetting via `equation(...)`. Put the LaTeX in BACKTICKS so the
// backslashes survive. Rendered white-on-transparent and tinted by the entity
// colour, so `color(...)` and `template(...)` both work.
title("Equations");
canvas("16:9");
equation(quad, (cx, 150), `x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}`, 68);
color(quad, cyan);
equation(sum, (cx, 340), `\sum_{k=1}^{n} k = \frac{n(n+1)}{2}`, 60);
equation(euler, (cx, 500), `e^{i\pi} + 1 = 0`, 64);
color(euler, gold);
equation(intg, (cx, 640), `\int_0^1 x^2\,dx = \tfrac{1}{3}`, 56);
color(intg, lime);
inline-math
Inline LaTeX mixed with ordinary prose, including wrapped explanatory text and a standalone display equation.
// Inline LaTeX everywhere: wrap math in `$…$` (backtick raw string) inside ANY
// text/caption/kit label. Plain text (no `$`) is unchanged. Mixed lines wrap.
title("Inline math");
canvas("16:9");
text(t1, (cx, 110), `The area of a circle is $A = \pi r^2$`);
color(t1, cyan);
// mixed text + math that WRAPS across lines
text(t2, (cx, 300), `By the Pythagorean theorem, if $a^2 + b^2 = c^2$ then the triangle with sides a, b, c is right-angled`);
wrap(t2, 760);
color(t2, lime);
// a whole-formula label (auto-typeset, centred)
equation(eq, (cx, 470), `\int_0^{1} x^2 \, dx = \tfrac{1}{3}`, 60);
color(eq, gold);
image
Embed a raster image (PNG/JPG) with image(id, (x,y), "asset:name.png"|"path", w, h) —
a bundled or user-provisioned file animated like any entity (shown, spun, pulsed, moved).
Unlocks logos, avatars and photo backdrops (e.g. a creator’s brand in a template).
// ============================================================================
// image.manic — embed a raster image (PNG/JPG) in a scene
// ----------------------------------------------------------------------------
// `image(id, (x,y), "asset:name.png"|"path", [w], [h])` loads a bundled or
// user-provisioned image file and draws it
// centred at (x,y), w×h px — and it's an ordinary entity, so every verb
// (`show`/`move`/`fade`/`pulse`/`spin`/…) animates it. Here a bundled logo
// slides + fades in, pulses, then a caption types beside it. A missing ordinary
// path draws a crossed placeholder; a missing bundled asset is an error.
//
// (This unlocks real logos, avatars and photo backdrops for creator templates —
// a creator drops their brand image into a slot.)
// ============================================================================
title("Raster Images");
canvas("16:9");
image(logo, (cx, 340), "asset:manic-logo.png", 300, 300); hidden(logo);
text(cap, (cx, 600), "any PNG/JPG — and it animates like anything else");
color(cap, fg); size(cap, 34); bold(cap); untraced(cap);
// ================= THE SCENE =================
show(logo, 0.6); pulse(logo);
wait(0.4);
spin(logo, 1.0);
type(cap, 2.0);
wait(0.6);
par { move(logo, (cx, 320), 0.8, smooth); pulse(logo); }
wait(1.0);