manic — the manual
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.
It’s built for explainer videos — math, algorithms, data structures, or anything you can draw — and it’s designed so a non-programmer can read and write it. It draws in 2D and, when you want depth, in 3D too.
The whole idea in 30 seconds
A manic file has two parts:
- The cast — the shapes on screen (a circle, a line, some text). You give each one a name.
- The script — what happens over time, called out by name: draw this, move that, flash it green.
title("Hello");
canvas("16:9");
circle(sun, (640, 360), 90); // the cast: a circle named `sun`
color(sun, cyan);
show(sun, 0.6); // the script: fade it in over 0.6s
pulse(sun); // then give it a little pulse
That’s the entire model. The rest of this guide walks through the vocabulary — one small, runnable example at a time — so by the end you can storyboard a video in your head and type it out.
How to read this book
Every section has a runnable sample and a short video of it playing, so
you see exactly what each word does. Copy any sample into a .manic file and:
manic yourfile.manic # live preview window
manic yourfile.manic --record out # render to out/out.mp4
Ready? Start with your first animation →
Making vertical social content? Take the production path directly: Create a polished Reel →
Choosing the visual surface? Manic defaults to the professional black-and-white
mono look; compare every option in Templates →.
Getting started
Let’s make the smallest real animation: a title fades in, a circle draws itself, and it pulses once.
Here primarily for vertical social video? Jump to Create a polished Reel, the complete Creator Kit workflow.
// getting started — one shape, drawn on, then a pulse.
title("Hello, manic");
canvas("16:9");
text(head, (cx, 140), "hello, manic");
color(head, cyan); size(head, 40); hidden(head);
circle(sun, (cx, cy), 110);
color(sun, magenta); stroke(sun, 5); glow(sun, 8); untraced(sun);
show(head, 0.5); // fade the title in
draw(sun, 1.2); // trace the circle on
pulse(sun); // a friendly pulse
wait(1.0);
▶ See it play:
What each line is doing
| line | meaning |
|---|---|
title("Hello, manic") | the window/file title (metadata) |
canvas("16:9") | the frame size — 16:9 is 1280×720 (see Colour & style) |
text(head, (cx, cy)…) | cast: a text entity named head at the canvas centre |
color / size / hidden | modifiers — style head, and start it invisible |
circle(sun, …) | cast: a circle named sun |
untraced(sun) | start with the stroke undrawn, ready to trace on |
show(head, 0.5) | script: fade head in over 0.5s |
draw(sun, 1.2) | script: trace sun’s outline on over 1.2s |
pulse(sun) | script: grow-and-settle attention pulse |
wait(1.0) | hold for a second at the end |
There is no template(...) line in this first sample, so it uses the
professional black-and-white mono template automatically. See
Templates when you want paper, blueprint, terminal, or a
colourful surface.
Two things worth internalising right away:
cx,cyare the canvas centre. manic gives youw,h,cx,cyfor free so you can place things without hard-coding pixels.(cx, cy)is always the middle.- The order of the cast doesn’t matter, but the script runs top-to-bottom.
show, thendraw, thenpulseplay one after another. To make things happen at the same time, you wrap them inpar { … }— that’s the Timing chapter.
Two ways to appear
Notice head uses hidden + show, but sun uses untraced + draw. That’s
the one gotcha worth learning early:
hidden+show→ a fade-in (good for text and filled shapes).untraced+draw→ a draw-on, like a pen tracing the outline (good for strokes, lines, plots).
Get those two pairs right and everything else clicks. Next: the shapes you can put on screen →
Create a polished Reel
Creator Kit v2 is the fastest path from an idea to a finished vertical video. It owns the responsive layout, safe areas, question hierarchy, answer cards, timer, reveal, creator footer, and end card. You provide the content and choose how restrained or energetic it should feel.
This chapter gives one dependable production recipe. Start here, then use the Creator reference and examples when you need every option.
The production recipe
| step | recommended starting choice | why |
|---|---|---|
| Format | canvas("9:16") | native vertical composition |
| Surface | omit the call or use template("mono") | professional black-and-white default; use shorts when accent hue matters |
| Safe area | safe=reels on the creator and quiz | protects important content from platform UI |
| Hierarchy | studio layout=media-first density=comfortable | question first, one focal visual, readable answers |
| Motion | motion=calm or motion=studio | purposeful movement without visual noise |
| Timing | 9–13 seconds for one quiz beat | enough time to read, think, and absorb the reveal |
| Close | signature footer plus one end-card CTA | consistent identity and one clear next action |
For Shorts or TikTok, change both safe-area declarations to safe=shorts or
safe=tiktok. The same source can also be reframed for 4:5, square, or
16:9; the Creator regions reflow automatically.
1. Write for a phone, not a slide
A strong first Reel usually has:
- one question or promise that fits in one or two lines;
- one visual idea—an equation, diagram, image, chart, or simulation;
- three or four short answer choices;
- one accent colour and one motion personality;
- a brief explanation and a single call to action.
Prefer direct wording. Move supporting context into explain; do not make the
viewer read a paragraph before the timer starts. Use LaTeX for mathematical
notation so formulas stay crisp and compact.
2. Let the format own layout
canvas("9:16");
template("mono"); // optional: mono is the DSL default
creator(me, "@anish2good name=Proof_Daily tagline=Think_then_prove \
yt=zarigatongy x=@anish2good web=8gwifi.org/manic \
accent=cyan secondary=magenta footer=signature cta=Save_and_share safe=reels");
quiz(q, "Your short question",
"studio layout=media-first density=comfortable motion=calm safe=reels accent=cyan");
Use layout=media-first when the visual is part of the question, stack for up
to four text-led answers, grid for four to six compact answers, and auto
when you want manic to decide. Pass the visual to figure(id) instead of tuning
its coordinates for each aspect ratio.
Temporarily add safezone(guide,"reels") while previewing a new design. It is a
visible diagnostic overlay, so remove it before the final record. The
safe=reels layout setting should remain.
3. Give reading, thinking, and payoff separate beats
The default run(q,10) is a good first draft. For a repeatable series, declare
the exact beat:
timing(q, "calm ask=1.1 options=1 think=5.5 reveal=0.75 hold=2.15 stagger=0.06");
timerstyle(q, "look=ring position=below direction=drain color=cyan finish=pulse");
run(q);
A useful starting range is 1–1.5 seconds for the hook, 4–6 seconds to think, 0.6–0.9 seconds for the reveal, and 1.5–2.5 seconds to hold the explanation. Adjust for the actual reading load; the right pace is the one that remains clear on a phone.
timing owns choreography and timerstyle owns appearance. Change the ring to
bar, segments, ticks, number, pulse, or none without moving a single
beat. With explicit phases, always use run(q)—adding run(q,dur) would create
a second competing duration.
Design shortcut: keep the default ring for most educational Reels. Use a bar for a longer process, segments for staged challenges, ticks for technical work, and pulse only for genuinely urgent moments.
4. Build a recognisable close
Call socials(profile) before the main animation so the selected footer is
present throughout. endcard(profile) creates a hidden final lockup; fade the
content, then reveal it for about one second or more.
Keep the CTA singular: “Save this”, “Try the next one”, or “Follow for more”. A
creator profile is reusable across a whole series, so brand decisions live in
one line rather than every scene. Inside a profile spec, use underscores where
a value needs spaces: name=Proof_Daily cta=Save_and_share.
For a channel-link row without external assets, choose footer=social and set
the platform values directly: yt=zarigatongy x=@anish2good web=8gwifi.org/manic. Manic draws matching normalized icons and keeps the text
inside the responsive footer. Use two or three identities for the clearest
phone-size close; a larger platform set automatically becomes icon-only.
Complete Reel
This example uses exact phases, LaTeX media, the Reels safe area, a restrained native timer, a signature footer, timeline markers, and a final end card:
// A production-ready Creator v2 Reel: safe layout, exact pacing, one focal
// equation, restrained motion, reusable branding, markers, and an end card.
title("Perfect Reel — Angle Ratio");
canvas("9:16");
template("mono");
creator(me, "@anish2good name=Proof_Daily tagline=Think_then_prove yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=signature cta=Save_and_share safe=reels");
quiz(q, "Triangle angles: 2:3:4. What is the largest?",
"studio layout=media-first reveal=fade density=comfortable motion=calm safe=reels accent=cyan");
option(q, `$60^\circ$`);
option(q, `$80^\circ$`, correct);
option(q, `$90^\circ$`);
option(q, `$100^\circ$`);
explain(q, `The parts total $9$, so $x=20^\circ$ and $4x=80^\circ$.`, "Angle sum");
equation(prompt, (cx, 650), `2x+3x+4x=180^\circ`, 70);
color(prompt, cyan);
figure(prompt);
hidden(prompt);
// Exact 10.5-second quiz beat. The timer look can change independently.
timing(q, "calm ask=1.1 options=1 think=5.5 reveal=0.75 hold=2.15 stagger=0.06");
timerstyle(q, "look=ring position=below number=inside direction=drain size=medium thickness=1.1 color=cyan track=dim label=THINK font=mono finish=pulse");
socials(me);
endcard(me, "cta=Save_and_share");
mark("hook");
par {
run(q);
show(prompt, 0.5);
}
mark("endcard");
par {
fade(q.parts, 0.45);
fade(prompt, 0.45);
fade(me.footer, 0.45);
}
show(me.endcard, 0.6);
wait(1.4);
Preview and export
manic check examples/perfect-reel.manic
manic examples/perfect-reel.manic
# Inspect representative phone frames: hook, countdown, and answer hold.
manic examples/perfect-reel.manic --still 0.8
manic examples/perfect-reel.manic --still 6.0
manic examples/perfect-reel.manic --still 10.0
# Vertical render with only your Creator profile branding.
manic examples/perfect-reel.manic --record out --preset reel --no-brand
Omit --no-brand if you also want the Manic intro and watermark. Recording
writes markers.json; use its mark(...) timestamps to align narration, music,
or captions in an editor.
Final phone-size review
Before publishing, check:
- Can the hook be understood during the first beat?
- Is every important word and the CTA inside the selected safe area?
- Is there one obvious focal point, with no competing motion?
- Are the choices short, distinct, and marked with exactly one correct answer?
- Does the timer contrast with its track without overpowering the content?
- Does the explanation remain visible long enough to read aloud?
- Does the final card ask for one action and hold long enough to register?
- Are logos, photos, fonts, narration, and music yours or licensed for use?
Once those pass, keep the same profile, motion, colour, and closing pattern across the series. Consistency will make the next Reel both faster to produce and easier to recognise.
Next: Explore every Creator control and format variant →
Shapes — the cast
Everything on screen is an entity with a name (its first argument). You declare shapes once; the name is how you address them later in the script.
The six primitives
Each line below is the whole call — copy it and tweak the numbers.
| shape | write | draws |
|---|---|---|
| circle | circle(sun, (cx, cy), 90); | a circle, radius 90, at the centre |
| rect | rect(box, (cx, cy), 200, 120); | a rectangle 200 wide, 120 tall |
| line | line(edge, (100, 100), (400, 300)); | a line from point to point |
| arrow | arrow(v, (100, 400), (400, 400)); | a line with an arrowhead at the end |
| dot | dot(p, (cx, cy), 8); | a small filled dot, radius 8 |
| text | text(cap, (cx, 640), "hello"); | a text label anchored at a point |
Plus a few composite helpers built from those: polygon, arc/sector, brace/
bracelabel, caption (a row of words), and support(id, (cx,cy), [len], ["dir"])
— the hatched wall / ceiling / floor for mechanics & textbook diagrams ("dir" is
the open side: "down" ceiling, "up" floor, "left"/"right" walls).
Two more helpers cover scenes that would otherwise need dozens of manual dots and endpoints:
circle(glass, (400,300), 100);
particles(bubbles, glass, 24, 5, 7); // id, container, count, radius, seed
rect(tank, (750,300), 180, 160);
link(pipe, glass, tank, 35); // 35 px of curve; 0 is straight
particles works inside circles and rectangles. Its bare id addresses every
child dot, so hidden(bubbles) and recolor(bubbles, fg) affect the whole
group. link meets those shapes at their boundaries and stays attached when
either endpoint moves.
Points are (x, y) in pixels, origin top-left, y increasing downward. Use
cx, cy, w, h to stay canvas-independent.
For display mathematics, equation accepts LaTeX in backticks. Standard
\textcolor can emphasize individual terms with Manic’s semantic palette; the
colors automatically follow the selected template:
equation(step, (cx, 620),
`\textcolor{magenta}{\mathrm{slope}}=
\frac{\textcolor{cyan}{\mathrm{rise}}}
{\textcolor{gold}{\mathrm{run}}}`, 40);
Leave a term uncolored to use the template foreground. A single-color equation
can still be styled as one entity with color and animated with recolor.
// the six primitive shapes, drawn on together.
title("Shapes");
canvas("16:9");
text(t, (cx, 90), "six primitives"); color(t, cyan); size(t, 32); hidden(t);
circle(c, (240, 380), 80); color(c, cyan); stroke(c, 4); untraced(c);
rect(r, (470, 380), 150, 150); color(r, magenta); stroke(r, 4); untraced(r);
line(l, (640, 300), (820, 460)); color(l, lime); stroke(l, 4); untraced(l);
arrow(a, (900, 460), (1040, 300)); color(a, cyan); stroke(a, 4); untraced(a);
dot(d, (1110, 380), 12); color(d, magenta); hidden(d);
text(lbl, (640, 620), "circle · rect · line · arrow · dot · text");
color(lbl, dim); size(lbl, 24); hidden(lbl);
show(t, 0.5);
par { draw(c); draw(r); draw(l); draw(a); show(d); }
show(lbl, 0.5);
wait(1.2);
▶ See it play:
Modifiers — style a shape at t = 0
A shape starts plain. Modifiers change how it looks before the animation begins. They take the entity name first, then a value:
| modifier | effect | example |
|---|---|---|
color(id, c) | fill / stroke colour | color(sun, cyan); |
stroke(id, w) | line thickness | stroke(sun, 4); |
size(id, n) | text size | size(cap, 30); |
glow(id, n) | neon halo strength | glow(sun, 8); |
opacity(id, 0..1) | transparency | opacity(sun, 0.5); |
filled(id) / outlined(id) | turn fill / outline on | filled(box); |
hue(id, deg) | colour by an angle (0–360) — for gradients & loops | hue(seg, 200); |
z(id, n) | draw order (higher = on top) | z(box, 5); |
sticky(id) | pin to the screen so it stays put through a cam/zoom (a HUD) | sticky(caption); |
And two that decide how a shape first appears:
| modifier | pairs with | gives |
|---|---|---|
hidden(id) | show(id) | a fade-in |
untraced(id) | draw(id) | a draw-on (pen tracing the outline) |
Colours are a fixed palette:
fg,void,cyan,magenta,lime,dim,panel. For a computed colour (say, one per item in a loop) usehue(id, degrees). More in Colour & style.
Naming things in a loop
When you make many shapes with a for loop, give each a unique name with
interpolation — {expr} glued to the name:
for i in 0..5 {
dot(p{i}, (200 + i*180, cy), 8); // p0, p1, p2, p3, p4
}
That’s your cast. Now let’s make it move → Verbs.
Verbs — bringing it to life
Verbs are the script. Each one names an entity and animates it. They run
top-to-bottom, one after another (use par for simultaneous).
Almost every verb takes two optional trailing arguments:
move(sun, (900, 400), 0.8, smooth);
// ^dur ^easing
[dur]— how long, in seconds (there’s a sensible default).[ease]— the motion curve:linear,smooth,in,out,back,bounce,elastic,spring(see Colour & style).
Reveal & hide
draw(sun, 1.2); // -> trace a stroke on (needs `untraced` first)
erase(sun); // -> the reverse: un-draw it
show(cap, 0.5); // -> fade in (needs `hidden` first)
fade(cap); // -> fade out
type(cap); // -> typewriter: reveal text character by character
Attention
flash(sun, cyan); // -> flash to a colour, then restore
pulse(sun); // -> quick grow-and-settle "look here"
shake(sun); // -> horizontal shake, an "error/no" gesture
spin(sun, 360); // -> spin about its centre
Motion
move(p, (900, 400)); // -> glide to an absolute point
shift(p, (0, -120)); // -> move by a delta (relative)
scale(r, 1.4); // -> animate uniform scale to 1.4x
rotate(r, 45); // -> rotate by 45 degrees
grow(arrow, (500, 200));// -> animate a line/arrow endpoint (draws or retargets)
cycle(x, y, z, 0.8, 90, smooth); // -> x→y→z→x along arcs
cycle(a, b, c, …, [dur], [arc], [ease]) moves every entity into the next
one’s position and the last into the first. The path arc is in degrees and
defaults to 90; pass 0 for straight paths. Repeated calls compose, making it
useful for symbol rearrangements, card carousels, and CyclicReplace-style moves.
Ambient motion and path flow
particles(bubbles, glass, 24, 5, 7);
link(pipe, glass, tank, 35);
untraced(pipe);
par {
wander(bubbles, 6); // always stays inside glass
seq { draw(pipe); flow(pipe, 1); }
}
wander is deterministic: the same optional particle seed gives the same
placement and motion in preview and in the final recording. It occupies the
duration you give it, so run it in par with the story it should accompany.
flow sends a temporary luminous pulse over any line, arrow, curve, spline,
arc, or tracked link; it is useful for a signal, energy, traffic, or simply
directing attention.
Content & colour
say(cap, "next step"); // -> crossfade a text entity to new words
recolor(sun, lime, 0.5); // -> permanently animate the colour
The escape hatch — to / set
Named verbs are shortcuts. to animates any single property, for whatever
isn’t pre-named:
to(sun, opacity, 0.3, 0.5); // animate opacity to 0.3 over 0.5s
to(sun, rot, 90); // rotation to 90 degrees
Properties: pos, color, opacity, scale, rot, trace, hue.
Move the camera
cam((300, 200), 1.0); // pan the camera centre
zoom(1.6, 0.8); // zoom to 1.6x
The camera moves the whole world, so a caption or counter would slide off with it.
Mark it sticky(id) to pin it to the screen and keep it readable through the move.
One verb, a whole group
If a name refers to a tag (a group) instead of a single entity, the verb runs on every member at once. This is how you animate a whole graph, table, or loop-generated set in one line:
hidden(g.nodes); // every node, at t=0
draw(g.edges); // trace every edge on together
flash(a.cells, cyan);// flash all array cells
More on grouping in the Kits chapter. Next, control when things happen → Timing.
Timing — flow, named phases & clocks
By default, verbs play one after another. Three wrappers change that — they turn “then, then, then” into “together” or “cascading”.
| wrapper | plays its steps… | use for |
|---|---|---|
| (nothing) | one after another | the normal flow |
par { … } | all at the same instant | reveal a group at once |
seq { … } | one after another (explicit) | grouping inside a par |
stagger(d) { … } | each one d seconds after the last | cascades / waves |
show(a); show(b); // a, THEN b
par { show(a); show(b); } // a and b together
stagger(0.1) { show(a); show(b); show(c); } // a, then b 0.1s later, then c…
Put a for loop inside one and it just works — the loop expands first, so all
its statements land in the wrapper:
par { for i in 0..6 { show(a{i}); } } // whole row at once
stagger(0.1) { for i in 0..6 { show(b{i}); } } // whole row cascading
// the same reveal, three ways: sequence, together, cascade.
title("Timing"); canvas("16:9");
text(t, (cx, 110), "seq · par · stagger"); color(t, cyan); size(t, 30); hidden(t);
for i in 0..6 { dot(a{i}, (220 + i*160, 300), 16); color(a{i}, cyan); hidden(a{i}); }
for i in 0..6 { dot(b{i}, (220 + i*160, 470), 16); color(b{i}, magenta); hidden(b{i}); }
show(t, 0.5);
// top row, all at the same instant
par { for i in 0..6 { show(a{i}); } }
wait(0.5);
// bottom row, cascading 0.1s apart
stagger(0.1) { for i in 0..6 { show(b{i}); } }
wait(1.0);
▶ See it play:
Beats & sections
Two more timing words structure a longer video:
wait(1.2); // hold — nothing moves for 1.2s
section("Part Two"); // a titled marker (jump to it in preview with keys 1–9;
// also exported for lining up narration)
mark("beat-3"); // a named timestamp for your editor
wait is your friend for pacing — a beat of stillness after something lands
reads far better than rushing to the next move.
Generic Timing v2 — one clock for any scene
Use a generic timing controller when several parts of a scene must share one exact schedule. It is format-neutral: the same controller can coordinate a physics simulation, geometry proof, chart, caption sequence, or ordinary shapes.
Think of it as four small pieces:
| piece | responsibility |
|---|---|
timing | declares the phase names, durations, exact total, and optional clock position |
timerstyle | changes only the visible clock; it never changes scene timing |
timed | runs the clock and schedules the complete phase contract |
during | contains the ordinary animation actions for one named phase |
The phase declaration is the source of truth. The clock is only one visual view of that choreography.
Quick reference
| form | use |
|---|---|
timing(clock,"intro=1 demo=6 result=2") | declare named phases |
timing(clock,(1160,80),"...") | declare phases and set the initial clock position |
timing(clock,"duration=6") | shorthand for one phase named main |
timerstyle(clock,"...") | change appearance without changing timing |
timerstyle(clock,(1160,80),"...") | restyle and reposition the clock |
timed(clock) { ... } | play the complete phase schedule and clock |
during("phase") { ... } | author one phase inside timed |
run(clock) | play the clock alone |
countdown(id,[at],[seconds],["style"]) | independent countdown without named phases |
Core pattern, after defining the referenced entities and simulation:
timing(clock, (1160,80), "intro=1 demo=6 result=2");
timerstyle(clock, "look=ring number=inside color=cyan");
timed(clock) {
during("intro") { show(title, 0.6); }
during("demo") { par { run(sim, 6); draw(sim.path, 6); } }
during("result") { show(answer, 0.6); }
}
Use countdown when you only need an independent visual countdown. Use
timing when named phases must govern other animation.
Compose inside phases
Each during block accepts the usual timeline language. In particular, use
par for actions that must occupy the same phase together:
timing(clock, "setup=1 experiment=6 result=2");
timed(clock) {
during("setup") { show(title, 0.6); }
during("experiment") {
par {
run(pendulum, 6);
draw(pendulum.path, 6);
karaoke(caption, 0.35);
}
}
during("result") {
par {
show(formula, 0.6);
show(explanation, 0.6);
}
wait(1.4);
}
}
Rules that prevent drift
- Give a generic controller a fresh id. Do not reuse an entity, quiz, simulation, or group id.
- Put constructors and style modifiers outside
timed/during; put timeline actions such asshow,draw,run,wait,par, andseqinside. - A short phase block is padded automatically. A block that exceeds its phase is an error.
- A phase may have at most one
duringblock. Combine related work inside it withparorseq. - An omitted phase is valid and becomes a blank hold.
- Phase blocks may appear in any source order: they are placed at the absolute
offsets declared by
timing. timed(clock)already runs the clock. Do not also callrun(clock)inside it.run(clock)is timer-only playback.run(clock, dur)is rejected because the phase declaration already owns the duration.
Choose the clock’s look
The clock uses native manic shapes, so it stays sharp at any output size and can be targeted like the rest of the scene. No SVG workflow is required.
| look | best fit |
|---|---|
ring | neutral default; compact and familiar |
bar | long processes or wide layouts |
segments | energetic stages and presentations |
ticks | precise, technical, or measurement-led scenes |
number | minimal layouts where the value is enough |
pulse | short, urgent moments; use sparingly |
none | keep exact phase choreography without showing a clock |
The main controls are:
number=inside|outside|noneanddirection=fill|drainsize=small|medium|large|0.5..2.0andthickness=0.4..3.0color,track,label, andfont=mono|displayfinish=fade|hold|flash|pulsefor the completion cue
Start with the default ring, then change the look only when it supports the
scene’s meaning. For legibility, keep strong contrast between color and
track, and avoid combining a busy clock with dense content in the same corner.
Advanced styling can target the stable tags clock.timer,
clock.timer.track, clock.timer.progress, clock.timer.value,
clock.timer.label, and clock.timer.effects (replace clock with the
controller id).
Generic controller or Creator quiz?
The same visual clock system serves two different timing contracts:
timing(fresh_id, "phase=seconds ...")creates a generic controller used bytimedandduring.timing(quiz_id, "calm ask=... think=... reveal=...")configures a Creator quiz and is played withrun(quiz_id).
Do not wrap a quiz in timed; the quiz runner already owns its ask, options,
think, reveal, hold, and stagger phases.
Common fixes
| message or symptom | fix |
|---|---|
| controller id is already in use | choose a fresh id for generic timing |
| a phase overruns | shorten its sequence, compose simultaneous work with par, or increase the declared phase |
| unknown phase | match the name in during to the timing declaration |
| duplicate phase block | combine the work into one during block |
| competing duration | remove the duration argument from run(clock, dur) |
| timer appears twice | remove run(clock) from inside timed(clock) |
Complete non-quiz example using a pendulum, caption, formula, and segmented clock:
// Generic Timing v2 — one named clock coordinates a non-quiz physics scene.
// `timed` runs the visual timer and places every `during` block at the exact
// offset declared by `timing`; short blocks are padded, overruns are errors.
title("Generic Timing v2 — Pendulum");
canvas("16:9");
template("mono");
text(head, (315, 72), "ONE CLOCK. EVERY BEAT.");
size(head, 30); bold(head); color(head, fg); hidden(head);
text(sub, (315, 112), "Generic Timing v2 — not a quiz");
size(sub, 18); color(sub, dim); hidden(sub);
pendulum(p, (640, 245), 1.5, 42, 140, 0.03);
untraced(p.path);
equation(law, (640, 602), `T\approx2\pi\sqrt{L/g}`, 54);
color(law, gold); hidden(law);
text(note, (640, 655), "The scene and clock share the same named phases.");
size(note, 21); color(note, dim); hidden(note);
text(done, (640, 680), "INTRO → EXPERIMENT → INSIGHT → OUTRO");
size(done, 18); color(done, cyan); hidden(done);
// A fresh id creates a format-neutral timing controller. Its phases total
// 10.4 seconds; no duplicate duration is passed to `timed` or `run`.
timing(showclock, (970, 86), "intro=1.2 experiment=6 insight=2 outro=1.2");
timerstyle(showclock,
"look=segments number=outside direction=drain size=small thickness=1.15 color=cyan track=dim label=MASTER_CLOCK font=display finish=pulse");
timed(showclock) {
during("intro") {
par { show(head, 0.7); show(sub, 0.7); }
}
during("experiment") {
par { run(p, 6); draw(p.path, 6); }
}
during("insight") {
par { show(law, 0.6); show(note, 0.6); }
pulse(p.bob, 0.7);
}
during("outro") {
show(done, 0.6);
}
}
Next: the palette, glow, and easings → Colour & style.
Templates — choose the whole visual system
A template controls the background, semantic palette, glow character and any page chrome for the entire movie. Choose it once near the top of the file:
canvas("9:16");
template("mono");
mono is the default. If a DSL file does not call template(...), it renders
exactly as if template("mono") had been written. Keeping the call explicit is
useful in shared examples; omitting it is convenient for ordinary authoring.
Available templates
| template | best for | character |
|---|---|---|
mono | professional explainers, proofs and restrained Reels | black-and-white editorial surface, clear luminance hierarchy, subtle glow |
plain | legacy or intentionally colourful scenes | original neon semantic colours on near-black |
terminal | code, algorithms and technical demos | neon terminal frame and stronger chrome |
paper | textbook figures, worksheets and print | white page, dark ink, crisp low-glow rendering |
blueprint | geometry, engineering and construction | cyan/white drafting marks on deep navy |
shorts | energetic social content where hue matters | restrained dark creator palette |
Useful aliases are monochrome, blackwhite, black-white, and bw for
mono; light and print for paper; and blue for blueprint.
Mono in practice
// Mono is the default; keep this line explicit when teaching or sharing.
title("Mono template");
canvas("16:9");
template("mono");
rect(card, (640, 360), 930, 430);
color(card, panel);
text(head, (640, 205), "ONE PALETTE · CLEAR HIERARCHY");
size(head, 42);
color(head, fg);
hidden(head);
circle(a, (410, 370), 58); color(a, cyan); hidden(a);
circle(b, (640, 370), 58); color(b, magenta); hidden(b);
circle(c, (870, 370), 58); color(c, lime); hidden(c);
text(al, (410, 470), "PRIMARY"); color(al, fg); hidden(al);
text(bl, (640, 470), "SECONDARY"); color(bl, dim); hidden(bl);
text(cl, (870, 470), "SUCCESS"); color(cl, lime); hidden(cl);
show(head, 0.4);
par {
show(a, 0.5);
show(b, 0.5);
show(c, 0.5);
}
par {
show(al, 0.35);
show(bl, 0.35);
show(cl, 0.35);
}
pulse(c);
wait(1);
Named colours remain meaningful under mono. fg, dim, panel, cyan,
magenta, lime, gold, red, orange, and blue are mapped to deliberately
different greys instead of collapsing to identical white. This preserves
hierarchy and correct-answer contrast while keeping the export monochrome.
Use named colours for template-aware work:
color(answer, lime); // success role; becomes a bright mono tone
color(note, dim); // secondary role; remains visually quiet
hue(...) is intentionally an explicit colour choice and bypasses semantic
palette remapping. Avoid it when the output must remain strictly black and
white.
DSL selection versus export override
The DSL call travels with the scene. The command-line option is useful for a one-off alternate render:
manic scene.manic --still 4.0
manic scene.manic --record out --template paper
An explicit --template NAME export option overrides the DSL template for that
run. This is a quick way to proof the same semantic scene on mono, paper and
blueprint without editing the source.
Creator and Reel guidance
Start with mono for a polished black-and-white identity. It works especially well for mathematics, question cards, native social icons and technical timers:
canvas("9:16");
// template("mono") is optional because mono is the default.
creator(me, "@anish2good yt=zarigatongy x=@anish2good web=8gwifi.org/manic footer=social");
quiz(q, "Which statement is true?");
Switch to shorts when accent hue communicates part of the channel identity,
or to paper for an exam-sheet or textbook treatment. Template choice does not
change Creator layout, safe areas, timing, option labels or social-platform
selection—it changes only their visual system.
Practical checks
- Review one reading frame and one reveal frame at phone size.
- In mono, check luminance contrast rather than relying on colour names.
- Prefer one semantic accent role; do not make every object equally bright.
- Use
plainwhen migrating an older example that must preserve its neon hue. - Use
paperorblueprintto test whether a construction remains legible on a very different surface.
Next: Colour, glow and semantic roles →
Colour & style
The palette
manic uses a small set of semantic colour names. The default mono template
maps all of them to distinct black-and-white luminance levels; choose plain
or another colour template when hue should carry meaning. Their original neon
roles are:
| name | is | name | is |
|---|---|---|---|
cyan | electric blue | gold | warm amber |
magenta | hot pink | red | warm red |
lime | green | orange | orange |
blue | true blue (≠ cyan) | dim | muted grey-violet |
void | the background | fg | near-white (default text) |
panel | dark fill |
color(sun, cyan);
recolor(sun, magenta, 0.5); // animate to a new palette colour
Any colour, by hue
For a computed colour — a gradient, or one per item in a loop — use hue,
which takes an angle from 0 to 360:
hue(sun, 200); // a fixed hue
for i in 0..24 {
hue(p{i}, 360*i/24); // a full rainbow around the loop
}
That’s how the rainbow-ring loop gets its colours.
Glow
Every entity has a neon glow (0 = crisp, higher = brighter halo):
glow(sun, 8); // strong halo
glow(grid, 0); // crisp, no halo — good for fine detail
Easings
The optional last argument of a motion verb is the easing — the shape of the motion over time:
| easing | feel |
|---|---|
linear | constant speed (mechanical) |
smooth | ease in and out (the default, natural) |
in / out | accelerate / decelerate |
back | overshoot slightly and settle |
bounce | bounce at the end |
elastic / spring | wobble / springy settle |
move(p, (900, 400), 0.8, bounce);
move(p, (900, 400), 0.8, smooth); // usually what you want
Canvas & size
canvas(...) sets the frame. Give it a preset or explicit pixels:
canvas("16:9"); // 1280x720 (also: 1080p, 4k, square, portrait/9:16, 4:3)
canvas(1280, 720); // explicit
portrait / 9:16 is 1080×1920 — pair it with the reel render preset for
vertical / social clips.
Templates — the whole look
For the complete selection guide, CLI override rules, mono tips and a runnable black-and-white example, see Templates — choose the whole visual system.
template("...") sets the movie’s look in one call: the background, how the
palette renders, the glow, and any page chrome. Put it near the top, after
canvas(...).
canvas("16:9");
template("paper"); // a flat white exam-paper page
| template | look |
|---|---|
mono (aliases monochrome, bw) | default — black-and-white editorial, near-black page, subtle glow |
plain | original neon palette on near-black, no chrome |
terminal | the neon-terminal frame (border, title, masthead) |
paper (aliases light, print) | white page, dark ink — for print / textbook figures |
blueprint (alias blue) | white & cyan lines on deep navy |
shorts | restrained dark creator palette for Shorts and Reels |
Omitting template(...) is exactly the same as selecting template("mono").
Use explicit plain when an older scene should keep the original neon colours.
The clever part is the palette remap: a template doesn’t just change the
background, it re-maps every named palette colour to that template’s role. So
on mono, cyan, magenta, lime, gold, red, orange, and blue become
carefully separated greys; on paper, panel → light box, fg → dark ink,
lime → forest green, and so on —
which means your existing scene renders legibly on the new page without
recolouring anything. That’s why template("paper") alone turns a pulley,
a spring, or a linked list into a clean textbook figure (see
Elevating a scene). paper/blueprint also drop the glow for
crisp print output.
hue(...) and future explicit RGB colours are intentionally bespoke, so they
pass through instead of being forced to greyscale. Use the named palette colours
when a scene must remain strictly monochrome.
Next: loops, variables, and macros → The language layer.
The language layer
Everything so far has been static text. manic also has a small computation layer that runs before the animation — variables, arithmetic, loops, and macros. It lets one rule draw a hundred shapes.
These are resolved at build time. By the time the animation plays, they’ve expanded into plain calls — so they cost nothing at render.
Variables — let
let r = 120;
let gap = r * 2 + 40;
circle(a, (cx - gap, cy), r);
circle(b, (cx + gap, cy), r);
Arithmetic is what you’d expect: + - * / ^, parentheses, and functions like
sin, cos, sqrt. Constants pi, tau, e are built in, as are the canvas
vars w, h, cx, cy.
Put a
*between names. A number can hug a name (2r,3(x+1)), but two names glued together read as one word — writer*x,i*dx,pi*t, neverrx/idx/pit. This is the most common slip in loops and formulas.
Loops — for
for i in 0..5 {
dot(p{i}, (200 + i*180, cy), 8); // p0 … p4
}
p{i} is id interpolation — {expr} glued to a name makes each entity
unique. Use i in the body to compute positions, sizes, hues…
// one for-loop paints a full rainbow ring.
title("One loop"); canvas("16:9");
text(t, (cx, 90), "one loop, 24 dots, every hue"); color(t, cyan); size(t, 28); hidden(t);
let n = 24;
for i in 0..n {
dot(p{i}, (cx + 300*cos(tau*i/n), cy + 300*sin(tau*i/n)), 14);
hue(p{i}, 360*i/n); // colour by angle -> a rainbow
hidden(p{i});
}
show(t, 0.5);
stagger(0.05) { for i in 0..n { show(p{i}); } }
wait(1.4);
▶ See it play:
Conditionals — if
let n = 5;
if n > 3 {
circle(big, (cx, cy), 120);
}
Macros — def
A def is a reusable rule. Its parameters are numbers; build ids inside with
interpolation. It can even call itself (recursion) — that’s how the fractal tree
in the gallery is one page of code:
def branch(k, x, y, ang, len, depth) {
if depth > 0 && len > 2 {
let x2 = x + len*cos(ang);
let y2 = y - len*sin(ang);
line(seg{k}, (x, y), (x2, y2));
branch(2*k, x2, y2, ang + 0.4, len*0.72, depth - 1);
branch(2*k + 1, x2, y2, ang - 0.4, len*0.72, depth - 1);
}
}
branch(1, cx, h - 40, 1.5708, 150, 12);
Reductions
Fold a range into one number with sum, prod, min, max:
let n = 6;
let total = sum(i in 1..n : i); // 1 + 2 + … + (n-1)
That’s the whole language. The rest is kits — bundles of higher-level figures → Kits.
Kits — math, geometry, algorithms
The words so far (circle, move, flash, for…) are the core. On top of
that, manic ships kits — bundles of higher-level figures for a domain. You
use them exactly like any other call.
math
Coordinate frames, function plots, vectors, tables:
axes(ax, (cx, cy), 520, 240); // a coordinate frame
plot(wave, (cx, cy), 78, 120, "sin(x)"); // y = f(x) from a formula
tangent(t, wave, 0.5); // the tangent line + dot at x = 0.5
vector(v, (cx, cy), (120, -90)); // an arrow from an origin
matrix(m, "1 0; 0 1", (cx, cy)); // a bracketed matrix
geo
Olympiad-style constructions — you write the geometry, not coordinates, and everything is live (drag a point and the circumcircle, centroid, angles all recompute):
point(A, (300, 500)); point(B, (900, 500)); point(C, (620, 180));
circumcircle(cc, A, B, C); // recomputes if A/B/C move
midpoint(m, A, B);
algo
Data structures and algorithms — arrays + sorting, linked lists, stacks/queues, graphs, hash maps, BFS/DFS, Dijkstra:
array(a, "5 2 8 1", (cx, cy)); compare(a, 0, 1); swap(a, 0, 1);
graph(g, "a b c d", "a-b:2 b-c:1 c-d:3", circular, (cx, cy), 200);
dijkstra(g, a); // animates shortest paths
Groups make these one-liners: a graph tags its nodes and edges, so
draw(g.edges) or flash(g.nodes, cyan) animates the whole set.
three (3D)
A whole second world — a camera, solids, surfaces, and curves in real 3D space,
which you spin and morph. Every 3D word ends in 3:
camera3((8, -10, 6), (0, 0, 1), 45); // an eye to look through
cube3(box, (0, 0, 1), (2, 2, 2)); // a shaded box
revolve3(vase, (3, 0, 1.5), "0.7+0.4*sin(t*2)", (0, 3)); // spin a profile
orbit3(70, 25, 12, 4, smooth); // swing the camera around
It has its own chapter — see Going 3D.
stats
Turn data — or a random process — into a picture that reveals its shape, centre, and spread. Each builtin animates a process: a histogram builds up bar by bar, sample means pile into a bell, a running proportion settles onto the truth.
histogram(h, (cx, cy), "72 85 90 68 95 88 76 91 83", 8, 640, 300, rainbow);
bellcurve(b, (cx, cy), 100, 15); // the 68-95-99.7 rule
clt(c, (cx, cy), 5, 1200); // the Central Limit Theorem
histogram · summary · boxplot · skew · bellcurve · correlation ·
lln · clt · hypothesis · covariance · bayes · distribution ·
confidence · montecarlo · randomwalk. Seeded, so renders are reproducible.
physics
Simulations built from their physics and pre-simulated with RK4 at build time,
so every render is deterministic. Each sim’s parts are ordinary manic entities, and
the optional views (phase · well · timegraph · energygraph) show the same
motion as math panels. run(id) (alias swing) plays it.
pendulum(p, (cx, 200), 2, 50); phase(p, (980, 200), 120);
well(p, (980, 470), 120); run(p, 8); // one swing, three views
doublependulum(dp, (400, 240)); par { run(dp, 12); draw(dp.path, 12); } // chaos
Pendulum family: pendulum · doublependulum · springpendulum · kapitza ·
cartpendulum · comparependulum. Spring family: spring · verticalspring ·
springincline · bungee · resonance · doublespring · seriesparallel ·
carsuspension. Mechanics: robotarm · piston · molecule · ramp (with a forces(id)
free-body diagram) · inclinepulley · doubleincline · inclinebumper ·
springchain · looptrack (a curved-track loop-the-loop) · stringwave (a wave on a string) · newtonscradle · collideblocks · bulletblock (event-driven collisions) · dropmass · raft ·
brachistochrone. Pulleys: pulley (Atwood) ·
pulleyscale (reads the tension) · blocktackle (N-strand block & tackle) ·
compoundpulley (fixed + movable, masses A/B/C).
Because a sim’s parts are ordinary entities, any base look composes over them —
e.g. template("paper") + a hatched support turns a pulley or spring into a
textbook figure (see Elevating a scene and the *-paper examples).
optics
Light as geometry, with the real physics underneath — Snell’s law, Sellmeier
dispersion, and full spherical/aspheric ray tracing — so the bending, the colours
and the focus are earned, not painted. Each builtin is static geometry that
animates by a parameter sweep (run(id)) or by sketching its rays on
(draw(id.rays)).
refract(r, (640, 380), 1.0, 1.52); run(r, 7); // Snell's law; run sweeps the angle (→ TIR)
lens(l, (620, 360)); run(l, 7); // parallel rays → a focal point
prism(p, (560, 400), "sf11"); run(p, 7); // white light → a real rainbow (dispersion)
Foundations: refract (Snell + total internal reflection) · lens (a converging
thin lens). Dispersion: prism (white → spectrum) · achromat (chromatic
aberration → the doublet fix). Real lenses: lenssystem(id, [center], [preset], [object]) traces a prescription through its actual spherical/aspheric
surfaces — pick a design by name ("singlet", "plano-convex", "aspheric",
"doublet", "triplet") or write your own surface table
"radius thickness glass [conic] [aperture] | …"; an optional finite object
distance images a nearby point. Analysis: rayfan (the ray-fan aberration plot) ·
spotdiagram (the on-axis spot at focus) · fieldspot(id, [center], [preset], [field]) (the off-axis spot — a coma comet / astigmatic blur, with an
Airy-disk diffraction-limit overlay). A rainbow glows on the dark bench; the
geometric ray diagrams also take template("paper") for a textbook look.
creator
A format layer (not a subject): responsive, pre-timed social-video templates a content creator fills in—question, answers, media and a reusable profile. V2 adapts the same source to 9:16, 4:5, 1:1 and 16:9, with named platform safe areas, a polished studio default, configurable motion/timers, responsive footers and end cards.
canvas("9:16"); template("shorts");
creator(me, "@anish2good name=Math_With_Me yt=zarigatongy x=@anish2good web=8gwifi.org/manic footer=social accent=magenta");
quiz(q, "What is 7 x 8?", "studio labels=letters pace=calm motion=calm");
option(q, "54"); option(q, "56", correct); option(q, "48"); option(q, "63");
timerstyle(q, "look=segments position=media finish=pulse");
run(q, 8); // scales the calm ask → think → reveal beat
socials(me);
endcard(me); // reveal later with show(me.endcard)
quiz(id, "question", ["style"]) starts the format; style mixes a card skin —
studio (rounded editorial default) · badge (framed panel + coloured letter badges) · minimal (kicker + accent
rule, outline rows) · glass (glowing borders) · plain (flat) — and a question
reveal — type (typewriter, default) · fade · rise · pop · cut.
option(id, "text", [correct]) adds an answer; run auto-lays-out one to six cards,
fits their type, slides them in, plays the selected native timer, and lights up the correct
card (green badge + check). timing(id,"preset ask=... options=... think=... reveal=... hold=... stagger=...")
separates exact choreography from timerstyle(id,"look=... position=... direction=... finish=...").
The zero-config default remains a balanced draining ring; run(id,dur) scales a preset,
while an explicitly timed quiz uses run(id) so authored seconds remain exact. Also standalone:
countdown(id, [at], [secs], ["style"]), safezone(id, [inset|"profile"]), figure(target, [center], [size]), optional explain, and endcard. Social icons are
vector-drawn with normalized native marks for YouTube, X, Instagram, TikTok,
Facebook, LinkedIn, GitHub, web, and email. Profile values appear beside up to
three icons; no image or SVG assets are required.
Each kit has a full reference at https://8gwifi.org/manic, and you can see them all in motion in the Examples gallery.
Elevating a scene
A one-line sim already animates:
spring(sp); run(sp, 8);
That’s the minimal scene — correct, but bare. An elevated scene turns the same sim into a narrated lesson: parts revealed one at a time, each labelled, the governing law stated on screen, then the motion played across several synchronized views.
The key fact that makes this possible: a sim’s parts are ordinary manic
entities. spring(sp, …) lays out sp.wall, sp.spring, sp.mass, sp.path,
plus the tag sp.parts over all of them. Every base verb, modifier, and
annotation addresses those ids directly — there is no separate “physics mode”.
So you elevate a scene with the vocabulary you already know.
The three moves
1 · Stage — hide the parts, then reveal them in order
Build the sim, then hide each part (and any extra view) so you can bring them in
deliberately. untraced on a path keeps it in the scene at zero draw-progress so
you can draw it on later.
spring(sp, (360,300), 10, 1.4, 110);
hidden(sp.wall); hidden(sp.spring); hidden(sp.mass); hidden(sp.overlays);
untraced(sp.path);
well(sp, (1010,230), 120); hidden(sp.well); // an extra "reading", revealed later
2 · Annotate — name the parts with base entities
Point at things with text + leader arrow, mark a length or displacement with
bracelabel, drop a reference line, and state the law with a text. Create
them all now, hidden, so the reveal order is yours to choreograph.
text(coilL,(300,205),"spring, stiffness k"); color(coilL,lime); display(coilL); hidden(coilL);
bracelabel(xb,(360,352),(514,352),"x₀",22); color(xb,gold); hidden(xb); hidden(xb.label);
text(hooke,(360,150),"restoring force F = −k·x"); color(hooke,gold); display(hooke); hidden(hooke);
3 · Choreograph — chapters, emphasis, then run
Narrate with section + say, reveal with show/draw, emphasize with
flash/pulse/recolor, fade the clutter before the motion, then run.
section("Hooke's law");
say(cap, "pull it x₀ from equilibrium — it pulls straight back, F = −k·x", 0.4);
show(sp.spring, 0.4); flash(sp.spring, gold); show(xb, 0.4); show(xb.label, 0.4);
wait(0.8);
fade(xb, 0.3); fade(xb.label, 0.3); fade(hooke, 0.3); // declutter
section("Motion");
show(sp.overlays, 0.4); show(sp.well, 0.5); draw(sp.path, 0.6);
run(sp, 10); // sim, overlays, well ball, energy sweep — all animate together
The lever kit
Every one of these is base vocabulary that works on any entity — a sim part, a shape, a label — so the same kit elevates a physics sim, a geometry construction, or an algorithm trace.
| Lever | Builtins | Buys you |
|---|---|---|
| Stage the parts | hidden(id.part), untraced(id.path), then show/draw | reveal piece-by-piece instead of all at once |
| Narrate (light) | say(cap,"…",dur), a small text kicker updated with say, wait(dur) | chapter the story without covering the stage |
| Typewriter | type(id,dur) + cursor(id) (set the string with say(id,"…",0.1) first) | a lab-note / terminal feel that types itself out |
| Live data | counter(id,(x,y),start,decimals,"pre","suf") + to(id, value, target, dur) | a number that ticks up (k, period, acceleration) |
| Camera | cam((x,y),dur,ease) + zoom(factor,dur,ease) | push in on a part, then pull back — cinematography |
| Pin a HUD | sticky(id) | keep a caption / counter fixed on screen while the camera cam/zooms the world |
| Kinetic type | caption(id,"…",(x,y),size,color) + wordpop(id,dur) / karaoke(id,dur,color) | words pop in, or a highlight sweeps across them |
| Name the parts | text, leader arrow, bracelabel, reference line | say what each thing is |
| Emphasize | flash(id,color), pulse(id), recolor(id,color,dur), glow(id,amt), shake(id), spin(id,deg,dur) | move the eye to the part being discussed |
| Add ambient life | particles(id, circle_or_rect, count, radius, seed) + wander(id,dur) | contained bubbles, dust, stars, data, or molecules without hand-animating dots |
| Show a transfer | link(id,a,b,bend) + flow(id,dur) | a tracked curved connection and a travelling emphasis pulse for energy, signals, traffic, or attention |
| Preserve identity | watermark(mark) or watermark(mark,(x,y),"text") | a persistent screen-fixed mark; use the responsive bottom-right default or move it away from platform UI / important content |
| Broadcast | flash(id.parts, lime) | hit every part of the sim at once |
| State the law | a text with the formula | tie the motion to the equation |
| Multiple readings | reveal well / phase / timegraph / energygraph, then run | one motion, several synchronized views |
| Declutter | fade(...) the static annotations | clean playback |
Careful with
section("…"). It drops a full-screen title card (an 820×240 backdrop over the whole stage) and holds it ~2.2 s. Two or three of those and the interstitials start to bury the animation you’re trying to show. Reach for it only for a genuine hard scene change; for beat-to-beat chapters prefer a lightweight persistent kicker/captionupdated withsay, or let the typewriter / camera / kinetic-type do the pacing. Vary the lens per scene — if every lesson uses the identicalsection+say+showloop, the medium looks one-note; the point of the kit above is that manic has range.
Reveal-order gotchas
show/fadeforce opacity to 1 / 0. They will override a deliberately faint entity (a dim reference line, a panel frame). To bring in something that should stay faint,drawit (traces the stroke, leaves opacity alone) or justdisplayit faint from the start — don’tshowit.drawis the nicest reveal for strokes — lines, arrows, curves, and paths sketch themselves on. Set the entityuntracedfirst, thendraw(id, dur).fadethe annotations before the motion so the swing plays against a clean stage.- Loops (
+timeline) reference only ids that exist — build every part before the script that animates it.
Worked examples
The gallery pairs each elevated lesson with its minimal reference — and each one leads with a different lens, to show the range:
- Anatomy of a Spring (
spring-annotated) — typewriter + live data: atyped lab-note with acursor, andcounters that tick k and the period up, over Hooke’s law → parabolic well → SHM. - The Atwood Machine (
pulley-annotated) — camera:cam+zoompush in on the masses for the imbalance beat, then pull back to release; the caption + counter aresticky, so they stay pinned through the zoom. - Brachistochrone race (
brachistochrone-annotated) — kinetic typography on a multi-body sim:wordpopthe question,karaokethe path names as the curves sketch on, then crown the cycloid.
Notice none of them use section — the pacing comes from the lens instead, so the
motion is never hidden behind a card.
The three-move recipe is domain-agnostic: it elevates anything with addressable parts, not just the physics kit.
Going 3D
So far every shape has lived on a flat page. manic has a second world: a real 3D space you look at through a camera. The idea is exactly the same — you name a cast of shapes and call them out in a script — but now the shapes have depth, and you can spin the camera around them.
One rule of thumb: everything 3D ends in 3 — cube3, sphere3, move3,
orbit3. That’s how you tell the two worlds apart.
First, a camera
A 3D scene needs a camera — an eye to look through. You say where the eye sits and which point it looks at:
camera3((8, -10, 6), (0, 0, 1), 45); // eye position, look-at point, zoom
Positions are (x, y, z), and here z is up (x and y are the ground). You
add one camera, and you can swing it around later.
▶ See it play:
The 3D cast
| shape | write | draws |
|---|---|---|
| cube3 | cube3(box, (0,0,1), (2,2,2)); | a box (width, depth, height) |
| sphere3 | sphere3(ball, (0,0,1), 0.9); | a ball of that radius |
| point3 | point3(p, (1,1,1)); | a small marker in space |
| line3 / arrow3 | arrow3(v, (0,0,0), (0,0,2)); | a segment / a vector |
| grid3 | grid3(floor, (0,0,0), 5, 1); | a ground grid to sit things on |
| axes3 | axes3(ax, (0,0,0), 3); | labelled x, y, z arrows |
Style and reveal them with words you already know — color, opacity, show,
flash:
cube3(box, (0, 0, 1), (2, 2, 2)); color(box, cyan);
show(box, 0.6);
Labelling a point in space
To put words on a 3D point, make an ordinary 2D text and pin it there with
pin3. As the camera moves, the label sticks to its point:
text(tag, (0, 0), "origin");
pin3(tag, (0, 0, 0));
Curves and surfaces
Draw a wire through space from three formulas of t (a helix, here), or a
surface from a height formula z = f(x, y):
curve3(helix, "cos(t)", "sin(t)", "t*0.2", (0, 12));
surface3(wave, "sin(x)*cos(y)", (-3, 3), (-3, 3));
For shapes a plain height field can’t make — a torus, a Möbius strip —
use param3, which takes three formulas of two parameters, u and v:
param3(torus, "(3 + cos(v))*cos(u)", "(3 + cos(v))*sin(u)", "sin(v)",
(0, 6.28), (0, 6.28));
One formula rule: always put
*between names. Writepi*t, neverpit(manic readspitas one unknown word). Same forv*v, notvv.
Solids
Build filled, shaded solids:
prism3/pyramid3— n-sided prisms and cones (use many sides for a cylinder or a smooth cone).revolve3— spin a radius profiler(t)around the upright axis (vases, spheres, lathe shapes).extrude3— lift a flat 2D shape (even a boolean cut-out) straight up into a solid.
prism3(hex, (0, 0, 1), 6, 1, 2);
revolve3(vase, (3, 0, 1.5), "0.7 + 0.4*sin(t*2)", (0, 3));
Giving lines some body
A 3D line, arrow, or curve is a thin thread by default. thick turns it into a
rounded tube (arrows grow a solid head):
arrow3(v, (0, 0, 0), (2, 2, 2)); thick(v, 0.04);
Moving in 3D
Same rhythm as the 2D verbs, with the 3 on the end:
par {
rotate3(box, (0, 0, 360), 4, linear); // spin the box
orbit3(70, 25, 12, 4, smooth); // orbit the camera around it
roll3(-20, 4, smooth); // bank around the view direction
}
move3/shift3— move to / by a pointrotate3— turn it (degrees around x, y, z)grow3— stretch a line or arrow’s tip to a new pointorbit3— swing the camera (angle around, angle up, distance)roll3— bank the camera around its viewing direction; it can run besideorbit3inpar, including through stable overhead/underside viewslook3— aim the camera at a new point
Morphing one shape into another
morph3 sets a shape up to become another; then to(..., morph, ...) blends
between them. It works for curves, surfaces, and solids — even a cube turning
into a sphere:
cube3(a, (0, 0, 1), (2, 2, 2));
sphere3(b, (0, 0, 0), 1.2); hidden(b);
morph3(a, b);
to(a, morph, 1, 2.5, smooth); // a cube melts into a ball
Which words work in 3D?
3D shapes speak most of the same vocabulary — color, opacity, hidden,
untraced, tag, and the verbs show, fade, draw, flash, pulse,
scale. A handful of words are 2D-only and will politely refuse on a 3D
shape (with a message that names the 3D replacement):
| if you reach for… | on a 3D shape, use… |
|---|---|
hue | color with a palette name |
stroke | thick |
move / rotate / spin | move3 / rotate3 |
cam / zoom | camera3 / orbit3 |
morph | morph3 |
Now see it all in motion in the 3D scenes gallery.
Examples gallery
Every animation in examples/, by topic — the code and the clip for each. Run any of them with manic examples/<name>.manic. Project: https://8gwifi.org/manic.
- Algorithms & data structures — 6 examples
- Graphs — 4 examples
- Calculus & functions — 19 examples
- Linear algebra & tables — 12 examples
- Statistics & probability — 13 examples
- Physics — simulations — 53 examples
- Optics — light as geometry — 14 examples
- Vectors, fields & coordinates — 3 examples
- Geometry (olympiad) — 8 examples
- Transforms & morphing — 3 examples
- Text & UI — 8 examples
- Generative & recursive — 10 examples
- Creator formats — responsive social video — 14 examples
- Boolean shapes — 1 example
- 3D scenes — 12 examples
Algorithms & data structures
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
bubble_sort
Real sliding swaps; array + compare + swap.
// Bubble Sort — real sliding swaps. `array` gives fixed slot boxes and value
// cells; `compare(a, i, j)` flashes the values now in slots i and j, and
// `swap(a, i, j)` slides them past each other into the swapped slots. `swap`
// carries the array's occupancy forward, so a whole chain of swaps composes
// correctly (no `say`). We sort [3, 1, 2] -> [1, 2, 3].
//
// manic examples/bubble_sort.manic
title("Bubble Sort");
canvas("16:9");
text(head, (cx, 130), "compare neighbours, swap if out of order");
display(head); color(head, cyan); size(head, 26); hidden(head);
text(cap, (cx, 560), ""); color(cap, dim); size(cap, 26);
array(a, "3 1 2", (cx, 360), 100, 100);
show(head, 0.5);
say(cap, "an unsorted array");
wait(0.6);
section("Pass 1");
say(cap, "compare slots 0 and 1: 3 > 1, swap");
compare(a, 0, 1);
swap(a, 0, 1);
say(cap, "compare slots 1 and 2: 3 > 2, swap");
compare(a, 1, 2);
swap(a, 1, 2);
section("Pass 2");
say(cap, "compare slots 0 and 1: 1 < 2, ok");
compare(a, 0, 1, lime);
say(cap, "compare slots 1 and 2: 2 < 3, ok");
compare(a, 1, 2, lime);
section("Sorted");
say(cap, "1 2 3 -- done");
recolor(a.cells, lime, 0.4);
par { pulse(a.c0); pulse(a.c1); pulse(a.c2); }
wait(1.4);
two_pointer
lo/hi index carets scanning inward on a sorted array.
// Two Pointers — the `pointer`/`pointat` primitive. `pointer(id, arr, slot, label)`
// drops a caret under a slot; `pointat(id, arr, slot)` slides it to another slot
// (its label follows). Pointers track slot *positions*, so they stay put as
// values move. Here `lo`/`hi` scan inward on a sorted array.
//
// manic examples/two_pointer.manic
title("Two Pointers");
canvas("16:9");
text(head, (cx, 120), "two pointers scan toward the middle");
display(head); color(head, cyan); size(head, 26); hidden(head);
text(cap, (cx, 560), ""); color(cap, dim); size(cap, 26);
array(a, "1 3 5 7 9 11", (cx, 340), 92, 92);
pointer(lo, a, 0, "lo");
pointer(hi, a, 5, "hi");
show(head, 0.5);
say(cap, "start at both ends");
wait(0.5);
compare(a, 0, 5);
say(cap, "step both inward");
par { pointat(lo, a, 1); pointat(hi, a, 4); }
compare(a, 1, 4);
par { pointat(lo, a, 2); pointat(hi, a, 3); }
compare(a, 2, 3);
say(cap, "the pointers have met");
wait(1.2);
stack_queue
LIFO stack + FIFO queue, with action-point carets.
// Stack & Queue — dynamic structures, with carets marking WHERE each op acts.
// `push`/`pop` (stack, LIFO, grows up) and `enqueue`/`dequeue` (queue, FIFO,
// grows right) are mutating verbs: they add a cell and animate it in, tracking
// occupancy so a chain of ops composes. A `caret` marks the action point and is
// `move`d in step with each op so it rides the changing top / back.
//
// manic examples/stack_queue.manic
title("Stack & Queue");
canvas("16:9");
let sx = 300; let sy = 500; // stack anchor (bottom cell centre)
let qx = 780; let qy = 300; // queue anchor (front cell centre)
let cw = 84; let ch = 64; // cell size
let stx = sx + 62; // stack "top" caret sits right of the column
text(head, (cx, 80), "LIFO stack, FIFO queue");
display(head); color(head, cyan); size(head, 28); hidden(head);
text(cap, (cx, 660), ""); color(cap, dim); size(cap, 24);
text(sl, (sx, sy + 66), "stack: push / pop"); color(sl, lime); size(sl, 22);
text(ql, (qx + cw, qy - 118), "queue: enqueue / dequeue"); color(ql, cyan); size(ql, 22);
stack(st, (sx, sy), cw, ch);
queue(qu, (qx, qy), cw, ch);
// action-point markers
caret(top, (stx, sy), "top", left); // rides the top of the stack
caret(front, (qx, qy - 52), "front", down); // fixed: dequeue leaves here
caret(back, (qx, qy + 52), "back", up); // rides the back of the queue
show(head, 0.5);
section("Stack");
say(cap, "push 5, 3, 8 — each lands on top, the top caret rises");
push(st, "5");
par { push(st, "3"); move(top, (stx, sy - ch)); }
par { push(st, "8"); move(top, (stx, sy - 2*ch)); }
say(cap, "pop — the top value (8) leaves, the caret drops back");
par { pop(st); move(top, (stx, sy - ch)); }
wait(0.5);
section("Queue");
say(cap, "enqueue A, B, C — they join at the back caret");
enqueue(qu, "A");
par { enqueue(qu, "B"); move(back, (qx + cw, qy + 52)); }
par { enqueue(qu, "C"); move(back, (qx + 2*cw, qy + 52)); }
say(cap, "dequeue — the front leaves, the rest advance, back shifts in");
par { dequeue(qu); move(back, (qx + cw, qy + 52)); }
par { dequeue(qu); move(back, (qx, qy + 52)); }
say(cap, "stack: in and out at the top. queue: in at back, out at front.");
wait(1.2);
linked_list
Singly / doubly / circular — classic node anatomy + pointer re-threading.
// Linked List — classic anatomy, three variations. A node is a framed box split
// into compartments: singly `[ data | .next ]`, doubly `[ .prev | data | next. ]`,
// where a pointer field carries a dot its arrow starts from. `head` marks the
// entry node; the tail's `next` ends at `NULL` (singly/doubly) or curves back to
// the head (circular). `insert`/`remove` re-thread the pointers — no shifting.
//
// manic examples/linked_list.manic
title("Linked List");
canvas("16:9");
text(head, (cx, 56), "node = data + pointer field(s); head in, NULL or loop at the tail");
display(head); color(head, cyan); size(head, 24); hidden(head);
text(cap, (cx, 700), ""); color(cap, dim); size(cap, 24);
text(t1, (150, 160), "singly"); color(t1, lime); size(t1, 22);
text(t2, (150, 350), "doubly"); color(t2, lime); size(t2, 22);
text(t3, (150, 545), "circular"); color(t3, lime); size(t3, 22);
list(sa, "3 8 5", (cx, 160), singly, 64, 50);
list(da, "3 8 5", (cx, 350), doubly, 64, 50);
list(ca, "3 8 5", (cx, 545), circular, 64, 50);
show(head, 0.5);
say(cap, "three classic variations of the same idea");
wait(0.9);
section("Insert");
say(cap, "insert 7 after node 1 in the doubly list — pointers re-thread, no shift");
insert(da, 1, "7");
wait(0.6);
section("Remove");
say(cap, "remove the head of the singly list — the list re-points past it");
remove(sa, 0);
say(cap, "data + pointers: the whole family from one primitive");
wait(1.4);
textbook-doubly-list
The paper/support TEXTBOOK treatment on the algo kit — proof it’s domain-agnostic:
template("paper") + a hatched support rule turn the doubly linked list into a flat
exam-paper figure (the theme’s palette remap inks it automatically), then it inserts
and removes. Same styling that dressed the physics sims, on a data structure.
// ============================================================================
// textbook-doubly-list.manic — the paper/support treatment on the ALGO kit
// ----------------------------------------------------------------------------
// Proof the textbook look is DOMAIN-AGNOSTIC: the same `template("paper")` page,
// the same std `support` hatch primitive, and the same `color`/`say` staging —
// here on the algo kit's doubly linked list instead of a physics sim. Nothing
// about `support`/`paper` knows about physics; they compose over any kit.
// ============================================================================
title("Doubly linked list — textbook style");
canvas("16:9");
template("paper"); // the white exam-paper page (engine theme)
text(hdr, (cx, 66), "Doubly linked list");
color(hdr, fg); size(hdr, 30); bold(hdr); display(hdr);
support(rule, (cx, 102), 380, "down"); // a hatched section rule — a std primitive, in an ALGO scene
// the algo-kit structure — the paper theme's palette REMAP turns the sim's
// neon colours into ink automatically (light boxes, dark text, muted pointers),
// so no recolouring is needed. The bare id `dll` also broadcasts now, e.g.
// `hidden(dll)` / `flash(dll, cyan)` address the whole list at once.
list(dll, "3 8 5 2", (cx, 340), doubly, 80, 58);
text(cap, (cx, 628), ""); color(cap, fg); size(cap, 22); display(cap);
say(cap, "each node = [ ·prev | data | next· ] — data plus two pointers");
wait(1.1);
say(cap, "insert 7 after node 1: the prev/next pointers re-thread — nothing shifts");
insert(dll, 1, "7");
wait(0.9);
say(cap, "remove the head: the list simply re-points past it");
remove(dll, 0);
wait(1.2);
hashmap
Hash a key to a bucket; collisions chain on (separate chaining).
// Hash Map — separate chaining. `hashmap(id, n, (cx,cy))` draws n numbered
// buckets in a column; `put(id, key, val)` hashes the key to a bucket and chains
// the `key:val` entry on (collisions extend the chain); `get(id, key)` hashes,
// then scans that bucket's chain — each entry flashes until the key matches
// (lime) or the chain ends (bucket flashes magenta = miss).
//
// (Hash = sum of the key's bytes mod n. "cat" and "act" are anagrams, so they
// collide — same bucket, chained.)
//
// manic examples/hashmap.manic
title("Hash Map");
canvas("16:9");
text(head, (cx, 60), "separate chaining: hash the key, chain on collision");
display(head); color(head, cyan); size(head, 26); hidden(head);
text(cap, (cx, 690), ""); color(cap, dim); size(cap, 24);
hashmap(ht, 5, (360, 360), 128, 46);
show(head, 0.5);
say(cap, "put cat, dog, ok — each hashes to its bucket");
put(ht, "cat", "7");
put(ht, "dog", "3");
put(ht, "ok", "1");
wait(0.5);
section("Collision");
say(cap, "put act — same bytes as cat, so it collides and chains on");
put(ht, "act", "9");
wait(0.6);
section("Lookup");
say(cap, "get act — scan the chain in bucket 2 until the key matches");
get(ht, "act");
say(cap, "get xyz — hashes to a bucket, scans, falls off the end: miss");
get(ht, "xyz");
wait(1.2);
Graphs
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
graph
Labelled nodes + edges, drawn on; reflowing links.
// Graph — the algo kit's `graph` builtin (Manim's Graph/DiGraph). Nodes are
// labelled circles; `a-b` is an undirected edge, `a>b` a directed arrow. Tag
// broadcast (`draw(g.edges)`, `flash(g.nodes, …)`) animates whole groups.
//
// manic examples/graph.manic
// manic examples/graph.manic --record out --fps 60
title("Graph");
canvas(1280, 720);
text(head, (640, 118), "a directed graph, traversed");
display(head); color(head, cyan); size(head, 34); hidden(head);
text(cap, (640, 664), ""); color(cap, dim); size(cap, 22);
// six vertices in a circle; directed edges (a>b)
graph(g, "a b c d e f",
"a>b b>c c>d d>e e>f f>a a>d b>e",
circular, (640, 384), 210);
// nodes fade in (hidden→show); edges trace on (untraced→draw)
hidden(g.nodes);
untraced(g.edges);
show(head, 0.5);
say(cap, "drop in the vertices");
show(g.nodes, 0.4); // broadcasts over every node
say(cap, "connect the directed edges");
draw(g.edges, 0.6); // broadcasts over every edge
section("Traversal");
say(cap, "walk a > b > c > d");
seq {
flash(g.a, magenta);
flash(g.b, magenta);
flash(g.c, magenta);
flash(g.d, magenta);
}
say(cap, "highlight the visited path");
par {
recolor(g.a, lime, 0.4);
recolor(g.b, lime, 0.4);
recolor(g.c, lime, 0.4);
recolor(g.d, lime, 0.4);
}
wait(1.2);
graph_moving
Drag a vertex and its incident edges follow.
// Moving Graph — vertices move and the edges reflow to follow them
// (Manim's MovingVertices / MovingDiGraph). Also exercises layout reveal,
// per-node moves, tag-broadcast recolour, and a highlight sweep.
//
// manic examples/graph_moving.manic
// manic examples/graph_moving.manic --record out --fps 60
//
// Node ids are g.1 … g.4 ; edge ids g.1-2 etc ; tags g.nodes / g.edges.
title("Moving Graph");
canvas(1280, 720);
text(head, (640, 118), "edges follow their vertices");
display(head); color(head, cyan); size(head, 34); hidden(head);
text(cap, (640, 664), ""); color(cap, dim); size(cap, 22);
// four vertices, five undirected edges, circular to start
graph(g, "1 2 3 4", "1-2 2-3 3-4 1-3 1-4", circular, (640, 384), 150);
hidden(g.nodes);
untraced(g.edges);
// --- reveal ---
show(head, 0.5);
say(cap, "a small graph");
show(g.nodes, 0.4);
draw(g.edges, 0.6);
wait(0.5);
// --- case 1: fling the vertices to the corners; edges reflow live ---
section("Moving vertices");
say(cap, "move each vertex — the edges stretch to follow");
par {
move(g.1, (360, 250), 1.2, overshoot);
move(g.2, (920, 250), 1.2, overshoot);
move(g.3, (920, 520), 1.2, overshoot);
move(g.4, (360, 520), 1.2, overshoot);
}
wait(0.8);
// --- case 2: orbit two vertices past each other ---
say(cap, "swap two vertices");
par {
move(g.2, (360, 520), 1.0, smooth);
move(g.4, (920, 250), 1.0, smooth);
}
wait(0.8);
// --- case 3: pull one vertex around; incident edges track it ---
say(cap, "drag one vertex around");
seq {
move(g.1, (640, 150), 0.7, smooth);
move(g.1, (1080, 384), 0.7, smooth);
move(g.1, (640, 620), 0.7, smooth);
move(g.1, (360, 250), 0.7, smooth);
}
wait(0.6);
// --- case 4: recolour the whole graph via tag broadcast, then highlight ---
section("Styling");
say(cap, "recolour every edge, then highlight a node");
recolor(g.edges, cyan, 0.5);
flash(g.1, magenta);
par {
recolor(g.1, lime, 0.4);
pulse(g.1);
}
wait(1.5);
bfs_dfs
The same graph, queue vs stack, with live frontier readouts.
// Graph Traversal — BFS vs DFS, the classic side-by-side. They're the SAME
// algorithm; only the frontier differs: BFS uses a QUEUE (explore level by
// level), DFS uses a STACK (dive deep first). `bfs(g, start)` / `dfs(g, start)`
// read the graph's adjacency, run the traversal, and animate the textbook
// states — discovered (cyan) -> current (magenta) -> done (lime) — with tree
// edges lighting up and live `queue:` / `stack:` + `visited:` readouts.
//
// manic examples/bfs_dfs.manic
title("Graph Traversal");
canvas("16:9");
text(head, (cx, 56), "BFS vs DFS: same graph, queue vs stack");
display(head); color(head, cyan); size(head, 26); hidden(head);
text(cap, (cx, 690), ""); color(cap, dim); size(cap, 24);
graph(gr, "a b c d e f g", "a-b a-c b-d b-e c-f c-g", circular, (cx, 320), 200, 30);
show(head, 0.5);
wait(0.4);
section("BFS");
say(cap, "BFS explores level by level, using a QUEUE");
bfs(gr, a);
say(cap, "queue order: a, then b c, then d e f g");
wait(0.8);
section("DFS");
par { recolor(gr.nodes, panel, 0.4); recolor(gr.edges, dim, 0.4); }
say(gr.frontier, "stack:");
say(gr.visited, "visited:");
say(cap, "DFS dives deep down one branch, using a STACK");
dfs(gr, a);
say(cap, "the stack drove it depth-first before backtracking");
wait(1.2);
dijkstra
Weighted edges, settling distances, a shortest-path tree.
// Dijkstra — single-source shortest paths on a WEIGHTED graph. Give edges a
// weight with `a-b:w` (drawn as a midpoint label). `dijkstra(g, start)` reads the
// weights, then runs the classic loop: repeatedly settle the nearest unsettled
// node (magenta -> lime), relaxing its edges and lowering neighbours' distances.
// Each node shows its best-known distance (inf -> the final shortest distance),
// and the shortest-path-tree edges stay lit at the end.
//
// manic examples/dijkstra.manic
title("Dijkstra");
canvas("16:9");
text(head, (cx, 58), "shortest paths: settle the nearest node, relax its edges");
display(head); color(head, cyan); size(head, 25); hidden(head);
text(cap, (cx, 690), ""); color(cap, dim); size(cap, 24);
graph(g, "a b c d e f",
"a-b:2 a-c:5 b-c:1 b-d:4 c-e:3 d-e:1 d-f:2 e-f:6",
circular, (cx, 350), 210, 30);
show(head, 0.5);
say(cap, "each node shows its best distance: start 0, the rest inf");
wait(0.7);
section("Relax");
say(cap, "settle the nearest node, then relax every edge leaving it");
dijkstra(g, a);
say(cap, "settled distances are final; the lime edges form the shortest-path tree");
wait(1.4);
Calculus & functions
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
calculus-demo
The flagship: two big ideas on one curve. A tangent slides along a bell curve with a live slope readout (flat at the peak), then the area sweeps open while the integral climbs to its true value — on properly numbered, scaled axes.
title("Calculus, visualized — the derivative and the integral");
canvas("16:9");
// screen anchor for the plot: math (0,0) maps here. Placing it at cx-450 with
// sx=150 centres the domain 0..6 under the canvas centre.
let ox = cx - 450;
let oy = cy + 120;
let sx = 150; // screen px per unit of x
let sy = 170; // screen px per unit of y
// ---------------- header ----------------
text(hdr, (cx, 54), "Two big ideas, one curve");
size(hdr, 32); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
text(flabel, (cx, 94), "f(x) = a smooth bell-shaped curve");
size(flabel, 18); color(flabel, dim); display(flabel); hidden(flabel);
// ---------------- the curve ----------------
axes(ax, (ox, oy), 930, 205, 1);
color(ax, dim); untraced(ax);
// axis labels, at the arrow tips
text(xlab, (ox + 958, oy + 6), "x");
size(xlab, 24); color(xlab, dim); display(xlab); hidden(xlab);
text(ylab, (ox, oy - 232), "f(x)");
size(ylab, 24); color(ylab, dim); display(ylab); hidden(ylab);
// numbered scale — each label sits at the plot's real coordinate:
// x = k -> (ox + k*sx, oy) y = v -> (ox, oy - v*sy)
line(tx1, (ox + 1*sx, oy - 7), (ox + 1*sx, oy + 7)); color(tx1, dim); tag(tx1, scale); hidden(tx1);
line(tx2, (ox + 2*sx, oy - 7), (ox + 2*sx, oy + 7)); color(tx2, dim); tag(tx2, scale); hidden(tx2);
line(tx3, (ox + 3*sx, oy - 7), (ox + 3*sx, oy + 7)); color(tx3, dim); tag(tx3, scale); hidden(tx3);
line(tx4, (ox + 4*sx, oy - 7), (ox + 4*sx, oy + 7)); color(tx4, dim); tag(tx4, scale); hidden(tx4);
line(tx5, (ox + 5*sx, oy - 7), (ox + 5*sx, oy + 7)); color(tx5, dim); tag(tx5, scale); hidden(tx5);
line(tx6, (ox + 6*sx, oy - 7), (ox + 6*sx, oy + 7)); color(tx6, dim); tag(tx6, scale); hidden(tx6);
text(nx1, (ox + 1*sx, oy + 28), "1"); size(nx1, 16); color(nx1, dim); tag(nx1, scale); hidden(nx1);
text(nx2, (ox + 2*sx, oy + 28), "2"); size(nx2, 16); color(nx2, dim); tag(nx2, scale); hidden(nx2);
text(nx3, (ox + 3*sx, oy + 28), "3"); size(nx3, 16); color(nx3, dim); tag(nx3, scale); hidden(nx3);
text(nx4, (ox + 4*sx, oy + 28), "4"); size(nx4, 16); color(nx4, dim); tag(nx4, scale); hidden(nx4);
text(nx5, (ox + 5*sx, oy + 28), "5"); size(nx5, 16); color(nx5, dim); tag(nx5, scale); hidden(nx5);
text(nx6, (ox + 6*sx, oy + 28), "6"); size(nx6, 16); color(nx6, dim); tag(nx6, scale); hidden(nx6);
line(tyh, (ox - 7, oy - 0.5*sy), (ox + 7, oy - 0.5*sy)); color(tyh, dim); tag(tyh, scale); hidden(tyh);
line(ty1, (ox - 7, oy - 1*sy), (ox + 7, oy - 1*sy)); color(ty1, dim); tag(ty1, scale); hidden(ty1);
text(nyh, (ox - 36, oy - 0.5*sy), "0.5"); size(nyh, 16); color(nyh, dim); tag(nyh, scale); hidden(nyh);
text(ny1, (ox - 24, oy - 1*sy), "1"); size(ny1, 16); color(ny1, dim); tag(ny1, scale); hidden(ny1);
plot(f, (ox, oy), sx, sy, "exp(-0.5*(x-3)*(x-3))", (0, 6));
color(f, cyan); stroke(f, 4); untraced(f);
// ---------------- captions (swap per act) ----------------
text(capd, (cx, h - 46), "THE DERIVATIVE — the slope at every point");
size(capd, 22); color(capd, gold); bold(capd); display(capd); hidden(capd);
text(capi, (cx, h - 46), "THE INTEGRAL — the area underneath");
size(capi, 22); color(capi, lime); bold(capi); display(capi); hidden(capi);
text(endcap, (cx, h - 46), "two views of one curve");
size(endcap, 22); color(endcap, cyan); bold(endcap); display(endcap); hidden(endcap);
// ---------------- derivative pieces ----------------
tangent(tan, f, 0.4, 220);
color(tan, gold); hidden(tan);
slope(sl, f, 0.4);
color(sl, gold); hidden(sl);
// ---------------- integral pieces ----------------
area(reg, f, 0, 0);
color(reg, lime);
text(ivlabel, (cx + 330, cy - 150), "area so far");
size(ivlabel, 16); color(ivlabel, dim); display(ivlabel); hidden(ivlabel);
integral(iv, f, 0, 0, (cx + 330, cy - 110));
size(iv, 30); color(iv, lime); hidden(iv);
// ================= timeline =================
show(hdr, 0.6);
show(flabel, 0.4);
draw(ax, 0.8);
par {
show(xlab, 0.4);
show(ylab, 0.4);
show(scale, 0.4);
}
draw(f, 1.6);
wait(0.4);
// --- Act 1: the derivative ---
show(capd, 0.5);
show(tan, 0.5);
show(sl, 0.4);
wait(0.3);
par { // climb to the peak — slope goes to zero
to(tan, x, 3, 2.2);
to(sl, x, 3, 2.2);
}
pulse(sl, 0.5);
wait(0.3);
par { // over the top — slope turns negative
to(tan, x, 5.6, 2.6);
to(sl, x, 5.6, 2.6);
}
wait(0.4);
// --- Act 2: the integral ---
par {
fade(capd, 0.4);
fade(tan, 0.4);
fade(sl, 0.4);
}
show(capi, 0.5);
show(ivlabel, 0.3);
show(iv, 0.3);
par { // sweep the area open, number climbs
to(reg, x, 6, 4);
to(iv, x, 6, 4);
}
wait(0.4);
flash(iv, lime);
pulse(iv, 0.5);
wait(0.4);
// --- close ---
fade(capi, 0.4);
show(endcap, 0.6);
sine_wave
axes + plot, a curve traced on, then vectors.
// The Sine Wave — a first taste of the manic math kit.
// manic examples/sine_wave.manic
// manic examples/sine_wave.manic --still 2.6 --scale 1.5 --crt
title("The Sine Wave");
canvas(1280, 720);
// --- cast: the world at t = 0 ---
// a coordinate frame centred on the stage
axes(ax, (640, 380), 520, 240);
text(xlab, (1180, 410), "x"); color(xlab, dim); size(xlab, 22);
text(ylab, (665, 152), "y"); color(ylab, dim); size(ylab, 22);
// the curve: visible but not yet drawn, so we can trace it on
plot(wave, (640, 380), 78, 120, sin, 6.6);
untraced(wave);
// a vector to point at, revealed later
vector(v1, (640, 380), (122, 108));
hidden(v1);
// headline + caption
text(head, (640, 118), "y = sin(x)");
display(head); color(head, cyan); size(head, 40); hidden(head);
text(cap, (640, 662), ""); color(cap, dim); size(cap, 22);
// --- script: beats, top to bottom ---
show(head, 0.5);
say(cap, "a coordinate frame on the void");
draw(wave, 1.7);
say(cap, "y = sin(x), traced on");
wait(0.6);
section("Vectors");
say(cap, "a vector from the origin");
par {
show(v1, 0.4);
pulse(v1);
}
wait(1.2);
function_graph
Plot an expression straight from a formula string.
// Function Graphs — plot ANY formula, not just a named curve. manic's answer to
// Manim's FunctionGraph(lambda t: ...): pass a formula string in x (alias t) and
// plot() samples it. This reproduces Manim's ExampleFunctionGraph — two
// Fourier-style packets and a domain-clipped, lifted copy of the second.
//
// manic examples/function_graph.manic
// manic examples/function_graph.manic --record out --fps 60
title("Function Graphs");
canvas(1280, 720);
text(head, (640, 92), "plot any formula — y = f(x)");
display(head); color(head, cyan); size(head, 26); hidden(head);
text(cap, (640, 656), ""); color(cap, dim); size(cap, 22);
// a faint frame to read the curves against (unit = 70 px)
plane(pl, (640, 384), 620, 300, 70);
hidden(pl.grid); untraced(pl.x); untraced(pl.y);
// a cosine packet: cos t + 1/2 cos 7t + 1/7 cos 14t, over x in [-7, 7]
plot(cosf, (640, 384), 70, 70, "cos(x) + 0.5*cos(7*x) + (1/7)*cos(14*x)", 7);
color(cosf, magenta); untraced(cosf);
// the sine version of the same packet
plot(sinf, (640, 384), 70, 70, "sin(x) + 0.5*sin(7*x) + (1/7)*sin(14*x)", 7);
color(sinf, cyan); untraced(sinf);
// same formula, clipped to x in [-4, 4] and lifted one unit (centre y - 70)
plot(sinf2, (640, 314), 70, 70, "sin(x) + 0.5*sin(7*x) + (1/7)*sin(14*x)", 4);
color(sinf2, lime); untraced(sinf2);
// --- reveal ---
show(head, 0.5);
section("The plane");
say(cap, "a grid to read against — arrows on the axes");
show(pl.grid, 0.6);
par { draw(pl.x, 0.5); draw(pl.y, 0.5); }
wait(0.3);
section("A cosine packet");
say(cap, "y = cos t + 1/2 cos 7t + 1/7 cos 14t");
draw(cosf, 1.3);
wait(0.6);
section("A sine packet");
say(cap, "same shape, sin for cos");
draw(sinf, 1.3);
wait(0.6);
section("Clip the domain");
say(cap, "same formula, but only x in [-4, 4], lifted one unit");
draw(sinf2, 1.1);
par { pulse(cosf); pulse(sinf); pulse(sinf2); }
wait(1.4);
area_under_curve
Riemann rectangles sweeping to the integral.
// Area Under a Curve — a Riemann sum sweeping n = 5, 10, 20, 40 to show the
// rectangles converging to the exact integral of x^2 on [0, 2.5] = 125/24.
//
// This is the FIRST example to use manic's loop layer: `let` variables,
// arithmetic in arguments, a `for` range loop, and id interpolation (`s5{i}`).
// The four bar-sets differ only in n / prefix / colour — a future `def` macro
// would collapse them to one call; loops already do the per-bar work.
//
// manic examples/area_under_curve.manic
// manic examples/area_under_curve.manic --record out --fps 60
title("Area Under a Curve");
canvas(1280, 720);
// --- parameters (edit freely) ---
let ox = 360; let oy = 590; // origin, in screen px
let ux = 200; let uy = 52; // px per unit on each axis
let a = 0; let b = 2.5; // integrate x^2 over [a, b]
text(head, (640, 96), "a Riemann sum becomes an integral");
display(head); color(head, cyan); size(head, 26); hidden(head);
text(cap, (640, 656), ""); color(cap, dim); size(cap, 24);
// axes
arrow(xax, (ox - 40, oy), (920, oy)); color(xax, dim); untraced(xax);
arrow(yax, (ox, oy + 20), (ox, 250)); color(yax, dim); untraced(yax);
text(t1, (ox + 1*ux, oy + 24), "1"); color(t1, dim); size(t1, 18);
text(t2, (ox + 2*ux, oy + 24), "2"); color(t2, dim); size(t2, 18);
text(tb, (ox + b*ux, oy + 24), "2.5"); color(tb, dim); size(tb, 18);
// the curve y = x^2 over [0, 2.5]
plot(curve, (ox, oy), ux, uy, "x*x", (a, b)); color(curve, cyan); z(curve, 3); untraced(curve);
text(clab, (ox + b*ux + 30, oy - b*b*uy), "y = x^2"); color(clab, cyan); size(clab, 22); hidden(clab);
// --- midpoint rectangles, one loop per count ---
let n = 5; let dx = (b - a) / n;
for i in 0..n {
let mid = a + (i + 0.5) * dx; let h = mid * mid;
rect(s5{i}, (ox + mid*ux, oy - h*uy/2), dx*ux, h*uy);
filled(s5{i}); color(s5{i}, magenta); opacity(s5{i}, 0.4); tag(s5{i}, r5);
}
let n = 10; let dx = (b - a) / n;
for i in 0..n {
let mid = a + (i + 0.5) * dx; let h = mid * mid;
rect(s10{i}, (ox + mid*ux, oy - h*uy/2), dx*ux, h*uy);
filled(s10{i}); color(s10{i}, magenta); opacity(s10{i}, 0.4); tag(s10{i}, r10);
}
let n = 20; let dx = (b - a) / n;
for i in 0..n {
let mid = a + (i + 0.5) * dx; let h = mid * mid;
rect(s20{i}, (ox + mid*ux, oy - h*uy/2), dx*ux, h*uy);
filled(s20{i}); color(s20{i}, magenta); opacity(s20{i}, 0.4); tag(s20{i}, r20);
}
let n = 40; let dx = (b - a) / n;
for i in 0..n {
let mid = a + (i + 0.5) * dx; let h = mid * mid;
rect(s40{i}, (ox + mid*ux, oy - h*uy/2), dx*ux, h*uy);
filled(s40{i}); color(s40{i}, magenta); opacity(s40{i}, 0.4); tag(s40{i}, r40);
}
hidden(r5); hidden(r10); hidden(r20); hidden(r40);
// --- script ---
show(head, 0.5);
say(cap, "the shaded area under y = x^2 from 0 to 2.5");
par { draw(xax, 0.5); draw(yax, 0.5); }
draw(curve, 0.9);
show(clab, 0.3);
wait(0.4);
section("Rectangles");
say(cap, "n = 5 rectangles -> area ~ 5.16");
show(r5, 0.6);
wait(0.7);
fade(r5, 0.3);
say(cap, "n = 10 -> area ~ 5.20");
show(r10, 0.5);
wait(0.6);
fade(r10, 0.3);
say(cap, "n = 20 -> area ~ 5.20");
show(r20, 0.5);
wait(0.6);
fade(r20, 0.3);
say(cap, "n = 40 -> area ~ 5.21 (hugging the curve)");
show(r40, 0.5);
wait(0.8);
section("The integral");
say(cap, "as n grows without bound, the sum IS the integral");
fade(r40, 0.4);
text(ans, (640, 300), "exact area = 125/24 = 5.208"); display(ans); color(ans, lime); size(ans, 30); hidden(ans);
show(ans, 0.5);
pulse(ans);
wait(1.6);
riemann_rainbow
Coloured Riemann rectangles revealed one by one.
// Riemann Rainbow — the area under y = sin(x) on [0, pi], sliced into rectangles
// that each get their own neon hue and rise into place one by one, left to right.
//
// A showcase for the loop layer: one `for` builds all the bars (each `hue`d by
// its index), and a `stagger` block sweeps them in. Exact area = 2.
//
// manic examples/riemann_rainbow.manic
// manic examples/riemann_rainbow.manic --record out --fps 60
title("Riemann Rainbow");
canvas(1280, 720);
// --- parameters ---
let ox = 190; let oy = 560; // origin (screen px)
let ux = 300; let uy = 340; // px per unit
let a = 0; let b = pi; // y = sin(x) over [0, pi]
let n = 28; let dx = (b - a) / n;
text(head, (640, 96), "area under y = sin(x), one slice at a time");
display(head); color(head, cyan); size(head, 26); hidden(head);
text(cap, (640, 640), ""); color(cap, dim); size(cap, 24);
// axes
arrow(xax, (ox - 40, oy), (1180, oy)); color(xax, dim); untraced(xax);
arrow(yax, (ox, oy + 20), (ox, 180)); color(yax, dim); untraced(yax);
text(l0, (ox, oy + 26), "0"); color(l0, dim); size(l0, 18);
text(lp, (ox + b*ux, oy + 26), "pi"); color(lp, dim); size(lp, 18);
// the curve
plot(curve, (ox, oy), ux, uy, "sin(x)", (a, b)); color(curve, fg); z(curve, 5); untraced(curve);
// --- one rainbow bar per slice (midpoint heights) ---
for i in 0..n {
let mid = a + (i + 0.5) * dx;
let h = sin(mid);
rect(bar{i}, (ox + mid*ux, oy - h*uy/2), dx*ux, h*uy);
filled(bar{i});
hue(bar{i}, 360 * i / n); // each slice its own colour
opacity(bar{i}, 0.9);
tag(bar{i}, bars);
}
hidden(bars);
// --- script ---
show(head, 0.5);
say(cap, "y = sin(x) from 0 to pi");
par { draw(xax, 0.5); draw(yax, 0.5); }
draw(curve, 1.0);
wait(0.3);
section("Slice by slice");
say(cap, "28 rectangles rise in, left to right");
stagger(0.05) {
for i in 0..n { show(bar{i}, 0.35); }
}
wait(0.6);
section("The area");
say(cap, "together they fill the area under the curve = 2");
par { pulse(curve); }
wait(1.6);
riemann_readout
Running sums shown as a live computed number.
// Riemann + Live Total — the midpoint area under y = x^2 on [0, 2.5] is
// COMPUTED in-language with a `sum(...)` reduction, and a `counter` readout
// tweens from 0 up to that total while the bars fill in. The number you see
// counting is the reduction's value.
//
// Showcases reductions + animated numeric readouts (`counter` + `to(_, value)`).
//
// manic examples/riemann_readout.manic
// manic examples/riemann_readout.manic --record out --fps 60
title("Riemann + Live Total");
canvas(1280, 720);
let ox = 300; let oy = 560;
let ux = 190; let uy = 52;
let a = 0; let b = 2.5; let n = 40; let dx = (b - a) / n;
// the midpoint Riemann sum, computed at build time
let area = sum(i in 0..n : (a + (i + 0.5)*dx)^2 * dx);
text(head, (640, 90), "area under y = x^2, summed as the bars fill");
display(head); color(head, cyan); size(head, 26); hidden(head);
counter(total, (950, 210), 0, 3, "area = ", "");
display(total); color(total, lime); size(total, 36); hidden(total);
text(exact, (950, 260), "exact 125/24 = 5.208"); color(exact, dim); size(exact, 20); hidden(exact);
// axes
arrow(xax, (ox - 40, oy), (900, oy)); color(xax, dim); untraced(xax);
arrow(yax, (ox, oy + 20), (ox, 210)); color(yax, dim); untraced(yax);
text(t1, (ox + 1*ux, oy + 24), "1"); color(t1, dim); size(t1, 18);
text(t2, (ox + 2*ux, oy + 24), "2"); color(t2, dim); size(t2, 18);
// curve
plot(curve, (ox, oy), ux, uy, "x*x", (a, b)); color(curve, cyan); z(curve, 4); untraced(curve);
// midpoint rectangles
for i in 0..n {
let mid = a + (i + 0.5) * dx;
let h = mid * mid;
rect(bar{i}, (ox + mid*ux, oy - h*uy/2), dx*ux, h*uy);
filled(bar{i}); color(bar{i}, magenta); opacity(bar{i}, 0.45); tag(bar{i}, bars);
}
hidden(bars);
// --- script ---
show(head, 0.5);
par { draw(xax, 0.5); draw(yax, 0.5); }
draw(curve, 0.9);
show(total, 0.3);
show(exact, 0.3);
wait(0.3);
// bars sweep in while the total counts up to the reduction's value
par {
stagger(0.03) { for i in 0..n { show(bar{i}, 0.25); } }
to(total, value, area, 1.6, linear);
}
wait(1.4);
pulse(total);
wait(1.0);
tangent
The tangent line to a curve, sliding along it — its tilt is read from the function itself, so it’s always the true slope (flat at the peaks).
title("The Tangent Line — a curve's slope, made visible");
canvas("16:9");
text(hdr, (cx, 60), "The slope of sin x, everywhere at once");
size(hdr, 30); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
// the curve — plot remembers its function, so tangent can ask it questions.
// center the domain 0..6.3 across the canvas (midpoint x=3.15 sits at cx)
plot(f, (cx - 440, cy + 40), 140, 120, "sin(x)", (0, 6.3));
color(f, cyan); stroke(f, 3); untraced(f);
// a tangent that touches f where x = 0.5 (line + contact dot, one call)
tangent(tan, f, 0.5, 260);
color(tan, gold); untraced(tan);
text(cap, (cx, h - 60), "watch the tilt follow the curve");
size(cap, 20); color(cap, dim); display(cap); hidden(cap);
// ---- timeline ----
show(hdr, 0.5);
draw(f, 1.4);
show(tan, 0.6);
show(cap, 0.4);
wait(0.4);
// slide the touch point along the whole curve — the slope is derived live
to(tan, x, 5.8, 5);
wait(0.3);
to(tan, x, 1.6, 2.5); // back toward the first peak, where the slope is 0
analysis
Ask one curve everything at once — tangent, a live slope number, the normal, the area sweeping open beneath it, and the integral climbing to its true value.
title("Reading a curve: slope, tangent, normal, area");
canvas("16:9");
text(hdr, (cx, 58), "Asking a curve questions");
size(hdr, 30); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
// one curve — everything below asks IT, by name
plot(f, (cx - 440, cy + 30), 140, 110, "sin(x)", (0, 6.3));
color(f, cyan); stroke(f, 3); untraced(f);
// the area under it — starts collapsed at x=0.6, sweeps open
area(reg, f, 0.6, 0.6);
color(reg, lime);
// a live integral readout that climbs as the area sweeps
integral(area_val, f, 0.6, 0.6, (cx + 340, cy - 210));
color(area_val, lime);
// tangent + its live slope number, both riding the same point
tangent(tan, f, 0.6, 240);
color(tan, gold);
slope(sl, f, 0.6);
color(sl, gold);
// the normal, perpendicular at the same place
normal(nrm, f, 0.6, 150);
color(nrm, magenta); hidden(nrm);
// ---- timeline ----
show(hdr, 0.5);
draw(f, 1.4);
show(tan, 0.5);
show(sl, 0.4);
wait(0.4);
// sweep the area open while the tangent + slope slide together and the
// integral readout climbs
par {
to(reg, x, 5.7, 4);
to(area_val, x, 5.7, 4);
to(tan, x, 5.7, 4);
to(sl, x, 5.7, 4);
}
wait(0.4);
// reveal the normal and glide everything back to the first peak (slope 0)
show(nrm, 0.4);
par {
to(tan, x, 1.571, 2.5);
to(sl, x, 1.571, 2.5);
to(nrm, x, 1.571, 2.5);
}
newton
Newton’s method, drawn as a zig-zag: from a first guess, slide down each tangent to the axis, back up to the curve, and watch the guesses walk to the root.
title("Newton's method: chasing a root");
canvas("16:9");
text(hdr, (cx, 56), "Newton's method finds where x squared = 2");
size(hdr, 28); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
// a reference frame so the x-axis (where the tangents land) is visible
axes(ax, (cx - 250, cy + 170), 540, 360, 1);
color(ax, dim); untraced(ax);
// f(x) = x^2 - 2, whose positive root is sqrt(2)
plot(f, (cx - 250, cy + 170), 180, 70, "x*x - 2", (-0.4, 3));
color(f, cyan); stroke(f, 3); untraced(f);
// the true zero-crossing, marked
roots(zero, f);
color(zero, lime); hidden(zero);
// the Newton zig-zag from a first guess of x = 3
newton(walk, f, 3);
color(walk, gold); stroke(walk, 3); untraced(walk);
text(cap, (cx, h - 60), "each tangent slides the guess closer");
size(cap, 20); color(cap, dim); display(cap); hidden(cap);
// ---- timeline ----
show(hdr, 0.5);
draw(ax, 0.8);
draw(f, 1.4);
wait(0.3);
show(cap, 0.4);
// walk the iterates in to the root
draw(walk, 3.5);
wait(0.3);
// reveal the exact root it converged on
show(zero, 0.5);
pulse(zero, 0.6);
inverse-derivatives
Why a function and its inverse have reciprocal slopes: e^x and ln x mirrored
across y = x, with the slopes at matching points multiplying to 1.
title("Inverse-function derivatives — why the pairs mirror");
canvas("16:9");
// uniform scale (sx == sy) so a function and its inverse reflect correctly
let ox = cx - 470;
let oy = cy + 110;
let s = 72;
text(hdr, (cx, 52), "A function and its inverse: mirror curves, reciprocal slopes");
size(hdr, 26); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
// ---------------- the mirror plane ----------------
axes(ax, (ox, oy), 470, 300, 1);
color(ax, dim); untraced(ax);
// y = x, the mirror line
line(diag, (ox - 2.6*s, oy + 2.6*s), (ox + 3.8*s, oy - 3.8*s));
color(diag, dim); untraced(diag);
text(diaglab, (ox + 3.4*s, oy - 3.4*s - 22), "y = x");
size(diaglab, 16); color(diaglab, dim); display(diaglab); hidden(diaglab);
// e^x and its inverse ln x — reflections of each other
plot(ef, (ox, oy), s, s, "exp(x)", (-2.6, 1.45));
color(ef, cyan); stroke(ef, 3); untraced(ef);
text(eflab, (ox + 1.45*s + 8, oy - 4.1*s), "e^x");
size(eflab, 18); color(eflab, cyan); display(eflab); hidden(eflab);
plot(lf, (ox, oy), s, s, "ln(x)", (0.075, 4.3));
color(lf, magenta); stroke(lf, 3); untraced(lf);
text(lflab, (ox + 4.2*s, oy - 1.45*s), "ln x");
size(lflab, 18); color(lflab, magenta); display(lflab); hidden(lflab);
// tangent + live slope at a point on e^x, and at its mirror point on ln x
tangent(te, ef, 0.5, 150);
color(te, cyan); hidden(te);
slope(se, ef, 0.5);
color(se, cyan); hidden(se);
tangent(tl, lf, 1.65, 150);
color(tl, magenta); hidden(tl);
slope(sl, lf, 1.65);
color(sl, magenta); hidden(sl);
text(rec, (cx + 340, cy + 60), "slope x slope = 1");
size(rec, 22); color(rec, lime); bold(rec); display(rec); hidden(rec);
text(rec2, (cx + 340, cy + 96), "(1.65) x (0.61)");
size(rec2, 18); color(rec2, dim); display(rec2); hidden(rec2);
// ---------------- the four identity pairs ----------------
text(t1, (cx + 340, cy - 190), "(e^x)' = e^x (ln x)' = 1/x");
size(t1, 17); color(t1, fg); display(t1); hidden(t1);
text(t2, (cx + 340, cy - 150), "(sin x)' = sqrt(1-sin^2 x) (arcsin x)' = 1/sqrt(1-x^2)");
size(t2, 15); color(t2, fg); display(t2); hidden(t2);
text(t3, (cx + 340, cy - 118), "(cos x)' = -sqrt(1-cos^2 x) (arccos x)' = -1/sqrt(1-x^2)");
size(t3, 15); color(t3, fg); display(t3); hidden(t3);
text(t4, (cx + 340, cy - 86), "(tan x)' = 1+tan^2 x (arctan x)' = 1/(1+x^2)");
size(t4, 15); color(t4, fg); display(t4); hidden(t4);
// ================= timeline =================
show(hdr, 0.6);
draw(ax, 0.7);
draw(diag, 0.5);
show(diaglab, 0.3);
par {
draw(ef, 1.4);
draw(lf, 1.4);
}
par {
show(eflab, 0.4);
show(lflab, 0.4);
}
wait(0.3);
// the reciprocal-slope check
par {
show(te, 0.4);
show(se, 0.4);
}
par {
show(tl, 0.4);
show(sl, 0.4);
}
show(rec, 0.5);
show(rec2, 0.4);
flash(rec, lime);
wait(0.4);
// the four pairs it generates
stagger(0.4) {
show(t1, 0.4);
show(t2, 0.4);
show(t3, 0.4);
show(t4, 0.4);
}
derivative-of-ln-x
A visual proof of (ln x)' = 1/x: one continuous 3D coordinate plane turns over,
x/y exchange screen roles, and unchanged algebra terms retain their identity.
// ============================================================================
// derivative-of-ln-x.manic — inverse-function derivatives on a turning plane
// ----------------------------------------------------------------------------
// The curve is first read as y=e^x. A true 3-D camera turn exchanges the screen
// roles of world x/y, so the same relationship reads x=e^y. While the graph is
// hidden for the first plane turn, morph3 exchanges its coordinates; the same
// geometry can then remain visible through the return turn into y=ln(x).
// ============================================================================
title("Why the derivative of ln x is 1/x");
canvas("16:9");
template("plain");
// Landscape proof: the upper-right stays clear while the algebra owns the
// bottom and the vertical axis label owns the top-centre.
watermark(manicMark, (1135, 30), "Made With Manic");
// Orthographic overhead camera: the continuous pole-safe orbit frame keeps the
// whole turn smooth, and roll3 controls which world direction is screen-up.
camera3((0, 0.5, 12), (0, 0.5, 0), 8.5, orthographic);
// ---------------------------------------------------------------------------
// ONE WORLD PLANE · axes, grid, exponential, tangent and exact slope triangle
// ---------------------------------------------------------------------------
grid3(grid, (0, 0, 0), 4, 1);
color(grid, dim); opacity(grid, 0.30); tag(grid, world);
arrow3(xaxis, (-4.05, 0, 0), (4.05, 0, 0));
arrow3(yaxis, (0, -3.4, 0), (0, 4.05, 0));
color(xaxis, fg); color(yaxis, fg); thick(xaxis, 0.012); thick(yaxis, 0.012);
tag(xaxis, world); tag(yaxis, world);
for i in -4..5 {
if i != 0 {
line3(xtick{i}, (i, -0.07, 0), (i, 0.07, 0));
line3(ytick{i}, (-0.07, i, 0), (0.07, i, 0));
color(xtick{i}, dim); color(ytick{i}, dim);
tag(xtick{i}, world); tag(ytick{i}, world);
}
}
equation(xlab, (0,0), `x`, 31); pin3(xlab, (4.10, -0.18, 0)); tag(xlab, world);
equation(ylab, (0,0), `y`, 31); pin3(ylab, (-0.18, 4.10, 0)); tag(ylab, world);
hidden(world);
// Source curve γ(t)=(t,e^t), and its inverse parameterisation γ⁻¹(t)=(e^t,t).
curve3(curve, "t", "exp(t)", "0", (-3.6, 1.42));
curve3(curveTarget, "exp(t)", "t", "0", (-3.6, 1.42));
color(curve, lime); color(curveTarget, lime); thick(curve, 0.024); thick(curveTarget, 0.024);
untraced(curve); hidden(curveTarget); morph3(curve, curveTarget);
equation(expName, (0,0), `y=e^x`, 31); color(expName, lime); pin3(expName, (-1.45, 2.20, 0)); hidden(expName);
equation(swappedName, (0,0), `x=e^y`, 31); color(swappedName, lime); pin3(swappedName, (2.65, -1.20, 0)); hidden(swappedName);
equation(logName, (0,0), `y=\ln(x)`, 31); color(logName, lime); pin3(logName, (2.65, 0.90, 0)); hidden(logName);
// At t=0.6: P=(0.6,e^0.6), and e^0.6 ≈ 1.822. The tangent reaches y=0
// exactly one unit of run before P, so its slope triangle is rise/run = y/1.
curve3(tangent, "-0.65+2*t", "-0.455529+3.644238*t", "0", (0,1));
curve3(tangentTarget, "-0.455529+3.644238*t", "-0.65+2*t", "0", (0,1));
color(tangent, magenta); color(tangentTarget, magenta); thick(tangent, 0.018); thick(tangentTarget, 0.018);
untraced(tangent); hidden(tangentTarget); morph3(tangent, tangentTarget);
curve3(runSide, "-0.4+t", "0", "0", (0,1));
curve3(runTarget, "0", "-0.4+t", "0", (0,1));
color(runSide, gold); color(runTarget, gold); thick(runSide, 0.024); thick(runTarget, 0.024);
untraced(runSide); hidden(runTarget); morph3(runSide, runTarget);
curve3(riseSide, "0.6", "1.822119*t", "0", (0,1));
curve3(riseTarget, "1.822119*t", "0.6", "0", (0,1));
color(riseSide, cyan); color(riseTarget, cyan); thick(riseSide, 0.024); thick(riseTarget, 0.024);
untraced(riseSide); hidden(riseTarget); morph3(riseSide, riseTarget);
point3(contact, (0.6, 1.822119, 0), 0.075); color(contact, fg); hidden(contact);
point3(contactTarget, (1.822119, 0.6, 0), 0.075); color(contactTarget, fg); hidden(contactTarget);
equation(riseY, (0,0), `y`, 28); color(riseY, cyan); pin3(riseY, (0.82, 0.91, 0)); hidden(riseY);
equation(runOne, (0,0), `1`, 28); color(runOne, gold); pin3(runOne, (0.10, -0.22, 0)); hidden(runOne);
equation(targetX, (0,0), `x`, 28); color(targetX, cyan); pin3(targetX, (0.91, 0.82, 0)); hidden(targetX);
equation(targetOne, (0,0), `1`, 28); color(targetOne, gold); pin3(targetOne, (-0.22, 0.10, 0)); hidden(targetOne);
// The diagonal is the hinge / mirror relation during the plane turns.
for i in -9..10 {
line3(diag{i}, (i*0.42, i*0.42, 0.02), (i*0.42+0.24, i*0.42+0.24, 0.02));
color(diag{i}, magenta); thick(diag{i}, 0.018); untraced(diag{i}); tag(diag{i}, mirror);
}
equation(diagName, (0,0), `y=x`, 27); color(diagName, magenta); pin3(diagName, (2.55, 2.55, 0.02)); hidden(diagName);
// Screen-space algebra. Separate equations keep the camera/geometry generic.
equation(slopeExp, (cx, 655), `\textcolor{magenta}{\mathrm{slope}}=\textcolor{cyan}{e^x}`, 40); hidden(slopeExp);
equation(slopeY, (cx, 655), `\textcolor{magenta}{\mathrm{slope}}=\textcolor{cyan}{y}`, 40); hidden(slopeY);
equation(slopeFracY, (cx, 655), `\textcolor{magenta}{\mathrm{slope}}=\frac{\textcolor{cyan}{y}}{\textcolor{gold}{1}}=\frac{\textcolor{cyan}{\mathrm{rise}}}{\textcolor{gold}{\mathrm{run}}}`, 40); hidden(slopeFracY);
// Persistent algebra pieces. The unchanged prefix/equality never disappear:
// only the term being justified moves, fades, or arrives. This is the same
// object-continuity principle used by matching-transform systems, expressed
// with ordinary Manic entities rather than replacing a whole equation image.
equation(slopeWord, (500, 655), `\textcolor{magenta}{\mathrm{slope}}`, 40); hidden(slopeWord); tag(slopeWord, inverseAlgebra);
equation(mainEquals, (575, 655), `=`, 40); hidden(mainEquals); tag(mainEquals, inverseAlgebra);
equation(heightTerm, (630, 655), `\textcolor{cyan}{\mathrm{height}}`, 40); hidden(heightTerm); tag(heightTerm, inverseAlgebra);
equation(xTerm, (630, 655), `\textcolor{cyan}{x}`, 40); hidden(xTerm); tag(xTerm, inverseAlgebra);
equation(oneTerm, (630, 674), `\textcolor{gold}{1}`, 36); hidden(oneTerm); tag(oneTerm, inverseAlgebra);
line(termBar, (616, 657), (644, 657)); color(termBar, fg); stroke(termBar, 2); untraced(termBar); tag(termBar, inverseAlgebra);
equation(ratioEquals, (685, 655), `=`, 40); hidden(ratioEquals); tag(ratioEquals, inverseAlgebra);
equation(ratioTerm, (780, 655), `\frac{\textcolor{cyan}{\mathrm{rise}}}{\textcolor{gold}{\mathrm{run}}}`, 40); hidden(ratioTerm); tag(ratioTerm, inverseAlgebra);
equation(derivativeTerm, (475, 655), `\frac{d}{dx}\ln(x)`, 41); hidden(derivativeTerm); tag(derivativeTerm, inverseAlgebra);
// Split glyphs let generic `cycle` retain their identity through xy → yx.
equation(planeX, (550, 640), `x`, 54); hidden(planeX); tag(planeX, planeLabel);
equation(planeY, (575, 640), `y`, 54); hidden(planeY); tag(planeY, planeLabel);
equation(planeWord, (685, 640), `\mathrm{plane}`, 54); hidden(planeWord); tag(planeWord, planeLabel);
// ---------------------------------------------------------------------------
// TIMELINE · 75.49 seconds including manic's final one-second tail
// ---------------------------------------------------------------------------
show(world, 1.20);
draw(curve, 2.40);
show(expName, 0.60);
wait(2.50);
show(slopeExp, 0.70);
wait(2.20);
par { draw(riseSide, 1.20); show(riseY, 0.45); show(contact, 0.30); }
par { fade(slopeExp, 0.55); show(slopeY, 0.55); }
wait(2.00);
par { draw(tangent, 1.20); draw(runSide, 1.00); show(runOne, 0.40); }
par { fade(slopeY, 0.60); show(slopeFracY, 0.60); }
wait(7.90);
// First turn: remove the measurement clutter, reveal the mirror hinge, and
// move from the top of the plane to its underside. Below + -90° roll makes
// screen-horizontal follow world y and screen-vertical follow world x.
par {
fade(curve, 0.70); fade(expName, 0.70); fade(tangent, 0.70);
fade(runSide, 0.70); fade(riseSide, 0.70); fade(contact, 0.70);
fade(riseY, 0.70); fade(runOne, 0.70); fade(slopeFracY, 0.70);
}
par { draw(mirror, 0.70); show(diagName, 0.50); show(planeLabel, 0.50); }
par {
orbit3(-90, -90, 12, 7.00, smooth);
roll3(90, 7.00, smooth);
// The graph is hidden here, so exchange its coordinates before it returns.
to(curve, morph, 1, 7.00, smooth);
to(tangent, morph, 1, 7.00, smooth);
to(runSide, morph, 1, 7.00, smooth);
to(riseSide, morph, 1, 7.00, smooth);
seq { wait(2.80); cycle(planeX, planeY, 1.40, 70, smooth); }
}
// Same world geometry, now read with exchanged screen axes.
par {
show(curve, 0.70); show(tangent, 0.70); show(runSide, 0.70); show(riseSide, 0.70);
show(contactTarget, 0.50); show(swappedName, 0.55); show(targetX, 0.45); show(targetOne, 0.45);
fade(diagName, 0.45); erase(mirror, 0.70); fade(planeLabel, 0.45);
}
par { show(slopeWord, 0.65); show(mainEquals, 0.65); show(heightTerm, 0.65); }
wait(3.00);
par { fade(heightTerm, 0.60); show(xTerm, 0.60); }
wait(3.00);
par { show(ratioEquals, 0.60); show(ratioTerm, 0.60); }
wait(3.00);
// x becomes x/1 in place: x lifts, the denominator arrives, and the rest of
// the displayed identity remains untouched.
par { move(xTerm, (630, 640), 0.60, smooth); show(oneTerm, 0.60); draw(termBar, 0.60); }
wait(6.20);
// Second turn: return the camera overhead while keeping the transformed curve
// and triangle visible. The same objects land as an ordinary y=ln(x) graph.
par {
draw(mirror, 0.60); show(diagName, 0.45); show(planeLabel, 0.45);
fade(slopeWord, 0.60); fade(mainEquals, 0.60); fade(xTerm, 0.60);
fade(oneTerm, 0.60); erase(termBar, 0.60); fade(ratioEquals, 0.60); fade(ratioTerm, 0.60);
}
par {
orbit3(-90, 90, 12, 7.00, smooth);
roll3(0, 7.00, smooth);
fade(swappedName, 3.00); show(logName, 3.00);
// Prepare the next algebra state invisibly while the plane is turning.
move(ratioTerm, (630, 655), 1.20, smooth);
move(oneTerm, (630, 640), 1.20, smooth);
move(xTerm, (630, 674), 1.20, smooth);
seq { wait(2.80); cycle(planeX, planeY, 1.40, -70, smooth); }
}
par {
fade(planeLabel, 0.45); fade(diagName, 0.45); erase(mirror, 0.70);
show(slopeWord, 0.65); show(mainEquals, 0.65); show(ratioTerm, 0.65);
}
wait(2.20);
// Only the right-hand term changes: rise/run becomes 1/x.
par { fade(ratioTerm, 0.60); show(oneTerm, 0.60); show(xTerm, 0.60); draw(termBar, 0.60); }
wait(3.00);
// The proven reciprocal stays; only the name on the left becomes d/dx ln(x).
par { fade(slopeWord, 0.70); show(derivativeTerm, 0.70); pulse(curve, 0.70); }
wait(9.89);
spline
Interpolation: one smooth curve drawn through a scattered set of points — it passes through every knot exactly.
title("A smooth curve through the data");
canvas("16:9");
text(hdr, (cx, 70), "Interpolation: one smooth curve through every point");
size(hdr, 26); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
// scattered data points — the spline passes through all of them
spline(s, (300, 620), (520, 340), (760, 560), (1000, 300), (1240, 480), (1500, 260));
color(s, cyan); stroke(s, 4); untraced(s);
text(cap, (cx, h - 60), "Catmull-Rom — it hits each knot exactly");
size(cap, 20); color(cap, dim); display(cap); hidden(cap);
// ---- timeline ----
show(hdr, 0.5);
// pop the knots in first
show(s.knots, 0.6);
wait(0.3);
// then trace the smooth curve through them
draw(s, 2.2);
show(cap, 0.4);
trajectory
A phase portrait: three paths flowing under a differential system, each spiralling into the sink at the origin.
title("Phase portrait: flowing into a spiral sink");
canvas("16:9");
text(hdr, (cx, 60), "dx/dt = -y - 0.2x, dy/dt = x - 0.2y");
size(hdr, 26); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
// a reference frame centered on the sink
axes(ax, (cx, cy + 20), 620, 360, 1);
color(ax, dim); untraced(ax);
// three trajectories from different starts, each spiraling into the origin
trajectory(t1, "-y - 0.2*x", "x - 0.2*y", (3.2, 0), (cx, cy + 20), 110, 520);
color(t1, cyan); stroke(t1, 3); untraced(t1);
trajectory(t2, "-y - 0.2*x", "x - 0.2*y", (0, 3.4), (cx, cy + 20), 110, 520);
color(t2, magenta); stroke(t2, 3); untraced(t2);
trajectory(t3, "-y - 0.2*x", "x - 0.2*y", (-3.0, -2.4), (cx, cy + 20), 110, 520);
color(t3, gold); stroke(t3, 3); untraced(t3);
// ---- timeline ----
show(hdr, 0.5);
draw(ax, 0.8);
par {
draw(t1, 3.5);
draw(t2, 3.5);
draw(t3, 3.5);
}
band
The area trapped between two curves, filled directly with band(top,bottom) while both
boundary plots remain visible.
title("The area between two curves");
canvas("16:9");
let ox = cx - 460;
let oy = cy + 40;
text(hdr, (cx, 58), "The region trapped between two curves");
size(hdr, 26); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
axes(ax, (ox, oy), 900, 260, 1);
color(ax, dim); untraced(ax);
// two curves over the same stretch of x
plot(upper, (ox, oy), 140, 90, "0.4*x + 1.4", (0, 6.3));
color(upper, cyan); stroke(upper, 3); untraced(upper);
plot(lower, (ox, oy), 140, 90, "sin(x)", (0, 6.3));
color(lower, magenta); stroke(lower, 3); untraced(lower);
// the band between them
band(gap, upper, lower);
color(gap, lime); hidden(gap);
text(cap, (cx, h - 56), "band(top, bottom) fills the space between");
size(cap, 20); color(cap, dim); display(cap); hidden(cap);
// ---- timeline ----
show(hdr, 0.6);
draw(ax, 0.7);
par {
draw(upper, 1.4);
draw(lower, 1.4);
}
wait(0.3);
to(gap, opacity, 0.3, 0.6); // fade in to translucent (so the curves read through)
show(cap, 0.4);
curve-features
Read a cubic by its geometry: maxima/minima where the slope is zero and an inflection
where the curve changes its bend (extrema, inflections).
title("Reading a curve's shape — maxima, minima, inflection");
canvas("16:9");
let ox = cx - 540; // math x=0 maps here (centres the domain 1..5 under cx)
let oy = cy;
text(hdr, (cx, 56), "Where the curve turns, and where it changes its bend");
size(hdr, 26); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
axes(ax, (ox, oy), 940, 240, 1);
color(ax, dim); untraced(ax);
// a cubic S-curve: one maximum, one minimum, one inflection
plot(f, (ox, oy), 180, 170, "0.4*((x-3)*(x-3)*(x-3) - 4*(x-3))", (1, 5));
color(f, cyan); stroke(f, 4); untraced(f);
// maxima & minima (slope = 0)
extrema(turn, f);
color(turn, gold);
// inflection (concavity flips, f'' = 0)
inflections(bend, f);
color(bend, magenta);
text(lg, (cx + 300, 150), "gold: max / min (slope 0)");
size(lg, 18); color(lg, gold); display(lg); hidden(lg);
text(lm, (cx + 300, 186), "pink: inflection (bend flips)");
size(lm, 18); color(lm, magenta); display(lm); hidden(lm);
// ---- timeline ----
show(hdr, 0.6);
draw(ax, 0.7);
draw(f, 1.6);
wait(0.3);
show(turn, 0.5);
show(lg, 0.4);
pulse(turn, 0.6);
wait(0.3);
show(bend, 0.5);
show(lm, 0.4);
pulse(bend, 0.6);
ftc
The Fundamental Theorem of Calculus: accumulate the area under a curve, differentiate that area function, and watch the original function return.
title("The Fundamental Theorem of Calculus");
canvas("16:9");
let ox = cx - 460; // math x=0 maps here
let oy = cy + 20;
text(hdr, (cx, 52), "Differentiate the area — and the function comes back");
size(hdr, 26); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
axes(ax, (ox, oy), 900, 200, 1);
color(ax, dim); untraced(ax);
// f(x) = cos x
plot(f, (ox, oy), 140, 150, "cos(x)", (0, 6.3));
color(f, cyan); stroke(f, 4); untraced(f);
// F(x) = area under f from 0 to x = sin x
accum(bigF, f);
color(bigF, lime); stroke(bigF, 3); untraced(bigF);
// F'(x): differentiate the area function — it lands right back on f
deriv(dF, bigF);
color(dF, gold); stroke(dF, 3); untraced(dF);
// legend
text(lf, (cx + 300, 150), "f(x) = cos x");
size(lf, 20); color(lf, cyan); display(lf); hidden(lf);
text(lF, (cx + 300, 190), "F(x) = area so far");
size(lF, 20); color(lF, lime); display(lF); hidden(lF);
text(ld, (cx + 300, 230), "F'(x) = f(x)");
size(ld, 20); color(ld, gold); bold(ld); display(ld); hidden(ld);
text(cap, (cx, h - 52), "the slope of the area-so-far IS the original curve");
size(cap, 20); color(cap, dim); display(cap); hidden(cap);
// ================= timeline =================
show(hdr, 0.6);
draw(ax, 0.7);
// 1. the function
draw(f, 1.3);
show(lf, 0.4);
wait(0.3);
// 2. its accumulated area, as a new curve (this is sin x)
draw(bigF, 1.6);
show(lF, 0.4);
wait(0.4);
// 3. differentiate that area function — it traces back onto f
draw(dF, 1.6);
show(ld, 0.4);
flash(dF, gold);
show(cap, 0.5);
limit
A removable discontinuity visualized as an approaching point, open circle and live finite limit at x→0.
title("A limit — the value a curve heads toward");
canvas("16:9");
let ox = cx - 120;
let oy = cy;
text(hdr, (cx, 54), "sin(x) / x is undefined at 0 — but it heads straight for 1");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
axes(ax, (ox, oy), 620, 300, 1);
color(ax, dim); untraced(ax);
// sin(x)/x — a removable hole at x = 0, where the limit is 1
plot(f, (ox, oy), 150, 220, "sin(x)/x", (-6.2, 6.2));
color(f, cyan); stroke(f, 4); untraced(f);
// the limit at x -> 0: open circle at (0, 1), guides, value, and an approaching dot
limit(lim, f, 0);
color(lim, gold);
text(cap, (cx, h - 54), "slide the point in — f(x) closes on the open circle");
size(cap, 20); color(cap, dim); display(cap); hidden(cap);
// ---- timeline ----
show(hdr, 0.6);
draw(ax, 0.7);
draw(f, 1.6);
wait(0.3);
show(lim, 0.5);
show(cap, 0.4);
wait(0.3);
// walk the point toward x = 0 from the left — it approaches the open circle
to(lim, x, 0, 3.5);
limit-infinity
A rational function settling onto its horizontal asymptote, with limit(...,inf)
detecting and marking the value at infinity.
title("A limit at infinity — limit(id, curve, inf)");
canvas("16:9");
let ox = 90; // x = 0 near the left
let oy = cy + 250; // y = 0 baseline
text(hdr, (cx, 52), "(5x^3 - 2x + 7) / (x^3 + 4x^2 + 3) -> 5 as x -> infinity");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
text(sub, (cx, 92), "same degree top and bottom: the ratio of leading coefficients, 5/1");
size(sub, 17); color(sub, dim); display(sub); hidden(sub);
// the x-axis (y = 0)
line(xaxis, (ox, oy), (ox + 1130, oy));
color(xaxis, dim); untraced(xaxis);
// the rational function, climbing toward its asymptote
plot(f, (ox, oy), 9, 88, "(5*x*x*x - 2*x + 7)/(x*x*x + 4*x*x + 3)", (0.5, 125));
color(f, cyan); stroke(f, 4); untraced(f);
// the limit at infinity: auto-detects the horizontal asymptote y = 5
limit(lim, f, inf);
color(lim, gold); hidden(lim);
// ---- timeline ----
show(hdr, 0.6);
show(sub, 0.4);
draw(xaxis, 0.5);
draw(f, 2.4);
show(lim, 0.7);
taylor
Taylor polynomials of increasing degree closing in on sin(x), one additional approximation at a time.
title("Taylor series — polynomials closing in on a curve");
canvas("16:9");
let ox = cx; // x=0 at centre
let oy = cy + 10;
text(hdr, (cx, 52), "Add one more term, and the polynomial hugs more of the curve");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
axes(ax, (ox, oy), 640, 260, 1);
color(ax, dim); untraced(ax);
// the target function
plot(f, (ox, oy), 95, 120, "sin(x)", (-6.2, 6.2));
color(f, cyan); stroke(f, 5); untraced(f);
// Taylor polynomials about x = 0, growing in degree
taylor(p1, f, 0, 1);
color(p1, dim); stroke(p1, 3); untraced(p1);
taylor(p3, f, 0, 3);
color(p3, gold); stroke(p3, 3); untraced(p3);
taylor(p5, f, 0, 5);
color(p5, magenta); stroke(p5, 3); untraced(p5);
taylor(p7, f, 0, 7);
color(p7, lime); stroke(p7, 3); untraced(p7);
text(l1, (cx + 300, 150), "n = 1 (a line)");
size(l1, 18); color(l1, dim); display(l1); hidden(l1);
text(l3, (cx + 300, 186), "n = 3");
size(l3, 18); color(l3, gold); display(l3); hidden(l3);
text(l5, (cx + 300, 222), "n = 5");
size(l5, 18); color(l5, magenta); display(l5); hidden(l5);
text(l7, (cx + 300, 258), "n = 7");
size(l7, 18); color(l7, lime); display(l7); hidden(l7);
// ---- timeline ----
show(hdr, 0.6);
draw(ax, 0.7);
draw(f, 1.6);
wait(0.3);
draw(p1, 0.8); show(l1, 0.3); wait(0.3);
draw(p3, 0.9); show(l3, 0.3); wait(0.3);
draw(p5, 1.0); show(l5, 0.3); wait(0.3);
draw(p7, 1.1); show(l7, 0.3);
Linear algebra & tables
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
linear-algebra — the whole subject in five ideas
A guided lesson, not a feature demo: five chapters that build linear algebra as
one connected story. Chapters 1–3 view the same matrix [[2,1],[1,2]]
through three lenses — a transformation of space (linmap), the determinant as
area scaling (determinant), and its eigenvectors / diagonalisation
(diagonalise) — then it moves on to solving Ax = b (linsolve → rref) and
projection / least-squares (project). Start here.
// ============================================================================
// linear-algebra.manic — Linear Algebra in five ideas (a lesson)
// ----------------------------------------------------------------------------
// A guided tour of the whole subject, not a feature demo. One stage, five
// chapters; chapters 1-3 view the SAME matrix A = [[2,1],[1,2]] through three
// lenses, so the ideas connect:
// 1. a matrix TRANSFORMS space (linmap)
// 2. the DETERMINANT is how area scales (determinant) -> det = 3
// 3. EIGENVECTORS only stretch (diagonalise) -> lambda = 3, 1
// 4. SOLVING A x = b (linsolve -> rref)
// 5. PROJECTION: the closest answer (project)
//
// Reveal recipe (works for lines, arrows, fills, and text alike, preserving
// each element's opacity): `untraced(tag)` at build time, `draw(tag)` to reveal,
// `fade(tag)` to clear before the next chapter. `tag` broadcasts to every entity
// carrying it (see the ergonomics in the manic guide).
// ============================================================================
title("Linear Algebra, in five ideas");
canvas("16:9");
let sx = cx;
let sy = cy + 44; // shared stage center
let u = 44;
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
// ---- build every chapter's visual up front, drawn-blank (untraced) ----
// A coordinate grid draws in with each chapter for consistent visual feedback.
// Chapters 1 & 3 bring their own (the deformed grid / the eigen-grid); chapters
// 2, 4, 5 get a plain reference grid that traces in at the start of the section.
plane(g2, (sx, sy), 176, 176, u); untraced(g2); // determinant — count the cells
plane(g4, (sx, sy), 240, 230, u); untraced(g4); // solving — read off (1, 3)
plane(g5, (sx, sy), 200, 200, u); untraced(g5); // projection
// 1. transformation of A = [[2,1],[1,2]]
linmap(lm, (sx, sy), u, 2, 1, 1, 2);
untraced(lm);
// 2. determinant of the same A (parallelogram is the bare id `dt`)
determinant(dt, (sx, sy), u, 2, 1, 1, 2);
untraced(dt); untraced(dt.unit); untraced(dt.val);
// 3. eigenvectors / diagonalisation of the same A
diagonalise(dg, (sx, sy), u, 2, 1, 1, 2);
untraced(dg);
// 4a. a 2x2 system as two lines (solution dot is the bare id `sys`)
linsolve(sys, (sx, sy), u, 2, 1, 1, 3, 5, 10);
untraced(sys.r1); untraced(sys.r2); hidden(sys); hidden(sys.val);
// 4b. the same system reduced by elimination
rref(rr, "2 1 5 ; 1 3 10", (sx, sy - 24), 120, 60);
untraced(rr.lbrack); untraced(rr.rbrack);
// 5. projection of a vector onto a line
project(pj, (sx, sy), u, (1, 3), (3, 1));
untraced(pj);
// ============================ the lesson ==================================
section("Transformations");
draw(lm, 1.2);
say(cap, "a matrix sends every point to a new one — and it does so LINEARLY", 0.5);
wait(1.8);
say(cap, "the grid stays parallel and evenly spaced; i-hat, j-hat land on its columns", 0.5);
wait(2.2);
fade(lm, 0.6);
wait(0.3);
say(cap, "", 0.2);
section("The determinant");
draw(g2, 0.7);
draw(dt.unit, 0.5); draw(dt, 0.7); draw(dt.val, 0.4);
say(cap, "how much does it stretch area? the unit square becomes this parallelogram", 0.5);
wait(2.0);
say(cap, "that area IS the determinant = 3 — three grid cells; a flip would be negative", 0.5);
wait(2.2);
fade(dt, 0.6); fade(dt.unit, 0.6); fade(dt.val, 0.6); fade(g2, 0.6);
wait(0.3);
say(cap, "", 0.2);
section("Eigenvectors");
draw(dg, 1.2);
say(cap, "most vectors change direction — but a few only STRETCH: the eigenvectors", 0.5);
wait(2.2);
say(cap, "in their basis A is pure scaling: A = P D P^-1, D = diag(3, 1)", 0.5);
wait(2.2);
fade(dg, 0.6);
wait(0.3);
say(cap, "", 0.2);
section("Solving A x = b");
draw(g4, 0.7);
draw(sys.r1, 0.7); draw(sys.r2, 0.7);
show(sys, 0.4); show(sys.val, 0.4);
say(cap, "a system of equations is a set of lines; the solution is where they meet", 0.5);
wait(2.2);
fade(sys, 0.5); fade(sys.r1, 0.5); fade(sys.r2, 0.5); fade(sys.val, 0.5); fade(g4, 0.5);
say(cap, "elimination finds it by reducing [ A | b ] to the identity beside the answer", 0.5);
draw(rr.lbrack, 0.4); draw(rr.rbrack, 0.4);
show(rr.s0, 0.4); show(rr.op0, 0.3);
wait(1.6);
par { show(rr.s4, 0.5); fade(rr.s0, 0.5); } show(rr.op4, 0.4); fade(rr.op0, 0.3);
wait(2.0);
fade(rr.s4, 0.5); fade(rr.op4, 0.4); fade(rr.lbrack, 0.4); fade(rr.rbrack, 0.4);
wait(0.3);
say(cap, "", 0.2);
section("Projection");
draw(g5, 0.7);
draw(pj, 1.0);
say(cap, "when no exact answer exists, take the CLOSEST point of the subspace", 0.5);
wait(2.0);
say(cap, "the error is perpendicular — that principle IS least-squares, the best fit", 0.5);
wait(2.4);
linear-map
What a 2×2 matrix does to space: the grid deforms and the basis lands on its
columns (linmap), the unit square’s area becomes the determinant
(determinant), and two directions only stretch — the eigenvectors (eigen).
title("What a matrix does to space");
canvas("16:9");
let ox = cx - 60;
let oy = cy + 40;
text(hdr, (cx, 54), "the matrix [[2,1],[1,2]] — grid, determinant, eigenvectors");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
text(cap, (cx, h - 50), "");
size(cap, 22); color(cap, dim); display(cap);
// the plane deformed by the matrix (î, ĵ land on its columns)
linmap(lm, (ox, oy), 46, 2, 1, 1, 2, 2);
// the unit square's image — area = determinant
determinant(dt, (ox, oy), 46, 2, 1, 1, 2);
hidden(dt); hidden(dt.unit); hidden(dt.val);
// the two invariant directions (eigenvectors)
eigen(ev, (ox, oy), 46, 2, 1, 1, 2);
hidden(ev); hidden(ev.l0); hidden(ev.l1);
// ---- timeline ----
show(hdr, 0.6);
say(cap, "the matrix bends the grid; i-hat and j-hat land on its columns", 0.5);
wait(1.4);
say(cap, "the unit square maps to a parallelogram — its area IS the determinant", 0.5);
show(dt, 0.6);
show(dt.unit, 0.4);
show(dt.val, 0.5);
wait(1.4);
say(cap, "two directions only stretch, never turn: the eigenvectors", 0.5);
show(ev, 0.6);
show(ev.l0, 0.4);
show(ev.l1, 0.4);
wait(1.0);
linear-system
The geometry of solving and spanning, in three panels: a 2×2 system as two lines
crossing at the solution (linsolve), two independent vectors reaching the whole
plane, and two parallel vectors collapsing to a line — rank 1 (span).
// ============================================================================
// linear-system.manic — the geometry of solving & spanning (a template)
// ----------------------------------------------------------------------------
// Three side-by-side panels, each built up with animation:
// A) a 2x2 system as two lines meeting at the solution (linsolve)
// B) two independent vectors spanning the whole plane (span)
// C) two parallel vectors collapsing to a single line, rank 1 (span)
//
// HOW TO ADAPT THIS FILE:
// * Change the system in panel A: linsolve(sys, center, unit, a,b,c,d, e,f)
// draws a*x + b*y = e and c*x + d*y = f, meeting at the solution.
// * Change the vectors in panels B/C: span(id, center, unit, (vx,vy),(wx,wy)).
// * Everything is positioned from cx/cy/w/h + a per-panel origin (ax/bx/dx)
// and a `unit` = pixels-per-grid-step, so it scales cleanly. Grid point
// (gx,gy) maps to screen (origin_x + gx*u, origin_y - gy*u) [math y-up].
// * OPTIONAL: shade the closed region the two lines bound with the axes —
// see the commented `polygon(feasible, ...)` block in Panel A below.
// ============================================================================
title("Solving & spanning, visually");
canvas("16:9");
let y0 = cy + 20; // shared vertical center of the three panels
let ax = 280; // panel A origin x — a system as two lines
let bx = 640; // panel B origin x — span = the whole plane
let dx = 1000; // panel C origin x — parallel vectors collapse to a line
let u = 24; // pixels per grid unit (shared scale)
text(hdr, (cx, 46), "linear algebra: the geometry of solving & spanning");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// per-panel captions, revealed as each panel animates
text(la, (ax, 96), "a system = two lines"); size(la, 18); color(la, dim); display(la); hidden(la);
text(lb, (bx, 96), "span of two vectors"); size(lb, 18); color(lb, dim); display(lb); hidden(lb);
text(lc, (dx, 96), "parallel = rank 1"); size(lc, 18); color(lc, dim); display(lc); hidden(lc);
// faint coordinate backdrops (drawn once, static)
plane(pa, (ax, y0), 150, 190, u);
plane(pb, (bx, y0), 150, 190, u);
plane(pc, (dx, y0), 150, 190, u);
// ---- Panel A — the system 2x + y = 5 and x + 3y = 10, meeting at (1, 3) ----
linsolve(sys, (ax, y0), u, 2, 1, 1, 3, 5, 10);
untraced(sys.r1); untraced(sys.r2); // start blank, drawn in on the timeline
hidden(sys); hidden(sys.val); // the solution dot + its label fade in
// -- OPTIONAL: fill the closed area the two lines bound with the axes --------
// The two rows, the x-axis and the y-axis enclose a quadrilateral (the classic
// "feasible region"). Its corners, in grid coordinates, are:
// (0,0) -> (2.5,0) -> (1,3) -> (0,3.33)
// origin line1 hits the two line2 hits
// the x-axis lines meet the y-axis
// Uncomment to shade it (points are origin + gridX*u, origin - gridY*u):
//
// polygon(feasible,
// (ax, y0), // (0, 0) the origin
// (ax + 2.5*u, y0), // (2.5, 0) line 1 crosses the x-axis
// (ax + u, y0 - 3*u), // (1, 3) the two lines meet
// (ax, y0 - 3.333*u), // (0, 3.33) line 2 crosses the y-axis
// lime);
// opacity(feasible, 0.18); // translucent, so grid + lines show through
// z(feasible, -1); // sit behind the lines
// (for an animated reveal instead of a static fill, also `hidden(feasible);`
// above, then `to(feasible, opacity, 0.18, 0.6);` in Panel A's timeline.)
// ---------------------------------------------------------------------------
// ---- Panel B — two independent vectors reach every point: the whole plane --
span(fill, (bx, y0), u, (3, 1), (-1, 2));
untraced(fill.v); untraced(fill.w); // vectors draw in
hidden(fill.plane); // faint region fades to 0.14
// ---- Panel C — two parallel vectors only reach one line: rank 1 ------------
span(rank, (dx, y0), u, (2, 1), (-2, -1), gold);
untraced(rank.v); untraced(rank.w); untraced(rank.line);
text(cap, (cx, h - 44), ""); size(cap, 22); color(cap, dim); display(cap);
// ---- Panel A timeline: two lines draw in, then the solution pops -----------
show(la, 0.4);
say(cap, "two equations are two lines; where they cross solves the system", 0.5);
par { draw(sys.r1, 0.8); draw(sys.r2, 0.8); }
show(sys, 0.4);
show(sys.val, 0.4);
pulse(sys);
wait(1.6);
// ---- Panel B timeline: two vectors grow out, then the plane fills in -------
show(lb, 0.4);
say(cap, "two independent vectors combine to reach every point: the whole plane", 0.5);
par { draw(fill.v, 0.7); draw(fill.w, 0.7); }
to(fill.plane, opacity, 0.14, 0.9);
wait(1.6);
// ---- Panel C timeline: two parallel vectors, then their line (collapse) ----
show(lc, 0.4);
say(cap, "but parallel vectors only reach one line: rank 1, a collapse", 0.5);
par { draw(rank.v, 0.7); draw(rank.w, 0.7); }
draw(rank.line, 0.8);
wait(1.8);
diagonalise
A = P D P⁻¹ made visual: every real-diagonalisable matrix has a basis — its
eigenvectors — in which it does nothing but stretch each axis. The unit
eigen-cell stretches by λ along each eigenvector, with no rotation or shear
(diagonalise).
// ============================================================================
// diagonalise.manic — A = P D P^-1 made visual (a template)
// ----------------------------------------------------------------------------
// The big idea of diagonalisation: every (real-diagonalisable) matrix has a
// basis — its EIGENVECTORS — in which it does nothing but STRETCH each axis.
// No rotation, no shear: just a diagonal scaling D = diag(lambda1, lambda2).
//
// `diagonalise(id, (cx,cy), unit, a,b,c,d, [color])` draws, for [[a,b],[c,d]]:
// * the (generally skewed) eigen-grid — the coordinate frame of the eigenbasis
// * the two eigen-axes dg.axis1 / dg.axis2
// * the unit eigen-cell dg.cell and its image under A dg.img
// * the eigenvector images as arrows dg.v1 / dg.v2 (+ labels dg.v1l / dg.v2l)
// Complex or repeated eigenvalues (no real 2-D eigenbasis) leave a note instead.
//
// TO ADAPT: change the four matrix numbers below. Try a non-symmetric matrix
// like (2,1,0,3) to see a SKEWED eigenbasis, or (0,-1,1,0) to see the
// "no real eigenbasis" note (a pure rotation).
// ============================================================================
title("Diagonalisation: a matrix in its own basis");
canvas("16:9");
let ox = cx - 40;
let oy = cy + 20;
text(hdr, (cx, 54), "A = P D P^-1 — in the eigenbasis, A is only a stretch");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// the matrix [[2,1],[1,2]] — eigenvalues 3 (along (1,1)) and 1 (along (1,-1))
diagonalise(dg, (ox, oy), 60, 2, 1, 1, 2);
// the faint eigen-grid stays as static context; reveal the rest in beats:
untraced(dg.axis1); untraced(dg.axis2); // eigen-axes draw in
hidden(dg.cell); hidden(dg.img); // unit cell fades in; image fades to 0.4
untraced(dg.v1); untraced(dg.v2); // eigenvector arrows draw in
hidden(dg.v1l); hidden(dg.v2l); // lambda labels fade in
text(cap, (cx, h - 50), ""); size(cap, 22); color(cap, dim); display(cap);
// ---- timeline ----
say(cap, "every matrix has special directions — its eigenvectors", 0.5);
par { draw(dg.axis1, 0.7); draw(dg.axis2, 0.7); }
wait(1.2);
say(cap, "build the unit cell from those two directions", 0.5);
show(dg.cell, 0.6);
wait(1.4);
say(cap, "apply A: the cell only STRETCHES along each axis — never rotates", 0.5);
to(dg.img, opacity, 0.4, 0.8);
par { draw(dg.v1, 0.7); draw(dg.v2, 0.7); }
wait(1.2);
say(cap, "the stretch factors ARE the eigenvalues — the diagonal of D", 0.5);
show(dg.v1l, 0.4); show(dg.v2l, 0.4);
wait(1.6);
rref
Gaussian elimination, animated: an augmented matrix [A | b] is reduced to
reduced row-echelon form one row operation at a time, the numbers transforming
in place until the left block is the identity and the last column is the
solution (rref).
// ============================================================================
// rref.manic — Gaussian elimination, animated (a template)
// ----------------------------------------------------------------------------
// Reduce an augmented matrix [A | b] to reduced row-echelon form, one row
// operation at a time. Each intermediate state is drawn at the same spot, so
// cross-fading s{k-1} -> s{k} makes the numbers transform IN PLACE.
//
// `rref(id, "row ; row ; ...", (cx,cy), [cellw], [rowh])` draws, for the given
// matrix (rows split on `;`, entries on spaces/commas):
// * static brackets (the frame the numbers fill)
// * one matrix per elimination state, tagged rr.s0, rr.s1, ... (hidden)
// * the row-op caption for each state: rr.op0, rr.op1, ...
// rr.s0 is the untouched input; the LAST state is the RREF (for [A|b] its final
// column is the solution). Reveal the states in order to animate the reduction.
//
// TO ADAPT: change the matrix string. A different system takes a different
// NUMBER of steps — add/remove reveal beats below to match (the ones past the
// last real state are harmless no-ops, so a few extra never hurt).
// ============================================================================
title("rref: Gaussian elimination, animated");
canvas("16:9");
text(hdr, (cx, 60), "reduce [ A | b ] to reduced row-echelon form");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// the augmented matrix of the system 2x + y = 5, x + 3y = 10
rref(rr, "2 1 5 ; 1 3 10", (cx, cy - 10), 120, 64);
text(cap, (cx, h - 56), ""); size(cap, 22); color(cap, dim); display(cap);
// ---- reveal each state in place, captioned with its row operation ----
// Per beat: fade the old op, CROSS-FADE the matrix state (identical cells sit
// still, only the changed ones morph), then show the new op.
say(cap, "start with the augmented matrix [ A | b ]", 0.5);
show(rr.s0, 0.5); show(rr.op0, 0.4);
wait(1.6);
fade(rr.op0, 0.25); par { show(rr.s1, 0.5); fade(rr.s0, 0.5); } show(rr.op1, 0.4);
wait(1.5);
fade(rr.op1, 0.25); par { show(rr.s2, 0.5); fade(rr.s1, 0.5); } show(rr.op2, 0.4);
wait(1.5);
fade(rr.op2, 0.25); par { show(rr.s3, 0.5); fade(rr.s2, 0.5); } show(rr.op3, 0.4);
wait(1.5);
say(cap, "the left block is now the identity — the last column is the solution", 0.5);
fade(rr.op3, 0.25); par { show(rr.s4, 0.5); fade(rr.s3, 0.5); } show(rr.op4, 0.4);
wait(2.2);
projection
One idea, two faces: orthogonal projection drops a vector onto a subspace
(the shadow is the closest point, the error meets the space at a right angle),
and least-squares fits a line to data the same way — minimising the squared
residuals (project, leastsquares).
// ============================================================================
// projection.manic — projection & least-squares (a template)
// ----------------------------------------------------------------------------
// One idea, two faces. ORTHOGONAL PROJECTION drops a vector onto a subspace;
// the shadow p is the closest point, and the error b - p meets the subspace at
// a right angle. LEAST-SQUARES fits a line to data the SAME way — the best line
// is the one that minimises the (squared) residuals, i.e. the projection of the
// data onto the space of lines.
//
// `project(id, (cx,cy), unit, (bx,by), (ax,ay), [color])` draws:
// id.line (the subspace = span of a), id.b, id.p (the shadow), id.res (error),
// id.rt (right-angle mark), id.blabel / id.plabel.
// `leastsquares(id, (cx,cy), unit, "x1 y1 x2 y2 ...", [color])` draws:
// id.line (best fit), id.points (dots), id.residuals (verticals), id.eq.
//
// TO ADAPT: change b/a in `project`, or the point list in `leastsquares`.
// ============================================================================
title("Projection & least-squares");
canvas("16:9");
text(hdr, (cx, 50), "the closest point is a projection — and so is the best-fit line");
size(hdr, 23); color(hdr, cyan); bold(hdr); display(hdr);
// ---- LEFT: project vector b onto the line spanned by a ----
plane(pa, (cx - 330, cy + 10), 150, 210, 40);
project(pj, (cx - 330, cy + 10), 40, (1, 3), (3, 1));
untraced(pj.line); untraced(pj.b); untraced(pj.p); untraced(pj.res); untraced(pj.rt);
hidden(pj.blabel); hidden(pj.plabel);
// ---- RIGHT: fit a line to a point cloud ----
plane(pb, (cx + 310, cy + 90), 150, 150, 34);
leastsquares(ls, (cx + 310, cy + 90), 34, "1 2 2 3 3 5 4 4 5 6");
hidden(ls.points); untraced(ls.line); untraced(ls.residuals); hidden(ls.eq);
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
// ---- Panel A: the projection ----
say(cap, "drop b onto the line spanned by a — its shadow is the projection p", 0.5);
draw(pj.line, 0.6);
draw(pj.b, 0.6);
wait(0.6);
par { draw(pj.p, 0.6); draw(pj.res, 0.6); }
draw(pj.rt, 0.3);
show(pj.blabel, 0.3); show(pj.plabel, 0.3);
wait(1.4);
say(cap, "the error b - p meets the line at a right angle: p is the nearest point", 0.5);
wait(1.8);
// ---- Panel B: least-squares is the same idea ----
say(cap, "fitting a line works the SAME way: minimise the squared residuals", 0.5);
show(ls.points, 0.5);
wait(0.7);
draw(ls.line, 0.7);
draw(ls.residuals, 0.6);
show(ls.eq, 0.4);
wait(2.0);
matrix
A bracketed matrix, rows/columns addressable via tags.
// Matrix — a bracketed grid of entries, addressable by row and column via tag
// broadcast (à la Manim's Matrix + set_row_colors / set_column_colors).
//
// manic examples/matrix.manic
// manic examples/matrix.manic --record out --fps 60
//
// Rows are separated by ';', entries by spaces/commas. Entry ids m.r{i}c{j};
// tags m.row{i} / m.col{j} / m.entries.
title("Matrix");
canvas(1280, 720);
text(head, (640, 130), "rows and columns you can address");
display(head); color(head, cyan); size(head, 28); hidden(head);
text(cap, (640, 620), ""); color(cap, dim); size(cap, 22);
matrix(m, "2 0 4; -1 1 5; 3 -2 0", (640, 370));
untraced(m.lbrack); untraced(m.rbrack);
hidden(m.entries);
show(head, 0.5);
say(cap, "a 3x3 matrix");
par { draw(m.lbrack, 0.5); draw(m.rbrack, 0.5); }
seq { show(m.row0, 0.35); show(m.row1, 0.35); show(m.row2, 0.35); }
wait(0.5);
section("Columns");
say(cap, "colour a column — set_column_colors");
recolor(m.col1, magenta, 0.4);
flash(m.col2, cyan);
wait(0.5);
section("Rows");
say(cap, "and highlight a row — set_row_colors");
recolor(m.row0, lime, 0.4);
par { pulse(m.r0c0); pulse(m.r0c1); pulse(m.r0c2); }
wait(1.2);
matrix_addition
Two matrices summed, cell by cell.
// Matrix Addition — A + B = C, computed entry by entry. Each matching pair of
// entries flashes, then their sum pops into the result matrix. A teaching
// animation: it shows *why* matrix addition is element-wise.
//
// manic examples/matrix_addition.manic
// manic examples/matrix_addition.manic --record out --fps 60
title("Matrix Addition");
canvas(1280, 720);
text(head, (640, 120), "add two matrices, entry by entry");
display(head); color(head, cyan); size(head, 28); hidden(head);
text(cap, (640, 600), ""); color(cap, dim); size(cap, 26);
// A + B = C
matrix(A, "2 1; 0 3", (280, 350), 74, 66);
matrix(B, "1 4; 5 2", (640, 350), 74, 66);
matrix(C, "3 5; 5 5", (1000, 350), 74, 66);
text(plus, (460, 350), "+"); display(plus); color(plus, magenta); size(plus, 44); hidden(plus);
text(eq, (820, 350), "="); display(eq); color(eq, magenta); size(eq, 44); hidden(eq);
// A and B trace/fade in; C is built up during the sweep
untraced(A.lbrack); untraced(A.rbrack);
untraced(B.lbrack); untraced(B.rbrack);
untraced(C.lbrack); untraced(C.rbrack);
hidden(A.entries); hidden(B.entries); hidden(C.entries);
// --- reveal the two matrices ---
show(head, 0.5);
say(cap, "two matrices, A and B");
par { draw(A.lbrack, 0.4); draw(A.rbrack, 0.4); draw(B.lbrack, 0.4); draw(B.rbrack, 0.4); }
par { show(A.entries, 0.4); show(B.entries, 0.4); }
show(plus, 0.3);
wait(0.5);
// --- add entry by entry ---
section("Entry by entry");
say(cap, "add matching entries, position by position");
par { show(eq, 0.3); draw(C.lbrack, 0.4); draw(C.rbrack, 0.4); }
seq {
par { flash(A.r0c0, lime); flash(B.r0c0, lime); }
say(cap, "2 + 1 = 3");
par { show(C.r0c0, 0.3); pulse(C.r0c0); }
par { flash(A.r0c1, lime); flash(B.r0c1, lime); }
say(cap, "1 + 4 = 5");
par { show(C.r0c1, 0.3); pulse(C.r0c1); }
par { flash(A.r1c0, lime); flash(B.r1c0, lime); }
say(cap, "0 + 5 = 5");
par { show(C.r1c0, 0.3); pulse(C.r1c0); }
par { flash(A.r1c1, lime); flash(B.r1c1, lime); }
say(cap, "3 + 2 = 5");
par { show(C.r1c1, 0.3); pulse(C.r1c1); }
}
wait(0.4);
// --- the result ---
section("Result");
say(cap, "A + B — every entry, all at once");
recolor(C.entries, cyan, 0.4);
par { pulse(C.r0c0); pulse(C.r0c1); pulse(C.r1c0); pulse(C.r1c1); }
wait(1.5);
matrix_addition_plane
The same sum, laid out on a coordinate plane.
// Matrix Addition, Geometrically — a 2x1 matrix IS a vector. Adding two of them
//
// [3] [1] [4]
// [1] + [2] = [3]
//
// is the same as sliding one arrow onto the tip of the other (tip-to-tail) and
// reading off where you land. The column matrices at the top stay in lockstep
// with the arrows on the plane, so you see the algebra and the geometry at once.
//
// manic examples/matrix_addition_plane.manic
// manic examples/matrix_addition_plane.manic --record out --fps 60
title("Matrix Addition on the Plane");
canvas(1280, 720);
text(head, (640, 96), "a 2x1 matrix is a vector — adding them is tip-to-tail");
display(head); color(head, cyan); size(head, 26); hidden(head);
text(cap, (640, 686), ""); color(cap, dim); size(cap, 24);
// --- the equation, as column matrices across the top ---
matrix(MA, "3; 1", (452, 186), 44, 42); color(MA, cyan);
text(plus, (528, 186), "+"); display(plus); color(plus, dim); size(plus, 40); hidden(plus);
matrix(MB, "1; 2", (600, 186), 44, 42); color(MB, magenta);
text(eq, (676, 186), "="); display(eq); color(eq, dim); size(eq, 40); hidden(eq);
matrix(MC, "4; 3", (748, 186), 44, 42); color(MC, lime);
// --- the plane, centred low so the arrows have room to climb ---
plane(pl, (640, 438), 560, 250, 48);
// vectors from the plane's origin (dy is up); unit = 48 px
vector(va, (640, 438), (144, 48), cyan); // a = (3, 1) -> tip (784, 390)
vector(vb, (640, 438), (48, 96), magenta); // b = (1, 2) -> tip (688, 342)
vector(vs, (640, 438), (192, 144), lime); // a+b = (4, 3) -> tip (832, 294)
// the two translated copies that build the parallelogram
arrow(vb2, (784, 390), (832, 294)); color(vb2, magenta); glow(vb2, 0);
arrow(va2, (688, 342), (832, 294)); color(va2, cyan); glow(va2, 0);
// everything but the plane grid starts hidden / untraced
untraced(pl.x); untraced(pl.y); hidden(pl.grid);
untraced(va); untraced(vb); untraced(vs); untraced(vb2); untraced(va2);
untraced(MA.lbrack); untraced(MA.rbrack); hidden(MA);
untraced(MB.lbrack); untraced(MB.rbrack); hidden(MB);
untraced(MC.lbrack); untraced(MC.rbrack); hidden(MC);
// --- reveal the plane ---
show(head, 0.5);
section("The plane");
say(cap, "a cartesian grid, arrows pinned to the origin");
show(pl.grid, 0.6);
par { draw(pl.x, 0.5); draw(pl.y, 0.5); }
wait(0.3);
// --- vector a ---
section("Vector a");
say(cap, "a = [3, 1] — three right, one up");
par { draw(MA.lbrack, 0.3); draw(MA.rbrack, 0.3); }
par { show(MA, 0.3); draw(va, 0.6); }
wait(0.4);
// --- vector b ---
section("Vector b");
say(cap, "b = [1, 2] — one right, two up");
show(plus, 0.3);
par { draw(MB.lbrack, 0.3); draw(MB.rbrack, 0.3); }
par { show(MB, 0.3); draw(vb, 0.6); }
wait(0.4);
// --- tip to tail ---
section("Tip to tail");
say(cap, "slide b so its tail sits on the tip of a");
draw(vb2, 0.7);
wait(0.5);
// --- the sum ---
section("The sum");
say(cap, "the arrow to that new point is a + b = [4, 3]");
show(eq, 0.3);
par { draw(MC.lbrack, 0.3); draw(MC.rbrack, 0.3); }
par { show(MC, 0.3); draw(vs, 0.8); }
par { pulse(vs); flash(MC, lime); }
wait(0.6);
// --- parallelogram ---
section("Either order");
say(cap, "slide a onto b instead — same point. a + b = b + a");
draw(va2, 0.7);
wait(0.4);
say(cap, "the two paths frame a parallelogram; a + b is its diagonal");
par { pulse(va); pulse(vb); pulse(vs); }
wait(1.4);
linear_transform
A 2x2 matrix shearing a grid + basis vectors.
// Linear Transformation — a 2x2 matrix bends the whole plane. The grid, the
// basis vectors i-hat / j-hat, and a sample point all carry the tag `pl`, so a
// single `transform` applies the matrix to everything at once (Manim's
// ApplyMatrix). Straight lines stay straight; the grid shears / rotates.
//
// manic examples/linear_transform.manic
// manic examples/linear_transform.manic --template blueprint
title("Linear Transformation");
canvas("16:9");
let ox = cx; let oy = cy;
text(head, (cx, 84), "a matrix bends the whole plane -- watch the grid");
display(head); color(head, cyan); size(head, 24); hidden(head);
text(cap, (cx, 672), ""); color(cap, dim); size(cap, 23);
// the plane (its grid + axes are all tagged `pl`)
plane(pl, (ox, oy), 580, 320, 60);
// basis vectors + a sample point, all tagged `pl` so they transform together
vector(vi, (ox, oy), (120, 0), cyan); stroke(vi, 4); tag(vi, pl);
vector(vj, (ox, oy), (0, -120), magenta); stroke(vj, 4); tag(vj, pl);
dot(mark, (ox + 180, oy - 100), 9); color(mark, lime); glow(mark, 1.6); tag(mark, pl);
// --- script ---
show(head, 0.5);
say(cap, "the identity grid, with i-hat (cyan) and j-hat (magenta)");
wait(0.7);
section("Shear");
say(cap, "shear: i-hat stays put, j-hat leans over");
transform(pl, (ox, oy), 1, 0.5, 0, 1, 1.4, smooth);
wait(0.8);
section("Undo");
say(cap, "the inverse matrix brings it right back");
transform(pl, (ox, oy), 1, -0.5, 0, 1, 1.4, smooth);
wait(0.6);
section("Rotate");
say(cap, "a rotation matrix turns the whole plane");
transform(pl, (ox, oy), 0.707, -0.707, 0.707, 0.707, 1.5, smooth);
wait(1.3);
table
A ruled table; cells, rows, columns, labels all addressable.
// Tables — a ruled grid of entries with row/column headers, manic's Table /
// MathTable / IntegerTable. This is an addition table: each body cell is
// row + column. We reveal it, then "look up" 2 + 5 by flashing that row and
// column and lighting the answer — a demo of the table's tag addressing
// (row{i} / col{j} / the labels / the grid lines are all recolourable).
//
// manic examples/table.manic
// manic examples/table.manic --record out --fps 60
title("Tables");
canvas(1280, 720);
text(head, (640, 110), "a grid you can read by row and column");
display(head); color(head, cyan); size(head, 28); hidden(head);
text(cap, (640, 620), ""); color(cap, dim); size(cap, 24);
// body cells are the sums; headers are the addends (top-left corner is blank)
table(t, "0 5 10; 2 7 12; 4 9 14", (640, 372), 120, 78, "0 5 10", "0 2 4");
untraced(t.lines);
hidden(t.labels); hidden(t.entries);
// --- reveal ---
show(head, 0.5);
say(cap, "rule the grid, then fill it in");
draw(t.lines, 1.0);
show(t.labels, 0.5);
show(t.entries, 0.5);
wait(0.5);
// --- a lookup: 2 + 5 = 7 ---
section("Look it up");
say(cap, "read a cell as row + column");
par { flash(t.rowlabel1, lime); flash(t.collabel1, lime); }
say(cap, "row 2, column 5 ...");
par { flash(t.row1, cyan); flash(t.col1, cyan); }
say(cap, "2 + 5 = 7");
recolor(t.r1c1, lime, 0.3);
pulse(t.r1c1);
wait(1.4);
table_braces
A table annotated with braces.
// Table + Braces — a quarterly sales table, annotated with curly braces that
// group its columns (the four quarters into two halves of the year) and its
// rows (the two regions). A practical pattern: use a table for the data and
// braces to call out how its rows/columns cluster.
//
// The brace coordinates are aligned to the table's grid lines by hand — the
// table is centred at (640,360) with 110x70 cells, so its vertical rules fall
// at x = 365 + k*110 and its rows span y = 325..465.
//
// manic examples/table_braces.manic
// manic examples/table_braces.manic --record out --fps 60
title("Sales by Region");
canvas(1280, 720);
text(head, (640, 96), "a data table, with its groups braced");
display(head); color(head, cyan); size(head, 26); hidden(head);
text(cap, (640, 640), ""); color(cap, dim); size(cap, 24);
// rows = regions, columns = quarters; body cells are sales (in $k)
table(t, "12 15 18 20; 9 11 14 16", (640, 360), 110, 70, "Q1 Q2 Q3 Q4", "North South");
untraced(t.lines);
hidden(t.labels); hidden(t.entries);
// column braces over the header row (bulge up: points run right -> left)
bracelabel(h1, (695, 248), (475, 248), "H1", 26); color(h1, magenta); hidden(h1);
bracelabel(h2, (915, 248), (695, 248), "H2", 26); color(h2, lime); hidden(h2);
// a vertical brace to the left of the row labels, grouping the two regions
bracelabel(reg, (356, 325), (356, 465), "Regions", 26); color(reg, cyan); hidden(reg);
// --- reveal the table ---
show(head, 0.5);
say(cap, "quarterly sales for two regions");
draw(t.lines, 1.0);
show(t.labels, 0.5);
show(t.entries, 0.5);
wait(0.4);
// --- brace the columns into halves of the year ---
section("Halves of the year");
say(cap, "Q1-Q2 are the first half, Q3-Q4 the second");
par { flash(t.col0, magenta); flash(t.col1, magenta); }
show(h1, 0.5);
par { flash(t.col2, lime); flash(t.col3, lime); }
show(h2, 0.5);
wait(0.4);
// --- brace the rows into regions ---
section("The regions");
say(cap, "and the two rows are the regions");
par { flash(t.rowlabel0, cyan); flash(t.rowlabel1, cyan); }
show(reg, 0.6);
wait(1.4);
Statistics & probability
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
statistics — the whole story in three ideas
A guided lesson, not a feature demo: describe a dataset, meet the normal curve, then see why the bell is everywhere (the Central Limit Theorem). The stats companion to the linear-algebra lesson. Start here.
// ============================================================================
// statistics.manic — Statistics, from data to the bell (a lesson)
// ----------------------------------------------------------------------------
// The stats-rung capstone, companion to linear-algebra.manic. Three chapters on
// one stage, chaptered with section():
// 1. DESCRIBE — any dataset has a shape, a centre, a spread (histogram)
// 2. THE BELL — measurements often pile into a normal curve (bellcurve)
// 3. WHY (CLT) — averages of anything become a bell (clt)
//
// Each chapter reveals its whole builtin with `untraced(tag)` → `draw(tag)`
// (preserving faint band opacities) and clears with `fade(tag)`.
// ============================================================================
title("Statistics: from data to the bell");
canvas("16:9");
let sy = cy + 30;
// 1. a real dataset (exam scores, already roughly bell-shaped)
histogram(hg, (cx, sy),
"68 72 75 71 74 77 73 70 76 72 74 78 71 73 75 69 74 72 76 73 71 75 70 74 72 73 74 71 75 73", 12, 620, 230);
untraced(hg);
// 2. the idealised normal curve
bellcurve(bc, (cx, sy - 24), 100, 15, 78);
untraced(bc);
// 3. why the bell is everywhere — the Central Limit Theorem
clt(ct, (cx, sy), 5, 1000, 7, 620, 250);
untraced(ct);
text(cap, (cx, h - 44), ""); size(cap, 22); color(cap, dim); display(cap);
// ---- 1. describe ----
section("Describe the data");
draw(hg, 1.3);
say(cap, "any dataset has a shape, a centre, and a spread", 0.5);
wait(1.8);
say(cap, "these exam scores cluster around the middle, tailing off either side", 0.5);
wait(2.0);
fade(hg, 0.6);
wait(0.3);
// ---- 2. the bell ----
say(cap, "", 0.2);
section("The normal curve");
draw(bc, 1.3);
say(cap, "so many measurements pile into the same bell — the normal distribution", 0.5);
wait(2.0);
say(cap, "68% within one standard deviation, 95% within two, 99.7% within three", 0.5);
wait(2.2);
fade(bc, 0.6);
wait(0.3);
// ---- 3. why ----
say(cap, "", 0.2);
section("Why? The Central Limit Theorem");
draw(ct, 1.4);
say(cap, "average five dice, a thousand times — however flat one die is…", 0.5);
wait(2.0);
say(cap, "…the averages form a bell. THAT is why the normal is everywhere", 0.5);
wait(2.4);
histogram
The shape of a dataset: a list of numbers binned into bars that stagger in one
at a time, with the mean marked and the range labelled (histogram). Paste your
own numbers into the data string — grades, prices, heights, times.
// ============================================================================
// histogram.manic — the shape of a dataset (stats Tier 1)
// ----------------------------------------------------------------------------
// `histogram(id, (cx,cy), "v1 v2 v3 ...", [bins], [width], [height], [color])`
// bins a list of numbers into bars — the SHAPE of the data. Bars are
// `{id}.bar{k}` (tagged `{id}.bars`) so they stagger in and recolour as a
// group; `{id}.meanline` + `{id}.mean` mark the mean, `{id}.min`/`{id}.max`
// label the range. Default bin count ≈ √n.
//
// TO ADAPT: paste your own numbers into the data string (grades, prices,
// heights, times…). More `bins` = finer shape; fewer = smoother. Pass a colour
// as the last argument, or `rainbow` to give every bar its own hue (below).
// ============================================================================
title("The shape of a dataset");
canvas("16:9");
let n = 14;
text(hdr, (cx, 60), "35 exam scores — where do they cluster?");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// `rainbow` colours each bar across the spectrum — no loop needed
histogram(hg, (cx, cy + 10),
"72 85 90 68 95 88 76 91 83 79 84 60 97 81 78 86 74 89 82 93 71 87 80 77 92 85 83 88 75 90 66 82 84 79 88",
n, 640, 300, rainbow);
untraced(hg.bars); // bars sketch in one at a time
hidden(hg.meanline); hidden(hg.mean);
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
// ---- bars build up, left to right ----
say(cap, "each bar counts how many scores fall in that range", 0.5);
stagger(0.06) { for k in 0..n { draw(hg.bar{k}, 0.35); } }
wait(1.0);
say(cap, "the shape emerges: most scores cluster in the 80s, tailing off below", 0.5);
wait(1.6);
// ---- reveal the mean ----
say(cap, "and the average sits right in the thick of it", 0.5);
show(hg.meanline, 0.5); show(hg.mean, 0.4);
pulse(hg.mean);
wait(1.8);
summary
Describe a dataset in one call: the numbers as dots on a number line, with the
mean, median and mode marked, a ±1σ spread band, and readouts of the range,
variance and standard deviation (summary). Central tendency and dispersion,
together.
// ============================================================================
// summary.manic — describe a dataset (stats Tier 1)
// ----------------------------------------------------------------------------
// `summary(id, (cx,cy), "v1 v2 v3 …", [width], [color])` is the descriptive-
// statistics workhorse: the data as dots on a number line, with the **mean**
// (gold), **median** (magenta) and **mode** (lime) marked, a translucent
// **±1σ spread band**, and a readout of **n / range / variance / std**.
// Pieces: `{id}.dots` (the cloud), `{id}.meanmark`/`.medianmark`/`.modemark`
// (+ `.*lbl`), `{id}.band`, `{id}.min`/`.max`, `{id}.readout`.
//
// TO ADAPT: paste your own numbers. Central tendency (mean/median/mode) and
// dispersion (range/variance/std) come out in one call.
// ============================================================================
title("Describe a dataset");
canvas("16:9");
text(hdr, (cx, 64), "20 daily temperatures — centre and spread");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
summary(sm, (cx, cy - 10),
"18 21 20 22 19 23 21 20 21 24 22 21 19 20 23 21 22 20 21 25", 640);
// reveal in beats
untraced(sm.line);
hidden(sm.dots);
hidden(sm.band);
hidden(sm.meanmark); hidden(sm.meanlbl);
hidden(sm.medianmark); hidden(sm.medianlbl);
hidden(sm.modemark); hidden(sm.modelbl);
hidden(sm.readout);
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
say(cap, "start with the raw numbers, laid out on a line", 0.5);
draw(sm.line, 0.6);
show(sm.dots, 0.6);
wait(1.4);
say(cap, "the centre: mean, median and mode", 0.5);
show(sm.meanmark, 0.4); show(sm.meanlbl, 0.4);
show(sm.medianmark, 0.4); show(sm.medianlbl, 0.4);
show(sm.modemark, 0.4); show(sm.modelbl, 0.4);
wait(1.6);
say(cap, "the spread: most values fall within one standard deviation of the mean", 0.5);
to(sm.band, opacity, 0.12, 0.6);
show(sm.readout, 0.4);
wait(2.0);
boxplot
The five-number summary as a box-and-whisker: the box spans Q1→Q3 (its width is
the interquartile range), a line marks the median, the whiskers reach the rest,
and a value far outside is flagged as an outlier (boxplot).
// ============================================================================
// boxplot.manic — the five-number summary (stats Tier 1)
// ----------------------------------------------------------------------------
// `boxplot(id, (cx,cy), "v1 v2 v3 …", [width], [color])` draws a box-and-whisker:
// the box spans Q1→Q3 (its width IS the interquartile range), a line marks the
// median, whiskers reach the extreme non-outliers (within 1.5·IQR), and points
// beyond are flagged as outliers. Pieces: `{id}.box`, `{id}.med`,
// `{id}.whiskerlo`/`.whiskerhi` (+ `.caplo`/`.caphi`), `{id}.outliers`,
// `{id}.iqr` and the value labels.
//
// TO ADAPT: paste your numbers. A value far from the box (here 40) shows up as
// an outlier automatically.
// ============================================================================
title("Box-and-whisker: the five-number summary");
canvas("16:9");
text(hdr, (cx, 70), "response times (seconds) — one slow outlier");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
boxplot(bp, (cx, cy), "12 15 14 10 18 16 13 15 14 17 40 11 16 15 14", 700);
untraced(bp.whiskerlo); untraced(bp.whiskerhi); untraced(bp.caplo); untraced(bp.caphi); untraced(bp.med);
hidden(bp.box);
hidden(bp.outliers);
hidden(bp.iqr); hidden(bp.lmin); hidden(bp.lmax); hidden(bp.lmed); hidden(bp.lq1); hidden(bp.lq3);
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
say(cap, "the box holds the middle half of the data — Q1 to Q3, the IQR", 0.5);
to(bp.box, opacity, 0.2, 0.6);
show(bp.lq1, 0.3); show(bp.lq3, 0.3); show(bp.iqr, 0.3);
wait(1.5);
say(cap, "the line inside is the median; the whiskers reach the rest", 0.5);
draw(bp.med, 0.4); show(bp.lmed, 0.3);
par { draw(bp.whiskerlo, 0.5); draw(bp.whiskerhi, 0.5); }
draw(bp.caplo, 0.3); draw(bp.caphi, 0.3);
show(bp.lmin, 0.3); show(bp.lmax, 0.3);
wait(1.6);
say(cap, "and a value far outside the whiskers is flagged as an outlier", 0.5);
show(bp.outliers, 0.4); pulse(bp.outliers);
wait(1.9);
skew
Which way does the tail point? A histogram with the mean and median marked and a
labelled skewness — when the mean is dragged right of the median, the data is
right-skewed (skew).
// ============================================================================
// skew.manic — the shape of a dataset: skewness (stats Tier 1)
// ----------------------------------------------------------------------------
// `skew(id, (cx,cy), "v1 v2 v3 …", [bins], [width], [height], [color])` draws a
// histogram with the **mean** (gold) and **median** (magenta) marked and a
// labelled skewness. The tell: when the mean sits right of the median, a right
// tail is pulling it — the data is right-skewed (positive); left of it, left-
// skewed; on top of it, symmetric.
//
// TO ADAPT: paste your numbers. Incomes, wait times and prices are classically
// right-skewed (a few big values pull the mean up).
// ============================================================================
title("Skewness: which way does the tail point?");
canvas("16:9");
text(hdr, (cx, 66), "monthly salaries (k) — a few big earners pull the mean up");
size(hdr, 23); color(hdr, cyan); bold(hdr); display(hdr);
skew(sk, (cx, cy - 10),
"28 30 32 31 29 33 35 30 31 34 42 38 30 32 31 33 30 36 55 68 90 30 34 31", 14, 560, 240);
untraced(sk.bars);
hidden(sk.meanline); hidden(sk.meanlbl);
hidden(sk.medianline); hidden(sk.medianlbl);
hidden(sk.skewlbl);
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
say(cap, "most salaries cluster low, with a long tail of high earners", 0.5);
stagger(0.05) { for k in 0..14 { draw(sk.bar{k}, 0.3); } }
wait(1.2);
say(cap, "the median sits in the cluster — but the mean is dragged toward the tail", 0.5);
show(sk.medianline, 0.4); show(sk.medianlbl, 0.3);
show(sk.meanline, 0.4); show(sk.meanlbl, 0.3);
wait(1.6);
say(cap, "mean to the right of median = right-skewed (positive skew)", 0.5);
show(sk.skewlbl, 0.4);
wait(1.8);
bellcurve
The normal (Gaussian) bell curve and the 68-95-99.7 rule: the bell draws in,
then the ±1σ / ±2σ / ±3σ bands shade one at a time, showing that 68% of values
fall within one standard deviation, 95% within two, and 99.7% within three
(bellcurve, alias gaussian).
// ============================================================================
// normal.manic — the bell curve & the 68-95-99.7 rule (stats Tier 2)
// ----------------------------------------------------------------------------
// `bellcurve(id, (cx,cy), mu, sigma, [unit], [color])` (alias `gaussian`) draws
// bell curve and shades the ±1σ/±2σ/±3σ bands — the 68-95-99.7 rule. Pieces:
// `{id}.curve` (the bell), `{id}.band1/2/3` (tagged `{id}.bands`), `{id}.mean`,
// `{id}.p1/p2/p3` (the percentages), `{id}.t{-3..3}` (value ticks).
// `unit` = pixels per σ; the bell is standardised, μ/σ set the axis values.
//
// TO ADAPT: change mu/sigma to your distribution (heights, IQ, measurement
// error…). The 68-95-99.7 rule holds for every normal.
// ============================================================================
title("The bell curve & the 68-95-99.7 rule");
canvas("16:9");
// IQ scores: mean 100, standard deviation 15
bellcurve(nd, (cx, cy + 10), 100, 15, 95);
untraced(nd.curve); // the bell draws in
hidden(nd.mean);
hidden(nd.bands); // the three σ-bands reveal one at a time
hidden(nd.p1); hidden(nd.p2); hidden(nd.p3);
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
say(cap, "IQ scores: mean 100, standard deviation 15 — the classic bell", 0.5);
draw(nd.curve, 1.1);
show(nd.mean, 0.4);
wait(1.2);
say(cap, "68% of values fall within ONE standard deviation of the mean", 0.5);
to(nd.band1, opacity, 0.16, 0.6); show(nd.p1, 0.4);
wait(1.7);
say(cap, "95% fall within two — almost everyone", 0.5);
to(nd.band2, opacity, 0.16, 0.6); show(nd.p2, 0.4);
wait(1.7);
say(cap, "and 99.7% within three: the tails are tiny", 0.5);
to(nd.band3, opacity, 0.16, 0.6); show(nd.p3, 0.4);
wait(1.9);
clt
The Central Limit Theorem — the flagship: however flat a single die is, the
average of five dice, taken 1200 times, piles into a bell that hugs the normal
curve (clt). Seeded, so it renders the same every time.
// ============================================================================
// clt.manic — the Central Limit Theorem (stats Tier 3, the flagship payoff)
// ----------------------------------------------------------------------------
// `clt(id, (cx,cy), samplesize, trials, [seed], [width], [height])` runs `trials`
// experiments — each the average of `samplesize` dice — and histograms those
// averages. However flat a single die is, the averages pile into a BELL. Draws
// the histogram of sample means (`{id}.bar{k}`, tagged `{id}.bars`), the normal
// curve they converge to (`{id}.curve`), the mean line, ticks, and an info
// label. Seeded → the render is identical every time.
//
// TO ADAPT: change `samplesize` (bigger n → tighter bell) or `trials` (more →
// smoother). `seed` picks the reproducible random sequence.
// ============================================================================
title("The Central Limit Theorem");
canvas("16:9");
clt(ct, (cx, cy + 20), 5, 1200, 7, 660, 300);
untraced(ct.bars); // the sample means pile up, bar by bar
untraced(ct.curve); // the normal curve draws in last
hidden(ct.mean);
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
say(cap, "one die is flat — 1 to 6 are equally likely. but average FIVE dice…", 0.5);
stagger(0.03) { for k in 0..30 { draw(ct.bar{k}, 0.3); } }
wait(1.0);
say(cap, "…and repeat 1200 times: the averages pile up around the middle", 0.5);
show(ct.mean, 0.4);
wait(1.6);
say(cap, "they trace a bell — the Central Limit Theorem: averages are normal", 0.5);
draw(ct.curve, 1.1);
wait(2.0);
correlation
Do two things move together? The scatter of paired data, the best-fit line, and
the Pearson correlation r — near +1 a tight upward line, near −1 downward, near
0 a shapeless blob (correlation).
// ============================================================================
// correlation.manic — how strongly two variables move together (stats T3)
// ----------------------------------------------------------------------------
// `correlation(id, (cx,cy), unit, "x1 y1 x2 y2 …", [color])` scatters the
// points, fits the best line, and reports the **Pearson correlation r** with a
// strong/moderate/weak · positive/negative reading. `unit` = pixels per data
// unit (x and y share it, so use data with comparable ranges). Points
// `{id}.p{k}` (tagged `{id}.points`), the fit `{id}.line`, and `{id}.r`.
//
// TO ADAPT: paste your paired data. r near ±1 = a tight line; near 0 = a blob.
// ============================================================================
title("Correlation: do they move together?");
canvas("16:9");
text(hdr, (cx, 70), "hours studied vs grade (out of 10)");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
correlation(co, (cx, cy - 10), 42,
"1 3 2 4 3 4 4 6 5 6 6 7 7 8 8 8 9 10 10 9");
hidden(co.points); untraced(co.line); hidden(co.r);
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
say(cap, "each dot is one student: hours studied, and the grade they got", 0.5);
show(co.points, 0.6);
wait(1.3);
say(cap, "the cloud slopes up — more study tends to mean a higher grade", 0.5);
draw(co.line, 0.9);
wait(1.3);
say(cap, "r measures how tightly the points track that line: near +1 is strong", 0.5);
show(co.r, 0.4); pulse(co.r);
wait(1.8);
lln
The Law of Large Numbers: flip a fair coin over and over and track the running
proportion of heads. It swings wildly at first, then settles onto the true 0.5
as the trials pile up (lln). Draw the curve in to watch it converge.
// ============================================================================
// lln.manic — the Law of Large Numbers (stats Tier 3)
// ----------------------------------------------------------------------------
// `lln(id, (cx,cy), trials, [seed], [width], [height])` plots the running
// proportion of heads over many coin flips: wild at first, settling onto the
// true 0.5. Draws `{id}.curve`, the reference line `{id}.ref`, axis labels, and
// the final value. Seeded → the render is identical every time.
//
// TO ADAPT: change `trials` (more → tighter settling) or `seed` (a different
// reproducible run).
// ============================================================================
title("The Law of Large Numbers");
canvas("16:9");
lln(ll, (cx, cy + 10), 600, 3, 720, 300);
untraced(ll.curve); // the proportion traces in — watch it settle
hidden(ll.finallbl);
hidden(ll.truelbl);
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
say(cap, "flip a fair coin over and over; track the proportion of heads so far", 0.5);
show(ll.truelbl, 0.4);
wait(0.8);
say(cap, "early on it swings wildly — a few flips prove nothing", 0.5);
draw(ll.curve, 2.6);
wait(0.4);
say(cap, "but over many trials it settles onto the true probability, 0.5", 0.5);
show(ll.finallbl, 0.4);
wait(1.9);
hypothesis
Is a result surprising enough to be real? Under the null hypothesis the test
statistic follows the standard normal; the observed z cuts off tails whose area
is the p-value. Smaller than α, reject (hypothesis).
// ============================================================================
// hypothesis.manic — a significance test & the p-value (stats Tier 5)
// ----------------------------------------------------------------------------
// `hypothesis(id, (cx,cy), z, [alpha], [unit])` — under the null hypothesis the
// test statistic is standard-normal; the observed z cuts off tails whose area is
// the p-value. If that area is smaller than alpha, the result is too surprising
// to be chance — reject. Pieces: `{id}.curve`, `{id}.tails`, `{id}.zline`,
// `{id}.p`, `{id}.verdict`.
// ============================================================================
title("Hypothesis testing: is this surprising?");
canvas("16:9");
hypothesis(hy, (cx, cy - 10), 2.3, 0.05, 92);
untraced(hy.curve);
hidden(hy.tails); hidden(hy.zline); hidden(hy.zlbl);
hidden(hy.p); hidden(hy.verdict);
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
say(cap, "if nothing's going on, the test statistic follows this bell", 0.5);
draw(hy.curve, 1.0);
wait(1.2);
say(cap, "we observed z = 2.3 — out here in the tails", 0.5);
show(hy.zline, 0.4); show(hy.zlbl, 0.3);
wait(1.4);
say(cap, "the shaded tail area is the p-value: how likely a result this extreme is", 0.5);
to(hy.tails, opacity, 0.5, 0.6); show(hy.p, 0.4);
wait(1.6);
say(cap, "p = 0.021 < 0.05, so it's too surprising for chance — reject the null", 0.5);
show(hy.verdict, 0.4);
wait(1.9);
covariance
Covariance as signed area: a cross at the means, and a rectangle from each point
to the centre — cyan where x and y agree, magenta where they disagree. Their
balance is the covariance (covariance).
// ============================================================================
// covariance.manic — covariance as signed area (stats Tier 5)
// ----------------------------------------------------------------------------
// `covariance(id, (cx,cy), unit, "x1 y1 x2 y2 …", [color])` — a cross at the
// means splits the plane into quadrants; each point draws a rectangle to the
// mean-corner, cyan where (x-x̄)(y-ȳ) > 0 (agreeing) and magenta where negative.
// Their signed-area balance IS the covariance. `{id}.points`, `{id}.rects`,
// `{id}.cross`, `{id}.cov`.
// ============================================================================
title("Covariance: signed area about the mean");
canvas("16:9");
covariance(cv, (cx, cy - 10), 34, "1 2 2 1 3 4 4 3 5 6 6 5 7 8 8 7 9 9");
hidden(cv.points); hidden(cv.rects);
untraced(cv.crossv); untraced(cv.crossh);
hidden(cv.cov);
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
say(cap, "mark each point, then draw the cross at the mean of x and the mean of y", 0.5);
show(cv.points, 0.5);
par { draw(cv.crossv, 0.5); draw(cv.crossh, 0.5); }
wait(1.4);
say(cap, "each point makes a rectangle to the centre — cyan if x and y agree", 0.5);
to(cv.rects, opacity, 0.13, 0.7);
wait(1.6);
say(cap, "more agreeing (cyan) area than disagreeing = positive covariance", 0.5);
show(cv.cov, 0.4);
wait(1.9);
bayes
Bayesian updating: a prior belief about a coin’s bias, the likelihood from the
data, and the posterior that combines them — pulled toward the evidence and
sharpening as it accumulates (bayes).
// ============================================================================
// bayes.manic — Bayesian updating (stats Tier 5)
// ----------------------------------------------------------------------------
// `bayes(id, (cx,cy), heads, tails, [width], [height])` — belief about a coin's
// bias: a mild PRIOR, the LIKELIHOOD from the data, and the POSTERIOR that
// combines them (pulled toward the data, sharpening as evidence grows). Pieces:
// `{id}.prior`, `{id}.likelihood`, `{id}.posterior`, `{id}.mean`.
// ============================================================================
title("Bayes: updating a belief with data");
canvas("16:9");
bayes(by, (cx, cy + 10), 7, 2, 640, 250);
untraced(by.prior); untraced(by.likelihood); untraced(by.posterior);
hidden(by.mean); hidden(by.priorlbl); hidden(by.postlbl); hidden(by.datalbl);
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
say(cap, "before any flips, a mild belief: the coin is probably fair-ish (the prior)", 0.5);
draw(by.prior, 0.8); show(by.priorlbl, 0.3);
wait(1.4);
say(cap, "then we flip: 7 heads, 2 tails — the data favours a biased coin (likelihood)", 0.5);
draw(by.likelihood, 0.8); show(by.datalbl, 0.3);
wait(1.6);
say(cap, "combine them and the posterior lands between: p about 0.69", 0.5);
draw(by.posterior, 0.8); show(by.mean, 0.4); show(by.postlbl, 0.3);
wait(2.0);
probability
A probability & sampling playground in four chapters: named distributions
(uniform / exponential / binomial / Poisson), a confidence interval, a
Monte-Carlo estimate of π, and a random walk (distribution, confidence,
montecarlo, randomwalk).
// ============================================================================
// probability.manic — a probability & sampling playground (stats)
// ----------------------------------------------------------------------------
// Four ideas on one stage, chaptered with section():
// 1. named DISTRIBUTIONS (uniform / exponential / binomial / poisson)
// 2. CONFIDENCE intervals (an estimate ± a margin)
// 3. MONTE-CARLO (estimate pi by throwing darts)
// 4. RANDOM WALK (a path that wanders)
// Each chapter reveals with untraced(tag) -> draw(tag) and clears with fade(tag).
// The seeded builtins (montecarlo, randomwalk) render identically every time.
// ============================================================================
title("A probability playground");
canvas("16:9");
// ---- 1. named distributions (a 2x2 gallery) ----
distribution(du, (cx - 350, cy - 120), "uniform", 2, 6); untraced(du);
distribution(de, (cx + 350, cy - 120), "exponential", 1); untraced(de);
distribution(db, (cx - 350, cy + 180), "binomial", 12, 0.4); untraced(db);
distribution(dp, (cx + 350, cy + 180), "poisson", 4); untraced(dp);
// ---- 2. a confidence interval ----
confidence(ci, (cx, cy), 50, 8, 25, 95, 620); untraced(ci); hidden(ci.estimate);
// ---- 3. monte-carlo pi ----
montecarlo(mc, (cx, cy - 10), 1200, 7, 210); untraced(mc);
// ---- 4. a random walk ----
randomwalk(rw, (cx, cy), 500, 4, 12); untraced(rw);
text(cap, (cx, h - 44), ""); size(cap, 22); color(cap, dim); display(cap);
section("Named distributions");
par { draw(du, 0.9); draw(de, 0.9); draw(db, 0.9); draw(dp, 0.9); }
say(cap, "flat, decaying, discrete counts — the classic shapes of chance", 0.5);
wait(2.2);
par { fade(du, 0.5); fade(de, 0.5); fade(db, 0.5); fade(dp, 0.5); }
wait(0.3);
say(cap, "", 0.2);
section("Confidence intervals");
draw(ci, 0.9); show(ci.estimate, 0.4);
say(cap, "an estimate is never exact — the interval says how sure we are", 0.5);
wait(2.2);
fade(ci, 0.6);
wait(0.3);
say(cap, "", 0.2);
section("Monte Carlo");
draw(mc, 1.2);
say(cap, "throw darts at random: the fraction landing in the circle gives pi", 0.5);
wait(2.4);
fade(mc, 0.6);
wait(0.3);
say(cap, "", 0.2);
section("Random walk");
draw(rw, 1.4);
say(cap, "each step a random direction — chance draws a wandering path", 0.5);
wait(2.2);
Physics — simulations
Each simulation is pre-simulated with RK4 at build time — deterministic and replayable — and its parts are ordinary manic entities the whole language composes with. The phase / time / well / energy views are optional and generic: any sim inherits them.
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
pendulum
One pendulum shown four ways from a single deterministic swing: the motion (with a
velocity arrow + KE/PE bars), the phase portrait (θ vs ω), a time series, the
potential-energy well, and energy over time (pendulum + phase/timegraph/
well/energygraph + swing).
// ============================================================================
// pendulum.manic — the physics kit's first sim, seen FOUR ways (Layer 1)
// ----------------------------------------------------------------------------
// `pendulum(id, [center], [length], [angle0], [unit], [damping])` builds a
// pendulum from its physics — PRE-SIMULATED with RK4 at build time
// (deterministic). Only `id` is required. The OPTIONAL, generic view builtins
// render the SAME simulation as math panels and all animate together on `swing`:
// · phase(id,(cx,cy),[size]) — phase portrait (θ vs ω): a closed loop
// · timegraph(id,(cx,cy),[size]) — θ(t) & ω(t) with a sweep line
// · well(id,(cx,cy),[size]) — energy well U(θ), bob = ball in a bowl
// · energygraph(id,(cx,cy),[size]) — KE / PE / total over time
//
// TO ADAPT: add damping (6th arg) and watch the phase loop spiral inward, the
// ball settle to the bottom of the well, and total energy decay — e.g.
// `pendulum(p, (250,220), 1.2, 55, 105, 0.5)`.
// ============================================================================
title("Pendulum — one swing, four views");
canvas("16:9");
text(hdr, (cx, 40), "One pendulum, four views");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// the physical sim (left), with velocity arrow + KE/PE bars
pendulum(p, (250, 220), 1.2, 55, 105);
untraced(p.path);
// four math views of the SAME simulation, 2×2 on the right
phase(p, (715, 165), 90); // θ vs ω — a closed loop
timegraph(p, (1000, 165), 90); // θ(t), ω(t) with a sweep line
well(p, (715, 455), 90); // U(θ) with the bob as a rolling ball
energygraph(p, (1000, 455), 90); // KE / PE / total over time
text(cap, (cx, h - 30), "sim · phase portrait · time series · energy well · energy over time — all from one pre-simulated swing");
size(cap, 16); color(cap, dim); display(cap);
draw(p.path, 0.8);
swing(p, 10); // every panel animates in lockstep
timing-v2-scene
Generic Timing v2 controlling an ordinary physics scene: one named-phase clock schedules the intro, pendulum motion and finish independently from its native timer look.
// Generic Timing v2 — one named clock coordinates a non-quiz physics scene.
// `timed` runs the visual timer and places every `during` block at the exact
// offset declared by `timing`; short blocks are padded, overruns are errors.
title("Generic Timing v2 — Pendulum");
canvas("16:9");
template("mono");
text(head, (315, 72), "ONE CLOCK. EVERY BEAT.");
size(head, 30); bold(head); color(head, fg); hidden(head);
text(sub, (315, 112), "Generic Timing v2 — not a quiz");
size(sub, 18); color(sub, dim); hidden(sub);
pendulum(p, (640, 245), 1.5, 42, 140, 0.03);
untraced(p.path);
equation(law, (640, 602), `T\approx2\pi\sqrt{L/g}`, 54);
color(law, gold); hidden(law);
text(note, (640, 655), "The scene and clock share the same named phases.");
size(note, 21); color(note, dim); hidden(note);
text(done, (640, 680), "INTRO → EXPERIMENT → INSIGHT → OUTRO");
size(done, 18); color(done, cyan); hidden(done);
// A fresh id creates a format-neutral timing controller. Its phases total
// 10.4 seconds; no duplicate duration is passed to `timed` or `run`.
timing(showclock, (970, 86), "intro=1.2 experiment=6 insight=2 outro=1.2");
timerstyle(showclock,
"look=segments number=outside direction=drain size=small thickness=1.15 color=cyan track=dim label=MASTER_CLOCK font=display finish=pulse");
timed(showclock) {
during("intro") {
par { show(head, 0.7); show(sub, 0.7); }
}
during("experiment") {
par { run(p, 6); draw(p.path, 6); }
}
during("insight") {
par { show(law, 0.6); show(note, 0.6); }
pulse(p.bob, 0.7);
}
during("outro") {
show(done, 0.6);
}
}
zeroth-law-thermodynamics
The Zeroth Law told through three particle-filled bodies: thermal relations connect, the bodies settle onto one temperature axis, and equilibrium becomes visible.
// ============================================================================
// zeroth-law-thermodynamics.manic — thermal equalisation explained visually
// ----------------------------------------------------------------------------
// A shot-for-shot structural recreation of engine-test.mp4 using only the
// generic std + math vocabulary. It deliberately avoids a thermal-specific
// kit: circles, dots, lines, LaTeX, formula plots and timeline composition
// should be enough to tell the story.
//
// Reference beats: network 0–9s · temperature axis 9–20s · transfer 20–35s
// · closing thought 35–39.4s. Record at 60 fps with the reel preset.
// ============================================================================
title("Zeroth Law of Thermodynamics");
canvas("9:16");
template("mono");
// Reels put controls down the right edge and captions near the bottom. Keep
// identity in the upper-left safe area, clear of both UI and the three bodies.
watermark(manicMark, (155, 145), "Made With Manic");
// Shared palette expressed as hues so the example keeps its identity even
// when somebody overrides the template from the CLI.
let teal = 168;
let warm = 38;
let cool = 246;
// ---------------------------------------------------------------------------
// SCENE 1 · three bodies connected by possible heat exchange
// ---------------------------------------------------------------------------
circle(s1A, (540, 280), 92);
circle(s1B, (300, 565), 92);
circle(s1C, (780, 565), 92);
outline(s1A, cyan); outline(s1B, cyan); outline(s1C, cyan);
stroke(s1A, 5); stroke(s1B, 5); stroke(s1C, 5);
glow(s1A, 1.5); glow(s1B, 1.5); glow(s1C, 1.5);
tag(s1A, s1); tag(s1B, s1); tag(s1C, s1);
equation(s1la, (540, 280), `A`, 68);
equation(s1lb, (300, 565), `B`, 68);
equation(s1lc, (780, 565), `C`, 68);
tag(s1la, s1); tag(s1lb, s1); tag(s1lc, s1);
// Generic contained dots: the ids make these "matter" here, but the engine
// words are equally useful for bubbles, dust, stars, or moving data.
particles(s1matterA, s1A, 22, 5, 7);
particles(s1matterB, s1B, 22, 5, 17);
particles(s1matterC, s1C, 22, 5, 27);
hue(s1matterA, teal, 0.65, 0.70); hue(s1matterB, teal, 0.65, 0.70); hue(s1matterC, teal, 0.65, 0.70);
glow(s1matterA, 1.0); glow(s1matterB, 1.0); glow(s1matterC, 1.0);
tag(s1matterA, s1); tag(s1matterB, s1); tag(s1matterC, s1);
// Curved tracked links replace hand-computed endpoints and keep following if
// the bodies move. Signed bends bow the two upper links away from the triangle.
link(s1ab, s1A, s1B, 48);
link(s1ac, s1A, s1C, -48);
link(s1bc, s1B, s1C, 55);
stroke(s1ab, 4); stroke(s1ac, 4); stroke(s1bc, 4);
hue(s1ab, teal, 0.72, 0.54); hue(s1ac, teal, 0.72, 0.54); hue(s1bc, teal, 0.72, 0.54);
glow(s1ab, 0.8); glow(s1ac, 0.8); glow(s1bc, 0.8);
untraced(s1ab); untraced(s1ac); untraced(s1bc);
tag(s1ab, s1); tag(s1ac, s1); tag(s1bc, s1); tag(s1ab, s1links); tag(s1ac, s1links); tag(s1bc, s1links);
// Each relation first appears as a faint possibility, then becomes definite as
// its curved connection draws on.
equation(s1eqAB, (315, 365), `A\sim B`, 35);
equation(s1eqBC, (540, 660), `B\sim C`, 35);
equation(s1eqAC, (765, 365), `A\sim C`, 35);
color(s1eqAB, dim); color(s1eqBC, dim); color(s1eqAC, dim);
tag(s1eqAB, s1); tag(s1eqBC, s1); tag(s1eqAC, s1);
tag(s1eqAB, s1relations); tag(s1eqBC, s1relations); tag(s1eqAC, s1relations);
tag(s1A, s1bodyA); tag(s1la, s1bodyA); tag(s1matterA, s1bodyA);
tag(s1B, s1bodyB); tag(s1lb, s1bodyB); tag(s1matterB, s1bodyB);
tag(s1C, s1bodyC); tag(s1lc, s1bodyC); tag(s1matterC, s1bodyC);
hidden(s1A); hidden(s1B); hidden(s1C);
hidden(s1la); hidden(s1lb); hidden(s1lc);
hidden(s1matterA); hidden(s1matterB); hidden(s1matterC);
hidden(s1eqAB); hidden(s1eqBC); hidden(s1eqAC);
// ---------------------------------------------------------------------------
// SCENE 2 · temperature becomes a position on one common scale
// ---------------------------------------------------------------------------
line(s2y, (180, 700), (180, 1390));
line(s2g0, (180, 820), (900, 820));
line(s2g1, (180, 1070), (900, 1070));
line(s2g2, (180, 1320), (900, 1320));
line(s2eqline, (230, 1070), (850, 1070));
color(s2y, blue); color(s2g0, dim); color(s2g1, dim); color(s2g2, dim); hue(s2eqline, teal, 0.65, 0.55);
stroke(s2y, 4); stroke(s2g0, 2); stroke(s2g1, 2); stroke(s2g2, 2); stroke(s2eqline, 5);
glow(s2eqline, 0.9);
tag(s2y, s2); tag(s2g0, s2); tag(s2g1, s2); tag(s2g2, s2); tag(s2eqline, s2);
tag(s2y, s2axes); tag(s2g0, s2axes); tag(s2g1, s2axes); tag(s2g2, s2axes); tag(s2eqline, s2axes);
equation(s2T, (180, 650), `T`, 44); color(s2T, fg); tag(s2T, s2); tag(s2T, s2axes);
dot(s2A, (430, 1070), 10); dot(s2B, (540, 1070), 10); dot(s2C, (650, 1070), 10);
hue(s2A, teal, 0.65, 0.60); hue(s2B, teal, 0.65, 0.60); hue(s2C, teal, 0.65, 0.60);
glow(s2A, 1.1); glow(s2B, 1.1); glow(s2C, 1.1);
tag(s2A, s2); tag(s2B, s2); tag(s2C, s2); tag(s2A, s2points); tag(s2B, s2points); tag(s2C, s2points);
equation(s2la, (430, 1025), `A`, 35); equation(s2lb, (540, 1025), `B`, 35); equation(s2lc, (650, 1025), `C`, 35);
tag(s2la, s2); tag(s2lb, s2); tag(s2lc, s2); tag(s2la, s2points); tag(s2lb, s2points); tag(s2lc, s2points);
equation(s2eq, (540, 1490), `T_A=T_B=T_C`, 48); tag(s2eq, s2); tag(s2eq, s2eqgroup);
line(s2hot, (230, 820), (850, 820)); line(s2cold, (230, 1320), (850, 1320));
hue(s2hot, warm, 0.70, 0.62); hue(s2cold, cool, 0.70, 0.62); stroke(s2hot, 4); stroke(s2cold, 4);
line(s2bracket, (850, 820), (850, 1320)); line(s2cap0, (825, 820), (875, 820)); line(s2cap1, (825, 1320), (875, 1320));
color(s2bracket, fg); color(s2cap0, fg); color(s2cap1, fg); stroke(s2bracket, 3); stroke(s2cap0, 3); stroke(s2cap1, 3);
equation(s2delta, (930, 1070), `\Delta T`, 42);
tag(s2hot, s2deltaGroup); tag(s2cold, s2deltaGroup); tag(s2bracket, s2deltaGroup);
tag(s2cap0, s2deltaGroup); tag(s2cap1, s2deltaGroup); tag(s2delta, s2deltaGroup);
tag(s2hot, s2); tag(s2cold, s2); tag(s2bracket, s2); tag(s2cap0, s2); tag(s2cap1, s2); tag(s2delta, s2);
hidden(s2);
// ---------------------------------------------------------------------------
// SCENE 3 · energy flows while two temperatures converge exponentially
// ---------------------------------------------------------------------------
circle(s3A, (300, 430), 98); circle(s3C, (780, 430), 98);
outline(s3A, orange); outline(s3C, blue);
stroke(s3A, 5); stroke(s3C, 5); glow(s3A, 1.5); glow(s3C, 1.5);
tag(s3A, s3initialrings); tag(s3C, s3initialrings);
link(s3link, s3A, s3C); hue(s3link, teal, 0.65, 0.54); stroke(s3link, 5); glow(s3link, 0.8);
// Crossfade to common teal rings as equilibrium approaches. Keeping the fill
// dark preserves the reservoir/particle texture instead of becoming a disc.
circle(s3Afinal, (300, 430), 98); circle(s3Cfinal, (780, 430), 98);
outline(s3Afinal, cyan); outline(s3Cfinal, cyan); stroke(s3Afinal, 5); stroke(s3Cfinal, 5);
glow(s3Afinal, 1.5); glow(s3Cfinal, 1.5); hidden(s3Afinal); hidden(s3Cfinal);
tag(s3Afinal, s3); tag(s3Cfinal, s3); tag(s3Afinal, s3finalrings); tag(s3Cfinal, s3finalrings);
equation(s3la, (300, 430), `A`, 70); equation(s3lc, (780, 430), `C`, 70);
tag(s3link, s3nodes); tag(s3A, s3nodes); tag(s3C, s3nodes); tag(s3la, s3nodes); tag(s3lc, s3nodes);
tag(s3link, s3); tag(s3A, s3); tag(s3C, s3); tag(s3la, s3); tag(s3lc, s3);
particles(s3hotparticles, s3A, 24, 5, 41);
particles(s3coldparticles, s3C, 24, 5, 73);
hue(s3hotparticles, warm, 0.72, 0.68); hue(s3coldparticles, cool, 0.72, 0.68);
glow(s3hotparticles, 1.0); glow(s3coldparticles, 1.0);
tag(s3hotparticles, s3); tag(s3hotparticles, s3nodes);
tag(s3coldparticles, s3); tag(s3coldparticles, s3nodes);
// Temperature-vs-time graph. The two exact formulas share the same asymptote.
line(s3x, (190, 1340), (900, 1340)); line(s3y, (190, 1340), (190, 760));
color(s3x, dim); color(s3y, dim); stroke(s3x, 3); stroke(s3y, 3);
equation(s3theta, (190, 710), `\theta`, 40); equation(s3time, (930, 1340), `t`, 38);
plot(s3upper, (230, 1340), 155, 190, "1 + exp(-x)", (0, 4));
plot(s3lower, (230, 1340), 155, 190, "1 - exp(-x)", (0, 4));
hue(s3upper, warm, 0.78, 0.62); hue(s3lower, cool, 0.78, 0.62); stroke(s3upper, 5); stroke(s3lower, 5);
glow(s3upper, 0.7); glow(s3lower, 0.7); untraced(s3upper); untraced(s3lower);
tag(s3x, s3); tag(s3y, s3); tag(s3theta, s3); tag(s3time, s3); tag(s3upper, s3); tag(s3lower, s3);
tag(s3x, s3axes); tag(s3y, s3axes); tag(s3theta, s3axes); tag(s3time, s3axes);
tag(s3upper, s3curves); tag(s3lower, s3curves);
equation(s3eq, (540, 1510), `\Delta T(t)=\Delta T_0 e^{-2k t/C}`, 44);
color(s3eq, fg); tag(s3eq, s3); tag(s3eq, s3equation);
hidden(s3);
// ---------------------------------------------------------------------------
// SCENE 4 · reduce the mechanics to one thought
// ---------------------------------------------------------------------------
line(s4line, (420, 850), (660, 850)); hue(s4line, teal, 0.72, 0.58); stroke(s4line, 6); glow(s4line, 1.0);
dot(s4a, (420, 850), 16); dot(s4c, (660, 850), 16); hue(s4a, teal, 0.70, 0.63); hue(s4c, teal, 0.70, 0.63); glow(s4a, 1.3); glow(s4c, 1.3);
text(s4quote, (540, 1130), "Temperature names sameness.\nDifference gives energy direction.");
size(s4quote, 38); color(s4quote, fg); bold(s4quote); untraced(s4quote);
tag(s4line, s4); tag(s4a, s4); tag(s4c, s4); tag(s4quote, s4);
hidden(s4line); hidden(s4a); hidden(s4c);
// ---------------------------------------------------------------------------
// TIMELINE · 39.4 seconds
// ---------------------------------------------------------------------------
par {
wander(s1matterA, 7.5);
wander(s1matterB, 7.5);
wander(s1matterC, 7.5);
seq {
show(s1bodyA, 0.45);
wait(0.20);
show(s1bodyB, 0.45);
show(s1eqAB, 0.25);
par { draw(s1ab, 0.70); recolor(s1eqAB, fg, 0.70); }
flow(s1ab, 0.75);
show(s1bodyC, 0.45);
show(s1eqBC, 0.25);
par { draw(s1bc, 0.70); recolor(s1eqBC, fg, 0.70); }
flow(s1bc, 0.75);
show(s1eqAC, 0.25);
par { draw(s1ac, 0.70); recolor(s1eqAC, fg, 0.70); }
par { flow(s1ab, 1.05); flow(s1bc, 1.05); flow(s1ac, 1.05); }
wait(0.55);
}
}
// The three bodies do not cut to new dots: they visibly shrink, line up on a
// common temperature axis, then hand off to the precise plotted markers.
par {
fade(s1relations, 0.45); fade(s1links, 0.45);
fade(s1matterA, 0.45); fade(s1matterB, 0.45); fade(s1matterC, 0.45);
show(s2axes, 0.70);
move(s1A, (430, 1070), 1.10, smooth); scale(s1A, 0.12, 1.10, smooth);
move(s1B, (540, 1070), 1.10, smooth); scale(s1B, 0.12, 1.10, smooth);
move(s1C, (650, 1070), 1.10, smooth); scale(s1C, 0.12, 1.10, smooth);
move(s1la, (430, 1025), 1.10, smooth); scale(s1la, 0.52, 1.10, smooth);
move(s1lb, (540, 1025), 1.10, smooth); scale(s1lb, 0.52, 1.10, smooth);
move(s1lc, (650, 1025), 1.10, smooth); scale(s1lc, 0.52, 1.10, smooth);
}
par {
fade(s1bodyA, 0.22); fade(s1bodyB, 0.22); fade(s1bodyC, 0.22);
show(s2points, 0.22);
}
show(s2eqgroup, 0.5);
wait(4.0);
par {
move(s2A, (430, 820), 1.3, smooth); move(s2la, (430, 775), 1.3, smooth);
move(s2C, (650, 1320), 1.3, smooth); move(s2lc, (650, 1275), 1.3, smooth);
recolor(s2A, orange, 1.0); recolor(s2la, orange, 1.0);
recolor(s2C, blue, 1.0); recolor(s2lc, blue, 1.0);
fade(s2eqgroup, 0.5);
}
show(s2deltaGroup, 0.5);
wait(3.5);
// Keep visual identity across the scene change: the two temperature points
// first become small bodies on their own level lines, then travel and enlarge
// into the particle-filled reservoirs. The old axis remains behind them until
// the new transfer graph is already visible—there is never an empty frame.
par {
fade(s2B, 0.40); fade(s2lb, 0.40);
scale(s2A, 5.0, 0.70, smooth); move(s2la, (430, 820), 0.70, smooth); scale(s2la, 1.35, 0.70, smooth);
scale(s2C, 5.0, 0.70, smooth); move(s2lc, (650, 1320), 0.70, smooth); scale(s2lc, 1.35, 0.70, smooth);
}
par {
move(s2A, (300, 430), 0.50, smooth); scale(s2A, 9.8, 0.50, smooth);
move(s2la, (300, 430), 0.50, smooth); scale(s2la, 2.0, 0.50, smooth);
move(s2C, (780, 430), 0.50, smooth); scale(s2C, 9.8, 0.50, smooth);
move(s2lc, (780, 430), 0.50, smooth); scale(s2lc, 2.0, 0.50, smooth);
show(s3axes, 0.50);
}
par {
show(s3nodes, 0.70);
fade(s2, 0.70);
}
show(s3curves, 0.01);
par {
draw(s3upper, 10.7, smooth);
draw(s3lower, 10.7, smooth);
wander(s3hotparticles, 10.7);
wander(s3coldparticles, 10.7);
fade(s3initialrings, 10.5); show(s3finalrings, 10.5);
recolor(s3hotparticles, cyan, 10.5); recolor(s3coldparticles, cyan, 10.5);
seq {
for i in 0..6 { seq { flow(s3link, 1.20); wait(0.55); } }
}
}
show(s3equation, 0.6);
wait(1.5);
fade(s3, 0.8);
show(s4line, 0.5); show(s4a, 0.01); show(s4c, 0.01);
type(s4quote, 1.2);
wait(2.55); // keeps the benchmark at 39.4 s including manic's final 1 s tail
pendulum-damped
The same four views with friction on (damping): the swing decays, the phase loop
spirals inward, the well ball settles, and the total-energy line drops — dissipation
told the same way by every panel.
// ============================================================================
// pendulum-damped.manic — the SAME four views, now with friction
// ----------------------------------------------------------------------------
// Identical to `pendulum.manic` but with `damping` (the 6th arg) turned on, so
// the swing loses energy. Watch every panel respond together:
// · sim — the amplitude shrinks each swing (slows toward rest)
// · phase — the closed loop becomes an INWARD SPIRAL
// · well — the ball SETTLES toward the bottom of the bowl
// · energygraph — the TOTAL (gold) line DECAYS (KE↔PE trade, sum drops)
//
// Set damping = 0 (or drop the 6th arg) for the frictionless version, where the
// loop stays closed and the total-energy line stays flat (conservation).
// ============================================================================
title("Damped pendulum — energy bleeds away");
canvas("16:9");
text(hdr, (cx, 40), "Add friction: the swing decays, and every view follows");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// the physical sim (left) — 6th arg 0.6 is the damping
pendulum(p, (250, 220), 1.2, 55, 105, 0.6);
untraced(p.path);
// the same four math views of the SAME (now damped) simulation
phase(p, (715, 165), 90); // loop → inward spiral
timegraph(p, (1000, 165), 90); // θ(t), ω(t) — envelope shrinks
well(p, (715, 455), 90); // ball settles to the bottom
energygraph(p, (1000, 455), 90); // total energy decays
text(cap, (cx, h - 30), "damping = 0.6 · the phase spiral, settling ball, and decaying total energy all show the same loss");
size(cap, 16); color(cap, dim); display(cap);
draw(p.path, 0.8);
swing(p, 10);
pendulum-annotated
A guided anatomy lesson proving physics composes with base manic: section chapters,
text / arrow / bracelabel annotations, and show/recolor/flash/pulse all
driving the sim’s parts — no special physics mode.
// ============================================================================
// pendulum-annotated.manic — a guided lesson: physics + base manic compose
// ----------------------------------------------------------------------------
// Proof that a sim's parts (`{id}.pivot/.rod/.bob/.path/…`) are ORDINARY manic
// entities. This whole lesson is built from BASE std vocabulary — `section`
// chapters, `text` labels, leader `arrow`s, a reference `line`, a `bracelabel`,
// and `show`/`fade`/`recolor`/`flash`/`pulse`/`draw`/`say` — wrapped around the
// physics `pendulum` + `energygraph` view + `swing`. No special "physics mode":
// every std verb/modifier/annotation addresses the physics entities directly.
// ============================================================================
title("Anatomy of a Pendulum");
canvas("16:9");
text(hdr, (cx, 44), "Anatomy of a Pendulum");
size(hdr, 30); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
// ---- the physics sim + one math view (parts start hidden; path untraced) ----
pendulum(p, (440, 210), 1.6, 50, 110);
hidden(p.pivot); hidden(p.rod); hidden(p.bob); hidden(p.overlays);
untraced(p.path); // trace 0 but keep opacity (reveal later)
energygraph(p, (1030, 250), 105); // built now (its sweep joins `swing`)
hidden(p.energy);
// ---- base annotations, all hidden to reveal in order ----
line(vref, (440, 210), (440, 386)); // vertical reference
color(vref, dim); stroke(vref, 2); hidden(vref);
bracelabel(Lb, (440, 210), (575, 323), "L = 1.6 m", 26); // rod length
color(Lb, gold); hidden(Lb); hidden(Lb.label);
text(pivL, (330, 200), "pivot (fixed)"); size(pivL, 18); color(pivL, dim); display(pivL); hidden(pivL);
arrow(pivA, (388, 205), (428, 209)); color(pivA, dim); stroke(pivA, 2); hidden(pivA);
text(bobL, (600, 355), "bob — mass m"); size(bobL, 18); color(bobL, magenta); display(bobL); hidden(bobL);
arrow(bobA, (628, 345), (585, 330)); color(bobA, dim); stroke(bobA, 2); hidden(bobA);
text(angL, (472, 300), "θ₀ = 50°"); size(angL, 18); color(angL, lime); display(angL); hidden(angL);
text(cap, (cx, h - 42), ""); size(cap, 20); color(cap, dim); display(cap);
// ============================== SCRIPT ==============================
show(hdr, 0.5);
section("Anatomy");
say(cap, "a fixed pivot …", 0.4);
show(p.pivot, 0.4); pulse(p.pivot); show(pivL, 0.4); draw(pivA, 0.3);
wait(0.5);
say(cap, "… a rigid rod of length L …", 0.4);
show(p.rod, 0.4); show(Lb, 0.4); show(Lb.label, 0.4);
wait(0.6);
say(cap, "… and a bob of mass m at the end", 0.4);
show(p.bob, 0.5); recolor(p.bob, magenta, 0.3); pulse(p.bob); show(bobL, 0.4); draw(bobA, 0.3);
wait(0.5);
say(cap, "all one group — a base broadcast flashes every part at once", 0.4);
flash(p.parts, lime); // std broadcast over the whole sim
wait(0.7);
section("Release");
say(cap, "held at θ₀ from the vertical, then let go", 0.4);
show(vref, 0.4); show(angL, 0.4); flash(p.rod, cyan);
wait(0.9);
// clear the static annotations before the motion
fade(Lb, 0.3); fade(Lb.label, 0.3); fade(pivA, 0.3); fade(bobA, 0.3);
fade(pivL, 0.3); fade(bobL, 0.3); fade(angL, 0.3); fade(vref, 0.3);
wait(0.3);
section("Swing");
say(cap, "gravity pulls it back — velocity arrow + energy bars ride along", 0.4);
show(p.overlays, 0.4); // reveal the velocity arrow + KE/PE bars
draw(p.path, 0.8); // trace the arc it will follow
section("Energy");
say(cap, "and the SAME swing, read as energy over time", 0.4);
show(p.energy, 0.5); // reveal the energy graph, then run it all
swing(p, 10); // every panel + annotation-free scene animates
spring
A mass on a spring (simple harmonic motion) drawn with a real stretching coil — the
same generic views on a different system; note the energy well is a parabola
(½kx²) rather than the pendulum’s cosine (spring + the views + run).
// ============================================================================
// spring.manic — the physics kit's SECOND sim (Layer 1)
// ----------------------------------------------------------------------------
// `spring(id, [center], [stiffness], [x0], [unit], [damping])` — a mass on a
// spring, PRE-SIMULATED with RK4 (deterministic). Only `id` is required. It's a
// different system from the pendulum, yet it inherits the SAME generic views for
// free — note the energy well here is a PARABOLA U(x)=½kx² (the pendulum's is a
// cosine). `run(id, [dur])` replays the motion (alias: `swing`).
//
// TO ADAPT: add damping (6th arg) — the phase ellipse spirals in, the ball
// settles to the bottom of the parabola, and total energy decays:
// `spring(s, (360,320), 10, 1.4, 110, 0.6)`.
// ============================================================================
title("Mass on a spring — four views");
canvas("16:9");
text(hdr, (cx, 44), "A different sim — same four views (well is a parabola)");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// the mass–spring on the left
spring(s, (330, 300), 10, 1.4, 110);
// the same generic views of the SAME simulation, 2×2 on the right
phase(s, (760, 175), 90); // x vs v — an ellipse
timegraph(s, (1010, 175), 90); // x(t), v(t) with a sweep line
well(s, (760, 460), 90); // parabolic energy well U(x)=½kx²
energygraph(s, (1010, 460), 90); // KE / PE / total over time
text(cap, (cx, h - 32), "one mass–spring, seen as motion · phase ellipse · time series · parabolic well · energy");
size(cap, 16); color(cap, dim); display(cap);
run(s, 10); // every panel animates in lockstep
spring-damped
The damped spring: the coil’s oscillation decays, the phase ellipse spirals in, the ball settles in the parabola, and total energy bleeds away.
// ============================================================================
// spring-damped.manic — the same four views, now with friction
// ----------------------------------------------------------------------------
// Identical to `spring.manic` but with `damping` (the 6th arg) turned on, so the
// oscillation loses energy. Watch every panel respond together:
// · sim — the coil's swing SHRINKS each cycle (settles to rest)
// · phase — the ellipse becomes an INWARD SPIRAL
// · well — the ball SETTLES to the bottom of the parabola U(x)=½kx²
// · energygraph — the TOTAL (gold) line DECAYS (KE↔PE trade, sum drops)
//
// Set damping = 0 (or drop the 6th arg) for the frictionless version, where the
// ellipse stays closed and the total-energy line stays flat (conservation).
// ============================================================================
title("Damped spring — energy bleeds away");
canvas("16:9");
text(hdr, (cx, 44), "Add friction: the oscillation decays, every view follows");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// the mass–spring (left) — 6th arg 0.6 is the damping
spring(s, (330, 300), 10, 1.4, 110, 0.6);
// the same generic views of the SAME (now damped) simulation
phase(s, (760, 175), 90); // ellipse → inward spiral
timegraph(s, (1010, 175), 90); // x(t), v(t) — envelope shrinks
well(s, (760, 460), 90); // ball settles to the bottom of the parabola
energygraph(s, (1010, 460), 90); // total energy decays
text(cap, (cx, h - 32), "damping = 0.6 · the spiral, settling ball, and decaying total energy all show the same loss");
size(cap, 16); color(cap, dim); display(cap);
run(s, 10);
spring-annotated
Elevating the spring with a TYPEWRITER lab-note (type + cursor) and LIVE COUNTERS
(counter + to(_, value, …)) ticking k and the period up — Hooke’s law → parabolic
well → SHM, with no stage-covering section cards. One of three elevation styles.
// ============================================================================
// spring-annotated.manic — ELEVATE a sim: typewriter narration + live data
// ----------------------------------------------------------------------------
// One elevation flavour of several (see pulley-annotated for camera work,
// brachistochrone-annotated for kinetic typography). Here the story is told by
// a TYPEWRITER lab-note (`type` + `cursor`) and LIVE COUNTERS (`counter` +
// `to(_, value, …)`) that tick up — no stage-covering section cards, so the
// motion is never hidden. The spring's parts are ordinary entities the whole
// language drives directly.
// ============================================================================
title("Anatomy of a Spring — Hooke's Law & SHM");
canvas("16:9");
text(hdr, (cx, 40), "Anatomy of a Spring"); size(hdr, 28); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
// ---- the sim + views (staged) ----
spring(sp, (340, 320), 10, 1.4, 105);
hidden(sp.wall); hidden(sp.spring); hidden(sp.mass); hidden(sp.overlays);
untraced(sp.path);
well(sp, (1015, 230), 112); hidden(sp.well);
energygraph(sp, (1015, 480), 112); hidden(sp.energy);
// ---- part annotations ----
line(eq, (340, 268), (340, 372)); color(eq, dim); stroke(eq, 2); untraced(eq);
text(coilL, (285, 232), "spring, stiffness k"); size(coilL, 16); color(coilL, lime); display(coilL); hidden(coilL);
text(massL, (520, 272), "mass m"); size(massL, 16); color(massL, cyan); display(massL); hidden(massL);
bracelabel(xb, (340, 372), (487, 372), "x₀", 22); color(xb, gold); hidden(xb); hidden(xb.label);
text(hooke, (340, 168), "F = −k·x"); size(hooke, 24); color(hooke, gold); display(hooke); hidden(hooke);
// ---- a TYPEWRITER lab-note with a live cursor (the narration device) ----
text(note, (cx, h - 44), ""); size(note, 21); color(note, fg); display(note); cursor(note);
text(kick, (96, 92), ""); size(kick, 17); color(kick, magenta); bold(kick); display(kick);
// ---- LIVE COUNTERS (tick up on reveal) ----
counter(kC, (150, 470), 0, 1, "k = ", " N/m"); size(kC, 26); color(kC, lime); display(kC); hidden(kC);
counter(tC, (150, 512), 0, 2, "T = ", " s"); size(tC, 26); color(tC, cyan); display(tC); hidden(tC);
// ============================== SCRIPT ==============================
show(hdr, 0.5);
say(kick, "SETUP", 0.2);
say(note, "a wall, a coil of stiffness k, and a mass", 0.1); type(note, 1.4);
show(sp.wall, 0.4); pulse(sp.wall); show(coilL, 0.4);
show(sp.spring, 0.4); flash(sp.spring, lime); show(sp.mass, 0.4); pulse(sp.mass); show(massL, 0.4);
show(kC, 0.3); to(kC, value, 10, 0.8);
wait(0.5);
say(kick, "HOOKE'S LAW", 0.2);
say(note, "pull it x₀ from rest — it pulls back, F = −k·x", 0.1); type(note, 1.7);
draw(eq, 0.4); show(xb, 0.4); show(xb.label, 0.4); show(hooke, 0.4); flash(sp.spring, gold);
show(tC, 0.3); to(tC, value, 1.99, 0.9); // period 2π√(m/k)
wait(0.6);
fade(coilL, 0.3); fade(massL, 0.3); fade(xb, 0.3); fade(xb.label, 0.3); fade(hooke, 0.3);
wait(0.2);
say(kick, "MOTION", 0.2);
say(note, "release — simple harmonic motion; every panel tells the same swing", 0.1); type(note, 1.9);
show(sp.well, 0.5); show(sp.overlays, 0.4); show(sp.energy, 0.5); draw(sp.path, 0.6);
run(sp, 10);
spring-paper
The SAME spring sim dressed as a textbook figure AND run: template("paper") inks it,
a hatched support wall, a forest-green coil and outlined mass box, Hooke’s law and x₀
revealed, then run plays the SHM — the paper treatment on a LIVE sim (see pulley-paper).
// ============================================================================
// spring-paper.manic — an ANIMATED sim in textbook paper style
// ----------------------------------------------------------------------------
// The `spring` sim (mass on a spring, SHM), dressed as a textbook figure AND run:
// `template("paper")` inks it automatically, a hatched `support` wall, the coil
// in forest green, an outlined mass box, plus a base-manic reveal (Hooke's law,
// equilibrium, x₀) — then `run` plays the oscillation. The companion to
// pulley-paper: the paper/support treatment on another live sim.
// ============================================================================
title("Mass on a spring — textbook style");
canvas("16:9");
template("paper");
text(hdr, (cx, 46), "Mass on a spring — Hooke's law"); color(hdr, fg); size(hdr, 26); bold(hdr); display(hdr); hidden(hdr);
// the sim, restyled to textbook ink (parts hidden, revealed in order)
spring(sp, (380, 300), 10, 1.3, 110);
outlined(sp.mass); outline(sp.mass, fg);
hidden(sp.wall); hidden(sp.spring); hidden(sp.mass); hidden(sp.overlays);
untraced(sp.path);
// a hatched wall stands in for the sim's plain anchor; equilibrium reference
support(wall, (177, 300), 150, "right"); untraced(wall);
line(eq, (380, 246), (380, 354)); color(eq, dim); stroke(eq, 2); untraced(eq);
text(eqL, (380, 232), "equilibrium"); color(eqL, dim); size(eqL, 15); display(eqL); hidden(eqL);
text(coilL, (300, 214), "spring, stiffness k"); color(coilL, fg); size(coilL, 17); display(coilL); hidden(coilL);
text(massL, (548, 258), "mass m"); color(massL, fg); size(massL, 17); display(massL); hidden(massL);
bracelabel(xb, (380, 360), (523, 360), "x₀", 22); color(xb, fg); hidden(xb); hidden(xb.label);
text(hooke, (380, 152), "F = −k·x"); color(hooke, fg); size(hooke, 24); display(hooke); hidden(hooke);
text(cap, (cx, h - 40), ""); color(cap, fg); size(cap, 20); display(cap);
// ============================== SCRIPT ==============================
show(hdr, 0.5);
say(cap, "a coil of stiffness k fixed to a wall, with a mass on the end", 0.4);
draw(wall, 0.4); show(sp.spring, 0.4); show(coilL, 0.4); show(sp.mass, 0.4); show(massL, 0.3);
wait(0.4);
say(cap, "pull it x₀ from equilibrium — it pulls straight back, F = −k·x", 0.4);
draw(eq, 0.4); show(eqL, 0.3); show(xb, 0.4); show(xb.label, 0.4); show(hooke, 0.5);
wait(0.7);
fade(coilL, 0.3); fade(massL, 0.3); fade(xb, 0.3); fade(xb.label, 0.3); fade(hooke, 0.3);
say(cap, "release — simple harmonic motion", 0.4);
run(sp, 9);
double-pendulum
Deterministic chaos: two arms hinged end-to-end whose outer bob traces a wild,
unrepeatable curve — yet the render is frame-identical every run. A 4-D system, so
it shows phase (θ₁ vs θ₂) and energygraph but has no potential well
(doublependulum + views + run).
// ============================================================================
// double-pendulum.manic — chaos, on the physics baseline (Layer 1)
// ----------------------------------------------------------------------------
// `doublependulum(id, [center], [angle1], [angle2], [unit])` — two arms hinged
// end-to-end: deterministic, yet exquisitely sensitive to initial conditions.
// PRE-SIMULATED with RK4 (so the render is frame-identical every run). It's a
// 4-D system, so it inherits `phase` (θ₁ vs θ₂), `timegraph`, and `energygraph`
// — but NOT `well` (there's no single-variable potential). `run(id,[dur])` plays
// it; drawing `{id}.path` in parallel traces the outer bob's chaotic curve.
//
// TO ADAPT: nudge angle2 by one degree and re-run — the trail diverges completely.
// ============================================================================
title("Double pendulum — deterministic chaos");
canvas("16:9");
text(hdr, (cx, 44), "Double pendulum — deterministic, yet unpredictable");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// the chaotic sim on the left (outer bob traces the wild curve)
doublependulum(dp, (400, 250), 125, 110);
untraced(dp.path);
// the views that DO apply to a 4-D system (no potential well here)
phase(dp, (900, 190), 100); // θ₁ vs θ₂ — the coupled angles
energygraph(dp, (900, 480), 100); // KE / PE / total (total ~conserved)
text(cap, (cx, h - 32), "outer bob's trail is chaotic; total energy stays (nearly) conserved — no friction");
size(cap, 16); color(cap, dim); display(cap);
// trace the chaotic trail AS the pendulum swings (draw + run in parallel)
par {
run(dp, 12);
draw(dp.path, 12);
}
spring-pendulum
An elastic pendulum — a bob on a springy rod (drawn as a stretching coil) that both
swings and bounces, energy sloshing between the two modes (springpendulum).
// spring-pendulum.manic — the elastic pendulum: swings AND bounces.
// springpendulum(id,[center],[angle0],[stretch0],[unit],[damping]) — a bob on a
// springy rod (drawn as a stretching coil). Energy sloshes between the swing and
// the bounce, so the phase portrait and energy graph are richer than a rigid rod.
title("Elastic pendulum — swing meets bounce");
canvas("16:9");
text(hdr,(cx,44),"Spring pendulum — energy sloshes between swing and bounce");
size(hdr,23); color(hdr,cyan); bold(hdr); display(hdr);
springpendulum(sp, (380,230), 35, 0.5, 105);
untraced(sp.path);
phase(sp, (900,190), 95); // θ vs ω
energygraph(sp, (900,470), 95); // KE / PE / total (~conserved, lightly damped)
text(cap,(cx,h-32),"the coil stretches as it swings — a two-mode system"); size(cap,16); color(cap,dim); display(cap);
par { run(sp, 11); draw(sp.path, 11); }
kapitza
The Kapitza pendulum: vibrate the pivot fast enough and the inverted position
becomes stable — the bob hovers near the top instead of falling (kapitza).
// kapitza.manic — vibrate the pivot fast enough and the pendulum stands UP.
// kapitza(id,[center],[angle0deg],[vibeamp],[unit]) — a driven (Kapitza) pendulum;
// with a strong enough vibration the INVERTED position becomes stable.
title("Kapitza pendulum — stable upside-down");
canvas("16:9");
text(hdr,(cx,44),"Kapitza pendulum — fast pivot vibration stabilises 'up'");
size(hdr,23); color(hdr,cyan); bold(hdr); display(hdr);
kapitza(kp, (640,430), 165, 240, 150); // start near inverted, strong vibration
untraced(kp.path);
text(cap,(cx,h-32),"the gold pivot bobs fast; the bob hovers near the top (inverted)"); size(cap,16); color(cap,dim); display(cap);
par { run(kp, 9); draw(kp.path, 9); }
cart-pendulum
A pendulum on a spring-mounted cart rolling on a track — the classic control-theory
system; cart and bob trade momentum and energy (cartpendulum).
// cart-pendulum.manic — a pendulum on a spring-mounted cart (the control classic).
// cartpendulum(id,[center],[angle0deg],[unit]) — cart rolls on a track (spring to a
// wall) while the pendulum swings; the two exchange momentum and energy.
title("Cart-pendulum — coupled motion");
canvas("16:9");
text(hdr,(cx,44),"Cart-pendulum — the cart and bob trade momentum"); size(hdr,23); color(hdr,cyan); bold(hdr); display(hdr);
cartpendulum(cp, (480,330), 55, 105);
phase(cp, (1000,190), 95); // θ vs ω
energygraph(cp, (1000,470), 95); // KE / PE / total (~conserved)
text(cap,(cx,h-32),"cart on a spring + swinging bob — energy stays (nearly) conserved"); size(cap,16); color(cap,dim); display(cap);
run(cp, 11);
compare-pendulum
Sensitive dependence: two identical driven pendulums started 0.001 rad apart drift
onto completely different paths — the butterfly effect, watched in phase/timegraph
(comparependulum).
// compare-pendulum.manic — sensitive dependence: two pendulums 0.001 rad apart.
// comparependulum(id,[center],[angle0deg],[unit]) — identical driven-damped
// physics, a hair-different start — yet they diverge completely (the butterfly effect).
title("Two pendulums, one hair apart");
canvas("16:9");
text(hdr,(cx,44),"Sensitive dependence — a 0.001 rad difference explodes"); size(hdr,23); color(hdr,cyan); bold(hdr); display(hdr);
comparependulum(cm, (380,230), 12, 120);
phase(cm, (900,190), 95); // θ_A vs θ_B — starts on the diagonal, then scatters
timegraph(cm, (900,470), 95); // θ_A(t) & θ_B(t) diverge
text(cap,(cx,h-32),"cyan and magenta start together, then go their separate ways"); size(cap,16); color(cap,dim); display(cap);
run(cm, 13);
vertical-spring
A mass bobbing on a vertical spring under gravity — gravity shifts the equilibrium
but the energy well stays a parabola (verticalspring).
// vertical-spring.manic — a mass bobbing on a vertical spring under gravity.
title("Vertical spring — bobbing under gravity");
canvas("16:9");
text(hdr,(cx,44),"Vertical spring — a mass bobs about its stretched equilibrium"); size(hdr,22); color(hdr,cyan); bold(hdr); display(hdr);
verticalspring(vs, (400,170), 0.7);
untraced(vs.path);
phase(vs, (900,190), 95); // d vs ḋ
well(vs, (900,470), 95); // parabolic well (shifted by gravity)
text(cap,(cx,h-32),"gravity shifts the equilibrium down; the well is still a parabola"); size(cap,16); color(cap,dim); display(cap);
par { run(vs, 10); draw(vs.path, 10); }
spring-incline
A mass on a spring on an inclined plane; gravity’s along-ramp component sets a new
stretched rest point it oscillates about (springincline).
// spring-incline.manic — a mass on a spring on an inclined plane.
title("Spring on an incline");
canvas("16:9");
text(hdr,(cx,44),"Spring on an incline — gravity's along-ramp pull shifts equilibrium"); size(hdr,22); color(hdr,cyan); bold(hdr); display(hdr);
springincline(si, (360,180), 32);
untraced(si.path);
phase(si, (960,200), 100);
energygraph(si, (960,470), 100);
text(cap,(cx,h-32),"the bob oscillates along the ramp about its stretched rest point"); size(cap,16); color(cap,dim); display(cap);
par { run(si, 10); draw(si.path, 10); }
bungee
A bungee jump: free-fall, then a ONE-SIDED elastic cord (it only pulls) catches and
bounces the jumper — note the lopsided energy well (bungee).
// bungee.manic — free-fall, then a one-sided elastic cord catches the jumper.
title("Bungee jump — free-fall, then bounce");
canvas("16:9");
text(hdr,(cx,44),"Bungee — the cord only PULLS: free-fall, then an elastic bounce"); size(hdr,22); color(hdr,cyan); bold(hdr); display(hdr);
bungee(bg, (640,110));
phase(bg, (1000,200), 95); // asymmetric well shows in the phase loop
well(bg, (1000,470), 95); // linear (fall) → parabola (cord): a lopsided well
text(cap,(cx,h-32),"note the lopsided energy well — flat during free-fall, steep once the cord bites"); size(cap,15); color(cap,dim); display(cap);
run(bg, 11);
resonance
A driven spring pushed near its natural frequency √(k/m): the amplitude climbs and
climbs — resonance, watched building up in phase/energygraph (resonance).
// resonance.manic — a driven spring pumped near its natural frequency.
title("Resonance — driving near the natural frequency");
canvas("16:9");
text(hdr,(cx,44),"Resonance — drive near √(k/m) and the amplitude grows and grows"); size(hdr,22); color(hdr,cyan); bold(hdr); display(hdr);
resonance(rs, (400,300), 3.8); // natural freq = √(16/1) = 4; drive 3.8 ≈ resonance
untraced(rs.path);
phase(rs, (960,200), 100); // spiral OUT to a big steady-state loop
energygraph(rs, (960,470), 100); // energy climbs, then plateaus
text(cap,(cx,h-32),"drive frequency 3.8 vs natural 4.0 — near resonance, so it builds up big"); size(cap,15); color(cap,dim); display(cap);
par { run(rs, 12); draw(rs.path, 12); }
double-spring
Two masses coupled by springs between walls — push one and the energy sloshes back
and forth (beating); normal modes show as diagonals in phase (doublespring).
// double-spring.manic — two masses coupled by springs: energy sloshes (beating).
title("Coupled springs — energy sloshes back and forth");
canvas("16:9");
text(hdr,(cx,44),"Two coupled masses — push one, and the energy beats between them"); size(hdr,22); color(hdr,cyan); bold(hdr); display(hdr);
doublespring(dd, (430,300), 85);
phase(dd, (980,200), 100); // x1 vs x2 — normal modes are diagonals
energygraph(dd, (980,470), 100);
text(cap,(cx,h-32),"block 1 (cyan) starts displaced; watch block 2 (magenta) pick up its swing"); size(cap,15); color(cap,dim); display(cap);
run(dd, 12);
series-parallel-springs
The same mass on springs in series (soft, slow) vs parallel (stiff, fast), side by
side — the timegraph makes the frequency difference obvious (seriesparallel).
// series-parallel-springs.manic — same mass, springs in series vs parallel.
title("Series vs parallel springs");
canvas("16:9");
text(hdr,(cx,44),"Same mass, same springs — series is soft (slow), parallel is stiff (fast)"); size(hdr,21); color(hdr,cyan); bold(hdr); display(hdr);
seriesparallel(sp, (560,150), 68);
timegraph(sp, (1060,320), 95); // y_s(t) vs y_p(t): parallel oscillates faster
text(cap,(cx,h-30),"parallel adds stiffness (k₁+k₂); series divides it (1/k = 1/k₁+1/k₂)"); size(cap,15); color(cap,dim); display(cap);
run(sp, 11);
car-suspension
A quarter-car riding a scrolling road — a speed bump, a washboard stretch, and a
pothole — its spring+damper soaking up the ride (carsuspension).
// car-suspension.manic — a quarter-car riding a road: bump, washboard, pothole.
title("Car suspension — riding the road");
canvas("16:9");
text(hdr,(cx,44),"Quarter-car suspension — the body bobs as the wheel rides the road"); size(hdr,22); color(hdr,cyan); bold(hdr); display(hdr);
carsuspension(car, (640,430));
energygraph(car, (1050,170), 100);
text(cap,(cx,h-30),"a speed bump, a washboard stretch, then a pothole — the spring+damper soak it up"); size(cap,15); color(cap,dim); display(cap);
run(car, 12);
car-suspension-annotated
A marketing hero: the quarter-car suspension on a template("paper") brochure page,
elevated with generic base-manic — a live counter (sprung mass), leader-arrow
callouts, and an energygraph of the shock being absorbed — riding a scrolling road.
// ============================================================================
// car-suspension-annotated.manic — a marketing hero, paper (brochure) style
// ----------------------------------------------------------------------------
// The `carsuspension` quarter-car sim, dressed as a clean product diagram on a
// `template("paper")` page and ELEVATED with generic base-manic: a live
// `counter` (the sprung mass ticking up), leader-`arrow` callouts, a
// `bracelabel` for the travel, and a compact `energygraph` showing the shock
// being absorbed — then it rides a scrolling road (bump · washboard · pothole).
// Every part of the sim is an ordinary entity, so it all composes.
// ============================================================================
title("Car suspension — smooth over every bump");
canvas("16:9");
template("paper");
// ---- hero title ----
text(hdr, (cx, 52), "Car suspension: how it soaks up the road"); color(hdr, fg); size(hdr, 30); bold(hdr); display(hdr); hidden(hdr);
text(sub, (cx, 88), "the quarter-car model — sprung mass · spring + damper · wheel"); color(sub, dim); size(sub, 18); display(sub); hidden(sub);
// ---- the sim, recoloured for a bold, clean brochure look ----
carsuspension(car, (430, 430));
color(car.body, blue); // the car body — bold accent
color(car.road, fg); // the road — dark ink
hidden(car); // reveal the whole sim (road, wheel, spring, body, energy) together
// a compact energy panel (top-right) — the suspension absorbing the shock
energygraph(car, (1055, 215), 88);
// ---- generic-kit callouts (leader arrows + labels), hidden to reveal in order ----
text(cb, (640, 296), "car body — the sprung mass"); color(cb, blue); size(cb, 18); display(cb); hidden(cb);
arrow(ab, (636, 302), (478, 320)); color(ab, dim); stroke(ab, 2); hidden(ab);
text(csp, (610, 382), "spring + damper"); color(csp, lime); size(csp, 18); display(csp); hidden(csp);
arrow(asp, (606, 384), (448, 382)); color(asp, dim); stroke(asp, 2); hidden(asp);
text(cw, (620, 472), "wheel — the unsprung mass"); color(cw, gold); size(cw, 18); display(cw); hidden(cw);
arrow(aw, (616, 470), (448, 434)); color(aw, dim); stroke(aw, 2); hidden(aw);
text(cr, (cx, 648), "the road: a bump, a washboard stretch, then a pothole"); color(cr, fg); size(cr, 17); display(cr); hidden(cr);
// a live spec counter (generic kit) — the sprung mass ticking up
counter(mc, (150, 320), 0, 0, "m = ", " kg"); color(mc, fg); size(mc, 30); display(mc); hidden(mc);
text(spec, (cx, h - 28), "quarter-car · k = 20 kN/m · c = 4 kN·s/m · v = 8 m/s"); color(spec, dim); size(spec, 16); display(spec); hidden(spec);
// ============================== SCRIPT ==============================
show(hdr, 0.5); show(sub, 0.4);
wait(0.3);
show(car, 0.6);
wait(0.3);
show(cb, 0.3); draw(ab, 0.3);
show(csp, 0.3); draw(asp, 0.3);
show(cw, 0.3); draw(aw, 0.3);
show(cr, 0.3);
show(mc, 0.3); to(mc, value, 500, 0.9); // the sprung mass counts up
show(spec, 0.4);
wait(0.5);
// the ride — the road scrolls, the body glides while the wheel tracks every bump
run(car, 11);
piston
An engine piston: a spinning crank + connecting rod turn rotation into the piston’s
up-and-down stroke — the slider-crank mechanism (piston).
// piston.manic — an engine piston: a spinning crank drives a slider in a cylinder.
title("Engine piston — the slider-crank");
canvas("16:9");
text(hdr,(cx,46),"Slider-crank — a spinning crank becomes up-and-down motion"); size(hdr,23); color(hdr,cyan); bold(hdr); display(hdr);
piston(eng, (cx, 480), 60);
text(cap,(cx,h-32),"gold crank sweeps the circle; the rod converts it to the piston's stroke"); size(cap,16); color(cap,dim); display(cap);
run(eng, 9);
molecule
A molecule as balls and springs — atoms bonded on every side, vibrating about their
equilibrium shape with the total energy conserved (molecule).
// molecule.manic — atoms bonded by springs, vibrating about their shape.
title("Vibrating molecule");
canvas("16:9");
text(hdr,(cx,46),"A molecule as balls and springs — bonds stretch and the atoms jiggle"); size(hdr,22); color(hdr,cyan); bold(hdr); display(hdr);
molecule(mol, (520, 300), 3);
energygraph(mol, (1000, 300), 110); // total energy is conserved as it vibrates
text(cap,(cx,h-32),"one atom starts pulled out; the bonds pull it back and the whole thing rings"); size(cap,15); color(cap,dim); display(cap);
run(mol, 11);
robot-arm
A two-link robot arm reaching for a target: the joint rates come from the analytic
inverse Jacobian, so the arm drives its end-effector to the goal and settles there —
inverse kinematics as a solved motion (robotarm).
// ============================================================================
// robot-arm.manic — inverse kinematics, on the physics baseline (Layer 1)
// ----------------------------------------------------------------------------
// `robotarm(id, [center], [mode], [unit])` — a two-link arm that tracks a target
// by inverse kinematics. Its joint rates are the analytic 2×2 inverse Jacobian
// times a gain on the end-effector error, so the gripper chases the target.
// `mode` 1 = trace a circle (default), 2 = figure-8, 0 = reach a fixed point and
// settle. PRE-SIMULATED with RK4; `run(id,[dur])` replays it, and `{id}.path`
// traces the route the gripper sweeps.
//
// TO ADAPT: set mode 2 for a figure-8, or mode 0 to reach one fixed point.
// ============================================================================
title("Robot arm — tracking by inverse kinematics");
canvas("16:9");
text(hdr, (cx, 46), "Two-link arm — inverse kinematics keeps the gripper on the moving target");
size(hdr, 22); color(hdr, cyan); bold(hdr); display(hdr);
robotarm(rb, (cx, 470), 1);
text(cap, (cx, h - 32), "gold shoulder + cyan forearm chase the lime ring around its circle — the gripper traces the path");
size(cap, 15); color(cap, dim); display(cap);
run(rb, 10);
pulley
The Atwood machine: two masses over one pulley, the heavier one accelerating down at
(m₁−m₂)g/(m₁+m₂). energygraph shows kinetic energy climbing as potential falls
(pulley).
// ============================================================================
// pulley.manic — the Atwood machine, on the physics baseline (Layer 1)
// ----------------------------------------------------------------------------
// `pulley(id, [center], [m1], [m2], [unit])` — two masses over one pulley. The
// heavier one accelerates down at a = (m₁−m₂)g/(m₁+m₂). PRE-SIMULATED with RK4;
// `run(id,[dur])` replays it. `energygraph` shows the KE↔PE trade as the system
// speeds up (total is conserved — no friction).
//
// TO ADAPT: set m1 and m2 closer together for a gentler acceleration.
// ============================================================================
title("Atwood machine — unequal masses accelerate");
canvas("16:9");
text(hdr, (cx, 44), "Atwood machine — a = (m₁−m₂)g / (m₁+m₂)");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
pulley(pl, (440, 170), 3, 2);
energygraph(pl, (960, 330), 120);
text(cap, (cx, h - 32), "cyan m₁ (heavier) sinks, magenta m₂ rises; kinetic energy climbs as potential falls");
size(cap, 16); color(cap, dim); display(cap);
run(pl, 5);
pulley-scale
The surprise every physics class remembers: an in-line spring scale on an Atwood
machine reads the rope TENSION 2·m₁·m₂·g/(m₁+m₂) — not the sum of the two weights
(pulleyscale).
// ============================================================================
// pulley-scale.manic — what does the scale read? (physics Layer 1)
// ----------------------------------------------------------------------------
// `pulleyscale(id, [center], [m1], [m2], [unit])` — an Atwood machine over two
// pulleys with a spring scale in the rope between them. The classic surprise:
// the scale reads the rope TENSION 2·m₁·m₂·g/(m₁+m₂), NOT the sum of the two
// weights. PRE-SIMULATED with RK4; `run(id,[dur])` replays it.
//
// TO ADAPT: set m1 = m2 — the system balances and the scale reads exactly m·g.
// ============================================================================
title("Pulley scale — it reads the tension, not the weight");
canvas("16:9");
text(hdr, (cx, 46), "The scale reads the rope tension T = 2·m₁·m₂·g / (m₁+m₂)");
size(hdr, 22); color(hdr, cyan); bold(hdr); display(hdr);
pulleyscale(ps, (cx, 200), 4, 3);
text(cap, (cx, h - 32), "not (m₁+m₂)g, not the heavier weight — the tension sits between the two weights");
size(cap, 16); color(cap, dim); display(cap);
run(ps, 5);
block-tackle
A compound pulley (block & tackle): a load on a movable block held by N rope strands,
pulled by an effort mass. N strands = a mechanical advantage of N — an effort of only
load/N balances the load, but the effort end travels N× as far (blocktackle).
// ============================================================================
// block-tackle.manic — the compound pulley (mechanical advantage), Layer 1
// ----------------------------------------------------------------------------
// `blocktackle(id, [center], [load], [effort], [strands], [unit])` — a load on a
// movable block held by N rope strands and pulled by an effort mass. The N
// strands give a MECHANICAL ADVANTAGE of N: an effort of only load/N balances
// the load, and the effort end travels N× as far as the load rises. PRE-SIMULATED
// with RK4; `run(id,[dur])` replays it. N = 1 is just the Atwood machine.
//
// TO ADAPT: change `strands` (1–4) — more strands lift the same load with less
// effort, but the effort mass has to travel that much further.
// ============================================================================
title("Block & Tackle — a compound pulley's mechanical advantage");
canvas("16:9");
text(hdr, (cx, 40), "Block & tackle — 3 strands lift 8 kg with 3 kg of effort");
size(hdr, 22); color(hdr, cyan); bold(hdr); display(hdr);
blocktackle(bt, (500, 130), 8, 3, 3);
energygraph(bt, (1030, 330), 120);
text(cap, (cx, h - 32), "3 strands ⇒ MA = 3: effort just over load/3 lifts it; the effort end travels 3× as far");
size(cap, 15); color(cap, dim); display(cap);
run(bt, 5);
compound-pulley
A compound pulley with a MOVABLE pulley: a fixed top pulley carries mass A on one side
and a movable lower pulley on the other; that pulley carries B and C. The string
constraints link them (a_A = −a_P, a_B + a_C = 2·a_P); static when mA = mB+mC
(compoundpulley).
// ============================================================================
// compound-pulley.manic — fixed + movable pulley, three masses (physics L1)
// ----------------------------------------------------------------------------
// `compoundpulley(id, [center], [mA], [mB], [mC], [unit])` — a fixed top pulley
// carries mass A on one side and a MOVABLE lower pulley on the other; the movable
// pulley carries masses B and C. The string constraints link them: a_A = −a_P and
// a_B + a_C = 2·a_P (the massless movable pulley gives T₁ = 2·T₂). Static exactly
// when mA = mB + mC. PRE-SIMULATED with RK4; `run(id,[dur])` replays it.
//
// TO ADAPT: set mA = mB + mC and it balances; make A heavier and it hauls B, C up.
// ============================================================================
title("Compound pulley — fixed + movable, masses A, B, C");
canvas("16:9");
text(hdr, (cx, 40), "Compound pulley — heavier A hauls B and C upward");
size(hdr, 23); color(hdr, cyan); bold(hdr); display(hdr);
compoundpulley(cp, (470, 120), 5, 2, 2);
energygraph(cp, (1010, 330), 120);
text(cap, (cx, h - 32), "A (5 kg) > B+C (4 kg): A sinks, the movable pulley rises, B and C are pulled up together");
size(cap, 15); color(cap, dim); display(cap);
run(cp, 4);
incline-pulley
The incline-Atwood: a block on an incline tied over a pulley at the top to a hanging
mass. m₂ outpulls m₁·sinθ, so the block climbs while the mass descends — energygraph
tracks the KE↔PE trade (inclinepulley).
// incline-pulley.manic — the incline-Atwood: a block on an incline tied over a
// pulley at the top to a hanging mass. `inclinepulley(id,[center],[angle],[m1],
// [m2],[unit])`. PRE-SIMULATED (RK4); `run(id)` plays it; `energygraph` shows the
// KE↔PE trade. Matches the classic textbook figure.
title("Incline + pulley — the incline-Atwood");
canvas("16:9");
text(hdr, (cx, 44), "Block on an incline, tied over a pulley to a hanging mass"); size(hdr, 22); color(hdr, cyan); bold(hdr); display(hdr);
inclinepulley(ip, (280, 500), 30, 3, 2);
energygraph(ip, (1010, 320), 120);
text(cap, (cx, h - 32), "m₂ (2 kg) hanging outpulls m₁·sinθ, so m₁ climbs the incline and m₂ descends"); size(cap, 15); color(cap, dim); display(cap);
run(ip, 4);
double-incline
Two blocks on a wedge’s two slopes, tied over a pulley at the apex (right slope rough).
The 70 kg block on the gentle 30° slope beats the 12 kg block on the steep 50° smooth
slope — connected motion on two inclines (doubleincline).
// double-incline.manic — two blocks on a wedge's two slopes, tied over a pulley
// at the apex. `doubleincline(id,[center],[angle1],[angle2],[m1],[m2],[unit])`;
// the right slope is rough. PRE-SIMULATED (RK4); `run(id)` plays it.
title("Double incline — connected blocks on two slopes");
canvas("16:9");
text(hdr, (cx, 42), "Two slopes, one rope over the apex — which way does it slide?"); size(hdr, 22); color(hdr, cyan); bold(hdr); display(hdr);
doubleincline(di, (cx, 520), 50, 30, 12, 70);
text(cap, (cx, h - 30), "M₂ (70 kg) on the gentle rough slope beats M₁ (12 kg) on the steep smooth one"); size(cap, 15); color(cap, dim); display(cap);
run(di, 5);
incline-bumper
A block slides down an incline into a spring bumper at the base, compresses it, and
launches back up — one-sided contact, gravity PE ↔ kinetic ↔ spring PE, energy
conserved (inclinebumper).
// incline-bumper.manic — a block slides down an incline into a spring bumper at
// the base, compresses it, and launches back up (one-sided contact).
// `inclinebumper(id,[center],[angle],[mass],[stiffness],[unit])`. PRE-SIMULATED
// (RK4); `energygraph` shows gravity PE ↔ kinetic ↔ spring PE (conserved).
title("Incline + spring bumper — slide, compress, launch");
canvas("16:9");
text(hdr, (cx, 44), "A block slides down and compresses a spring at the base, then launches back"); size(hdr, 21); color(hdr, cyan); bold(hdr); display(hdr);
inclinebumper(ib, (300, 500), 40, 2, 500);
energygraph(ib, (1010, 320), 120);
text(cap, (cx, h - 32), "gravity PE → kinetic → spring PE → back: one-sided contact, energy conserved"); size(cap, 15); color(cap, dim); display(cap);
run(ib, 6);
collide-blocks
The classic momentum demo: block 1 hangs on a spring to the wall, block 2 slides in and
they collide. A live Σp readout shows momentum conserved at every collision; elastic
(e=1) keeps total energy flat while it sloshs between KE and the spring (collideblocks).
// collide-blocks.manic — the classic momentum demo. Block 1 (left) is attached to
// the wall by a SPRING; block 2 slides in freely and they collide with restitution
// e (1 = elastic → energy conserved; <1 → lost). The live Σp readout (top) shows
// momentum is conserved at every collision. Built on the shared `collide_1d`.
title("Colliding blocks — momentum is conserved");
canvas("16:9");
text(hdr, (cx, 96), "Block 1 on a spring, block 2 sliding in — watch the momentum readout"); size(hdr, 21); color(hdr, cyan); bold(hdr); display(hdr);
collideblocks(cb, (cx, 440), 3, 1, 1);
energygraph(cb, (1040, 210), 90);
text(cap, (cx, h - 36), "elastic (e=1): total energy (KE + spring PE) is conserved; Σp is conserved at each collision"); size(cap, 15); color(cap, dim); display(cap);
run(cb, 10);
collide-blocks-annotated
Conservation of momentum, the MANIC way — not a 1:1 port of the lab sim but a guided lesson: the live Σp readout as the star, the KE↔spring-PE energy view, staged callouts, and honest narration (with a wall-spring, Σp is conserved AT each collision, not constant).
// ============================================================================
// collide-blocks-annotated.manic — conservation of momentum, the MANIC way
// ----------------------------------------------------------------------------
// Not a 1:1 port of the lab sim — a guided lesson that ELEVATES it: the live Σp
// readout, the KE↔spring-PE energy view, staged callouts, and honest narration
// (with a wall-spring, Σp is conserved AT each collision — Newton's 3rd law — not
// constant, since the spring is an external force). All base manic over the sim.
// ============================================================================
title("Conservation of Momentum");
canvas("16:9");
// the sim's Σp readout sits at the very top (y≈46) — the star of the scene
collideblocks(cb, (cx, 460), 3, 1, 1);
hidden(cb); // the bare id hides every part + the readout + energy view
energygraph(cb, (1055, 250), 84); // KE ↔ spring PE (tagged cb, revealed by show(cb))
text(hdr, (cx, 104), "Conservation of Momentum"); size(hdr, 30); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
text(sub, (cx, 140), "block 1 on a spring · block 2 slides in · watch Σp through the collision"); size(sub, 17); color(sub, dim); display(sub); hidden(sub);
// callouts
text(csp, (360, 330), "spring holds block 1 to the wall"); size(csp, 16); color(csp, lime); display(csp); hidden(csp);
arrow(asp, (392, 344), (418, 424)); color(asp, dim); stroke(asp, 2); hidden(asp);
text(cb1, (470, 300), "block 1 · m₁ = 3 kg"); size(cb1, 16); color(cb1, cyan); display(cb1); hidden(cb1);
text(cb2, (800, 300), "block 2 · m₂ = 1 kg →"); size(cb2, 16); color(cb2, magenta); display(cb2); hidden(cb2);
text(cap, (cx, h - 38), ""); size(cap, 19); color(cap, fg); display(cap);
// ============================== SCRIPT ==============================
show(hdr, 0.5); show(sub, 0.4);
wait(0.3);
show(cb, 0.6); // sim + Σp readout + energy view
show(csp, 0.3); draw(asp, 0.3); show(cb1, 0.3); show(cb2, 0.3);
wait(0.4);
say(cap, "block 2 slides toward block 1, which is held by its spring", 0.4);
wait(0.5);
say(cap, "they collide — each pushes the other equal and opposite (Newton's 3rd law)", 0.4);
flash(cb.mom, gold);
wait(0.5);
say(cap, "so Σp doesn't jump at the hit — the spring only shifts it BETWEEN collisions", 0.4);
wait(0.5);
say(cap, "energy sloshes between motion and the spring, but the total stays put — elastic", 0.4);
run(cb, 12);
bullet-block
A bullet fired into a block EMBEDS (perfectly inelastic). The flight is slow-mo so you can
watch it cross, then a live speed readout collapses from 40 m/s to ~1 — momentum survives,
energy does not. Uses collide_1d(e=0) (bulletblock).
// bullet-block.manic — a bullet fired into a block EMBEDS (perfectly inelastic).
// `bulletblock(id,[center],[bulletmass],[speed],[blockmass],[unit])`. The combined
// mass crawls off at m_b·v_b/(m_b+M) — most of the kinetic energy is lost to the
// collision, so `energygraph`'s total STEPS DOWN at impact. Uses `collide_1d(e=0)`.
title("Bullet into a block — an inelastic collision");
canvas("16:9");
text(hdr, (cx, 70), "The bullet embeds: 40 m/s becomes ~1 m/s — most of the energy is gone"); size(hdr, 21); color(hdr, cyan); bold(hdr); display(hdr);
bulletblock(bb, (cx, 430), 0.05, 40, 1.95);
energygraph(bb, (1040, 200), 90);
text(cap, (cx, h - 36), "momentum is conserved, but kinetic energy is NOT — it drops sharply at impact"); size(cap, 15); color(cap, dim); display(cap);
run(bb, 6);
bullet-impact
BEST OF BOTH: the cinematic gun-shot (gun · muzzle flash · a flying cam/zoom · BOOM)
wrapped around the REAL bulletblock physics — the collision is genuinely inelastic, the
live speed readout actually collapses 40 → ~1, and the BOOM is synced to the true impact.
// ============================================================================
// bullet-impact.manic — best of both: the movie AND the physics
// ----------------------------------------------------------------------------
// The cinematic gun-shot (gun · muzzle flash · a flying camera · BOOM) wrapped
// around the REAL `bulletblock` physics sim. The collision is genuinely true —
// a perfectly inelastic impact, momentum conserved, the live speed readout
// actually collapsing 40 → ~1 — and the BOOM is SYNCED to the physical impact.
// A sim's parts are ordinary entities, so the story and the physics compose.
// ============================================================================
title("Impact — the movie meets the physics");
canvas("16:9");
// ---- the REAL physics: bullet + block, inelastic collision + live speed readout ----
// a lighter block, so the hit visibly KNOCKS it and it slides on afterward
bulletblock(bb, (cx, 430), 0.05, 40, 0.9);
hidden(bb);
sticky(bb.vel); // pin the live speed readout through the camera moves
// ---- cinematic dressing (base manic) ----
rect(barrel, (200, 380), 82, 20); color(barrel, dim); filled(barrel);
rect(body, (166, 386), 42, 34); color(body, dim); filled(body);
polygon(grip, (150, 404), (176, 404), (170, 442), (146, 438), dim);
circle(mflash, (242, 380), 28); color(mflash, gold); glow(mflash, 3.5); hidden(mflash);
text(boom, (730, 250), "BOOM!"); size(boom, 84); color(boom, magenta); bold(boom); glow(boom, 2.5); display(boom); hidden(boom);
for i in 0..14 {
let ang = i * tau / 14.0;
line(spark{i}, (730, 380), (730 + 150*cos(ang), 380 + 150*sin(ang)));
color(spark{i}, gold); stroke(spark{i}, 5); glow(spark{i}, 2); untraced(spark{i}); tag(spark{i}, sparks);
}
text(cap, (cx, h - 46), ""); color(cap, fg); size(cap, 22); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
cam((430, 400), 0.4, smooth);
say(cap, "a scene — and it's really physics underneath", 0.4);
wait(0.5);
say(cap, "FIRE!", 0.2);
par { show(mflash, 0.06); pulse(mflash); show(bb, 0.1); }
fade(mflash, 0.3);
// the bullet flies (REAL physics, slow-mo) · the camera pushes toward the block ·
// the BOOM fires exactly when the sim's bullet embeds (55% of the run) · then it
// KEEPS GOING — the block is knocked, slides on, and the camera follows it
par {
run(bb, 7);
cam((720, 380), 3.6, smooth); // push toward the impact during the flight
zoom(1.2, 3.6, smooth);
seq {
wait(3.85); // the physical impact (55% of the run)
// CONTACT → zoom-punch + BOOM
say(cap, "CONTACT!", 0.12);
par { flash(bb.block, gold); shake(bb.block, 0.6); zoom(1.6, 0.15); show(boom, 0.12); pulse(boom); draw(sparks, 0.3); }
// …and it CONTINUES — sparks clear, the block slides on, camera follows
say(cap, "…knocked back — it slides on", 0.3);
par { fade(sparks, 0.5); fade(boom, 0.7); cam((1010, 380), 2.4, smooth); zoom(1.15, 2.4, smooth); }
}
}
// it comes to rest — and the physics was true the whole way through
par { zoom(1.0, 0.9, smooth); cam((cx, 360), 0.9, smooth); }
say(cap, "…and rest. A movie on top, real physics underneath: 40 m/s → the block's crawl", 0.4);
wait(1.0);
bullet-block-annotated
The bullet’s JOURNEY, the manic way: a gun fires, a muzzle flash, a glowing bullet crosses
the gap in slow-motion and embeds — the speed readout crashing 40 → ~1. A scene, not the
bare lab sim (base-manic staging over bulletblock).
// ============================================================================
// bullet-block-annotated.manic — a bullet's journey (inelastic collision)
// ----------------------------------------------------------------------------
// Not the bare lab sim — a scene: a gun fires, a muzzle flash, a glowing bullet
// crosses the gap in slow-motion, embeds in the block, and the live speed readout
// COLLAPSES from 40 m/s to ~1. The block barely lurches — it soaked up the bullet
// but almost none of the speed. Base-manic staging over the `bulletblock` sim.
// ============================================================================
title("A bullet fired into a block");
canvas("16:9");
text(hdr, (cx, 44), "A bullet fired into a block"); size(hdr, 30); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
text(sub, (cx, 80), "perfectly inelastic: the bullet embeds — momentum survives, speed collapses"); size(sub, 17); color(sub, dim); display(sub); hidden(sub);
// the sim (bullet at the muzzle, block at rest, floor, live speed readout, energy)
bulletblock(bb, (cx, 430), 0.05, 40, 1.95);
hidden(bb);
energygraph(bb, (1055, 250), 82);
// the gun (all base-manic shapes), at the bullet's start height
rect(barrel, (200, 380), 82, 20); color(barrel, dim); filled(barrel);
rect(body, (166, 386), 42, 34); color(body, dim); filled(body);
polygon(grip, (150, 404), (176, 404), (170, 442), (146, 438), dim);
circle(flash, (242, 380), 17); color(flash, gold); glow(flash, 2.4);
hidden(barrel); hidden(body); hidden(grip); hidden(flash);
// callouts
text(cbl, (300, 336), "the bullet — light + fast"); size(cbl, 16); color(cbl, red); display(cbl); hidden(cbl);
text(cblk, (720, 300), "the block — heavy, at rest"); size(cblk, 16); color(cblk, cyan); display(cblk); hidden(cblk);
text(cap, (cx, h - 38), ""); size(cap, 19); color(cap, fg); display(cap);
// ============================== SCRIPT ==============================
show(hdr, 0.5); show(sub, 0.4);
wait(0.3);
show(bb, 0.5); show(barrel, 0.3); show(body, 0.3); show(grip, 0.3);
show(cbl, 0.3); show(cblk, 0.3);
wait(0.4);
say(cap, "a heavy block sits at rest; a fast, light bullet is loaded", 0.4);
wait(0.5);
say(cap, "FIRE — 40 m/s across the gap …", 0.4);
flash(flash, gold); pulse(flash);
run(bb, 6);
say(cap, "… it embeds. Momentum is conserved, but 40 m/s collapses to ~1 — the energy is gone", 0.4);
wait(1.0);
newtons-cradle
Newton’s cradle: pull one ball, one swings out the far side — momentum and energy pass
straight through the chain. An EVENT-DRIVEN sim (free-flight pendulums between elastic
collisions resolved by a shared 1-D impulse), the crowd-pleaser (newtonscradle).
// newtons-cradle.manic — Newton's cradle: pull N balls, N swing out. An EVENT-
// DRIVEN sim — free-flight pendulums between elastic collisions resolved by the
// shared `collide_1d` impulse. `newtonscradle(id,[center],[balls],[pulled])`.
title("Newton's cradle — momentum passes through");
canvas("16:9");
text(hdr, (cx, 70), "Pull one, one swings out — momentum and energy pass straight through"); size(hdr, 22); color(hdr, cyan); bold(hdr); display(hdr);
newtonscradle(nc, (cx, 170), 5, 1);
energygraph(nc, (1040, 470), 90);
text(cap, (cx, h - 40), "five equal balls, elastic collisions (e = 1): the chain conserves momentum and energy"); size(cap, 15); color(cap, dim); display(cap);
run(nc, 8);
string-wave
A wave on a plucked string: 36 masses on springs, both ends fixed (the discretised wave
equation). Pluck it off-centre and the pulse splits, travels, and reflects off the ends —
a rainbow chain that wiggles, pre-simulated with RK4 (stringwave).
// string-wave.manic — a wave on a plucked string: N masses on springs, both ends
// fixed (the discretised wave equation). `stringwave(id,[center],[width],[amp],
// [pluck])`. Pluck it off-centre and the pulse splits into two, travels out, and
// reflects (inverting) off the fixed ends. Drawn as a rainbow chain that wiggles.
title("Wave on a string — pluck it and watch it travel");
canvas("16:9");
text(hdr, (cx, 70), "Pluck a string: the pulse splits, travels, and reflects off the ends"); size(hdr, 23); color(hdr, cyan); bold(hdr); display(hdr);
stringwave(sw, (cx, 380), 900, 110, 0.28);
text(cap, (cx, h - 40), "36 masses on springs, fixed at both ends — the wave equation, pre-simulated with RK4"); size(cap, 16); color(cap, dim); display(cap);
run(sw, 10);
loop-track
A ball rolls down a ramp and around a vertical LOOP-THE-LOOP — the curved-track case.
A bead energy solver (v = √(2g(H−y)) along the arc) so it visibly slows at the top;
release above 2·radius to clear it. energygraph tracks KE↔PE (looptrack).
// loop-track.manic — a ball rolls down a ramp and around a vertical loop-the-loop.
// `looptrack(id,[center],[radius],[height],[unit])`. A curved-track energy solver:
// v = √(2g(H−y)) along the arc, so the ball visibly SLOWS at the top. The release
// height must exceed 2·radius to clear the top. `energygraph` shows KE↔PE.
title("Loop-the-loop — down the ramp, around the loop");
canvas("16:9");
text(hdr, (cx, 44), "Release high enough (H > 2r) and the ball clears the top of the loop"); size(hdr, 21); color(hdr, cyan); bold(hdr); display(hdr);
looptrack(lt, (470, 560), 1, 3);
energygraph(lt, (1010, 330), 120);
text(cap, (cx, h - 32), "energy conserved: it trades kinetic for potential, slowest at the top of the loop"); size(cap, 15); color(cap, dim); display(cap);
run(lt, 5);
loop-cinematic
The loop-the-loop as a MOVIE with real physics inside: the camera pushes in as the ball
climbs, and the tension is genuine — a modest release height means it truly crawls over
the top before rocketing out. cam/zoom beats synced to the looptrack sim.
// ============================================================================
// loop-cinematic.manic — the loop-the-loop as a movie (real physics inside)
// ----------------------------------------------------------------------------
// Same recipe as bullet-impact: build the real `looptrack` sim, aim the camera,
// sync the beats to the physics. A ball is released, races down the ramp, and
// the camera pushes in as it climbs the loop — the tension is REAL (it genuinely
// slows at the top, energy traded for height), then it clears and rockets out.
// ============================================================================
title("Loop the Loop");
canvas("16:9");
// the real curved-track physics — a modest release height, so it BARELY clears
// the top (H just over 2·r) — that's where the real tension lives
looptrack(lt, (500, 560), 1, 2.4, 95);
hidden(lt);
text(cap, (cx, h - 46), ""); color(cap, fg); size(cap, 24); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
cam((420, 400), 0.4, smooth); // frame the whole ramp + loop
say(cap, "release…", 0.4);
show(lt, 0.5);
wait(0.4);
// down the ramp, into the loop — the camera pushes in for the climb
par {
run(lt, 6.0);
seq {
say(cap, "released — down the ramp!", 0.3);
par { cam((500, 455), 3.0, smooth); zoom(1.35, 3.0, smooth); } // slow push through the descent + entry
say(cap, "up and over — will it clear the top?!", 0.3);
wait(1.7); // the climb + the crawl over the top
say(cap, "…MADE IT!", 0.25);
par { cam((470, 410), 1.3, smooth); zoom(1.1, 1.3, smooth); } // ease back as it rockets out
}
}
// the physics was the whole story
say(cap, "…real energy: fastest at the bottom, barely crawling over the top", 0.4);
wait(1.0);
spring-chain
Three blocks joined by two springs on an incline — coupled oscillators. Pull one and the
whole chain rings (normal modes / beating); shown in the incline’s frame since uniform
gravity doesn’t touch the internal motion (springchain).
// spring-chain.manic — three blocks joined by two springs on an incline: coupled
// oscillators / normal modes. `springchain(id,[center],[angle],[unit])`. A uniform
// gravity component doesn't change the internal motion, so it's shown in the
// incline's frame (CM held). `energygraph` shows the energy sloshing (beating).
title("Spring chain on an incline — coupled oscillators");
canvas("16:9");
text(hdr, (cx, 44), "Three masses, two springs — the energy sloshs between the modes"); size(hdr, 21); color(hdr, cyan); bold(hdr); display(hdr);
springchain(sc, (500, 300), 22);
energygraph(sc, (1010, 340), 120);
text(cap, (cx, h - 32), "pull one block and the whole chain rings — normal modes / beating (coupled springs)"); size(cap, 15); color(cap, dim); display(cap);
run(sc, 9);
incline-showcase
One paper page, FOUR live incline problems: a friction ramp, an incline+pulley, a
two-slope wedge, and a spring bumper — revealed one at a time with narration, then all
run in parallel. Real base-manic staging (template("paper") + hidden/show + say
par), not a physics dump.
// ============================================================================
// incline-showcase.manic — four incline problems, one paper page, all live
// ----------------------------------------------------------------------------
// A single annotated lesson that puts ALL FOUR animated incline sims on one
// `template("paper")` page and runs them together — real base-manic composition
// (staged reveal with `hidden`/`show`, `say` narration, per-quadrant labels),
// not a physics dump. Each sim's bare id broadcasts, so `hidden(rp)`/`show(rp)`
// address a whole sim at once. Finishes with all four running in parallel.
// ============================================================================
title("Four inclined-plane problems — one page");
canvas("16:9");
template("paper");
text(hdr, (cx, 34), "Four inclined-plane problems, one page"); color(hdr, fg); size(hdr, 26); bold(hdr); display(hdr); hidden(hdr);
// the four sims (each hidden; the bare id broadcasts to all its parts)
ramp(rp, (110, 340), 28, 5, 0, 45); hidden(rp);
inclinepulley(ip, (690, 340), 30, 3, 2, 42); hidden(ip);
doubleincline(dw, (300, 660), 50, 30, 12, 70, 34); hidden(dw);
inclinebumper(ib, (720, 660), 38, 2, 500, 42); hidden(ib);
// quadrant labels
text(l1, (150, 175), "① friction on a ramp"); color(l1, dim); size(l1, 17); display(l1); hidden(l1);
text(l2, (770, 175), "② incline + pulley"); color(l2, dim); size(l2, 17); display(l2); hidden(l2);
text(l3, (150, 505), "③ two-slope wedge"); color(l3, dim); size(l3, 17); display(l3); hidden(l3);
text(l4, (770, 505), "④ spring bumper"); color(l4, dim); size(l4, 17); display(l4); hidden(l4);
text(cap, (cx, h - 26), ""); color(cap, fg); size(cap, 19); display(cap);
// ============================== SCRIPT ==============================
show(hdr, 0.5);
say(cap, "one page, four classic incline problems — all real, pre-simulated physics", 0.4);
wait(0.5);
say(cap, "① a block sliding down a rough ramp", 0.4);
show(rp, 0.5); show(l1, 0.3);
wait(0.5);
say(cap, "② a block on an incline, tied over a pulley to a hanging mass", 0.4);
show(ip, 0.5); show(l2, 0.3);
wait(0.5);
say(cap, "③ two blocks on a wedge, connected over the apex", 0.4);
show(dw, 0.5); show(l3, 0.3);
wait(0.5);
say(cap, "④ a block sliding into a spring bumper at the base", 0.4);
show(ib, 0.5); show(l4, 0.3);
wait(0.6);
say(cap, "…and they all run — same page, same physics engine, four different motions", 0.4);
par {
run(rp, 7);
run(ip, 7);
run(dw, 7);
run(ib, 7);
}
textbook-incline-fbd
A block on an incline as a physics-class FREE-BODY DIAGRAM: the reusable forces(id)
view draws gravity/normal/friction/a vectors on the block, a second panel redraws them
from a point, and template("paper") inks it — then run slides the block (ramp).
// ============================================================================
// textbook-incline-fbd.manic — the free-body diagram, textbook style
// ----------------------------------------------------------------------------
// A block on an incline shown the classic physics-class way: the physical
// picture on the left with the force vectors ON the block (via the reusable
// `forces(id)` view on the `ramp` sim — gravity `mg`, normal `N`, friction `f`,
// and the acceleration `a`), and a free-body-diagram panel on the right drawing
// the same forces from a single point. `template("paper")` inks it; then `run`
// plays the slide with the vectors riding the block.
// ============================================================================
title("Block on an incline — the free-body diagram");
canvas("16:9");
template("paper");
text(hdr, (cx, 42), "Block on an incline — the free-body diagram"); color(hdr, fg); size(hdr, 24); bold(hdr); display(hdr);
// left: the incline sim, inked; its force vectors revealed by forces()
ramp(rp, (170, 470), 30);
outlined(rp.block); outline(rp.block, fg);
// right: the free-body diagram — the same four forces from one point
dot(o, (1000, 330), 6); color(o, fg);
text(fbdT, (1000, 205), "free-body diagram"); color(fbdT, dim); size(fbdT, 16); display(fbdT);
arrow(aN, (1000, 330), (955, 252)); color(aN, lime); stroke(aN, 3);
text(lN, (944, 240), "N"); color(lN, lime); size(lN, 18); display(lN);
arrow(af, (1000, 330), (1078, 285)); color(af, magenta); stroke(af, 3);
text(lf, (1086, 280), "f"); color(lf, magenta); size(lf, 18); display(lf);
arrow(ag, (1000, 330), (1000, 440)); color(ag, blue); stroke(ag, 3);
text(lg, (1010, 448), "mg"); color(lg, blue); size(lg, 18); display(lg);
arrow(aa, (1000, 330), (930, 372)); color(aa, red); stroke(aa, 3);
text(la, (908, 380), "a"); color(la, red); size(la, 18); display(la);
text(cap, (cx, h - 34), ""); color(cap, fg); size(cap, 19); display(cap);
// ============================== SCRIPT ==============================
say(cap, "three forces act on the block: gravity, the normal force, and friction", 0.4);
forces(rp, 0.9);
wait(1.0);
say(cap, "drawn from a single point, they make the free-body diagram →", 0.4);
wait(0.9);
say(cap, "their sum points down the slope, so the block accelerates and slides", 0.4);
run(rp, 5);
pulley-annotated
The Atwood machine elevated with CAMERA work: cam + zoom push in on the two masses
for the imbalance beat and glow the heavier one, a counter ticks the acceleration up,
then it pulls back to release — cinematography instead of section cards.
// ============================================================================
// pulley-annotated.manic — ELEVATE a sim: CAMERA work (cam + zoom)
// ----------------------------------------------------------------------------
// A different elevation flavour from spring-annotated (typewriter) — here the
// camera does the storytelling: `cam` + `zoom` push in on the two masses for
// the "imbalance" beat, glow the heavier one, then pull back for the release.
// A live `counter` ticks the acceleration up. No stage-covering section cards,
// so the motion is always visible. All base manic over the `pulley` parts.
// ============================================================================
title("The Atwood Machine — Why the Heavier Side Falls");
canvas("16:9");
text(hdr, (cx, 40), "The Atwood Machine"); size(hdr, 28); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
// ---- the sim, centred so the camera can push in ----
pulley(pl, (cx, 190), 3, 2);
hidden(pl.wheel); hidden(pl.hub); hidden(pl.ropeL); hidden(pl.ropeR); hidden(pl.mass1); hidden(pl.mass2);
text(wheelL, (cx, 118), "frictionless pulley"); size(wheelL, 17); color(wheelL, dim); display(wheelL); hidden(wheelL);
text(m1L, (cx - 150, 320), "m₁ = 3 kg"); size(m1L, 19); color(m1L, cyan); display(m1L); hidden(m1L);
text(m2L, (cx + 150, 320), "m₂ = 2 kg"); size(m2L, 19); color(m2L, magenta); display(m2L); hidden(m2L);
counter(aC, (cx, 250), 0, 2, "a = ", " m/s²"); size(aC, 26); color(aC, gold); display(aC); hidden(aC); sticky(aC);
// caption is `sticky` — it stays pinned to the screen through the camera push-in
text(cap, (cx, h - 44), ""); size(cap, 21); color(cap, dim); display(cap); sticky(cap);
// ============================== SCRIPT ==============================
show(hdr, 0.5);
say(cap, "one rope over one frictionless pulley …", 0.4);
show(pl.wheel, 0.4); show(pl.hub, 0.3); pulse(pl.wheel); show(wheelL, 0.4);
wait(0.3);
say(cap, "… a heavier mass on the left, a lighter one on the right", 0.4);
show(pl.ropeL, 0.3); show(pl.mass1, 0.4); pulse(pl.mass1); show(m1L, 0.4);
show(pl.ropeR, 0.3); show(pl.mass2, 0.4); pulse(pl.mass2); show(m2L, 0.4);
wait(0.5);
// ---- CAMERA: push in on the masses for the imbalance beat ----
// cap + aC are sticky, so they stay put on screen while the world zooms
say(cap, "look closely — the net pull favours the heavier side", 0.4);
par { cam((cx, 330), 1.4, smooth); zoom(1.7, 1.4, smooth); }
glow(pl.mass1, 1.9); flash(pl.mass1, gold); pulse(pl.mass1);
wait(1.0);
par { cam((cx, cy), 1.2, smooth); zoom(1, 1.2, smooth); } // pull back out
wait(0.2);
// the acceleration, as a live readout
show(aC, 0.3); to(aC, value, 1.96, 1.0); // a = (m₁−m₂)g/(m₁+m₂)
fade(wheelL, 0.3); fade(m1L, 0.3); fade(m2L, 0.3);
wait(0.5);
say(cap, "release: m₁ sinks, m₂ rises", 0.4);
run(pl, 4);
pulley-paper
The SAME Atwood sim dressed as a textbook figure AND run: template("paper") inks it
automatically, a hatched support ceiling, a forest-green wheel and outlined mass boxes,
a base-manic reveal, then run plays the motion — the paper treatment on a LIVE sim.
// ============================================================================
// pulley-paper.manic — an ANIMATED sim in textbook paper style
// ----------------------------------------------------------------------------
// The Atwood `pulley` sim, dressed as a physics-textbook figure AND run: the
// `template("paper")` page (its palette remap inks the sim automatically), a
// hatched `support` ceiling, a forest-green wheel, outlined mass boxes, and a
// base-manic reveal (`say`/`show`/`draw`/`fade`) — then `run` plays the motion.
// Proof the paper/support treatment composes with a live, animated simulation.
// ============================================================================
title("The Atwood machine — textbook style");
canvas("16:9");
template("paper"); // white page; the theme remaps neon → ink
text(hdr, (cx, 46), "The Atwood machine"); color(hdr, fg); size(hdr, 27); bold(hdr); display(hdr); hidden(hdr);
// hatched ceiling + suspension rope (traced on)
support(ceil, (cx, 108), 320); untraced(ceil);
line(sus, (cx, 108), (cx, 160)); color(sus, fg); stroke(sus, 2); untraced(sus);
// the sim, restyled to textbook ink (parts hidden, revealed in order)
pulley(pl, (cx, 192), 3, 2);
color(pl.wheel, lime); // lime → forest green on the paper palette
color(pl.hub, fg); color(pl.ropeL, fg); color(pl.ropeR, fg);
outlined(pl.mass1); outline(pl.mass1, fg);
outlined(pl.mass2); outline(pl.mass2, fg);
hidden(pl.wheel); hidden(pl.hub); hidden(pl.ropeL); hidden(pl.ropeR); hidden(pl.mass1); hidden(pl.mass2);
// annotations
text(m1L, (cx - 150, 316), "m₁ = 3 kg"); color(m1L, fg); size(m1L, 20); display(m1L); hidden(m1L);
text(m2L, (cx + 150, 316), "m₂ = 2 kg"); color(m2L, fg); size(m2L, 20); display(m2L); hidden(m2L);
text(acc, (cx, 604), "a = (m₁ − m₂)·g / (m₁ + m₂)"); color(acc, fg); size(acc, 22); display(acc); hidden(acc);
text(cap, (cx, h - 40), ""); color(cap, fg); size(cap, 20); display(cap);
// ============================== SCRIPT ==============================
show(hdr, 0.5);
say(cap, "a rope over one frictionless pulley, hung from the ceiling", 0.4);
draw(ceil, 0.4); draw(sus, 0.4); show(pl.wheel, 0.4); show(pl.hub, 0.3);
wait(0.4);
say(cap, "a heavier mass on the left, a lighter one on the right", 0.4);
show(pl.ropeL, 0.3); show(pl.mass1, 0.4); show(m1L, 0.3);
show(pl.ropeR, 0.3); show(pl.mass2, 0.4); show(m2L, 0.3);
wait(0.5);
say(cap, "the system accelerates toward the heavier side", 0.4);
show(acc, 0.5);
wait(0.7);
fade(m1L, 0.3); fade(m2L, 0.3);
say(cap, "release: m₁ sinks, m₂ rises", 0.4);
run(pl, 4);
ramp
A block sliding down an inclined plane with static/kinetic friction — the full force
model. Friction turns motion into heat, so the total-energy line steadily falls
(ramp + energygraph).
// ============================================================================
// ramp.manic — friction on an incline, on the physics baseline (Layer 1)
// ----------------------------------------------------------------------------
// `ramp(id, [center], [angle], [mass], [applied], [unit])` — a block sliding
// down an inclined plane. Full force model: gravity along the slope, the normal
// force, and a static→kinetic friction switch. Friction turns mechanical energy
// into heat, so `energygraph`'s total DECAYS as the block slides. PRE-SIMULATED
// with RK4; `run(id,[dur])` replays it.
//
// TO ADAPT: lower the angle below the friction angle and the block won't budge.
// ============================================================================
title("Inclined plane — friction dissipates energy");
canvas("16:9");
text(hdr, (cx, 44), "Block on a ramp — μ friction bleeds the energy away");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
ramp(rp, (300, 470), 30);
energygraph(rp, (960, 320), 120);
text(cap, (cx, h - 32), "as the block slides down, the gold total energy line falls — friction converts it to heat");
size(cap, 16); color(cap, dim); display(cap);
run(rp, 6);
drop-mass
A mass dropped onto a spring-block STICKS — a perfectly inelastic collision. Watch the
total-energy line step down at impact, then the heavier combined mass oscillate about
a lower equilibrium (dropmass + energygraph).
// ============================================================================
// drop-mass.manic — an inelastic collision, on the physics baseline (Layer 1)
// ----------------------------------------------------------------------------
// `dropmass(id, [center], [dropheight], [unit])` — a mass is dropped onto a
// block resting on a spring. It free-falls, STICKS (perfectly inelastic), then
// the heavier combined mass oscillates about a lower equilibrium. The collision
// loses kinetic energy — so `energygraph`'s total STEPS DOWN at impact. Then it
// is conserved again. PRE-SIMULATED with RK4; `run(id,[dur])` replays it.
//
// TO ADAPT: raise dropheight for a faster impact and a bigger energy step.
// ============================================================================
title("Drop mass — an inelastic collision loses energy");
canvas("16:9");
text(hdr, (cx, 44), "Dropped mass sticks — energy is lost in the inelastic collision");
size(hdr, 22); color(hdr, cyan); bold(hdr); display(hdr);
dropmass(dm, (440, 150), 1.2);
energygraph(dm, (960, 330), 120);
text(cap, (cx, h - 32), "watch the gold total energy line step DOWN the instant the magenta mass sticks to the block");
size(cap, 15); color(cap, dim); display(cap);
run(dm, 7);
raft-cm
A person walks back and forth on a floating raft; with no external force the centre of
mass stays fixed, so the raft glides the opposite way — momentum conservation you can
see (raft).
// ============================================================================
// raft-cm.manic — centre of mass, on the physics baseline (Layer 1)
// ----------------------------------------------------------------------------
// `raft(id, [center], [personmass], [raftmass], [unit])` — a person walking on
// a floating raft. With no external horizontal force, momentum is conserved and
// the centre of mass stays FIXED: the raft slides the opposite way by
// −m_person/(m_person+m_raft) of each step. PRE-SIMULATED; `run(id,[dur])`
// replays it. The dashed line marks the (unmoving) centre of mass.
//
// TO ADAPT: make the raft much lighter (raftmass) — it slides much further.
// ============================================================================
title("Raft — walk one way, the raft slides the other");
canvas("16:9");
text(hdr, (cx, 46), "Momentum stays zero, so the centre of mass never moves");
size(hdr, 23); color(hdr, cyan); bold(hdr); display(hdr);
raft(rf, (cx, 380), 70, 200);
text(cap, (cx, h - 32), "the person walks; the raft glides back — the dashed centre-of-mass line stays put");
size(cap, 16); color(cap, dim); display(cap);
run(rf, 9);
brachistochrone
Four beads race under gravity from A to B down a straight line, a circular arc, a
parabola, and a cycloid. The cycloid — the curve of fastest descent — wins, even
though it dips lower and travels farther (brachistochrone).
// ============================================================================
// brachistochrone.manic — the curve of fastest descent (physics Layer 1)
// ----------------------------------------------------------------------------
// `brachistochrone(id, [center], [unit])` — four beads race under gravity from
// A down to B along four curves: a straight line, a circular arc, a parabola,
// and a CYCLOID. Each is a full RK4 bead-on-wire integration. The cycloid — the
// brachistochrone — wins, even though it dips below and travels farther. The
// steep early drop buys speed that more than pays back the extra distance.
// `run(id,[dur])` replays the race.
//
// TO ADAPT: raise `dur` to watch the finish order in slow motion.
// ============================================================================
title("Brachistochrone — the fastest slide is a cycloid");
canvas("16:9");
text(hdr, (cx, 44), "Which curve is fastest from A to B? Steepness early beats a short path");
size(hdr, 21); color(hdr, cyan); bold(hdr); display(hdr);
brachistochrone(br, (330, 150), 150);
text(leg, (cx, h - 58), "dim: straight cyan: arc gold: parabola magenta: CYCLOID (winner)");
size(leg, 16); color(leg, fg); display(leg);
text(cap, (cx, h - 30), "same start, same finish — the magenta cycloid bead reaches B first");
size(cap, 16); color(cap, dim); display(cap);
run(br, 5);
brachistochrone-annotated
The elevation recipe on a RACE, told with KINETIC TYPOGRAPHY: wordpop pops the
question in, karaoke sweeps a highlight across the four path names as the curves
sketch on, then flash/glow crown the cycloid — a third, distinct elevation style.
// ============================================================================
// brachistochrone-annotated.manic — ELEVATE a sim: KINETIC TYPOGRAPHY
// ----------------------------------------------------------------------------
// A third elevation flavour (see spring-annotated for typewriter, pulley-
// annotated for camera). Here the text itself performs: `caption` + `wordpop`
// pops the question in word-by-word, `caption` + `karaoke` sweeps a highlight
// across the four path names as the curves are sketched, and `flash`/`glow`/
// `pulse` crown the winner — the same recipe on a multi-body RACE, no section
// cards. All base manic over the `brachistochrone` parts.
// ============================================================================
title("Brachistochrone — Shortest Path, or Fastest?");
canvas("16:9");
// ---- the sim (curves untraced to sketch on; beads + markers hidden) ----
brachistochrone(br, (330, 165), 150);
untraced(br.straight); untraced(br.circle); untraced(br.parabola); untraced(br.cycloid);
hidden(br.bead_straight); hidden(br.bead_circle); hidden(br.bead_parabola); hidden(br.bead_cycloid);
hidden(br.markA); hidden(br.labelA); hidden(br.markB); hidden(br.labelB);
// ---- kinetic-typography captions ----
caption(q, "shortest path — or fastest?", (cx, 62), 34, gold);
caption(leg, "straight arc parabola cycloid", (cx, h - 66), 26, dim);
text(cap, (cx, h - 32), ""); size(cap, 19); color(cap, dim); display(cap);
// ============================== SCRIPT ==============================
say(cap, "a bead slides from A down to B under gravity — same start, same finish", 0.4);
show(br.markA, 0.4); show(br.labelA, 0.3); pulse(br.markA);
show(br.markB, 0.4); show(br.labelB, 0.3); pulse(br.markB);
wait(0.4);
// the question pops in, word by word
wordpop(q, 0.18);
wait(0.6);
// sketch the four curves; the legend highlight sweeps across their names
say(cap, "four paths from A to B — the cycloid dips below the line", 0.4);
draw(br.straight, 0.5); draw(br.circle, 0.5); draw(br.parabola, 0.5); draw(br.cycloid, 0.6);
karaoke(leg, 0.5, cyan);
flash(br.cycloid, magenta);
wait(0.5);
// the race
say(cap, "release all four at once …", 0.4);
show(br.bead_straight, 0.3); show(br.bead_circle, 0.3); show(br.bead_parabola, 0.3); show(br.bead_cycloid, 0.3);
run(br, 6);
// crown the winner
say(cap, "the CYCLOID wins — the steep early drop buys speed that repays the longer path", 0.4);
flash(br.cycloid, lime); glow(br.bead_cycloid, 1.9); pulse(br.bead_cycloid);
wait(1.0);
textbook-pulley
A physics-TEXTBOOK figure, manic style: the template("paper") white page, a hatched
support ceiling, a green pulley wheel, and outlined labelled mass boxes — the classic
m over 2m+3m arrangement, all base primitives.
// ============================================================================
// textbook-pulley.manic — a physics-textbook figure, manic style
// ----------------------------------------------------------------------------
// The classic "m over 2m+3m" pulley arrangement, drawn in the flat textbook
// look: the `paper` theme (white page, dark ink), a hatched `support` ceiling,
// a solid green pulley wheel, thin ropes, and outlined labelled mass boxes.
// A STATIC figure — pure base manic (`support`, `circle`, `line`, `rect`, `text`).
// ============================================================================
title("Pulley arrangement — m, 2m, 3m");
canvas("16:9");
template("paper");
// hatched ceiling + the rope suspending the pulley
support(ceil, (cx, 120), 340);
line(rope0, (cx, 120), (cx, 222)); color(rope0, fg); stroke(rope0, 2);
// the green pulley wheel (dark edge + hub)
circle(wheel, (cx, 300), 80); color(wheel, lime); outline(wheel, fg);
circle(hub, (cx, 300), 5); color(hub, fg); filled(hub);
// ropes leave the wheel vertically, left and right
line(ropeL, (560, 300), (560, 456)); color(ropeL, fg); stroke(ropeL, 2);
line(ropeR, (720, 300), (720, 456)); color(ropeR, fg); stroke(ropeR, 2);
// mass on the left: m
rect(mbox, (560, 485), 110, 58); outlined(mbox); outline(mbox, fg); stroke(mbox, 2);
text(ml, (560, 485), "m"); color(ml, fg); size(ml, 26); bold(ml);
// masses on the right: 2m, with 3m hanging below it
rect(m2, (720, 485), 120, 58); outlined(m2); outline(m2, fg); stroke(m2, 2);
text(l2, (720, 485), "2m"); color(l2, fg); size(l2, 26); bold(l2);
line(r23, (720, 514), (720, 596)); color(r23, fg); stroke(r23, 2);
rect(m3, (720, 625), 120, 58); outlined(m3); outline(m3, fg); stroke(m3, 2);
text(l3, (720, 625), "3m"); color(l3, fg); size(l3, 26); bold(l3);
textbook-tension
Another textbook figure: two support ropes at 60°/30° meeting a knot, a string over a
hanging pulley carrying 10 kg with the other end anchored to a hatched floor — support
template("paper")for the flat exam-paper look.
// ============================================================================
// textbook-tension.manic — a physics-textbook figure, manic style
// ----------------------------------------------------------------------------
// Two support ropes at 60° / 30° meet at a knot; a string over a hanging pulley
// carries a 10 kg load on one side and anchors to the floor on the other. Drawn
// in the flat textbook look: `paper` theme, hatched `support` ceiling + floor,
// a green pulley wheel, and outlined labels. A STATIC figure, all base manic.
// ============================================================================
title("Tension in a rope over a pulley (g = 10 m/s²)");
canvas("16:9");
template("paper");
// hatched ceiling
support(ceil, (cx, 110), 700);
// the two support ropes meeting at the knot, with a dashed-style vertical ref
line(t1, (553, 110), (cx, 260)); color(t1, fg); stroke(t1, 2);
line(t2, (900, 110), (cx, 260)); color(t2, fg); stroke(t2, 2);
line(vref, (cx, 110), (cx, 260)); color(vref, dim); stroke(vref, 1);
text(a1, (588, 150), "60°"); color(a1, fg); size(a1, 22);
text(a2, (852, 150), "30°"); color(a2, fg); size(a2, 22);
text(t1l, (556, 210), "T₁"); color(t1l, fg); size(t1l, 24); bold(t1l);
text(t2l, (820, 210), "T₂"); color(t2l, fg); size(t2l, 24); bold(t2l);
// knot → hanging pulley
line(drop, (cx, 260), (cx, 364)); color(drop, fg); stroke(drop, 2);
circle(wheel, (cx, 430), 66); color(wheel, lime); outline(wheel, fg);
circle(hub, (cx, 430), 5); color(hub, fg); filled(hub);
// 10 kg load on the right side of the string
line(ropeM, (706, 430), (706, 520)); color(ropeM, fg); stroke(ropeM, 2);
rect(mbox, (706, 548), 84, 56); outlined(mbox); outline(mbox, fg); stroke(mbox, 2);
text(ml, (792, 548), "10 Kg"); color(ml, fg); size(ml, 22); bold(ml);
// the other side anchored to a hatched floor
line(ropeF, (cx, 496), (cx, 600)); color(ropeF, fg); stroke(ropeF, 2);
support(floor, (cx, 612), 150, "up");
Optics — light as geometry
Easy builtins with the real physics underneath — Snell’s law and Sellmeier dispersion — so the bending and the colours are earned, not painted. Each is static geometry that animates by a parameter sweep: call run(id).
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
refraction
Snell’s law you can watch: a ray crossing from air into glass bends toward the normal,
and run sweeps the incidence angle so the refracted ray swings — the live in/out
read-outs are the true angles. Start in the denser medium and it shows total internal
reflection past the critical angle (refract).
// ============================================================================
// refraction.manic — Snell's law you can watch (real optics)
// ----------------------------------------------------------------------------
// The `optics` kit's first builtin: `refract` draws a light ray meeting the
// boundary between two media and bending. `run` SWEEPS the incidence angle, so
// the refracted ray genuinely swings — and the "in"/"out" read-outs are the
// true Snell angles, not painted. Here: air (n=1.00) into crown glass (n=1.52).
// ============================================================================
title("Refraction — Snell's Law");
canvas("16:9");
// air on top, glass below; no fixed angle → run(r) sweeps it
refract(r, (640, 380), 1.0, 1.52);
text(cap, (cx, h - 46), ""); color(cap, fg); size(cap, 24); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
say(cap, "a ray of light, crossing from air into glass…", 0.5);
wait(0.5);
par {
run(r, 7);
seq {
say(cap, "shallow angle — it barely bends", 0.3);
wait(2.2);
say(cap, "steeper in → it bends MORE, toward the normal", 0.3);
wait(2.6);
say(cap, "sin(in) / sin(out) is fixed — THAT ratio is the refractive index", 0.3);
}
}
wait(1.2);
lens
A converging lens: a parallel beam bends to meet at the focal point F (ideal thin lens —
every ray passes through F). run sweeps the focal length, so the focus slides in as the
lens gets stronger (lens).
// ============================================================================
// lens.manic — a converging lens, and where it focuses (real optics)
// ----------------------------------------------------------------------------
// The `optics` kit's `lens` builtin: a beam of parallel rays is bent by a
// converging lens to meet at the focal point F. `run` SWEEPS the focal length,
// so you watch the focus slide in toward the lens — a shorter focal length is
// a stronger lens. Ideal thin lens (every parallel ray passes through F).
// ============================================================================
title("Converging Lens");
canvas("16:9");
lens(l, (620, 360));
text(cap, (cx, h - 46), ""); color(cap, fg); size(cap, 24); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
say(cap, "parallel light meets a converging lens…", 0.5);
wait(0.5);
par {
run(l, 7);
seq {
say(cap, "every ray bends to the SAME point — the focus, F", 0.3);
wait(2.6);
say(cap, "shorter focal length = a stronger lens → the focus slides IN", 0.3);
wait(2.4);
say(cap, "past F the rays cross and spread apart again", 0.3);
}
}
wait(1.2);
prism
White light into a prism, out as a RAINBOW — each colour traced through both faces with
its own refractive index (real Sellmeier dispersion), so blue bends more than red because
glass genuinely slows blue more. run sweeps the incidence angle and the fan widens (prism).
// ============================================================================
// prism.manic — white light into a rainbow (real dispersion)
// ----------------------------------------------------------------------------
// The `optics` kit's `prism`: white light enters a triangular prism and splits
// into a spectrum. Each colour is traced through both faces with its OWN
// refractive index (real Sellmeier dispersion) — blue bends more than red
// because glass genuinely slows blue more. `run` sweeps the incidence angle,
// so the fan swings and its spread widens.
// ============================================================================
title("Prism — White Light Splits");
canvas("16:9");
prism(p, (560, 400), "sf11"); // dense flint — a wide, vivid spectrum
text(cap, (cx, h - 46), ""); color(cap, fg); size(cap, 24); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
say(cap, "one beam of white light, into a prism…", 0.5);
wait(0.5);
par {
run(p, 7);
seq {
say(cap, "it comes out as a RAINBOW — the colours were always there", 0.3);
wait(2.6);
say(cap, "blue bends more than red: glass really does slow blue more", 0.3);
wait(2.4);
say(cap, "steeper in → the spectrum fans WIDER (real Sellmeier dispersion)", 0.3);
}
}
wait(1.2);
achromat
The optics capstone — chromatic aberration and its fix: a single lens focuses blue nearer
than red, so white light never comes to one point; run sweeps in the achromatic doublet
and the colours snap back to a single sharp focus (achromat).
// ============================================================================
// achromat.manic — chromatic aberration, and the fix (real optics)
// ----------------------------------------------------------------------------
// The optics kit's capstone. A single lens can't focus all colours to one
// point: it bends blue more than red (its index is higher for blue — real
// dispersion), so blue focuses NEARER and red FARTHER. `run` then sweeps in the
// achromatic doublet (crown + flint) and the two foci slide back together to a
// single sharp point — the trick every camera and telescope lens uses.
// ============================================================================
title("The Achromatic Doublet");
canvas("16:9");
achromat(ac, (540, 360));
text(cap, (cx, h - 46), ""); color(cap, fg); size(cap, 24); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
say(cap, "one simple lens — but the colours don't agree…", 0.5);
wait(0.6);
par {
run(ac, 7);
seq {
say(cap, "blue focuses NEARER, red FARTHER — chromatic aberration", 0.3);
wait(2.6);
say(cap, "now add a flint element behind it — the achromatic doublet", 0.3);
wait(2.2);
say(cap, "…and the colours snap back to ONE sharp focus", 0.3);
}
}
wait(1.2);
refraction-paper
Snell’s law as a template("paper") TEXTBOOK figure: inked media labels, the normal, and
the law itself, with a camera easing in on the bending point as run sweeps the angle —
the geometric builtins suit paper (refract + annotation + camera).
// ============================================================================
// refraction-paper.manic — Snell's law as a textbook figure (annotated)
// ----------------------------------------------------------------------------
// The `refract` sim dressed as a physics-class page: `template("paper")` inks
// it on cream, labelled media + normal + the law itself, and a gentle camera
// push on the bending point while `run` sweeps the incidence angle. Elevation
// lens: textbook annotation + camera. (Paper suits the geometry; the coloured
// dispersion demos stay on a dark bench where light glows.)
// ============================================================================
title("Refraction — Snell's Law");
canvas("16:9");
template("paper");
refract(r, (660, 400), 1.0, 1.52);
color(r.incident, red); // gold washes out on cream — ink the incoming ray red
color(r.thetai, red);
// ---- textbook annotations (screen-fixed) ----
text(mAir, (150, 300), "air n = 1.00"); color(mAir, dim); size(mAir, 22); sticky(mAir);
text(mGla, (150, 500), "glass n = 1.52"); color(mGla, dim); size(mGla, 22); sticky(mGla);
text(nlab, (708, 250), "normal"); color(nlab, dim); size(nlab, 18); sticky(nlab);
text(law, (cx, 92), "n1 · sin(in) = n2 · sin(out)"); color(law, fg); size(law, 30); bold(law); sticky(law);
text(cap, (cx, h - 46), ""); color(cap, fg); size(cap, 24); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
say(cap, "light bends as it crosses into glass — Snell's law", 0.5);
wait(0.5);
par {
run(r, 8);
seq {
par { cam((660, 400), 2.2, smooth); zoom(1.18, 2.2, smooth); } // ease in on the boundary
say(cap, "steeper in → it bends more, toward the normal", 0.3);
wait(3.0);
say(cap, "…but sin(in) / sin(out) never changes — that ratio IS n", 0.3);
}
}
wait(1.2);
lens-paper
The converging lens inked on paper and narrated by a TYPEWRITER caption (a different
elevation lens): labelled parallel rays and focal point F while run slides the focus
(lens + type).
// ============================================================================
// lens-paper.manic — a converging lens as a textbook figure (annotated)
// ----------------------------------------------------------------------------
// The `lens` sim inked on paper, narrated by a TYPEWRITER caption (a different
// elevation lens from refraction-paper's camera), with labelled parallel rays
// and focal point while `run` sweeps the focal length. Ideal thin lens.
// ============================================================================
title("Converging Lens");
canvas("16:9");
template("paper");
lens(l, (640, 360));
// ---- textbook annotations (screen-fixed) ----
text(inlab, (150, 250), "parallel rays"); color(inlab, dim); size(inlab, 22); sticky(inlab);
text(flab, (980, 300), "focal point F"); color(flab, dim); size(flab, 22); sticky(flab);
arrow(farr, (980, 320), (900, 356)); color(farr, dim); sticky(farr);
// a typewriter caption performs the narration (untraced → type reveals it)
text(cap, (cx, h - 48), "a lens bends parallel light to a single point"); color(cap, fg); size(cap, 26); bold(cap); untraced(cap); sticky(cap);
// ================= THE SCENE =================
type(cap, 1.6); // type the line out
wait(0.4);
par {
run(l, 8);
seq {
wait(0.6);
say(cap, "every ray meets at the focus, F", 0.3);
wait(2.8);
say(cap, "a shorter focal length is a stronger lens — F slides in", 0.3);
}
}
wait(1.2);
prism-cinematic
The prism on a dark optics bench where the spectrum GLOWS (a rainbow washes out on paper):
the colour names pop in word-by-word (wordpop) as the fan spreads and the camera flies
toward it — KINETIC-TYPE elevation (prism).
// ============================================================================
// prism-cinematic.manic — white light into a rainbow (annotated, dark bench)
// ----------------------------------------------------------------------------
// The `prism` sim on a dark optics bench, where the spectrum GLOWS (a rainbow
// washes out on paper — light is a dark-background subject). Elevation lens:
// KINETIC TYPE — the colour names pop in word-by-word (`wordpop`) as the fan
// spreads, plus a camera push toward the exit spectrum.
// ============================================================================
title("Prism — White Light Splits");
canvas("16:9");
prism(p, (560, 400), "sf11");
// the colour names, popped in one at a time over the fan
caption(names, "red orange yellow green blue violet", (cx + 40, 150), 26, gold);
text(cap, (cx, h - 46), ""); color(cap, fg); size(cap, 24); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
say(cap, "one beam of white light, into a prism…", 0.5);
wait(0.5);
par {
run(p, 8);
seq {
par { cam((820, 430), 2.6, smooth); zoom(1.22, 2.6, smooth); } // fly toward the spectrum
say(cap, "…out as a rainbow — the colours were always hidden inside", 0.3);
wordpop(names, 0.22); // the colours pop in
wait(1.6);
say(cap, "blue bends most, red least — real Sellmeier dispersion", 0.3);
wait(1.6);
}
}
par { cam((cx, 400), 1.2, smooth); zoom(1.0, 1.2, smooth); }
wait(1.0);
achromat-cinematic
The achromat with the CAMERA magnifying the focal region so the red/blue split is dramatic,
a bracelabel marking the aberration gap that closes as run sweeps in the doublet and
the colours merge (achromat + camera + brace).
// ============================================================================
// achromat-cinematic.manic — chromatic aberration & its fix (annotated, dark)
// ----------------------------------------------------------------------------
// The `achromat` capstone on a dark bench, where the coloured foci glow.
// Elevation lens: the CAMERA magnifies the focal region so the red/blue split
// is dramatic, and a `bracelabel` marks the aberration gap — which then closes
// as `run` sweeps in the doublet and the colours merge to one point.
// ============================================================================
title("The Achromatic Doublet");
canvas("16:9");
achromat(ac, (540, 360));
// bracket the axial gap between the two foci (fades as they merge)
bracelabel(gap, (852, 406), (908, 406), "CA", 20); color(gap, red);
text(cap, (cx, h - 46), ""); color(cap, fg); size(cap, 24); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
say(cap, "one simple lens — but the colours refuse to agree…", 0.5);
wait(0.6);
par {
run(ac, 8);
seq {
par { cam((880, 360), 2.4, smooth); zoom(1.7, 2.4, smooth); } // magnify the focal region
say(cap, "blue focuses NEARER, red FARTHER — chromatic aberration", 0.3);
wait(2.8);
say(cap, "now cement a flint element behind it — the achromat", 0.3);
fade(gap, 0.7); // the gap closes
wait(2.0);
say(cap, "…and the colours snap back to ONE sharp focus", 0.3);
}
}
par { cam((cx, 360), 1.3, smooth); zoom(1.0, 1.3, smooth); }
wait(1.2);
lens-system
A REAL multi-element lens, ray-traced through its actual spherical surfaces (not the ideal
thin lens): the fast singlet reveals SPHERICAL ABERRATION — draw sketches the rays, then
run sweeps a sensor plane and the live spot read-out dips but never reaches a point,
because the outer rays focus short (lenssystem, presets singlet/doublet/triplet).
// ============================================================================
// lens-system.manic — a REAL lens, ray-traced (best of physics + manic)
// ----------------------------------------------------------------------------
// `lenssystem` traces a parallel beam through the ACTUAL spherical surfaces of
// a lens (not the ideal thin lens of `lens`). Here the fast singlet reveals
// SPHERICAL ABERRATION: sketch the rays on with `draw`, then `run` sweeps a
// sensor plane along the axis while the live spot read-out dips to its minimum
// — which never reaches a point, because the outer rays focus short. Then swap
// the caption's promise: that's the flaw a doublet/triplet is built to fix.
// ============================================================================
title("Spherical Aberration — a Real Lens Traced");
canvas("16:9");
lenssystem(ls, (620, 380), "singlet");
text(cap, (cx, h - 42), ""); color(cap, fg); size(cap, 24); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
say(cap, "a parallel beam into a real, fast single lens…", 0.5);
draw(ls.rays, 2.2); // the rays sketch through the glass
wait(0.3);
par {
run(ls, 6); // the sensor plane sweeps the axis
seq {
say(cap, "a sensor hunts for focus — watch the spot size", 0.3);
wait(2.4);
say(cap, "the outer rays focus SHORT — it never reaches a point", 0.3);
wait(2.0);
say(cap, "that blur is spherical aberration (a doublet/triplet fixes it)", 0.3);
}
}
wait(1.2);
ray-fan
Reading an aberration: rayfan plots each ray’s error at focus against where it entered the
lens. A flat line is a perfect lens — the singlet’s cubic S-CURVE is textbook spherical
aberration (the edges bend too much), which a doublet flattens (rayfan).
// ============================================================================
// ray-fan.manic — the signature of an aberration (real optics analysis)
// ----------------------------------------------------------------------------
// A ray-fan plot: for each ray, how far off focus does it land (y) against
// where it entered the lens (x, edge → edge)? `rayfan` traces the real
// spherical surfaces. A perfect lens is a flat line at zero; the singlet's
// cubic S-CURVE is textbook spherical aberration — the outer rays bend too
// much. (The doublet/triplet flatten it, drawn to the same scale.)
// ============================================================================
title("Ray-Fan — Reading an Aberration");
canvas("16:9");
rayfan(rf, (640, 340), "singlet");
text(cap, (cx, h - 46), ""); color(cap, fg); size(cap, 24); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
say(cap, "each ray: how far off focus (y) vs where it entered (x)…", 0.5);
draw(rf.curve, 2.2);
wait(0.5);
say(cap, "a flat line would be a perfect lens — this ISN'T flat", 0.3);
wait(2.0);
say(cap, "that S-curve is spherical aberration: the edges bend too much", 0.3);
wait(1.4);
spot-diagram
Lens quality as a picture: spotdiagram plots where a ray bundle actually lands at focus.
A fast single lens smears into a blur disc (the circle of least confusion), while a cemented
doublet collapses to a point — both to the same scale, RMS 4 px → under 1 px (spotdiagram).
// ============================================================================
// spot-diagram.manic — a blur vs a point (lens quality, real optics)
// ----------------------------------------------------------------------------
// Where does a bundle of rays actually LAND at focus? `spotdiagram` traces the
// real spherical surfaces and plots the hits. A fast single lens smears into a
// disc — the circle of least confusion from SPHERICAL ABERRATION — while a
// cemented doublet collapses to a point. The green dot is the ideal (point)
// focus; the RMS read-out is the blur radius. Both drawn to the SAME scale.
// ============================================================================
title("Spot Diagram — Blur vs Point");
canvas("16:9");
spotdiagram(bad, (370, 380), "singlet");
spotdiagram(good, (910, 380), "doublet");
text(cap, (cx, h - 46), ""); color(cap, fg); size(cap, 24); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
say(cap, "where do the rays actually land at focus?", 0.5);
wait(0.4);
say(cap, "a fast SINGLE lens → a blur disc (spherical aberration)", 0.3);
draw(bad.dots, 1.8);
wait(0.6);
say(cap, "a cemented DOUBLET → very nearly a point", 0.3);
draw(good.dots, 1.4);
wait(0.6);
say(cap, "same rays, same scale — RMS 4 px collapses to under 1 px", 0.3);
wait(1.2);
lens-prescription
Type your OWN lens: lenssystem takes a design by name ("plano-convex", "doublet", …)
OR a custom PRESCRIPTION — the designer’s surface table "radius thickness glass [conic] [aperture] | …" — traced through the true surfaces with real Sellmeier glass (lenssystem).
// ============================================================================
// lens-prescription.manic — type your OWN lens (real prescription)
// ----------------------------------------------------------------------------
// `lenssystem` takes a lens two ways: pick a real design by NAME
// ("plano-convex" / "meniscus" / "doublet" / "triplet" / …), or write a CUSTOM
// prescription — the lens designer's surface table — as "radius thickness glass"
// per surface, separated by "|". Here: a cemented crown+flint achromat, traced
// through its actual spherical surfaces with real glass (Sellmeier).
// ============================================================================
title("A Lens, By Prescription");
canvas("16:9");
// surface table: radius thickness glass (| between surfaces; "flat"/"air" allowed)
lenssystem(l, (600, 380), "160 26 bk7 | -140 8 f2 | -420 0 air");
text(cap, (cx, h - 46), ""); color(cap, fg); size(cap, 24); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
say(cap, "a lens written as a prescription: radius · thickness · glass", 0.5);
draw(l.rays, 2.2); // trace the real surfaces
wait(0.4);
par {
run(l, 6); // sweep the sensor to best focus
seq {
say(cap, "crown + flint, cemented — the same table a designer would type", 0.3);
wait(2.6);
say(cap, "traced through the true spherical surfaces (real Sellmeier glass)", 0.3);
wait(2.0);
}
}
wait(1.0);
aspheric-lens
How an ASPHERE kills spherical aberration: a spherical surface can’t focus a wide beam to a
point (a blur, RMS 1.5 px), but reshaping it to the right conic — one constant in the
prescription — collapses every ray to a point (RMS 0.1 px). Two real ray-traced lenses,
spherical vs aspheric, side by side (lenssystem conic + spotdiagram).
// ============================================================================
// aspheric-lens.manic — how an asphere kills spherical aberration
// ----------------------------------------------------------------------------
// A prescription surface has a CONIC constant. A plain spherical lens can't
// focus a wide beam to a point — the edges bend too much (spherical aberration,
// a blur). Reshape that surface into the right ellipse/hyperbola (a conic ≠ 0)
// and every ray meets at ONE point. Two real ray-traced lenses, side by side:
// same shape, one spherical, one aspheric.
// ============================================================================
title("Aspheric Correction");
canvas("16:9");
spotdiagram(sph, (360, 380), "plano-convex"); // spherical surface → a blur
spotdiagram(asp, (912, 380), "aspheric"); // conic surface (K≈−0.55) → a point
text(cap, (cx, h - 46), ""); color(cap, fg); size(cap, 24); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
say(cap, "same lens shape — where do the rays land at focus?", 0.5);
wait(0.4);
say(cap, "a SPHERICAL surface can't focus the edges — a blur (RMS 1.5 px)", 0.3);
draw(sph.dots, 1.6);
wait(0.6);
say(cap, "reshape it to an ASPHERE (conic surface) — a point (RMS 0.1 px)", 0.3);
draw(asp.dots, 1.4);
wait(0.6);
say(cap, "one conic constant in the prescription — spherical aberration, gone", 0.3);
wait(1.2);
off-axis
The hard test — light 8° OFF the axis. fieldspot traces a full 2-D pupil in 3-D: a single
lens flares into a COMA comet, while a doublet holds the spot near the Airy disk (the
diffraction limit). Real field aberration only a 3-D trace shows (fieldspot).
// ============================================================================
// off-axis.manic — coma & astigmatism, off the axis (3-D ray-traced)
// ----------------------------------------------------------------------------
// On the axis a lens can be sharp — but light coming in at an ANGLE is a harder
// test. `fieldspot` traces a full 2-D pupil in 3-D at a field angle and plots
// where the rays land. A single lens flares into a COMA comet; a doublet holds
// the spot near the Airy disk (the diffraction limit). Same 8° field, side by
// side — real off-axis aberration only a 3-D trace reveals.
// ============================================================================
title("Off-Axis — Coma vs a Corrected Lens");
canvas("16:9");
fieldspot(bad, (350, 380), "singlet", 8); // one element → a coma comet
fieldspot(good, (930, 380), "doublet", 8); // doublet → tight, near diffraction
text(cap, (cx, h - 44), ""); color(cap, fg); size(cap, 24); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
say(cap, "now aim the light 8° OFF the axis — the hard test", 0.5);
wait(0.4);
say(cap, "a single lens smears into a COMA comet (RMS ~7 px)", 0.3);
draw(bad.dots, 1.6);
wait(0.6);
say(cap, "the doublet holds it near the Airy disk — the diffraction limit", 0.3);
draw(good.dots, 1.4);
wait(0.6);
say(cap, "the circle is Airy: shrink the blur to it and you're diffraction-limited", 0.3);
wait(1.2);
Vectors, fields & coordinates
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
vector_field
A magnitude-coloured vector field.
// Vector Field — a grid of arrows sampling a named field, coloured by
// magnitude (cyan → lime → magenta), à la Manim's ArrowVectorField.
//
// manic examples/vector_field.manic
// manic examples/vector_field.manic --record out --fps 60
//
// Named fields: radial, sink, swirl, saddle, wave, shear, uniform, spiral.
title("Vector Field");
canvas(1280, 720);
text(head, (640, 118), ""); display(head); color(head, cyan); size(head, 34); hidden(head);
text(cap, (640, 668), ""); color(cap, dim); size(cap, 22);
// two fields, revealed in turn
arrowfield(swirl, (640, 384), 520, 250, swirl, 15);
untraced(swirl);
arrowfield(rad, (640, 384), 520, 250, radial, 15);
untraced(rad); hidden(rad);
show(head, 0.4);
say(head, "swirl");
say(cap, "a rotational field: (-y, x)");
draw(swirl, 1.2);
wait(1.0);
section("Radial");
say(head, "radial");
say(cap, "an outward source: (x, y) — arrows grow with distance");
par { fade(swirl, 0.5); show(rad, 0.01); }
draw(rad, 1.2);
wait(1.2);
coordinates
Axes, planes, number lines, polar & complex planes.
// Coordinate Systems — a guided tour of manic's four coordinate frames:
// Axes (ticks + labels), NumberPlane, PolarPlane, and ComplexPlane. Each frame
// fades in, holds, then clears before the next — one centre, four lenses.
//
// manic examples/coordinates.manic
// manic examples/coordinates.manic --record out --fps 60
title("Coordinate Systems");
canvas(1280, 720);
text(head, (640, 120), "four ways to draw a plane");
display(head); color(head, cyan); size(head, 28); hidden(head);
text(cap, (640, 640), ""); color(cap, dim); size(cap, 24);
// --- the four systems, all centred; each starts hidden ---
axes(ax, (640, 384), 540, 210, 45); // + tick marks and integer labels
plot(wave, (640, 384), 45, 45, sin, 7); // y = sin(x) drawn on the axes
color(wave, magenta); untraced(wave); hidden(ax);
plane(pl, (640, 384), 560, 230, 56); hidden(pl);
polarplane(pp, (640, 384), 230, 5, 16); hidden(pp);
complexplane(cp, (640, 384), 560, 230, 56); hidden(cp);
// --- 1. Axes ---
show(head, 0.5);
section("Axes");
say(cap, "a numbered cross — tick marks every unit");
show(ax, 0.7);
say(cap, "plot y = sin(x) on it");
draw(wave, 1.1);
wait(0.9);
par { fade(ax, 0.4); fade(wave, 0.4); }
// --- 2. NumberPlane ---
section("Number Plane");
say(cap, "a full cartesian grid");
show(pl, 0.7);
wait(1.0);
fade(pl, 0.4);
// --- 3. PolarPlane ---
section("Polar Plane");
say(cap, "concentric rings and radial spokes — angle and radius");
show(pp, 0.7);
wait(1.0);
fade(pp, 0.4);
// --- 4. ComplexPlane ---
section("Complex Plane");
say(cap, "the same grid, read as real and imaginary parts");
show(cp, 0.7);
wait(1.4);
pie
A pie chart built from sectors.
// Equal Slices — a circle cut into equal *sectors* (real filled pieces, not
// just lines) with the math-kit `pie(id, center, r, n)` builtin. Each slice is
// addressable as p0 … p5, so we can trace them on, then pull two out.
//
// manic examples/pie.manic
// manic examples/pie.manic --record out --fps 60
title("Equal Slices");
canvas(1280, 720);
// six equal sectors centred at (560, 400), radius 230 → p0 … p5, tag `p`
pie(p, (560, 400), 230, 6);
untraced(p0); untraced(p1); untraced(p2);
untraced(p3); untraced(p4); untraced(p5);
text(head, (560, 120), "six equal slices");
display(head); color(head, cyan); size(head, 38); hidden(head);
text(cap, (560, 690), ""); color(cap, dim); size(cap, 22);
// --- cut the circle equally, one slice at a time ---
show(head, 0.5);
say(cap, "cut the circle into six equal sectors");
stagger(0.12) {
draw(p0, 0.4);
draw(p1, 0.4);
draw(p2, 0.4);
draw(p3, 0.4);
draw(p4, 0.4);
draw(p5, 0.4);
}
wait(0.6);
// --- each sector is a real piece: pull two out and recolour them ---
say(cap, "each sector is a real piece — pull two out");
par {
move(p0, (621, 435), 0.6, overshoot);
move(p3, (499, 365), 0.6, overshoot);
recolor(p0, magenta, 0.5);
recolor(p3, lime, 0.5);
}
wait(1.0);
Geometry (olympiad)
Every construction is live — the derived points recompute as the inputs move.
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
equilateral
Euclid I.1 — an equilateral triangle from two circles.
// Equilateral Triangle — Euclid, Elements Book I, Proposition 1. Given a segment
// AB: draw the circle centred at A through B and the circle centred at B through
// A; where they cross is the apex of an equilateral triangle. Every side then
// equals AB. It's a fully DYNAMIC construction — drag B at the end and the
// circles, the intersection, and the triangle all stay correct.
//
// New geo builtins: circle2 (circle by centre + a point on it) and circlecircle
// (the two intersection points of two circles).
//
// Note: each compass circle has radius |AB|, so keep A and B close enough that
// the circles fit the canvas (here |AB| ~ 200 px).
//
// manic examples/equilateral.manic
title("Equilateral Triangle");
canvas("16:9");
text(head, (cx, 96), "Euclid I.1 -- two circles give an equilateral triangle");
color(head, cyan); size(head, 24); hidden(head);
point(A, (540, 470), "A");
point(B, (740, 470), "B");
segment(ab, A, B); color(ab, fg); stroke(ab, 3); untraced(ab);
// the two compass circles (each of radius AB)
circle2(cA, A, B); color(cA, dim); stroke(cA, 1.5); untraced(cA);
circle2(cB, B, A); color(cB, dim); stroke(cB, 1.5); untraced(cB);
// where they meet: C0 (below AB) and C1 (above AB) — take the apex above
circlecircle(C, A, B, B, A);
hidden(C0); hidden(C1);
label(C1, "C", (16, -16)); color(C1.label, lime);
segment(ac, A, C1); color(ac, lime); stroke(ac, 3); untraced(ac);
segment(bc, B, C1); color(bc, lime); stroke(bc, 3); untraced(bc);
// --- construct it ---
show(head, 0.5);
show(A, 0.3); show(B, 0.3);
draw(ab, 0.6);
section("Two circles");
par { draw(cA, 0.9); draw(cB, 0.9); }
show(C1, 0.4);
section("The triangle");
par { draw(ac, 0.7); draw(bc, 0.7); }
par { pulse(ac); pulse(bc); pulse(ab); }
wait(0.6);
// --- drag a vertex: it stays equilateral (circles stay on-canvas) ---
section("Drag a vertex");
move(B, (700, 360), 1.6, smooth);
wait(0.3);
move(B, (660, 560), 1.6, smooth);
wait(0.3);
move(B, (740, 470), 1.2, smooth);
wait(1.0);
triangle
A triangle with its centres and cevians.
// Triangle Geometry — the geo kit (olympiad helpers à la olympiad.asy/cse5.asy).
// Points drive everything: circumcircle, incircle, centroid, circumcenter,
// angle mark, and the foot of an altitude are all *constructed* from A, B, C.
//
// manic examples/triangle.manic
// manic examples/triangle.manic --record out --fps 60
title("Triangle Geometry");
canvas(1280, 720);
text(head, (640, 118), "constructed from three points");
display(head); color(head, cyan); size(head, 32); hidden(head);
text(cap, (640, 668), ""); color(cap, dim); size(cap, 22);
// the three free points
point(A, (380, 560), "A");
point(B, (900, 560), "B");
point(C, (640, 190), "C");
hidden(A); hidden(B); hidden(C);
// sides
segment(ab, A, B); segment(bc, B, C); segment(ca, C, A);
untraced(ab); untraced(bc); untraced(ca);
// constructions
circumcircle(cc, A, B, C); untraced(cc);
circumcenter(O, A, B, C); hidden(O);
incircle(ic, A, B, C); untraced(ic);
centroid(G, A, B, C); hidden(G);
anglemark(angC, A, C, B); untraced(angC);
foot(F, C, A, B); hidden(F);
segment(alt, C, F); untraced(alt);
// --- script ---
show(head, 0.5);
say(cap, "three points make a triangle");
par { show(A); show(B); show(C); }
par { draw(ab, 0.5); draw(bc, 0.5); draw(ca, 0.5); }
draw(angC, 0.4);
wait(0.5);
section("Circumcircle");
say(cap, "the unique circle through all three vertices");
par { show(O); draw(cc, 0.9); }
wait(0.6);
section("Incircle & Centroid");
say(cap, "incircle (tangent to all sides) and centroid");
par { draw(ic, 0.9); show(G); }
wait(0.6);
section("Altitude");
say(cap, "drop a perpendicular from C to AB — its foot F");
par { show(F); draw(alt, 0.6); }
flash(F, magenta);
wait(1.0);
// the payoff: constructions are dynamic — drag a vertex and everything
// (circumcircle, incircle, centroid, foot, angle mark, sides) recomputes.
section("Drag a vertex");
say(cap, "move C — every construction follows");
move(C, (430, 230), 1.2, smooth);
move(C, (850, 210), 1.2, smooth);
move(C, (640, 190), 1.0, smooth);
say(cap, "and drag A");
move(A, (300, 520), 0.9, smooth);
move(A, (380, 560), 0.8, smooth);
wait(1.2);
incircle_tangents
The incircle and its tangent points.
// The Incircle & Contact Triangle — the incircle touches each side at the foot
// of the perpendicular from the incenter, and each radius meets the side at a
// right angle. The three touch points form the contact triangle.
//
// manic examples/incircle_tangents.manic
// manic examples/incircle_tangents.manic --record out --fps 60
title("The Incircle");
canvas(1280, 720);
text(head, (640, 120), "tangent to all three sides");
display(head); color(head, cyan); size(head, 28); hidden(head);
text(cap, (640, 668), ""); color(cap, dim); size(cap, 22);
point(A, (300, 560), "A");
point(B, (1000, 560), "B");
point(C, (640, 160), "C");
hidden(A); hidden(B); hidden(C);
segment(ab, A, B); segment(bc, B, C); segment(ca, C, A);
untraced(ab); untraced(bc); untraced(ca);
incenter(I, A, B, C); color(I, cyan); label(I, "I", (16, -14)); hidden(I);
incircle(ic, A, B, C); untraced(ic);
// touch points = feet of perpendiculars from I to each side
foot(tBC, I, B, C); foot(tCA, I, C, A); foot(tAB, I, A, B);
color(tBC, magenta); color(tCA, magenta); color(tAB, magenta);
hidden(tBC); hidden(tCA); hidden(tAB);
// radii to the touch points, with right-angle marks
segment(rBC, I, tBC); segment(rCA, I, tCA); segment(rAB, I, tAB);
color(rBC, lime); color(rCA, lime); color(rAB, lime);
untraced(rBC); untraced(rCA); untraced(rAB);
rightangle(qBC, I, tBC, B); rightangle(qCA, I, tCA, C); rightangle(qAB, I, tAB, A);
untraced(qBC); untraced(qCA); untraced(qAB);
// the contact triangle
segment(k1, tBC, tCA); segment(k2, tCA, tAB); segment(k3, tAB, tBC);
untraced(k1); untraced(k2); untraced(k3);
show(head, 0.5);
say(cap, "a triangle and its incentre I");
par { show(A); show(B); show(C); }
par { draw(ab, 0.5); draw(bc, 0.5); draw(ca, 0.5); }
show(I, 0.3);
wait(0.3);
section("Inscribed circle");
say(cap, "the incircle touches each side once");
draw(ic, 1.0);
stagger(0.15) { show(tBC); show(tCA); show(tAB); }
wait(0.3);
section("Radii ⟂ sides");
say(cap, "each radius meets its side at a right angle");
par { draw(rBC, 0.5); draw(rCA, 0.5); draw(rAB, 0.5); }
par { draw(qBC, 0.4); draw(qCA, 0.4); draw(qAB, 0.4); }
wait(0.4);
section("Contact triangle");
say(cap, "the three touch points form the contact triangle");
par { draw(k1, 0.5); draw(k2, 0.5); draw(k3, 0.5); }
wait(1.2);
tangents
Tangent lines from a point to a circle.
// Tangent Lines — the two tangents from an external point P to a circle, and
// the theorem that each tangent is perpendicular to the radius at its touch
// point. Everything is a DYNAMIC construction: move P and the touch points,
// tangent lines, radii, and right-angle marks all recompute live.
//
// New geo builtins: circle2 (circle by centre + a point on it), tangent
// (touch points from an external point), plus segment/rightangle tracking them.
//
// manic examples/tangents.manic
title("Tangent Lines");
canvas("16:9");
text(head, (cx, 96), "two tangents from a point -- each meets the radius at 90 degrees");
color(head, cyan); size(head, 24); hidden(head);
point(O, (520, 400), "O");
point(A, (520, 200)); // a point on the circle -> radius 200
point(P, (940, 380), "P");
hidden(A); // A just defines the radius; don't show it
circle2(circ, O, A); color(circ, dim); stroke(circ, 2); untraced(circ);
// the two touch points t0 / t1, and the tangent lines to them
tangent(t, P, O, A);
segment(l0, P, t0); color(l0, cyan); stroke(l0, 3); untraced(l0);
segment(l1, P, t1); color(l1, cyan); stroke(l1, 3); untraced(l1);
// radius to each touch point + the right-angle marks
segment(r0, O, t0); color(r0, dim); untraced(r0);
segment(r1, O, t1); color(r1, dim); untraced(r1);
rightangle(ra0, O, t0, P); color(ra0, lime); hidden(ra0);
rightangle(ra1, O, t1, P); color(ra1, lime); hidden(ra1);
// --- reveal ---
show(head, 0.5);
draw(circ, 0.8);
show(O, 0.3); show(P, 0.3);
section("The tangents");
par { draw(l0, 0.7); draw(l1, 0.7); }
show(t0, 0.3); show(t1, 0.3);
section("Radius meets tangent");
par { draw(r0, 0.5); draw(r1, 0.5); }
par { show(ra0, 0.4); show(ra1, 0.4); }
wait(0.6);
// --- prove it's dynamic: move P, everything tracks ---
section("Move the point");
move(P, (820, 230), 1.6, smooth);
wait(0.4);
move(P, (980, 470), 1.6, smooth);
wait(0.8);
move(P, (940, 380), 1.2, smooth);
wait(1.0);
orthocenter
The orthocentre from the three altitudes.
// Altitudes & Orthocenter — the three altitudes of a triangle meet at one
// point, the orthocenter H. Each altitude drops perpendicular to a side.
// Dynamic: drag a vertex and the altitudes still concur.
//
// manic examples/orthocenter.manic
// manic examples/orthocenter.manic --record out --fps 60
title("Altitudes & Orthocenter");
canvas(1280, 720);
text(head, (640, 120), "the three altitudes concur");
display(head); color(head, cyan); size(head, 28); hidden(head);
text(cap, (640, 668), ""); color(cap, dim); size(cap, 22);
point(A, (330, 540), "A");
point(B, (980, 560), "B");
point(C, (700, 190), "C");
hidden(A); hidden(B); hidden(C);
segment(ab, A, B); segment(bc, B, C); segment(ca, C, A);
untraced(ab); untraced(bc); untraced(ca);
// feet of the three altitudes
foot(fA, A, B, C); foot(fB, B, C, A); foot(fC, C, A, B);
color(fA, magenta); color(fB, magenta); color(fC, magenta);
hidden(fA); hidden(fB); hidden(fC);
// the altitudes themselves
segment(hA, A, fA); segment(hB, B, fB); segment(hC, C, fC);
color(hA, lime); color(hB, lime); color(hC, lime);
untraced(hA); untraced(hB); untraced(hC);
rightangle(qA, A, fA, B); rightangle(qB, B, fB, C); rightangle(qC, C, fC, A);
untraced(qA); untraced(qB); untraced(qC);
orthocenter(H, A, B, C); color(H, cyan); label(H, "H", (16, -14)); hidden(H);
show(head, 0.5);
say(cap, "a triangle");
par { show(A); show(B); show(C); }
par { draw(ab, 0.5); draw(bc, 0.5); draw(ca, 0.5); }
wait(0.3);
section("Drop the altitudes");
say(cap, "from each vertex, perpendicular to the opposite side");
seq {
par { draw(hA, 0.5); draw(qA, 0.4); show(fA); }
par { draw(hB, 0.5); draw(qB, 0.4); show(fB); }
par { draw(hC, 0.5); draw(qC, 0.4); show(fC); }
}
wait(0.3);
section("Orthocenter");
say(cap, "all three meet at the orthocenter H");
show(H, 0.4);
flash(H, magenta);
wait(0.6);
section("Drag a vertex");
say(cap, "move C — the altitudes still concur");
move(C, (520, 230), 1.2, smooth);
move(C, (820, 250), 1.2, smooth);
move(C, (700, 190), 1.0, smooth);
wait(1.0);
euler_line
The Euler line through centroid, circumcentre, orthocentre.
// The Euler Line — in any triangle, the circumcenter O, centroid G, and
// orthocenter H are collinear (and OG : GH = 1 : 2). Constructions are
// dynamic: drag C and the three centres stay on one line.
//
// manic examples/euler_line.manic
// manic examples/euler_line.manic --record out --fps 60
title("The Euler Line");
canvas(1280, 720);
text(head, (640, 120), "circumcenter, centroid, orthocenter — collinear");
display(head); color(head, cyan); size(head, 26); hidden(head);
text(cap, (640, 668), ""); color(cap, dim); size(cap, 22);
point(A, (300, 560), "A");
point(B, (1000, 540), "B");
point(C, (560, 190), "C");
hidden(A); hidden(B); hidden(C);
segment(ab, A, B); segment(bc, B, C); segment(ca, C, A);
untraced(ab); untraced(bc); untraced(ca);
circumcircle(cc, A, B, C); untraced(cc);
circumcenter(O, A, B, C); color(O, magenta); label(O, "O", (18, -14)); hidden(O);
centroid(G, A, B, C); color(G, lime); label(G, "G", (18, -14)); hidden(G);
orthocenter(H, A, B, C); color(H, cyan); label(H, "H", (-30, -14)); hidden(H);
segment(euler, O, H); color(euler, magenta); stroke(euler, 3); untraced(euler);
show(head, 0.5);
say(cap, "any triangle, with its circumcircle");
par { show(A); show(B); show(C); }
par { draw(ab, 0.5); draw(bc, 0.5); draw(ca, 0.5); }
draw(cc, 0.9);
wait(0.4);
section("Three centres");
say(cap, "circumcenter O, centroid G, orthocenter H");
stagger(0.3) { show(O); show(G); show(H); }
wait(0.4);
section("The Euler line");
say(cap, "they always lie on a single line");
draw(euler, 0.9);
wait(0.6);
section("Drag a vertex");
say(cap, "move C — O, G, H stay collinear");
move(C, (770, 220), 1.2, smooth);
move(C, (420, 250), 1.2, smooth);
move(C, (560, 190), 1.0, smooth);
wait(1.0);
nine_point
The nine-point circle.
// The Nine-Point Circle — one circle through the three side-midpoints AND the
// three altitude feet. (It's the circumcircle of the medial triangle.)
// Dynamic: drag C and the circle still catches all six points.
//
// manic examples/nine_point.manic
// manic examples/nine_point.manic --record out --fps 60
title("The Nine-Point Circle");
canvas(1280, 720);
text(head, (640, 120), "three midpoints + three feet, one circle");
display(head); color(head, cyan); size(head, 26); hidden(head);
text(cap, (640, 668), ""); color(cap, dim); size(cap, 22);
point(A, (320, 560), "A");
point(B, (1000, 560), "B");
point(C, (620, 175), "C");
hidden(A); hidden(B); hidden(C);
segment(ab, A, B); segment(bc, B, C); segment(ca, C, A);
untraced(ab); untraced(bc); untraced(ca);
// side midpoints
midpoint(mAB, A, B); midpoint(mBC, B, C); midpoint(mCA, C, A);
color(mAB, lime); color(mBC, lime); color(mCA, lime);
hidden(mAB); hidden(mBC); hidden(mCA);
// altitude feet
foot(fA, A, B, C); foot(fB, B, C, A); foot(fC, C, A, B);
color(fA, magenta); color(fB, magenta); color(fC, magenta);
hidden(fA); hidden(fB); hidden(fC);
// the nine-point circle = circumcircle of the medial triangle
circumcircle(npc, mAB, mBC, mCA); outline(npc, cyan); untraced(npc);
show(head, 0.5);
say(cap, "start with a triangle");
par { show(A); show(B); show(C); }
par { draw(ab, 0.5); draw(bc, 0.5); draw(ca, 0.5); }
wait(0.3);
section("Six points");
say(cap, "the three side-midpoints (lime)");
stagger(0.2) { show(mAB); show(mBC); show(mCA); }
say(cap, "and the three altitude feet (magenta)");
stagger(0.2) { show(fA); show(fB); show(fC); }
wait(0.4);
section("One circle");
say(cap, "a single circle passes through all six");
draw(npc, 1.0);
wait(0.6);
section("Drag a vertex");
say(cap, "move C — the circle still catches all six");
move(C, (820, 220), 1.3, smooth);
move(C, (440, 240), 1.3, smooth);
move(C, (620, 175), 1.0, smooth);
wait(1.0);
conics
Ellipse, parabola, hyperbola.
// The Conic Sections — the three curves you get by slicing a cone: the ellipse,
// the parabola, and the hyperbola. Each is a geo-kit primitive; they reveal one
// at a time with the defining property.
//
// manic examples/conics.manic
// manic examples/conics.manic --template blueprint
title("The Conic Sections");
canvas("16:9");
text(head, (cx, 80), "three curves from slicing a cone");
display(head); color(head, cyan); size(head, 26); hidden(head);
text(cap, (cx, 662), ""); color(cap, dim); size(cap, 23);
// --- ellipse (left) ---
ellipse(el, (300, 400), 165, 100); color(el, cyan); stroke(el, 3); untraced(el);
text(ell, (300, 250), "ellipse"); color(ell, cyan); size(ell, 26); hidden(ell);
// --- parabola (centre) ---
parabola(pa, (660, 540), 150, 270); color(pa, lime); stroke(pa, 3); untraced(pa);
text(pal, (660, 235), "parabola"); color(pal, lime); size(pal, 26); hidden(pal);
// --- hyperbola (right) — two branches, tagged `hy` ---
hyperbola(hy, (1010, 400), 55, 120); color(hy, magenta); stroke(hy, 3); untraced(hy);
text(hyl, (1010, 205), "hyperbola"); color(hyl, magenta); size(hyl, 26); hidden(hyl);
// --- reveal ---
show(head, 0.5);
section("Ellipse");
say(cap, "ellipse -- the sum of distances to two foci stays constant");
draw(el, 0.9);
show(ell, 0.4);
wait(0.5);
section("Parabola");
say(cap, "parabola -- every point is equidistant from a focus and a line");
draw(pa, 0.9);
show(pal, 0.4);
wait(0.5);
section("Hyperbola");
say(cap, "hyperbola -- two branches; the difference of distances stays constant");
draw(hy, 0.9);
show(hyl, 0.4);
wait(1.4);
Transforms & morphing
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
transforms
Apply a 2x2 matrix (ApplyMatrix) to a group.
// Transforms — the "animate anything" showcase.
// Named verbs (rotate, spin, scale, move) plus the general `to(id, prop, value)`
// escape hatch, composed with par / seq / stagger.
//
// manic examples/transforms.manic
// manic examples/transforms.manic --record out --fps 60 --crt
title("Transforms");
canvas(1280, 720);
// cast
rect(box, (330, 400), 150, 150); outline(box, cyan);
label(box, "rotate");
rect(dia, (650, 400), 140, 140); outline(dia, magenta);
label(dia, "to");
circle(orb, (980, 400), 62); outline(orb, lime);
label(orb, "spin");
dot(p, (150, 620), 12);
text(cap, (640, 662), ""); color(cap, dim); size(cap, 22);
text(head, (640, 120), "animate anything");
display(head); color(head, cyan); size(head, 40); hidden(head);
// script
show(head, 0.5);
section("Named verbs");
say(cap, "rotate to an absolute angle");
rotate(box, 45, 0.7, overshoot);
say(cap, "spin by a relative angle, twice");
seq {
spin(orb, 180, 0.6);
spin(orb, 180, 0.6);
}
wait(0.4);
section("Animate anything");
say(cap, "to(id, property, value) reaches any property");
par {
to(dia, angle, 45, 0.6, smooth);
to(dia, scale, 1.4, 0.6);
to(dia, color, lime, 0.6);
}
wait(0.4);
say(cap, "compose freely with par / seq / stagger");
stagger(0.12) {
to(box, opacity, 0.4, 0.4);
to(dia, opacity, 0.4, 0.4);
to(orb, opacity, 0.4, 0.4);
}
to(p, x, 1130, 1.0, overshoot);
to(p, color, magenta, 0.4);
wait(1.0);
transform_copy
Duplicate an entity, then transform the copy.
// Copy + Winding Morph — two of the Transform family niceties. `copy(c, a)`
// duplicates a shape so the original stays while the copy transforms; `morph`
// with a spin angle winds the blend (Manim's Clockwise / Counterclockwise
// Transform). Left copy morphs clockwise, right copy counter-clockwise.
//
// manic examples/transform_copy.manic
title("Copy + Winding Morph");
canvas("16:9");
text(head, (cx, 96), "a copy morphs while the original stays -- one CW, one CCW");
color(head, cyan); size(head, 23); hidden(head);
// left: original circle (dim) + a cyan copy that morphs into a square, clockwise
circle(o1, (400, 380), 120); color(o1, dim); stroke(o1, 3); hidden(o1);
rect(t1, (400, 380), 220, 220); hidden(t1);
copy(c1, o1); color(c1, cyan); stroke(c1, 5); glow(c1, 1.6); hidden(c1);
morph(c1, t1, 200); // +200 deg = clockwise wind
// right: same idea, counter-clockwise into a triangle-ish (use another square)
circle(o2, (900, 380), 120); color(o2, dim); stroke(o2, 3); hidden(o2);
rect(t2, (900, 380), 220, 220); hidden(t2);
copy(c2, o2); color(c2, magenta); stroke(c2, 5); glow(c2, 1.6); hidden(c2);
morph(c2, t2, -200); // -200 deg = counter-clockwise
// --- script ---
show(head, 0.5);
par { show(o1, 0.4); show(o2, 0.4); show(c1, 0.4); show(c2, 0.4); }
wait(0.5);
section("Morph the copies");
par { to(c1, morph, 1, 1.8, smooth); to(c2, morph, 1, 1.8, smooth); }
wait(0.9);
par { to(c1, morph, 0, 1.8, smooth); to(c2, morph, 0, 1.8, smooth); }
wait(1.2);
morph
A sampled-point shape morph from A to B.
// Shape Morph — a circle's outline blends smoothly into a square's and back
// (Manim's Transform). `morph(a, b)` samples both outlines to the same number
// of points; `to(a, morph, t)` interpolates between them (t = 0 is `a`'s shape,
// 1 is `b`'s).
//
// manic examples/morph.manic
title("Shape Morph");
canvas("16:9");
text(head, (cx, 110), "a circle becomes a square -- and back");
display(head); color(head, cyan); size(head, 26); hidden(head);
circle(sh, (cx, cy), 150); color(sh, cyan); stroke(sh, 5); glow(sh, 1.6); hidden(sh);
rect(target, (cx, cy), 290, 290); hidden(target); // defines the square outline
morph(sh, target); // set sh up to morph into it
// --- script ---
show(head, 0.5);
show(sh, 0.6);
wait(0.5);
section("Morph");
to(sh, morph, 1, 1.6, smooth); // circle -> square
wait(0.7);
to(sh, morph, 0, 1.6, smooth); // square -> circle
wait(0.7);
to(sh, morph, 1, 1.1, overshoot); // and back, with a bounce
wait(1.4);
Text & UI
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
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), "path", w, h) — a real file drawn
into the scene and 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), "path", [w], [h])` loads a real 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 path
// draws a crossed placeholder box instead of failing.
//
// (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), "assets/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);
Generative & recursive
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
gun-shot
A pure-imagination SCENE — no physics kit, just storytelling: a gun fires, the camera
flies along with the bullet (cam/zoom), a block drops in out of nowhere, and BOOM —
flash/shake/pulse + a for-loop spark burst. manic as a movie language.
// ============================================================================
// gun-shot.manic — a scene, not a lesson. No physics kit, just imagination.
// ----------------------------------------------------------------------------
// A gun fires · the camera races along with the bullet · a block drops in out
// of nowhere · BOOM. Built entirely from base manic — shapes, `move`, `cam`/
// `zoom` to fly the camera, `flash`/`shake`/`pulse`, and a `for`-loop spark
// burst. This is manic as a storytelling language: dream a scene, write it.
// ============================================================================
title("Gun Shot");
canvas("16:9");
// ---- the world (wide — the camera pans across it) ----
line(ground, (-300, 560), (2400, 560)); color(ground, dim); stroke(ground, 4);
// the gun: barrel + body + grip
rect(barrel, (250, 470), 96, 22); color(barrel, dim); filled(barrel);
rect(body, (206, 478), 52, 42); color(body, dim); filled(body);
polygon(grip, (186, 500), (220, 500), (212, 554), (180, 550), dim);
// the bullet at the muzzle, and a muzzle flash — both waiting
circle(bullet, (302, 470), 12); color(bullet, gold); glow(bullet, 2.2); hidden(bullet);
circle(mflash, (312, 470), 30); color(mflash, gold); glow(mflash, 3.5); hidden(mflash);
// the block — waiting above, off-screen, to drop in ahead
rect(block, (1750, 250), 130, 130); color(block, cyan); filled(block); glow(block, 1.4); hidden(block);
text(boom, (1750, 320), "BOOM!"); size(boom, 96); color(boom, magenta); bold(boom); glow(boom, 2.5); display(boom); hidden(boom);
// a ring of impact sparks around the block (revealed at the hit)
for i in 0..14 {
let ang = i * tau / 14.0;
line(spark{i}, (1700, 470), (1700 + 160*cos(ang), 470 + 160*sin(ang)));
color(spark{i}, gold); stroke(spark{i}, 5); glow(spark{i}, 2); untraced(spark{i}); tag(spark{i}, sparks);
}
// a caption pinned to the screen (rides along through the camera move)
text(cap, (cx, h - 56), ""); color(cap, fg); size(cap, 26); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
cam((440, 380), 0.4, smooth); // frame the gun
say(cap, "steady…", 0.4);
wait(0.6);
// FIRE!
say(cap, "FIRE!", 0.2);
par { show(mflash, 0.06); pulse(mflash); show(bullet, 0.08); }
fade(mflash, 0.3);
// the bullet races off — the camera flies with it — and mid-flight, out of
// nowhere, a block slams down into its path
par {
move(bullet, (1690, 470), 2.6, smooth);
cam((1560, 380), 2.6, smooth);
zoom(1.15, 2.6, smooth);
seq {
wait(1.5);
say(cap, "…wait — what's THAT?!", 0.3);
show(block, 0.1);
move(block, (1750, 470), 0.4, bounce);
}
}
// BOOM — impact
say(cap, "BOOM!", 0.15);
par {
flash(block, gold);
shake(block, 0.5);
zoom(1.5, 0.15);
show(boom, 0.12); pulse(boom);
draw(sparks, 0.35);
}
wait(0.5);
// settle — pull back
par {
fade(sparks, 0.5);
fade(boom, 0.6);
fade(bullet, 0.4);
zoom(1.0, 0.9, smooth);
}
say(cap, "…scene.", 0.4);
wait(0.8);
fractal_tree
One recursive def, drawn to depth 12.
// Fractal Tree — a recursive `def` macro draws a branching tree. Each branch
// splits into two shorter branches at a fixed angle; `if depth > 0` is the base
// case that stops the recursion. Branches are keyed by a binary-heap index
// (k -> 2k, 2k+1) so every segment gets a unique id, hued and thinned by depth.
//
// Showcases the Phase-2 language layer: `def`, recursion, `if`, comparisons.
//
// manic examples/fractal_tree.manic
// manic examples/fractal_tree.manic --record out --fps 60
title("Fractal Tree");
canvas(1280, 720);
text(head, (640, 92), "one recursive rule, drawn to depth 9");
display(head); color(head, cyan); size(head, 26); hidden(head);
// draw a branch, then recurse into two children (unless we've bottomed out)
def branch(k, x, y, ang, len, depth) {
// stop at the base depth OR once a branch is too short to see — so even a
// large `depth` self-limits (the tree is bounded by branch length)
if depth > 0 && len > 2 {
let x2 = x + len * cos(ang);
let y2 = y - len * sin(ang); // screen y grows downward
line(seg{k}, (x, y), (x2, y2));
stroke(seg{k}, 1 + depth * 0.8);
hue(seg{k}, 120 + depth * 15); // trunk bluish -> tips green
untraced(seg{k}); tag(seg{k}, tree);
branch(2*k, x2, y2, ang + 0.42, len * 0.72, depth - 1);
branch(2*k + 1, x2, y2, ang - 0.42, len * 0.72, depth - 1);
}
}
// grow from the bottom centre, pointing up (angle pi/2)
branch(1, 640, 700, 1.5708, 150, 20);
// --- script ---
show(head, 0.5);
draw(tree, 1.8);
wait(1.6);
particles-flow
Contained ambient motion and live curved connections in four generic words: particles,
wander, link, and flow. The ids supply the domain meaning.
// Generic contained motion: the ids give the dots their meaning.
// The same four words work for bubbles, dust, stars, data, or molecules.
title("Three bodies, one relation");
canvas("9:16");
// No template call: mono is the polished black-and-white default.
circle(A, (540, 390), 105);
circle(B, (260, 760), 105);
circle(C, (820, 760), 105);
stroke(A, 5); stroke(B, 5); stroke(C, 5);
particles(insideA, A, 24, 5, 7);
particles(insideB, B, 24, 5, 17);
particles(insideC, C, 24, 5, 27);
equation(labelA, (540, 390), `A`, 64);
equation(labelB, (260, 760), `B`, 64);
equation(labelC, (820, 760), `C`, 64);
link(ab, A, B, -48);
link(bc, B, C, -56);
link(ac, A, C, 48);
stroke(ab, 5); stroke(bc, 5); stroke(ac, 5);
untraced(ab); untraced(bc); untraced(ac);
equation(relAB, (310, 445), `A\sim B`, 34);
equation(relBC, (540, 690), `B\sim C`, 34);
equation(relAC, (770, 445), `A\sim C`, 34);
color(relAB, dim); color(relBC, dim); color(relAC, dim);
hidden(A); hidden(B); hidden(C);
hidden(labelA); hidden(labelB); hidden(labelC);
hidden(insideA); hidden(insideB); hidden(insideC);
hidden(relAB); hidden(relBC); hidden(relAC);
par {
wander(insideA, 9);
wander(insideB, 9);
wander(insideC, 9);
seq {
par { show(A, 0.35); show(labelA, 0.35); show(insideA, 0.45); }
wait(0.25);
par { show(B, 0.35); show(labelB, 0.35); show(insideB, 0.45); }
show(relAB, 0.25);
par { draw(ab, 0.75); recolor(relAB, fg, 0.75); }
flow(ab, 0.9);
par { show(C, 0.35); show(labelC, 0.35); show(insideC, 0.45); }
show(relBC, 0.25);
par { draw(bc, 0.75); recolor(relBC, fg, 0.75); }
flow(bc, 0.9);
show(relAC, 0.25);
par { draw(ac, 0.75); recolor(relAC, fg, 0.75); }
par { flow(ab, 1.1); flow(bc, 1.1); flow(ac, 1.1); }
wait(0.55);
}
}
hue_wave
An animated hue wave across a grid.
// Hue Wave — a ring of dots, each with its own starting hue, all advancing
// their hue at the same rate so the rainbow *rotates* around the ring. Shows
// off `hue` as an animatable track: `to(id, hue, degrees)` cycles colour over
// time (unlike `recolor`, it travels around the colour wheel, not through grey).
//
// manic examples/hue_wave.manic
// manic examples/hue_wave.manic --record out --fps 60
title("Hue Wave");
canvas(1280, 720);
text(head, (640, 110), "an animated hue track — colour that cycles");
display(head); color(head, cyan); size(head, 26); hidden(head);
let n = 36; let cx = 640; let cy = 400; let r = 210;
// a ring of dots, rainbow-coloured by angle
for i in 0..n {
let a = tau * i / n;
dot(d{i}, (cx + r*cos(a), cy + r*sin(a)), 18);
hue(d{i}, 360 * i / n);
glow(d{i}, 1.4);
tag(d{i}, ring);
}
// --- script ---
show(head, 0.5);
// spin the whole rainbow: every dot advances its hue by 720 deg (two full
// cycles) over 6s, in parallel — the pattern rotates around the ring
par {
for i in 0..n {
to(d{i}, hue, 360*i/n + 720, 6.0, linear);
}
}
hill_run
A little scene animated with the language layer.
// Uphill / Downhill — a rate x time = distance word problem.
// "Up a hill at 4 mph, back down the same path at 6 mph, round trip = 1 hour.
// Total distance?" Answer: one-way d = 2.4 mi, round trip = 4.8 mi.
//
// The distance is SOLVED in-language: d = 1 / (1/4 + 1/6) = 2.4, total = 2d.
// The runner climbs slowly, descends faster (3s vs 2s ~ the real 0.6h : 0.4h),
// then the equation is derived and the answer counts up on a live readout.
//
// manic examples/hill_run.manic
// manic examples/hill_run.manic --record out --fps 60
title("Uphill / Downhill");
canvas("16:9");
// --- the numbers, computed the same way you'd reason it out ---
let up = 4; // mph, uphill
let down = 6; // mph, downhill
let d = 1 / (1/up + 1/down); // one-way distance = 2.4 mi (from d/4 + d/6 = 1)
let total = 2 * d; // round trip = 4.8 mi
text(head, (cx, 84), "up at 4 mph, down at 6 mph -- round trip takes 1 hour");
display(head); color(head, cyan); size(head, 24); hidden(head);
text(cap, (cx, 668), ""); color(cap, dim); size(cap, 23);
// --- the hill (a single path, run up then down) ---
line(ground, (150, 560), (700, 560)); color(ground, dim); stroke(ground, 2); untraced(ground);
line(path, (200, 560), (620, 210)); color(path, cyan); stroke(path, 4); untraced(path);
text(flag, (628, 196), "top"); color(flag, dim); size(flag, 18); hidden(flag);
dot(runner, (200, 560), 16); color(runner, lime); glow(runner, 1.7); hidden(runner);
text(uplbl, (300, 470), "4 mph"); color(uplbl, cyan); size(uplbl, 24); hidden(uplbl);
text(downlbl, (520, 320), "6 mph"); color(downlbl, magenta); size(downlbl, 24); hidden(downlbl);
// --- the derivation, on the right ---
text(e1, (960, 230), "time = distance / rate"); color(e1, dim); size(e1, 22); hidden(e1);
text(e2, (960, 300), "d/4 + d/6 = 1"); display(e2); color(e2, fg); size(e2, 30); hidden(e2);
text(e3, (960, 360), "5d/12 = 1 -> d = 2.4"); display(e3); color(e3, cyan); size(e3, 26); hidden(e3);
counter(ans, (960, 450), 0, 1, "round trip = 2d = ", " mi"); display(ans); color(ans, lime); size(ans, 30); hidden(ans);
// --- script ---
show(head, 0.5);
say(cap, "an athlete runs up a hill, then back down the same path");
par { draw(ground, 0.5); draw(path, 0.7); }
par { show(flag, 0.3); show(runner, 0.3); }
wait(0.3);
section("Up the hill");
say(cap, "uphill at 4 mph -- the slow leg");
show(uplbl, 0.3);
move(runner, (620, 210), 3.0, linear);
section("Back down");
say(cap, "downhill at 6 mph -- faster, so less time");
show(downlbl, 0.3);
move(runner, (200, 560), 2.0, linear);
wait(0.3);
section("Set up the equation");
say(cap, "let d = the one-way distance; time = distance / rate");
show(e1, 0.4);
show(e2, 0.4);
say(cap, "combine the fractions: 5d/12 = 1, so d = 2.4 miles");
show(e3, 0.5);
flash(e3, lime);
section("Total distance");
say(cap, "the round trip is 2d");
show(ans, 0.3);
to(ans, value, total, 1.4);
pulse(ans);
wait(1.6);
walk
An articulated stick figure walking down a road — legs swing, arms counter-swing, the body
bobs — built purely from the language layer (let + for + trig), no character rig.
title("A Generic Figure Walking Down the Road");
canvas("16:9");
let groundY = cy + 160;
let startX = cx - 420;
let stepDist = 15;
let swingAmp = 26;
let bobAmp = 10;
// ================= road =================
rect(road, (0, groundY), w, h - groundY);
color(road, dim);
filled(road);
untraced(road);
line(roadLine, (0, groundY + 40), (w, groundY + 40));
color(roadLine, panel);
stroke(roadLine, 2);
untraced(roadLine);
for i in 0..12 {
rect(dash{i}, (i*120 - 40, groundY + 36), 50, 8);
color(dash{i}, fg);
filled(dash{i});
untraced(dash{i});
}
// ================= stick figure as points + reflowing segments =================
point(neck, (startX, groundY - 118));
point(hip, (startX, groundY - 10));
point(handL, (startX - 30, groundY - 40));
point(handR, (startX + 30, groundY - 40));
point(footL, (startX - 30, groundY + 100));
point(footR, (startX + 30, groundY + 100));
hidden(neck);
hidden(hip);
hidden(handL);
hidden(handR);
hidden(footL);
hidden(footR);
circle(head, (startX, groundY - 140), 22);
color(head, fg);
outlined(head);
stroke(head, 3);
untraced(head);
segment(spine, neck, hip);
segment(armL, neck, handL);
segment(armR, neck, handR);
segment(legL, hip, footL);
segment(legR, hip, footR);
color(spine, fg);
color(armL, cyan);
color(armR, cyan);
color(legL, gold);
color(legR, gold);
stroke(spine, 4);
stroke(armL, 4);
stroke(armR, 4);
stroke(legL, 4);
stroke(legR, 4);
untraced(spine);
untraced(armL);
untraced(armR);
untraced(legL);
untraced(legR);
// ================= text =================
text(head_label, (cx, 55), "A Generic Figure Walking Down the Road");
color(head_label, cyan);
hidden(head_label);
text(caption, (cx, h - 30), "");
color(caption, dim);
hidden(caption);
// ================= script =================
show(head_label, 0.6);
wait(0.3);
par {
draw(road, 0.5);
draw(roadLine, 0.5);
stagger(0.03) {
for i in 0..12 {
draw(dash{i}, 0.1);
}
}
}
par {
show(neck, 0.01); show(hip, 0.01);
show(handL, 0.01); show(handR, 0.01);
show(footL, 0.01); show(footR, 0.01);
draw(head, 0.4);
draw(spine, 0.3);
draw(armL, 0.3);
draw(armR, 0.3);
draw(legL, 0.3);
draw(legR, 0.3);
}
wait(0.3);
show(caption, 0.4);
say(caption, "Camera pulls back to see the whole road");
par {
cam((cx, cy), 1.0, smooth);
zoom(0.85, 1.0, smooth);
}
wait(0.3);
// --- walk cycle: phase steps by 90 deg so sin actually alternates ---
for i in 0..28 {
let baseX = startX + i*stepDist;
let phase = i*90;
let swing = swingAmp*sin(phase*pi/180);
let legLift = bobAmp*abs(sin(phase*pi/180));
par {
move(neck, (baseX, groundY - 118 - legLift*0.4), 0.15, smooth);
move(hip, (baseX, groundY - 10), 0.15, smooth);
move(handL, (baseX - swing, groundY - 40), 0.15, smooth);
move(handR, (baseX + swing, groundY - 40), 0.15, smooth);
move(footL, (baseX + swing, groundY + 100 - legLift), 0.15, smooth);
move(footR, (baseX - swing, groundY + 100 - legLift), 0.15, smooth);
move(head, (baseX, groundY - 140 - legLift*0.4), 0.15, smooth);
}
}
wait(0.2);
say(caption, "Camera zooms in as the figure gets close");
par {
cam((startX + 420, groundY - 80), 1.4, smooth);
zoom(2.2, 1.4, smooth);
}
wait(0.4);
say(caption, "A close-up look, then pulling back out");
par {
cam((cx, cy), 1.2, smooth);
zoom(1, 1.2, smooth);
}
wait(0.4);
show(caption, 0.3);
say(caption, "A generic stick figure walking -- no specific person depicted");
two_person_walk
Two figures walk toward each other, MEET in the middle, shake hands, then continue past — a little choreographed scene from loops and arithmetic alone (the language layer as animation).
title("Two Figures Meet, Shake Hands, and Continue Walking");
canvas("16:9");
let groundY = cy + 160;
let startX1 = cx - 420;
let startX2 = cx + 420;
let stepDist = 15;
let swingAmp = 26;
let bobAmp = 10;
let meetX = cx;
let endX1 = cx + 420;
let endX2 = cx - 420;
// ================= road =================
rect(road, (0, groundY), w, h - groundY);
color(road, dim);
filled(road);
untraced(road);
line(roadLine, (0, groundY + 40), (w, groundY + 40));
color(roadLine, panel);
stroke(roadLine, 2);
untraced(roadLine);
for i in 0..14 {
rect(dash{i}, (i*120 - 40, groundY + 36), 50, 8);
color(dash{i}, fg);
filled(dash{i});
untraced(dash{i});
}
// ================= figure 1 (walks left -> right) =================
point(neck1, (startX1, groundY - 118));
point(hip1, (startX1, groundY - 10));
point(handL1, (startX1 - 30, groundY - 40));
point(handR1, (startX1 + 30, groundY - 40));
point(footL1, (startX1 - 30, groundY + 100));
point(footR1, (startX1 + 30, groundY + 100));
hidden(neck1); hidden(hip1);
hidden(handL1); hidden(handR1);
hidden(footL1); hidden(footR1);
circle(head1, (startX1, groundY - 140), 22);
color(head1, fg);
outlined(head1);
stroke(head1, 3);
untraced(head1);
segment(spine1, neck1, hip1);
segment(armL1, neck1, handL1);
segment(armR1, neck1, handR1);
segment(legL1, hip1, footL1);
segment(legR1, hip1, footR1);
color(spine1, fg);
color(armL1, cyan);
color(armR1, cyan);
color(legL1, gold);
color(legR1, gold);
stroke(spine1, 4); stroke(armL1, 4); stroke(armR1, 4);
stroke(legL1, 4); stroke(legR1, 4);
untraced(spine1); untraced(armL1); untraced(armR1);
untraced(legL1); untraced(legR1);
// ================= figure 2 (walks right -> left, mirrored) =================
point(neck2, (startX2, groundY - 118));
point(hip2, (startX2, groundY - 10));
point(handL2, (startX2 - 30, groundY - 40));
point(handR2, (startX2 + 30, groundY - 40));
point(footL2, (startX2 - 30, groundY + 100));
point(footR2, (startX2 + 30, groundY + 100));
hidden(neck2); hidden(hip2);
hidden(handL2); hidden(handR2);
hidden(footL2); hidden(footR2);
circle(head2, (startX2, groundY - 140), 22);
color(head2, fg);
outlined(head2);
stroke(head2, 3);
untraced(head2);
segment(spine2, neck2, hip2);
segment(armL2, neck2, handL2);
segment(armR2, neck2, handR2);
segment(legL2, hip2, footL2);
segment(legR2, hip2, footR2);
color(spine2, fg);
color(armL2, magenta);
color(armR2, magenta);
color(legL2, lime);
color(legR2, lime);
stroke(spine2, 4); stroke(armL2, 4); stroke(armR2, 4);
stroke(legL2, 4); stroke(legR2, 4);
untraced(spine2); untraced(armL2); untraced(armR2);
untraced(legL2); untraced(legR2);
// ================= text =================
text(head_label, (cx, 55), "Two Figures Meet, Shake Hands, and Continue Walking");
color(head_label, cyan);
hidden(head_label);
text(caption, (cx, h - 30), "");
color(caption, dim);
hidden(caption);
// ================= script =================
show(head_label, 0.6);
wait(0.3);
par {
draw(road, 0.5);
draw(roadLine, 0.5);
stagger(0.03) {
for i in 0..14 {
draw(dash{i}, 0.1);
}
}
}
par {
show(neck1, 0.01); show(hip1, 0.01);
show(handL1, 0.01); show(handR1, 0.01);
show(footL1, 0.01); show(footR1, 0.01);
draw(head1, 0.4);
draw(spine1, 0.3);
draw(armL1, 0.3);
draw(armR1, 0.3);
draw(legL1, 0.3);
draw(legR1, 0.3);
show(neck2, 0.01); show(hip2, 0.01);
show(handL2, 0.01); show(handR2, 0.01);
show(footL2, 0.01); show(footR2, 0.01);
draw(head2, 0.4);
draw(spine2, 0.3);
draw(armL2, 0.3);
draw(armR2, 0.3);
draw(legL2, 0.3);
draw(legR2, 0.3);
}
wait(0.3);
show(caption, 0.4);
say(caption, "Camera pulls back to see the whole road");
par {
cam((cx, cy), 1.0, smooth);
zoom(0.85, 1.0, smooth);
}
wait(0.3);
// --- walk cycle: both figures walk toward each other, meeting at meetX ---
for i in 0..24 {
let baseX1 = startX1 + i*stepDist;
let baseX2 = startX2 - i*stepDist;
let phase = i*90;
let swing = swingAmp*sin(phase*pi/180);
let legLift = bobAmp*abs(sin(phase*pi/180));
par {
move(neck1, (baseX1, groundY - 118 - legLift*0.4), 0.15, smooth);
move(hip1, (baseX1, groundY - 10), 0.15, smooth);
move(handL1, (baseX1 - swing, groundY - 40), 0.15, smooth);
move(handR1, (baseX1 + swing, groundY - 40), 0.15, smooth);
move(footL1, (baseX1 + swing, groundY + 100 - legLift), 0.15, smooth);
move(footR1, (baseX1 - swing, groundY + 100 - legLift), 0.15, smooth);
move(head1, (baseX1, groundY - 140 - legLift*0.4), 0.15, smooth);
move(neck2, (baseX2, groundY - 118 - legLift*0.4), 0.15, smooth);
move(hip2, (baseX2, groundY - 10), 0.15, smooth);
move(handL2, (baseX2 - swing, groundY - 40), 0.15, smooth);
move(handR2, (baseX2 + swing, groundY - 40), 0.15, smooth);
move(footL2, (baseX2 + swing, groundY + 100 - legLift), 0.15, smooth);
move(footR2, (baseX2 - swing, groundY + 100 - legLift), 0.15, smooth);
move(head2, (baseX2, groundY - 140 - legLift*0.4), 0.15, smooth);
}
}
wait(0.2);
say(caption, "They arrive face to face");
par {
cam((meetX, groundY - 80), 1.2, smooth);
zoom(1.8, 1.2, smooth);
}
// settle into a standing pose facing each other
par {
move(neck1, (meetX - 40, groundY - 118), 0.3, smooth);
move(hip1, (meetX - 40, groundY - 10), 0.3, smooth);
move(footL1, (meetX - 60, groundY + 100), 0.3, smooth);
move(footR1, (meetX - 20, groundY + 100), 0.3, smooth);
move(head1, (meetX - 40, groundY - 140), 0.3, smooth);
move(handL1, (meetX - 70, groundY - 40), 0.3, smooth);
move(neck2, (meetX + 40, groundY - 118), 0.3, smooth);
move(hip2, (meetX + 40, groundY - 10), 0.3, smooth);
move(footL2, (meetX + 60, groundY + 100), 0.3, smooth);
move(footR2, (meetX + 20, groundY + 100), 0.3, smooth);
move(head2, (meetX + 40, groundY - 140), 0.3, smooth);
move(handR2, (meetX + 70, groundY - 40), 0.3, smooth);
}
wait(0.3);
say(caption, "Reaching out to shake hands");
par {
move(handR1, (meetX - 5, groundY - 55), 0.4, smooth);
move(handL2, (meetX + 5, groundY - 55), 0.4, smooth);
}
wait(0.2);
par {
move(handR1, (meetX, groundY - 55), 0.25, smooth);
move(handL2, (meetX, groundY - 55), 0.25, smooth);
}
wait(0.2);
say(caption, "Shaking hands");
for i in 0..4 {
par {
move(handR1, (meetX, groundY - 65), 0.12, smooth);
move(handL2, (meetX, groundY - 65), 0.12, smooth);
}
par {
move(handR1, (meetX, groundY - 48), 0.12, smooth);
move(handL2, (meetX, groundY - 48), 0.12, smooth);
}
}
par {
move(handR1, (meetX, groundY - 55), 0.15, smooth);
move(handL2, (meetX, groundY - 55), 0.15, smooth);
}
wait(0.3);
flash(handR1, gold);
flash(handL2, gold);
wait(0.3);
say(caption, "Letting go and continuing on their separate ways");
par {
cam((cx, cy), 1.2, smooth);
zoom(1, 1.2, smooth);
}
// release hands back to normal swing position before resuming walk
par {
move(handR1, (meetX - 40 + 30, groundY - 40), 0.25, smooth);
move(handL2, (meetX + 40 - 30, groundY - 40), 0.25, smooth);
}
wait(0.2);
// --- resume walk cycle: figure1 continues toward endX1, figure2 toward endX2 ---
for i in 0..24 {
let baseX1 = (meetX - 40) + i*stepDist;
let baseX2 = (meetX + 40) - i*stepDist;
let phase = i*90;
let swing = swingAmp*sin(phase*pi/180);
let legLift = bobAmp*abs(sin(phase*pi/180));
par {
move(neck1, (baseX1, groundY - 118 - legLift*0.4), 0.15, smooth);
move(hip1, (baseX1, groundY - 10), 0.15, smooth);
move(handL1, (baseX1 - swing, groundY - 40), 0.15, smooth);
move(handR1, (baseX1 + swing, groundY - 40), 0.15, smooth);
move(footL1, (baseX1 + swing, groundY + 100 - legLift), 0.15, smooth);
move(footR1, (baseX1 - swing, groundY + 100 - legLift), 0.15, smooth);
move(head1, (baseX1, groundY - 140 - legLift*0.4), 0.15, smooth);
move(neck2, (baseX2, groundY - 118 - legLift*0.4), 0.15, smooth);
move(hip2, (baseX2, groundY - 10), 0.15, smooth);
move(handL2, (baseX2 - swing, groundY - 40), 0.15, smooth);
move(handR2, (baseX2 + swing, groundY - 40), 0.15, smooth);
move(footL2, (baseX2 + swing, groundY + 100 - legLift), 0.15, smooth);
move(footR2, (baseX2 - swing, groundY + 100 - legLift), 0.15, smooth);
move(head2, (baseX2, groundY - 140 - legLift*0.4), 0.15, smooth);
}
}
wait(0.3);
say(caption, "Two generic stick figures -- no specific persons depicted");
par {
cam((cx, cy), 1.0, smooth);
zoom(0.85, 1.0, smooth);
}
wait(0.4);
equal_cuts
A circle halved again and again (pizza cuts).
// Equal Cuts — a circle sliced into equal pieces, repeatedly doubled:
// 2 → 4 → 8 equal wedges. Each "cut" is a diameter traced across the circle
// at an equal angle. (manic has no sector primitive yet, so cuts are lines.)
//
// manic examples/equal_cuts.manic
// manic examples/equal_cuts.manic --record out --fps 60
title("Equal Cuts");
canvas(1280, 720);
// the circle to divide, centred at (640, 400) with radius 240
circle(pie, (640, 400), 240); stroke(pie, 3);
// four diameters through the centre at 0, 45, 90, 135 degrees.
// revealed in stages, they cut the circle into 2, then 4, then 8 equal pieces.
line(c0, (400, 400), (880, 400)); color(c0, magenta); stroke(c0, 3); untraced(c0); // 0
line(c1, (640, 160), (640, 640)); color(c1, magenta); stroke(c1, 3); untraced(c1); // 90
line(c2, (470, 230), (810, 570)); color(c2, lime); stroke(c2, 3); untraced(c2); // 135
line(c3, (810, 230), (470, 570)); color(c3, lime); stroke(c3, 3); untraced(c3); // 45
text(cap, (640, 690), ""); color(cap, dim); size(cap, 22);
text(count, (1040, 170), ""); color(count, cyan); size(count, 34); bold(count);
// --- cut in half ---
say(cap, "cut the circle in half");
draw(c0, 0.6);
say(count, "2 pieces");
wait(0.5);
// --- cut again: four equal pieces ---
say(cap, "cut again at a right angle — four equal pieces");
draw(c1, 0.6);
say(count, "4 pieces");
wait(0.5);
// --- and again: eight equal pieces ---
say(cap, "and again on both diagonals — eight equal pieces");
par {
draw(c2, 0.6);
draw(c3, 0.6);
}
say(count, "8 pieces");
pulse(pie);
wait(1.2);
archimedes_pi
Bounding pi with inscribed / circumscribed polygons.
// Approximating pi — Archimedes' method (c. 250 BC): inscribe a regular polygon
// in a circle and its perimeter closes in on the circumference. For an n-gon in
// a circle of radius R the perimeter is 2R * n*sin(pi/n), so pi ~ n*sin(pi/n),
// which -> pi as n grows. We sweep n = 6, 24, 96 (Archimedes' own 96-gon) and
// zoom in to see the last polygon nearly kiss the circle.
//
// Uses: a `for` loop per polygon, computed estimates, a live counter, and the
// camera (cam + zoom).
//
// manic examples/archimedes_pi.manic
// manic examples/archimedes_pi.manic --record out --fps 60
title("Approximating pi");
canvas("16:9");
let ox = 440; let oy = 400; let R = 240; // circle centre + radius
// the estimates, computed in-language
let e6 = 6 * sin(pi/6); // 3.000
let e24 = 24 * sin(pi/24); // 3.133
let e96 = 96 * sin(pi/96); // 3.141
text(head, (640, 78), "Archimedes: straight lines closing in on a circle");
display(head); color(head, cyan); size(head, 25); hidden(head);
text(cap, (640, 675), ""); color(cap, dim); size(cap, 22);
// the true circle (the target)
circle(circ, (ox, oy), R); outlined(circ); outline(circ, dim); stroke(circ, 2); untraced(circ);
// live pi readout
counter(est, (990, 330), 0, 3, "pi ~ ", ""); display(est); color(est, lime); size(est, 40); hidden(est);
text(truth, (990, 395), "true pi = 3.14159..."); color(truth, dim); size(truth, 20); hidden(truth);
// --- hexagon: n = 6 (magenta) ---
let n = 6;
for i in 0..n {
let a0 = tau*i/n; let a1 = tau*(i+1)/n;
line(h{i}, (ox + R*cos(a0), oy + R*sin(a0)), (ox + R*cos(a1), oy + R*sin(a1)));
color(h{i}, magenta); stroke(h{i}, 3); untraced(h{i}); tag(h{i}, p6);
}
// --- 24-gon (cyan) ---
let n = 24;
for i in 0..n {
let a0 = tau*i/n; let a1 = tau*(i+1)/n;
line(g{i}, (ox + R*cos(a0), oy + R*sin(a0)), (ox + R*cos(a1), oy + R*sin(a1)));
color(g{i}, cyan); stroke(g{i}, 3); untraced(g{i}); tag(g{i}, p24);
}
// --- 96-gon (lime), Archimedes' own ---
let n = 96;
for i in 0..n {
let a0 = tau*i/n; let a1 = tau*(i+1)/n;
line(k{i}, (ox + R*cos(a0), oy + R*sin(a0)), (ox + R*cos(a1), oy + R*sin(a1)));
color(k{i}, lime); stroke(k{i}, 2); untraced(k{i}); tag(k{i}, p96);
}
// --- script ---
show(head, 0.5);
say(cap, "how close can straight lines get to a curve?");
draw(circ, 1.0);
par { show(est, 0.3); show(truth, 0.3); }
wait(0.4);
section("6 sides");
say(cap, "start with a hexagon inside the circle");
draw(p6, 0.8);
to(est, value, e6, 1.0);
wait(0.7);
fade(p6, 0.4);
section("24 sides");
say(cap, "more sides hug the circle more tightly");
draw(p24, 1.0);
to(est, value, e24, 1.0);
wait(0.7);
fade(p24, 0.4);
section("96 sides");
say(cap, "Archimedes went to 96 sides -- around 250 BC");
draw(p96, 1.2);
to(est, value, e96, 1.0);
pulse(est);
wait(0.7);
section("Almost a circle");
say(cap, "zoom in: the polygon edge and the arc nearly touch");
par { cam((ox, oy - R), 1.5, smooth); zoom(5, 1.5, smooth); }
wait(1.4);
par { cam((cx, cy), 1.0, smooth); zoom(1, 1.0, smooth); }
wait(0.8);
pieday
A Pi Day card: a rainbow petal-flower built from a loop of circles, radial rays,
the digits of π, and the definition circumference / diameter = pi.
title("Pi Day");
canvas("16:9");
let r = h*0.23;
let centerY = cy + 25;
let n = 64;
let petalsN = 12;
text(head, (cx, 70), "Happy Pi Day");
text(bigPi, (cx, centerY - 8), "pi");
text(digits, (cx, h - 92), "3.1415926535897932384626433832795028841971...");
text(formula, (cx, h - 50), "circumference / diameter = pi");
size(head, 40);
size(bigPi, 112);
size(digits, 24);
size(formula, 26);
bold(head);
bold(bigPi);
color(head, magenta);
color(bigPi, gold);
color(digits, cyan);
color(formula, lime);
hidden(head);
hidden(bigPi);
hidden(digits);
hidden(formula);
circle(mainCircle, (cx, centerY), r);
line(diameter, (cx - r, centerY), (cx + r, centerY));
text(diamLab, (cx, centerY + 34), "diameter");
text(circLab, (cx, centerY - r - 30), "circumference");
stroke(mainCircle, 5);
stroke(diameter, 3);
color(mainCircle, cyan);
color(diameter, lime);
color(diamLab, lime);
color(circLab, cyan);
size(diamLab, 22);
size(circLab, 22);
hidden(diamLab);
hidden(circLab);
untraced(mainCircle);
untraced(diameter);
for i in 0..petalsN {
circle(petal{i}, (cx + 0.54*r*cos(tau*i/petalsN), centerY + 0.54*r*sin(tau*i/petalsN)), 0.46r);
stroke(petal{i}, 2);
hue(petal{i}, 360i/petalsN);
opacity(petal{i}, 0.34);
untraced(petal{i});
tag(petal{i}, petals);
}
for i in 0..n {
dot(spark{i}, (cx + 1.23*r*cos(tau*i/n), centerY + 1.23*r*sin(tau*i/n)), 4);
hue(spark{i}, 360*i/n);
hidden(spark{i});
tag(spark{i}, sparks);
}
for i in 0..24 {
line(ray{i}, (cx + 1.02*r*cos(tau*i/24), centerY + 1.02*r*sin(tau*i/24)), (cx + 1.18*r*cos(tau*i/24), centerY + 1.18*r*sin(tau*i/24)));
stroke(ray{i}, 3);
hue(ray{i}, 360*i/24);
untraced(ray{i});
tag(ray{i}, rays);
}
dot(centerDot, (cx, centerY), 6);
color(centerDot, gold);
hidden(centerDot);
show(head, 0.7);
par {
draw(petals, 1.4);
draw(mainCircle, 1.2);
}
par {
draw(diameter, 0.8);
show(centerDot, 0.4);
show(diamLab, 0.5);
show(circLab, 0.5);
}
par {
show(bigPi, 0.9);
draw(rays, 0.9);
}
stagger(0.018) {
for i in 0..n {
show(spark{i}, 0.25);
}
}
par {
show(digits, 0.7);
show(formula, 0.7);
}
pulse(bigPi, 0.8);
pulse(mainCircle, 0.8);
par {
spin(petals, 18, 3.0, smooth);
spin(sparks, -35, 3.0, smooth);
}
wait(1.2);
Creator formats — responsive social video
Creator Kit v2 turns a question, answers, media and a reusable creator profile into a polished timed social clip. The same source adapts to 9:16, 4:5, 1:1 and 16:9 with platform-safe regions. studio plus a balanced ring is the restrained default; timing controls the beat independently from timerstyle, whose native ring, bar, number, segments, ticks and pulse looks remain crisp at every size. Explicit layout, density, labels, motion, safe and accent controls customise the rest. Responsive native social icons, optional explanations and final end cards share the same brand profile.
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
perfect-reel
The gold-path production starter: phone-safe composition, real LaTeX, exact pacing, professional mono styling, creator identity, timeline markers and a focused end card.
// A production-ready Creator v2 Reel: safe layout, exact pacing, one focal
// equation, restrained motion, reusable branding, markers, and an end card.
title("Perfect Reel — Angle Ratio");
canvas("9:16");
template("mono");
creator(me, "@anish2good name=Proof_Daily tagline=Think_then_prove yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=signature cta=Save_and_share safe=reels");
quiz(q, "Triangle angles: 2:3:4. What is the largest?",
"studio layout=media-first reveal=fade density=comfortable motion=calm safe=reels accent=cyan");
option(q, `$60^\circ$`);
option(q, `$80^\circ$`, correct);
option(q, `$90^\circ$`);
option(q, `$100^\circ$`);
explain(q, `The parts total $9$, so $x=20^\circ$ and $4x=80^\circ$.`, "Angle sum");
equation(prompt, (cx, 650), `2x+3x+4x=180^\circ`, 70);
color(prompt, cyan);
figure(prompt);
hidden(prompt);
// Exact 10.5-second quiz beat. The timer look can change independently.
timing(q, "calm ask=1.1 options=1 think=5.5 reveal=0.75 hold=2.15 stagger=0.06");
timerstyle(q, "look=ring position=below number=inside direction=drain size=medium thickness=1.1 color=cyan track=dim label=THINK font=mono finish=pulse");
socials(me);
endcard(me, "cta=Save_and_share");
mark("hook");
par {
run(q);
show(prompt, 0.5);
}
mark("endcard");
par {
fade(q.parts, 0.45);
fade(prompt, 0.45);
fade(me.footer, 0.45);
}
show(me.endcard, 0.6);
wait(1.4);
creator-v2-options-socials
The asset-free v2.4 review scene: collision-safe question hierarchy, professional A/B/C/D cards, uniform correct-state spacing, and native YouTube/X/web identity lockups.
// Creator v2.4 review scene: question hierarchy, A/B/C/D answer cards,
// uniform correct-state spacing, and asset-free native social identities.
title("Creator v2.4 — Options + Socials");
canvas("9:16");
template("mono");
creator(me, "@anish2good name=Olympiad_Minute yt=zarigatongy x=@anish2good web=8gwifi.org/manic footer=social accent=cyan safe=reels");
quiz(q, `In cyclic quadrilateral $ABCD$, $\angle A=68^\circ$. What is $\angle C$?`,
"studio labels=letters layout=auto density=comfortable motion=calm safe=reels accent=cyan");
option(q, `$68^\circ$`);
option(q, `$102^\circ$`);
option(q, `$112^\circ$`, correct);
option(q, `$122^\circ$`);
timing(q, "balanced ask=1.2 options=1 think=4.8 reveal=0.8 hold=2.2 stagger=0.06");
timerstyle(q, "look=bar position=below number=outside direction=drain color=cyan track=dim label=THINK finish=pulse");
socials(me);
run(q);
creator-v2
The complete v2 core: responsive studio quiz, optics media, width-aware answer cards, a signature creator footer, optional explanation and a branded final end card.
// Creator Kit v2 core — responsive studio quiz + reusable brand system.
// Change the canvas to (1080,1350), (1080,1080), or (1280,720): the same
// question, media, choices, timer and footer reflow into the available space.
title("Creator Kit v2 — Optics Quiz");
canvas("9:16");
template("shorts");
creator(me, "@anish2good name=Optics_Lab tagline=Physics_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=signature cta=Follow_for_more safe=shorts");
quiz(q, "Which glass separates blue and red light more?",
"studio layout=media-first reveal=rise timer=bar density=comfortable motion=studio safe=shorts accent=cyan");
option(q, "BK7 crown glass");
option(q, "SF11 flint glass", correct);
option(q, "Both equally");
option(q, "Neither material");
explain(q, "SF11 has stronger wavelength dispersion.", "Sellmeier model");
// Any static kit figure or tagged group can occupy the responsive media region.
prism(p, (540, 670), "sf11");
figure(p);
socials(me);
endcard(me, "cta=Follow_for_more");
par {
run(q, 12);
run(p, 12);
}
par {
fade(q.parts, 0.45);
fade(p, 0.45);
fade(me.footer, 0.45);
}
show(me.endcard, 0.6);
wait(1.4);
creator-v2-timing
Timing v2 in a portrait quiz: exact ask/options/think/reveal/hold phases, LaTeX media, and a segmented timer whose presentation can change without changing the choreography.
// Creator Timing v2 — independent choreography and timer presentation.
// `timing` controls WHEN each beat happens; `timerstyle` controls how the
// countdown looks. With explicit phases, call run(q) without a second duration.
title("Creator Timing v2");
canvas("9:16");
template("shorts");
creator(me, "@anish2good name=Number_Lab tagline=Pause_predict_prove yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=magenta secondary=cyan footer=signature cta=Try_the_next_one safe=shorts");
quiz(q, `Which number is equal to $2^5$?`,
"studio layout=media-first reveal=rise density=comfortable motion=calm safe=shorts accent=magenta");
option(q, `$10$`);
option(q, `$16$`);
option(q, `$32$`, correct);
option(q, `$64$`);
explain(q, `$2^5=2\times2\times2\times2\times2=32$.`);
equation(eq, (cx, 650), `2^5 = 2\times2\times2\times2\times2`, 72);
color(eq, magenta); figure(eq); hidden(eq);
// Absolute phases: 1.2s question, 1.1s answers, 6s thinking, 0.8s reveal,
// then a 2.2s answer hold. The total is derived automatically by run(q).
timing(q, "calm ask=1.2 options=1.1 think=6 reveal=0.8 hold=2.2 stagger=0.07");
// A creative segmented timer in the media corner. Change only this line to
// ring/bar/number/ticks/pulse/none without changing the choreography.
timerstyle(q, "look=segments position=below number=outside direction=drain size=large thickness=1.2 color=magenta track=dim label=THINK font=display finish=pulse");
socials(me);
par {
run(q);
show(eq, 0.55);
}
creator-v2-timers
All six native Timing v2 looks—ring, bar, number, segments, ticks and pulse—running side by side. Native shapes keep every look scalable, theme-aware and progress-animatable.
// Timing v2 look gallery — every timer uses the same six-second behaviour.
// All visuals are native manic primitives: scalable, theme-aware and animated.
title("Creator Timing v2 — Native Looks");
canvas("16:9");
template("shorts");
text(head, (cx, 70), "TIMING V2 — CHOOSE THE LOOK");
size(head, 34); bold(head); color(head, fg);
countdown(ring, (220, 245), 6, "ring size=small color=cyan label=RING finish=hold");
countdown(bar, (640, 245), 6, "bar size=small color=magenta number=outside label=BAR finish=hold");
countdown(number, (1060, 245), 6, "number size=large font=display color=gold label=NUMBER finish=hold");
countdown(segments, (220, 520), 6, "segments size=small color=lime label=SEGMENTS finish=hold");
countdown(ticks, (640, 520), 6, "ticks size=small direction=fill color=cyan label=TICKS finish=hold");
countdown(pulse, (1060, 520), 6, "pulse size=small font=display color=magenta label=PULSE finish=hold");
par {
run(ring, 6);
run(bar, 6);
run(number, 6);
run(segments, 6);
run(ticks, 6);
run(pulse, 6);
}
creator-v2-timing-square
A square feed-card variant with a scaled dramatic preset and a filling tick timer, showing that timing and timer placement reflow independently across formats.
// Creator Timing v2 — square preset scaling and responsive timer placement.
title("Creator Timing v2 — Square");
canvas("1:1");
template("shorts");
creator(me, "@anish2good name=Proof_Minute tagline=See_the_pattern yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=gold footer=compact cta=Save_this safe=clean");
quiz(q, `If $x+1/x=3$, find $x^2+1/x^2$`,
"glass layout=grid density=comfortable motion=studio pace=dramatic accent=cyan");
option(q, `$5$`);
option(q, `$7$`, correct);
option(q, `$9$`);
option(q, `$11$`);
explain(q, `Square the given relation: $x^2+2+\frac{1}{x^2}=9$.`);
equation(clue, (cx, cy), `\left(x+\frac1x\right)^2`, 72);
color(clue, cyan); figure(clue);
// The timer fills clockwise below the header. The dramatic preset is scaled
// proportionally to a compact eight-second feed clip by run(q, 8).
timerstyle(q, "look=ticks position=below number=inside direction=fill size=small thickness=1.1 color=cyan track=dim label=SOLVE font=mono finish=flash");
socials(me);
run(q, 8);
creator-v2-olympiad-geometry
An olympiad-level geometry Reel built as pause → predict → prove, with a responsive construction, authored explanation and reusable creator identity.
// Creator Kit v2 — olympiad geometry problem.
//
// Problem: From an external point P, tangents PA and PB touch a circle with
// centre O and radius 5. If OP = 13, find the chord length AB.
//
// The tangent points are computed by the geo kit. Nothing is eyeballed:
// tangent-radius perpendicularity, chord AB, and M = AB ∩ OP remain live.
title("Creator v2 — Olympiad Geometry");
canvas("9:16");
template("shorts");
creator(me, "@anish2good name=Geometry_Lab tagline=See_the_hidden_lemma yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=signature cta=Pause_then_prove safe=shorts");
quiz(q, `From $P$, $PA$ and $PB$ are tangents to circle $(O)$. If $r=5$ and $OP=13$, find $AB$.`,
"studio layout=media-first reveal=rise timer=bar density=compact motion=calm safe=shorts accent=cyan");
option(q, `$\tfrac{60}{13}$`);
option(q, `$10$`);
option(q, `$\tfrac{120}{13}$`, correct);
option(q, `$12$`);
explain(q, `Tangent-radius gives $OA\perp PA$, hence $PA=12$. With $M=AB\cap OP$, $\triangle OMA\sim\triangle OAP$, so $AM/5=12/13$ and $AB=120/13$.`);
// Source geometry: 40 px represents one unit, so r = 5 and OP = 13.
point(O, (430, 560), "O");
point(R, (430, 360));
point(P, (950, 560), "P");
hidden(O); hidden(R); hidden(P);
circle2(circ, O, R);
color(circ, dim); stroke(circ, 3); untraced(circ);
tangent(t, P, O, R);
label(t0, "A", (22, -18));
label(t1, "B", (22, 18));
color(t0, cyan); color(t1, cyan); hidden(t0); hidden(t1);
segment(pa, P, t0); segment(pb, P, t1);
color(pa, cyan); color(pb, cyan); stroke(pa, 4); stroke(pb, 4);
untraced(pa); untraced(pb);
segment(oa, O, t0); segment(ob, O, t1); segment(op, O, P);
color(oa, dim); color(ob, dim); color(op, fg);
stroke(oa, 3); stroke(ob, 3); stroke(op, 3);
untraced(oa); untraced(ob); untraced(op);
segment(chord, t0, t1);
color(chord, magenta); stroke(chord, 5); untraced(chord);
meet(M, t0, t1, O, P);
label(M, "M", (-28, 24));
color(M, lime); hidden(M);
rightangle(raA, O, t0, P); rightangle(raB, O, t1, P);
color(raA, lime); color(raB, lime); hidden(raA); hidden(raB);
text(r5, (390, 455), "5"); color(r5, fg); size(r5, 30); hidden(r5);
text(op13, (690, 595), "13"); color(op13, fg); size(op13, 30); hidden(op13);
// Every source and dependent entity is included so figure() can safely reflow
// the construction into the responsive media region.
tag(O, fig); tag(R, fig); tag(P, fig); tag(circ, fig);
tag(t0, fig); tag(t1, fig); tag(pa, fig); tag(pb, fig);
tag(oa, fig); tag(ob, fig); tag(op, fig); tag(chord, fig);
tag(M, fig); tag(raA, fig); tag(raB, fig); tag(r5, fig); tag(op13, fig);
figure(fig);
socials(me);
endcard(me, "cta=Pause_then_prove");
par {
run(q, 14);
seq {
wait(1.3);
par { show(O, 0.25); show(P, 0.25); }
draw(circ, 0.7);
draw(op, 0.45);
par { draw(pa, 0.7); draw(pb, 0.7); }
par { show(t0, 0.25); show(t1, 0.25); }
par { draw(oa, 0.45); draw(ob, 0.45); }
par { show(raA, 0.3); show(raB, 0.3); }
draw(chord, 0.5);
par { show(M, 0.25); show(r5, 0.25); show(op13, 0.25); }
}
}
par {
fade(q.parts, 0.45);
fade(fig, 0.45);
fade(me.footer, 0.45);
}
show(me.endcard, 0.6);
wait(1.4);
creator-v2-latex-calculus
Portrait Creator v2 with inline and display LaTeX: a calculus question, fitted formula answers and crisp typesetting throughout the timed reveal.
// Creator Kit v2 + LaTeX — portrait calculus lesson.
// Review focus: mixed inline math, a responsive equation figure, explanation,
// branded footer, progress timer, and the Creator v2 end card.
title("Creator v2 — LaTeX Calculus");
canvas("9:16");
template("shorts");
creator(me, "@anish2good name=Calculus_Lab tagline=One_integral_at_a_time yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=signature cta=Save_for_revision safe=shorts");
quiz(q, `Evaluate $\int_0^1 2x\,dx$.`,
"studio layout=media-first reveal=rise timer=bar density=comfortable motion=calm safe=shorts accent=cyan");
option(q, `$0$`);
option(q, `$\tfrac{1}{2}$`);
option(q, `$1$`, correct);
option(q, `$2$`);
explain(q, `The antiderivative is $x^2$, so $1^2-0^2=1$.`, "Fundamental Theorem of Calculus");
equation(eq, (cx, 650), `\int_0^1 2x\,dx = [x^2]_0^1 = 1`, 76);
color(eq, cyan);
figure(eq);
hidden(eq);
socials(me);
endcard(me, "cta=Save_for_revision");
par {
run(q, 12);
show(eq, 0.55);
}
par {
fade(q.parts, 0.45);
fade(eq, 0.45);
fade(me.footer, 0.45);
}
show(me.endcard, 0.6);
wait(1.4);
creator-v2-latex-algebra
Square Creator v2 on a paper surface, checking that algebraic LaTeX and answer cards remain balanced and readable outside the vertical format.
// Creator Kit v2 + LaTeX — square algebra card.
// Review focus: automatic square reflow, paper styling, compact choices,
// inline factor notation, and a clean numeric timer.
title("Creator v2 — LaTeX Algebra");
canvas("square");
template("paper");
creator(me, "@anish2good name=Algebra_Studio tagline=Patterns_before_procedures yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=magenta secondary=cyan footer=compact cta=Try_it_first safe=clean");
quiz(q, `Which expression equals $x^2-5x+6$?`,
"studio layout=auto reveal=fade timer=number density=compact motion=calm safe=clean accent=magenta");
option(q, `$(x-1)(x-6)$`);
option(q, `$(x-2)(x-3)$`, correct);
option(q, `$(x+2)(x+3)$`);
option(q, `$(x-2)(x+3)$`);
explain(q, `The two numbers multiply to $6$ and add to $-5$.`, "Factor pair: -2 and -3");
equation(eq, (cx, cy), `x^2-5x+6`, 78);
color(eq, magenta);
figure(eq);
hidden(eq);
socials(me);
par {
run(q, 10);
show(eq, 0.55);
}
creator-v2-latex-physics
Landscape Creator v2 with a physics equation, proving the same LaTeX quiz system reflows cleanly for widescreen explainers.
// Creator Kit v2 + LaTeX — landscape physics explainer.
// Review focus: automatic widescreen split layout, formula options, equation
// media, branded footer, and a restrained ring timer.
title("Creator v2 — LaTeX Physics");
canvas("16:9");
template("shorts");
creator(me, "@anish2good name=Physics_Notes tagline=Concepts_in_one_frame yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=gold secondary=cyan footer=signature cta=Follow_for_more safe=clean");
quiz(q, `A mass $m$ moves at speed $v$. What is its kinetic energy?`,
"studio layout=auto reveal=fade timer=ring density=compact motion=studio safe=clean accent=gold");
option(q, `$mv$`);
option(q, `$mv^2$`);
option(q, `$\tfrac{1}{2}mv^2$`, correct);
option(q, `$2mv^2$`);
explain(q, `Work changes kinetic energy: $W=\Delta K$.`, "Work–energy theorem");
equation(eq, (cx, cy), `K = \frac{1}{2}mv^2`, 82);
color(eq, gold);
figure(eq);
hidden(eq);
socials(me);
par {
run(q, 10);
show(eq, 0.55);
}
quiz-skins
The quiz Short in a dozen lines: quiz/option/run + a creator/socials footer.
Change the one style word on quiz(...) to switch card SKIN — badge (framed panel +
coloured letter badges), minimal, glass (glowing borders) or plain — and add a
question REVEAL in the same string (e.g. "glass fade"). The correct card lights up with a
green badge + check on reveal; a draining ring counts the timer down.
// A creator quiz Short (9:16). Try the four card SKINS by changing the style
// string on `quiz(...)`: "badge" (default) · "minimal" · "glass" · "plain".
// You can also add a question REVEAL in the same string, order-free, e.g.
// quiz(q, "...", "glass fade") // glass cards + fade-in question
canvas("9:16");
template("shorts");
// a reusable creator profile (set once, drawn by `socials`)
creator(me, "@anish2good yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=magenta");
// the quiz: a question + four answers, one marked `correct`
quiz(q, "What is 7 x 8?", "badge");
option(q, "54");
option(q, "56", correct);
option(q, "48");
option(q, "63");
// play the whole ask -> countdown -> reveal beat over 8 seconds
run(q, 8);
// the footer with the creator's socials
socials(me);
quiz-euler
A quiz Short with an ANIMATED figure: the geo kit constructs the Euler line (the answer),
and figure(...) AUTO-FITS the whole triangle+circumcircle into the zone between the
question header and the answer cards — no coordinate tuning. The question, four cards, the
countdown and the whole ask→countdown→reveal beat are just quiz/option/run.
// ============================================================================
// quiz-euler.manic — the SAME quiz Short, now via the `creator` kit
// ----------------------------------------------------------------------------
// Compare with quiz-geometry.manic (≈60 hand-authored lines): the question,
// four option cards, the countdown, and the whole ask→countdown→reveal beat
// are now `quiz` / `option` / `run`. The creator's footer is `creator`/`socials`.
// Only the figure is hand-drawn (author-supplied — any manic entity/kit).
// ============================================================================
title("Quiz — the Euler Line");
canvas("9:16");
template("paper");
creator(me, "@anish2good yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=magenta");
// --- the quiz: question + four answers (kit handles layout + the beat) ---
quiz(q, "3 triangle centres are always collinear — on WHICH line?");
option(q, "the Euler line", correct);
option(q, "a perpendicular bisector");
option(q, "an angle bisector");
option(q, "a median");
// --- the figure (author-supplied): the geo kit constructs the Euler line ---
point(pA, (335, 760)); point(pB, (745, 745)); point(pC, (540, 470));
hidden(pA); hidden(pB); hidden(pC);
segment(sa, pA, pB); segment(sb, pB, pC); segment(sc, pC, pA);
untraced(sa); untraced(sb); untraced(sc);
circumcircle(cc, pA, pB, pC); color(cc, dim); untraced(cc);
circumcenter(oo, pA, pB, pC); color(oo, magenta); hidden(oo);
centroid(gg, pA, pB, pC); color(gg, lime); hidden(gg);
orthocenter(hh, pA, pB, pC); color(hh, cyan); hidden(hh);
segment(eu, oo, hh); color(eu, fg); stroke(eu, 7); glow(eu, 1.5); untraced(eu);
// tag every figure part, then AUTO-FIT the whole group into the zone between
// the question header and the answer cards — no hand-tuning coordinates.
tag(pA, fig); tag(pB, fig); tag(pC, fig); tag(sa, fig); tag(sb, fig); tag(sc, fig);
tag(cc, fig); tag(oo, fig); tag(gg, fig); tag(hh, fig); tag(eu, fig);
figure(fig, (540, 645), (720, 360));
// ================= THE BEAT =================
socials(me);
par {
run(q, 14); // ask · countdown · reveal — the whole beat
seq { // the figure builds while the question sits
wait(1.6);
par { show(pA, 0.2); show(pB, 0.2); show(pC, 0.2); }
par { draw(sa, 0.5); draw(sb, 0.5); draw(sc, 0.5); }
draw(cc, 0.5);
par { show(oo); show(gg); show(hh); }
draw(eu, 0.9); pulse(eu);
}
}
quiz-geometry
The hand-authored proof behind the kit (≈60 lines from shipped primitives): a question, an
animated geometry figure, four option cards, a countdown and a time-out reveal. Useful to
see what quiz/option/run automate under the hood.
// ============================================================================
// quiz-geometry.manic — a QUIZ SHORT, hand-authored (creator-template proto)
// ----------------------------------------------------------------------------
// The first proof of the "quiz Short" format the `creator` kit will later
// automate (see CAPABILITIES → Creator format templates). Built ENTIRELY from
// shipped primitives on a 9:16 canvas: a typewriter question, an ANIMATED
// geometry figure (the geo kit constructs the Euler line — which IS the answer),
// four option cards, a countdown, a time-out reveal, and a socials footer.
// When the `quiz`/`countdown`/`socials` builtins land, this whole file collapses
// to a few lines — but the beat/layout are proven here first.
// ============================================================================
title("Quiz — the Euler Line");
canvas("9:16"); // portrait 1080×1920 · cx=540 cy=960
template("paper");
// ---- the question (two typewriter lines, top / title-safe) ----
text(q1, (cx, 210), "3 triangle centres are always"); color(q1, fg); size(q1, 44); bold(q1); untraced(q1);
text(q2, (cx, 285), "collinear — on WHICH line?"); color(q2, cyan); size(q2, 44); bold(q2); untraced(q2);
// ---- the animated geometry figure (geo kit) : the Euler line construction ----
// (colours are palette-semantic — fg/cyan/magenta/lime/dim — so they REMAP with
// the template; gold/red/etc. are fixed and would lose contrast on `paper`)
point(A, (335, 760), "A"); point(B, (745, 745), "B"); point(C, (540, 470), "C");
hidden(A); hidden(B); hidden(C);
segment(ab, A, B); segment(bc, B, C); segment(ca, C, A); untraced(ab); untraced(bc); untraced(ca);
circumcircle(cc, A, B, C); color(cc, dim); untraced(cc);
circumcenter(O, A, B, C); color(O, magenta); label(O, "O", (20, -12)); hidden(O);
centroid(G, A, B, C); color(G, lime); label(G, "G", (20, -12)); hidden(G);
orthocenter(H, A, B, C); color(H, cyan); label(H, "H", (-34, -12)); hidden(H);
segment(euler, O, H); color(euler, fg); stroke(euler, 7); glow(euler, 1.5); untraced(euler);
// ---- four option cards, laid out 2×2 (compact — clears the figure) ----
rect(c1, (305, 1010), 428, 120); color(c1, panel); filled(c1); hidden(c1);
rect(c2, (775, 1010), 428, 120); color(c2, panel); filled(c2); hidden(c2);
rect(c3, (305, 1150), 428, 120); color(c3, panel); filled(c3); hidden(c3);
rect(c4, (775, 1150), 428, 120); color(c4, panel); filled(c4); hidden(c4);
text(t1, (305, 1010), "A Euler line"); color(t1, fg); size(t1, 34); hidden(t1);
text(t2, (775, 1010), "B Perp. bisector"); color(t2, fg); size(t2, 34); hidden(t2);
text(t3, (305, 1150), "C Angle bisector"); color(t3, fg); size(t3, 34); hidden(t3);
text(t4, (775, 1150), "D Median"); color(t4, fg); size(t4, 34); hidden(t4);
// ---- countdown ring + digit (palette-semantic → template-safe) ----
circle(ring, (cx, 1400), 62); color(ring, cyan); stroke(ring, 6); outlined(ring); hidden(ring);
text(timer, (cx, 1400), "5"); color(timer, fg); size(timer, 62); bold(timer); hidden(timer);
// ---- socials footer : icons DRAWN from primitives (manic has no raster images),
// so they render on any template; grouped as `foot` to show together ----
line(rule, (150, 1740), (930, 1740)); color(rule, dim); stroke(rule, 2);
// ▶ YouTube chip
rect(yt, (312, 1815), 58, 40); color(yt, magenta); filled(yt);
polygon(ytp, (300, 1802), (300, 1828), (326, 1815), bg);
// ✕ X chip
rect(xb, (404, 1815), 46, 46); color(xb, fg); outlined(xb);
line(x1, (392, 1803), (416, 1827)); color(x1, fg); stroke(x1, 4);
line(x2, (416, 1803), (392, 1827)); color(x2, fg); stroke(x2, 4);
// ◎ Instagram chip
rect(ig, (496, 1815), 46, 46); color(ig, fg); outlined(ig);
circle(igc, (496, 1815), 13); color(igc, fg); outlined(igc);
circle(igd, (509, 1802), 3); color(igd, fg); filled(igd);
// handle + link
text(foot, (740, 1815), "@manic · 8gwifi.org/manic"); color(foot, dim); size(foot, 28);
tag(rule, footer); tag(yt, footer); tag(ytp, footer); tag(xb, footer); tag(x1, footer);
tag(x2, footer); tag(ig, footer); tag(igc, footer); tag(igd, footer); tag(foot, footer);
hidden(footer);
// ================= THE BEAT =================
// 1) ask — typewriter
type(q1, 1.1);
type(q2, 1.0);
wait(0.2);
// 2) the figure constructs itself (animated geometry — the geo kit)
par { show(A, 0.25); show(B, 0.25); show(C, 0.25); }
par { draw(ab, 0.5); draw(bc, 0.5); draw(ca, 0.5); }
draw(cc, 0.6);
par { show(O); show(G); show(H); }
draw(euler, 0.9); pulse(euler); // the three centres line up → the Euler line
wait(0.3);
// 3) options fly in, staggered
seq {
par { show(c1, 0.2); show(t1, 0.2); }
par { show(c2, 0.2); show(t2, 0.2); }
par { show(c3, 0.2); show(t3, 0.2); }
par { show(c4, 0.2); show(t4, 0.2); }
}
show(footer, 0.3);
// 4) countdown 5 → 1
par { show(ring, 0.2); show(timer, 0.2); }
wait(0.8); say(timer, "4", 0.15);
wait(0.8); say(timer, "3", 0.15);
wait(0.8); say(timer, "2", 0.15);
wait(0.8); say(timer, "1", 0.15);
wait(0.8);
// 5) TIME OUT → reveal: card A (the Euler line) glows, the rest dim
say(timer, "0", 0.15); flash(ring, gold);
par {
recolor(c1, lime); flash(c1, gold); pulse(c1);
recolor(t1, void);
fade(c2, 0.5); fade(t2, 0.5);
fade(c3, 0.5); fade(t3, 0.5);
fade(c4, 0.5); fade(t4, 0.5);
flash(euler, gold);
}
wait(1.6);
quiz-geometry-2
A layout stress-test: a different olympiad question with TWO figures side by side (an acute triangle with its circumcentre INSIDE vs an obtuse one with it OUTSIDE), proving the 2×2 options, countdown and footer keep their spacing for richer figure content.
// ============================================================================
// quiz-geometry-2.manic — a second QUIZ SHORT (olympiad geometry)
// ----------------------------------------------------------------------------
// A layout STRESS TEST: same 9:16 quiz scaffold as quiz-geometry.manic, but a
// different olympiad question with TWO figures side-by-side (acute vs obtuse
// triangle) — to prove the spacing + visibility hold for richer figure content.
// Template-agnostic (palette-semantic colours), drawn social icons, 2×2 options.
// ============================================================================
title("Quiz — Circumcentre");
canvas("9:16");
template("terminal");
// ---- the question (two typewriter lines) ----
text(q1, (cx, 210), "A triangle's CIRCUMCENTRE falls"); color(q1, fg); size(q1, 42); bold(q1); untraced(q1);
text(q2, (cx, 283), "outside it — when is it…?"); color(q2, cyan); size(q2, 42); bold(q2); untraced(q2);
// ---- figure ① : an ACUTE triangle (circumcentre INSIDE) ----
point(a1, (190, 800)); point(b1, (455, 800)); point(c1, (320, 560));
hidden(a1); hidden(b1); hidden(c1);
segment(s1a, a1, b1); segment(s1b, b1, c1); segment(s1c, c1, a1);
untraced(s1a); untraced(s1b); untraced(s1c);
circumcenter(o1, a1, b1, c1); color(o1, magenta); hidden(o1);
text(lab1, (322, 865), "acute · inside"); color(lab1, dim); size(lab1, 26);
// ---- figure ② : an OBTUSE triangle (circumcentre OUTSIDE) ----
// c2 sits LEFT of a2, so the angle at a2 is obtuse → circumcentre lands outside
point(a2, (735, 800)); point(b2, (965, 800)); point(c2, (700, 600));
hidden(a2); hidden(b2); hidden(c2);
segment(s2a, a2, b2); segment(s2b, b2, c2); segment(s2c, c2, a2);
untraced(s2a); untraced(s2b); untraced(s2c);
circumcenter(o2, a2, b2, c2); color(o2, magenta); hidden(o2);
text(lab2, (830, 865), "obtuse · outside"); color(lab2, dim); size(lab2, 26);
// ---- four option cards, 2×2 (correct = C, obtuse → c3) ----
rect(c1c, (305, 1010), 428, 120); color(c1c, panel); filled(c1c); hidden(c1c);
rect(c2c, (775, 1010), 428, 120); color(c2c, panel); filled(c2c); hidden(c2c);
rect(c3c, (305, 1150), 428, 120); color(c3c, panel); filled(c3c); hidden(c3c);
rect(c4c, (775, 1150), 428, 120); color(c4c, panel); filled(c4c); hidden(c4c);
text(t1, (305, 1010), "A Acute"); color(t1, fg); size(t1, 34); hidden(t1);
text(t2, (775, 1010), "B Right"); color(t2, fg); size(t2, 34); hidden(t2);
text(t3, (305, 1150), "C Obtuse"); color(t3, fg); size(t3, 34); hidden(t3);
text(t4, (775, 1150), "D Equilateral"); color(t4, fg); size(t4, 34); hidden(t4);
// ---- countdown ring + digit ----
circle(ring, (cx, 1400), 62); color(ring, cyan); stroke(ring, 6); outlined(ring); hidden(ring);
text(timer, (cx, 1400), "5"); color(timer, fg); size(timer, 62); bold(timer); hidden(timer);
// ---- socials footer (drawn vector icons — template-safe) ----
line(rule, (150, 1740), (930, 1740)); color(rule, dim); stroke(rule, 2);
rect(yt, (312, 1815), 58, 40); color(yt, magenta); filled(yt);
polygon(ytp, (300, 1802), (300, 1828), (326, 1815), bg);
rect(xb, (404, 1815), 46, 46); color(xb, fg); outlined(xb);
line(x1, (392, 1803), (416, 1827)); color(x1, fg); stroke(x1, 4);
line(x2, (416, 1803), (392, 1827)); color(x2, fg); stroke(x2, 4);
rect(ig, (496, 1815), 46, 46); color(ig, fg); outlined(ig);
circle(igc, (496, 1815), 13); color(igc, fg); outlined(igc);
circle(igd, (509, 1802), 3); color(igd, fg); filled(igd);
text(foot, (740, 1815), "@manic · 8gwifi.org/manic"); color(foot, dim); size(foot, 28);
tag(rule, footer); tag(yt, footer); tag(ytp, footer); tag(xb, footer); tag(x1, footer);
tag(x2, footer); tag(ig, footer); tag(igc, footer); tag(igd, footer); tag(foot, footer);
hidden(footer);
// ================= THE BEAT =================
// 1) ask
type(q1, 1.1);
type(q2, 1.0);
wait(0.2);
// 2) both figures construct (side by side)
par { show(a1, 0.2); show(b1, 0.2); show(c1, 0.2); show(a2, 0.2); show(b2, 0.2); show(c2, 0.2); }
par {
draw(s1a, 0.5); draw(s1b, 0.5); draw(s1c, 0.5);
draw(s2a, 0.5); draw(s2b, 0.5); draw(s2c, 0.5);
}
par { show(o1); show(o2); pulse(o1); pulse(o2); }
par { show(lab1, 0.3); show(lab2, 0.3); }
wait(0.3);
// 3) options fly in, 2×2 reading order
seq {
par { show(c1c, 0.2); show(t1, 0.2); }
par { show(c2c, 0.2); show(t2, 0.2); }
par { show(c3c, 0.2); show(t3, 0.2); }
par { show(c4c, 0.2); show(t4, 0.2); }
}
show(footer, 0.3);
// 4) countdown 5 → 1
par { show(ring, 0.2); show(timer, 0.2); }
wait(0.8); say(timer, "4", 0.15);
wait(0.8); say(timer, "3", 0.15);
wait(0.8); say(timer, "2", 0.15);
wait(0.8); say(timer, "1", 0.15);
wait(0.8);
// 5) TIME OUT → reveal: card C (Obtuse) glows, the rest fade
say(timer, "0", 0.15); flash(ring, gold);
par {
recolor(c3c, lime); flash(c3c, gold); pulse(c3c);
recolor(t3, void);
fade(c1c, 0.5); fade(t1, 0.5);
fade(c2c, 0.5); fade(t2, 0.5);
fade(c4c, 0.5); fade(t4, 0.5);
flash(o2, gold); // the obtuse triangle's outside centre
}
wait(1.6);
Boolean shapes
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
boolean
Union / intersection / difference of shapes.
// Boolean Ops — combine two shapes into a new region: union, intersection,
// difference, exclusion (xor). Each cell overlaps a square (cyan outline) and
// a circle (magenta outline); the filled lime shape is the result.
//
// manic examples/boolean.manic
// manic examples/boolean.manic --record out --fps 60
title("Boolean Ops");
canvas(1280, 720);
text(head, (640, 118), "boolean shape ops");
display(head); color(head, cyan); size(head, 36); hidden(head);
// --- union (top-left) ---
rect(aS, (330, 300), 130, 130); outlined(aS); outline(aS, cyan); opacity(aS, 0.4);
circle(aC, (400, 250), 78); outlined(aC); outline(aC, magenta); opacity(aC, 0.4);
union(aR, aS, aC, lime); hidden(aR);
text(aL, (365, 430), "union"); color(aL, dim); size(aL, 22);
// --- intersection (top-right) ---
rect(bS, (880, 300), 130, 130); outlined(bS); outline(bS, cyan); opacity(bS, 0.4);
circle(bC, (950, 250), 78); outlined(bC); outline(bC, magenta); opacity(bC, 0.4);
intersect(bR, bS, bC, lime); hidden(bR);
text(bL, (915, 430), "intersection"); color(bL, dim); size(bL, 20);
// --- difference (bottom-left): square minus circle ---
rect(cS, (330, 545), 130, 130); outlined(cS); outline(cS, cyan); opacity(cS, 0.4);
circle(cC, (400, 495), 78); outlined(cC); outline(cC, magenta); opacity(cC, 0.4);
difference(cR, cS, cC, lime); hidden(cR);
text(cL, (355, 675), "difference (rect - circle)"); color(cL, dim); size(cL, 18);
// --- exclusion / xor (bottom-right) ---
rect(dS, (880, 545), 130, 130); outlined(dS); outline(dS, cyan); opacity(dS, 0.4);
circle(dC, (950, 495), 78); outlined(dC); outline(dC, magenta); opacity(dC, 0.4);
xor(dR, dS, dC, lime); hidden(dR);
text(dL, (915, 675), "exclusion (xor)"); color(dL, dim); size(dL, 18);
// --- script: reveal each result in turn ---
show(head, 0.5);
stagger(0.3) {
show(aR, 0.4);
show(bR, 0.4);
show(cR, 0.4);
show(dR, 0.4);
}
wait(1.5);
3D scenes
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video). See the Going 3D chapter for the words used here.
three_d
Cubes, spheres, arrows, a curve, a surface and solids together — the 3D basics on one stage.
title("3D coordinate space");
canvas(1280, 720);
template("terminal");
camera3((8, -10, 6), (0, 0, 1), 45);
grid3(floor, (0, 0, 0), 5, 1);
color(floor, dim);
axes3(world, (0, 0, 0), 4);
cube3(box, (0, 0, 1), (2, 2, 2));
color(box, magenta);
sphere3(ball, (-2, 1, 1), 0.7);
color(ball, lime);
arrow3(vector, (0, 0, 0), (2.5, 2, 3));
// a parametric helix (curve3), drawn on as a shaded tube
curve3(spiral, "cos(t)*2.6", "sin(t)*2.6", "t*0.32", (0, 12));
color(spiral, gold);
thick(spiral, 0.08);
// a height-field surface z = f(x,y) (surface3), filled + flat-shaded
surface3(wave, "0.6*sin(x)*cos(y)", (-3.5, 3.5), (-3.5, 3.5), 24);
color(wave, magenta);
opacity(wave, 0.5);
// filled, flat-shaded solids (prism3 / pyramid3)
prism3(hex, (-3.6, -2.4, 1.2), 6, 0.9, 2.2);
pyramid3(cone, (3.6, -2.4, 1.4), 22, 1.0, 2.6);
// a 2D label glued to the moving sphere — tracks it as the camera orbits
text(balltag, (0, 0), "ball");
color(balltag, gold);
pin3(balltag, ball);
par {
rotate3(box, (0, 0, 360), 4, linear);
orbit3(70, 28, 11, 4, smooth);
move3(ball, (2, -1, 2), 4, inout);
}
solids3
Filled, shaded solids: a prism, a cone, and a lathed vase.
title("3D solids");
canvas(1280, 720);
template("terminal");
camera3((9, -11, 7), (0, 0, 1), 45);
grid3(floor, (0, 0, 0), 6, 1);
color(floor, dim);
axes3(world, (0, 0, 0), 3);
// hexagonal prism
prism3(hex, (-4, 0, 1.2), 6, 1.1, 2.4);
color(hex, cyan);
// a cone (a pyramid with many sides)
pyramid3(cone, (0, 0, 1.4), 28, 1.2, 2.8);
color(cone, magenta);
// a lathed vase: radius profile r(t) swept around the vertical axis
revolve3(vase, (4, 0, 1.5), "0.7 + 0.45*sin(t*2.4)", (0, 3), 32);
color(vase, gold);
orbit3(60, 24, 17, 6, smooth);
param3
Parametric surfaces a height field can’t make — a torus, a sphere, and a Möbius strip.
// param3 — general parametric surfaces x(u,v), y(u,v), z(u,v). Unlike surface3
// (a height field z=f(x,y)) these can wrap and close, so a torus and a Möbius
// strip are just three formulas each.
//
// manic examples/param3.manic
// manic examples/param3.manic --record out --fps 60
title("parametric surfaces");
canvas("16:9");
template("terminal");
camera3((11, -13, 8.5), (0, 0, 1.4), 42);
grid3(floor, (0, 0, 0), 7, 1);
color(floor, dim);
// torus (left)
param3(torus,
"0 - 4.8 + (2 + 0.7*cos(v))*cos(u)",
"(2 + 0.7*cos(v))*sin(u)",
"1.5 + 0.7*sin(v)",
(0, 6.283), (0, 6.283), 40);
color(torus, cyan);
// parametric sphere (centre)
param3(ball,
"1.3*sin(v)*cos(u)",
"1.3*sin(v)*sin(u)",
"1.6 + 1.3*cos(v)",
(0, 6.283), (0, 3.1416), 28);
color(ball, magenta);
// Möbius strip (right)
param3(mobius,
"4.8 + (1.7 + v*cos(u/2))*cos(u)",
"(1.7 + v*cos(u/2))*sin(u)",
"1.6 + v*sin(u/2)",
(0, 6.283), (0 - 0.6, 0.6), 60);
color(mobius, gold);
show(floor, 0.3);
par { show(torus, 0.7); show(ball, 0.7); show(mobius, 0.7); }
orbit3(60, 24, 15, 5.0, smooth);
wait(0.4);
extrude3
Lifting flat shapes into solids, including a boolean cut-out (a plate with a hole) and an L-beam.
// Extrude & CSG — turn 2D shapes into 3D solids. `extrude3` sweeps any 2D
// fillable shape (or a boolean region) straight up; extruding a union/
// difference/intersect gives constructive-solid-geometry solids. The 2D
// source shapes are auto-hidden — they're just the cross-section recipe.
//
// manic examples/extrude3.manic
// manic examples/extrude3.manic --record out --fps 60
title("Extrude & CSG");
canvas(1280, 720);
template("terminal");
camera3((9, -11, 7), (0, 0, 0.8), 45);
grid3(floor, (0, 0, 0), 6, 1);
color(floor, dim);
axes3(world, (0, 0, 0), 3);
// CSG: a square plate MINUS a circular hole → an extruded plate-with-a-hole
rect(plate, (0, 0), 3, 3);
circle(hole, (0, 0), 0.9);
difference(cut, plate, hole);
extrude3(block, cut, 1.0, (-3.5, 0, 0.6));
color(block, cyan);
// arbitrary concave polygon: a union of two rects → an L-beam, extruded
rect(la, (0, 0), 2.4, 0.8);
rect(lb, (-0.8, 0.8), 0.8, 2.4);
union(lshape, la, lb);
extrude3(ell, lshape, 0.9, (3.5, 0, 0.55));
color(ell, magenta);
// a plain shape extrudes just as happily (a hexagon via a sector sweep)
sector(hexface, (0, 0), 1.2, 0, 360);
extrude3(disc, hexface, 0.5, (0, 3.5, 0.3));
color(disc, gold);
orbit3(60, 24, 17, 8, smooth);
morph3
Morphing across families — a cube into a sphere, a saddle into a bowl, a helix into a ring.
// 3D morphing (morph3) — set a 3D entity up to become another shape, then
// blend with `to(id, morph, 1, dur)`. Works across three families:
// • solids — a cube becomes a sphere (reparameterised spherically)
// • surfaces — a rippling saddle settles into a bowl (filled + shaded)
// • curves — a helix unwinds into a flat ring
//
// manic examples/morph3.manic
// manic examples/morph3.manic --record out --fps 60
title("3D morph");
canvas("16:9");
template("terminal");
camera3((11, -13, 8), (0, 0, 0.8), 42);
grid3(floor, (0, 0, 0), 7, 1);
color(floor, dim);
// solid: cube -> sphere
cube3(box, (-5, 0, 1.4), (2.4, 2.4, 2.4));
color(box, cyan);
sphere3(ball, (0, 0, 0), 1.5);
hidden(ball);
morph3(box, ball);
// surface: rippling saddle -> smooth bowl
surface3(saddle, "0.8*sin(x*1.3)*cos(y*1.3)", (-2.3, 2.3), (-2.3, 2.3), 24);
color(saddle, gold);
surface3(bowl, "0.16*(x*x + y*y) - 1", (-2.3, 2.3), (-2.3, 2.3), 24);
hidden(bowl);
morph3(saddle, bowl);
// curve: helix -> flat ring
curve3(helix, "5 + 1.5*cos(t)", "1.5*sin(t)", "0.2*t", (0, 18));
color(helix, magenta);
thick(helix, 0.06);
curve3(ring, "5 + 1.5*cos(t)", "1.5*sin(t)", "1.8", (0, 6.283));
hidden(ring);
morph3(helix, ring);
show(floor, 0.3);
par { show(box, 0.5); show(saddle, 0.5); show(helix, 0.5); }
wait(0.7);
par {
to(box, morph, 1, 3.0, smooth);
to(saddle, morph, 1, 3.0, smooth);
to(helix, morph, 1, 3.0, smooth);
orbit3(48, 25, 21, 3.0, smooth);
}
wait(0.8);
linear-algebra-3d — the essence, in 3D
The 3D companion to the linear-algebra lesson: one matrix
[[1,0,0],[0,3,1],[0,1,3]] (det 8; eigenvalues 1, 2, 4) seen two ways on an
orbiting stage — first as a transformation (the unit cube → a parallelepiped
whose volume is the determinant), then through its eigenvectors (the invariant
axes that only stretch). Start here for 3D.
// ============================================================================
// linear-algebra-3d.manic — Linear algebra in 3D (a lesson)
// ----------------------------------------------------------------------------
// The 3-D companion to linear-algebra.manic. One matrix, two lenses, one
// orbiting stage:
// 1. a 3x3 matrix TRANSFORMS space — the unit cube becomes a parallelepiped
// whose VOLUME is the determinant (linmap3)
// 2. its EIGENVECTORS are the invariant axes that only stretch (eigen3)
//
// Both use the same A = [[1,0,0],[0,3,1],[0,1,3]] (det = 8; eigenvalues 1,2,4),
// so the two views describe the very same matrix.
// ============================================================================
title("Linear algebra in 3D");
canvas("16:9");
camera3((12, 9.5, 10), (0.4, 1.6, 1.6), 40);
grid3(floor, (0, 0, 0), 4, 1); color(floor, dim); opacity(floor, 0.3);
// A = [[1,0,0],[0,3,1],[0,1,3]] — det 8, eigenvalues 1, 2, 4
linmap3(lm, (0, 0, 0), 1, 0, 0, 0, 3, 1, 0, 1, 3);
eigen3(ev, (0, 0, 0), 1, 0, 0, 0, 3, 1, 0, 1, 3);
// the eigen-axes wait until chapter 2
hidden(ev.axis0); hidden(ev.axis1); hidden(ev.axis2);
hidden(ev.l0); hidden(ev.l1); hidden(ev.l2);
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
// ---- 1. the matrix as a transformation of space ----
section("A matrix in 3D");
say(cap, "a 3x3 matrix sends the unit cube to a parallelepiped — its columns", 0.5);
orbit3(50, 26, 13, 4, smooth);
say(cap, "the volume it encloses is the determinant: det = 8, so space grew 8x", 0.5);
orbit3(116, 30, 13, 4, smooth);
// clear the transformation view
fade(lm, 0.6); fade(lm.ref, 0.6);
fade(lm.i, 0.6); fade(lm.j, 0.6); fade(lm.k, 0.6);
fade(lm.li, 0.6); fade(lm.lj, 0.6); fade(lm.lk, 0.6); fade(lm.val, 0.6);
wait(0.3);
// ---- 2. the same matrix, through its eigenvectors ----
say(cap, "", 0.2);
section("Eigenvectors");
say(cap, "three special directions only STRETCH when A acts — the eigenvectors", 0.5);
show(ev.axis0, 0.5); show(ev.axis1, 0.5); show(ev.axis2, 0.5);
show(ev.l0, 0.4); show(ev.l1, 0.4); show(ev.l2, 0.4);
orbit3(58, 24, 7, 4, smooth);
say(cap, "their stretch factors are the eigenvalues: 1, 2 and 4", 0.5);
orbit3(122, 32, 6.5, 4.5, smooth);
wait(0.5);
linear-map3
Linear algebra in 3D: a 3×3 matrix deforms the unit cube into a parallelepiped,
with basis arrows i/j/k landing on the matrix’s columns and the enclosed volume
labelled as the determinant (linmap3). The 3D echo of linear-map.
// ============================================================================
// linear-map3.manic — a 3x3 matrix, in 3D (the 3-D echo of linear-map)
// ----------------------------------------------------------------------------
// `linmap3(id, (cx,cy,cz), a,b,c,d,e,f,g,h,i, [color])` applies the 3x3 matrix
// [[a,b,c],[d,e,f],[g,h,i]] to space: the unit cube (faint wireframe) becomes a
// parallelepiped, with basis arrows i/j/k landing on the matrix's COLUMNS. The
// volume it encloses IS the determinant (labelled) — it flips colour when the
// determinant is negative (orientation reversed) and collapses flat at det = 0.
//
// TO ADAPT: change the nine numbers. Try a shear (1,0.6,0, 0,1,0, 0,0,1) → det 1
// (volume unchanged), or swap two columns to make det negative (a reflection).
// ============================================================================
title("A 3x3 matrix, in 3D");
canvas("16:9");
camera3((7, 5.5, 6), (1, 1, 1), 40);
grid3(floor, (0, 0, 0), 4, 1); color(floor, dim); opacity(floor, 0.4);
// M = [[1.5,0.5,0],[0,1.5,0.5],[0.5,0,1.5]] — a skew + stretch, det = 3.5
linmap3(lm, (0, 0, 0), 1.5, 0.5, 0, 0, 1.5, 0.5, 0.5, 0, 1.5);
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
say(cap, "a 3x3 matrix sends the unit cube to a parallelepiped", 0.5);
orbit3(58, 26, 10, 4, smooth);
say(cap, "i, j, k land on its columns — where the three axes are sent", 0.5);
orbit3(116, 32, 10, 4, smooth);
say(cap, "the volume enclosed IS the determinant: det = 3.5, so space grew 3.5x", 0.5);
orbit3(60, 20, 9, 4.5, smooth);
wait(0.5);
eigen3
The real eigenvectors of a 3×3 matrix, in 3D: the invariant lines through the
origin that only stretch (by λ) when the matrix acts (eigen3). The 3D echo of
eigen. A symmetric matrix gives three perpendicular real eigen-axes; a rotation
leaves one real axis and two complex eigenvalues.
// ============================================================================
// eigen3.manic — eigenvectors of a 3x3 matrix (the 3-D echo of eigen)
// ----------------------------------------------------------------------------
// `eigen3(id, (cx,cy,cz), a,b,c,d,e,f,g,h,i, [color])` draws the real
// EIGENVECTORS of the 3x3 [[a,b,c],[d,e,f],[g,h,i]] as invariant lines through
// the origin — the directions a vector on them only STRETCHES (by λ), never
// turns. A real 3x3 always has at least one real eigenvector; a rotation leaves
// two complex eigenvalues, which are noted.
//
// TO ADAPT: change the nine numbers. A symmetric matrix (as here) has three
// perpendicular real eigen-axes; a rotation like (0,-1,0, 1,0,0, 0,0,1) shows
// one real axis (the axis of rotation) + a "2 complex" note.
// ============================================================================
title("Eigenvectors of a 3x3 matrix");
canvas("16:9");
camera3((7, 5.5, 6), (0, 0, 0), 42);
grid3(floor, (0, 0, 0), 3, 1); color(floor, dim); opacity(floor, 0.35);
// symmetric M = [[1,0,0],[0,3,1],[0,1,3]] — three real eigenvalues: 1, 2, 4
eigen3(ev, (0, 0, 0), 1, 0, 0, 0, 3, 1, 0, 1, 3);
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
say(cap, "when the matrix acts, most directions turn — but a few only stretch", 0.5);
orbit3(52, 24, 10, 4, smooth);
say(cap, "those invariant lines are the eigenvectors; the stretch factor is lambda", 0.5);
orbit3(122, 34, 10, 4, smooth);
say(cap, "this symmetric 3x3 has three real eigenvalues: 1, 2 and 4", 0.5);
orbit3(58, 22, 9, 4.5, smooth);
wait(0.5);
matrix3
A 3×3×3 block of cubes, with a shear matrix M and its inverse M⁻¹ applied and undone.
// A 3 × 3 × 3 block of 27 cubes, then a matrix operation: a 3×3 matrix is a
// linear map of space. We apply a shear M to every cell, then its inverse
// M^-1 — which sends the block back exactly, because M^-1 M = I.
//
// manic examples/matrix3.manic
// manic examples/matrix3.manic --record out --fps 60
title("3 × 3 × 3 · M and M^-1");
canvas("16:9");
template("terminal");
camera3((7.5, -9, 6), (2.4, 0, 2.1), 42);
grid3(floor, (0, 0, 0), 6, 1);
color(floor, dim);
let s = 1.3; // cell spacing (cubes are 1 wide → 0.3 gaps)
let zc = 2.1; // height of the block's centre
let a = 0.7; // shear amount: M sends x → x + a·z
text(cap, (cx, h - 42), "");
display(cap); color(cap, fg); size(cap, 21); hidden(cap);
// --- 27 cubes, three x-slices coloured cyan / magenta / lime ---
for j in 0..3 { for k in 0..3 {
cube3(xa{j}{k}, (0 - s, (j - 1)*s, zc + (k - 1)*s), (1, 1, 1));
color(xa{j}{k}, cyan); hidden(xa{j}{k});
} }
for j in 0..3 { for k in 0..3 {
cube3(xb{j}{k}, (0, (j - 1)*s, zc + (k - 1)*s), (1, 1, 1));
color(xb{j}{k}, magenta); hidden(xb{j}{k});
} }
for j in 0..3 { for k in 0..3 {
cube3(xc{j}{k}, (s, (j - 1)*s, zc + (k - 1)*s), (1, 1, 1));
color(xc{j}{k}, lime); hidden(xc{j}{k});
} }
// --- the matrices, as monospace text on the right ---
text(mLabel, (995, 232), "M =");
text(m0, (1035, 274), "[ 1 0 0.7 ]");
text(m1, (1035, 310), "[ 0 1 0 ]");
text(m2, (1035, 346), "[ 0 0 1 ]");
text(iLabel, (995, 446), "M^-1 =");
text(i0, (1035, 488), "[ 1 0 -0.7 ]");
text(i1, (1035, 524), "[ 0 1 0 ]");
text(i2, (1035, 560), "[ 0 0 1 ]");
display(mLabel); display(m0); display(m1); display(m2);
display(iLabel); display(i0); display(i1); display(i2);
color(mLabel, cyan); color(m0, cyan); color(m1, cyan); color(m2, cyan);
color(iLabel, gold); color(i0, gold); color(i1, gold); color(i2, gold);
size(mLabel, 24); size(m0, 22); size(m1, 22); size(m2, 22);
size(iLabel, 24); size(i0, 22); size(i1, 22); size(i2, 22);
hidden(mLabel); hidden(m0); hidden(m1); hidden(m2);
hidden(iLabel); hidden(i0); hidden(i1); hidden(i2);
// ------------------------------ script ------------------------------
show(floor, 0.3);
show(cap, 0.3);
say(cap, "A 3 by 3 by 3 block — 27 cells.", 0.7);
stagger(0.04) {
for j in 0..3 { for k in 0..3 { show(xa{j}{k}, 0.3); } }
for j in 0..3 { for k in 0..3 { show(xb{j}{k}, 0.3); } }
for j in 0..3 { for k in 0..3 { show(xc{j}{k}, 0.3); } }
}
orbit3(38, 22, 13, 1.6, smooth);
say(cap, "A 3×3 matrix M is a linear map of space. This M shears x by height z.", 1.0);
stagger(0.12) { show(mLabel, 0.3); show(m0, 0.3); show(m1, 0.3); show(m2, 0.3); }
// apply M: x' = x + a·(z - zc)
par {
for j in 0..3 { for k in 0..3 {
move3(xa{j}{k}, (0 - s + a*(k - 1)*s, (j - 1)*s, zc + (k - 1)*s), 1.8, smooth);
} }
for j in 0..3 { for k in 0..3 {
move3(xb{j}{k}, (a*(k - 1)*s, (j - 1)*s, zc + (k - 1)*s), 1.8, smooth);
} }
for j in 0..3 { for k in 0..3 {
move3(xc{j}{k}, (s + a*(k - 1)*s, (j - 1)*s, zc + (k - 1)*s), 1.8, smooth);
} }
}
say(cap, "Applying M slants the whole block — the higher a cell, the more it shifts.", 1.0);
stagger(0.12) { show(iLabel, 0.3); show(i0, 0.3); show(i1, 0.3); show(i2, 0.3); }
say(cap, "The inverse M^-1 just negates the shear.", 0.8);
// apply M^-1: sends every cell back to where it started
par {
for j in 0..3 { for k in 0..3 {
move3(xa{j}{k}, (0 - s, (j - 1)*s, zc + (k - 1)*s), 1.8, smooth);
} }
for j in 0..3 { for k in 0..3 {
move3(xb{j}{k}, (0, (j - 1)*s, zc + (k - 1)*s), 1.8, smooth);
} }
for j in 0..3 { for k in 0..3 {
move3(xc{j}{k}, (s, (j - 1)*s, zc + (k - 1)*s), 1.8, smooth);
} }
}
say(cap, "M^-1 sends every cell home: M^-1 M = I, the identity.", 1.0);
orbit3(70, 26, 13, 2.4, smooth);
wait(0.6);
double-integral3
Multivariable calculus: the volume under a surface as a limit of finer and finer columns — a double integral, made solid. The coarse blocks refine until they hug the surface.
title("The double integral — volume as a limit of finer columns");
canvas("16:9");
camera3((9, -11, 9), (0, 0, 0.7), 40);
axes3(world, (0, 0, 0), 3);
color(world, dim);
text(cap, (cx, h - 54), "coarse columns");
size(cap, 22); color(cap, cyan); bold(cap); display(cap); hidden(cap);
// the surface, kept faint so the columns beneath it read
surface3(hill, "1.6*exp(-(x*x + y*y)*0.28)", (-3, 3), (-3, 3), 30);
color(hill, cyan); opacity(hill, 0.22); untraced(hill);
// the volume under it, at three resolutions — each finer sum hugs the surface
volume3(v1, hill, 3);
color(v1, lime); hidden(v1);
volume3(v2, hill, 7);
color(v2, lime); hidden(v2);
volume3(v3, hill, 16);
color(v3, lime); hidden(v3);
// ---- timeline ----
show(world, 0.5);
draw(hill, 1.4);
wait(0.3);
// 3x3: a crude estimate
show(cap, 0.4);
show(v1, 0.7);
wait(1.2);
// 7x7: closer
fade(v1, 0.5);
say(cap, "finer: the sum gets closer");
show(v2, 0.7);
wait(1.2);
// 16x16: the columns fill the true volume under the surface
fade(v2, 0.5);
say(cap, "finer still -> the true volume (a double integral)");
show(v3, 0.8);
orbit3(70, 34, 16, 4.5);
multivariable3
Calculus on a surface: a smooth hill, its tangent plane and the gradient arrow at a chosen point, inspected with an orbiting camera.
title("Calculus on a surface — gradient, tangent plane, volume");
canvas("16:9");
camera3((8, -10, 7), (0, 0, 0.8), 42);
axes3(world, (0, 0, 0), 3);
color(world, dim);
// a smooth hill: z = 1.6 e^(-(x^2 + y^2)/3.6)
surface3(hill, "1.6*exp(-(x*x + y*y)*0.28)", (-3, 3), (-3, 3), 28);
color(hill, cyan); untraced(hill);
// the tangent plane at a point on the slope
tangentplane3(tp, hill, 1.1, -0.7);
color(tp, magenta); hidden(tp);
// the gradient there — an arrow pointing straight up the steepest slope
gradient3(grad, hill, 1.1, -0.7);
color(grad, gold); hidden(grad);
// ---- timeline ----
show(world, 0.6);
draw(hill, 1.8);
wait(0.4);
show(tp, 0.6);
show(grad, 0.6);
orbit3(70, 30, 15, 4.5);
volume3
The volume under a surface represented as solid 3D Riemann-sum columns, turning a double integral into visible geometry.
title("Volume under a surface — a double integral, made solid");
canvas("16:9");
camera3((9, -11, 8), (0, 0, 0.6), 40);
axes3(world, (0, 0, 0), 3);
color(world, dim);
// a smooth bump
surface3(hill, "1.6*exp(-(x*x + y*y)*0.28)", (-3, 3), (-3, 3), 28);
color(hill, cyan); untraced(hill);
// the volume beneath it, as a grid of columns (a 3D Riemann sum)
volume3(vol, hill, 11);
color(vol, lime); hidden(vol);
// ---- timeline ----
show(world, 0.6);
draw(hill, 1.6);
wait(0.4);
show(vol, 1.0);
orbit3(70, 32, 16, 4.5);
Troubleshooting
manic checks your scene as you type — it underlines the spot and shows the message, and where possible offers a one-click Fix. Here are the mistakes people actually hit, the message you’ll see, and the fix.
Errors the editor catches
1. unknown variable — a missing *
The #1 mistake. Two names written together are read as a single word.
# ❌ unknown variable `idx`
dot(p, (cx + idx, cy), 6);
# ✅ put a * between the two names
dot(p, (cx + i*dx, cy), 6);
A number can hug a name (2r, 3(x+1)), but two names can’t. Add *
at every name-next-to-name: i*dx, tau*i, xmid*sx, and especially
r*cos(t) / r*sin(t) — rcos/rsin are the classic trap (they mean
“radius × cos”, not a function).
2. unknown function in a plot — quote the formula
Only a short list of names work as bare words (sin, cos, tan,
sqrt, abs, exp, log, parabola, cubic, gauss, sinc, …). Anything
else — acos, tanh, log10, sums of terms — must be a “quoted formula”.
# ❌ unknown function `acos`
plot(f, (cx, cy), 80, 80, acos, (-1, 1));
# ✅ wrap it in quotes as a formula in x
plot(f, (cx, cy), 80, 80, "acos(x)", (-1, 1));
3. needs at least N argument(s) — you dropped the id (or an argument)
Every builtin’s first argument is its id — a name you pick. Modifiers and verbs need that id too.
# ❌ `size` needs at least 2 argument(s), got 1
size(30);
# ✅ say which entity
size(title, 30);
# ❌ `circle` needs at least 3 argument(s), got 2 (no radius)
circle(c, (cx, cy));
# ✅
circle(c, (cx, cy), 120);
4. no entity named X — a typo, or used too early
You referred to an id that doesn’t exist — misspelled, or used before it’s made.
text(title, (cx, 60), "Hello");
# ❌ no entity named `titel`
color(titel, cyan);
# ✅ match the id exactly
color(title, cyan);
5. unknown colour — use the palette (or hue)
Only the named palette colours work — no red, no #ff0000.
# ❌ unknown colour `red`
color(dot, red);
# ✅ a palette colour…
color(dot, magenta);
# ✅ …or a computed one, 0–360
hue(dot, 210);
Palette: cyan magenta lime gold fg dim void panel.
6. stroke is 2D-only — a 2D styler on a 3D shape
Some styling is 2D-only. On 3D shapes use the 3D equivalent.
cube3(bx, (0, 0, 1), (2, 2, 2));
# ❌ `stroke` is 2D-only; for a 3D line/arrow/curve use `thick(bx, radius)`
stroke(bx, 3);
# ✅ thickness in world units
thick(bx, 0.1);
The message names the fix. Also: hue → use color on 3D entities.
7. argument 1 should be a name — a reserved word as an id
pi, tau, e, inf, w, h, cx, cy are built-in values — you can’t
name an entity one of them.
# ❌ argument 1 of `dot` should be a name (e is Euler's number)
dot(e, (cx, cy), 6);
# ✅ pick any other name
dot(pt, (cx, cy), 6);
8. expected a statement … found ; — a stray semicolon
Blocks (par, seq, stagger, for, if) end with } — no semicolon.
# ❌ expected a statement …, found `;`
par { show(a, 1); show(b, 1); };
# ✅
par { show(a, 1); show(b, 1); }
No error — but it looks wrong
These pass the check, so watch for them yourself.
9. A curve appears all at once instead of drawing on
draw animates a shape that starts hidden. Declare it untraced first.
plot(f, (cx, cy), 80, 80, "sin(x)", (0, 6));
# ❌ f is already fully shown — draw does nothing visible
draw(f, 2);
# ✅ hide the line, then draw traces it on
untraced(f);
draw(f, 2);
10. Things land off-screen
The canvas is a fixed logical size — 1280×720 for 16:9 — not your
video’s pixel size. Position with cx, cy, w, h, never hard-coded pixels.
# ❌ 1700 is past the right edge (width is 1280)
text(t, (1700, 300), "hi");
# ✅ relative to the centre
text(t, (cx + 200, 300), "hi");
11. A matrix/table cell with a comma
Cells are single tokens split by spaces or commas, so a cell can’t contain
a comma — (0,0) silently becomes two cells and the grid comes out malformed.
# ❌ no error, but the row breaks apart
matrix(m, "(0,0) (1,1)", (cx, cy));
# ✅ one token per cell
matrix(m, "0 1; 2 3", (cx, cy));
Rules of thumb: put a * between names · quote any formula that isn’t a
bare-word function · give every entity an id · use cx/cy/w/h for
position · untraced + draw to trace a curve on.